@zhin.js/core 1.4.2 → 1.5.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/README.md CHANGED
@@ -193,8 +193,8 @@ Core **不包含** ZhinAgent 实现。IM 侧的 AI 对话、工具收集、执
193
193
 
194
194
  | 类别 | 示例导出 |
195
195
  |------|----------|
196
- | Provider | `OpenAIProvider`、`OllamaProvider`、`AnthropicProvider` |
197
- | Agent 原语 | `Agent`、`createAgent`、`ModelRegistry` |
196
+ | Provider | `AIProvider` 接口、`createSdkProviderAdapter`(AI SDK 传输) |
197
+ | Agent 原语 | `ModelRegistry`、`agentLoop` |
198
198
  | 会话 / 上下文 | `ContextRepository`、`ImTranscriptStore`、`ContextManager`、`ConversationMemory` |
199
199
  | 压缩 / 限流 / 输出 | `compactSession`、`RateLimiter`、`parseOutput`、`CostTracker` |
200
200
 
@@ -233,7 +233,7 @@ export { htmlToFallbackText, coerceHtmlSegmentsToText, registerHtmlSegmentFallba
233
233
 
234
234
  // AI 原语(来自 @zhin.js/ai,非 ZhinAgent)
235
235
  export {
236
- OpenAIProvider, OllamaProvider, Agent, createAgent, ModelRegistry,
236
+ AIProvider, createSdkProviderAdapter, ModelRegistry,
237
237
  ContextRepository, ContextManager, ConversationMemory, compactSession, ...
238
238
  } from '@zhin.js/ai'
239
239
  ```
@@ -2,10 +2,34 @@ import { mergeAITriggerConfig, resolveSenderRoles, } from './ai-trigger.js';
2
2
  import { formatCompact, getLogger } from '@zhin.js/logger';
3
3
  const logger = getLogger('Authorization');
4
4
  function findEndpointEntryFromConfig(config, adapter, endpointId) {
5
- const endpoints = config.endpoints;
6
- if (!Array.isArray(endpoints))
5
+ // 1) top-level endpoints[] (legacy / flat format)
6
+ const topLevel = config.endpoints;
7
+ if (Array.isArray(topLevel)) {
8
+ const found = topLevel.find((b) => b.context === adapter && String(b.name) === endpointId);
9
+ if (found)
10
+ return found;
11
+ }
12
+ // 2) plugins.<adapter> — adapter-level master/trusted + nested endpoints[]
13
+ const adapterConfig = config.plugins?.[adapter];
14
+ if (!adapterConfig)
7
15
  return undefined;
8
- return endpoints.find((b) => b.context === adapter && String(b.name) === endpointId);
16
+ const nested = adapterConfig.endpoints;
17
+ const entry = Array.isArray(nested)
18
+ ? nested.find((b) => String(b.name) === endpointId)
19
+ : undefined;
20
+ // Merge adapter-level master/trusted onto the matched endpoint entry
21
+ // so that resolveSenderRoles sees them in one place
22
+ const merged = {
23
+ context: adapter,
24
+ ...(entry ?? { name: endpointId }),
25
+ };
26
+ if (adapterConfig.master != null && merged.master == null) {
27
+ merged.master = adapterConfig.master;
28
+ }
29
+ if (adapterConfig.trusted != null && merged.trusted == null) {
30
+ merged.trusted = adapterConfig.trusted;
31
+ }
32
+ return merged;
9
33
  }
10
34
  function readTriggerConfig(plugin) {
11
35
  const root = plugin.root ?? plugin;
@@ -1,6 +1,4 @@
1
- import type { MessageElement, SendContent } from '../types.js';
1
+ import type { MessageElement } from '../types.js';
2
2
  import type { Segment } from './segment-contract/types.js';
3
- /** 通用 IM adapter:legacy wire → canonical Segment[] */
3
+ /** wire 段数组 → canonical Segment[](包括所有媒体的 `data.media` 归一)。 */
4
4
  export declare function toCanonicalSegments(content: readonly MessageElement[] | readonly unknown[]): Segment[];
5
- /** canonical → wire(mention→at;image MediaRef→legacy 字段) */
6
- export declare function fromCanonicalSegments(content: SendContent): MessageElement[];
@@ -1,4 +1,4 @@
1
- import { createImageSegment, isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields, readMentionTarget, } from './segment-contract/index.js';
1
+ import { isMediaRef, readMentionTarget, } from './segment-contract/index.js';
2
2
  function isRecord(value) {
3
3
  return typeof value === 'object' && value !== null && !Array.isArray(value);
4
4
  }
@@ -17,38 +17,68 @@ function normalizeMention(seg) {
17
17
  ...(platform ? { platform } : {}),
18
18
  };
19
19
  }
20
- function normalizeImage(seg) {
20
+ const MEDIA_SEGMENT_TYPES = new Set(['image', 'audio', 'video', 'file']);
21
+ function nonEmptyString(value) {
22
+ return typeof value === 'string' && value.trim() ? value : undefined;
23
+ }
24
+ function legacyMediaRef(data) {
25
+ if (isMediaRef(data.media))
26
+ return data.media;
27
+ const url = nonEmptyString(data.url) ?? nonEmptyString(data.href) ?? nonEmptyString(data.src);
28
+ const path = nonEmptyString(data.path) ?? nonEmptyString(data.file_path);
29
+ const base64 = nonEmptyString(data.base64);
30
+ const file = nonEmptyString(data.file) ?? nonEmptyString(data.file_id);
31
+ const value = url ?? path ?? base64 ?? file;
32
+ if (!value)
33
+ return undefined;
34
+ const kind = url ? 'url'
35
+ : path ? 'path'
36
+ : base64 ? 'base64'
37
+ : /^(?:[a-z][a-z0-9+.-]*:)?\/\//iu.test(value) ? 'url'
38
+ : /^(?:\.{1,2}\/|\/|~\/)/u.test(value) ? 'path'
39
+ : 'file';
40
+ const mimeType = nonEmptyString(data.mime_type) ?? nonEmptyString(data.mimeType);
41
+ // Legacy top-level fileName names the rendered file segment. Keep source
42
+ // metadata reserved for an explicit media object instead of duplicating it.
43
+ const fileName = nonEmptyString(data.media_file_name);
44
+ const size = typeof data.size === 'number' && Number.isFinite(data.size) ? data.size : undefined;
45
+ return {
46
+ kind,
47
+ value,
48
+ ...(mimeType ? { mime_type: mimeType } : {}),
49
+ ...(fileName ? { file_name: fileName } : {}),
50
+ ...(size === undefined ? {} : { size }),
51
+ };
52
+ }
53
+ /**
54
+ * The sole ingress compatibility boundary for media. Adapters may emit their
55
+ * native `url`/`path`/`file` fields, but every consumer receives `data.media`.
56
+ */
57
+ function normalizeMedia(seg) {
21
58
  const data = seg.data;
22
59
  const platform = readPlatform(seg);
23
- if (isMediaRef(data.media)) {
24
- return {
25
- type: 'image',
26
- data: {
27
- media: data.media,
28
- ...(typeof data.alt === 'string' ? { alt: data.alt } : {}),
29
- },
30
- ...(platform ? { platform } : {}),
31
- };
32
- }
33
- const media = mediaRefFromLegacyData(data);
34
- if (media) {
35
- const mergedPlatform = { ...(platform ?? {}) };
36
- for (const key of ['url', 'file', 'src', 'file_id']) {
37
- if (typeof data[key] === 'string' && data[key])
38
- mergedPlatform[key] = data[key];
39
- }
40
- const platformKeys = Object.keys(mergedPlatform);
41
- const legacyPlatform = platformKeys.length === 0
42
- ? undefined
43
- : platformKeys.length === 1 && mergedPlatform[platformKeys[0]] === media.value
44
- ? undefined
45
- : mergedPlatform;
46
- return createImageSegment(media, {
47
- alt: typeof data.alt === 'string' ? data.alt : undefined,
48
- platform: legacyPlatform,
49
- });
50
- }
51
- return seg;
60
+ const media = legacyMediaRef(data);
61
+ if (!media)
62
+ return seg;
63
+ const attributes = seg.type === 'image'
64
+ ? { ...(typeof data.alt === 'string' ? { alt: data.alt } : {}) }
65
+ : seg.type === 'audio'
66
+ ? { ...(typeof data.duration === 'number' ? { duration: data.duration } : {}) }
67
+ : seg.type === 'video'
68
+ ? {
69
+ ...(typeof data.duration === 'number' ? { duration: data.duration } : {}),
70
+ ...(typeof data.alt === 'string' ? { alt: data.alt } : {}),
71
+ }
72
+ : {
73
+ ...(nonEmptyString(data.name) ?? nonEmptyString(data.file_name) ?? nonEmptyString(data.fileName)
74
+ ? { name: nonEmptyString(data.name) ?? nonEmptyString(data.file_name) ?? nonEmptyString(data.fileName) }
75
+ : {}),
76
+ };
77
+ return {
78
+ type: seg.type,
79
+ data: { media, ...attributes },
80
+ ...(platform ? { platform } : {}),
81
+ };
52
82
  }
53
83
  function normalizeReply(seg) {
54
84
  const data = seg.data;
@@ -131,8 +161,8 @@ function normalizeMarkdown(seg) {
131
161
  function normalizeSegment(seg) {
132
162
  if (seg.type === 'at' || seg.type === 'mention')
133
163
  return normalizeMention(seg);
134
- if (seg.type === 'image')
135
- return normalizeImage(seg);
164
+ if (MEDIA_SEGMENT_TYPES.has(seg.type))
165
+ return normalizeMedia(seg);
136
166
  if (seg.type === 'reply')
137
167
  return normalizeReply(seg);
138
168
  if (seg.type === 'forward')
@@ -154,79 +184,7 @@ function asMessageSegments(content) {
154
184
  return item;
155
185
  });
156
186
  }
157
- function asCanonicalSegments(content) {
158
- if (typeof content === 'string') {
159
- return [{ type: 'text', data: { text: content } }];
160
- }
161
- const items = Array.isArray(content) ? content : [content];
162
- return items.map((item) => {
163
- if (typeof item === 'string')
164
- return { type: 'text', data: { text: item } };
165
- return item;
166
- });
167
- }
168
- /** 通用 IM adapter:legacy wire → canonical Segment[] */
187
+ /** wire 段数组 → canonical Segment[](包括所有媒体的 `data.media` 归一)。 */
169
188
  export function toCanonicalSegments(content) {
170
189
  return asMessageSegments(content).map((seg) => normalizeSegment(seg));
171
190
  }
172
- function mapCanonicalToWire(seg) {
173
- if (seg.type === 'image' && isRecord(seg.data) && seg.data.media) {
174
- const media = seg.data.media;
175
- const legacy = mediaRefToLegacyFields(media);
176
- return {
177
- type: 'image',
178
- data: {
179
- ...legacy,
180
- media,
181
- ...(typeof seg.data.alt === 'string' ? { alt: seg.data.alt } : {}),
182
- },
183
- ...(seg.platform ? { platform: seg.platform } : {}),
184
- };
185
- }
186
- if (seg.type === 'mention') {
187
- const data = seg.data;
188
- return {
189
- type: 'at',
190
- data: {
191
- id: data.target,
192
- ...(data.name ? { name: data.name } : {}),
193
- },
194
- ...(seg.platform ? { platform: seg.platform } : {}),
195
- };
196
- }
197
- if (seg.type === 'reply') {
198
- const messageId = String(seg.data.message_id);
199
- return {
200
- type: 'reply',
201
- data: { id: messageId, message_id: messageId },
202
- ...(seg.platform ? { platform: seg.platform } : {}),
203
- };
204
- }
205
- if (seg.type === 'forward') {
206
- const data = seg.data;
207
- const resid = seg.platform?.resid ?? data.forward_id;
208
- return {
209
- type: 'forward',
210
- data: {
211
- id: data.forward_id,
212
- resid,
213
- ...(data.title ? { title: data.title } : {}),
214
- ...(data.messages ? { messages: data.messages } : {}),
215
- },
216
- ...(seg.platform ? { platform: seg.platform } : {}),
217
- };
218
- }
219
- if (seg.type === 'link') {
220
- const data = seg.data;
221
- return {
222
- type: 'link',
223
- data: { url: data.url, ...(data.text ? { text: data.text } : {}) },
224
- ...(seg.platform ? { platform: seg.platform } : {}),
225
- };
226
- }
227
- return seg;
228
- }
229
- /** canonical → wire(mention→at;image MediaRef→legacy 字段) */
230
- export function fromCanonicalSegments(content) {
231
- return asCanonicalSegments(content).map((seg) => mapCanonicalToWire(seg));
232
- }
@@ -1,4 +1,7 @@
1
1
  import type { Segment } from './types.js';
2
- /** 宽松判断:已知 canonical type 走严格校验;未知 type 仅校验顶层形状(adapter 渐进迁移) */
2
+ /**
3
+ * 内建 canonical 类型始终走 schema 严格校验。Core 预留扩展和命名空间扩展
4
+ * 只承诺稳定顶层形状;裸未知类型会被拒绝,避免拼写错误绕过 canonical 校验。
5
+ */
3
6
  export declare function isCanonicalSegment(value: unknown): value is Segment;
4
7
  export declare function assertCanonicalSegments(segments: unknown): asserts segments is Segment[];
@@ -1,11 +1,21 @@
1
1
  import { isStrictCanonicalSegment } from './validate.js';
2
2
  const STRICT_CANONICAL_TYPES = new Set([
3
- 'text', 'mention', 'image', 'reply', 'forward', 'face', 'dice', 'rps',
3
+ 'text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'forward', 'face', 'dice', 'rps',
4
4
  ]);
5
+ // These are Core-owned wire extensions rather than canonical data types. Keep
6
+ // their gradual contracts isolated here; third-party extensions must be
7
+ // namespaced so a future canonical type cannot silently collide with them.
8
+ const CORE_EXTENSION_TYPES = new Set([
9
+ 'action', 'html', 'keyboard', 'link', 'markdown', 'qrcode', 'record', 'tts', 'voice',
10
+ ]);
11
+ const extensionTypePattern = /^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*)+$/u;
5
12
  function isPlainObject(value) {
6
13
  return typeof value === 'object' && value !== null && !Array.isArray(value);
7
14
  }
8
- /** 宽松判断:已知 canonical type 走严格校验;未知 type 仅校验顶层形状(adapter 渐进迁移) */
15
+ /**
16
+ * 内建 canonical 类型始终走 schema 严格校验。Core 预留扩展和命名空间扩展
17
+ * 只承诺稳定顶层形状;裸未知类型会被拒绝,避免拼写错误绕过 canonical 校验。
18
+ */
9
19
  export function isCanonicalSegment(value) {
10
20
  if (!isPlainObject(value) || typeof value.type !== 'string')
11
21
  return false;
@@ -13,10 +23,9 @@ export function isCanonicalSegment(value) {
13
23
  return false;
14
24
  if (value.platform !== undefined && !isPlainObject(value.platform))
15
25
  return false;
16
- if (!STRICT_CANONICAL_TYPES.has(value.type)) {
17
- return true;
18
- }
19
- return isStrictCanonicalSegment(value);
26
+ if (STRICT_CANONICAL_TYPES.has(value.type))
27
+ return isStrictCanonicalSegment(value);
28
+ return CORE_EXTENSION_TYPES.has(value.type) || extensionTypePattern.test(value.type);
20
29
  }
21
30
  export function assertCanonicalSegments(segments) {
22
31
  if (!Array.isArray(segments)) {
@@ -1,9 +1,9 @@
1
- export type { Segment, SegmentBase, MediaRef, TextSegment, MentionSegment, ImageSegment, ReplySegment, ForwardSegment, FaceSegment, DiceSegment, RpsSegment, } from './types.js';
2
- export { mediaRefSchema, textSegmentSchema, mentionSegmentSchema, imageSegmentSchema, replySegmentSchema, forwardSegmentSchema, faceSegmentSchema, diceSegmentSchema, rpsSegmentSchema, canonicalSegmentSchema, segmentArraySchema, } from './validate.js';
1
+ export type { Segment, SegmentBase, MediaRef, TextSegment, MentionSegment, ImageSegment, AudioSegment, VideoSegment, FileSegment, ReplySegment, ForwardSegment, FaceSegment, DiceSegment, RpsSegment, } from './types.js';
2
+ export { mediaRefSchema, textSegmentSchema, mentionSegmentSchema, imageSegmentSchema, audioSegmentSchema, videoSegmentSchema, fileSegmentSchema, replySegmentSchema, forwardSegmentSchema, faceSegmentSchema, diceSegmentSchema, rpsSegmentSchema, canonicalSegmentSchema, segmentArraySchema, } from './validate.js';
3
3
  export { assertCanonicalSegments, isCanonicalSegment } from './assert.js';
4
4
  export { mediaRefJsonSchema, outboundSegmentJsonSchema, aiOutboundJsonSchema, STRICT_OUTBOUND_SEGMENT_TYPES, } from './json-schema.js';
5
5
  export { segmentsForImDelivery } from './delivery.js';
6
- export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields, collectSegmentMedia, type SegmentMediaRef, } from './media.js';
6
+ export { isMediaRef, collectSegmentMedia, type SegmentMediaRef, } from './media.js';
7
7
  export { createImageSegment } from './image.js';
8
8
  export { formatSegmentPreview } from './preview.js';
9
9
  export { segmentsToPlainText } from './text.js';
@@ -1,8 +1,8 @@
1
- export { mediaRefSchema, textSegmentSchema, mentionSegmentSchema, imageSegmentSchema, replySegmentSchema, forwardSegmentSchema, faceSegmentSchema, diceSegmentSchema, rpsSegmentSchema, canonicalSegmentSchema, segmentArraySchema, } from './validate.js';
1
+ export { mediaRefSchema, textSegmentSchema, mentionSegmentSchema, imageSegmentSchema, audioSegmentSchema, videoSegmentSchema, fileSegmentSchema, replySegmentSchema, forwardSegmentSchema, faceSegmentSchema, diceSegmentSchema, rpsSegmentSchema, canonicalSegmentSchema, segmentArraySchema, } from './validate.js';
2
2
  export { assertCanonicalSegments, isCanonicalSegment } from './assert.js';
3
3
  export { mediaRefJsonSchema, outboundSegmentJsonSchema, aiOutboundJsonSchema, STRICT_OUTBOUND_SEGMENT_TYPES, } from './json-schema.js';
4
4
  export { segmentsForImDelivery } from './delivery.js';
5
- export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields, collectSegmentMedia, } from './media.js';
5
+ export { isMediaRef, collectSegmentMedia, } from './media.js';
6
6
  export { createImageSegment } from './image.js';
7
7
  export { formatSegmentPreview } from './preview.js';
8
8
  export { segmentsToPlainText } from './text.js';
@@ -1,18 +1,12 @@
1
1
  import type { MediaRef, Segment } from './types.js';
2
2
  import { isMediaRef } from './validate.js';
3
3
  export { isMediaRef };
4
- export declare function mediaRefFromLegacyData(data: Record<string, unknown>): MediaRef | undefined;
5
- export declare function mediaRefToLegacyFields(media: MediaRef): {
6
- url?: string;
7
- file?: string;
8
- };
9
4
  export interface SegmentMediaRef {
10
5
  readonly type: string;
11
6
  readonly media: MediaRef;
12
7
  }
13
8
  /**
14
9
  * 从入站 canonical 段收集媒体引用(image / audio / video / file)。
15
- * 兼容 canonical `data.media` 与旧轨 url/file/base64 字段;
16
- * 无媒体段时返回空数组(调用方无需特判 undefined)。
10
+ * 只认 canonical `data.media`;无媒体段时返回空数组(调用方无需特判 undefined)。
17
11
  */
18
12
  export declare function collectSegmentMedia(segments: readonly Segment[] | undefined): SegmentMediaRef[];
@@ -1,57 +1,10 @@
1
1
  import { isMediaRef } from './validate.js';
2
2
  export { isMediaRef };
3
- export function mediaRefFromLegacyData(data) {
4
- if (isMediaRef(data.media)) {
5
- return data.media;
6
- }
7
- const mimeType = typeof data.mime_type === 'string' ? data.mime_type : undefined;
8
- // 平台不透明文件引用(Telegram file_id / Milky resource_id 等)
9
- const fileRef = typeof data.file_id === 'string' && data.file_id.trim()
10
- ? data.file_id.trim()
11
- : undefined;
12
- if (fileRef) {
13
- return { kind: 'file', value: fileRef, ...(mimeType ? { mime_type: mimeType } : {}) };
14
- }
15
- const base64 = typeof data.base64 === 'string' && data.base64.trim()
16
- ? data.base64.trim()
17
- : typeof data.data === 'string' && data.data.trim() && !String(data.data).startsWith('http')
18
- ? data.data.trim()
19
- : undefined;
20
- if (base64) {
21
- return { kind: 'base64', value: base64, ...(mimeType ? { mime_type: mimeType } : {}) };
22
- }
23
- const raw = [data.url, data.file, data.src, data.href]
24
- .find((v) => typeof v === 'string' && v.trim().length > 0);
25
- if (!raw)
26
- return undefined;
27
- const value = raw.trim();
28
- if (value.startsWith('base64://')) {
29
- return { kind: 'base64', value: value.slice('base64://'.length), ...(mimeType ? { mime_type: mimeType } : {}) };
30
- }
31
- if (value.startsWith('http://') || value.startsWith('https://')) {
32
- return { kind: 'url', value, ...(mimeType ? { mime_type: mimeType } : {}) };
33
- }
34
- if (value.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(value)) {
35
- return { kind: 'path', value, ...(mimeType ? { mime_type: mimeType } : {}) };
36
- }
37
- return { kind: 'url', value, ...(mimeType ? { mime_type: mimeType } : {}) };
38
- }
39
- export function mediaRefToLegacyFields(media) {
40
- if (media.kind === 'url')
41
- return { url: media.value, file: media.value };
42
- if (media.kind === 'path')
43
- return { file: media.value, url: media.value };
44
- if (media.kind === 'file')
45
- return { file: media.value, url: media.value };
46
- const encoded = media.value.startsWith('base64://') ? media.value : `base64://${media.value}`;
47
- return { file: encoded, url: encoded };
48
- }
49
3
  /** 入站段携带媒体引用的段类型(纯文本视图无法承载的信息)。 */
50
4
  const MEDIA_SEGMENT_TYPES = new Set(['image', 'audio', 'video', 'file']);
51
5
  /**
52
6
  * 从入站 canonical 段收集媒体引用(image / audio / video / file)。
53
- * 兼容 canonical `data.media` 与旧轨 url/file/base64 字段;
54
- * 无媒体段时返回空数组(调用方无需特判 undefined)。
7
+ * 只认 canonical `data.media`;无媒体段时返回空数组(调用方无需特判 undefined)。
55
8
  */
56
9
  export function collectSegmentMedia(segments) {
57
10
  if (!segments?.length)
@@ -61,8 +14,8 @@ export function collectSegmentMedia(segments) {
61
14
  if (!segment || typeof segment.type !== 'string' || !MEDIA_SEGMENT_TYPES.has(segment.type)) {
62
15
  continue;
63
16
  }
64
- const media = mediaRefFromLegacyData(segment.data ?? {});
65
- if (media)
17
+ const media = segment.data?.media;
18
+ if (isMediaRef(media))
66
19
  out.push({ type: segment.type, media });
67
20
  }
68
21
  return out;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { htmlToFallbackText } from '../html-to-text.js';
6
6
  import { readMentionName, readMentionTarget } from './mention.js';
7
- import { mediaRefFromLegacyData } from './media.js';
7
+ import { isMediaRef } from './validate.js';
8
8
  const PREVIEW_MAX = 80;
9
9
  function clip(text, max = PREVIEW_MAX) {
10
10
  const normalized = text.trim();
@@ -13,7 +13,7 @@ function clip(text, max = PREVIEW_MAX) {
13
13
  return `${normalized.slice(0, max)}…`;
14
14
  }
15
15
  function previewMedia(data) {
16
- const media = mediaRefFromLegacyData(data);
16
+ const media = isMediaRef(data.media) ? data.media : undefined;
17
17
  if (!media)
18
18
  return undefined;
19
19
  if (media.kind === 'base64') {
@@ -8,7 +8,7 @@ export interface SegmentBase {
8
8
  platform?: Record<string, unknown>;
9
9
  }
10
10
  /**
11
- * 媒体引用占位(完整 schema 随 adapter 迁移补齐)。
11
+ * 媒体引用占位(全框架唯一媒体表达)。
12
12
  * kind=file:平台侧不透明文件引用(如 Telegram file_id、Milky resource_id),
13
13
  * 非 URL/本地路径,消费方需经平台 API 解析。
14
14
  */
@@ -16,6 +16,9 @@ export interface MediaRef {
16
16
  kind: 'url' | 'path' | 'base64' | 'file';
17
17
  value: string;
18
18
  mime_type?: string;
19
+ file_name?: string;
20
+ /** 字节数(已知时携带,供大小预检与日志) */
21
+ size?: number;
19
22
  }
20
23
  export interface TextSegment extends SegmentBase {
21
24
  type: 'text';
@@ -37,6 +40,28 @@ export interface ImageSegment extends SegmentBase {
37
40
  alt?: string;
38
41
  };
39
42
  }
43
+ export interface AudioSegment extends SegmentBase {
44
+ type: 'audio';
45
+ data: {
46
+ media: MediaRef;
47
+ duration?: number;
48
+ };
49
+ }
50
+ export interface VideoSegment extends SegmentBase {
51
+ type: 'video';
52
+ data: {
53
+ media: MediaRef;
54
+ duration?: number;
55
+ alt?: string;
56
+ };
57
+ }
58
+ export interface FileSegment extends SegmentBase {
59
+ type: 'file';
60
+ data: {
61
+ media: MediaRef;
62
+ name?: string;
63
+ };
64
+ }
40
65
  export interface ReplySegment extends SegmentBase {
41
66
  type: 'reply';
42
67
  data: {
@@ -70,5 +95,5 @@ export interface RpsSegment extends SegmentBase {
70
95
  result?: number;
71
96
  };
72
97
  }
73
- /** 规范态 segment(严格校验 text / mention / image / reply / forward / face / dice / rps) */
74
- export type Segment = TextSegment | MentionSegment | ImageSegment | ReplySegment | ForwardSegment | FaceSegment | DiceSegment | RpsSegment | SegmentBase;
98
+ /** 规范态 segment(严格校验 text / mention / image / audio / video / file / reply / forward / face / dice / rps) */
99
+ export type Segment = TextSegment | MentionSegment | ImageSegment | AudioSegment | VideoSegment | FileSegment | ReplySegment | ForwardSegment | FaceSegment | DiceSegment | RpsSegment | SegmentBase;
@@ -4,10 +4,14 @@ export declare const mediaRefSchema: Schema<{
4
4
  kind?: any;
5
5
  value?: string | undefined;
6
6
  mime_type?: string | undefined;
7
+ file_name?: string | undefined;
8
+ size?: number | undefined;
7
9
  }, {
8
10
  kind?: any;
9
11
  value?: string | undefined;
10
12
  mime_type?: string | undefined;
13
+ file_name?: string | undefined;
14
+ size?: number | undefined;
11
15
  }>;
12
16
  export declare const textSegmentSchema: Schema<{
13
17
  type?: "text" | undefined;
@@ -44,6 +48,8 @@ export declare const imageSegmentSchema: Schema<{
44
48
  kind?: any;
45
49
  value?: string | undefined;
46
50
  mime_type?: string | undefined;
51
+ file_name?: string | undefined;
52
+ size?: number | undefined;
47
53
  } | undefined;
48
54
  alt?: string | undefined;
49
55
  } | undefined;
@@ -55,11 +61,96 @@ export declare const imageSegmentSchema: Schema<{
55
61
  kind?: any;
56
62
  value?: string | undefined;
57
63
  mime_type?: string | undefined;
64
+ file_name?: string | undefined;
65
+ size?: number | undefined;
58
66
  } | undefined;
59
67
  alt?: string | undefined;
60
68
  } | undefined;
61
69
  platform?: Record<string, any> | undefined;
62
70
  }>;
71
+ export declare const audioSegmentSchema: Schema<{
72
+ type?: "audio" | undefined;
73
+ data?: {
74
+ media?: {
75
+ kind?: any;
76
+ value?: string | undefined;
77
+ mime_type?: string | undefined;
78
+ file_name?: string | undefined;
79
+ size?: number | undefined;
80
+ } | undefined;
81
+ duration?: number | undefined;
82
+ } | undefined;
83
+ platform?: Record<string, any> | undefined;
84
+ }, {
85
+ type?: "audio" | undefined;
86
+ data?: {
87
+ media?: {
88
+ kind?: any;
89
+ value?: string | undefined;
90
+ mime_type?: string | undefined;
91
+ file_name?: string | undefined;
92
+ size?: number | undefined;
93
+ } | undefined;
94
+ duration?: number | undefined;
95
+ } | undefined;
96
+ platform?: Record<string, any> | undefined;
97
+ }>;
98
+ export declare const videoSegmentSchema: Schema<{
99
+ type?: "video" | undefined;
100
+ data?: {
101
+ media?: {
102
+ kind?: any;
103
+ value?: string | undefined;
104
+ mime_type?: string | undefined;
105
+ file_name?: string | undefined;
106
+ size?: number | undefined;
107
+ } | undefined;
108
+ duration?: number | undefined;
109
+ alt?: string | undefined;
110
+ } | undefined;
111
+ platform?: Record<string, any> | undefined;
112
+ }, {
113
+ type?: "video" | undefined;
114
+ data?: {
115
+ media?: {
116
+ kind?: any;
117
+ value?: string | undefined;
118
+ mime_type?: string | undefined;
119
+ file_name?: string | undefined;
120
+ size?: number | undefined;
121
+ } | undefined;
122
+ duration?: number | undefined;
123
+ alt?: string | undefined;
124
+ } | undefined;
125
+ platform?: Record<string, any> | undefined;
126
+ }>;
127
+ export declare const fileSegmentSchema: Schema<{
128
+ type?: "file" | undefined;
129
+ data?: {
130
+ media?: {
131
+ kind?: any;
132
+ value?: string | undefined;
133
+ mime_type?: string | undefined;
134
+ file_name?: string | undefined;
135
+ size?: number | undefined;
136
+ } | undefined;
137
+ name?: string | undefined;
138
+ } | undefined;
139
+ platform?: Record<string, any> | undefined;
140
+ }, {
141
+ type?: "file" | undefined;
142
+ data?: {
143
+ media?: {
144
+ kind?: any;
145
+ value?: string | undefined;
146
+ mime_type?: string | undefined;
147
+ file_name?: string | undefined;
148
+ size?: number | undefined;
149
+ } | undefined;
150
+ name?: string | undefined;
151
+ } | undefined;
152
+ platform?: Record<string, any> | undefined;
153
+ }>;
63
154
  export declare const replySegmentSchema: Schema<{
64
155
  type?: "reply" | undefined;
65
156
  data?: {