@zhin.js/adapter-telegram 1.1.18 → 1.1.20

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.
@@ -0,0 +1,79 @@
1
+ import { type ResolvedTelegramConfig, type TelegramChatMember } from './protocol.js';
2
+ export type TelegramFetch = (url: string, init?: {
3
+ readonly method?: string;
4
+ readonly headers?: Record<string, string>;
5
+ readonly body?: string | FormData;
6
+ readonly signal?: AbortSignal;
7
+ }) => Promise<{
8
+ readonly ok: boolean;
9
+ readonly status: number;
10
+ text(): Promise<string>;
11
+ json(): Promise<unknown>;
12
+ arrayBuffer?(): Promise<ArrayBuffer>;
13
+ readonly headers?: {
14
+ get(name: string): string | null;
15
+ };
16
+ }>;
17
+ export interface TelegramClientApi {
18
+ callApi<T = unknown>(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
19
+ callApiForm<T = unknown>(method: string, form: FormData, signal?: AbortSignal): Promise<T>;
20
+ pinMessage(chatId: number, messageId: number): Promise<boolean>;
21
+ unpinMessage(chatId: number, messageId?: number): Promise<boolean>;
22
+ setChatDescription(chatId: number, description: string): Promise<boolean>;
23
+ setMessageReaction(chatId: number, messageId: number, reaction: string): Promise<boolean>;
24
+ getChatMemberCount(chatId: number): Promise<number>;
25
+ getChatAdmins(chatId: number): Promise<TelegramChatMember[]>;
26
+ sendStickerMessage(chatId: number, sticker: string): Promise<{
27
+ message_id: number;
28
+ }>;
29
+ setChatPermissionsAll(chatId: number, permissions: Record<string, boolean | undefined>): Promise<boolean>;
30
+ createInviteLink(chatId: number): Promise<string>;
31
+ sendPoll(request: TelegramPollRequest): Promise<{
32
+ message_id: number;
33
+ }>;
34
+ }
35
+ export interface TelegramPollRequest {
36
+ readonly chatId: number;
37
+ readonly question: string;
38
+ readonly options: string[];
39
+ readonly isAnonymous?: boolean;
40
+ readonly allowsMultipleAnswers?: boolean;
41
+ }
42
+ interface TelegramDeadlineOperation<T> {
43
+ readonly method: string;
44
+ readonly run: (signal: AbortSignal) => Promise<T>;
45
+ readonly signal?: AbortSignal;
46
+ readonly timeoutMs?: number;
47
+ }
48
+ /** Owns Telegram Bot API transport, cancellation, deadlines and native API operations. */
49
+ export declare class TelegramClient implements TelegramClientApi {
50
+ #private;
51
+ protected readonly fetchTransport: TelegramFetch;
52
+ protected requestSignal?: AbortSignal;
53
+ constructor(options: Readonly<{
54
+ config: ResolvedTelegramConfig;
55
+ fetch?: TelegramFetch;
56
+ requestTimeoutMs?: number;
57
+ }>);
58
+ protected restartRequests(): void;
59
+ protected cancelRequests(): void;
60
+ callApi<T = unknown>(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
61
+ /** multipart/form-data 变体(attach:// 媒体上传;Content-Type 边界由 FormData 自带)。 */
62
+ callApiForm<T = unknown>(method: string, form: FormData, signal?: AbortSignal): Promise<T>;
63
+ protected withRequestDeadline<T>({ method, run, signal, timeoutMs, }: TelegramDeadlineOperation<T>): Promise<T>;
64
+ pinMessage(chatId: number, messageId: number): Promise<boolean>;
65
+ unpinMessage(chatId: number, messageId?: number): Promise<boolean>;
66
+ setChatDescription(chatId: number, description: string): Promise<boolean>;
67
+ setMessageReaction(chatId: number, messageId: number, reaction: string): Promise<boolean>;
68
+ getChatMemberCount(chatId: number): Promise<number>;
69
+ getChatAdmins(chatId: number): Promise<TelegramChatMember[]>;
70
+ sendStickerMessage(chatId: number, sticker: string): Promise<{
71
+ message_id: number;
72
+ }>;
73
+ setChatPermissionsAll(chatId: number, permissions: Record<string, boolean | undefined>): Promise<boolean>;
74
+ createInviteLink(chatId: number): Promise<string>;
75
+ sendPoll({ chatId, question, options, isAnonymous, allowsMultipleAnswers }: TelegramPollRequest): Promise<{
76
+ message_id: number;
77
+ }>;
78
+ }
79
+ export {};
@@ -0,0 +1,159 @@
1
+ import { botApiUrl } from './protocol.js';
2
+ import { TelegramApiError } from './api-error.js';
3
+ const DEFAULT_API_TIMEOUT_MS = 30_000;
4
+ /** Owns Telegram Bot API transport, cancellation, deadlines and native API operations. */
5
+ export class TelegramClient {
6
+ #options;
7
+ fetchTransport;
8
+ requestSignal;
9
+ #apiAbort = new AbortController();
10
+ constructor(options) {
11
+ if (options.requestTimeoutMs !== undefined && (!Number.isFinite(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)) {
12
+ throw new TypeError('Telegram requestTimeoutMs must be a positive finite number');
13
+ }
14
+ this.#options = options;
15
+ this.fetchTransport = options.fetch ?? globalThis.fetch;
16
+ }
17
+ restartRequests() { if (this.#apiAbort.signal.aborted)
18
+ this.#apiAbort = new AbortController(); }
19
+ cancelRequests() { this.#apiAbort.abort(); }
20
+ async callApi(method, params = {}, signal) {
21
+ return this.#requestApi({ method, init: {
22
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params),
23
+ }, signal, timeoutMs: method === 'getUpdates'
24
+ ? Math.max(this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS, (Number(params.timeout) || 0) * 1_000 + 15_000)
25
+ : undefined });
26
+ }
27
+ /** multipart/form-data 变体(attach:// 媒体上传;Content-Type 边界由 FormData 自带)。 */
28
+ async callApiForm(method, form, signal) {
29
+ return this.#requestApi({ method, init: { method: 'POST', body: form }, signal });
30
+ }
31
+ async #requestApi({ method, init, signal, timeoutMs }) {
32
+ return this.withRequestDeadline({ method, run: async (requestSignal) => {
33
+ const response = await this.fetchTransport(botApiUrl(this.#options.config, method), { ...init, signal: requestSignal });
34
+ return this.#parseApiResponse(method, response);
35
+ }, signal, timeoutMs });
36
+ }
37
+ async withRequestDeadline({ method, run, signal, timeoutMs = this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS, }) {
38
+ const abort = new AbortController();
39
+ let issued = false;
40
+ const sources = [...new Set([signal, this.requestSignal, this.#apiAbort.signal].filter((item) => !!item))];
41
+ let rejectAbort;
42
+ const cancelled = new Promise((_resolve, reject) => { rejectAbort = reject; });
43
+ const onCancel = () => {
44
+ const error = new TelegramApiError('cancelled', `Telegram API ${method} cancelled`, {
45
+ disposition: issued ? 'unknown' : 'not_sent',
46
+ });
47
+ abort.abort(error);
48
+ rejectAbort(error);
49
+ };
50
+ const timer = setTimeout(() => {
51
+ const error = new TelegramApiError('timeout', `Telegram API ${method} timed out`);
52
+ abort.abort(error);
53
+ rejectAbort(error);
54
+ }, timeoutMs);
55
+ for (const source of sources)
56
+ source.addEventListener('abort', onCancel, { once: true });
57
+ try {
58
+ if (sources.some((source) => source.aborted))
59
+ onCancel();
60
+ const request = Promise.resolve().then(async () => {
61
+ abort.signal.throwIfAborted();
62
+ issued = true;
63
+ return run(abort.signal);
64
+ });
65
+ return await Promise.race([request, cancelled]);
66
+ }
67
+ catch (error) {
68
+ if (abort.signal.aborted)
69
+ throw abort.signal.reason;
70
+ if (error instanceof TelegramApiError)
71
+ throw error;
72
+ // Network exceptions can contain credential-bearing URLs. Keep those out of errors/logs.
73
+ const detail = error instanceof Error
74
+ ? error.message.replaceAll(this.#options.config.token, '[redacted]') : 'transport failure';
75
+ throw new TelegramApiError('network', `Telegram API ${method} network request failed: ${detail}`);
76
+ }
77
+ finally {
78
+ clearTimeout(timer);
79
+ for (const source of sources)
80
+ source.removeEventListener('abort', onCancel);
81
+ }
82
+ }
83
+ async #parseApiResponse(method, response) {
84
+ const text = await response.text();
85
+ let body;
86
+ try {
87
+ body = JSON.parse(text);
88
+ }
89
+ catch {
90
+ throw new TelegramApiError('invalid_response', `Telegram API ${method} invalid JSON (${response.status})`, { status: response.status });
91
+ }
92
+ if (!body || typeof body !== 'object' || typeof body.ok !== 'boolean') {
93
+ throw new TelegramApiError('invalid_response', `Telegram API ${method} invalid response envelope`, { status: response.status });
94
+ }
95
+ if (!body.ok || response.status < 200 || response.status >= 300) {
96
+ const status = !body.ok ? body.error_code ?? response.status : response.status;
97
+ const kind = status === 401 || status === 403 ? 'authentication'
98
+ : status === 429 ? 'rate_limit' : response.status >= 400 ? 'http' : 'api';
99
+ const description = !body.ok ? body.description?.replaceAll(this.#options.config.token, '[redacted]') : undefined;
100
+ throw new TelegramApiError(kind, `Telegram API ${method} failed (${status}): ${description ?? 'request rejected'}`, { status, disposition: !body.ok ? 'rejected' : 'unknown', ...(!body.ok && body.parameters?.retry_after != null
101
+ ? { retryAfterSeconds: body.parameters.retry_after } : {}) });
102
+ }
103
+ if (!('result' in body))
104
+ throw new TelegramApiError('invalid_response', `Telegram API ${method} missing result`);
105
+ return body.result;
106
+ }
107
+ // ── Agent tool surface ──────────────────────────────────────────────
108
+ async pinMessage(chatId, messageId) {
109
+ await this.callApi('pinChatMessage', { chat_id: chatId, message_id: messageId });
110
+ return true;
111
+ }
112
+ async unpinMessage(chatId, messageId) {
113
+ if (messageId != null) {
114
+ await this.callApi('unpinChatMessage', { chat_id: chatId, message_id: messageId });
115
+ }
116
+ else {
117
+ await this.callApi('unpinAllChatMessages', { chat_id: chatId });
118
+ }
119
+ return true;
120
+ }
121
+ async setChatDescription(chatId, description) {
122
+ await this.callApi('setChatDescription', { chat_id: chatId, description });
123
+ return true;
124
+ }
125
+ async setMessageReaction(chatId, messageId, reaction) {
126
+ await this.callApi('setMessageReaction', {
127
+ chat_id: chatId,
128
+ message_id: messageId,
129
+ reaction: [{ type: 'emoji', emoji: reaction }],
130
+ });
131
+ return true;
132
+ }
133
+ async getChatMemberCount(chatId) {
134
+ return this.callApi('getChatMemberCount', { chat_id: chatId });
135
+ }
136
+ async getChatAdmins(chatId) {
137
+ return this.callApi('getChatAdministrators', { chat_id: chatId });
138
+ }
139
+ async sendStickerMessage(chatId, sticker) {
140
+ return this.callApi('sendSticker', { chat_id: chatId, sticker });
141
+ }
142
+ async setChatPermissionsAll(chatId, permissions) {
143
+ await this.callApi('setChatPermissions', { chat_id: chatId, permissions });
144
+ return true;
145
+ }
146
+ async createInviteLink(chatId) {
147
+ const link = await this.callApi('createChatInviteLink', { chat_id: chatId });
148
+ return link.invite_link;
149
+ }
150
+ async sendPoll({ chatId, question, options, isAnonymous = true, allowsMultipleAnswers = false }) {
151
+ return this.callApi('sendPoll', {
152
+ chat_id: chatId,
153
+ question,
154
+ options,
155
+ is_anonymous: isAnonymous,
156
+ allows_multiple_answers: allowsMultipleAnswers,
157
+ });
158
+ }
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-telegram",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
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.25",
35
- "@zhin.js/agent-feature": "1.1.10",
36
- "@zhin.js/client": "1.1.21",
34
+ "@zhin.js/adapter": "1.1.26",
35
+ "@zhin.js/agent-feature": "1.1.11",
36
+ "@zhin.js/client": "1.1.22",
37
37
  "@zhin.js/console-contract": "1.1.3",
38
- "@zhin.js/core": "1.1.53",
39
- "@zhin.js/feature-kit": "1.1.9",
40
- "@zhin.js/host-http": "1.1.11",
38
+ "@zhin.js/core": "1.1.54",
39
+ "@zhin.js/feature-kit": "1.1.10",
40
+ "@zhin.js/host-http": "1.1.12",
41
41
  "@zhin.js/im-contract": "1.1.9",
42
42
  "@zhin.js/logger": "1.1.2",
43
- "@zhin.js/page": "1.1.10",
44
- "@zhin.js/skill": "1.1.10",
45
- "@zhin.js/tool": "1.1.12"
43
+ "@zhin.js/page": "1.1.12",
44
+ "@zhin.js/skill": "1.1.11",
45
+ "@zhin.js/tool": "1.1.13"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "zod": "^4.0.0",
49
- "@zhin.js/adapter": "^1.1.25",
50
- "@zhin.js/command": "^1.1.11",
51
- "@zhin.js/core": "^1.1.53",
52
- "@zhin.js/permission": "^1.1.9",
53
- "zhin.js": "^1.1.18"
49
+ "@zhin.js/adapter": "^1.1.26",
50
+ "@zhin.js/command": "^1.1.12",
51
+ "@zhin.js/core": "^1.1.54",
52
+ "@zhin.js/permission": "^1.1.10",
53
+ "zhin.js": "^1.1.19"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "@zhin.js/command": {
@@ -68,9 +68,9 @@
68
68
  "typescript": "^6.0.3",
69
69
  "vitest": "^4.1.10",
70
70
  "zod": "^4.4.3",
71
- "@zhin.js/host-http": "1.1.11",
72
- "@zhin.js/pagemanager": "1.1.17",
73
- "zhin.js": "1.1.18"
71
+ "@zhin.js/host-http": "1.1.12",
72
+ "@zhin.js/pagemanager": "1.1.19",
73
+ "zhin.js": "1.1.19"
74
74
  },
75
75
  "files": [
76
76
  "adapters",
@@ -102,7 +102,7 @@
102
102
  "features": [
103
103
  {
104
104
  "package": "@zhin.js/adapter",
105
- "api": "^1.0.0"
105
+ "api": "^2.0.0"
106
106
  },
107
107
  {
108
108
  "package": "@zhin.js/command",
package/plugin.js CHANGED
@@ -1,9 +1,7 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { createEndpointRuntimeState } from 'zhin.js/adapter';
3
2
  import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
4
3
  import { permissionHostToken } from '@zhin.js/permission';
5
4
  import { checkTelegramPlatformPermit } from "./lib/platform-permit.js";
6
- import { telegramRuntimeStateToken } from "./lib/telegram-runtime-state.js";
7
5
  import { createTelegramConsoleRpc } from "./lib/console.js";
8
6
  export default definePlugin({
9
7
  name: 'telegram',
@@ -11,8 +9,7 @@ export default definePlugin({
11
9
  displayName: 'Telegram Bot API Adapter',
12
10
  },
13
11
  setup(context) {
14
- context.resources.provide(telegramRuntimeStateToken, createEndpointRuntimeState());
15
- context.resources.provide(pluginConsoleRpcToken, createTelegramConsoleRpc(context.resources, () => context.config.get()));
12
+ context.resources.provide(pluginConsoleRpcToken, createTelegramConsoleRpc(() => context.config.get()));
16
13
  if (context.resources.has(permissionHostToken)) {
17
14
  const host = context.resources.use(permissionHostToken);
18
15
  return host.registerPlatform('telegram', checkTelegramPlatformPermit);
package/src/adapter.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
2
+ import { httpHostToken } from '@zhin.js/host-http';
3
+ import { TelegramEndpoint, type TelegramEndpointOptions } from './endpoint.js';
4
+ import { TelegramClient } from './telegram-client.js';
5
+ import { resolveTelegramConfig, type TelegramEndpointConfig } from './protocol.js';
6
+ import type { TelegramClientEventMap } from './client.js';
7
+
8
+ export class TelegramAdapter extends Adapter<TelegramEndpoint, TelegramEndpointConfig, TelegramClientEventMap> {
9
+ constructor(private readonly context: AdapterContext<TelegramEndpointConfig>,
10
+ private readonly transport: Pick<TelegramEndpointOptions, 'fetch' | 'requestTimeoutMs'> = {}) { super(); }
11
+ override get configurationIdentityFields(): readonly string[] { return ['id']; }
12
+ override get configurationIdentityProbeFields(): readonly string[] { return ['token', 'apiBaseUrl']; }
13
+ override get configurationUpdateFields(): readonly string[] { return ['token', 'polling', 'webhook',
14
+ 'webhook.domain', 'webhook.path', 'webhook.secretToken', 'allowedUpdates', 'apiBaseUrl']; }
15
+ protected override validateConfiguration(config: TelegramEndpointConfig): void { resolveTelegramConfig(config); }
16
+ protected override async identifyConfiguration(input: TelegramEndpointConfig, signal: AbortSignal): Promise<string> {
17
+ const client = new TelegramClient({ config: resolveTelegramConfig(input), ...this.transport });
18
+ const me = await client.callApi<{ id?: number }>('getMe', {}, signal);
19
+ if (typeof me?.id !== 'number' || !Number.isFinite(me.id)) throw new Error('Telegram identity response is incomplete');
20
+ return JSON.stringify(['telegram', String(me.id)]);
21
+ }
22
+ protected createEndpoint(input: TelegramEndpointConfig, execution: EndpointCreationContext): TelegramEndpoint {
23
+ const config = resolveTelegramConfig(input);
24
+ return new TelegramEndpoint({ id: execution.canonicalId, config, ...this.transport,
25
+ ...(config.mode === 'webhook' ? { http: this.context.use(httpHostToken) } : {}) });
26
+ }
27
+ }
package/src/client.ts CHANGED
@@ -1,16 +1,8 @@
1
- import { defineEndpointClient } from 'zhin.js/adapter';
2
- import type { TelegramClient } from './endpoint.js';
1
+ import type { TelegramAdapter } from './adapter.js';
3
2
  import type { TelegramUpdate } from './protocol.js';
4
3
 
5
4
  export type TelegramClientEventMap = Record<string, TelegramUpdate>;
6
5
 
7
6
  declare module '@zhin.js/feature-kit' {
8
- interface AdapterClientRegistry {
9
- readonly telegram: {
10
- readonly client: TelegramClient;
11
- readonly events: TelegramClientEventMap;
12
- };
13
- }
7
+ interface AdapterRegistry { readonly telegram: TelegramAdapter; }
14
8
  }
15
-
16
- export const telegramClient = defineEndpointClient<TelegramClient, TelegramClientEventMap>('telegram');
@@ -8,6 +8,11 @@ export const telegramConsoleSpec = {
8
8
  adapterDisplayName: 'Telegram',
9
9
  fields: [
10
10
  { key: 'token', required: true, env: true, description: 'Telegram bot token' },
11
+ { key: 'apiBaseUrl', description: 'Telegram Bot API URL' },
12
+ { key: 'polling', type: 'boolean', description: 'Use long polling; false selects webhook mode' },
13
+ { key: 'webhook.domain', description: 'Public webhook origin' },
14
+ { key: 'webhook.path', description: 'Local webhook route' },
15
+ { key: 'webhook.secretToken', env: true, description: 'Webhook secret token' },
11
16
  ] as readonly AdapterConsoleField[],
12
17
  docsUrl: 'https://zhin.js.org/adapters/telegram',
13
18
  readPermission: 'telegram:read',
package/src/console.ts CHANGED
@@ -3,19 +3,15 @@
3
3
  *
4
4
  * 页面绑定 `pages/index`,读取 / 写入分别要求 `<adapter>:read` / `<adapter>:write`。
5
5
  */
6
- import { createAdapterConsoleRpc, type AdapterConsoleResources } from 'zhin.js/adapter';
6
+ import { createAdapterConsoleRpc } from 'zhin.js/adapter';
7
7
  import type { PluginConsoleRpc } from 'zhin.js';
8
- import { telegramRuntimeStateToken } from './telegram-runtime-state.js';
9
8
  import { telegramConsoleSpec } from './console-spec.js';
10
9
 
11
10
  export function createTelegramConsoleRpc(
12
- resources: AdapterConsoleResources,
13
11
  getConfig: () => unknown,
14
12
  ): PluginConsoleRpc {
15
- const state = resources.use(telegramRuntimeStateToken);
16
13
  return createAdapterConsoleRpc({
17
14
  ...telegramConsoleSpec,
18
- running: () => state.endpoints.values(),
19
15
  getConfig,
20
- }, resources);
16
+ });
21
17
  }