@zhin.js/adapter-onebot11 5.0.5 → 6.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
  # @zhin.js/adapter-onebot11
2
2
 
3
+ ## 6.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
+ ## 5.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
  ## 5.0.5
4
48
 
5
49
  ### Patch Changes
@@ -21,7 +21,7 @@ export default defineAdapter({
21
21
  create(context) {
22
22
  const config = resolveOneBot11Config(context.config);
23
23
  const gateway = context.use(messageGatewayToken);
24
- // 注册到插件运行时状态(onebot11 endpoint list 的"运行中"数据源)
24
+ // 注册到插件运行时状态(onebot11.endpoint list 的"运行中"数据源)
25
25
  context.use(onebot11RuntimeStateToken).endpoints.set(config.name, {
26
26
  name: config.name,
27
27
  mode: config.connection,
@@ -28,7 +28,7 @@ export default defineAdapter<OneBot11AdapterConfig>({
28
28
  create(context) {
29
29
  const config = resolveOneBot11Config(context.config);
30
30
  const gateway = context.use(messageGatewayToken);
31
- // 注册到插件运行时状态(onebot11 endpoint list 的"运行中"数据源)
31
+ // 注册到插件运行时状态(onebot11.endpoint list 的"运行中"数据源)
32
32
  context.use(onebot11RuntimeStateToken).endpoints.set(config.name, {
33
33
  name: config.name,
34
34
  mode: config.connection,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `onebot11 endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `onebot11.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
  * OneBot11 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `onebot11 endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `onebot11.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6
  export const onebot11RuntimeStateToken = defineEndpointRuntimeStateToken('onebot11');
package/lib/protocol.d.ts CHANGED
@@ -130,13 +130,14 @@ export declare function isOneBot11BotMentioned(input: {
130
130
  export declare function formatInboundMetadata(ev: OneBot11Event, endpoint: string): Readonly<Record<string, unknown>>;
131
131
  /**
132
132
  * canonical MediaRef → OneBot `file` 参数:url 直传、base64 → `base64://`、
133
- * 本地路径 → `file://`(路径在 OneBot 实现侧解析)。
133
+ * 本地路径 → `file://`(路径在 OneBot 实现侧解析)、kind=file 不透明引用原样透传。
134
134
  */
135
135
  export declare function mediaRefToOneBotFile(media: MediaRef): string;
136
136
  /**
137
137
  * Wire-encode an already-rendered outbound payload into OneBot 11 message segments.
138
138
  * 入参假定已经 core `normalizeOutboundPayload` 归一为 canonical Segment[];
139
- * 旧 wire 形状(at / 裸 `{file,url,base64}`)保持兼容透传。
139
+ * 媒体段只消费 canonical `data.media`(无 MediaRef 的媒体段 warn + 丢弃),
140
+ * 其余 wire 形状段(at / 平台扩展段)原样透传。
140
141
  */
141
142
  export declare function formatOutboundSegments(payload: unknown): OneBot11Segment[];
142
143
  export declare function buildSendAction(target: string, message: OneBot11Segment[]): {
package/lib/protocol.js CHANGED
@@ -3,7 +3,9 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  * Spec: https://github.com/botuniverse/onebot-11
5
5
  */
6
- import { isMediaRef, mediaRefFromLegacyData } from '@zhin.js/core';
6
+ import { isMediaRef } from '@zhin.js/core';
7
+ import { formatCompact, getLogger } from '@zhin.js/logger';
8
+ const logger = getLogger('onebot11');
7
9
  function normalizeConnection(connection, legacyType) {
8
10
  if (connection === 'ws' || connection === 'wss')
9
11
  return connection;
@@ -172,7 +174,7 @@ export function formatInboundMetadata(ev, endpoint) {
172
174
  }
173
175
  /**
174
176
  * canonical MediaRef → OneBot `file` 参数:url 直传、base64 → `base64://`、
175
- * 本地路径 → `file://`(路径在 OneBot 实现侧解析)。
177
+ * 本地路径 → `file://`(路径在 OneBot 实现侧解析)、kind=file 不透明引用原样透传。
176
178
  */
177
179
  export function mediaRefToOneBotFile(media) {
178
180
  if (media.kind === 'base64') {
@@ -184,25 +186,36 @@ export function mediaRefToOneBotFile(media) {
184
186
  return media.value;
185
187
  }
186
188
  /** 媒体段 data 里的 canonical-only 字段,不进 OneBot wire。 */
187
- const MEDIA_DATA_SKIP_KEYS = new Set(['media', 'alt', 'url', 'base64', 'file', 'mime_type']);
189
+ const MEDIA_DATA_SKIP_KEYS = new Set(['media', 'alt']);
190
+ /**
191
+ * image / record / video 段:只消费 canonical `data.media` MediaRef,
192
+ * 映射为 OneBot `file` 参数;无 MediaRef 时 warn + 丢弃(返回 null)。
193
+ */
188
194
  function oneBotMediaSegment(type, data) {
189
- const media = isMediaRef(data.media) ? data.media : mediaRefFromLegacyData(data);
190
- if (!media)
191
- return { type, data };
195
+ if (!isMediaRef(data.media)) {
196
+ logger.warn(formatCompact({
197
+ op: 'onebot11_outbound_media_dropped',
198
+ type,
199
+ reason: 'missing_media_ref',
200
+ }));
201
+ return null;
202
+ }
192
203
  const extra = {};
193
204
  for (const [key, value] of Object.entries(data)) {
194
205
  if (!MEDIA_DATA_SKIP_KEYS.has(key))
195
206
  extra[key] = value;
196
207
  }
197
- return { type, data: { ...extra, file: mediaRefToOneBotFile(media) } };
208
+ return { type, data: { ...extra, file: mediaRefToOneBotFile(data.media) } };
198
209
  }
199
210
  /**
200
211
  * canonical Segment → OneBot 11 数组段:
201
212
  * - mention → at(`qq: target`);
202
213
  * - reply(`message_id`)→ reply(`id`);
203
- * - image / audio→record / video 的 MediaRef → `file`(url / base64:// / file://);
214
+ * - image / audio→record / video 的 MediaRef → `file`(url / base64:// / file:// / 不透明引用);
215
+ * - file 段无 OneBot 11 消息段承载(文件走 upload_*_file API),warn + 丢弃;
204
216
  * - face 取 `id`;
205
217
  * - 其余(已是 wire 形状的段、平台扩展段)原样透传。
218
+ * 返回 null 表示该段被丢弃。
206
219
  */
207
220
  function canonicalToOneBotSegment(segment) {
208
221
  const data = segment.data ?? {};
@@ -230,6 +243,13 @@ function canonicalToOneBotSegment(segment) {
230
243
  return oneBotMediaSegment(segment.type, data);
231
244
  case 'audio':
232
245
  return oneBotMediaSegment('record', data);
246
+ case 'file':
247
+ logger.warn(formatCompact({
248
+ op: 'onebot11_outbound_media_dropped',
249
+ type: 'file',
250
+ reason: 'file_segment_not_supported',
251
+ }));
252
+ return null;
233
253
  default:
234
254
  return { type: segment.type, data };
235
255
  }
@@ -237,7 +257,8 @@ function canonicalToOneBotSegment(segment) {
237
257
  /**
238
258
  * Wire-encode an already-rendered outbound payload into OneBot 11 message segments.
239
259
  * 入参假定已经 core `normalizeOutboundPayload` 归一为 canonical Segment[];
240
- * 旧 wire 形状(at / 裸 `{file,url,base64}`)保持兼容透传。
260
+ * 媒体段只消费 canonical `data.media`(无 MediaRef 的媒体段 warn + 丢弃),
261
+ * 其余 wire 形状段(at / 平台扩展段)原样透传。
241
262
  */
242
263
  export function formatOutboundSegments(payload) {
243
264
  if (typeof payload === 'string') {
@@ -262,7 +283,9 @@ export function formatOutboundSegments(payload) {
262
283
  segs.push({ type: 'text', data: { text: item } });
263
284
  continue;
264
285
  }
265
- segs.push(canonicalToOneBotSegment(item));
286
+ const seg = canonicalToOneBotSegment(item);
287
+ if (seg)
288
+ segs.push(seg);
266
289
  }
267
290
  return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
268
291
  }
@@ -21,6 +21,7 @@ export declare class OneBot11WsEndpoint implements EndpointInstance {
21
21
  readonly target: string;
22
22
  readonly payload: unknown;
23
23
  }): Promise<string>;
24
+ recallMessage(messageId: string): Promise<void>;
24
25
  /** Public API for agent tools / callers. */
25
26
  callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
26
27
  setTitle(groupId: number, userId: number, title: string, duration?: number): Promise<boolean>;
@@ -85,6 +85,11 @@ export class OneBot11WsEndpoint {
85
85
  }));
86
86
  return messageId;
87
87
  }
88
+ async recallMessage(messageId) {
89
+ if (!messageId)
90
+ return;
91
+ await this.callApi('delete_msg', { message_id: Number(messageId) });
92
+ }
88
93
  /** Public API for agent tools / callers. */
89
94
  callApi(action, params = {}) {
90
95
  return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
@@ -21,6 +21,7 @@ export declare class OneBot11WssEndpoint implements EndpointInstance {
21
21
  readonly target: string;
22
22
  readonly payload: unknown;
23
23
  }): Promise<string>;
24
+ recallMessage(messageId: string): Promise<void>;
24
25
  callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
25
26
  setTitle(groupId: number, userId: number, title: string, duration?: number): Promise<boolean>;
26
27
  admit(ev: OneBot11Event): void;
@@ -82,6 +82,11 @@ export class OneBot11WssEndpoint {
82
82
  const data = await this.callApi(action, params);
83
83
  return data?.message_id != null ? String(data.message_id) : '';
84
84
  }
85
+ async recallMessage(messageId) {
86
+ if (!messageId)
87
+ return;
88
+ await this.callApi('delete_msg', { message_id: Number(messageId) });
89
+ }
85
90
  callApi(action, params = {}) {
86
91
  return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
87
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-onebot11",
3
- "version": "5.0.5",
3
+ "version": "6.0.0",
4
4
  "description": "Zhin.js OneBot 11 adapter for Plugin Runtime (WebSocket client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -41,9 +41,9 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "ws": "^8.21.1",
44
- "@zhin.js/adapter": "1.1.2",
45
- "@zhin.js/command": "1.0.4",
46
- "@zhin.js/core": "1.4.3",
44
+ "@zhin.js/adapter": "1.1.3",
45
+ "@zhin.js/command": "1.0.5",
46
+ "@zhin.js/core": "1.5.0",
47
47
  "@zhin.js/host-http": "1.0.4",
48
48
  "@zhin.js/logger": "1.0.75",
49
49
  "@zhin.js/plugin-runtime": "1.1.1"
@@ -54,16 +54,16 @@
54
54
  "typescript": "^6.0.3",
55
55
  "vitest": "^4.1.10",
56
56
  "zod": "^4.4.3",
57
- "@zhin.js/agent": "1.0.10",
57
+ "@zhin.js/agent": "1.1.0",
58
58
  "@zhin.js/host-http": "1.0.4"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "zod": "^4.0.0",
62
- "@zhin.js/adapter": "1.1.2",
63
- "@zhin.js/agent": "1.0.10",
64
- "@zhin.js/core": "1.4.3",
62
+ "@zhin.js/adapter": "1.1.3",
63
+ "@zhin.js/agent": "1.1.0",
64
+ "@zhin.js/core": "1.5.0",
65
65
  "@zhin.js/plugin-runtime": "1.1.1",
66
- "zhin.js": "5.0.3"
66
+ "zhin.js": "6.0.0"
67
67
  },
68
68
  "peerDependenciesMeta": {
69
69
  "zhin.js": {
package/plugin.js CHANGED
@@ -9,7 +9,7 @@ export default definePlugin({
9
9
  displayName: 'OneBot 11 Adapter',
10
10
  },
11
11
  setup(context) {
12
- // 运行中 endpoint 注册表(onebot11 endpoint list 的"运行中"数据源)
12
+ // 运行中 endpoint 注册表(onebot11.endpoint list 的"运行中"数据源)
13
13
  context.resources.provide(onebot11RuntimeStateToken, createEndpointRuntimeState());
14
14
  // 平台权限门禁:scene_admin / scene_owner 由 sender role 判定(见 ws/wss-endpoint admit metadata)
15
15
  return registerDefaultScenePlatformPermitChecker('onebot11');
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `onebot11 endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `onebot11.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
  * OneBot11 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `onebot11 endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `onebot11.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6
 
package/src/protocol.ts CHANGED
@@ -3,7 +3,10 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  * Spec: https://github.com/botuniverse/onebot-11
5
5
  */
6
- import { isMediaRef, mediaRefFromLegacyData, type MediaRef } from '@zhin.js/core';
6
+ import { isMediaRef, type MediaRef } from '@zhin.js/core';
7
+ import { formatCompact, getLogger } from '@zhin.js/logger';
8
+
9
+ const logger = getLogger('onebot11');
7
10
 
8
11
  /** Transitional legacy endpoint row (`endpoints[]` with `context: onebot11`). */
9
12
  export interface OneBot11LegacyEndpointRow {
@@ -295,7 +298,7 @@ export function formatInboundMetadata(
295
298
 
296
299
  /**
297
300
  * canonical MediaRef → OneBot `file` 参数:url 直传、base64 → `base64://`、
298
- * 本地路径 → `file://`(路径在 OneBot 实现侧解析)。
301
+ * 本地路径 → `file://`(路径在 OneBot 实现侧解析)、kind=file 不透明引用原样透传。
299
302
  */
300
303
  export function mediaRefToOneBotFile(media: MediaRef): string {
301
304
  if (media.kind === 'base64') {
@@ -308,30 +311,42 @@ export function mediaRefToOneBotFile(media: MediaRef): string {
308
311
  }
309
312
 
310
313
  /** 媒体段 data 里的 canonical-only 字段,不进 OneBot wire。 */
311
- const MEDIA_DATA_SKIP_KEYS = new Set(['media', 'alt', 'url', 'base64', 'file', 'mime_type']);
314
+ const MEDIA_DATA_SKIP_KEYS = new Set(['media', 'alt']);
312
315
 
316
+ /**
317
+ * image / record / video 段:只消费 canonical `data.media` MediaRef,
318
+ * 映射为 OneBot `file` 参数;无 MediaRef 时 warn + 丢弃(返回 null)。
319
+ */
313
320
  function oneBotMediaSegment(
314
321
  type: 'image' | 'record' | 'video',
315
322
  data: Record<string, unknown>,
316
- ): OneBot11Segment {
317
- const media = isMediaRef(data.media) ? data.media : mediaRefFromLegacyData(data);
318
- if (!media) return { type, data };
323
+ ): OneBot11Segment | null {
324
+ if (!isMediaRef(data.media)) {
325
+ logger.warn(formatCompact({
326
+ op: 'onebot11_outbound_media_dropped',
327
+ type,
328
+ reason: 'missing_media_ref',
329
+ }));
330
+ return null;
331
+ }
319
332
  const extra: Record<string, unknown> = {};
320
333
  for (const [key, value] of Object.entries(data)) {
321
334
  if (!MEDIA_DATA_SKIP_KEYS.has(key)) extra[key] = value;
322
335
  }
323
- return { type, data: { ...extra, file: mediaRefToOneBotFile(media) } };
336
+ return { type, data: { ...extra, file: mediaRefToOneBotFile(data.media) } };
324
337
  }
325
338
 
326
339
  /**
327
340
  * canonical Segment → OneBot 11 数组段:
328
341
  * - mention → at(`qq: target`);
329
342
  * - reply(`message_id`)→ reply(`id`);
330
- * - image / audio→record / video 的 MediaRef → `file`(url / base64:// / file://);
343
+ * - image / audio→record / video 的 MediaRef → `file`(url / base64:// / file:// / 不透明引用);
344
+ * - file 段无 OneBot 11 消息段承载(文件走 upload_*_file API),warn + 丢弃;
331
345
  * - face 取 `id`;
332
346
  * - 其余(已是 wire 形状的段、平台扩展段)原样透传。
347
+ * 返回 null 表示该段被丢弃。
333
348
  */
334
- function canonicalToOneBotSegment(segment: OneBot11WireSegment): OneBot11Segment {
349
+ function canonicalToOneBotSegment(segment: OneBot11WireSegment): OneBot11Segment | null {
335
350
  const data = segment.data ?? {};
336
351
  switch (segment.type) {
337
352
  case 'mention': {
@@ -354,6 +369,13 @@ function canonicalToOneBotSegment(segment: OneBot11WireSegment): OneBot11Segment
354
369
  return oneBotMediaSegment(segment.type, data);
355
370
  case 'audio':
356
371
  return oneBotMediaSegment('record', data);
372
+ case 'file':
373
+ logger.warn(formatCompact({
374
+ op: 'onebot11_outbound_media_dropped',
375
+ type: 'file',
376
+ reason: 'file_segment_not_supported',
377
+ }));
378
+ return null;
357
379
  default:
358
380
  return { type: segment.type, data };
359
381
  }
@@ -362,7 +384,8 @@ function canonicalToOneBotSegment(segment: OneBot11WireSegment): OneBot11Segment
362
384
  /**
363
385
  * Wire-encode an already-rendered outbound payload into OneBot 11 message segments.
364
386
  * 入参假定已经 core `normalizeOutboundPayload` 归一为 canonical Segment[];
365
- * 旧 wire 形状(at / 裸 `{file,url,base64}`)保持兼容透传。
387
+ * 媒体段只消费 canonical `data.media`(无 MediaRef 的媒体段 warn + 丢弃),
388
+ * 其余 wire 形状段(at / 平台扩展段)原样透传。
366
389
  */
367
390
  export function formatOutboundSegments(payload: unknown): OneBot11Segment[] {
368
391
  if (typeof payload === 'string') {
@@ -390,7 +413,8 @@ export function formatOutboundSegments(payload: unknown): OneBot11Segment[] {
390
413
  segs.push({ type: 'text', data: { text: item } });
391
414
  continue;
392
415
  }
393
- segs.push(canonicalToOneBotSegment(item));
416
+ const seg = canonicalToOneBotSegment(item);
417
+ if (seg) segs.push(seg);
394
418
  }
395
419
  return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
396
420
  }
@@ -129,6 +129,11 @@ export class OneBot11WsEndpoint implements EndpointInstance {
129
129
  return messageId;
130
130
  }
131
131
 
132
+ async recallMessage(messageId: string): Promise<void> {
133
+ if (!messageId) return;
134
+ await this.callApi('delete_msg', { message_id: Number(messageId) });
135
+ }
136
+
132
137
  /** Public API for agent tools / callers. */
133
138
  callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
134
139
  return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
@@ -123,6 +123,11 @@ export class OneBot11WssEndpoint implements EndpointInstance {
123
123
  return data?.message_id != null ? String(data.message_id) : '';
124
124
  }
125
125
 
126
+ async recallMessage(messageId: string): Promise<void> {
127
+ if (!messageId) return;
128
+ await this.callApi('delete_msg', { message_id: Number(messageId) });
129
+ }
130
+
126
131
  callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
127
132
  return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
128
133
  }