@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.
- package/README.en.md +16 -4
- package/README.md +16 -4
- package/lib/index.js +208 -178
- package/package.json +1 -1
- package/plugin-src/host/index.mjs +2 -0
- package/src/channels/dingtalk/config-store.mjs +5 -3
- package/src/channels/dingtalk/dingtalk-api.mjs +2 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +45 -40
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +3 -1
- package/src/channels/dingtalk/dingtalk-controller.mjs +24 -23
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -3
- package/src/channels/dingtalk/state-store.mjs +3 -1
- package/src/channels/discord/config-store.mjs +2 -1
- package/src/channels/discord/discord-api.mjs +25 -1
- package/src/channels/discord/discord-runtime.mjs +237 -11
- package/src/channels/feishu/bridge.mjs +131 -123
- package/src/channels/feishu/feishu-cards.mjs +62 -54
- package/src/channels/feishu/feishu-channel.mjs +5 -4
- package/src/channels/feishu/feishu-runtime.mjs +10 -9
- package/src/channels/feishu/message-utils.mjs +4 -3
- package/src/channels/office/office-job-executor.mjs +15 -14
- package/src/channels/office/office-runtime.mjs +6 -5
- package/src/channels/qq/config-store.mjs +3 -1
- package/src/channels/qq/markdown-reply.mjs +4 -2
- package/src/channels/qq/qq-bridge.mjs +57 -52
- package/src/channels/qq/qq-controller.mjs +17 -16
- package/src/channels/qq/qq-runtime.mjs +3 -2
- package/src/channels/shared/agent-preset.mjs +3 -1
- package/src/channels/shared/compact-command.mjs +15 -14
- package/src/channels/shared/connection-test.mjs +6 -4
- package/src/channels/shared/control-command.mjs +13 -11
- package/src/channels/shared/editable-message-stream.mjs +4 -2
- package/src/channels/shared/harness-approval.mjs +23 -21
- package/src/channels/shared/harness-client.mjs +3 -2
- package/src/channels/shared/harness-question.mjs +8 -6
- package/src/channels/shared/i18n-en/dingtalk.mjs +52 -0
- package/src/channels/shared/i18n-en/discord.mjs +8 -0
- package/src/channels/shared/i18n-en/feishu.mjs +217 -0
- package/src/channels/shared/i18n-en/office.mjs +23 -0
- package/src/channels/shared/i18n-en/qq.mjs +54 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +91 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +215 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +99 -0
- package/src/channels/shared/i18n-en/slack.mjs +27 -0
- package/src/channels/shared/i18n-en/telegram.mjs +21 -0
- package/src/channels/shared/i18n-en/wecom.mjs +38 -0
- package/src/channels/shared/i18n-en/weixin.mjs +80 -0
- package/src/channels/shared/i18n-en/whatsapp.mjs +17 -0
- package/src/channels/shared/i18n-en.mjs +35 -0
- package/src/channels/shared/i18n.mjs +35 -0
- package/src/channels/shared/image-prompt.mjs +17 -15
- package/src/channels/shared/inbound-file.mjs +5 -3
- package/src/channels/shared/model-command.mjs +35 -31
- package/src/channels/shared/preset-command.mjs +41 -38
- package/src/channels/shared/semantic/artifact-delivery.mjs +1 -1
- package/src/channels/shared/semantic/delivery.mjs +38 -0
- package/src/channels/shared/text-harness-bridge.mjs +130 -63
- package/src/channels/shared/token-bot-controller.mjs +32 -11
- package/src/channels/shared/workspace-command.mjs +69 -58
- package/src/channels/slack/config-store.mjs +3 -1
- package/src/channels/slack/slack-api.mjs +6 -5
- package/src/channels/slack/slack-controller.mjs +10 -9
- package/src/channels/slack/slack-runtime.mjs +16 -4
- package/src/channels/telegram/config-store.mjs +2 -1
- package/src/channels/telegram/telegram-api.mjs +72 -6
- package/src/channels/telegram/telegram-rich-message.mjs +147 -0
- package/src/channels/telegram/telegram-runtime.mjs +373 -5
- package/src/channels/wecom/config-store.mjs +3 -1
- package/src/channels/wecom/wecom-bridge.mjs +59 -55
- package/src/channels/wecom/wecom-controller.mjs +20 -19
- package/src/channels/wecom/wecom-runtime.mjs +2 -1
- package/src/channels/weixin/config-store.mjs +2 -1
- package/src/channels/weixin/weixin-bridge.mjs +50 -49
- package/src/channels/weixin/weixin-controller.mjs +22 -21
- package/src/channels/weixin/weixin-runtime.mjs +3 -2
- package/src/channels/whatsapp/config-store.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
- package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -4
- 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('&', '&')
|
|
69
|
+
.replaceAll('<', '<')
|
|
70
|
+
.replaceAll('>', '>');
|
|
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 =
|
|
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:
|
|
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({
|
|
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
|
}
|