@zhin.js/adapter-telegram 1.1.20 → 1.1.22
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 +76 -0
- package/README.md +28 -0
- package/adapters/telegram/index.js +20 -5
- package/adapters/telegram/index.ts +20 -5
- package/lib/adapter.js +1 -1
- package/lib/console-spec.js +2 -0
- package/lib/endpoint.js +11 -0
- package/lib/markdown-to-html.d.ts +5 -7
- package/lib/markdown-to-html.js +90 -51
- package/lib/protocol.d.ts +7 -1
- package/lib/protocol.js +33 -31
- package/lib/self-profile.d.ts +11 -0
- package/lib/self-profile.js +61 -0
- package/lib/stream-markdown.d.ts +6 -0
- package/lib/stream-markdown.js +66 -0
- package/lib/streaming.d.ts +4 -0
- package/lib/streaming.js +118 -0
- package/package.json +22 -21
- package/plugin.js +1 -0
- package/schema.json +126 -1
- package/src/adapter.ts +1 -1
- package/src/console-spec.ts +2 -0
- package/src/endpoint.ts +10 -0
- package/src/markdown-to-html.ts +104 -64
- package/src/protocol.ts +33 -28
- package/src/self-profile.ts +58 -0
- package/src/stream-markdown.ts +67 -0
- package/src/streaming.ts +110 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { TelegramApiError } from './api-error.js';
|
|
2
|
+
/** Bot API names/descriptions are account fields, not the title/description of a chat. */
|
|
3
|
+
export function createTelegramSelfProfile({ client, current, nicknameChanged }) {
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
writableFields: Object.freeze(['nickname', 'signature', 'bio', 'avatar']),
|
|
6
|
+
get current() { return current(); },
|
|
7
|
+
async read({ signal }) {
|
|
8
|
+
const [name, description, shortDescription] = await Promise.all([
|
|
9
|
+
client.callApi('getMyName', {}, signal),
|
|
10
|
+
client.callApi('getMyDescription', {}, signal),
|
|
11
|
+
client.callApi('getMyShortDescription', {}, signal),
|
|
12
|
+
]);
|
|
13
|
+
if (typeof name.name !== 'string' || typeof description.description !== 'string' || typeof shortDescription.short_description !== 'string') {
|
|
14
|
+
throw new Error('Telegram returned an incomplete bot profile');
|
|
15
|
+
}
|
|
16
|
+
return { nickname: name.name, signature: shortDescription.short_description, bio: description.description };
|
|
17
|
+
},
|
|
18
|
+
async write({ change, signal }) {
|
|
19
|
+
try {
|
|
20
|
+
if (change.field === 'avatar') {
|
|
21
|
+
if (change.value.kind !== 'base64' || change.value.mime_type !== 'image/jpeg') {
|
|
22
|
+
return { status: 'rejected', message: 'Telegram profile photos require resolved JPEG image bytes' };
|
|
23
|
+
}
|
|
24
|
+
const form = new FormData();
|
|
25
|
+
form.set('photo', JSON.stringify({ type: 'static', photo: 'attach://avatar' }));
|
|
26
|
+
form.set('avatar', new Blob([Uint8Array.from(Buffer.from(change.value.value, 'base64'))], { type: 'image/jpeg' }), 'avatar.jpg');
|
|
27
|
+
const accepted = await client.callApiForm('setMyProfilePhoto', form, signal);
|
|
28
|
+
return accepted === true ? { status: 'updated' } : { status: 'rejected', message: 'Telegram rejected the avatar change' };
|
|
29
|
+
}
|
|
30
|
+
let method;
|
|
31
|
+
let params;
|
|
32
|
+
switch (change.field) {
|
|
33
|
+
case 'nickname':
|
|
34
|
+
method = 'setMyName';
|
|
35
|
+
params = { name: change.value };
|
|
36
|
+
break;
|
|
37
|
+
case 'signature':
|
|
38
|
+
method = 'setMyShortDescription';
|
|
39
|
+
params = { short_description: change.value };
|
|
40
|
+
break;
|
|
41
|
+
case 'bio':
|
|
42
|
+
method = 'setMyDescription';
|
|
43
|
+
params = { description: change.value };
|
|
44
|
+
break;
|
|
45
|
+
default: return { status: 'rejected', message: `Telegram bot profile does not expose ${change.field} through this port` };
|
|
46
|
+
}
|
|
47
|
+
const accepted = await client.callApi(method, params, signal);
|
|
48
|
+
if (accepted !== true)
|
|
49
|
+
return { status: 'rejected', message: `Telegram rejected the ${change.field} change` };
|
|
50
|
+
if (change.field === 'nickname')
|
|
51
|
+
nicknameChanged(change.value);
|
|
52
|
+
return { status: 'updated', profile: { [change.field]: change.value } };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!(error instanceof TelegramApiError) || error.disposition === 'not_sent')
|
|
56
|
+
throw error;
|
|
57
|
+
return { status: error.disposition === 'rejected' ? 'rejected' : 'unknown', message: error.message };
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Render a physical slice while retaining the original Markdown's source coordinates. */
|
|
2
|
+
export declare function sliceTelegramMarkdown(text: string, precedingText: string, capacity: number): {
|
|
3
|
+
readonly html: string;
|
|
4
|
+
readonly acceptedText: string;
|
|
5
|
+
readonly exhausted: boolean;
|
|
6
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
2
|
+
import { projectTelegramMarkdown } from './markdown-to-html.js';
|
|
3
|
+
/** Render a physical slice while retaining the original Markdown's source coordinates. */
|
|
4
|
+
export function sliceTelegramMarkdown(text, precedingText, capacity) {
|
|
5
|
+
const full = precedingText + text;
|
|
6
|
+
const projection = projectTelegramMarkdown(full);
|
|
7
|
+
const units = [];
|
|
8
|
+
const tags = [];
|
|
9
|
+
const trimStart = projection.html.length - projection.html.trimStart().length;
|
|
10
|
+
const trimEnd = projection.html.trimEnd().length;
|
|
11
|
+
const html = projection.html.slice(trimStart, trimEnd);
|
|
12
|
+
const matcher = /<\/?[a-z][^>]*>|&(?:amp|lt|gt|quot);|[\s\S]/gu;
|
|
13
|
+
for (const match of html.matchAll(matcher)) {
|
|
14
|
+
const raw = match[0];
|
|
15
|
+
if (raw.startsWith('<')) {
|
|
16
|
+
const tag = /^<(\/?)([a-z]+)/.exec(raw);
|
|
17
|
+
if (tag[1]) {
|
|
18
|
+
if (tags.at(-1)?.name !== tag[2])
|
|
19
|
+
throw unsupportedProjection();
|
|
20
|
+
tags.pop();
|
|
21
|
+
}
|
|
22
|
+
else
|
|
23
|
+
tags.push({ name: tag[2], html: raw });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const origins = projection.source.slice(trimStart + match.index, trimStart + match.index + raw.length).filter(position => position >= 0);
|
|
27
|
+
if (!origins.length)
|
|
28
|
+
throw unsupportedProjection();
|
|
29
|
+
const sourceStart = Math.min(...origins), sourceEnd = Math.max(...origins) + 1;
|
|
30
|
+
if (sourceEnd > precedingText.length)
|
|
31
|
+
units.push({ html: raw, sourceStart, sourceEnd, tags: [...tags] });
|
|
32
|
+
}
|
|
33
|
+
if (tags.length)
|
|
34
|
+
throw unsupportedProjection();
|
|
35
|
+
if (!units.length)
|
|
36
|
+
return { html: '', acceptedText: text, exhausted: false };
|
|
37
|
+
// One decoded code point is one unit. Keep replacement expansions together
|
|
38
|
+
// when their source positions coincide (for example a Markdown bullet).
|
|
39
|
+
let take = Math.min(capacity, units.length);
|
|
40
|
+
while (take > 0 && take < units.length && units[take].sourceStart < units[take - 1].sourceEnd)
|
|
41
|
+
take--;
|
|
42
|
+
if (take <= 0)
|
|
43
|
+
throw new EndpointDeliveryError('stream_capacity', 'Telegram prefix leaves no room for a Markdown unit', 'not_sent');
|
|
44
|
+
const end = take < units.length ? units[take].sourceStart : full.length;
|
|
45
|
+
if (end <= precedingText.length)
|
|
46
|
+
throw unsupportedProjection();
|
|
47
|
+
let rendered = '';
|
|
48
|
+
let active = [];
|
|
49
|
+
for (const unit of units.slice(0, take)) {
|
|
50
|
+
let common = 0;
|
|
51
|
+
while (common < active.length && common < unit.tags.length && active[common].html === unit.tags[common].html)
|
|
52
|
+
common++;
|
|
53
|
+
for (let index = active.length - 1; index >= common; index--)
|
|
54
|
+
rendered += `</${active[index].name}>`;
|
|
55
|
+
for (const tag of unit.tags.slice(common))
|
|
56
|
+
rendered += tag.html;
|
|
57
|
+
rendered += unit.html;
|
|
58
|
+
active = unit.tags;
|
|
59
|
+
}
|
|
60
|
+
for (let index = active.length - 1; index >= 0; index--)
|
|
61
|
+
rendered += `</${active[index].name}>`;
|
|
62
|
+
return { html: rendered, acceptedText: text.slice(0, end - precedingText.length), exhausted: take < units.length || take === capacity };
|
|
63
|
+
}
|
|
64
|
+
function unsupportedProjection() {
|
|
65
|
+
return new EndpointDeliveryError('unsupported_operation', 'Telegram Markdown projection has no safe source mapping', 'not_sent');
|
|
66
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EndpointStreaming } from 'zhin.js/adapter';
|
|
2
|
+
import type { TelegramClientApi } from './telegram-client.js';
|
|
3
|
+
/** Drafts are previews; only finish performs a persistent send. */
|
|
4
|
+
export declare function createTelegramStreaming(getClient: () => Pick<TelegramClientApi, 'callApi'>, minIntervalMs?: number): EndpointStreaming;
|
package/lib/streaming.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { randomInt } from 'node:crypto';
|
|
2
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
3
|
+
import { formatOutboundPlan } from './protocol.js';
|
|
4
|
+
import { escapeTelegramHtml } from './markdown-to-html.js';
|
|
5
|
+
import { sliceTelegramMarkdown } from './stream-markdown.js';
|
|
6
|
+
function supportsTelegramStream(request) {
|
|
7
|
+
return request.conversation.kind === 'private' && !request.conversation.parent;
|
|
8
|
+
}
|
|
9
|
+
/** Drafts are previews; only finish performs a persistent send. */
|
|
10
|
+
export function createTelegramStreaming(getClient, minIntervalMs = 500) {
|
|
11
|
+
return {
|
|
12
|
+
mode: 'draft',
|
|
13
|
+
minIntervalMs,
|
|
14
|
+
supports: supportsTelegramStream,
|
|
15
|
+
open(request) {
|
|
16
|
+
if (!supportsTelegramStream(request))
|
|
17
|
+
return undefined;
|
|
18
|
+
if (request.prefix?.some(segment => segment.type !== 'reply' && segment.type !== 'mention'))
|
|
19
|
+
return undefined;
|
|
20
|
+
const prefix = (request.prefix ?? []).map(segment => segment.type === 'mention'
|
|
21
|
+
? { type: 'at', data: { id: 'target' in segment.data ? segment.data.target : undefined,
|
|
22
|
+
name: 'name' in segment.data ? segment.data.name : undefined } }
|
|
23
|
+
: { type: segment.type, data: { ...segment.data } });
|
|
24
|
+
const draftId = randomInt(1, 2_147_483_647);
|
|
25
|
+
const cancellation = new AbortController();
|
|
26
|
+
const signal = request.signal ? AbortSignal.any([request.signal, cancellation.signal]) : cancellation.signal;
|
|
27
|
+
const threadId = request.conversation.threadId === undefined ? undefined : Number(request.conversation.threadId);
|
|
28
|
+
if (threadId !== undefined && (!Number.isSafeInteger(threadId) || threadId <= 0)) {
|
|
29
|
+
throw new EndpointDeliveryError('invalid_payload', 'Telegram topic id must be a positive integer', 'not_sent');
|
|
30
|
+
}
|
|
31
|
+
let acceptedText = '';
|
|
32
|
+
let messageId;
|
|
33
|
+
let exhausted = false;
|
|
34
|
+
let contentUnavailable = false;
|
|
35
|
+
let confirmed;
|
|
36
|
+
let failure;
|
|
37
|
+
let completion;
|
|
38
|
+
const acknowledgement = () => ({
|
|
39
|
+
status: messageId ? 'sent' : acceptedText ? 'preview' : 'buffered', acceptedText,
|
|
40
|
+
...(messageId ? { messageId } : {}),
|
|
41
|
+
...(exhausted ? { exhausted: true } : {}),
|
|
42
|
+
...(contentUnavailable ? { contentUnavailable: true } : {}),
|
|
43
|
+
});
|
|
44
|
+
const prepare = (text) => {
|
|
45
|
+
const mentionText = prefix.map(segment => segment.type === 'at' && segment.data?.id
|
|
46
|
+
? `@${segment.data.name || segment.data.id}` : '').join('');
|
|
47
|
+
const mentionLength = Array.from(mentionText).length;
|
|
48
|
+
const capacity = 4096 - mentionLength;
|
|
49
|
+
if (capacity <= 0)
|
|
50
|
+
throw new EndpointDeliveryError('stream_capacity', 'Telegram prefix exceeds its text container', 'not_sent');
|
|
51
|
+
const markdown = request.format === 'markdown'
|
|
52
|
+
? sliceTelegramMarkdown(text, request.continuation?.precedingText ?? '', capacity) : undefined;
|
|
53
|
+
const source = markdown?.acceptedText ?? Array.from(text).slice(0, capacity).join('');
|
|
54
|
+
const plan = formatOutboundPlan(request.conversation.id, [...prefix, request.format === 'markdown'
|
|
55
|
+
? { type: 'markdown', data: { content: source } }
|
|
56
|
+
: { type: 'text', data: { text: source } }]);
|
|
57
|
+
const action = plan.actions[0];
|
|
58
|
+
if (plan.actions.length !== 1 || action?.method !== 'sendMessage') {
|
|
59
|
+
throw new EndpointDeliveryError('unsupported_operation', 'Telegram draft requires one text container', 'not_sent');
|
|
60
|
+
}
|
|
61
|
+
const params = { ...action.params,
|
|
62
|
+
...(markdown ? { text: escapeTelegramHtml(mentionText) + markdown.html } : {}),
|
|
63
|
+
...(threadId !== undefined ? { message_thread_id: threadId } : {}) };
|
|
64
|
+
return { source, params, exhausted: markdown?.exhausted ?? (source.length < text.length || Array.from(params.text).length === 4096) };
|
|
65
|
+
};
|
|
66
|
+
const send = async (text, final) => {
|
|
67
|
+
if (failure)
|
|
68
|
+
throw failure;
|
|
69
|
+
if (signal.aborted)
|
|
70
|
+
throw new EndpointDeliveryError('endpoint_inactive', 'Telegram draft cancelled', 'not_sent');
|
|
71
|
+
if (!text.startsWith(acceptedText))
|
|
72
|
+
throw new EndpointDeliveryError('invalid_payload', 'Telegram draft cannot replace its accepted source prefix', 'not_sent');
|
|
73
|
+
if (!text.trim() || (!final && text === acceptedText))
|
|
74
|
+
return acknowledgement();
|
|
75
|
+
const next = final && text === acceptedText && confirmed ? confirmed : prepare(text);
|
|
76
|
+
const { source, params } = next;
|
|
77
|
+
if (!final && source === acceptedText)
|
|
78
|
+
return acknowledgement();
|
|
79
|
+
if (!params.text)
|
|
80
|
+
return acknowledgement();
|
|
81
|
+
try {
|
|
82
|
+
if (final) {
|
|
83
|
+
const result = await getClient().callApi('sendMessage', params, signal);
|
|
84
|
+
if (!Number.isSafeInteger(result?.message_id) || result.message_id <= 0) {
|
|
85
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', 'Telegram final send returned no valid message_id', 'unknown');
|
|
86
|
+
}
|
|
87
|
+
messageId = String(result.message_id);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const { reply_parameters: _reply, ...draftParams } = params;
|
|
91
|
+
const result = await getClient().callApi('sendMessageDraft', { ...draftParams, draft_id: draftId }, signal);
|
|
92
|
+
if (result !== true)
|
|
93
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', 'Telegram draft response is unconfirmed', 'unknown');
|
|
94
|
+
}
|
|
95
|
+
acceptedText = source;
|
|
96
|
+
// A temporary Markdown preview can still reinterpret an unfinished fence/link.
|
|
97
|
+
// Rollover persists this container, so only the final source may trigger it.
|
|
98
|
+
exhausted = next.exhausted && (final || request.format !== 'markdown');
|
|
99
|
+
contentUnavailable = request.format === 'markdown'
|
|
100
|
+
&& (Boolean(request.continuation?.precedingText) || exhausted || source.length < text.length);
|
|
101
|
+
confirmed = next;
|
|
102
|
+
return acknowledgement();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
failure = error;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
write: text => completion
|
|
111
|
+
? Promise.reject(new EndpointDeliveryError('stream_closed', 'Telegram draft is finishing', 'not_sent'))
|
|
112
|
+
: send(text, false),
|
|
113
|
+
finish: text => completion ??= send(text, true),
|
|
114
|
+
cancel: () => { cancellation.abort(); },
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-telegram",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.22",
|
|
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",
|
|
@@ -31,26 +31,26 @@
|
|
|
31
31
|
"directory": "plugins/adapters/telegram"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@zhin.js/adapter": "1.1.
|
|
35
|
-
"@zhin.js/agent-feature": "1.1.
|
|
36
|
-
"@zhin.js/client": "1.1.
|
|
34
|
+
"@zhin.js/adapter": "1.1.28",
|
|
35
|
+
"@zhin.js/agent-feature": "1.1.13",
|
|
36
|
+
"@zhin.js/client": "1.1.24",
|
|
37
37
|
"@zhin.js/console-contract": "1.1.3",
|
|
38
|
-
"@zhin.js/core": "1.1.
|
|
39
|
-
"@zhin.js/feature-kit": "1.1.
|
|
40
|
-
"@zhin.js/host-http": "1.1.
|
|
41
|
-
"@zhin.js/im-contract": "1.1.
|
|
38
|
+
"@zhin.js/core": "1.1.56",
|
|
39
|
+
"@zhin.js/feature-kit": "1.1.12",
|
|
40
|
+
"@zhin.js/host-http": "1.1.14",
|
|
41
|
+
"@zhin.js/im-contract": "1.1.11",
|
|
42
42
|
"@zhin.js/logger": "1.1.2",
|
|
43
|
-
"@zhin.js/page": "1.1.
|
|
44
|
-
"@zhin.js/skill": "1.1.
|
|
45
|
-
"@zhin.js/tool": "1.1.
|
|
43
|
+
"@zhin.js/page": "1.1.14",
|
|
44
|
+
"@zhin.js/skill": "1.1.13",
|
|
45
|
+
"@zhin.js/tool": "1.1.15"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"zod": "^4.0.0",
|
|
49
|
-
"@zhin.js/adapter": "^1.1.
|
|
50
|
-
"@zhin.js/command": "^1.1.
|
|
51
|
-
"@zhin.js/core": "^1.1.
|
|
52
|
-
"@zhin.js/permission": "^1.1.
|
|
53
|
-
"zhin.js": "^1.1.
|
|
49
|
+
"@zhin.js/adapter": "^1.1.28",
|
|
50
|
+
"@zhin.js/command": "^1.1.14",
|
|
51
|
+
"@zhin.js/core": "^1.1.56",
|
|
52
|
+
"@zhin.js/permission": "^1.1.12",
|
|
53
|
+
"zhin.js": "^1.1.21"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"@zhin.js/command": {
|
|
@@ -65,12 +65,13 @@
|
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@types/node": "^26.1.2",
|
|
68
|
+
"htmlparser2": "12.0.0",
|
|
68
69
|
"typescript": "^6.0.3",
|
|
69
70
|
"vitest": "^4.1.10",
|
|
70
71
|
"zod": "^4.4.3",
|
|
71
|
-
"@zhin.js/host-http": "1.1.
|
|
72
|
-
"@zhin.js/pagemanager": "1.1.
|
|
73
|
-
"zhin.js": "1.1.
|
|
72
|
+
"@zhin.js/host-http": "1.1.14",
|
|
73
|
+
"@zhin.js/pagemanager": "1.1.21",
|
|
74
|
+
"zhin.js": "1.1.21"
|
|
74
75
|
},
|
|
75
76
|
"files": [
|
|
76
77
|
"adapters",
|
|
@@ -102,7 +103,7 @@
|
|
|
102
103
|
"features": [
|
|
103
104
|
{
|
|
104
105
|
"package": "@zhin.js/adapter",
|
|
105
|
-
"api": "^2.
|
|
106
|
+
"api": "^2.1.0"
|
|
106
107
|
},
|
|
107
108
|
{
|
|
108
109
|
"package": "@zhin.js/command",
|
|
@@ -110,7 +111,7 @@
|
|
|
110
111
|
},
|
|
111
112
|
{
|
|
112
113
|
"package": "@zhin.js/tool",
|
|
113
|
-
"api": "^1.
|
|
114
|
+
"api": "^1.1.0"
|
|
114
115
|
},
|
|
115
116
|
{
|
|
116
117
|
"package": "@zhin.js/skill",
|
package/plugin.js
CHANGED
|
@@ -5,6 +5,7 @@ import { checkTelegramPlatformPermit } from "./lib/platform-permit.js";
|
|
|
5
5
|
import { createTelegramConsoleRpc } from "./lib/console.js";
|
|
6
6
|
export default definePlugin({
|
|
7
7
|
name: 'telegram',
|
|
8
|
+
configUpdate: { featurePaths: [['nickname'], ['profile'], ['endpoints']] },
|
|
8
9
|
metadata: {
|
|
9
10
|
displayName: 'Telegram Bot API Adapter',
|
|
10
11
|
},
|
package/schema.json
CHANGED
|
@@ -141,6 +141,36 @@
|
|
|
141
141
|
"id": {
|
|
142
142
|
"type": "string",
|
|
143
143
|
"description": "Telegram bot name"
|
|
144
|
+
},
|
|
145
|
+
"streaming": {
|
|
146
|
+
"type": "boolean",
|
|
147
|
+
"description": "Publish supported replies while generating. Set false to send only complete replies."
|
|
148
|
+
},
|
|
149
|
+
"streamingIntervalMs": {
|
|
150
|
+
"type": "number",
|
|
151
|
+
"minimum": 0,
|
|
152
|
+
"description": "Minimum delay in milliseconds between reply updates; the first update is immediate."
|
|
153
|
+
},
|
|
154
|
+
"nickname": {
|
|
155
|
+
"type": [
|
|
156
|
+
"string",
|
|
157
|
+
"null"
|
|
158
|
+
],
|
|
159
|
+
"description": "账号昵称兜底",
|
|
160
|
+
"x-i18n": {
|
|
161
|
+
"en": {
|
|
162
|
+
"description": "Fallback account nickname"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"profile": {
|
|
167
|
+
"$ref": "#/$defs/selfProfileFallback",
|
|
168
|
+
"description": "账号资料兜底",
|
|
169
|
+
"x-i18n": {
|
|
170
|
+
"en": {
|
|
171
|
+
"description": "Fallback account profile"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
144
174
|
}
|
|
145
175
|
},
|
|
146
176
|
"required": [
|
|
@@ -163,9 +193,104 @@
|
|
|
163
193
|
"description": "Command prefix. The default empty string allows unprefixed commands; '/' requires a leading slash. Each endpoints[i] can override it."
|
|
164
194
|
}
|
|
165
195
|
}
|
|
196
|
+
},
|
|
197
|
+
"streaming": {
|
|
198
|
+
"type": "boolean",
|
|
199
|
+
"default": true,
|
|
200
|
+
"description": "Publish supported replies while generating. Set false to send only complete replies."
|
|
201
|
+
},
|
|
202
|
+
"streamingIntervalMs": {
|
|
203
|
+
"type": "number",
|
|
204
|
+
"minimum": 0,
|
|
205
|
+
"default": 500,
|
|
206
|
+
"description": "Minimum delay in milliseconds between reply updates; the first update is immediate."
|
|
207
|
+
},
|
|
208
|
+
"nickname": {
|
|
209
|
+
"type": [
|
|
210
|
+
"string",
|
|
211
|
+
"null"
|
|
212
|
+
],
|
|
213
|
+
"description": "账号昵称兜底",
|
|
214
|
+
"x-i18n": {
|
|
215
|
+
"en": {
|
|
216
|
+
"description": "Fallback account nickname"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
"profile": {
|
|
221
|
+
"$ref": "#/$defs/selfProfileFallback",
|
|
222
|
+
"description": "账号资料兜底",
|
|
223
|
+
"x-i18n": {
|
|
224
|
+
"en": {
|
|
225
|
+
"description": "Fallback account profile"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
166
228
|
}
|
|
167
229
|
},
|
|
168
230
|
"required": [
|
|
169
231
|
"endpoints"
|
|
170
|
-
]
|
|
232
|
+
],
|
|
233
|
+
"$defs": {
|
|
234
|
+
"selfProfileFallback": {
|
|
235
|
+
"type": "object",
|
|
236
|
+
"additionalProperties": false,
|
|
237
|
+
"properties": {
|
|
238
|
+
"gender": {
|
|
239
|
+
"type": [
|
|
240
|
+
"string",
|
|
241
|
+
"null"
|
|
242
|
+
],
|
|
243
|
+
"enum": [
|
|
244
|
+
"male",
|
|
245
|
+
"female",
|
|
246
|
+
"other",
|
|
247
|
+
"unknown",
|
|
248
|
+
null
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
"age": {
|
|
252
|
+
"type": [
|
|
253
|
+
"integer",
|
|
254
|
+
"null"
|
|
255
|
+
],
|
|
256
|
+
"minimum": 0
|
|
257
|
+
},
|
|
258
|
+
"birthday": {
|
|
259
|
+
"type": [
|
|
260
|
+
"string",
|
|
261
|
+
"null"
|
|
262
|
+
],
|
|
263
|
+
"pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$"
|
|
264
|
+
},
|
|
265
|
+
"signature": {
|
|
266
|
+
"type": [
|
|
267
|
+
"string",
|
|
268
|
+
"null"
|
|
269
|
+
]
|
|
270
|
+
},
|
|
271
|
+
"avatar": {
|
|
272
|
+
"type": [
|
|
273
|
+
"string",
|
|
274
|
+
"null"
|
|
275
|
+
],
|
|
276
|
+
"pattern": "^(https?://[^\\s/?#]+(?:[/?#].*)?|)$"
|
|
277
|
+
},
|
|
278
|
+
"interests": {
|
|
279
|
+
"type": [
|
|
280
|
+
"array",
|
|
281
|
+
"null"
|
|
282
|
+
],
|
|
283
|
+
"items": {
|
|
284
|
+
"type": "string"
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
"bio": {
|
|
288
|
+
"type": [
|
|
289
|
+
"string",
|
|
290
|
+
"null"
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
171
296
|
}
|
package/src/adapter.ts
CHANGED
|
@@ -10,7 +10,7 @@ export class TelegramAdapter extends Adapter<TelegramEndpoint, TelegramEndpointC
|
|
|
10
10
|
private readonly transport: Pick<TelegramEndpointOptions, 'fetch' | 'requestTimeoutMs'> = {}) { super(); }
|
|
11
11
|
override get configurationIdentityFields(): readonly string[] { return ['id']; }
|
|
12
12
|
override get configurationIdentityProbeFields(): readonly string[] { return ['token', 'apiBaseUrl']; }
|
|
13
|
-
override get configurationUpdateFields(): readonly string[] { return ['token', 'polling', 'webhook',
|
|
13
|
+
override get configurationUpdateFields(): readonly string[] { return ['token', 'polling', 'streaming', 'streamingIntervalMs', 'webhook',
|
|
14
14
|
'webhook.domain', 'webhook.path', 'webhook.secretToken', 'allowedUpdates', 'apiBaseUrl']; }
|
|
15
15
|
protected override validateConfiguration(config: TelegramEndpointConfig): void { resolveTelegramConfig(config); }
|
|
16
16
|
protected override async identifyConfiguration(input: TelegramEndpointConfig, signal: AbortSignal): Promise<string> {
|
package/src/console-spec.ts
CHANGED
|
@@ -10,6 +10,8 @@ export const telegramConsoleSpec = {
|
|
|
10
10
|
{ key: 'token', required: true, env: true, description: 'Telegram bot token' },
|
|
11
11
|
{ key: 'apiBaseUrl', description: 'Telegram Bot API URL' },
|
|
12
12
|
{ key: 'polling', type: 'boolean', description: 'Use long polling; false selects webhook mode' },
|
|
13
|
+
{ key: 'streaming', type: 'boolean', description: 'Show private-chat drafts while generating (default true)' },
|
|
14
|
+
{ key: 'streamingIntervalMs', type: 'number', description: 'Delay between draft updates in milliseconds (default 500)' },
|
|
13
15
|
{ key: 'webhook.domain', description: 'Public webhook origin' },
|
|
14
16
|
{ key: 'webhook.path', description: 'Local webhook route' },
|
|
15
17
|
{ key: 'webhook.secretToken', env: true, description: 'Webhook secret token' },
|
package/src/endpoint.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TelegramClient, type TelegramFetch } from './telegram-client.js';
|
|
2
|
+
import { createTelegramStreaming } from './streaming.js';
|
|
2
3
|
import {
|
|
3
4
|
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification, createEndpointLifecycle,
|
|
4
5
|
type EndpointLifecycle, type EndpointContentPort, type EndpointContentResolveContext,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
type TelegramUpdate,
|
|
41
42
|
} from './protocol.js';
|
|
42
43
|
import { registerTelegramWebhookRoutes } from './webhook.js';
|
|
44
|
+
import { createTelegramSelfProfile } from './self-profile.js';
|
|
43
45
|
|
|
44
46
|
const CHAT_MEMBER_CACHE_TTL_MS = 60_000;
|
|
45
47
|
const CHAT_MEMBER_CACHE_MAX = 2_000;
|
|
@@ -93,6 +95,7 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
93
95
|
readonly #inflightUpdates = new Map<number, Promise<void>>();
|
|
94
96
|
#botUserId?: number;
|
|
95
97
|
#botUsername?: string;
|
|
98
|
+
#botName?: string;
|
|
96
99
|
readonly #chatMemberCache = new Map<string, ChatMemberPermit>();
|
|
97
100
|
readonly #control: EndpointControl = Object.freeze<EndpointControl>({
|
|
98
101
|
recall: ({ message, signal }) => this.recallMessage(message, signal),
|
|
@@ -112,6 +115,11 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
112
115
|
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
|
|
113
116
|
get name() { return options.config.id; }, get transportState() { return endpoint.#transportState; },
|
|
114
117
|
control: this.#control, content: this.#content,
|
|
118
|
+
streaming: options.config.streaming !== false ? createTelegramStreaming(() => this, options.config.streamingIntervalMs) : undefined,
|
|
119
|
+
profile: createTelegramSelfProfile({ client: this,
|
|
120
|
+
current: () => this.#botName === undefined ? {} : { nickname: this.#botName },
|
|
121
|
+
nicknameChanged: nickname => { this.#botName = nickname; },
|
|
122
|
+
}),
|
|
115
123
|
});
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -168,6 +176,8 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
168
176
|
if (abort.signal.aborted) return;
|
|
169
177
|
this.#botUserId = me.id;
|
|
170
178
|
this.#botUsername = me.username;
|
|
179
|
+
this.#botName = me.first_name;
|
|
180
|
+
if (me.id) this[endpointSpec].reportSelfUserId(String(me.id));
|
|
171
181
|
|
|
172
182
|
if (this.#options.config.mode === 'webhook') {
|
|
173
183
|
if (!this.#options.http) {
|