سند فنی — پیامرسان، تماس صوتی/تصویری و جلسات زندهٔ MiniPMS
نسخه: اکتبر ۲۰۲۶ · مخاطب: متخصصین فنی / داوری معماری
۱. نمای کلی معماری
سیستم از سه پروسهٔ مجزا تشکیل شده که روی یک سرور Windows Server 2019 میزبانی میشوند:
پروسهها و سرویسها (NSSM)
| سرویس | پورت | نقش |
|---|---|---|
minipms | 127.0.0.1:8090 | اپلیکیشن Next.js (App Router + Turbopack) — UI، REST APIها، auth |
minipms-signaling | 127.0.0.1:8091 (WS) + 0.0.0.0:3478 UDP/TCP (TURN) | سرویس Node جداگانه (server/signaling.mjs) — WebSocket signaling + سرور TURN/STUN |
ollama | 127.0.0.1:11434 | ترجمهٔ محلی (fallback ابری) |
دلیل جدایی signaling از Next: WebSocket لایفتاایم طولانی در Next.js standalone بهخوبی مدیریت نمیشود؛ یک پروسهٔ Node خام (server/signaling.mjs) هم WS signaling و هم TURN را یکجا میزبانی میکند — ساده و بدون وابستگی خارجی (پکیج turn-server برای Node).
فناوریها
- Next.js + TypeScript + Tailwind (SPA-like داخل App Router، اکثر صفحات
'use client') - SQLite via better-sqlite3 (کوئری مستقیم
query/queryOne/executeدرsrc/lib/db.ts) + Prisma برای schema - NextAuth.js احراز هویت (session cookie)
- next-intl 4 — چندزبانگی ۸ زبانه (en/fa/ar/zh/tr/fr/it/es)، کوکیمحور، RTL خودکار
- WebRTC — mesh topology برای جلسات، ۱به۱ برای تماسها
ws— WebSocket server برای signalingturn-server(npm) — TURN/STUN روی Node بهجای coturn- Web Speech API + MediaRecorder — تشخیص گفتار زندهٔ جلسات
۲. مدل داده (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 استفاده میشود.
/api/chat/roomsلیست رومها + lastMessage + unreadCount/api/chat/rooms/[id]/messagesصفحهبندی پیامها (cursor/page)/api/messagesصندوق ورود یکپارچه (all|unread|sent|private)/api/messages/unread-countبج نوتیفیکیشن sidebar۳.۲ ارسال پیام
/api/chat/rooms/[id]/messages- Reply:
parentId→ نمایش کوتاهشدهٔ پیام مرجع + scroll-to-message - Forward:
forwardFrom→ کپی بدنه/مدیا به روم مقصد - Priority:
NORMAL | IMPORTANT | URGENT— رنگ و بج متفاوت در UI - Edit/Delete:
PATCH/DELETE /api/chat/messages/[id](فقط نویسنده) - Read receipts:
POST /api/chat/messages/[id]/read+ جدولMessageReadStatus→ تیک خواندهشدن/تعداد خوانندگان در گروهها
۳.۳ قابلیتهای کلاینت (messages/page.tsx ~۲۸۰۰ خط)
- پیام صوتی: MediaRecorder مرورگر → webm → آپلود multipart → پلیر سفارشی (
VoiceMessagePlayerبا waveform و سرعت پخش) - مدیا: عکس/ویدیو/فایل عمومی +
ImageViewermodal - منشن:
@با autocomplete اعضای روم → ذخیره درmentionsJSON → نمایش در صندوق «منشنشدهها» - دیکشنری UI: ۵ تم رنگی روم (slate/ocean/sand/indigo/amethyst)، چیدمان ریسپانسیو موبایل
- جستجو روی متن، ریاکشنهای priority، pin، long-text collapse
- تماس/جلسه از همان هدر (دکمههای Phone/Video/MonitorPlay/Languages)
۳.۴ ترجمهٔ هوشمند پیامها
لایهٔ provider قابلتعویض (src/lib/translate/index.ts):
POST /api/messages/translate— تکپیام{commentId}یا دسته{commentIds:[]}- Term-shield: کدهای پروژه (
PRJ-)، کدهای سند (D-)، URL، ایمیل، منشن و اعداد با__PHn__محافظت و پسترجمه بازگردانده میشوند - Prompt LLM: ترجمهٔ مفهومی + ممنوعیت ترجمهٔ اصطلاحات فنی/اختصارها + تحمل تایپو + خروجی JSON
{lang, text} - کش پایدار در
Comment.translationsJSON — هر پیام فقط یکبار ترجمه میشود - UI: دکمهٔ 🌐 روی هر حباب + toggle «ترجمهٔ همه» در هدر روم (per-room، ذخیره در localStorage) — ترجمههای جدید بهصورت تدریجی و زنده میرسند
- مدل فعلی: OpenRouter
nvidia/nemotron-3-ultra-550b:free(~۵ ثانیه) → fallbacktranslategemma:4bمحلی روی Ollama
۴. زیرساخت Real-time: Signaling + TURN
۴.۱ WebSocket Signaling (server/signaling.mjs)
- Endpoint:
wss://minipms.irhba.ir/call-ws?token=<JWT>— Caddy/call-wsرا به127.0.0.1:8091پروکسی میکند - Auth: ابتدا
GET /api/webrtc/signaling-token— توکن =base64url({userId,exp:1h}).HMAC-SHA256باSIGNALING_SECRET— در WS صحتسنجی میشود؛ کاربر صرفاً با session معتبر توکن میگیرد - Heartbeat:
ping/pong+ Presence:get-presence→ لیست آنلاینها (نشان سبز/خاکستری در UI)
۴.۲ پروتکل WS — پیامهای کلیدی
| جهت | Type | کاربرد |
|---|---|---|
| تماس ۱به۱ | call-invite, call-answer, call-reject, call-end | handshake تماس |
| WebRTC | webrtc-offer, webrtc-answer, webrtc-ice-candidate | مسیریابی SDP/ICE بین دو کاربر (سرور relay-only) |
| جلسه | meet-join, meet-admit, meet-deny, meet-leave, meet-end | waiting-room + کنترل host |
| جلسه | meet-signal (به peerId خاص) | SDP/ICE بین هر جفت peer در mesh |
| جلسه | meet-chat, meet-media, meet-file, meet-hand | چت درونجلسه، وضعیت میک/وبکم، ارسال فایل، دست بلند |
| Whiteboard | meet-wb-toggle/draw/stroke/undo/clear + meet-wb-sync | تخته مشترک روی WS |
| Invite | meet-invite | دعوت به جلسه از داخل پیامرسان (بنر + لینک پیام) |
| Debug | call-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 میزند → سرور میسازد:
username = "<unix-expiry-1h>:<userId>"credential = base64(HMAC-SHA1(TURN_SECRET, username))- پاسخ =
{ username, credential, iceServers: [stun:3478, turn-udp, turn-tcp, google-stun] }
اعتبار اعتبارنامه ۱ ساعت است (time-limited، مطابق استاندارد REST-API coturn). تیمآپ ICE در کلاینت: fallback به stun.l.google.com اگر TURN credentials گرفته نشود.
نکتهٔ پشتیبانی تاریخی: قبلاً relay range با TURNهای دو پروژهٔ دیگر روی همان ماشین همپوشه داشت → allocation صادر میشد ولی دیتا رد نمیشد. رنج جدا (49450–49650) + قوانین فایروال مجزا حل شد.
۵. تماس صوتی/تصویری ۱به۱
۵.۱ جریان
- Context مشترک:
src/contexts/call-context.tsx— یکRTCPeerConnection،getUserMedia{audio, echoCancellation+noiseSuppression}، fallbackaudio:trueساده و در صورت شکست «حالت شنونده» - getUserMedia fail-safe: اگر میکروفن رد شود، کاربر بهعنوان listener وارد میشود و recvonly transceiver اضافه میگردد — تماس ساق نمیشود
- remoteAudio:
<audio autoplay>کهremoteStreamمیگیرد + ویدیو در تماس ویدیویی - تخلیه: ریاستارت مرورگر/قطعی →
call-end→call-cleared
۵.۲ دکمهٔ اسپیکر موبایل
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):
- Glare/perfect-negotiation ساده: peer جدید → موجود بودن peerها از رویداد
meet-peer-joined→ هر peer قدیمی به جدید offer میدهد (initiator = قدیمیتر) — تعارض offer نداریم - ICE: همان TURN creds جلسات —
iceServersاز/api/webrtc/turn-credentials - مقیاس: mesh → O(n²) اتصال — برای ~۸-۱۲ شرکتکننده مناسب؛ بالاتر از آن SFU لازم است (محدودیت طراحی شناختهشده)
۶.۲ جریان ورود (waiting room)
- میزبان: پذیرش/رد در waiting room، پایان جلسه برای همه
- Peer states در WS broadcast:
meet-media{audio,video,screen}→ نمایش Mic/Video/screen-badge روی تایلها - Dance:
meet-hand→ دست بلندشده با انیمیشن روی تایل
۶.۳ قابلیتها
| فیچر | پیادهسازی |
|---|---|
| ویدیو/صدا | mesh getUserMedia per participant، تایلهای object-cover |
| Screen share | getDisplayMedia — بهعنوان MediaStream دوم روی همان peerConnection (screenSendersRef map per peer) با بج screen |
| چت درونجلسه | meet-chat روی WS (بدون ذخیرهٔ دیتابیس — ephemeral) |
| اشتراک فایل | آپلود به /api/live-meetings/[code]/files → LiveMeetingFile → meet-file broadcast → لینک دانلود برای همه |
| Whiteboard | canvas + رویدادهای 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 |
۶.۴ نقاط ورود
- از پیامرسان: دکمهٔ جلسه در هدر روم →
POST /api/live-meetings(code تصادفی) →meet-inviteبرای اعضای آنلاین + لینک/meet/<code>بهعنوان پیام در روم (افراد آفلاین بعداً join میشوند) - URL مستقیم
/meet/<code>— guest با نام میتواند knock کند
۷. امنیت
| لایه | مکانیزم |
|---|---|
| Auth | NextAuth 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 proxy | Caddyfile: /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²) پهنای باند/CPU | mediasoup/Janus SFU برای جلسات بزرگ |
| ترجمهٔ محلی آهسته (~۱ tok/s) | CPU سرور ضعیف | GPU یا ابری (OpenRouter فعلی) |
| setSinkId iOS کار نمیکند | محدودیت Safari | audio route native (PWA→app) |
| چت درونجلسه ephemeral | نیاز نبود ذخیره | اتصال به ChatRoom پروژه |
۱۰. فایلهای مرجع
| بخش | مسیر |
|---|---|
| Signaling + TURN | server/signaling.mjs |
| Call context/UI | src/contexts/call-context.tsx, src/components/messages/call-ui.tsx |
| Meeting room | src/components/meetings/meeting-room.tsx (+ meeting-whiteboard.tsx) |
| Messenger UI | src/app/(dashboard)/messages/page.tsx |
| Chat APIs | src/app/api/chat/, src/app/api/messages/ |
| Live meeting APIs | src/app/api/live-meetings/** |
| WebRTC helpers | src/app/api/webrtc/{signaling-token,turn-credentials} |
| Translation | src/lib/translate/index.ts, src/app/api/messages/translate/route.ts |
| Schema | prisma/schema.prisma (Comment/ChatRoom/LiveMeeting) |