@zhin.js/adapter-telegram 6.0.5 → 7.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/CHANGELOG.md +44 -0
- package/adapters/telegram.js +2 -1
- package/adapters/telegram.ts +2 -1
- package/lib/endpoint.d.ts +4 -5
- package/lib/endpoint.js +30 -3
- package/lib/protocol.d.ts +1 -1
- package/lib/protocol.js +23 -14
- package/lib/telegram-endpoint-commands.js +1 -1
- package/lib/telegram-runtime-state.js +1 -1
- package/package.json +11 -10
- package/plugin.js +1 -1
- package/src/endpoint.ts +34 -4
- package/src/protocol.ts +25 -14
- package/src/telegram-endpoint-commands.ts +1 -1
- package/src/telegram-runtime-state.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4fbff5d: feat!: 多模态双向 Segment 一贯制(BREAKING,无兼容层)
|
|
8
|
+
|
|
9
|
+
全框架唯一媒体表达统一为 canonical `Segment` + `MediaRef{kind: url|path|base64|file, value, mime_type?, file_name?, size?}`,新增 audio/video/file 段类型;所有第二形状(legacy `data.url/file/base64` 字段、`mediaRefFromLegacyData`/`mediaRefToLegacyFields` 桥、双写)全部删除。
|
|
10
|
+
|
|
11
|
+
- **core**:`SendContent` 一等支持 `Segment[]`;endpoint 出站载荷只含 canonical 段;`resolveOutboundMediaPolicy` 改为纯声明驱动(adapter definition `segments.outboundMedia`),内置策略表删除,未声明回退 `url-or-text`;`ImageContent` 旧桥删除。
|
|
12
|
+
- **ai**:新增 `MediaContentBlock`/`MediaBlockRef`(Segment 同构)与 `UserMessage.media`(当前 turn 媒体,**不持久化**——存储层自动剥离);`createUserMessage(text, media?)` 签名变更(`ImageContent` 删除);provider 边界序列化器 `filterMediaBlocksForProvider` + 能力表(缺省 image-only,不支持类型降级占位文本);ai-sdk 桥媒体块 → SDK image/file parts。
|
|
13
|
+
- **agent**:入站 turn 注入(`turn/inbound-media.ts`)——commMessage 媒体段 → 当前 turn `UserMessage.media`;图片 path 物化、音频默认 STT(`@zhin.js/speech` 可选,失败降级占位)、视频/文件占位;`publishOutboundElements` 产出 canonical Segment;`transcribeAudioPayload` 导出。
|
|
14
|
+
- **cli**:`bridgeRuntimeMessage` 回复链路媒体段透传,不再压平为文本(`$reply` 直达 normalize → adapter)。
|
|
15
|
+
- **全部 20 个平台适配器**:出站媒体只消费 `data.media`(url 直发 / base64 直发 / 平台上传 / 读盘),入站媒体产出 canonical `data.media`;`segments.outboundMedia` 声明与实际消费逐一核对修正;QQ 入站新增 canonical segments(image/audio/video/file/mention/face/reply),图片/语音/视频不再丢失。
|
|
16
|
+
|
|
17
|
+
迁移:适配器/插件产媒体一律用 `{ type, data: { media: MediaRef } }`;发送 legacy `data.url/file/base64` 形状的段会被 warn 丢弃。
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [7c1e63a]
|
|
20
|
+
- Updated dependencies [4fbff5d]
|
|
21
|
+
- Updated dependencies [5b94d9c]
|
|
22
|
+
- @zhin.js/command@1.0.5
|
|
23
|
+
- @zhin.js/adapter@1.1.3
|
|
24
|
+
- @zhin.js/core@1.5.0
|
|
25
|
+
- @zhin.js/agent@1.1.0
|
|
26
|
+
- zhin.js@6.0.0
|
|
27
|
+
|
|
28
|
+
## 6.0.6
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- d52f3c5: fix: QQ 适配器群聊误识别为私聊、game-kit 启动崩溃、全面补齐 recallMessage
|
|
33
|
+
|
|
34
|
+
- fix(cli): `resolveChannelType` 增加 `metadata.channelKind` 检查,修复 QQ/Discord/KOOK 群聊消息被误分类为私聊
|
|
35
|
+
- fix(game-kit): `createHostGameDb` 改用延迟代理模型,避免数据库启动前解析模型导致崩溃
|
|
36
|
+
- feat(qq): 实现 `recallMessage`,解析复合 messageId 路由到对应 SDK 撤回方法;`normalizeQqMessage` 增加 `message_type` 缺失时的回退检测
|
|
37
|
+
- feat(slack): 实现 `recallMessage`,利用已有 compound ref 和 `chat.delete` API
|
|
38
|
+
- feat(onebot11): 两个 endpoint 类实现 `recallMessage`(`delete_msg`)
|
|
39
|
+
- feat(onebot12): 两个 endpoint 类实现 `recallMessage`(`delete_message`)
|
|
40
|
+
- feat(napcat): 三个 endpoint 类实现 `recallMessage`(`delete_msg`)
|
|
41
|
+
- feat(discord): 两个 endpoint 类实现 `recallMessage`;`send()` 改为返回 `channelId:snowflake` 复合 ID
|
|
42
|
+
- feat(telegram): 实现 `recallMessage`(`deleteMessage`);`send()` 改为返回 `chatId:messageId` 复合 ID
|
|
43
|
+
- feat(kook): 两个 endpoint 类实现 `recallMessage`,利用 kook-client `recallMsg` API
|
|
44
|
+
- feat(lark): 实现 `recallMessage`(`DELETE /im/v1/messages/{id}`)
|
|
45
|
+
- feat(wecom): 实现 `recallMessage`(`POST /cgi-bin/message/recall`)
|
|
46
|
+
|
|
3
47
|
## 6.0.5
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
package/adapters/telegram.js
CHANGED
|
@@ -11,6 +11,7 @@ import { telegramRuntimeStateToken } from "../lib/telegram-runtime-state.js";
|
|
|
11
11
|
export { TelegramEndpoint } from "../lib/endpoint.js";
|
|
12
12
|
export default defineAdapter({
|
|
13
13
|
capabilities: ['inbound', 'outbound'],
|
|
14
|
+
operations: ['recall'],
|
|
14
15
|
// 媒体 url / file_id 直发,base64 / 本地路径经 multipart attach:// 上传物化;
|
|
15
16
|
// inline keyboard 原生按钮承载交互段。
|
|
16
17
|
segments: {
|
|
@@ -19,7 +20,7 @@ export default defineAdapter({
|
|
|
19
20
|
},
|
|
20
21
|
create(context) {
|
|
21
22
|
const config = resolveTelegramConfig(context.config);
|
|
22
|
-
// 注册到插件运行时状态(telegram
|
|
23
|
+
// 注册到插件运行时状态(telegram.endpoint list 的"运行中"数据源)
|
|
23
24
|
context.use(telegramRuntimeStateToken).endpoints.set(config.name, {
|
|
24
25
|
name: config.name,
|
|
25
26
|
mode: config.mode,
|
package/adapters/telegram.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type { TelegramEndpointOptions, TelegramFetch } from '../src/endpoint.js'
|
|
|
16
16
|
|
|
17
17
|
export default defineAdapter<TelegramAdapterConfig>({
|
|
18
18
|
capabilities: ['inbound', 'outbound'],
|
|
19
|
+
operations: ['recall'],
|
|
19
20
|
// 媒体 url / file_id 直发,base64 / 本地路径经 multipart attach:// 上传物化;
|
|
20
21
|
// inline keyboard 原生按钮承载交互段。
|
|
21
22
|
segments: {
|
|
@@ -24,7 +25,7 @@ export default defineAdapter<TelegramAdapterConfig>({
|
|
|
24
25
|
},
|
|
25
26
|
create(context) {
|
|
26
27
|
const config = resolveTelegramConfig(context.config);
|
|
27
|
-
// 注册到插件运行时状态(telegram
|
|
28
|
+
// 注册到插件运行时状态(telegram.endpoint list 的"运行中"数据源)
|
|
28
29
|
context.use(telegramRuntimeStateToken).endpoints.set(config.name, {
|
|
29
30
|
name: config.name,
|
|
30
31
|
mode: config.mode,
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
1
|
+
import type { EndpointControl, EndpointInstance, EndpointSendRequest } from '@zhin.js/adapter';
|
|
2
2
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
3
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
4
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
@@ -28,6 +28,7 @@ export interface TelegramEndpointOptions {
|
|
|
28
28
|
*/
|
|
29
29
|
export declare class TelegramEndpoint implements EndpointInstance {
|
|
30
30
|
#private;
|
|
31
|
+
readonly control: EndpointControl;
|
|
31
32
|
constructor(options: TelegramEndpointOptions);
|
|
32
33
|
/** Used by webhook handler. */
|
|
33
34
|
get isOpen(): boolean;
|
|
@@ -39,10 +40,8 @@ export declare class TelegramEndpoint implements EndpointInstance {
|
|
|
39
40
|
open(): void;
|
|
40
41
|
close(): void;
|
|
41
42
|
stop(): Promise<void>;
|
|
42
|
-
send({ target, payload }:
|
|
43
|
-
|
|
44
|
-
readonly payload: unknown;
|
|
45
|
-
}): Promise<string>;
|
|
43
|
+
send({ target, conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
44
|
+
recallMessage(messageId: string): Promise<void>;
|
|
46
45
|
/** Test / internal: admit a message when open. */
|
|
47
46
|
admit(msg: TelegramMessage): void;
|
|
48
47
|
/** Test / internal: admit a callback query when open. */
|
package/lib/endpoint.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* TelegramEndpoint — lifecycle, outbound, admit, Bot API helpers for agent tools.
|
|
3
3
|
*/
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { formatLegacyMessageReference, nativeConversationId, parseLegacyMessageReference, } from '@zhin.js/im-contract';
|
|
5
6
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
7
|
import { runTelegramPollLoop } from './polling.js';
|
|
7
8
|
import { normalizeTelegramChatMember } from './platform-permit.js';
|
|
@@ -29,6 +30,9 @@ export class TelegramEndpoint {
|
|
|
29
30
|
#botUserId;
|
|
30
31
|
#botUsername;
|
|
31
32
|
#chatMemberCache = new Map();
|
|
33
|
+
control = Object.freeze({
|
|
34
|
+
recall: (messageId) => this.recallMessage(messageId),
|
|
35
|
+
});
|
|
32
36
|
constructor(options) {
|
|
33
37
|
this.#options = options;
|
|
34
38
|
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
@@ -127,8 +131,9 @@ export class TelegramEndpoint {
|
|
|
127
131
|
this.#started = false;
|
|
128
132
|
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
129
133
|
}
|
|
130
|
-
async send({ target, payload }) {
|
|
131
|
-
const
|
|
134
|
+
async send({ target, conversation, payload }) {
|
|
135
|
+
const chatId = nativeConversationId(target, conversation);
|
|
136
|
+
const plan = formatOutboundPlan(chatId, payload);
|
|
132
137
|
let lastId = '';
|
|
133
138
|
for (const action of plan.actions) {
|
|
134
139
|
const form = await this.#buildUploadForm(action.params, plan.uploads);
|
|
@@ -138,7 +143,19 @@ export class TelegramEndpoint {
|
|
|
138
143
|
if (result.message_id != null)
|
|
139
144
|
lastId = String(result.message_id);
|
|
140
145
|
}
|
|
141
|
-
|
|
146
|
+
if (!lastId)
|
|
147
|
+
return `telegram-${Date.now()}`;
|
|
148
|
+
return formatLegacyMessageReference({ target, messageId: lastId });
|
|
149
|
+
}
|
|
150
|
+
async recallMessage(messageId) {
|
|
151
|
+
if (!messageId || messageId.startsWith('telegram-'))
|
|
152
|
+
return;
|
|
153
|
+
const reference = parseLegacyMessageReference(messageId);
|
|
154
|
+
if (!reference)
|
|
155
|
+
return;
|
|
156
|
+
const chatId = nativeConversationId(reference.target);
|
|
157
|
+
const msgId = reference.messageId;
|
|
158
|
+
await this.callApi('deleteMessage', { chat_id: chatId, message_id: Number(msgId) });
|
|
142
159
|
}
|
|
143
160
|
/**
|
|
144
161
|
* 含 `attach://` 占位的媒体参数 → multipart/form-data:
|
|
@@ -193,6 +210,9 @@ export class TelegramEndpoint {
|
|
|
193
210
|
id: String(msg.message_id),
|
|
194
211
|
metadata: Object.freeze({
|
|
195
212
|
endpoint: this.#options.config.name,
|
|
213
|
+
// Preserve Telegram's native value below, but publish the canonical
|
|
214
|
+
// scene kind so every Runtime consumer sees the same conversation type.
|
|
215
|
+
channelType: resolveTelegramChannelType(msg.chat.type),
|
|
196
216
|
chatType: msg.chat.type,
|
|
197
217
|
userId: msg.from?.id,
|
|
198
218
|
date: msg.date,
|
|
@@ -393,3 +413,10 @@ export class TelegramEndpoint {
|
|
|
393
413
|
});
|
|
394
414
|
}
|
|
395
415
|
}
|
|
416
|
+
function resolveTelegramChannelType(chatType) {
|
|
417
|
+
if (chatType === 'private')
|
|
418
|
+
return 'private';
|
|
419
|
+
if (chatType === 'channel')
|
|
420
|
+
return 'channel';
|
|
421
|
+
return 'group';
|
|
422
|
+
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -284,6 +284,6 @@ export declare function formatOutboundActions(target: string | number, payload:
|
|
|
284
284
|
/**
|
|
285
285
|
* formatOutboundActions 的上传感知变体:canonical MediaRef kind=base64/path
|
|
286
286
|
* 的媒体段产出 `attach://` 占位 + uploads 清单(endpoint 走 multipart 表单上传);
|
|
287
|
-
* kind=url/file
|
|
287
|
+
* kind=url/file 保持字符串直发(file 即 Telegram file_id 不透明引用)。
|
|
288
288
|
*/
|
|
289
289
|
export declare function formatOutboundPlan(target: string | number, payload: unknown): TelegramOutboundPlan;
|
package/lib/protocol.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Telegram Bot API protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
*/
|
|
5
|
-
import { isMediaRef
|
|
5
|
+
import { isMediaRef } from '@zhin.js/core';
|
|
6
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
const logger = getLogger('telegram');
|
|
6
8
|
export function resolveTelegramConfig(config = {}) {
|
|
7
9
|
const entry = config.endpoints?.find((item) => item.context === 'telegram');
|
|
8
10
|
const token = config.token
|
|
@@ -206,7 +208,7 @@ export function formatOutboundActions(target, payload) {
|
|
|
206
208
|
/**
|
|
207
209
|
* formatOutboundActions 的上传感知变体:canonical MediaRef kind=base64/path
|
|
208
210
|
* 的媒体段产出 `attach://` 占位 + uploads 清单(endpoint 走 multipart 表单上传);
|
|
209
|
-
* kind=url/file
|
|
211
|
+
* kind=url/file 保持字符串直发(file 即 Telegram file_id 不透明引用)。
|
|
210
212
|
*/
|
|
211
213
|
export function formatOutboundPlan(target, payload) {
|
|
212
214
|
const uploads = [];
|
|
@@ -241,14 +243,21 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
241
243
|
const actions = [];
|
|
242
244
|
const replyParams = () => (replyTo != null ? { reply_parameters: { message_id: replyTo } } : {});
|
|
243
245
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
246
|
+
* 媒体来源归一:唯一来源是 canonical `data.media` MediaRef。
|
|
247
|
+
* kind=url/file → 字符串直发(file 即 Telegram file_id 不透明引用);
|
|
248
|
+
* kind=base64/path → attach:// 占位并登记上传。
|
|
249
|
+
* 无 MediaRef 时 warn + 丢弃(返回 undefined)。
|
|
247
250
|
*/
|
|
248
|
-
const mediaSource = (data, defaultName) => {
|
|
249
|
-
const media = isMediaRef(data.media) ? data.media :
|
|
250
|
-
if (!media)
|
|
251
|
+
const mediaSource = (segType, data, defaultName) => {
|
|
252
|
+
const media = isMediaRef(data.media) ? data.media : undefined;
|
|
253
|
+
if (!media) {
|
|
254
|
+
logger.warn(formatCompact({
|
|
255
|
+
op: 'telegram_outbound_media_dropped',
|
|
256
|
+
type: segType,
|
|
257
|
+
reason: 'missing_media_ref',
|
|
258
|
+
}));
|
|
251
259
|
return undefined;
|
|
260
|
+
}
|
|
252
261
|
if (media.kind === 'file' || media.kind === 'url')
|
|
253
262
|
return media.value;
|
|
254
263
|
const named = data.name ?? data.filename;
|
|
@@ -313,7 +322,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
313
322
|
break;
|
|
314
323
|
}
|
|
315
324
|
case 'image': {
|
|
316
|
-
const photo = mediaSource(data, 'image.png');
|
|
325
|
+
const photo = mediaSource('image', data, 'image.png');
|
|
317
326
|
if (photo) {
|
|
318
327
|
actions.push({
|
|
319
328
|
method: 'sendPhoto',
|
|
@@ -329,7 +338,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
329
338
|
break;
|
|
330
339
|
}
|
|
331
340
|
case 'video': {
|
|
332
|
-
const video = mediaSource(data, 'video.mp4');
|
|
341
|
+
const video = mediaSource('video', data, 'video.mp4');
|
|
333
342
|
if (video) {
|
|
334
343
|
actions.push({
|
|
335
344
|
method: 'sendVideo',
|
|
@@ -345,7 +354,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
345
354
|
break;
|
|
346
355
|
}
|
|
347
356
|
case 'audio': {
|
|
348
|
-
const audio = mediaSource(data, 'audio.mp3');
|
|
357
|
+
const audio = mediaSource('audio', data, 'audio.mp3');
|
|
349
358
|
if (audio) {
|
|
350
359
|
actions.push({
|
|
351
360
|
method: 'sendAudio',
|
|
@@ -361,7 +370,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
361
370
|
break;
|
|
362
371
|
}
|
|
363
372
|
case 'voice': {
|
|
364
|
-
const voice = mediaSource(data, 'voice.ogg');
|
|
373
|
+
const voice = mediaSource('voice', data, 'voice.ogg');
|
|
365
374
|
if (voice) {
|
|
366
375
|
actions.push({
|
|
367
376
|
method: 'sendVoice',
|
|
@@ -377,7 +386,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
377
386
|
break;
|
|
378
387
|
}
|
|
379
388
|
case 'file': {
|
|
380
|
-
const document = mediaSource(data, 'file');
|
|
389
|
+
const document = mediaSource('file', data, 'file');
|
|
381
390
|
if (document) {
|
|
382
391
|
actions.push({
|
|
383
392
|
method: 'sendDocument',
|
|
@@ -393,7 +402,7 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
393
402
|
break;
|
|
394
403
|
}
|
|
395
404
|
case 'sticker': {
|
|
396
|
-
const sticker =
|
|
405
|
+
const sticker = mediaSource('sticker', data, 'sticker.webp');
|
|
397
406
|
if (sticker) {
|
|
398
407
|
actions.push({
|
|
399
408
|
method: 'sendSticker',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `telegram
|
|
2
|
+
* `telegram.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
3
|
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
4
|
*/
|
|
5
5
|
import { createEndpointCommands } from '@zhin.js/adapter';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
-
* 由 plugin.ts setup() provide,adapter create 与 `telegram
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `telegram.endpoint` 命令共享(同一 owner generation)。
|
|
4
4
|
*/
|
|
5
5
|
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
6
|
export const telegramRuntimeStateToken = defineEndpointRuntimeStateToken('telegram');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-telegram",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
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,21 @@
|
|
|
32
32
|
"directory": "plugins/adapters/telegram"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zhin.js/adapter": "1.1.
|
|
36
|
-
"@zhin.js/command": "1.0.
|
|
37
|
-
"@zhin.js/core": "1.
|
|
35
|
+
"@zhin.js/adapter": "1.1.3",
|
|
36
|
+
"@zhin.js/command": "1.0.5",
|
|
37
|
+
"@zhin.js/core": "1.5.0",
|
|
38
38
|
"@zhin.js/host-http": "1.0.4",
|
|
39
|
+
"@zhin.js/im-contract": "1.0.0",
|
|
39
40
|
"@zhin.js/logger": "1.0.75",
|
|
40
41
|
"@zhin.js/plugin-runtime": "1.1.1"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"zod": "^4.0.0",
|
|
44
|
-
"@zhin.js/adapter": "1.1.
|
|
45
|
-
"@zhin.js/agent": "1.0
|
|
46
|
-
"@zhin.js/core": "1.
|
|
45
|
+
"@zhin.js/adapter": "1.1.3",
|
|
46
|
+
"@zhin.js/agent": "1.1.0",
|
|
47
|
+
"@zhin.js/core": "1.5.0",
|
|
47
48
|
"@zhin.js/plugin-runtime": "1.1.1",
|
|
48
|
-
"zhin.js": "
|
|
49
|
+
"zhin.js": "6.0.0"
|
|
49
50
|
},
|
|
50
51
|
"peerDependenciesMeta": {
|
|
51
52
|
"zhin.js": {
|
|
@@ -63,9 +64,9 @@
|
|
|
63
64
|
"typescript": "^6.0.3",
|
|
64
65
|
"vitest": "^4.1.10",
|
|
65
66
|
"zod": "^4.4.3",
|
|
66
|
-
"@zhin.js/agent": "1.0
|
|
67
|
+
"@zhin.js/agent": "1.1.0",
|
|
67
68
|
"@zhin.js/host-http": "1.0.4",
|
|
68
|
-
"zhin.js": "
|
|
69
|
+
"zhin.js": "6.0.0"
|
|
69
70
|
},
|
|
70
71
|
"files": [
|
|
71
72
|
"adapters",
|
package/plugin.js
CHANGED
|
@@ -9,7 +9,7 @@ export default definePlugin({
|
|
|
9
9
|
displayName: 'Telegram Bot API Adapter',
|
|
10
10
|
},
|
|
11
11
|
setup(context) {
|
|
12
|
-
// 运行中 endpoint 注册表(telegram
|
|
12
|
+
// 运行中 endpoint 注册表(telegram.endpoint list 的"运行中"数据源)
|
|
13
13
|
context.resources.provide(telegramRuntimeStateToken, createEndpointRuntimeState());
|
|
14
14
|
// 平台权限门禁:chat_creator / chat_administrator / pin_messages 等(agent 工具 platformPermit)
|
|
15
15
|
return registerTelegramPlatformPermitChecker();
|
package/src/endpoint.ts
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
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 { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { EndpointControl, EndpointInstance, EndpointSendRequest } from '@zhin.js/adapter';
|
|
6
6
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
7
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
|
+
import {
|
|
9
|
+
formatLegacyMessageReference,
|
|
10
|
+
nativeConversationId,
|
|
11
|
+
parseLegacyMessageReference,
|
|
12
|
+
} from '@zhin.js/im-contract';
|
|
8
13
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
14
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
15
|
import { runTelegramPollLoop } from './polling.js';
|
|
@@ -92,6 +97,9 @@ export class TelegramEndpoint implements EndpointInstance {
|
|
|
92
97
|
#botUserId?: number;
|
|
93
98
|
#botUsername?: string;
|
|
94
99
|
readonly #chatMemberCache = new Map<string, ChatMemberPermit>();
|
|
100
|
+
readonly control: EndpointControl = Object.freeze({
|
|
101
|
+
recall: (messageId: string) => this.recallMessage(messageId),
|
|
102
|
+
});
|
|
95
103
|
|
|
96
104
|
constructor(options: TelegramEndpointOptions) {
|
|
97
105
|
this.#options = options;
|
|
@@ -199,8 +207,9 @@ export class TelegramEndpoint implements EndpointInstance {
|
|
|
199
207
|
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
200
208
|
}
|
|
201
209
|
|
|
202
|
-
async send({ target, payload }:
|
|
203
|
-
const
|
|
210
|
+
async send({ target, conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
211
|
+
const chatId = nativeConversationId(target, conversation);
|
|
212
|
+
const plan = formatOutboundPlan(chatId, payload);
|
|
204
213
|
let lastId = '';
|
|
205
214
|
for (const action of plan.actions) {
|
|
206
215
|
const form = await this.#buildUploadForm(action.params, plan.uploads);
|
|
@@ -209,7 +218,17 @@ export class TelegramEndpoint implements EndpointInstance {
|
|
|
209
218
|
: await this.callApi<{ message_id?: number }>(action.method, action.params);
|
|
210
219
|
if (result.message_id != null) lastId = String(result.message_id);
|
|
211
220
|
}
|
|
212
|
-
|
|
221
|
+
if (!lastId) return `telegram-${Date.now()}`;
|
|
222
|
+
return formatLegacyMessageReference({ target, messageId: lastId });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async recallMessage(messageId: string): Promise<void> {
|
|
226
|
+
if (!messageId || messageId.startsWith('telegram-')) return;
|
|
227
|
+
const reference = parseLegacyMessageReference(messageId);
|
|
228
|
+
if (!reference) return;
|
|
229
|
+
const chatId = nativeConversationId(reference.target);
|
|
230
|
+
const msgId = reference.messageId;
|
|
231
|
+
await this.callApi('deleteMessage', { chat_id: chatId, message_id: Number(msgId) });
|
|
213
232
|
}
|
|
214
233
|
|
|
215
234
|
/**
|
|
@@ -270,6 +289,9 @@ export class TelegramEndpoint implements EndpointInstance {
|
|
|
270
289
|
id: String(msg.message_id),
|
|
271
290
|
metadata: Object.freeze({
|
|
272
291
|
endpoint: this.#options.config.name,
|
|
292
|
+
// Preserve Telegram's native value below, but publish the canonical
|
|
293
|
+
// scene kind so every Runtime consumer sees the same conversation type.
|
|
294
|
+
channelType: resolveTelegramChannelType(msg.chat.type),
|
|
273
295
|
chatType: msg.chat.type,
|
|
274
296
|
userId: msg.from?.id,
|
|
275
297
|
date: msg.date,
|
|
@@ -499,3 +521,11 @@ export class TelegramEndpoint implements EndpointInstance {
|
|
|
499
521
|
});
|
|
500
522
|
}
|
|
501
523
|
}
|
|
524
|
+
|
|
525
|
+
function resolveTelegramChannelType(
|
|
526
|
+
chatType: TelegramMessage['chat']['type'],
|
|
527
|
+
): 'private' | 'group' | 'channel' {
|
|
528
|
+
if (chatType === 'private') return 'private';
|
|
529
|
+
if (chatType === 'channel') return 'channel';
|
|
530
|
+
return 'group';
|
|
531
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { IncomingMessage } from 'node:http';
|
|
7
|
-
import { isMediaRef
|
|
7
|
+
import { isMediaRef } from '@zhin.js/core';
|
|
8
8
|
import type { Segment } from '@zhin.js/core/runtime';
|
|
9
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
10
|
+
|
|
11
|
+
const logger = getLogger('telegram');
|
|
9
12
|
|
|
10
13
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
11
14
|
export interface TelegramAdapterConfig {
|
|
@@ -473,7 +476,7 @@ export function formatOutboundActions(
|
|
|
473
476
|
/**
|
|
474
477
|
* formatOutboundActions 的上传感知变体:canonical MediaRef kind=base64/path
|
|
475
478
|
* 的媒体段产出 `attach://` 占位 + uploads 清单(endpoint 走 multipart 表单上传);
|
|
476
|
-
* kind=url/file
|
|
479
|
+
* kind=url/file 保持字符串直发(file 即 Telegram file_id 不透明引用)。
|
|
477
480
|
*/
|
|
478
481
|
export function formatOutboundPlan(
|
|
479
482
|
target: string | number,
|
|
@@ -521,13 +524,21 @@ function buildOutboundActions(
|
|
|
521
524
|
);
|
|
522
525
|
|
|
523
526
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
+
* 媒体来源归一:唯一来源是 canonical `data.media` MediaRef。
|
|
528
|
+
* kind=url/file → 字符串直发(file 即 Telegram file_id 不透明引用);
|
|
529
|
+
* kind=base64/path → attach:// 占位并登记上传。
|
|
530
|
+
* 无 MediaRef 时 warn + 丢弃(返回 undefined)。
|
|
527
531
|
*/
|
|
528
|
-
const mediaSource = (data: Record<string, unknown>, defaultName: string): string | undefined => {
|
|
529
|
-
const media = isMediaRef(data.media) ? data.media :
|
|
530
|
-
if (!media)
|
|
532
|
+
const mediaSource = (segType: string, data: Record<string, unknown>, defaultName: string): string | undefined => {
|
|
533
|
+
const media = isMediaRef(data.media) ? data.media : undefined;
|
|
534
|
+
if (!media) {
|
|
535
|
+
logger.warn(formatCompact({
|
|
536
|
+
op: 'telegram_outbound_media_dropped',
|
|
537
|
+
type: segType,
|
|
538
|
+
reason: 'missing_media_ref',
|
|
539
|
+
}));
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
531
542
|
if (media.kind === 'file' || media.kind === 'url') return media.value;
|
|
532
543
|
const named = data.name ?? data.filename;
|
|
533
544
|
let filename = typeof named === 'string' && named ? named : undefined;
|
|
@@ -590,7 +601,7 @@ function buildOutboundActions(
|
|
|
590
601
|
break;
|
|
591
602
|
}
|
|
592
603
|
case 'image': {
|
|
593
|
-
const photo = mediaSource(data, 'image.png');
|
|
604
|
+
const photo = mediaSource('image', data, 'image.png');
|
|
594
605
|
if (photo) {
|
|
595
606
|
actions.push({
|
|
596
607
|
method: 'sendPhoto',
|
|
@@ -606,7 +617,7 @@ function buildOutboundActions(
|
|
|
606
617
|
break;
|
|
607
618
|
}
|
|
608
619
|
case 'video': {
|
|
609
|
-
const video = mediaSource(data, 'video.mp4');
|
|
620
|
+
const video = mediaSource('video', data, 'video.mp4');
|
|
610
621
|
if (video) {
|
|
611
622
|
actions.push({
|
|
612
623
|
method: 'sendVideo',
|
|
@@ -622,7 +633,7 @@ function buildOutboundActions(
|
|
|
622
633
|
break;
|
|
623
634
|
}
|
|
624
635
|
case 'audio': {
|
|
625
|
-
const audio = mediaSource(data, 'audio.mp3');
|
|
636
|
+
const audio = mediaSource('audio', data, 'audio.mp3');
|
|
626
637
|
if (audio) {
|
|
627
638
|
actions.push({
|
|
628
639
|
method: 'sendAudio',
|
|
@@ -638,7 +649,7 @@ function buildOutboundActions(
|
|
|
638
649
|
break;
|
|
639
650
|
}
|
|
640
651
|
case 'voice': {
|
|
641
|
-
const voice = mediaSource(data, 'voice.ogg');
|
|
652
|
+
const voice = mediaSource('voice', data, 'voice.ogg');
|
|
642
653
|
if (voice) {
|
|
643
654
|
actions.push({
|
|
644
655
|
method: 'sendVoice',
|
|
@@ -654,7 +665,7 @@ function buildOutboundActions(
|
|
|
654
665
|
break;
|
|
655
666
|
}
|
|
656
667
|
case 'file': {
|
|
657
|
-
const document = mediaSource(data, 'file');
|
|
668
|
+
const document = mediaSource('file', data, 'file');
|
|
658
669
|
if (document) {
|
|
659
670
|
actions.push({
|
|
660
671
|
method: 'sendDocument',
|
|
@@ -670,7 +681,7 @@ function buildOutboundActions(
|
|
|
670
681
|
break;
|
|
671
682
|
}
|
|
672
683
|
case 'sticker': {
|
|
673
|
-
const sticker =
|
|
684
|
+
const sticker = mediaSource('sticker', data, 'sticker.webp');
|
|
674
685
|
if (sticker) {
|
|
675
686
|
actions.push({
|
|
676
687
|
method: 'sendSticker',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `telegram
|
|
2
|
+
* `telegram.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
3
|
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
4
|
*/
|
|
5
5
|
import { createEndpointCommands } from '@zhin.js/adapter';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
-
* 由 plugin.ts setup() provide,adapter create 与 `telegram
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `telegram.endpoint` 命令共享(同一 owner generation)。
|
|
4
4
|
*/
|
|
5
5
|
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
6
|
|