MiniPMS · Technical Document

معماری پیام‌رسان، تماس‌ها و جلسات زندهٔ MiniPMS

سند فنی — WebRTC mesh · TURN/STUN · WebSocket signaling · ترجمهٔ AI · SQLite/Prisma · اکتبر ۲۰۲۶

سند فنی — پیام‌رسان، تماس صوتی/تصویری و جلسات زندهٔ MiniPMS

نسخه: اکتبر ۲۰۲۶ · مخاطب: متخصصین فنی / داوری معماری

۱. نمای کلی معماری

سیستم از سه پروسهٔ مجزا تشکیل شده که روی یک سرور Windows Server 2019 میزبانی می‌شوند:

Clientمرورگر کاربر
TLS · 443
Caddy v2Reverse Proxy + HTTPS
minipms.irhba.ir/*
▼
Next.js App127.0.0.1:8090 · UI + REST API + Auth
/call-ws
▼
Signaling127.0.0.1:8091 · WebSocket + اتاق‌ها
▼
TURN/STUN0.0.0.0:3478 UDP+TCP · relay 49450–49650
Browser Amesh-P2P
⬌ media RTP · ICE/STUN/TURN ⬌
Browser Bmesh-P2P
media می‌تواند مستقیم (host/srflx) یا از طریق relay TURN برود — دادهٔ مدیا هرگز از Next.js رد نمی‌شود

پروسه‌ها و سرویس‌ها (NSSM)

سرویسپورتنقش
minipms127.0.0.1:8090اپلیکیشن Next.js (App Router + Turbopack) — UI، REST APIها، auth
minipms-signaling127.0.0.1:8091 (WS) + 0.0.0.0:3478 UDP/TCP (TURN)سرویس Node جداگانه (server/signaling.mjs) — WebSocket signaling + سرور TURN/STUN
ollama127.0.0.1:11434ترجمهٔ محلی (fallback ابری)

دلیل جدایی signaling از Next: WebSocket لایف‌تاایم طولانی در Next.js standalone به‌خوبی مدیریت نمی‌شود؛ یک پروسهٔ Node خام (server/signaling.mjs) هم WS signaling و هم TURN را یک‌جا میزبانی می‌کند — ساده و بدون وابستگی خارجی (پکیج turn-server برای Node).

فناوری‌ها


۲. مدل داده (Prisma → SQLite)

پیام‌رسان

ChatRoom         { id, name, type, projectId?, taskId?, createdBy, avatarUrl }
                 // type: DIRECT | GROUP | PROJECT | WORKGROUP
ChatRoomMember   { roomId, userId, role, joinedAt, lastReadAt }   // @@unique([roomId,userId])
Comment          { id, body, authorId, targetType, targetId,      // مدل مشترک پیام/کامنت
                   parentId?, isPrivate, recipientId?,
                   isRead, readAt, mentions (JSON string),
                   roomId?, mediaUrl?, mediaType?,
                   mediaFileName?, mediaFileSize?,
                   priority (NORMAL|...), translations (JSON) }
MessageReadStatus { messageId, userId, readAt }                   // @@unique([messageId,userId])

نکتهٔ طراحی: پیام‌های چت و کامنت‌های کار/پروژه در یک مدل Comment واحد نگه‌داری می‌شوند — roomId وجود چت را مشخص می‌کند و isPrivate+recipientId پیام خصوصی را.

جلسات زنده

LiveMeeting      { code @unique, title, hostId, hostName, chatRoomId?,
                   status (ACTIVE|ENDED), recordingEnabled,
                   transcriptStatus (NONE|RECORDING|QUEUED|PROCESSING|DONE|FAILED),
                   transcriptJson, transcriptError, endedAt }
LiveMeetingFile  { meetingId, uploaderId, uploaderName, fileName,
                   fileUrl, fileType, fileSize }

۳. پیام‌رسان (Messenger)

۳.۱ جریان داده — HTTP polling (نه WebSocket push)

طراحی آگاهانه: لیست گفتگوها هر ۱۵ ثانیه و پیام‌های گفتگوی باز با polling سریع به‌روز می‌شوند. دلیل: سادگی، قابلیت اطمینان، عدم نیاز به حالت سرور-در-حافظه. WebSocket صرفاً برای call/meeting signaling استفاده می‌شود.

GET/api/chat/roomsلیست روم‌ها + lastMessage + unreadCount
GET/api/chat/rooms/[id]/messagesصفحه‌بندی پیام‌ها (cursor/page)
GET/api/messagesصندوق ورود یکپارچه (all|unread|sent|private)
GET/api/messages/unread-countبج نوتیفیکیشن sidebar

۳.۲ ارسال پیام

POST/api/chat/rooms/[id]/messages
- application/json → { text, priority, parentId, forwardFrom }
- multipart/form-data → همان فیلدها + media=<file>
فایل در public/uploads/chat/<uuid><ext> ذخیره و URL در Comment.mediaUrl می‌شود

۳.۳ قابلیت‌های کلاینت (messages/page.tsx ~۲۸۰۰ خط)

۳.۴ ترجمهٔ هوشمند پیام‌ها

لایهٔ provider قابل‌تعویض (src/lib/translate/index.ts):

پیامPOST /api/messages/translate
→
ChainTRANSLATE_PROVIDER=auto
→
OpenRouternemotron-3-ultra-550b:free · ~5s
fallback
Ollama محلیtranslategemma:4b · 127.0.0.1:11434
→
کش DBComment.translations · یک‌بار برای همیشه
term-shield: PRJ-*, D-*, URL، ایمیل، منشن و اعداد با __PHn__ محافظت و برمی‌گردند — اصطلاحات فنی ترجمه نمی‌شوند

۴. زیرساخت Real-time: Signaling + TURN

۴.۱ WebSocket Signaling (server/signaling.mjs)

۴.۲ پروتکل WS — پیام‌های کلیدی

جهتTypeکاربرد
تماس ۱به۱call-invite, call-answer, call-reject, call-endhandshake تماس
WebRTCwebrtc-offer, webrtc-answer, webrtc-ice-candidateمسیریابی SDP/ICE بین دو کاربر (سرور relay-only)
جلسهmeet-join, meet-admit, meet-deny, meet-leave, meet-endwaiting-room + کنترل host
جلسهmeet-signal (به peerId خاص)SDP/ICE بین هر جفت peer در mesh
جلسهmeet-chat, meet-media, meet-file, meet-handچت درون‌جلسه، وضعیت میک/وبکم، ارسال فایل، دست بلند
Whiteboardmeet-wb-toggle/draw/stroke/undo/clear + meet-wb-syncتخته مشترک روی WS
Invitemeet-inviteدعوت به جلسه از داخل پیام‌رسان (بنر + لینک پیام)
Debugcall-debug, meet-debugرویدادهای ICE/ontrack/conn — همه در لاگ signaling قابل ردگیری

سرور هیچ state سنگینی ندارد: فقط Map کاربران آنلاین + روم‌های جلسه فعال در حافظه — اتصال قطع شود، روم هم تخلیه می‌شود.

۴.۳ TURN/STUN سرور (داخل همان پروسه)

TURN_PORT      3478 UDP+TCP          realm: minipms.irhba.ir
Relay range    UDP 49450–49650       externalIp: 202.133.92.249
Auth           long-term + TURN REST API (shared secret HMAC-SHA1)

TURN REST API convention: کلاینت GET /api/webrtc/turn-credentials می‌زند → سرور می‌سازد:

اعتبار اعتبارنامه ۱ ساعت است (time-limited، مطابق استاندارد REST-API coturn). تیم‌آپ ICE در کلاینت: fallback به stun.l.google.com اگر TURN credentials گرفته نشود.

نکتهٔ پشتیبانی تاریخی: قبلاً relay range با TURNهای دو پروژهٔ دیگر روی همان ماشین هم‌پوشه داشت → allocation صادر می‌شد ولی دیتا رد نمی‌شد. رنج جدا (49450–49650) + قوانین فایروال مجزا حل شد.


۵. تماس صوتی/تصویری ۱به۱

۵.۱ جریان

Caller
call-invite
WS :8091
→ بنر زنگ + صدای WebAudio برای گیرنده
Callee
call-answer
WS
Caller
webrtc-offer (SDP)
Callee
Callee
webrtc-answer (SDP)
Caller
دو طرف
webrtc-ice-candidate
دو طرف
ICE negotiation دوطرفه
media: RTP مستقیم (host/srflx) یا relay = TURN/UDP

۵.۲ دکمهٔ اسپیکر موبایل

HTMLMediaElement.setSinkId('speaker' | '') روی remote audio/video — در اندروید کروم جواب می‌دهد؛ iOS Safari پشتیبانی نمی‌کند (محدودیت مرورگر). UI: دکمهٔ 🔊 در نوار کنترل تماس (call-ui.tsx).

۵.۳ قابلیت دیباگ از روی لاگ

تمام رویدادها (call-debug) به‌صورت [dbg:call] <userId> event detail در logs/signaling-nssm-out.log — امکان ردگیری کامل ICE candidates، connectionState، ontrack بدون دسترسی به کلاینت.


۶. جلسات زنده (Live Meetings — شبیه Google Meet)

۶.۱ توپولوژی: Full-Mesh WebRTC

هر شرکت‌کننده به هر peer دیگر یک RTCPeerConnection جدا می‌سازد (meeting-room.tsx):

۶.۲ جریان ورود (waiting room)

Guestنام + code
meet-join
WS
WS
meet-knock
Hostلیست انتظار
Host
meet-admit
Guestmeet-joined ✅
Host
meet-deny
Guestmeet-denied ❌
mesh: هر peer به هر peer دیگر RTCPeerConnection می‌سازد (initiator = قدیمی‌تر)

۶.۳ قابلیت‌ها

فیچرپیاده‌سازی
ویدیو/صداmesh getUserMedia per participant، تایل‌های object-cover
Screen sharegetDisplayMedia — به‌عنوان MediaStream دوم روی همان peerConnection (screenSendersRef map per peer) با بج screen
چت درون‌جلسهmeet-chat روی WS (بدون ذخیرهٔ دیتابیس — ephemeral)
اشتراک فایلآپلود به /api/live-meetings/[code]/files → LiveMeetingFile → meet-file broadcast → لینک دانلود برای همه
Whiteboardcanvas + رویدادهای meet-wb-* — رسم strokeبه‌stroke بین peerها + meet-wb-sync برای join-during
تشخیص گفتارWeb Speech API (SpeechRecognition/webkitSpeechRecognition) با زبان به‌گرد از locale فعلی (fa-IR/ar-SA/zh-CN/...) + fallback MediaRecorder برای مرورگرهای بدون STT — سگمنت‌ها POST /api/live-meetings/[code]/transcript و در transcriptJson ذخیره می‌شوند (diarization = speakerName هر سگمنت)
ضبط صوتMediaRecorder روی audio local → chunkهای ≤۱۵MB به /api/live-meetings/[code]/recording → recordings/<code>/ — recordingEnabled انتخاب میزبان هنگام ساخت
پایانmeet-end → LiveMeeting.status=ENDED + endedAt

۶.۴ نقاط ورود


۷. امنیت

لایهمکانیزم
AuthNextAuth session در همهٔ API routes (await auth()); routeها 401/403 در غیر این‌صورت
WS authتوکن زمان‌دار HMAC-SHA256 ({userId,exp}) — بدون session نمی‌شود توکن گرفت
TURN authاعتبارنامهٔ زمان‌دار ۱ ساعته HMAC-SHA1 با shared secret — فقط کاربر لاگین‌شده
Membershipچک عضویت ChatRoomMember در پیام‌رسان؛ چک roomId/isPrivate در ترجمه
Private msgفقط فرستنده/گیرنده/ادمین — در translate هم enforce شده
File accessفایل‌های چت/جلسه در public/uploads — نام uuid تصادفی (بدون index)
Rate/sizeفایل ضبط ≤۱۵MB در chunk؛ ترجمه batch ≤۱۰۰ پیام
Transportهمهٔ ترافیک از HTTPS/WSS (Caddy TLS) — داخلی روی 127.0.0.1

۸. استقرار و عملیات

موردمقدار
سرورWindows Server 2019، NAT: ورودی 202.133.92.249 / خروجی 116.130.143.220
Reverse proxyCaddyfile: /call-ws → 8091، بقیه → 8090 (TLS خودکار)
پورت‌های باز443, 3478 UDP+TCP, 49450–49650 UDP (TURN relay)
لاگ‌هاlogs/service.{log,err}، logs/signaling-nssm-out.log، logs/ollama.log
Debug تماس[dbg:call]/[dbg:meet] و [turn] در لاگ signaling — ICE/candidates/allocations ردیابی‌شدنی

۹. محدودیت‌های شناخته‌شده و نقشهٔ راه

محدودیت فعلیدلیلمسیر بهبود
پیام‌رسان polling دارد (۱۵s/۳s)سادگی/قابلیت اطمینانانتقال به SSE یا WS push با همان signaling
Mesh تا ~۱۰ نفرO(n²) پهنای باند/CPUmediasoup/Janus SFU برای جلسات بزرگ
ترجمهٔ محلی آهسته (~۱ tok/s)CPU سرور ضعیفGPU یا ابری (OpenRouter فعلی)
setSinkId iOS کار نمی‌کندمحدودیت Safariaudio route native (PWA→app)
چت درون‌جلسه ephemeralنیاز نبود ذخیرهاتصال به ChatRoom پروژه

۱۰. فایل‌های مرجع

بخشمسیر
Signaling + TURNserver/signaling.mjs
Call context/UIsrc/contexts/call-context.tsx, src/components/messages/call-ui.tsx
Meeting roomsrc/components/meetings/meeting-room.tsx (+ meeting-whiteboard.tsx)
Messenger UIsrc/app/(dashboard)/messages/page.tsx
Chat APIssrc/app/api/chat/, src/app/api/messages/
Live meeting APIssrc/app/api/live-meetings/**
WebRTC helperssrc/app/api/webrtc/{signaling-token,turn-credentials}
Translationsrc/lib/translate/index.ts, src/app/api/messages/translate/route.ts
Schemaprisma/schema.prisma (Comment/ChatRoom/LiveMeeting)