@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,5 +1,6 @@
1
1
  import { fetchFileStream } from '../shared/file-download.mjs';
2
2
  import { fetchImageBuffer } from '../shared/image-prompt.mjs';
3
+ import { t } from '../shared/i18n.mjs';
3
4
 
4
5
  const DEFAULT_BASE_URL = 'https://api.telegram.org/';
5
6
  const TELEGRAM_FILE_HOSTS = Object.freeze(['api.telegram.org']);
@@ -35,33 +36,52 @@ function preserveProviderMetadata(target, source) {
35
36
  return target;
36
37
  }
37
38
 
38
- function telegramArtifactProviderError(cause) {
39
+ function inputRichMessage(value) {
40
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
41
+ throw new TypeError('A Telegram rich message is required');
42
+ }
43
+ const formats = ['markdown', 'html', 'blocks'].filter((key) => value[key] !== undefined);
44
+ if (formats.length !== 1) {
45
+ throw new TypeError('A Telegram rich message requires exactly one format');
46
+ }
47
+ const selected = formats[0];
48
+ if ((selected === 'markdown' || selected === 'html')
49
+ && (typeof value[selected] !== 'string' || !value[selected].trim())) {
50
+ throw new TypeError('Telegram rich message text is invalid');
51
+ }
52
+ if (selected === 'blocks' && !Array.isArray(value.blocks)) {
53
+ throw new TypeError('Telegram rich message blocks are invalid');
54
+ }
55
+ return value;
56
+ }
57
+
58
+ function telegramArtifactProviderError(cause, mediaLabel = 'document') {
39
59
  const providerCode = Number(cause?.providerCode);
40
60
  const status = Number(cause?.status);
41
61
  const message = cleanString(cause?.message) ?? '';
42
62
  let code = 'artifact-provider-rejected';
43
- let summary = 'Telegram rejected the document.';
63
+ let summary = `Telegram rejected the ${mediaLabel}.`;
44
64
  if (providerCode === 401 || providerCode === 403 || status === 401 || status === 403) {
45
65
  code = 'artifact-permission-required';
46
- summary = 'Telegram denied permission to send the document.';
66
+ summary = `Telegram denied permission to send the ${mediaLabel}.`;
47
67
  } else if (providerCode === 413 || status === 413
48
68
  || /(?:file|request|entity).{0,20}(?:too (?:big|large)|size limit)|too (?:big|large)/i.test(message)) {
49
69
  code = 'artifact-too-large';
50
- summary = 'The document exceeds Telegram\'s size limit.';
70
+ summary = `The ${mediaLabel} exceeds Telegram's size limit.`;
51
71
  } else if (providerCode === 429 || status === 429) {
52
72
  code = 'artifact-rate-limited';
53
- summary = 'Telegram rate-limited document delivery.';
73
+ summary = `Telegram rate-limited ${mediaLabel} delivery.`;
54
74
  } else if (providerCode >= 500 || status >= 500) {
55
75
  code = 'artifact-delivery-uncertain';
56
- summary = 'Telegram document delivery result is uncertain.';
76
+ summary = `Telegram ${mediaLabel} delivery result is uncertain.`;
57
77
  }
58
78
  const error = new Error(summary, { cause });
59
79
  error.code = code;
60
80
  return preserveProviderMetadata(error, cause);
61
81
  }
62
82
 
63
- function uncertainTelegramDelivery(cause) {
64
- const error = new Error('Telegram document delivery result is uncertain', { cause });
83
+ function uncertainTelegramDelivery(cause, mediaLabel = 'document') {
84
+ const error = new Error(`Telegram ${mediaLabel} delivery result is uncertain`, { cause });
65
85
  error.code = 'artifact-delivery-uncertain';
66
86
  return preserveProviderMetadata(error, cause);
67
87
  }
@@ -183,16 +203,71 @@ export class TelegramApi {
183
203
  }, { signal });
184
204
  }
185
205
 
206
+ async sendRichMessage({
207
+ chatId,
208
+ richMessage,
209
+ replyToMessageId,
210
+ messageThreadId,
211
+ signal,
212
+ }) {
213
+ return this.#call('sendRichMessage', {
214
+ chat_id: chatId,
215
+ rich_message: inputRichMessage(richMessage),
216
+ ...(replyToMessageId ? {
217
+ reply_parameters: { message_id: replyToMessageId, allow_sending_without_reply: true },
218
+ } : {}),
219
+ ...(messageThreadId ? { message_thread_id: messageThreadId } : {}),
220
+ }, { signal });
221
+ }
222
+
223
+ async sendRichMessageDraft({ chatId, draftId, richMessage, messageThreadId, signal }) {
224
+ if (!Number.isSafeInteger(draftId) || draftId === 0) {
225
+ throw new TypeError('Telegram rich message draft id must be a non-zero integer');
226
+ }
227
+ return this.#call('sendRichMessageDraft', {
228
+ chat_id: chatId,
229
+ draft_id: draftId,
230
+ rich_message: inputRichMessage(richMessage),
231
+ ...(messageThreadId ? { message_thread_id: messageThreadId } : {}),
232
+ }, { signal });
233
+ }
234
+
186
235
  async sendDocument({ chatId, file, replyToMessageId, messageThreadId, signal }) {
236
+ return this.#sendArtifact('sendDocument', 'document', 'document', {
237
+ chatId,
238
+ file,
239
+ replyToMessageId,
240
+ messageThreadId,
241
+ signal,
242
+ });
243
+ }
244
+
245
+ async sendPhoto({ chatId, file, replyToMessageId, messageThreadId, signal }) {
246
+ return this.#sendArtifact('sendPhoto', 'photo', 'photo', {
247
+ chatId,
248
+ file,
249
+ replyToMessageId,
250
+ messageThreadId,
251
+ signal,
252
+ });
253
+ }
254
+
255
+ async #sendArtifact(method, fieldName, mediaLabel, {
256
+ chatId,
257
+ file,
258
+ replyToMessageId,
259
+ messageThreadId,
260
+ signal,
261
+ }) {
187
262
  if (!file || typeof file !== 'object'
188
263
  || typeof file.fileName !== 'string' || !file.fileName
189
264
  || !Buffer.isBuffer(file.bytes)) {
190
- throw new TypeError('A Telegram document is required');
265
+ throw new TypeError(`A Telegram ${mediaLabel} is required`);
191
266
  }
192
267
  const payload = new FormData();
193
268
  payload.append('chat_id', String(chatId));
194
269
  payload.append(
195
- 'document',
270
+ fieldName,
196
271
  new Blob([file.bytes], { type: file.mediaType ?? 'application/octet-stream' }),
197
272
  file.fileName,
198
273
  );
@@ -206,26 +281,34 @@ export class TelegramApi {
206
281
  if (signal?.aborted) throw abortReason(signal);
207
282
  const uploadSignal = requestSignal(signal, this.#fileUploadTimeoutMs);
208
283
  try {
209
- return await this.#call('sendDocument', payload, {
284
+ return await this.#call(method, payload, {
210
285
  signal: uploadSignal,
211
286
  timeoutMs: this.#fileUploadTimeoutMs,
212
287
  multipart: true,
213
288
  });
214
289
  } catch (error) {
215
290
  if (signal?.aborted) throw abortReason(signal);
291
+ if (error?.deliveryOutcome === 'unknown') {
292
+ throw uncertainTelegramDelivery(error?.cause ?? error, mediaLabel);
293
+ }
216
294
  if (error?.code?.startsWith?.('telegram-')) {
217
- throw telegramArtifactProviderError(error);
295
+ throw telegramArtifactProviderError(error, mediaLabel);
218
296
  }
219
- throw uncertainTelegramDelivery(error);
297
+ throw uncertainTelegramDelivery(error, mediaLabel);
220
298
  }
221
299
  }
222
300
 
223
- async editMessageText({ chatId, messageId, text, signal }) {
301
+ async editMessageText({ chatId, messageId, text, richMessage, signal }) {
302
+ if ((text === undefined) === (richMessage === undefined)) {
303
+ throw new TypeError('Telegram message edit requires exactly one of text or richMessage');
304
+ }
224
305
  return this.#call('editMessageText', {
225
306
  chat_id: chatId,
226
307
  message_id: messageId,
227
- text,
228
- link_preview_options: { is_disabled: true },
308
+ ...(text === undefined ? { rich_message: inputRichMessage(richMessage) } : {
309
+ text,
310
+ link_preview_options: { is_disabled: true },
311
+ }),
229
312
  }, { signal });
230
313
  }
231
314
 
@@ -257,6 +340,7 @@ export class TelegramApi {
257
340
  }
258
341
 
259
342
  async #call(method, payload, { signal, timeoutMs = 15_000, multipart = false } = {}) {
343
+ if (signal?.aborted) throw abortReason(signal);
260
344
  const url = new URL(this.#baseUrl);
261
345
  url.pathname = `${url.pathname.replace(/\/$/, '')}/bot${this.#token}/${method}`;
262
346
  let response;
@@ -269,8 +353,14 @@ export class TelegramApi {
269
353
  redirect: 'error',
270
354
  });
271
355
  } catch (error) {
272
- if (error?.name === 'AbortError' || error?.name === 'TimeoutError') throw error;
273
- throw new Error(`Telegram ${method} transport failed`);
356
+ const transport = new Error(`Telegram ${method} transport failed`, { cause: error });
357
+ transport.code = error?.name === 'TimeoutError'
358
+ ? 'telegram-timeout'
359
+ : error?.name === 'AbortError'
360
+ ? 'telegram-aborted-after-dispatch'
361
+ : 'telegram-transport-error';
362
+ transport.deliveryOutcome = 'unknown';
363
+ throw transport;
274
364
  }
275
365
  let body;
276
366
  try {
@@ -278,6 +368,8 @@ export class TelegramApi {
278
368
  } catch {
279
369
  const error = new Error(`Telegram ${method} returned invalid JSON`);
280
370
  error.status = response?.status;
371
+ error.code = 'telegram-response-invalid';
372
+ error.deliveryOutcome = 'unknown';
281
373
  throw error;
282
374
  }
283
375
  if (!response.ok || body?.ok !== true) {
@@ -303,7 +395,7 @@ export async function inspectTelegramToken(token, options = {}) {
303
395
  if (!bot?.id || bot?.is_bot !== true) throw new Error('Telegram token does not belong to a bot');
304
396
  return {
305
397
  platformId: String(bot.id),
306
- name: cleanString([bot.first_name, bot.last_name].filter(Boolean).join(' ')) ?? 'Telegram机器人',
398
+ name: cleanString([bot.first_name, bot.last_name].filter(Boolean).join(' ')) ?? t('Telegram机器人'),
307
399
  username: cleanString(bot.username),
308
400
  };
309
401
  }
@@ -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
+ }