@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,147 @@
1
+ export const TELEGRAM_RICH_TEXT_LIMIT = 30_000;
2
+ export const TELEGRAM_REGULAR_TEXT_LIMIT = 4_000;
3
+
4
+ function textValue(value) {
5
+ if (typeof value !== 'string' || !value.trim()) {
6
+ throw new TypeError('Telegram message text must be a non-empty string');
7
+ }
8
+ return value;
9
+ }
10
+
11
+ function escapedCharacterLength(value) {
12
+ if (value === '&') return 5;
13
+ if (value === '<' || value === '>') return 4;
14
+ return 1;
15
+ }
16
+
17
+ function escapedLength(value) {
18
+ return Array.from(value).reduce((total, character) => (
19
+ total + escapedCharacterLength(character)
20
+ ), 0);
21
+ }
22
+
23
+ function preferredCut(points, offset, hardEnd, limit) {
24
+ const minimum = offset + Math.floor(limit * 0.5);
25
+ for (let index = hardEnd - 1; index >= minimum; index -= 1) {
26
+ if (points[index] === '\n' || points[index] === ' ') return index + 1;
27
+ }
28
+ return hardEnd;
29
+ }
30
+
31
+ function splitText(value, limit, characterLength = () => 1) {
32
+ if (typeof value !== 'string') throw new TypeError('Telegram message text must be a string');
33
+ if (!value) return [];
34
+ const points = Array.from(value);
35
+ const chunks = [];
36
+ let offset = 0;
37
+ while (offset < points.length) {
38
+ let end = offset;
39
+ let length = 0;
40
+ while (end < points.length) {
41
+ const nextLength = characterLength(points[end]);
42
+ if (length + nextLength > limit) break;
43
+ length += nextLength;
44
+ end += 1;
45
+ }
46
+ if (end === offset) throw new Error('Telegram message character exceeds the chunk limit');
47
+ if (end < points.length) end = preferredCut(points, offset, end, limit);
48
+ chunks.push(points.slice(offset, end).join(''));
49
+ offset = end;
50
+ }
51
+ return chunks;
52
+ }
53
+
54
+ function fenceMarkers(markdown) {
55
+ return [...markdown.matchAll(/^```[^\n]*(?:\n|$)/gm)];
56
+ }
57
+
58
+ function assertCompleteFences(markdown) {
59
+ const markers = fenceMarkers(markdown);
60
+ if (markers.length % 2 !== 0) {
61
+ throw new Error('Telegram Rich Markdown contains an unfinished code fence');
62
+ }
63
+ return markers;
64
+ }
65
+
66
+ function escapedMarkdown(value) {
67
+ return value
68
+ .replaceAll('&', '&amp;')
69
+ .replaceAll('<', '&lt;')
70
+ .replaceAll('>', '&gt;');
71
+ }
72
+
73
+ function plainRichChunks(source, limit) {
74
+ return splitText(source, limit, escapedCharacterLength).map((chunk) => ({
75
+ source: chunk,
76
+ markdown: escapedMarkdown(chunk),
77
+ }));
78
+ }
79
+
80
+ function fencedRichChunks(source, opening, closing, limit) {
81
+ const body = source.slice(opening.length, source.length - closing.length);
82
+ const wrapperLength = escapedLength(opening)
83
+ + Math.max(escapedLength(closing), escapedLength('```'))
84
+ + 1;
85
+ const bodyLimit = limit - wrapperLength;
86
+ if (bodyLimit < 1) throw new Error('Telegram Rich Markdown code fence exceeds the limit');
87
+ const bodyChunks = splitText(body, bodyLimit, escapedCharacterLength);
88
+ return bodyChunks.map((bodyChunk, index) => {
89
+ const last = index === bodyChunks.length - 1;
90
+ const separator = bodyChunk.endsWith('\n') ? '' : '\n';
91
+ const renderedClosing = last ? closing : '```';
92
+ const sourceChunk = `${index === 0 ? opening : ''}${bodyChunk}${last ? closing : ''}`;
93
+ const markdown = escapedMarkdown(`${opening}${bodyChunk}${separator}${renderedClosing}`);
94
+ if (Array.from(markdown).length > limit) {
95
+ throw new Error('Telegram Rich Markdown code block exceeds the limit');
96
+ }
97
+ return { source: sourceChunk, markdown };
98
+ });
99
+ }
100
+
101
+ export function toTelegramRichMarkdown(value) {
102
+ const markdown = textValue(value);
103
+ assertCompleteFences(markdown);
104
+ // Rich Markdown also accepts HTML tags. Escape entities first so encoded or
105
+ // raw model output cannot be interpreted as Telegram-specific markup.
106
+ return escapedMarkdown(markdown);
107
+ }
108
+
109
+ export function splitTelegramRichMarkdown(value, limit = TELEGRAM_RICH_TEXT_LIMIT) {
110
+ if (!Number.isInteger(limit) || limit < 128 || limit > 32_768) {
111
+ throw new TypeError('Telegram Rich Markdown limit is invalid');
112
+ }
113
+ const source = textValue(value);
114
+ const markers = assertCompleteFences(source);
115
+ const complete = escapedMarkdown(source);
116
+ if (Array.from(complete).length <= limit) return [{ source, markdown: complete }];
117
+
118
+ const chunks = [];
119
+ let cursor = 0;
120
+ for (let index = 0; index < markers.length; index += 2) {
121
+ const opening = markers[index];
122
+ const closing = markers[index + 1];
123
+ if (opening.index > cursor) {
124
+ chunks.push(...plainRichChunks(source.slice(cursor, opening.index), limit));
125
+ }
126
+ const end = closing.index + closing[0].length;
127
+ const fenced = source.slice(opening.index, end);
128
+ const rendered = escapedMarkdown(fenced);
129
+ if (Array.from(rendered).length <= limit) {
130
+ chunks.push({ source: fenced, markdown: rendered });
131
+ } else {
132
+ chunks.push(...fencedRichChunks(fenced, opening[0], closing[0], limit));
133
+ }
134
+ cursor = end;
135
+ }
136
+ if (cursor < source.length) chunks.push(...plainRichChunks(source.slice(cursor), limit));
137
+ return chunks;
138
+ }
139
+
140
+ export function splitTelegramRegularText(value, limit = TELEGRAM_REGULAR_TEXT_LIMIT) {
141
+ if (!Number.isInteger(limit) || limit < 1 || limit > 4_096) {
142
+ throw new TypeError('Telegram regular message limit is invalid');
143
+ }
144
+ // Keep Telegram's established 4000 UTF-16-unit boundary without ever cutting
145
+ // a surrogate pair in half.
146
+ return splitText(textValue(value), limit, (character) => character.length);
147
+ }
@@ -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({
@@ -221,6 +326,265 @@ export class TelegramBotClient {
221
326
  });
222
327
  }
223
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
+
224
588
  async openStream(target) {
225
589
  const stream = createEditableMessageStream({
226
590
  limit: 4_000,
@@ -347,12 +711,12 @@ export class TelegramRuntime {
347
711
  }
348
712
  const webhook = await api.getWebhookInfo({ signal: controller.signal });
349
713
  if (typeof webhook?.url === 'string' && webhook.url) {
350
- const error = new Error('该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。');
714
+ const error = new Error(t('该 Telegram 机器人已配置 Webhook,请先在原服务中移除 Webhook 后重试。'));
351
715
  error.code = 'webhook-configured';
352
716
  throw error;
353
717
  }
354
718
  try {
355
- await api.setMyCommands({ commands: TELEGRAM_COMMAND_MENU, signal: controller.signal });
719
+ await api.setMyCommands({ commands: telegramCommandMenu(), signal: controller.signal });
356
720
  await api.setChatMenuButton({ menuButton: COMMANDS_MENU_BUTTON, signal: controller.signal });
357
721
  } catch (error) {
358
722
  this.#logger.warn?.(
@@ -360,7 +724,11 @@ export class TelegramRuntime {
360
724
  error,
361
725
  );
362
726
  }
363
- const client = new TelegramBotClient({ api, signal: controller.signal });
727
+ const client = new TelegramBotClient({
728
+ api,
729
+ signal: controller.signal,
730
+ logger: this.#logger,
731
+ });
364
732
  this.#bridge = new TelegramHarnessBridge({
365
733
  bot: client,
366
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
  }