SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

+226
View File
@@ -0,0 +1,226 @@
import type { MessageElement } from '@snowluma/protocol/events';
import { serializeMediaInfo } from './media-store';
import type { JsonObject, JsonValue } from './types';
export type MessageMedia = Extract<MessageElement, { type: 'image' | 'record' | 'video' }>;
export type StoredMediaSegment = JsonObject & { type: MessageMedia['type']; data: JsonObject };
export function isStoredMediaSegment(segment: JsonValue): segment is StoredMediaSegment {
return (
segment !== null &&
typeof segment === 'object' &&
!Array.isArray(segment) &&
(segment.type === 'image' || segment.type === 'record' || segment.type === 'video') &&
segment.data !== null &&
typeof segment.data === 'object' &&
!Array.isArray(segment.data)
);
}
/** Legacy image segments use a content hash filename; arbitrary aliases prove nothing. */
export function imageMd5FromFile(file: unknown): string | undefined {
return typeof file === 'string'
? /^([a-f0-9]{32})\.(?:png|jpe?g|webp|gif|bmp)$/i.exec(file)?.[1]?.toLowerCase()
: undefined;
}
const mediaSnapshot = Symbol('message media snapshot');
type MediaSegment = JsonObject & { [mediaSnapshot]?: MessageMedia };
/** Snapshot queued writes, including metadata that is deliberately absent from JSON. */
export function snapshotMessageEvent(event: JsonObject): JsonObject {
const snapshot = structuredClone(event);
if (Array.isArray(event.message) && Array.isArray(snapshot.message)) {
for (const [index, segment] of event.message.entries()) {
if (!segment || typeof segment !== 'object' || Array.isArray(segment)) {
continue;
}
const media = (segment as MediaSegment)[mediaSnapshot];
const copied = snapshot.message[index];
if (media && copied && typeof copied === 'object') {
Object.defineProperty(copied, mediaSnapshot, { value: structuredClone(media) });
}
}
}
return snapshot;
}
export function isMessageMedia(element: MessageElement): element is MessageMedia {
return element.type === 'image' || element.type === 'record' || element.type === 'video';
}
/** Keep persistence-only metadata beside the converted segment, never on the
* public JSON wire. Capture after successful conversion so skipped elements
* cannot shift the association between a message and its media snapshots.
*/
export function attachMessageMedia(segment: JsonObject, element: MessageElement): void {
if (!isMessageMedia(element)) {
return;
}
const {
type,
fileId,
fileName,
fileSize,
url,
imageUrl,
md5Hex,
sha1Hex,
subType,
summary,
width,
height,
picFormat,
duration,
voiceFormat,
videoFormat,
fileHash,
mediaNode,
flash,
} = element;
const snapshot = JSON.parse(
serializeMediaInfo({
type,
fileId,
fileName,
fileSize,
url,
imageUrl,
md5Hex,
sha1Hex,
subType,
summary,
width,
height,
picFormat,
duration,
voiceFormat,
videoFormat,
fileHash,
mediaNode,
flash,
}),
) as MessageMedia;
Object.defineProperty(segment, mediaSnapshot, { value: snapshot });
}
export function serializeMessageMedia(event: JsonObject): string | null {
if (!Array.isArray(event.message)) {
return null;
}
const snapshots = event.message.flatMap((segment) => {
if (!segment || typeof segment !== 'object' || Array.isArray(segment)) {
return [];
}
const snapshot = (segment as MediaSegment)[mediaSnapshot];
return snapshot ? [snapshot] : [];
});
return snapshots.length > 0 ? JSON.stringify(snapshots) : null;
}
export function parseMessageMedia(data: string): MessageMedia[] {
const parsed: unknown = JSON.parse(data);
if (
!Array.isArray(parsed) ||
parsed.some(
(value) =>
!value ||
typeof value !== 'object' ||
!['image', 'record', 'video'].includes(value.type),
)
) {
throw new Error('invalid persisted message media metadata');
}
return parsed as MessageMedia[];
}
/** A repeated server representation may omit fields already learned from an
* upload or earlier receive. Preserve those fields only for the same resource;
* conflicting fingerprints or uncertain associations must not rewrite history.
*/
export function mergeMessageMedia(previous: string | null, next: string | null): string | null {
if (!previous) {
return next;
}
if (previous === next) {
return previous;
}
if (!next) {
throw new Error('message update is missing previously stored media metadata');
}
const oldMedia = parseMessageMedia(previous);
const newMedia = parseMessageMedia(next);
if (oldMedia.length !== newMedia.length) {
throw new Error('message media association changed');
}
const merged = newMedia.map((media, index) => {
const old = oldMedia[index]!;
if (old.type !== media.type) {
throw new Error('message media association changed');
}
let matched = false;
for (const field of ['md5Hex', 'sha1Hex'] as const) {
if (!old[field] || !media[field]) {
continue;
}
if (old[field].toLowerCase() !== media[field].toLowerCase()) {
throw new Error('message update has conflicting media fingerprints');
}
matched = true;
}
// Images can expose a filename as fileId, so only their fingerprints can
// prove an association. Voice/video fileId is a source resource UUID.
matched ||= media.type !== 'image' && !!old.fileId && old.fileId === media.fileId;
if (!matched && JSON.stringify(old) !== JSON.stringify(media)) {
throw new Error('message update has an unverified media association');
}
return mergeKnownMediaFields(old, media);
});
return JSON.stringify(merged);
}
function mergeKnownMediaFields(old: object, next: object): Record<string, unknown> {
const merged: Record<string, unknown> = { ...old };
for (const [key, value] of Object.entries(next)) {
// Zero dimensions, sizes and durations are incomplete observations, not
// evidence that known properties of the same resource have changed.
if (value === undefined || value === null || value === '' || value === 0) {
continue;
}
const previous = merged[key];
merged[key] =
typeof value === 'object' &&
!Array.isArray(value) &&
previous !== null &&
typeof previous === 'object' &&
!Array.isArray(previous)
? mergeKnownMediaFields(previous, value)
: value;
}
return merged;
}