@canonmsg/backend-contracts 0.1.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/dist/agentBehaviorPolicy.d.ts +59 -0
- package/dist/agentBehaviorPolicy.js +240 -0
- package/dist/cjs/agentBehaviorPolicy.js +250 -0
- package/dist/cjs/contactRequest.js +72 -0
- package/dist/cjs/index.js +21 -0
- package/dist/cjs/media.js +101 -0
- package/dist/cjs/message.js +122 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/turnProtocol.js +83 -0
- package/dist/contactRequest.d.ts +20 -0
- package/dist/contactRequest.js +69 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/media.d.ts +24 -0
- package/dist/media.js +93 -0
- package/dist/message.d.ts +47 -0
- package/dist/message.js +119 -0
- package/dist/turnProtocol.d.ts +32 -0
- package/dist/turnProtocol.js +75 -0
- package/package.json +48 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inferMediaAttachmentKind = inferMediaAttachmentKind;
|
|
4
|
+
exports.getStoredFileExtension = getStoredFileExtension;
|
|
5
|
+
exports.normalizeStoredAttachments = normalizeStoredAttachments;
|
|
6
|
+
exports.getMessageAttachments = getMessageAttachments;
|
|
7
|
+
exports.getPrimaryAttachment = getPrimaryAttachment;
|
|
8
|
+
exports.describeAttachment = describeAttachment;
|
|
9
|
+
function inferMediaAttachmentKind(mimeType) {
|
|
10
|
+
if (mimeType.startsWith('image/'))
|
|
11
|
+
return 'image';
|
|
12
|
+
if (mimeType.startsWith('audio/'))
|
|
13
|
+
return 'audio';
|
|
14
|
+
return 'file';
|
|
15
|
+
}
|
|
16
|
+
function getStoredFileExtension(input) {
|
|
17
|
+
const mimeType = input.mimeType?.toLowerCase().trim() || '';
|
|
18
|
+
const explicitByMime = {
|
|
19
|
+
'image/jpeg': 'jpg',
|
|
20
|
+
'image/jpg': 'jpg',
|
|
21
|
+
'image/png': 'png',
|
|
22
|
+
'image/webp': 'webp',
|
|
23
|
+
'image/gif': 'gif',
|
|
24
|
+
'audio/mp4': 'm4a',
|
|
25
|
+
'audio/x-m4a': 'm4a',
|
|
26
|
+
'audio/mpeg': 'mp3',
|
|
27
|
+
'audio/webm': 'webm',
|
|
28
|
+
'audio/ogg': 'ogg',
|
|
29
|
+
'application/pdf': 'pdf',
|
|
30
|
+
'text/plain': 'txt',
|
|
31
|
+
'application/json': 'json',
|
|
32
|
+
'application/zip': 'zip',
|
|
33
|
+
};
|
|
34
|
+
if (explicitByMime[mimeType])
|
|
35
|
+
return explicitByMime[mimeType];
|
|
36
|
+
const trimmedName = input.fileName?.trim() || '';
|
|
37
|
+
const lastDot = trimmedName.lastIndexOf('.');
|
|
38
|
+
if (lastDot > 0 && lastDot < trimmedName.length - 1) {
|
|
39
|
+
const fromName = trimmedName.slice(lastDot + 1).toLowerCase();
|
|
40
|
+
if (/^[a-z0-9]{1,10}$/.test(fromName))
|
|
41
|
+
return fromName;
|
|
42
|
+
}
|
|
43
|
+
const slashIndex = mimeType.indexOf('/');
|
|
44
|
+
if (slashIndex !== -1 && slashIndex < mimeType.length - 1) {
|
|
45
|
+
const subtype = mimeType.slice(slashIndex + 1).split('+')[0].toLowerCase();
|
|
46
|
+
if (/^[a-z0-9.-]{1,20}$/.test(subtype)) {
|
|
47
|
+
if (subtype === 'plain')
|
|
48
|
+
return 'txt';
|
|
49
|
+
if (subtype === 'octet-stream')
|
|
50
|
+
return 'bin';
|
|
51
|
+
return subtype.replace(/[^a-z0-9]/g, '') || 'bin';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return 'bin';
|
|
55
|
+
}
|
|
56
|
+
function normalizeStoredAttachments(value) {
|
|
57
|
+
if (!Array.isArray(value))
|
|
58
|
+
return null;
|
|
59
|
+
const attachments = [];
|
|
60
|
+
for (const item of value) {
|
|
61
|
+
if (!item || typeof item !== 'object')
|
|
62
|
+
return null;
|
|
63
|
+
const candidate = item;
|
|
64
|
+
if (candidate.kind !== 'image'
|
|
65
|
+
&& candidate.kind !== 'audio'
|
|
66
|
+
&& candidate.kind !== 'file') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (typeof candidate.url !== 'string' || candidate.url.length === 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
attachments.push({
|
|
73
|
+
kind: candidate.kind,
|
|
74
|
+
url: candidate.url,
|
|
75
|
+
...(typeof candidate.mimeType === 'string' && candidate.mimeType ? { mimeType: candidate.mimeType } : {}),
|
|
76
|
+
...(typeof candidate.fileName === 'string' && candidate.fileName ? { fileName: candidate.fileName } : {}),
|
|
77
|
+
...(typeof candidate.sizeBytes === 'number' ? { sizeBytes: candidate.sizeBytes } : {}),
|
|
78
|
+
...(typeof candidate.width === 'number' ? { width: candidate.width } : {}),
|
|
79
|
+
...(typeof candidate.height === 'number' ? { height: candidate.height } : {}),
|
|
80
|
+
...(typeof candidate.durationMs === 'number' ? { durationMs: candidate.durationMs } : {}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return attachments.length > 0 ? attachments : [];
|
|
84
|
+
}
|
|
85
|
+
function getMessageAttachments(input) {
|
|
86
|
+
const normalizedAttachments = normalizeStoredAttachments(input.attachments);
|
|
87
|
+
return normalizedAttachments ?? [];
|
|
88
|
+
}
|
|
89
|
+
function getPrimaryAttachment(input) {
|
|
90
|
+
const attachments = getMessageAttachments(input);
|
|
91
|
+
return attachments[0] ?? null;
|
|
92
|
+
}
|
|
93
|
+
function describeAttachment(attachment) {
|
|
94
|
+
if (!attachment)
|
|
95
|
+
return null;
|
|
96
|
+
if (attachment.kind === 'image')
|
|
97
|
+
return '📷 Image';
|
|
98
|
+
if (attachment.kind === 'audio')
|
|
99
|
+
return '🎤 Voice message';
|
|
100
|
+
return attachment.fileName ? `📎 ${attachment.fileName}` : '📎 File';
|
|
101
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serializeStoredMessage = serializeStoredMessage;
|
|
4
|
+
const media_js_1 = require("./media.js");
|
|
5
|
+
const MISSING_CREATED_AT_ISO = new Date(0).toISOString();
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function normalizeStringArray(value) {
|
|
10
|
+
if (!Array.isArray(value))
|
|
11
|
+
return [];
|
|
12
|
+
return value.filter((entry) => typeof entry === 'string');
|
|
13
|
+
}
|
|
14
|
+
function normalizeTimestamp(value) {
|
|
15
|
+
if (value instanceof Date)
|
|
16
|
+
return value;
|
|
17
|
+
if (isRecord(value) && typeof value.toDate === 'function') {
|
|
18
|
+
const result = value.toDate();
|
|
19
|
+
if (result instanceof Date)
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function normalizeCreatedAt(value, fallback) {
|
|
25
|
+
const timestamp = normalizeTimestamp(value) ?? normalizeTimestamp(fallback);
|
|
26
|
+
return timestamp ? timestamp.toISOString() : MISSING_CREATED_AT_ISO;
|
|
27
|
+
}
|
|
28
|
+
function normalizeSenderType(value, fallback) {
|
|
29
|
+
if (value === 'ai_agent' || fallback === 'ai_agent')
|
|
30
|
+
return 'ai_agent';
|
|
31
|
+
return 'human';
|
|
32
|
+
}
|
|
33
|
+
function normalizeContentType(value, attachments) {
|
|
34
|
+
if (value === 'text'
|
|
35
|
+
|| value === 'image'
|
|
36
|
+
|| value === 'audio'
|
|
37
|
+
|| value === 'file'
|
|
38
|
+
|| value === 'contact_card') {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
const primary = attachments[0]?.kind;
|
|
42
|
+
if (primary === 'image' || primary === 'audio' || primary === 'file')
|
|
43
|
+
return primary;
|
|
44
|
+
return 'text';
|
|
45
|
+
}
|
|
46
|
+
function normalizeForwardedFrom(value) {
|
|
47
|
+
if (!isRecord(value))
|
|
48
|
+
return undefined;
|
|
49
|
+
if (typeof value.sourceConversationId !== 'string')
|
|
50
|
+
return undefined;
|
|
51
|
+
if (typeof value.messageId !== 'string')
|
|
52
|
+
return undefined;
|
|
53
|
+
return {
|
|
54
|
+
sourceConversationId: value.sourceConversationId,
|
|
55
|
+
messageId: value.messageId,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function normalizeContactCard(value) {
|
|
59
|
+
if (!isRecord(value))
|
|
60
|
+
return undefined;
|
|
61
|
+
if (typeof value.userId !== 'string' || value.userId.length === 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
const userType = value.userType === 'ai_agent' ? 'ai_agent' : 'human';
|
|
64
|
+
const card = {
|
|
65
|
+
userId: value.userId,
|
|
66
|
+
displayName: typeof value.displayName === 'string' ? value.displayName : 'Unknown',
|
|
67
|
+
avatarUrl: typeof value.avatarUrl === 'string' ? value.avatarUrl : null,
|
|
68
|
+
userType,
|
|
69
|
+
};
|
|
70
|
+
if (typeof value.about === 'string')
|
|
71
|
+
card.about = value.about;
|
|
72
|
+
if (typeof value.isActive === 'boolean')
|
|
73
|
+
card.isActive = value.isActive;
|
|
74
|
+
if (typeof value.ownerId === 'string')
|
|
75
|
+
card.ownerId = value.ownerId;
|
|
76
|
+
if (typeof value.ownerName === 'string')
|
|
77
|
+
card.ownerName = value.ownerName;
|
|
78
|
+
if (typeof value.lifecycleState === 'string')
|
|
79
|
+
card.lifecycleState = value.lifecycleState;
|
|
80
|
+
return card;
|
|
81
|
+
}
|
|
82
|
+
function serializeStoredMessage(input) {
|
|
83
|
+
const { data } = input;
|
|
84
|
+
const attachments = (0, media_js_1.getMessageAttachments)({
|
|
85
|
+
attachments: data.attachments,
|
|
86
|
+
});
|
|
87
|
+
const result = {
|
|
88
|
+
id: input.id,
|
|
89
|
+
senderId: typeof data.senderId === 'string' ? data.senderId : '',
|
|
90
|
+
senderType: normalizeSenderType(data.senderType, input.senderTypeFallback),
|
|
91
|
+
isOwner: input.isOwner,
|
|
92
|
+
contentType: normalizeContentType(data.contentType, attachments),
|
|
93
|
+
text: typeof data.text === 'string' ? data.text : null,
|
|
94
|
+
attachments,
|
|
95
|
+
mentions: normalizeStringArray(data.mentions),
|
|
96
|
+
replyTo: typeof data.replyTo === 'string' ? data.replyTo : null,
|
|
97
|
+
replyToPosition: typeof data.replyToPosition === 'number' ? data.replyToPosition : null,
|
|
98
|
+
status: data.status === 'read' ? 'read' : 'sent',
|
|
99
|
+
createdAt: normalizeCreatedAt(data.createdAt, input.createdAtFallback),
|
|
100
|
+
};
|
|
101
|
+
const forwardedFrom = normalizeForwardedFrom(data.forwardedFrom);
|
|
102
|
+
if (data.forwarded === true || forwardedFrom) {
|
|
103
|
+
result.forwarded = true;
|
|
104
|
+
}
|
|
105
|
+
if (forwardedFrom) {
|
|
106
|
+
result.forwardedFrom = forwardedFrom;
|
|
107
|
+
}
|
|
108
|
+
if (typeof input.senderName === 'string') {
|
|
109
|
+
result.senderName = input.senderName;
|
|
110
|
+
}
|
|
111
|
+
if (data.workSession !== undefined && data.workSession !== null) {
|
|
112
|
+
result.workSession = data.workSession;
|
|
113
|
+
}
|
|
114
|
+
if (isRecord(data.metadata)) {
|
|
115
|
+
result.metadata = data.metadata;
|
|
116
|
+
}
|
|
117
|
+
const contactCard = normalizeContactCard(data.contactCard);
|
|
118
|
+
if (contactCard) {
|
|
119
|
+
result.contactCard = contactCard;
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeTurnMetadata = normalizeTurnMetadata;
|
|
4
|
+
exports.isTurnOpen = isTurnOpen;
|
|
5
|
+
exports.resolveTurnMessageSemantics = resolveTurnMessageSemantics;
|
|
6
|
+
exports.shouldPromoteConversationMessage = shouldPromoteConversationMessage;
|
|
7
|
+
exports.shouldTriggerAgentTurn = shouldTriggerAgentTurn;
|
|
8
|
+
exports.normalizeRuntimeTurnState = normalizeRuntimeTurnState;
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function normalizeTurnMetadata(metadata) {
|
|
13
|
+
if (!isRecord(metadata))
|
|
14
|
+
return null;
|
|
15
|
+
const turnSemantics = metadata.turnSemantics === 'progress'
|
|
16
|
+
|| metadata.turnSemantics === 'turn_complete'
|
|
17
|
+
|| metadata.turnSemantics === 'control'
|
|
18
|
+
? metadata.turnSemantics
|
|
19
|
+
: undefined;
|
|
20
|
+
const replyBehavior = metadata.replyBehavior === 'allow_auto_reply'
|
|
21
|
+
|| metadata.replyBehavior === 'suppress_auto_reply'
|
|
22
|
+
? metadata.replyBehavior
|
|
23
|
+
: undefined;
|
|
24
|
+
if (!turnSemantics && typeof metadata.turnComplete !== 'boolean' && !replyBehavior) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...(turnSemantics ? { turnSemantics } : {}),
|
|
29
|
+
...(typeof metadata.turnComplete === 'boolean' ? { turnComplete: metadata.turnComplete } : {}),
|
|
30
|
+
...(replyBehavior ? { replyBehavior } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function isTurnOpen(turnState) {
|
|
34
|
+
if (!turnState)
|
|
35
|
+
return false;
|
|
36
|
+
return turnState.state !== 'idle'
|
|
37
|
+
&& turnState.state !== 'completed'
|
|
38
|
+
&& turnState.state !== 'interrupted';
|
|
39
|
+
}
|
|
40
|
+
function resolveTurnMessageSemantics(input) {
|
|
41
|
+
const turnMetadata = normalizeTurnMetadata(input.metadata);
|
|
42
|
+
if (turnMetadata?.turnSemantics)
|
|
43
|
+
return turnMetadata.turnSemantics;
|
|
44
|
+
if (turnMetadata?.turnComplete === true)
|
|
45
|
+
return 'turn_complete';
|
|
46
|
+
if (input.senderType === 'human')
|
|
47
|
+
return 'turn_complete';
|
|
48
|
+
return isTurnOpen(input.senderTurnState) ? 'progress' : 'turn_complete';
|
|
49
|
+
}
|
|
50
|
+
function shouldPromoteConversationMessage(input) {
|
|
51
|
+
return resolveTurnMessageSemantics(input) !== 'progress';
|
|
52
|
+
}
|
|
53
|
+
function shouldTriggerAgentTurn(input) {
|
|
54
|
+
const semantics = resolveTurnMessageSemantics(input);
|
|
55
|
+
const turnMetadata = normalizeTurnMetadata(input.metadata);
|
|
56
|
+
if (turnMetadata?.replyBehavior === 'suppress_auto_reply') {
|
|
57
|
+
return { allow: false, semantics };
|
|
58
|
+
}
|
|
59
|
+
if (input.senderType === 'human') {
|
|
60
|
+
return { allow: true, semantics };
|
|
61
|
+
}
|
|
62
|
+
return { allow: semantics !== 'progress', semantics };
|
|
63
|
+
}
|
|
64
|
+
function normalizeRuntimeTurnState(value) {
|
|
65
|
+
if (!isRecord(value))
|
|
66
|
+
return null;
|
|
67
|
+
if (value.state === 'idle'
|
|
68
|
+
|| value.state === 'thinking'
|
|
69
|
+
|| value.state === 'streaming'
|
|
70
|
+
|| value.state === 'tool'
|
|
71
|
+
|| value.state === 'waiting_input'
|
|
72
|
+
|| value.state === 'completed'
|
|
73
|
+
|| value.state === 'interrupted') {
|
|
74
|
+
return { state: value.state };
|
|
75
|
+
}
|
|
76
|
+
if (value.state === 'running') {
|
|
77
|
+
return { state: 'streaming' };
|
|
78
|
+
}
|
|
79
|
+
if (value.state === 'requires_action') {
|
|
80
|
+
return { state: 'waiting_input' };
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type SerializedContactRequestStatus = 'pending' | 'approved' | 'rejected' | 'expired';
|
|
2
|
+
export interface SerializedContactRequest {
|
|
3
|
+
id: string;
|
|
4
|
+
requesterId: string;
|
|
5
|
+
requesterName: string;
|
|
6
|
+
requesterAvatarUrl: string | null;
|
|
7
|
+
targetId: string;
|
|
8
|
+
approverId: string;
|
|
9
|
+
message: string | null;
|
|
10
|
+
status: SerializedContactRequestStatus;
|
|
11
|
+
kind: 'dm' | 'group_invite';
|
|
12
|
+
groupContext?: {
|
|
13
|
+
conversationId: string;
|
|
14
|
+
groupName: string | null;
|
|
15
|
+
};
|
|
16
|
+
createdAt: string | null;
|
|
17
|
+
resolvedAt?: string | null;
|
|
18
|
+
expiresAt?: string | null;
|
|
19
|
+
}
|
|
20
|
+
export declare function serializeContactRequest(requestId: string, data: Record<string, unknown>): SerializedContactRequest | null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function normalizeTimestamp(value) {
|
|
5
|
+
if (value instanceof Date)
|
|
6
|
+
return value.toISOString();
|
|
7
|
+
if (isRecord(value) && typeof value.toDate === 'function') {
|
|
8
|
+
const result = value.toDate();
|
|
9
|
+
if (result instanceof Date)
|
|
10
|
+
return result.toISOString();
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function normalizeStatus(value) {
|
|
15
|
+
if (value === 'pending'
|
|
16
|
+
|| value === 'approved'
|
|
17
|
+
|| value === 'rejected'
|
|
18
|
+
|| value === 'expired') {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
return 'pending';
|
|
22
|
+
}
|
|
23
|
+
export function serializeContactRequest(requestId, data) {
|
|
24
|
+
if (typeof data.approverId !== 'string' || data.approverId.length === 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (typeof data.requesterId !== 'string' || data.requesterId.length === 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (typeof data.targetId !== 'string' || data.targetId.length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const kindValue = typeof data.kind === 'string' ? data.kind : 'dm';
|
|
34
|
+
if (kindValue !== 'dm' && kindValue !== 'group_invite') {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
let groupContext = null;
|
|
38
|
+
if (kindValue === 'group_invite') {
|
|
39
|
+
const ctx = isRecord(data.groupContext) ? data.groupContext : null;
|
|
40
|
+
const conversationId = typeof ctx?.conversationId === 'string'
|
|
41
|
+
? ctx.conversationId.trim()
|
|
42
|
+
: '';
|
|
43
|
+
if (!conversationId) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
groupContext = {
|
|
47
|
+
conversationId,
|
|
48
|
+
groupName: typeof ctx?.groupName === 'string' ? ctx.groupName : null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const payload = {
|
|
52
|
+
id: requestId,
|
|
53
|
+
requesterId: data.requesterId,
|
|
54
|
+
requesterName: typeof data.requesterName === 'string' ? data.requesterName : 'Unknown',
|
|
55
|
+
requesterAvatarUrl: typeof data.requesterAvatarUrl === 'string' ? data.requesterAvatarUrl : null,
|
|
56
|
+
targetId: data.targetId,
|
|
57
|
+
approverId: data.approverId,
|
|
58
|
+
message: typeof data.message === 'string' ? data.message : null,
|
|
59
|
+
status: normalizeStatus(data.status),
|
|
60
|
+
kind: kindValue,
|
|
61
|
+
createdAt: normalizeTimestamp(data.createdAt),
|
|
62
|
+
resolvedAt: normalizeTimestamp(data.resolvedAt),
|
|
63
|
+
expiresAt: normalizeTimestamp(data.expiresAt),
|
|
64
|
+
};
|
|
65
|
+
if (groupContext) {
|
|
66
|
+
payload.groupContext = groupContext;
|
|
67
|
+
}
|
|
68
|
+
return payload;
|
|
69
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type MediaAttachmentKind = 'image' | 'audio' | 'file';
|
|
2
|
+
export interface MediaAttachment {
|
|
3
|
+
kind: MediaAttachmentKind;
|
|
4
|
+
url: string;
|
|
5
|
+
mimeType?: string;
|
|
6
|
+
fileName?: string;
|
|
7
|
+
sizeBytes?: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
durationMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function inferMediaAttachmentKind(mimeType: string): MediaAttachmentKind;
|
|
13
|
+
export declare function getStoredFileExtension(input: {
|
|
14
|
+
mimeType?: string | null;
|
|
15
|
+
fileName?: string | null;
|
|
16
|
+
}): string;
|
|
17
|
+
export declare function normalizeStoredAttachments(value: unknown): MediaAttachment[] | null;
|
|
18
|
+
export declare function getMessageAttachments(input: {
|
|
19
|
+
attachments?: unknown;
|
|
20
|
+
}): MediaAttachment[];
|
|
21
|
+
export declare function getPrimaryAttachment(input: {
|
|
22
|
+
attachments?: unknown;
|
|
23
|
+
}): MediaAttachment | null;
|
|
24
|
+
export declare function describeAttachment(attachment: MediaAttachment | null | undefined): string | null;
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export function inferMediaAttachmentKind(mimeType) {
|
|
2
|
+
if (mimeType.startsWith('image/'))
|
|
3
|
+
return 'image';
|
|
4
|
+
if (mimeType.startsWith('audio/'))
|
|
5
|
+
return 'audio';
|
|
6
|
+
return 'file';
|
|
7
|
+
}
|
|
8
|
+
export function getStoredFileExtension(input) {
|
|
9
|
+
const mimeType = input.mimeType?.toLowerCase().trim() || '';
|
|
10
|
+
const explicitByMime = {
|
|
11
|
+
'image/jpeg': 'jpg',
|
|
12
|
+
'image/jpg': 'jpg',
|
|
13
|
+
'image/png': 'png',
|
|
14
|
+
'image/webp': 'webp',
|
|
15
|
+
'image/gif': 'gif',
|
|
16
|
+
'audio/mp4': 'm4a',
|
|
17
|
+
'audio/x-m4a': 'm4a',
|
|
18
|
+
'audio/mpeg': 'mp3',
|
|
19
|
+
'audio/webm': 'webm',
|
|
20
|
+
'audio/ogg': 'ogg',
|
|
21
|
+
'application/pdf': 'pdf',
|
|
22
|
+
'text/plain': 'txt',
|
|
23
|
+
'application/json': 'json',
|
|
24
|
+
'application/zip': 'zip',
|
|
25
|
+
};
|
|
26
|
+
if (explicitByMime[mimeType])
|
|
27
|
+
return explicitByMime[mimeType];
|
|
28
|
+
const trimmedName = input.fileName?.trim() || '';
|
|
29
|
+
const lastDot = trimmedName.lastIndexOf('.');
|
|
30
|
+
if (lastDot > 0 && lastDot < trimmedName.length - 1) {
|
|
31
|
+
const fromName = trimmedName.slice(lastDot + 1).toLowerCase();
|
|
32
|
+
if (/^[a-z0-9]{1,10}$/.test(fromName))
|
|
33
|
+
return fromName;
|
|
34
|
+
}
|
|
35
|
+
const slashIndex = mimeType.indexOf('/');
|
|
36
|
+
if (slashIndex !== -1 && slashIndex < mimeType.length - 1) {
|
|
37
|
+
const subtype = mimeType.slice(slashIndex + 1).split('+')[0].toLowerCase();
|
|
38
|
+
if (/^[a-z0-9.-]{1,20}$/.test(subtype)) {
|
|
39
|
+
if (subtype === 'plain')
|
|
40
|
+
return 'txt';
|
|
41
|
+
if (subtype === 'octet-stream')
|
|
42
|
+
return 'bin';
|
|
43
|
+
return subtype.replace(/[^a-z0-9]/g, '') || 'bin';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return 'bin';
|
|
47
|
+
}
|
|
48
|
+
export function normalizeStoredAttachments(value) {
|
|
49
|
+
if (!Array.isArray(value))
|
|
50
|
+
return null;
|
|
51
|
+
const attachments = [];
|
|
52
|
+
for (const item of value) {
|
|
53
|
+
if (!item || typeof item !== 'object')
|
|
54
|
+
return null;
|
|
55
|
+
const candidate = item;
|
|
56
|
+
if (candidate.kind !== 'image'
|
|
57
|
+
&& candidate.kind !== 'audio'
|
|
58
|
+
&& candidate.kind !== 'file') {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (typeof candidate.url !== 'string' || candidate.url.length === 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
attachments.push({
|
|
65
|
+
kind: candidate.kind,
|
|
66
|
+
url: candidate.url,
|
|
67
|
+
...(typeof candidate.mimeType === 'string' && candidate.mimeType ? { mimeType: candidate.mimeType } : {}),
|
|
68
|
+
...(typeof candidate.fileName === 'string' && candidate.fileName ? { fileName: candidate.fileName } : {}),
|
|
69
|
+
...(typeof candidate.sizeBytes === 'number' ? { sizeBytes: candidate.sizeBytes } : {}),
|
|
70
|
+
...(typeof candidate.width === 'number' ? { width: candidate.width } : {}),
|
|
71
|
+
...(typeof candidate.height === 'number' ? { height: candidate.height } : {}),
|
|
72
|
+
...(typeof candidate.durationMs === 'number' ? { durationMs: candidate.durationMs } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return attachments.length > 0 ? attachments : [];
|
|
76
|
+
}
|
|
77
|
+
export function getMessageAttachments(input) {
|
|
78
|
+
const normalizedAttachments = normalizeStoredAttachments(input.attachments);
|
|
79
|
+
return normalizedAttachments ?? [];
|
|
80
|
+
}
|
|
81
|
+
export function getPrimaryAttachment(input) {
|
|
82
|
+
const attachments = getMessageAttachments(input);
|
|
83
|
+
return attachments[0] ?? null;
|
|
84
|
+
}
|
|
85
|
+
export function describeAttachment(attachment) {
|
|
86
|
+
if (!attachment)
|
|
87
|
+
return null;
|
|
88
|
+
if (attachment.kind === 'image')
|
|
89
|
+
return '📷 Image';
|
|
90
|
+
if (attachment.kind === 'audio')
|
|
91
|
+
return '🎤 Voice message';
|
|
92
|
+
return attachment.fileName ? `📎 ${attachment.fileName}` : '📎 File';
|
|
93
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { MediaAttachment } from './media.js';
|
|
2
|
+
export type SerializedSenderType = 'human' | 'ai_agent';
|
|
3
|
+
export type SerializedContentType = 'text' | 'image' | 'audio' | 'file' | 'contact_card';
|
|
4
|
+
export interface ForwardedFrom {
|
|
5
|
+
sourceConversationId: string;
|
|
6
|
+
messageId: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SerializedContactCard {
|
|
9
|
+
userId: string;
|
|
10
|
+
displayName: string;
|
|
11
|
+
avatarUrl: string | null;
|
|
12
|
+
userType: SerializedSenderType;
|
|
13
|
+
about?: string;
|
|
14
|
+
isActive?: boolean;
|
|
15
|
+
ownerId?: string;
|
|
16
|
+
ownerName?: string;
|
|
17
|
+
lifecycleState?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SerializedMessage {
|
|
20
|
+
id: string;
|
|
21
|
+
senderId: string;
|
|
22
|
+
senderName?: string;
|
|
23
|
+
senderType: SerializedSenderType;
|
|
24
|
+
isOwner: boolean;
|
|
25
|
+
contentType: SerializedContentType;
|
|
26
|
+
text: string | null;
|
|
27
|
+
attachments: MediaAttachment[];
|
|
28
|
+
mentions: string[];
|
|
29
|
+
replyTo: string | null;
|
|
30
|
+
replyToPosition: number | null;
|
|
31
|
+
forwarded?: boolean;
|
|
32
|
+
forwardedFrom?: ForwardedFrom;
|
|
33
|
+
status: 'sent' | 'read';
|
|
34
|
+
createdAt: string;
|
|
35
|
+
workSession?: unknown;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
contactCard?: SerializedContactCard;
|
|
38
|
+
}
|
|
39
|
+
export interface SerializeMessageInput {
|
|
40
|
+
id: string;
|
|
41
|
+
data: Record<string, unknown>;
|
|
42
|
+
createdAtFallback?: unknown;
|
|
43
|
+
senderTypeFallback?: unknown;
|
|
44
|
+
senderName?: string;
|
|
45
|
+
isOwner: boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare function serializeStoredMessage(input: SerializeMessageInput): SerializedMessage;
|