@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 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
@@ -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 endpoint list 的"运行中"数据源)
23
+ // 注册到插件运行时状态(telegram.endpoint list 的"运行中"数据源)
23
24
  context.use(telegramRuntimeStateToken).endpoints.set(config.name, {
24
25
  name: config.name,
25
26
  mode: config.mode,
@@ -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 endpoint list 的"运行中"数据源)
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
- readonly target: string;
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 plan = formatOutboundPlan(target, payload);
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
- return lastId || `telegram-${Date.now()}`;
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 与旧 wire 字段(file_id/url)保持字符串直发。
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, mediaRefFromLegacyData } from '@zhin.js/core';
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 与旧 wire 字段(file_id/url)保持字符串直发。
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
- * 媒体来源归一:canonical `data.media` 优先,旧 wire 字段
245
- * `{file_id,url,file,base64}` mediaRefFromLegacyData 兼容。
246
- * url/file → 字符串直发;base64/path → attach:// 占位并登记上传。
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 : mediaRefFromLegacyData(data);
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 = typeof data.file_id === 'string' ? data.file_id : mediaSource(data, 'sticker.webp');
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 endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
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 endpoint` 命令共享(同一 owner generation)。
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": "6.0.5",
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.2",
36
- "@zhin.js/command": "1.0.4",
37
- "@zhin.js/core": "1.4.3",
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.2",
45
- "@zhin.js/agent": "1.0.10",
46
- "@zhin.js/core": "1.4.3",
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": "5.0.3"
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.10",
67
+ "@zhin.js/agent": "1.1.0",
67
68
  "@zhin.js/host-http": "1.0.4",
68
- "zhin.js": "5.0.3"
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 endpoint list 的"运行中"数据源)
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 }: { readonly target: string; readonly payload: unknown }): Promise<string> {
203
- const plan = formatOutboundPlan(target, payload);
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
- return lastId || `telegram-${Date.now()}`;
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, mediaRefFromLegacyData } from '@zhin.js/core';
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 与旧 wire 字段(file_id/url)保持字符串直发。
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
- * 媒体来源归一:canonical `data.media` 优先,旧 wire 字段
525
- * `{file_id,url,file,base64}` mediaRefFromLegacyData 兼容。
526
- * url/file → 字符串直发;base64/path → attach:// 占位并登记上传。
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 : mediaRefFromLegacyData(data);
530
- if (!media) return undefined;
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 = typeof data.file_id === 'string' ? data.file_id : mediaSource(data, 'sticker.webp');
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 endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
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 endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `telegram.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6