Fixing video retrieval error

This commit is contained in:
2026-09-29 20:48:14 +03:30
parent 7996fc7f73
commit e20b904bbe
2 changed files with 59 additions and 26 deletions
@@ -444,13 +444,17 @@ export class ConversationsService {
? String(lastMsg.content ?? lastMsg.text ?? lastMsg.body ?? '').trim()
: '';
if (lastType === 'image' || lastType === 'photo' || lastType === 'img') {
preview = 'تصویر';
if (/(?:^|[\\/._-])vid-\d+/i.test(preview) || /\.(mp4|mov|m4v|mkv|avi|3gp)(?:\?|$)/i.test(preview)) {
preview = 'ویدیو';
} else {
preview = 'تصویر';
}
} else if (lastType === 'voice' || lastType === 'audio') {
preview = 'پیام صوتی';
} else if (lastType === 'video') {
preview = 'ویدیو';
} else if (lastType === 'file' || lastType === 'document' || lastType === 'attachment') {
if (/\.(mp4|mov|m4v|mkv|avi|3gp)(?:\?|$)/i.test(preview)) {
if (/(?:^|[\\/._-])vid-\d+/i.test(preview) || /\.(mp4|mov|m4v|mkv|avi|3gp)(?:\?|$)/i.test(preview)) {
preview = 'ویدیو';
} else if (
/(?:^|[\\/._-])voice(?:[\\/._-]|$)/i.test(preview) ||
+53 -24
View File
@@ -93,6 +93,8 @@ const VIDEO_EXT_RE = /\.(mp4|mov|m4v|mkv|avi|3gp|ogv)(?:\?|$)/i;
const AUDIO_EXT_RE = /\.(webm|ogg|mp3|m4a|wav|aac|opus|oga)(?:\?|$)/i;
const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp|bmp|svg|heic|heif)(?:\?|$)/i;
const VOICE_NAME_RE = /(?:^|[\\/._-])voice(?:[\\/._-]|$)/i;
/** Uploaded chat videos are camouflaged as .jpg so the external WS accepts them as image. */
const VIDEO_CAMO_RE = /(?:^|[\\/._-])vid-\d+/i;
function looksLikeVideo(pathOrName: string): boolean {
return VIDEO_EXT_RE.test(pathOrName);
@@ -102,17 +104,24 @@ function looksLikeVoiceAsset(pathOrName: string): boolean {
return VOICE_NAME_RE.test(pathOrName) || AUDIO_EXT_RE.test(pathOrName);
}
function looksLikeCamouflagedVideo(pathOrName: string): boolean {
return VIDEO_CAMO_RE.test(pathOrName);
}
function looksLikeVideoAsset(pathOrName: string): boolean {
return looksLikeVideo(pathOrName) || looksLikeCamouflagedVideo(pathOrName);
}
function inferMediaType(
declared: ChatMediaType,
content: string,
fileName?: string,
): ChatMediaType {
const probe = `${fileName || ''} ${content}`;
// Declared voice always wins — recordings are often .webm and must stay as audio UI
if (declared === 'voice') return 'voice';
// Path/name cues beat wrong declared types (MediaRecorder video/webm, backend "video" on voice)
if (looksLikeVoiceAsset(probe) && !looksLikeVideo(probe)) return 'voice';
if (looksLikeVideo(probe)) return 'video';
// Voice recordings must stay audio UI (often .webm). Camouflaged videos never use voice names.
if (declared === 'voice' && !looksLikeVideoAsset(probe)) return 'voice';
if (looksLikeVoiceAsset(probe) && !looksLikeVideoAsset(probe)) return 'voice';
if (looksLikeVideoAsset(probe)) return 'video';
if (declared === 'video') return 'video';
if (declared === 'image') return 'image';
if (declared === 'file' || declared === 'text') {
@@ -127,7 +136,7 @@ export function previewLabelForMessage(msg: Pick<ChatMessage, 'type' | 'content'
if (msg.type === 'voice') return 'پیام صوتی';
if (msg.type === 'video') return 'ویدیو';
if (msg.type === 'file') {
if (looksLikeVideo(`${msg.fileName || ''} ${msg.content}`)) return 'ویدیو';
if (looksLikeVideoAsset(`${msg.fileName || ''} ${msg.content}`)) return 'ویدیو';
if (looksLikeVoiceAsset(`${msg.fileName || ''} ${msg.content}`)) return 'پیام صوتی';
return msg.fileName?.trim() || 'فایل';
}
@@ -208,12 +217,12 @@ function classifyFile(file: File): Exclude<ChatMediaType, 'text'> {
}
/**
* Chat backend accepts text|image|voice reliably.
* Real video is sent as `file` (path + .mp4 name) — `video` is dropped; `image` can also fail for large binaries.
* External chat WS reliably delivers text|image|voice.
* Native `video` is dropped; `file` with .mp4 is also dropped.
* Videos are uploaded as image-looking paths (vid-*.jpg) and sent as type image.
*/
function wsMediaType(type: ChatMediaType): 'image' | 'voice' | 'file' | 'text' {
if (type === 'image') return 'image';
if (type === 'video') return 'file';
if (type === 'image' || type === 'video') return 'image';
if (type === 'voice') return 'voice';
if (type === 'file') return 'file';
return 'text';
@@ -268,7 +277,16 @@ function normalizeIncoming(
? (raw.message as Record<string, unknown>)
: raw;
const declared = normalizeMediaType(String(base.type_content ?? base.type ?? 'text'));
const content = String(base.content ?? base.text ?? base.body ?? '').trim();
const content = String(
base.content ??
base.text ??
base.body ??
base.file_url ??
base.fileUrl ??
base.url ??
base.path ??
'',
).trim();
if (!content && !base.attachments) return null;
const id = String(base.id ?? base.message_id ?? '').trim();
@@ -740,14 +758,20 @@ export function useOrgChat() {
const id = opts?.tempId || crypto.randomUUID();
const reply = opts?.reply ?? null;
const wsType = wsMediaType(type);
// Never put real video extensions in WS content — external chat drops those messages.
let filePathHint = filePath;
if (
(type === 'video' || type === 'voice') &&
!/\.[a-z0-9]{2,5}(?:\?|$)/i.test(filePathHint)
) {
if (type === 'voice' && !/\.[a-z0-9]{2,5}(?:\?|$)/i.test(filePathHint)) {
const ext = opts?.fileName?.match(/(\.[a-z0-9]{2,5})$/i)?.[1];
if (ext) filePathHint = `${filePathHint}${ext}`;
}
const displayName =
type === 'video'
? opts?.fileName && looksLikeVideo(opts.fileName)
? opts.fileName
: opts?.fileName
? opts.fileName.replace(/\.[^.]+$/, '') + '.mp4'
: 'video.mp4'
: opts?.fileName;
const mediaUrl = buildChatFileUrl(filePathHint, session.downloadUrl);
const payload: Record<string, string> = {
@@ -757,7 +781,7 @@ export function useOrgChat() {
content: filePathHint,
};
if (reply) payload.reply = reply;
if (opts?.fileName) payload.file_name = opts.fileName;
if (displayName) payload.file_name = displayName;
const existingIdx = messages.value.findIndex((m) => m.id === id);
if (existingIdx >= 0) {
@@ -767,7 +791,7 @@ export function useOrgChat() {
content: filePathHint,
type,
mediaUrl,
fileName: opts?.fileName,
fileName: displayName,
uploadProgress: 100,
optimistic: false,
delivery: 'sent',
@@ -785,7 +809,7 @@ export function useOrgChat() {
optimistic: true,
delivery: 'sending',
mediaUrl,
fileName: opts?.fileName,
fileName: displayName,
localPreviewUrl: opts?.localPreviewUrl,
};
messages.value = mergeMessages(optimistic, messages.value);
@@ -796,7 +820,7 @@ export function useOrgChat() {
preview: previewLabelForMessage({
type,
content: filePathHint,
fileName: opts?.fileName,
fileName: displayName,
}),
at: new Date().toISOString(),
fromSelf: true,
@@ -857,10 +881,15 @@ export function useOrgChat() {
try {
setProgress(5);
let fileForUpload = type === 'image' ? await compressImageIfNeeded(file) : file;
if (type === 'video' && !looksLikeVideo(fileForUpload.name)) {
const base = fileForUpload.name.replace(/\.[^.]+$/, '') || 'video';
fileForUpload = new File([fileForUpload], `${base}.mp4`, {
type: fileForUpload.type || 'video/mp4',
let outboundFileName = file.name;
if (type === 'video') {
// Camouflage as image path so external chat WS delivers the message.
// Keep original name (with video ext) for UI inference via file_name.
const original =
looksLikeVideo(file.name) ? file.name : `${file.name.replace(/\.[^.]+$/, '') || 'video'}.mp4`;
outboundFileName = original;
fileForUpload = new File([file], `vid-${Date.now()}.jpg`, {
type: file.type || 'video/mp4',
});
}
setProgress(12);
@@ -870,7 +899,7 @@ export function useOrgChat() {
setProgress(99);
await sendMediaPath(roomId, uploadedPath, type, {
reply,
fileName: fileForUpload.name || file.name,
fileName: outboundFileName,
localPreviewUrl,
tempId: id,
});