@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 +3 -3
- package/lib/built/authorization.js +27 -3
- package/lib/built/generic-segment-mapper.d.ts +2 -4
- package/lib/built/generic-segment-mapper.js +64 -106
- package/lib/built/segment-contract/assert.d.ts +4 -1
- package/lib/built/segment-contract/assert.js +15 -6
- package/lib/built/segment-contract/index.d.ts +3 -3
- package/lib/built/segment-contract/index.js +2 -2
- package/lib/built/segment-contract/media.d.ts +1 -7
- package/lib/built/segment-contract/media.js +3 -50
- package/lib/built/segment-contract/preview.js +2 -2
- package/lib/built/segment-contract/types.d.ts +28 -3
- package/lib/built/segment-contract/validate.d.ts +91 -0
- package/lib/built/segment-contract/validate.js +49 -0
- package/lib/plugin-runtime/im/contracts.d.ts +34 -4
- package/lib/plugin-runtime/im/contracts.js +24 -2
- package/lib/plugin-runtime/im/im-runtime.d.ts +28 -9
- package/lib/plugin-runtime/im/im-runtime.js +175 -70
- package/lib/plugin-runtime/im/outbound-renderer.js +4 -1
- package/lib/plugin-runtime/im/outbound-segments.d.ts +13 -14
- package/lib/plugin-runtime/im/outbound-segments.js +37 -65
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -193,8 +193,8 @@ Core **不包含** ZhinAgent 实现。IM 侧的 AI 对话、工具收集、执
|
|
|
193
193
|
|
|
194
194
|
| 类别 | 示例导出 |
|
|
195
195
|
|------|----------|
|
|
196
|
-
| Provider | `
|
|
197
|
-
| Agent 原语 | `
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
import type { MessageElement } from '../types.js';
|
|
2
2
|
import type { Segment } from './segment-contract/types.js';
|
|
3
|
-
/**
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
135
|
-
return
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 (
|
|
17
|
-
return
|
|
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,
|
|
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,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
|
-
*
|
|
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?: {
|