@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,15 @@
1
+ import { randomInt } from 'node:crypto';
2
+
1
3
  import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
4
+ import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
5
+ import { t } from '../shared/i18n.mjs';
2
6
  import { COMMANDS_MENU_BUTTON, TelegramApi } from './telegram-api.mjs';
3
7
  import { createTelegramBridgeStatus, TelegramHarnessBridge } from './telegram-bridge.mjs';
8
+ import {
9
+ splitTelegramRegularText,
10
+ splitTelegramRichMarkdown,
11
+ toTelegramRichMarkdown,
12
+ } from './telegram-rich-message.mjs';
4
13
  import {
5
14
  TELEGRAM_ACCESS_MODES,
6
15
  normalizeTelegramAccessPolicy,
@@ -23,6 +32,13 @@ export const TELEGRAM_COMMAND_MENU = Object.freeze([
23
32
  { command: 'help', description: '显示帮助' },
24
33
  ]);
25
34
 
35
+ export function telegramCommandMenu() {
36
+ return TELEGRAM_COMMAND_MENU.map((item) => ({
37
+ ...item,
38
+ description: t(item.description),
39
+ }));
40
+ }
41
+
26
42
  function escaped(value) {
27
43
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
44
  }
@@ -149,6 +165,7 @@ export function normalizeTelegramUpdate(update, {
149
165
  addressed,
150
166
  replyTarget: {
151
167
  chatId,
168
+ chatType: message.chat.type,
152
169
  replyToMessageId: messageId,
153
170
  messageThreadId,
154
171
  },
@@ -166,17 +183,105 @@ export function telegramInboundAllowed(message, {
166
183
  && allowedPrivateUserIds.has(String(message.senderId));
167
184
  }
168
185
 
186
+ function telegramMessageId(value) {
187
+ return Number.isSafeInteger(value?.message_id) ? String(value.message_id) : null;
188
+ }
189
+
190
+ function telegramFailure(error) {
191
+ const providerCode = Number(error?.providerCode);
192
+ const status = Number(error?.status);
193
+ const unknown = error?.deliveryOutcome === 'unknown'
194
+ || providerCode >= 500
195
+ || status >= 500
196
+ || ['telegram-timeout', 'telegram-aborted-after-dispatch', 'telegram-transport-error',
197
+ 'telegram-response-invalid'].includes(error?.code);
198
+ return {
199
+ outcome: unknown ? 'unknown' : 'failed',
200
+ reason: typeof error?.code === 'string' && error.code
201
+ ? error.code
202
+ : unknown ? 'telegram-delivery-uncertain' : 'telegram-provider-rejected',
203
+ };
204
+ }
205
+
206
+ function deliveryResult(presentation, providerMessageIds, deliveryOutcome = 'sent', reason) {
207
+ return {
208
+ presentation,
209
+ providerMessageIds: [...new Set(providerMessageIds)],
210
+ deliveryOutcome,
211
+ ...(reason ? { reason } : {}),
212
+ };
213
+ }
214
+
215
+ class TelegramDeliveryStream {
216
+ #update;
217
+ #finish;
218
+ #fail;
219
+ #logger;
220
+ #providerMessageIds;
221
+ #closed = false;
222
+ #lastUpdate = null;
223
+
224
+ constructor({ update, finish, fail, providerMessageIds = [], presentation, logger }) {
225
+ this.#update = update;
226
+ this.#finish = finish;
227
+ this.#fail = fail;
228
+ this.#providerMessageIds = providerMessageIds;
229
+ this.presentation = presentation;
230
+ this.#logger = logger;
231
+ }
232
+
233
+ get providerMessageIds() {
234
+ return [...this.#providerMessageIds];
235
+ }
236
+
237
+ async update(value) {
238
+ if (this.#closed) return undefined;
239
+ const block = createTextDeliveryBlock(value);
240
+ const key = `${block.format}:${block.text}`;
241
+ if (key === this.#lastUpdate) return undefined;
242
+ this.#lastUpdate = key;
243
+ try {
244
+ return await this.#update(block);
245
+ } catch (error) {
246
+ this.#logger.warn?.('[dsh-im:telegram] rich stream update failed:', error);
247
+ return undefined;
248
+ }
249
+ }
250
+
251
+ async finish(value) {
252
+ if (this.#closed) throw new Error('Message stream is already closed');
253
+ this.#closed = true;
254
+ const result = await this.#finish(createTextDeliveryBlock(value));
255
+ this.#providerMessageIds.push(...(result?.providerMessageIds ?? []));
256
+ return result;
257
+ }
258
+
259
+ async fail(text) {
260
+ if (this.#closed) return undefined;
261
+ this.#closed = true;
262
+ const result = await this.#fail(createTextDeliveryBlock(text, 'plain'));
263
+ this.#providerMessageIds.push(...(result?.providerMessageIds ?? []));
264
+ return result;
265
+ }
266
+
267
+ cancel() {
268
+ this.#closed = true;
269
+ }
270
+ }
271
+
169
272
  export class TelegramBotClient {
170
273
  #api;
171
274
  #signal;
275
+ #logger;
172
276
 
173
- constructor({ api, signal }) {
277
+ constructor({ api, signal, logger = console }) {
174
278
  this.#api = api;
175
279
  this.#signal = signal;
280
+ this.#logger = logger;
176
281
  }
177
282
 
178
283
  async sendText(target, text) {
179
- const chunks = splitMessageText(text, 4_000);
284
+ const chunks = splitTelegramRegularText(text);
180
285
  const providerMessageIds = [];
181
286
  for (const [index, chunk] of chunks.entries()) {
182
287
  const result = await this.#api.sendMessage({
@@ -211,6 +316,275 @@ export class TelegramBotClient {
211
316
  });
212
317
  }
213
318
 
319
+ sendImage(target, file) {
320
+ return this.#api.sendPhoto({
321
+ chatId: target.chatId,
322
+ file,
323
+ replyToMessageId: target.replyToMessageId,
324
+ messageThreadId: target.messageThreadId,
325
+ signal: this.#signal,
326
+ });
327
+ }
328
+
329
+ async #sendPlain(target, text, { placeholderMessageId } = {}) {
330
+ const chunks = splitTelegramRegularText(text);
331
+ const providerMessageIds = placeholderMessageId === undefined
332
+ ? []
333
+ : [String(placeholderMessageId)];
334
+ let firstUnsent = 0;
335
+ if (placeholderMessageId !== undefined) {
336
+ try {
337
+ await this.#api.editMessageText({
338
+ chatId: target.chatId,
339
+ messageId: placeholderMessageId,
340
+ text: chunks[0],
341
+ signal: this.#signal,
342
+ });
343
+ firstUnsent = 1;
344
+ } catch (error) {
345
+ const failure = telegramFailure(error);
346
+ if (failure.outcome === 'unknown') {
347
+ return deliveryResult('text-fallback', providerMessageIds, 'unknown', failure.reason);
348
+ }
349
+
350
+ const fallback = await this.#sendPlain(target, text);
351
+ const terminalText = fallback.deliveryOutcome === 'sent'
352
+ ? '回复已发送。'
353
+ : fallback.deliveryOutcome === 'unknown'
354
+ ? '回复发送结果未能确认。'
355
+ : '消息发送失败,请稍后重试。';
356
+ try {
357
+ await this.#api.editMessageText({
358
+ chatId: target.chatId,
359
+ messageId: placeholderMessageId,
360
+ text: terminalText,
361
+ signal: this.#signal,
362
+ });
363
+ } catch (terminalError) {
364
+ this.#logger.warn?.(
365
+ '[dsh-im:telegram] unable to replace the processing placeholder:',
366
+ terminalError,
367
+ );
368
+ }
369
+ return deliveryResult(
370
+ 'text-fallback',
371
+ [...providerMessageIds, ...fallback.providerMessageIds],
372
+ fallback.deliveryOutcome,
373
+ fallback.reason,
374
+ );
375
+ }
376
+ }
377
+
378
+ for (let index = firstUnsent; index < chunks.length; index += 1) {
379
+ try {
380
+ const result = await this.#api.sendMessage({
381
+ chatId: target.chatId,
382
+ text: chunks[index],
383
+ replyToMessageId: index === 0 ? target.replyToMessageId : undefined,
384
+ messageThreadId: target.messageThreadId,
385
+ signal: this.#signal,
386
+ });
387
+ const id = telegramMessageId(result);
388
+ if (id) providerMessageIds.push(id);
389
+ } catch (error) {
390
+ const failure = telegramFailure(error);
391
+ return deliveryResult(
392
+ 'text-fallback',
393
+ providerMessageIds,
394
+ failure.outcome,
395
+ failure.reason,
396
+ );
397
+ }
398
+ }
399
+ return deliveryResult('text-fallback', providerMessageIds);
400
+ }
401
+
402
+ async #sendRich(target, block) {
403
+ if (block.format === 'plain') return this.#sendPlain(target, block.text);
404
+
405
+ let chunks;
406
+ try {
407
+ chunks = splitTelegramRichMarkdown(block.text);
408
+ } catch {
409
+ return this.#sendPlain(target, block.text);
410
+ }
411
+ const providerMessageIds = [];
412
+ for (const [index, chunk] of chunks.entries()) {
413
+ try {
414
+ const result = await this.#api.sendRichMessage({
415
+ chatId: target.chatId,
416
+ richMessage: { markdown: chunk.markdown },
417
+ replyToMessageId: index === 0 ? target.replyToMessageId : undefined,
418
+ messageThreadId: target.messageThreadId,
419
+ signal: this.#signal,
420
+ });
421
+ const id = telegramMessageId(result);
422
+ if (id) providerMessageIds.push(id);
423
+ } catch (error) {
424
+ const failure = telegramFailure(error);
425
+ if (failure.outcome === 'unknown') {
426
+ return deliveryResult(
427
+ 'telegram-rich-final',
428
+ providerMessageIds,
429
+ 'unknown',
430
+ failure.reason,
431
+ );
432
+ }
433
+ const remaining = chunks.slice(index).map((part) => part.source).join('');
434
+ const fallback = await this.#sendPlain({
435
+ ...target,
436
+ replyToMessageId: index === 0 ? target.replyToMessageId : undefined,
437
+ }, remaining);
438
+ return deliveryResult(
439
+ fallback.presentation,
440
+ [...providerMessageIds, ...fallback.providerMessageIds],
441
+ fallback.deliveryOutcome,
442
+ fallback.reason,
443
+ );
444
+ }
445
+ }
446
+ return deliveryResult('telegram-rich-final', providerMessageIds);
447
+ }
448
+
449
+ async #editRich(target, messageId, block) {
450
+ if (block.format === 'plain') {
451
+ return this.#sendPlain(target, block.text, {
452
+ placeholderMessageId: messageId,
453
+ });
454
+ }
455
+
456
+ let chunks;
457
+ try {
458
+ chunks = splitTelegramRichMarkdown(block.text);
459
+ } catch {
460
+ return this.#sendPlain(target, block.text, { placeholderMessageId: messageId });
461
+ }
462
+ const providerMessageIds = [String(messageId)];
463
+ try {
464
+ await this.#api.editMessageText({
465
+ chatId: target.chatId,
466
+ messageId,
467
+ richMessage: { markdown: chunks[0].markdown },
468
+ signal: this.#signal,
469
+ });
470
+ } catch (error) {
471
+ const failure = telegramFailure(error);
472
+ if (failure.outcome === 'unknown') {
473
+ return deliveryResult(
474
+ 'telegram-rich-final',
475
+ providerMessageIds,
476
+ 'unknown',
477
+ failure.reason,
478
+ );
479
+ }
480
+ return this.#sendPlain(target, block.text, { placeholderMessageId: messageId });
481
+ }
482
+
483
+ for (const [offset, chunk] of chunks.slice(1).entries()) {
484
+ try {
485
+ const result = await this.#api.sendRichMessage({
486
+ chatId: target.chatId,
487
+ richMessage: { markdown: chunk.markdown },
488
+ messageThreadId: target.messageThreadId,
489
+ signal: this.#signal,
490
+ });
491
+ const id = telegramMessageId(result);
492
+ if (id) providerMessageIds.push(id);
493
+ } catch (error) {
494
+ const failure = telegramFailure(error);
495
+ if (failure.outcome === 'unknown') {
496
+ return deliveryResult(
497
+ 'telegram-rich-final',
498
+ providerMessageIds,
499
+ 'unknown',
500
+ failure.reason,
501
+ );
502
+ }
503
+ const remaining = chunks.slice(offset + 1).map((part) => part.source).join('');
504
+ const fallback = await this.#sendPlain({
505
+ ...target,
506
+ replyToMessageId: undefined,
507
+ }, remaining);
508
+ return deliveryResult(
509
+ fallback.presentation,
510
+ [...providerMessageIds, ...fallback.providerMessageIds],
511
+ fallback.deliveryOutcome,
512
+ fallback.reason,
513
+ );
514
+ }
515
+ }
516
+ return deliveryResult('telegram-rich-final', providerMessageIds);
517
+ }
518
+
519
+ sendDelivery(target, value) {
520
+ return this.#sendRich(target, createTextDeliveryBlock(value));
521
+ }
522
+
523
+ async openDeliveryStream(target) {
524
+ if (target.chatType === 'private') {
525
+ const draftId = randomInt(1, 2_147_483_647);
526
+ const updateDraft = async (block) => {
527
+ const richMessage = { markdown: toTelegramRichMarkdown(block.text) };
528
+ await this.#api.sendRichMessageDraft({
529
+ chatId: target.chatId,
530
+ draftId,
531
+ richMessage,
532
+ messageThreadId: target.messageThreadId,
533
+ signal: this.#signal,
534
+ });
535
+ return deliveryResult('telegram-rich-draft', []);
536
+ };
537
+ const stream = new TelegramDeliveryStream({
538
+ update: updateDraft,
539
+ finish: (block) => this.#sendRich(target, block),
540
+ fail: (block) => this.#sendPlain(target, block.text),
541
+ presentation: 'telegram-rich-draft',
542
+ logger: this.#logger,
543
+ });
544
+ await stream.update(createTextDeliveryBlock('正在处理…', 'plain'));
545
+ return stream;
546
+ }
547
+
548
+ const placeholder = await this.#api.sendMessage({
549
+ chatId: target.chatId,
550
+ text: '正在处理…',
551
+ replyToMessageId: target.replyToMessageId,
552
+ messageThreadId: target.messageThreadId,
553
+ signal: this.#signal,
554
+ });
555
+ const messageId = placeholder?.message_id;
556
+ if (!Number.isSafeInteger(messageId)) {
557
+ throw new Error('Telegram did not return a placeholder message id');
558
+ }
559
+ return new TelegramDeliveryStream({
560
+ update: async (block) => {
561
+ if (block.format === 'plain') {
562
+ await this.#api.editMessageText({
563
+ chatId: target.chatId,
564
+ messageId,
565
+ text: block.text,
566
+ signal: this.#signal,
567
+ });
568
+ return deliveryResult('text-fallback', [String(messageId)]);
569
+ }
570
+ await this.#api.editMessageText({
571
+ chatId: target.chatId,
572
+ messageId,
573
+ richMessage: { markdown: toTelegramRichMarkdown(block.text) },
574
+ signal: this.#signal,
575
+ });
576
+ return deliveryResult('telegram-rich-draft', [String(messageId)]);
577
+ },
578
+ finish: (block) => this.#editRich(target, messageId, block),
579
+ fail: (block) => this.#sendPlain(target, block.text, {
580
+ placeholderMessageId: messageId,
581
+ }),
582
+ providerMessageIds: [String(messageId)],
583
+ presentation: 'telegram-regular',
584
+ logger: this.#logger,
585
+ });
586
+ }
587
+
214
588
  async openStream(target) {
215
589
  const stream = createEditableMessageStream({
216
590
  limit: 4_000,
@@ -337,12 +711,12 @@ export class TelegramRuntime {
337
711
  }
338
712
  const webhook = await api.getWebhookInfo({ signal: controller.signal });
339
713
  if (typeof webhook?.url === 'string' && webhook.url) {
340
- const error = new Error('该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。');
714
+ const error = new Error(t('该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。'));
341
715
  error.code = 'webhook-configured';
342
716
  throw error;
343
717
  }
344
718
  try {
345
- await api.setMyCommands({ commands: TELEGRAM_COMMAND_MENU, signal: controller.signal });
719
+ await api.setMyCommands({ commands: telegramCommandMenu(), signal: controller.signal });
346
720
  await api.setChatMenuButton({ menuButton: COMMANDS_MENU_BUTTON, signal: controller.signal });
347
721
  } catch (error) {
348
722
  this.#logger.warn?.(
@@ -350,7 +724,11 @@ export class TelegramRuntime {
350
724
  error,
351
725
  );
352
726
  }
353
- const client = new TelegramBotClient({ api, signal: controller.signal });
727
+ const client = new TelegramBotClient({
728
+ api,
729
+ signal: controller.signal,
730
+ logger: this.#logger,
731
+ });
354
732
  this.#bridge = new TelegramHarnessBridge({
355
733
  bot: client,
356
734
  harness: this.#harness,
@@ -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
 
7
9
  function cleanString(value) {
@@ -30,7 +32,7 @@ export function deriveWecomBotIdentity(remoteBotId) {
30
32
 
31
33
  export function maskWecomBotId(remoteBotId) {
32
34
  const value = cleanString(remoteBotId) ?? '';
33
- if (!value) return '企业微信机器人';
35
+ if (!value) return t('企业微信机器人');
34
36
  if (value.length <= 10) return `${value.slice(0, 3)}•••`;
35
37
  return `${value.slice(0, 6)}••••${value.slice(-4)}`;
36
38
  }