@zhin.js/adapter-telegram 7.0.12 → 7.0.14
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/CHANGELOG.md +50 -0
- package/README.md +2 -1
- package/adapters/telegram.js +3 -1
- package/adapters/telegram.ts +3 -1
- package/lib/endpoint.d.ts +8 -2
- package/lib/endpoint.js +47 -0
- package/lib/markdown-to-html.d.ts +9 -0
- package/lib/markdown-to-html.js +75 -0
- package/lib/protocol.d.ts +7 -1
- package/lib/protocol.js +21 -16
- package/package.json +14 -14
- package/src/endpoint.ts +54 -2
- package/src/markdown-to-html.ts +86 -0
- package/src/protocol.ts +27 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.0.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5969c5b: Remove legacy compound-string message targets and Endpoint control probing. Endpoint control and outbound Host operations now carry structured `MessageRef` identities. Endpoint send has one exact result contract (a platform message id), which IM Runtime projects into `DeliveryReceipt.message`; arbitrary result guessing is removed.
|
|
8
|
+
- 974772e: Replace the user-facing `Prompt` vocabulary with the `UserInteraction` authoring surface for input, confirmation, and selection. Commands and handlers now expose `interaction`; IM Runtime exposes `createInteraction`; schema-driven endpoint collection is named `SchemaInteraction`. The old prompt-named interaction types and properties are removed rather than aliased. User interactions render through one canonical Markdown and keyboard/list presentation module shared by commands and Agent `ask_user` turns.
|
|
9
|
+
|
|
10
|
+
Extract the transport-neutral interaction contract into `@zhin.js/interaction`. A discriminated `ask()` API supports text, number, confirmation, single-select, multi-select, and typed lists with structured `title`, `description`, and `tip` content. Typed `sequence()` interactions return one result object keyed by step id, render progress, and retry invalid replies without leaking invalid values to callers.
|
|
11
|
+
|
|
12
|
+
Preserve AI Markdown and card command actions through outbound publishing. QQ delivers Markdown with native command buttons; KOOK, Discord, Telegram, DingTalk, and Lark/Feishu now declare and encode their native Markdown dialects while retaining each adapter's interaction policy. Correct QQ callback button action encoding and button style mapping.
|
|
13
|
+
|
|
14
|
+
- 5969c5b: Add SideEventGateway so adapters forward notice/request/system into HandlerIndex. HandlerContext now exposes only generation-safe capabilities and prompt ports; live Endpoint escape hatches are removed.
|
|
15
|
+
- 059555a: Add the canonical conversation content port for scoped Telegram media references.
|
|
16
|
+
Opaque Bot API file ids are materialized to bounded base64 without exposing token-bearing download URLs.
|
|
17
|
+
- Updated dependencies [5969c5b]
|
|
18
|
+
- Updated dependencies [d336a3f]
|
|
19
|
+
- Updated dependencies [0c82a7e]
|
|
20
|
+
- Updated dependencies [b9217e4]
|
|
21
|
+
- Updated dependencies [5969c5b]
|
|
22
|
+
- Updated dependencies [5969c5b]
|
|
23
|
+
- Updated dependencies [974772e]
|
|
24
|
+
- Updated dependencies [5969c5b]
|
|
25
|
+
- Updated dependencies [5969c5b]
|
|
26
|
+
- Updated dependencies [2f786bd]
|
|
27
|
+
- Updated dependencies [63d89f9]
|
|
28
|
+
- Updated dependencies [71c7cdd]
|
|
29
|
+
- Updated dependencies [3cca0ea]
|
|
30
|
+
- Updated dependencies [1312ca0]
|
|
31
|
+
- Updated dependencies [985fa22]
|
|
32
|
+
- Updated dependencies [04b861d]
|
|
33
|
+
- Updated dependencies [a23d544]
|
|
34
|
+
- Updated dependencies [8cddabf]
|
|
35
|
+
- Updated dependencies [dbe5081]
|
|
36
|
+
- @zhin.js/im-contract@1.0.4
|
|
37
|
+
- @zhin.js/core@1.5.12
|
|
38
|
+
- @zhin.js/adapter@1.1.11
|
|
39
|
+
- @zhin.js/agent@1.1.14
|
|
40
|
+
- @zhin.js/host-http@1.0.11
|
|
41
|
+
- @zhin.js/command@1.0.15
|
|
42
|
+
- zhin.js@6.0.12
|
|
43
|
+
- @zhin.js/permission@1.0.3
|
|
44
|
+
|
|
45
|
+
## 7.0.13
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- @zhin.js/core@1.5.11
|
|
50
|
+
- @zhin.js/agent@1.1.13
|
|
51
|
+
- zhin.js@6.0.11
|
|
52
|
+
|
|
3
53
|
## 7.0.12
|
|
4
54
|
|
|
5
55
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Zhin.js Telegram Bot API 适配器(Plugin Runtime),默认通过 **长轮
|
|
|
7
7
|
- 长轮询 `getUpdates` 入站(默认;无需公网 IP / host-http)
|
|
8
8
|
- 解析 text / image / video / audio / voice / document / sticker / location / callback_query
|
|
9
9
|
- 支持私聊与群组
|
|
10
|
-
- 出站 `send({ conversation, payload })` → Bot API(
|
|
10
|
+
- 出站 `send({ conversation, payload })` → Bot API(Markdown→安全 HTML / media / keyboard)
|
|
11
11
|
- 约定式 `defineAdapter` / `definePlugin`(无需 `usePlugin`)
|
|
12
12
|
- Webhook 模式延期(需 `httpHostToken`);配置 `polling: false` 会明确报错
|
|
13
13
|
|
|
@@ -78,6 +78,7 @@ plugins:
|
|
|
78
78
|
| Telegram | 入站 content(文本摘要) | 出站 wire |
|
|
79
79
|
|----------|--------------------------|-----------|
|
|
80
80
|
| text | 原文 | sendMessage |
|
|
81
|
+
| markdown | 原文 | sendMessage(`parse_mode: HTML`) |
|
|
81
82
|
| photo | `[image]` / caption | sendPhoto(`file_id` / `url`) |
|
|
82
83
|
| video | `[video]` | sendVideo |
|
|
83
84
|
| audio / voice | `[audio]` / `[voice]` | sendAudio / sendVoice |
|
package/adapters/telegram.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Convention entry: discover `adapters/telegram.ts` → defineAdapter.
|
|
4
4
|
*/
|
|
5
5
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
-
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
6
|
+
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
7
7
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
8
8
|
import { TelegramEndpoint } from "../lib/endpoint.js";
|
|
9
9
|
import { resolveTelegramConfig, } from "../lib/protocol.js";
|
|
@@ -17,6 +17,7 @@ export default defineAdapter({
|
|
|
17
17
|
segments: {
|
|
18
18
|
outboundMedia: ['url', 'upload'],
|
|
19
19
|
interactive: 'native',
|
|
20
|
+
markdown: 'native',
|
|
20
21
|
},
|
|
21
22
|
create(context) {
|
|
22
23
|
const config = resolveTelegramConfig(context.config);
|
|
@@ -28,6 +29,7 @@ export default defineAdapter({
|
|
|
28
29
|
return new TelegramEndpoint({
|
|
29
30
|
id: context.id,
|
|
30
31
|
gateway: context.use(messageGatewayToken),
|
|
32
|
+
sideEvents: context.use(sideEventGatewayToken),
|
|
31
33
|
config,
|
|
32
34
|
http: config.mode === 'webhook' ? context.use(httpHostToken) : undefined,
|
|
33
35
|
});
|
package/adapters/telegram.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Convention entry: discover `adapters/telegram.ts` → defineAdapter.
|
|
3
3
|
*/
|
|
4
4
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
-
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
5
|
+
import { messageGatewayToken, sideEventGatewayToken } from '@zhin.js/core/runtime';
|
|
6
6
|
import { httpHostToken } from '@zhin.js/host-http';
|
|
7
7
|
import { TelegramEndpoint } from '../src/endpoint.js';
|
|
8
8
|
import {
|
|
@@ -22,6 +22,7 @@ export default defineAdapter<TelegramAdapterConfig>({
|
|
|
22
22
|
segments: {
|
|
23
23
|
outboundMedia: ['url', 'upload'],
|
|
24
24
|
interactive: 'native',
|
|
25
|
+
markdown: 'native',
|
|
25
26
|
},
|
|
26
27
|
create(context) {
|
|
27
28
|
const config = resolveTelegramConfig(context.config);
|
|
@@ -33,6 +34,7 @@ export default defineAdapter<TelegramAdapterConfig>({
|
|
|
33
34
|
return new TelegramEndpoint({
|
|
34
35
|
id: context.id,
|
|
35
36
|
gateway: context.use(messageGatewayToken),
|
|
37
|
+
sideEvents: context.use(sideEventGatewayToken),
|
|
36
38
|
config,
|
|
37
39
|
http: config.mode === 'webhook' ? context.use(httpHostToken) : undefined,
|
|
38
40
|
});
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EndpointControl, EndpointInstance, EndpointSendRequest } from 'zhin.js/adapter';
|
|
2
|
-
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
1
|
+
import type { EndpointContentPort, EndpointControl, EndpointInstance, EndpointSendRequest } from 'zhin.js/adapter';
|
|
2
|
+
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
3
3
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
4
|
import { type MessageRef } from '@zhin.js/im-contract';
|
|
5
5
|
import type { CapabilityId } from 'zhin.js';
|
|
@@ -14,10 +14,15 @@ export type TelegramFetch = (url: string, init?: {
|
|
|
14
14
|
readonly status: number;
|
|
15
15
|
text(): Promise<string>;
|
|
16
16
|
json(): Promise<unknown>;
|
|
17
|
+
arrayBuffer?(): Promise<ArrayBuffer>;
|
|
18
|
+
readonly headers?: {
|
|
19
|
+
get(name: string): string | null;
|
|
20
|
+
};
|
|
17
21
|
}>;
|
|
18
22
|
export interface TelegramEndpointOptions {
|
|
19
23
|
readonly id: CapabilityId;
|
|
20
24
|
readonly gateway: MessageGateway;
|
|
25
|
+
readonly sideEvents?: SideEventGateway;
|
|
21
26
|
readonly config: ResolvedTelegramConfig;
|
|
22
27
|
readonly http?: HttpHost;
|
|
23
28
|
readonly fetch?: TelegramFetch;
|
|
@@ -30,6 +35,7 @@ export interface TelegramEndpointOptions {
|
|
|
30
35
|
export declare class TelegramEndpoint implements EndpointInstance {
|
|
31
36
|
#private;
|
|
32
37
|
readonly control: EndpointControl;
|
|
38
|
+
readonly content: EndpointContentPort;
|
|
33
39
|
constructor(options: TelegramEndpointOptions);
|
|
34
40
|
/** Used by webhook handler. */
|
|
35
41
|
get isOpen(): boolean;
|
package/lib/endpoint.js
CHANGED
|
@@ -32,6 +32,9 @@ export class TelegramEndpoint {
|
|
|
32
32
|
control = Object.freeze({
|
|
33
33
|
recall: (message) => this.recallMessage(message),
|
|
34
34
|
});
|
|
35
|
+
content = Object.freeze({
|
|
36
|
+
resolve: (reference, context) => this.#resolveContent(reference, context.signal),
|
|
37
|
+
});
|
|
35
38
|
constructor(options) {
|
|
36
39
|
this.#logger = getAdapterLogger('telegram', options.config.id);
|
|
37
40
|
this.#options = options;
|
|
@@ -152,6 +155,50 @@ export class TelegramEndpoint {
|
|
|
152
155
|
message_id: Number(message.id),
|
|
153
156
|
});
|
|
154
157
|
}
|
|
158
|
+
async #resolveContent(reference, signal) {
|
|
159
|
+
if (reference.kind !== 'media') {
|
|
160
|
+
return Object.freeze({ status: 'unsupported', code: 'telegram_message_lookup_unavailable' });
|
|
161
|
+
}
|
|
162
|
+
if (reference.media.kind !== 'file') {
|
|
163
|
+
return Object.freeze({ status: 'resolved', reference, value: reference.media });
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
signal.throwIfAborted();
|
|
167
|
+
const file = await this.callApi('getFile', {
|
|
168
|
+
file_id: reference.media.value,
|
|
169
|
+
});
|
|
170
|
+
if (!file.file_path)
|
|
171
|
+
return Object.freeze({ status: 'not_found', code: 'telegram_file_not_found' });
|
|
172
|
+
if ((file.file_size ?? 0) > 26_214_400)
|
|
173
|
+
return Object.freeze({ status: 'forbidden', code: 'media_size_limit' });
|
|
174
|
+
const response = await this.#fetch(`${this.#options.config.apiBaseUrl}/file/bot${this.#options.config.token}/${file.file_path}`, { signal });
|
|
175
|
+
if (!response.ok)
|
|
176
|
+
return Object.freeze({ status: 'failed', code: 'telegram_file_download_failed' });
|
|
177
|
+
if (!response.arrayBuffer)
|
|
178
|
+
return Object.freeze({ status: 'failed', code: 'telegram_binary_transport_unavailable' });
|
|
179
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
180
|
+
if (bytes.byteLength > 26_214_400)
|
|
181
|
+
return Object.freeze({ status: 'forbidden', code: 'media_size_limit' });
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
status: 'resolved',
|
|
184
|
+
reference,
|
|
185
|
+
value: Object.freeze({
|
|
186
|
+
kind: 'base64',
|
|
187
|
+
value: bytes.toString('base64'),
|
|
188
|
+
...(response.headers?.get('content-type')?.split(';')[0] || reference.media.mime_type
|
|
189
|
+
? { mime_type: response.headers?.get('content-type')?.split(';')[0] ?? reference.media.mime_type }
|
|
190
|
+
: {}),
|
|
191
|
+
...(reference.media.file_name ? { file_name: reference.media.file_name } : {}),
|
|
192
|
+
size: bytes.byteLength,
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (signal.aborted)
|
|
198
|
+
return Object.freeze({ status: 'expired', code: 'turn_aborted' });
|
|
199
|
+
return Object.freeze({ status: 'failed', code: 'telegram_file_resolution_failed', message: error instanceof Error ? error.message : String(error) });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
155
202
|
/**
|
|
156
203
|
* 含 `attach://` 占位的媒体参数 → multipart/form-data:
|
|
157
204
|
* 标量参数原样、对象参数 JSON 序列化、attach 占位替换为文件 part
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Markdown → Telegram Bot API HTML.
|
|
3
|
+
*
|
|
4
|
+
* Telegram's MarkdownV2 is not CommonMark-compatible and rejects unescaped
|
|
5
|
+
* punctuation. HTML gives the adapter a safer dialect seam: user text is
|
|
6
|
+
* escaped first and only the subset supported by Bot API is emitted as tags.
|
|
7
|
+
*/
|
|
8
|
+
export declare function markdownToTelegramHtml(markdown: string): string;
|
|
9
|
+
export declare function escapeTelegramHtml(value: string): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Markdown → Telegram Bot API HTML.
|
|
3
|
+
*
|
|
4
|
+
* Telegram's MarkdownV2 is not CommonMark-compatible and rejects unescaped
|
|
5
|
+
* punctuation. HTML gives the adapter a safer dialect seam: user text is
|
|
6
|
+
* escaped first and only the subset supported by Bot API is emitted as tags.
|
|
7
|
+
*/
|
|
8
|
+
export function markdownToTelegramHtml(markdown) {
|
|
9
|
+
const protectedFragments = [];
|
|
10
|
+
const protect = (html) => {
|
|
11
|
+
const token = `\uE000${protectedFragments.length}\uE001`;
|
|
12
|
+
protectedFragments.push(html);
|
|
13
|
+
return token;
|
|
14
|
+
};
|
|
15
|
+
let value = protectTelegramFencedCode(markdown, protect);
|
|
16
|
+
value = value.replace(/`([^`\n]+)`/g, (_match, code) => (protect(`<code>${escapeTelegramHtml(String(code))}</code>`)));
|
|
17
|
+
value = escapeTelegramHtml(value);
|
|
18
|
+
value = value
|
|
19
|
+
.replace(/!\[([^\[\]]*)\]\(([^()\s]+)(?:\s+"[^&]*")?\)/g, '$1')
|
|
20
|
+
.replace(/\[([^\[\]]+)\]\(([^()\s]+)(?:\s+"[^&]*")?\)/g, (_match, label, href) => isSafeTelegramHref(href)
|
|
21
|
+
? `<a href="${href}">${label}</a>`
|
|
22
|
+
: label)
|
|
23
|
+
.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>')
|
|
24
|
+
.replace(/^>\s?(.*)$/gm, '<blockquote>$1</blockquote>')
|
|
25
|
+
.replace(/^\s*[-+*]\s+(.+)$/gm, '• $1')
|
|
26
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
|
27
|
+
.replace(/__([^_\n]+)__/g, '<b>$1</b>')
|
|
28
|
+
.replace(/~~([^~\n]+)~~/g, '<s>$1</s>')
|
|
29
|
+
.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<i>$2</i>')
|
|
30
|
+
.replace(/(^|[^_])_([^_\n]+)_/g, '$1<i>$2</i>');
|
|
31
|
+
for (let index = 0; index < protectedFragments.length; index += 1) {
|
|
32
|
+
value = value.replace(`\uE000${index}\uE001`, protectedFragments[index]);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function protectTelegramFencedCode(markdown, protect) {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
let cursor = 0;
|
|
39
|
+
while (cursor < markdown.length) {
|
|
40
|
+
const opening = markdown.indexOf('```', cursor);
|
|
41
|
+
if (opening < 0) {
|
|
42
|
+
chunks.push(markdown.slice(cursor));
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
chunks.push(markdown.slice(cursor, opening));
|
|
46
|
+
const contentStart = opening + 3;
|
|
47
|
+
const closing = markdown.indexOf('```', contentStart);
|
|
48
|
+
if (closing < 0) {
|
|
49
|
+
chunks.push(markdown.slice(opening));
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
const newline = markdown.indexOf('\n', contentStart);
|
|
53
|
+
const hasHeader = newline >= 0 && newline < closing;
|
|
54
|
+
const language = markdown.slice(contentStart, hasHeader ? newline : closing).trim();
|
|
55
|
+
let code = hasHeader ? markdown.slice(newline + 1, closing) : '';
|
|
56
|
+
if (code.endsWith('\n'))
|
|
57
|
+
code = code.slice(0, -1);
|
|
58
|
+
const escapedCode = escapeTelegramHtml(code);
|
|
59
|
+
chunks.push(/^[A-Za-z0-9_+-]+$/.test(language)
|
|
60
|
+
? protect(`<pre><code class="language-${language}">${escapedCode}</code></pre>`)
|
|
61
|
+
: protect(`<pre>${escapedCode}</pre>`));
|
|
62
|
+
cursor = closing + 3;
|
|
63
|
+
}
|
|
64
|
+
return chunks.join('');
|
|
65
|
+
}
|
|
66
|
+
function isSafeTelegramHref(value) {
|
|
67
|
+
return /^(?:https?:\/\/|tg:\/\/|mailto:)[^\s<>]+$/iu.test(value);
|
|
68
|
+
}
|
|
69
|
+
export function escapeTelegramHtml(value) {
|
|
70
|
+
return value
|
|
71
|
+
.replace(/&/g, '&')
|
|
72
|
+
.replace(/</g, '<')
|
|
73
|
+
.replace(/>/g, '>')
|
|
74
|
+
.replace(/"/g, '"');
|
|
75
|
+
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -153,6 +153,7 @@ export type TelegramOutboundAction = {
|
|
|
153
153
|
readonly params: {
|
|
154
154
|
readonly chat_id: number | string;
|
|
155
155
|
readonly text: string;
|
|
156
|
+
readonly parse_mode?: 'HTML';
|
|
156
157
|
readonly reply_parameters?: {
|
|
157
158
|
readonly message_id: number;
|
|
158
159
|
};
|
|
@@ -166,6 +167,7 @@ export type TelegramOutboundAction = {
|
|
|
166
167
|
readonly chat_id: number | string;
|
|
167
168
|
readonly photo: string;
|
|
168
169
|
readonly caption?: string;
|
|
170
|
+
readonly parse_mode?: 'HTML';
|
|
169
171
|
readonly reply_parameters?: {
|
|
170
172
|
readonly message_id: number;
|
|
171
173
|
};
|
|
@@ -176,6 +178,7 @@ export type TelegramOutboundAction = {
|
|
|
176
178
|
readonly chat_id: number | string;
|
|
177
179
|
readonly video: string;
|
|
178
180
|
readonly caption?: string;
|
|
181
|
+
readonly parse_mode?: 'HTML';
|
|
179
182
|
readonly reply_parameters?: {
|
|
180
183
|
readonly message_id: number;
|
|
181
184
|
};
|
|
@@ -186,6 +189,7 @@ export type TelegramOutboundAction = {
|
|
|
186
189
|
readonly chat_id: number | string;
|
|
187
190
|
readonly audio: string;
|
|
188
191
|
readonly caption?: string;
|
|
192
|
+
readonly parse_mode?: 'HTML';
|
|
189
193
|
readonly reply_parameters?: {
|
|
190
194
|
readonly message_id: number;
|
|
191
195
|
};
|
|
@@ -196,6 +200,7 @@ export type TelegramOutboundAction = {
|
|
|
196
200
|
readonly chat_id: number | string;
|
|
197
201
|
readonly voice: string;
|
|
198
202
|
readonly caption?: string;
|
|
203
|
+
readonly parse_mode?: 'HTML';
|
|
199
204
|
readonly reply_parameters?: {
|
|
200
205
|
readonly message_id: number;
|
|
201
206
|
};
|
|
@@ -206,6 +211,7 @@ export type TelegramOutboundAction = {
|
|
|
206
211
|
readonly chat_id: number | string;
|
|
207
212
|
readonly document: string;
|
|
208
213
|
readonly caption?: string;
|
|
214
|
+
readonly parse_mode?: 'HTML';
|
|
209
215
|
readonly reply_parameters?: {
|
|
210
216
|
readonly message_id: number;
|
|
211
217
|
};
|
|
@@ -249,7 +255,7 @@ export declare function senderDisplayName(user?: TelegramUser): string;
|
|
|
249
255
|
export declare function formatInboundContent(msg: TelegramMessage): string;
|
|
250
256
|
export declare function formatCallbackContent(query: TelegramCallbackQuery): string;
|
|
251
257
|
/**
|
|
252
|
-
* 入站消息 → canonical Segment[]
|
|
258
|
+
* 入站消息 → canonical Segment[];媒体只存在于 canonical 段,不重复写入文本。
|
|
253
259
|
* Telegram 附件只有不透明 file_id(需 getFile 二次解析,非 URL),
|
|
254
260
|
* 统一进 MediaRef kind=file;photo 取数组末尾(最大尺寸)。
|
|
255
261
|
*/
|
package/lib/protocol.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { isMediaRef } from '@zhin.js/core';
|
|
6
6
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
import { escapeTelegramHtml, markdownToTelegramHtml } from './markdown-to-html.js';
|
|
7
8
|
const logger = getLogger('telegram');
|
|
8
9
|
export function resolveTelegramConfig(config = {}) {
|
|
9
10
|
const entry = config.endpoints?.find((item) => item.context === 'telegram');
|
|
@@ -106,17 +107,6 @@ export function formatInboundContent(msg) {
|
|
|
106
107
|
return msg.text;
|
|
107
108
|
if (msg.caption)
|
|
108
109
|
return msg.caption;
|
|
109
|
-
if (msg.photo?.length)
|
|
110
|
-
return '[image]';
|
|
111
|
-
if (msg.video)
|
|
112
|
-
return '[video]';
|
|
113
|
-
if (msg.audio)
|
|
114
|
-
return '[audio]';
|
|
115
|
-
if (msg.voice)
|
|
116
|
-
return '[voice]';
|
|
117
|
-
if (msg.document) {
|
|
118
|
-
return msg.document.file_name ? `[file: ${msg.document.file_name}]` : '[file]';
|
|
119
|
-
}
|
|
120
110
|
if (msg.sticker) {
|
|
121
111
|
return msg.sticker.emoji ? `[sticker: ${msg.sticker.emoji}]` : '[sticker]';
|
|
122
112
|
}
|
|
@@ -129,7 +119,7 @@ export function formatCallbackContent(query) {
|
|
|
129
119
|
return query.data ? `[action: ${query.data}]` : '[action]';
|
|
130
120
|
}
|
|
131
121
|
/**
|
|
132
|
-
* 入站消息 → canonical Segment[]
|
|
122
|
+
* 入站消息 → canonical Segment[];媒体只存在于 canonical 段,不重复写入文本。
|
|
133
123
|
* Telegram 附件只有不透明 file_id(需 getFile 二次解析,非 URL),
|
|
134
124
|
* 统一进 MediaRef kind=file;photo 取数组末尾(最大尺寸)。
|
|
135
125
|
*/
|
|
@@ -240,6 +230,11 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
240
230
|
: payload && typeof payload === 'object' && 'type' in payload
|
|
241
231
|
? [payload]
|
|
242
232
|
: [];
|
|
233
|
+
const hasMarkdown = items.some((item) => typeof item !== 'string' && item.type === 'markdown');
|
|
234
|
+
const appendPlain = (value) => hasMarkdown
|
|
235
|
+
? escapeTelegramHtml(String(value ?? ''))
|
|
236
|
+
: String(value ?? '');
|
|
237
|
+
const htmlMode = hasMarkdown ? { parse_mode: 'HTML' } : {};
|
|
243
238
|
if (items.length === 0) {
|
|
244
239
|
const text = payload == null
|
|
245
240
|
? ''
|
|
@@ -300,17 +295,20 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
300
295
|
};
|
|
301
296
|
for (const item of items) {
|
|
302
297
|
if (typeof item === 'string') {
|
|
303
|
-
textContent += item;
|
|
298
|
+
textContent += appendPlain(item);
|
|
304
299
|
continue;
|
|
305
300
|
}
|
|
306
301
|
const data = item.data ?? {};
|
|
307
302
|
switch (item.type) {
|
|
308
303
|
case 'text':
|
|
309
|
-
textContent +=
|
|
304
|
+
textContent += appendPlain(data.text ?? data.content ?? '');
|
|
305
|
+
break;
|
|
306
|
+
case 'markdown':
|
|
307
|
+
textContent += markdownToTelegramHtml(String(data.content ?? data.text ?? ''));
|
|
310
308
|
break;
|
|
311
309
|
case 'at':
|
|
312
310
|
if (data.id)
|
|
313
|
-
textContent += `@${
|
|
311
|
+
textContent += `@${appendPlain(data.name || data.id)}`;
|
|
314
312
|
break;
|
|
315
313
|
case 'reply': {
|
|
316
314
|
const id = Number(data.id ?? data.message_id);
|
|
@@ -343,6 +341,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
343
341
|
chat_id: chatId,
|
|
344
342
|
photo,
|
|
345
343
|
caption: textContent.trim() || undefined,
|
|
344
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
346
345
|
...replyParams(),
|
|
347
346
|
},
|
|
348
347
|
});
|
|
@@ -359,6 +358,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
359
358
|
chat_id: chatId,
|
|
360
359
|
video,
|
|
361
360
|
caption: textContent.trim() || undefined,
|
|
361
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
362
362
|
...replyParams(),
|
|
363
363
|
},
|
|
364
364
|
});
|
|
@@ -375,6 +375,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
375
375
|
chat_id: chatId,
|
|
376
376
|
audio,
|
|
377
377
|
caption: textContent.trim() || undefined,
|
|
378
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
378
379
|
...replyParams(),
|
|
379
380
|
},
|
|
380
381
|
});
|
|
@@ -391,6 +392,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
391
392
|
chat_id: chatId,
|
|
392
393
|
voice,
|
|
393
394
|
caption: textContent.trim() || undefined,
|
|
395
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
394
396
|
...replyParams(),
|
|
395
397
|
},
|
|
396
398
|
});
|
|
@@ -407,6 +409,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
407
409
|
chat_id: chatId,
|
|
408
410
|
document,
|
|
409
411
|
caption: textContent.trim() || undefined,
|
|
412
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
410
413
|
...replyParams(),
|
|
411
414
|
},
|
|
412
415
|
});
|
|
@@ -437,7 +440,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
437
440
|
break;
|
|
438
441
|
}
|
|
439
442
|
default:
|
|
440
|
-
textContent +=
|
|
443
|
+
textContent += appendPlain(data.text ?? `[${item.type}]`);
|
|
441
444
|
}
|
|
442
445
|
}
|
|
443
446
|
if (actions.length === 0) {
|
|
@@ -449,6 +452,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
449
452
|
params: {
|
|
450
453
|
chat_id: chatId,
|
|
451
454
|
text: text || ' ',
|
|
455
|
+
...htmlMode,
|
|
452
456
|
...replyParams(),
|
|
453
457
|
...(keyboard ? { reply_markup: { inline_keyboard: keyboard } } : {}),
|
|
454
458
|
},
|
|
@@ -460,6 +464,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
460
464
|
params: {
|
|
461
465
|
chat_id: chatId,
|
|
462
466
|
text: textContent.trim() || ' ',
|
|
467
|
+
...htmlMode,
|
|
463
468
|
...replyParams(),
|
|
464
469
|
...(keyboard ? { reply_markup: { inline_keyboard: keyboard } } : {}),
|
|
465
470
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-telegram",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.14",
|
|
4
4
|
"description": "Zhin.js Telegram Bot API adapter for Plugin Runtime (long-poll getUpdates)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -32,20 +32,20 @@
|
|
|
32
32
|
"directory": "plugins/adapters/telegram"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zhin.js/adapter": "1.1.
|
|
36
|
-
"@zhin.js/core": "1.5.
|
|
37
|
-
"@zhin.js/host-http": "1.0.
|
|
38
|
-
"@zhin.js/im-contract": "1.0.
|
|
35
|
+
"@zhin.js/adapter": "1.1.11",
|
|
36
|
+
"@zhin.js/core": "1.5.12",
|
|
37
|
+
"@zhin.js/host-http": "1.0.11",
|
|
38
|
+
"@zhin.js/im-contract": "1.0.4",
|
|
39
39
|
"@zhin.js/logger": "1.0.76"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"zod": "^4.0.0",
|
|
43
|
-
"@zhin.js/adapter": "1.1.
|
|
44
|
-
"@zhin.js/agent": "1.1.
|
|
45
|
-
"@zhin.js/command": "1.0.
|
|
46
|
-
"@zhin.js/core": "1.5.
|
|
47
|
-
"@zhin.js/permission": "1.0.
|
|
48
|
-
"zhin.js": "6.0.
|
|
43
|
+
"@zhin.js/adapter": "1.1.11",
|
|
44
|
+
"@zhin.js/agent": "1.1.14",
|
|
45
|
+
"@zhin.js/command": "1.0.15",
|
|
46
|
+
"@zhin.js/core": "1.5.12",
|
|
47
|
+
"@zhin.js/permission": "1.0.3",
|
|
48
|
+
"zhin.js": "6.0.12"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"@zhin.js/agent": {
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
"typescript": "^6.0.3",
|
|
67
67
|
"vitest": "^4.1.10",
|
|
68
68
|
"zod": "^4.4.3",
|
|
69
|
-
"@zhin.js/agent": "1.1.
|
|
70
|
-
"@zhin.js/host-http": "1.0.
|
|
71
|
-
"zhin.js": "6.0.
|
|
69
|
+
"@zhin.js/agent": "1.1.14",
|
|
70
|
+
"@zhin.js/host-http": "1.0.11",
|
|
71
|
+
"zhin.js": "6.0.12"
|
|
72
72
|
},
|
|
73
73
|
"files": [
|
|
74
74
|
"adapters",
|
package/src/endpoint.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* TelegramEndpoint — lifecycle, outbound, admit, Bot API helpers for agent tools.
|
|
3
3
|
*/
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
|
-
import type { EndpointControl, EndpointInstance, EndpointSendRequest } from 'zhin.js/adapter';
|
|
6
|
-
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
5
|
+
import type { EndpointContentPort, EndpointContentResolveContext, EndpointControl, EndpointInstance, EndpointSendRequest } from 'zhin.js/adapter';
|
|
6
|
+
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
7
7
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
8
|
import {
|
|
9
9
|
type ConversationRef,
|
|
10
|
+
type ConversationReference,
|
|
11
|
+
type ConversationResolution,
|
|
10
12
|
type MessageRef,
|
|
11
13
|
} from '@zhin.js/im-contract';
|
|
12
14
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
@@ -56,11 +58,14 @@ export type TelegramFetch = (
|
|
|
56
58
|
readonly status: number;
|
|
57
59
|
text(): Promise<string>;
|
|
58
60
|
json(): Promise<unknown>;
|
|
61
|
+
arrayBuffer?(): Promise<ArrayBuffer>;
|
|
62
|
+
readonly headers?: { get(name: string): string | null };
|
|
59
63
|
}>;
|
|
60
64
|
|
|
61
65
|
export interface TelegramEndpointOptions {
|
|
62
66
|
readonly id: CapabilityId;
|
|
63
67
|
readonly gateway: MessageGateway;
|
|
68
|
+
readonly sideEvents?: SideEventGateway;
|
|
64
69
|
readonly config: ResolvedTelegramConfig;
|
|
65
70
|
readonly http?: HttpHost;
|
|
66
71
|
readonly fetch?: TelegramFetch;
|
|
@@ -100,6 +105,9 @@ export class TelegramEndpoint implements EndpointInstance {
|
|
|
100
105
|
readonly control: EndpointControl = Object.freeze({
|
|
101
106
|
recall: (message: MessageRef) => this.recallMessage(message),
|
|
102
107
|
});
|
|
108
|
+
readonly content: EndpointContentPort = Object.freeze({
|
|
109
|
+
resolve: (reference: ConversationReference, context: EndpointContentResolveContext) => this.#resolveContent(reference, context.signal),
|
|
110
|
+
});
|
|
103
111
|
|
|
104
112
|
constructor(options: TelegramEndpointOptions) {
|
|
105
113
|
this.#logger = getAdapterLogger('telegram', options.config.id);
|
|
@@ -229,6 +237,50 @@ export class TelegramEndpoint implements EndpointInstance {
|
|
|
229
237
|
});
|
|
230
238
|
}
|
|
231
239
|
|
|
240
|
+
async #resolveContent(
|
|
241
|
+
reference: Parameters<EndpointContentPort['resolve']>[0],
|
|
242
|
+
signal: AbortSignal,
|
|
243
|
+
): Promise<ConversationResolution> {
|
|
244
|
+
if (reference.kind !== 'media') {
|
|
245
|
+
return Object.freeze({ status: 'unsupported', code: 'telegram_message_lookup_unavailable' });
|
|
246
|
+
}
|
|
247
|
+
if (reference.media.kind !== 'file') {
|
|
248
|
+
return Object.freeze({ status: 'resolved', reference, value: reference.media });
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
signal.throwIfAborted();
|
|
252
|
+
const file = await this.callApi<{ file_path?: string; file_size?: number }>('getFile', {
|
|
253
|
+
file_id: reference.media.value,
|
|
254
|
+
});
|
|
255
|
+
if (!file.file_path) return Object.freeze({ status: 'not_found', code: 'telegram_file_not_found' });
|
|
256
|
+
if ((file.file_size ?? 0) > 26_214_400) return Object.freeze({ status: 'forbidden', code: 'media_size_limit' });
|
|
257
|
+
const response = await this.#fetch(
|
|
258
|
+
`${this.#options.config.apiBaseUrl}/file/bot${this.#options.config.token}/${file.file_path}`,
|
|
259
|
+
{ signal },
|
|
260
|
+
);
|
|
261
|
+
if (!response.ok) return Object.freeze({ status: 'failed', code: 'telegram_file_download_failed' });
|
|
262
|
+
if (!response.arrayBuffer) return Object.freeze({ status: 'failed', code: 'telegram_binary_transport_unavailable' });
|
|
263
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
264
|
+
if (bytes.byteLength > 26_214_400) return Object.freeze({ status: 'forbidden', code: 'media_size_limit' });
|
|
265
|
+
return Object.freeze({
|
|
266
|
+
status: 'resolved',
|
|
267
|
+
reference,
|
|
268
|
+
value: Object.freeze({
|
|
269
|
+
kind: 'base64',
|
|
270
|
+
value: bytes.toString('base64'),
|
|
271
|
+
...(response.headers?.get('content-type')?.split(';')[0] || reference.media.mime_type
|
|
272
|
+
? { mime_type: response.headers?.get('content-type')?.split(';')[0] ?? reference.media.mime_type }
|
|
273
|
+
: {}),
|
|
274
|
+
...(reference.media.file_name ? { file_name: reference.media.file_name } : {}),
|
|
275
|
+
size: bytes.byteLength,
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (signal.aborted) return Object.freeze({ status: 'expired', code: 'turn_aborted' });
|
|
280
|
+
return Object.freeze({ status: 'failed', code: 'telegram_file_resolution_failed', message: error instanceof Error ? error.message : String(error) });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
232
284
|
/**
|
|
233
285
|
* 含 `attach://` 占位的媒体参数 → multipart/form-data:
|
|
234
286
|
* 标量参数原样、对象参数 JSON 序列化、attach 占位替换为文件 part
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Markdown → Telegram Bot API HTML.
|
|
3
|
+
*
|
|
4
|
+
* Telegram's MarkdownV2 is not CommonMark-compatible and rejects unescaped
|
|
5
|
+
* punctuation. HTML gives the adapter a safer dialect seam: user text is
|
|
6
|
+
* escaped first and only the subset supported by Bot API is emitted as tags.
|
|
7
|
+
*/
|
|
8
|
+
export function markdownToTelegramHtml(markdown: string): string {
|
|
9
|
+
const protectedFragments: string[] = [];
|
|
10
|
+
const protect = (html: string): string => {
|
|
11
|
+
const token = `\uE000${protectedFragments.length}\uE001`;
|
|
12
|
+
protectedFragments.push(html);
|
|
13
|
+
return token;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let value = protectTelegramFencedCode(markdown, protect);
|
|
17
|
+
value = value.replace(/`([^`\n]+)`/g, (_match, code) => (
|
|
18
|
+
protect(`<code>${escapeTelegramHtml(String(code))}</code>`)
|
|
19
|
+
));
|
|
20
|
+
value = escapeTelegramHtml(value);
|
|
21
|
+
|
|
22
|
+
value = value
|
|
23
|
+
.replace(/!\[([^\[\]]*)\]\(([^()\s]+)(?:\s+"[^&]*")?\)/g, '$1')
|
|
24
|
+
.replace(/\[([^\[\]]+)\]\(([^()\s]+)(?:\s+"[^&]*")?\)/g,
|
|
25
|
+
(_match, label: string, href: string) => isSafeTelegramHref(href)
|
|
26
|
+
? `<a href="${href}">${label}</a>`
|
|
27
|
+
: label)
|
|
28
|
+
.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>')
|
|
29
|
+
.replace(/^>\s?(.*)$/gm, '<blockquote>$1</blockquote>')
|
|
30
|
+
.replace(/^\s*[-+*]\s+(.+)$/gm, '• $1')
|
|
31
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
|
32
|
+
.replace(/__([^_\n]+)__/g, '<b>$1</b>')
|
|
33
|
+
.replace(/~~([^~\n]+)~~/g, '<s>$1</s>')
|
|
34
|
+
.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<i>$2</i>')
|
|
35
|
+
.replace(/(^|[^_])_([^_\n]+)_/g, '$1<i>$2</i>');
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < protectedFragments.length; index += 1) {
|
|
38
|
+
value = value.replace(`\uE000${index}\uE001`, protectedFragments[index]!);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function protectTelegramFencedCode(
|
|
44
|
+
markdown: string,
|
|
45
|
+
protect: (html: string) => string,
|
|
46
|
+
): string {
|
|
47
|
+
const chunks: string[] = [];
|
|
48
|
+
let cursor = 0;
|
|
49
|
+
while (cursor < markdown.length) {
|
|
50
|
+
const opening = markdown.indexOf('```', cursor);
|
|
51
|
+
if (opening < 0) {
|
|
52
|
+
chunks.push(markdown.slice(cursor));
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
chunks.push(markdown.slice(cursor, opening));
|
|
56
|
+
const contentStart = opening + 3;
|
|
57
|
+
const closing = markdown.indexOf('```', contentStart);
|
|
58
|
+
if (closing < 0) {
|
|
59
|
+
chunks.push(markdown.slice(opening));
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const newline = markdown.indexOf('\n', contentStart);
|
|
63
|
+
const hasHeader = newline >= 0 && newline < closing;
|
|
64
|
+
const language = markdown.slice(contentStart, hasHeader ? newline : closing).trim();
|
|
65
|
+
let code = hasHeader ? markdown.slice(newline + 1, closing) : '';
|
|
66
|
+
if (code.endsWith('\n')) code = code.slice(0, -1);
|
|
67
|
+
const escapedCode = escapeTelegramHtml(code);
|
|
68
|
+
chunks.push(/^[A-Za-z0-9_+-]+$/.test(language)
|
|
69
|
+
? protect(`<pre><code class="language-${language}">${escapedCode}</code></pre>`)
|
|
70
|
+
: protect(`<pre>${escapedCode}</pre>`));
|
|
71
|
+
cursor = closing + 3;
|
|
72
|
+
}
|
|
73
|
+
return chunks.join('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isSafeTelegramHref(value: string): boolean {
|
|
77
|
+
return /^(?:https?:\/\/|tg:\/\/|mailto:)[^\s<>]+$/iu.test(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function escapeTelegramHtml(value: string): string {
|
|
81
|
+
return value
|
|
82
|
+
.replace(/&/g, '&')
|
|
83
|
+
.replace(/</g, '<')
|
|
84
|
+
.replace(/>/g, '>')
|
|
85
|
+
.replace(/"/g, '"');
|
|
86
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { isMediaRef } from '@zhin.js/core';
|
|
|
8
8
|
import type { Segment } from '@zhin.js/core/runtime';
|
|
9
9
|
import type { ConversationKind, ConversationRef } from '@zhin.js/im-contract';
|
|
10
10
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
11
|
+
import { escapeTelegramHtml, markdownToTelegramHtml } from './markdown-to-html.js';
|
|
11
12
|
|
|
12
13
|
const logger = getLogger('telegram');
|
|
13
14
|
|
|
@@ -172,6 +173,7 @@ export type TelegramOutboundAction =
|
|
|
172
173
|
readonly params: {
|
|
173
174
|
readonly chat_id: number | string;
|
|
174
175
|
readonly text: string;
|
|
176
|
+
readonly parse_mode?: 'HTML';
|
|
175
177
|
readonly reply_parameters?: { readonly message_id: number };
|
|
176
178
|
readonly reply_markup?: { readonly inline_keyboard: TelegramInlineButton[][] };
|
|
177
179
|
};
|
|
@@ -182,6 +184,7 @@ export type TelegramOutboundAction =
|
|
|
182
184
|
readonly chat_id: number | string;
|
|
183
185
|
readonly photo: string;
|
|
184
186
|
readonly caption?: string;
|
|
187
|
+
readonly parse_mode?: 'HTML';
|
|
185
188
|
readonly reply_parameters?: { readonly message_id: number };
|
|
186
189
|
};
|
|
187
190
|
}
|
|
@@ -191,6 +194,7 @@ export type TelegramOutboundAction =
|
|
|
191
194
|
readonly chat_id: number | string;
|
|
192
195
|
readonly video: string;
|
|
193
196
|
readonly caption?: string;
|
|
197
|
+
readonly parse_mode?: 'HTML';
|
|
194
198
|
readonly reply_parameters?: { readonly message_id: number };
|
|
195
199
|
};
|
|
196
200
|
}
|
|
@@ -200,6 +204,7 @@ export type TelegramOutboundAction =
|
|
|
200
204
|
readonly chat_id: number | string;
|
|
201
205
|
readonly audio: string;
|
|
202
206
|
readonly caption?: string;
|
|
207
|
+
readonly parse_mode?: 'HTML';
|
|
203
208
|
readonly reply_parameters?: { readonly message_id: number };
|
|
204
209
|
};
|
|
205
210
|
}
|
|
@@ -209,6 +214,7 @@ export type TelegramOutboundAction =
|
|
|
209
214
|
readonly chat_id: number | string;
|
|
210
215
|
readonly voice: string;
|
|
211
216
|
readonly caption?: string;
|
|
217
|
+
readonly parse_mode?: 'HTML';
|
|
212
218
|
readonly reply_parameters?: { readonly message_id: number };
|
|
213
219
|
};
|
|
214
220
|
}
|
|
@@ -218,6 +224,7 @@ export type TelegramOutboundAction =
|
|
|
218
224
|
readonly chat_id: number | string;
|
|
219
225
|
readonly document: string;
|
|
220
226
|
readonly caption?: string;
|
|
227
|
+
readonly parse_mode?: 'HTML';
|
|
221
228
|
readonly reply_parameters?: { readonly message_id: number };
|
|
222
229
|
};
|
|
223
230
|
}
|
|
@@ -355,13 +362,6 @@ export function senderDisplayName(user?: TelegramUser): string {
|
|
|
355
362
|
export function formatInboundContent(msg: TelegramMessage): string {
|
|
356
363
|
if (msg.text) return msg.text;
|
|
357
364
|
if (msg.caption) return msg.caption;
|
|
358
|
-
if (msg.photo?.length) return '[image]';
|
|
359
|
-
if (msg.video) return '[video]';
|
|
360
|
-
if (msg.audio) return '[audio]';
|
|
361
|
-
if (msg.voice) return '[voice]';
|
|
362
|
-
if (msg.document) {
|
|
363
|
-
return msg.document.file_name ? `[file: ${msg.document.file_name}]` : '[file]';
|
|
364
|
-
}
|
|
365
365
|
if (msg.sticker) {
|
|
366
366
|
return msg.sticker.emoji ? `[sticker: ${msg.sticker.emoji}]` : '[sticker]';
|
|
367
367
|
}
|
|
@@ -376,7 +376,7 @@ export function formatCallbackContent(query: TelegramCallbackQuery): string {
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
/**
|
|
379
|
-
* 入站消息 → canonical Segment[]
|
|
379
|
+
* 入站消息 → canonical Segment[];媒体只存在于 canonical 段,不重复写入文本。
|
|
380
380
|
* Telegram 附件只有不透明 file_id(需 getFile 二次解析,非 URL),
|
|
381
381
|
* 统一进 MediaRef kind=file;photo 取数组末尾(最大尺寸)。
|
|
382
382
|
*/
|
|
@@ -518,6 +518,11 @@ function buildOutboundActions(
|
|
|
518
518
|
: payload && typeof payload === 'object' && 'type' in (payload as object)
|
|
519
519
|
? [payload as TelegramWireSegment]
|
|
520
520
|
: [];
|
|
521
|
+
const hasMarkdown = items.some((item) => typeof item !== 'string' && item.type === 'markdown');
|
|
522
|
+
const appendPlain = (value: unknown): string => hasMarkdown
|
|
523
|
+
? escapeTelegramHtml(String(value ?? ''))
|
|
524
|
+
: String(value ?? '');
|
|
525
|
+
const htmlMode = hasMarkdown ? { parse_mode: 'HTML' as const } : {};
|
|
521
526
|
|
|
522
527
|
if (items.length === 0) {
|
|
523
528
|
const text = payload == null
|
|
@@ -583,16 +588,19 @@ function buildOutboundActions(
|
|
|
583
588
|
|
|
584
589
|
for (const item of items) {
|
|
585
590
|
if (typeof item === 'string') {
|
|
586
|
-
textContent += item;
|
|
591
|
+
textContent += appendPlain(item);
|
|
587
592
|
continue;
|
|
588
593
|
}
|
|
589
594
|
const data = item.data ?? {};
|
|
590
595
|
switch (item.type) {
|
|
591
596
|
case 'text':
|
|
592
|
-
textContent +=
|
|
597
|
+
textContent += appendPlain(data.text ?? data.content ?? '');
|
|
598
|
+
break;
|
|
599
|
+
case 'markdown':
|
|
600
|
+
textContent += markdownToTelegramHtml(String(data.content ?? data.text ?? ''));
|
|
593
601
|
break;
|
|
594
602
|
case 'at':
|
|
595
|
-
if (data.id) textContent += `@${
|
|
603
|
+
if (data.id) textContent += `@${appendPlain(data.name || data.id)}`;
|
|
596
604
|
break;
|
|
597
605
|
case 'reply': {
|
|
598
606
|
const id = Number(data.id ?? data.message_id);
|
|
@@ -624,6 +632,7 @@ function buildOutboundActions(
|
|
|
624
632
|
chat_id: chatId,
|
|
625
633
|
photo,
|
|
626
634
|
caption: textContent.trim() || undefined,
|
|
635
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
627
636
|
...replyParams(),
|
|
628
637
|
},
|
|
629
638
|
});
|
|
@@ -640,6 +649,7 @@ function buildOutboundActions(
|
|
|
640
649
|
chat_id: chatId,
|
|
641
650
|
video,
|
|
642
651
|
caption: textContent.trim() || undefined,
|
|
652
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
643
653
|
...replyParams(),
|
|
644
654
|
},
|
|
645
655
|
});
|
|
@@ -656,6 +666,7 @@ function buildOutboundActions(
|
|
|
656
666
|
chat_id: chatId,
|
|
657
667
|
audio,
|
|
658
668
|
caption: textContent.trim() || undefined,
|
|
669
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
659
670
|
...replyParams(),
|
|
660
671
|
},
|
|
661
672
|
});
|
|
@@ -672,6 +683,7 @@ function buildOutboundActions(
|
|
|
672
683
|
chat_id: chatId,
|
|
673
684
|
voice,
|
|
674
685
|
caption: textContent.trim() || undefined,
|
|
686
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
675
687
|
...replyParams(),
|
|
676
688
|
},
|
|
677
689
|
});
|
|
@@ -688,6 +700,7 @@ function buildOutboundActions(
|
|
|
688
700
|
chat_id: chatId,
|
|
689
701
|
document,
|
|
690
702
|
caption: textContent.trim() || undefined,
|
|
703
|
+
...(textContent.trim() ? htmlMode : {}),
|
|
691
704
|
...replyParams(),
|
|
692
705
|
},
|
|
693
706
|
});
|
|
@@ -718,7 +731,7 @@ function buildOutboundActions(
|
|
|
718
731
|
break;
|
|
719
732
|
}
|
|
720
733
|
default:
|
|
721
|
-
textContent +=
|
|
734
|
+
textContent += appendPlain(data.text ?? `[${item.type}]`);
|
|
722
735
|
}
|
|
723
736
|
}
|
|
724
737
|
|
|
@@ -730,6 +743,7 @@ function buildOutboundActions(
|
|
|
730
743
|
params: {
|
|
731
744
|
chat_id: chatId,
|
|
732
745
|
text: text || ' ',
|
|
746
|
+
...htmlMode,
|
|
733
747
|
...replyParams(),
|
|
734
748
|
...(keyboard ? { reply_markup: { inline_keyboard: keyboard } } : {}),
|
|
735
749
|
},
|
|
@@ -742,6 +756,7 @@ function buildOutboundActions(
|
|
|
742
756
|
params: {
|
|
743
757
|
chat_id: chatId,
|
|
744
758
|
text: textContent.trim() || ' ',
|
|
759
|
+
...htmlMode,
|
|
745
760
|
...replyParams(),
|
|
746
761
|
...(keyboard ? { reply_markup: { inline_keyboard: keyboard } } : {}),
|
|
747
762
|
},
|