SHA256
Merge branch 'main' of https://github.com/ai5590/SHiNE-server
# Conflicts: # shine-UI/js/app.js
This commit is contained in:
+111
-1
@@ -3,7 +3,13 @@ import { renderToolbar } from './components/toolbar.js';
|
||||
import { captureClientError, setClientErrorTransport } from './services/client-error-reporter.js';
|
||||
import { initPwaInstallPromptHandling } from './services/pwa-install-service.js';
|
||||
import { initPwaPush } from './services/pwa-push-service.js';
|
||||
import { handleIncomingCallInvite, handleIncomingCallSignal } from './services/call-service.js';
|
||||
import {
|
||||
handleIncomingCallInvite,
|
||||
handleIncomingCallSignal,
|
||||
setCallDebugReporter,
|
||||
startDebugConnectionAsInitiator,
|
||||
startDebugConnectionAsResponder,
|
||||
} from './services/call-service.js';
|
||||
import {
|
||||
authService,
|
||||
addAppLogEntry,
|
||||
@@ -104,12 +110,14 @@ let currentCleanup = null;
|
||||
let pingIntervalId = null;
|
||||
let versionCheckIntervalId = null;
|
||||
let versionCheckInFlight = false;
|
||||
let reconnectIntervalId = null;
|
||||
let sessionRuntimeStarted = false;
|
||||
let connectionStatusEl = null;
|
||||
let connectionState = '';
|
||||
|
||||
setClientErrorTransport((payload) => authService.reportClientError(payload));
|
||||
initPwaInstallPromptHandling();
|
||||
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
|
||||
|
||||
function ensureConnectionStatusEl() {
|
||||
if (connectionStatusEl) return connectionStatusEl;
|
||||
@@ -219,6 +227,16 @@ function startConnectionMonitor() {
|
||||
}, CONNECTION_CHECK_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function isReconnectAllowedNow() {
|
||||
const pageId = getRoute().pageId || '';
|
||||
return !PRE_AUTH_PAGES.includes(pageId);
|
||||
}
|
||||
|
||||
function wsIsOpen() {
|
||||
const ws = authService?.ws?.ws;
|
||||
return !!(ws && ws.readyState === WebSocket.OPEN);
|
||||
}
|
||||
|
||||
function showGlobalErrorAlert(title, details = {}) {
|
||||
const lines = [title];
|
||||
if (details.message) lines.push(`Сообщение: ${details.message}`);
|
||||
@@ -411,6 +429,32 @@ async function ensureSessionRuntimeStarted() {
|
||||
});
|
||||
|
||||
startConnectionMonitor();
|
||||
|
||||
if (reconnectIntervalId) {
|
||||
window.clearInterval(reconnectIntervalId);
|
||||
reconnectIntervalId = null;
|
||||
}
|
||||
reconnectIntervalId = window.setInterval(async () => {
|
||||
if (!state.session.isAuthorized) return;
|
||||
if (!isReconnectAllowedNow()) return;
|
||||
if (wsIsOpen()) return;
|
||||
|
||||
try {
|
||||
await authService.ws.open();
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
source: 'ws-reconnect',
|
||||
message: 'WS переподключен автоматически',
|
||||
});
|
||||
} catch (error) {
|
||||
addAppLogEntry({
|
||||
level: 'warn',
|
||||
source: 'ws-reconnect',
|
||||
message: 'Попытка автопереподключения не удалась',
|
||||
details: { error: error?.message || 'unknown' },
|
||||
});
|
||||
}
|
||||
}, 15_000);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
@@ -423,6 +467,10 @@ async function init() {
|
||||
setSessionResetHandler(() => {
|
||||
sessionRuntimeStarted = false;
|
||||
startConnectionMonitor();
|
||||
if (reconnectIntervalId) {
|
||||
window.clearInterval(reconnectIntervalId);
|
||||
reconnectIntervalId = null;
|
||||
}
|
||||
navigate('start-view');
|
||||
});
|
||||
|
||||
@@ -564,6 +612,68 @@ async function init() {
|
||||
try { await handleIncomingCallSignal(evt); } catch {}
|
||||
});
|
||||
|
||||
authService.onEvent('DebugConnectPrepareResponder', async (evt) => {
|
||||
try {
|
||||
const p = evt?.payload || {};
|
||||
await startDebugConnectionAsResponder({
|
||||
runId: p.runId,
|
||||
callId: p.callId,
|
||||
peerLogin: p.peerLogin,
|
||||
peerSessionId: p.peerSessionId,
|
||||
});
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
source: 'debug-connect',
|
||||
message: 'Получена команда debug responder',
|
||||
details: p,
|
||||
});
|
||||
} catch (error) {
|
||||
addAppLogEntry({
|
||||
level: 'error',
|
||||
source: 'debug-connect',
|
||||
message: 'Ошибка запуска debug responder',
|
||||
details: { error: error?.message || 'unknown' },
|
||||
});
|
||||
await authService.reportClientDebug({
|
||||
runId: evt?.payload?.runId || '',
|
||||
level: 'error',
|
||||
message: 'debug_responder_failed',
|
||||
details: error?.message || 'unknown',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
authService.onEvent('DebugConnectStartInitiator', async (evt) => {
|
||||
try {
|
||||
const p = evt?.payload || {};
|
||||
await startDebugConnectionAsInitiator({
|
||||
runId: p.runId,
|
||||
callId: p.callId,
|
||||
peerLogin: p.peerLogin,
|
||||
peerSessionId: p.peerSessionId,
|
||||
});
|
||||
addAppLogEntry({
|
||||
level: 'info',
|
||||
source: 'debug-connect',
|
||||
message: 'Получена команда debug initiator',
|
||||
details: p,
|
||||
});
|
||||
} catch (error) {
|
||||
addAppLogEntry({
|
||||
level: 'error',
|
||||
source: 'debug-connect',
|
||||
message: 'Ошибка запуска debug initiator',
|
||||
details: { error: error?.message || 'unknown' },
|
||||
});
|
||||
await authService.reportClientDebug({
|
||||
runId: evt?.payload?.runId || '',
|
||||
level: 'error',
|
||||
message: 'debug_initiator_failed',
|
||||
details: error?.message || 'unknown',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await tryAutoLogin();
|
||||
await hydrateMessagesFromStore();
|
||||
startVersionMonitor();
|
||||
|
||||
@@ -1639,6 +1639,16 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
|
||||
async reportClientDebug({ runId = '', level = 'info', message = '', details = '' } = {}) {
|
||||
try {
|
||||
const response = await this.ws.request('ClientDebugLog', { runId, level, message, details }, 3000);
|
||||
return response?.status === 200;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async reportClientError(details) {
|
||||
try {
|
||||
const response = await this.ws.request('ClientErrorLog', details || {}, 3000);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addChatMessage, state, authService } from '../state.js';
|
||||
import { addChatMessage, authService } from '../state.js';
|
||||
|
||||
const TYPES = {
|
||||
INVITE: 100,
|
||||
@@ -14,6 +14,7 @@ const TYPES = {
|
||||
|
||||
const calls = new Map();
|
||||
let activeCallId = '';
|
||||
let debugReporter = null;
|
||||
|
||||
function nowMs() {
|
||||
return Date.now();
|
||||
@@ -27,9 +28,26 @@ function getCall(callId) {
|
||||
return calls.get(callId) || null;
|
||||
}
|
||||
|
||||
function toErrorText(error) {
|
||||
return error?.message || String(error || 'unknown');
|
||||
}
|
||||
|
||||
async function emitDebug(call, level, message, details = '') {
|
||||
if (!call?.debugRunId || typeof debugReporter !== 'function') return;
|
||||
try {
|
||||
await debugReporter({
|
||||
runId: call.debugRunId,
|
||||
level,
|
||||
message,
|
||||
details,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function setStatus(call, text) {
|
||||
call.status = text;
|
||||
addChatMessage(call.peerLogin, `[call] ${text}`);
|
||||
addChatMessage(call.peerLogin || 'debug-peer', `[call] ${text}`);
|
||||
void emitDebug(call, 'info', `call_status: ${text}`, `callId=${call.callId}`);
|
||||
}
|
||||
|
||||
function cleanupTimers(call) {
|
||||
@@ -61,10 +79,30 @@ async function finishCall(call, reason, notifyRemote = false) {
|
||||
}
|
||||
await closeMedia(call);
|
||||
setStatus(call, `завершен: ${reason}`);
|
||||
if (String(reason || '').toLowerCase().includes('connected')) {
|
||||
await emitDebug(call, 'info', 'debug_connection_success', reason);
|
||||
}
|
||||
calls.delete(call.callId);
|
||||
if (activeCallId === call.callId) activeCallId = '';
|
||||
}
|
||||
|
||||
async function sendSignal(call, type, data = '') {
|
||||
if (!call.remoteSessionId) return;
|
||||
try {
|
||||
await authService.callSignalToSession({
|
||||
toLogin: call.peerLogin,
|
||||
targetSessionId: call.remoteSessionId,
|
||||
callId: call.callId,
|
||||
type,
|
||||
data,
|
||||
});
|
||||
await emitDebug(call, 'info', `signal_sent_${type}`, `len=${String(data || '').length}`);
|
||||
} catch (error) {
|
||||
await emitDebug(call, 'error', `signal_send_failed_${type}`, toErrorText(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePeerConnection(call) {
|
||||
if (call.pc) return call.pc;
|
||||
|
||||
@@ -72,25 +110,45 @@ async function ensurePeerConnection(call) {
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
||||
});
|
||||
|
||||
if (call.debugMode && call.debugRole === 'initiator') {
|
||||
const dc = pc.createDataChannel('debug-ping');
|
||||
dc.onopen = () => {
|
||||
try { dc.send('ping'); } catch {}
|
||||
void emitDebug(call, 'info', 'debug_datachannel_open', 'sent ping');
|
||||
};
|
||||
dc.onmessage = (evt) => {
|
||||
void emitDebug(call, 'info', 'debug_datachannel_message', String(evt?.data || ''));
|
||||
};
|
||||
}
|
||||
|
||||
pc.ondatachannel = (evt) => {
|
||||
const ch = evt?.channel;
|
||||
if (!ch) return;
|
||||
ch.onmessage = (msg) => {
|
||||
const incoming = String(msg?.data || '');
|
||||
void emitDebug(call, 'info', 'debug_datachannel_message_in', incoming);
|
||||
if (incoming === 'ping') {
|
||||
try { ch.send('pong'); } catch {}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pc.onicecandidate = async (event) => {
|
||||
if (!event.candidate || !call.remoteSessionId) return;
|
||||
try {
|
||||
await authService.callSignalToSession({
|
||||
toLogin: call.peerLogin,
|
||||
targetSessionId: call.remoteSessionId,
|
||||
callId: call.callId,
|
||||
type: TYPES.ICE,
|
||||
data: JSON.stringify(event.candidate),
|
||||
});
|
||||
await sendSignal(call, TYPES.ICE, JSON.stringify(event.candidate));
|
||||
} catch {}
|
||||
};
|
||||
|
||||
pc.onconnectionstatechange = () => {
|
||||
if (pc.connectionState === 'connected') {
|
||||
const state = pc.connectionState;
|
||||
if (state === 'connected') {
|
||||
setStatus(call, 'соединение установлено');
|
||||
void emitDebug(call, 'info', 'peer_connection_connected', `callId=${call.callId}`);
|
||||
}
|
||||
if (pc.connectionState === 'failed' || pc.connectionState === 'closed' || pc.connectionState === 'disconnected') {
|
||||
finishCall(call, `state=${pc.connectionState}`, false);
|
||||
if (state === 'failed' || state === 'closed' || state === 'disconnected') {
|
||||
void emitDebug(call, 'warn', 'peer_connection_closed', `state=${state}`);
|
||||
finishCall(call, `state=${state}`, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,6 +158,7 @@ async function ensurePeerConnection(call) {
|
||||
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
|
||||
} catch (e) {
|
||||
setStatus(call, `нет доступа к микрофону: ${e?.message || 'unknown'}`);
|
||||
await emitDebug(call, 'warn', 'microphone_access_failed', toErrorText(e));
|
||||
}
|
||||
|
||||
pc.ontrack = (evt) => {
|
||||
@@ -113,17 +172,6 @@ async function ensurePeerConnection(call) {
|
||||
return pc;
|
||||
}
|
||||
|
||||
async function sendSignal(call, type, data = '') {
|
||||
if (!call.remoteSessionId) return;
|
||||
await authService.callSignalToSession({
|
||||
toLogin: call.peerLogin,
|
||||
targetSessionId: call.remoteSessionId,
|
||||
callId: call.callId,
|
||||
type,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async function onAccept(call) {
|
||||
cleanupTimers(call);
|
||||
const pc = await ensurePeerConnection(call);
|
||||
@@ -133,6 +181,72 @@ async function onAccept(call) {
|
||||
setStatus(call, 'отправлен offer');
|
||||
}
|
||||
|
||||
export function setCallDebugReporter(fn) {
|
||||
debugReporter = typeof fn === 'function' ? fn : null;
|
||||
}
|
||||
|
||||
export async function startDebugConnectionAsResponder({ runId, callId, peerLogin, peerSessionId }) {
|
||||
const cleanCallId = String(callId || '').trim();
|
||||
const cleanPeerLogin = String(peerLogin || '').trim();
|
||||
const cleanPeerSessionId = String(peerSessionId || '').trim();
|
||||
if (!cleanCallId || !cleanPeerLogin || !cleanPeerSessionId) return;
|
||||
|
||||
let call = getCall(cleanCallId);
|
||||
if (!call) {
|
||||
call = {
|
||||
callId: cleanCallId,
|
||||
peerLogin: cleanPeerLogin,
|
||||
direction: 'in',
|
||||
state: 'accepted',
|
||||
remoteSessionId: cleanPeerSessionId,
|
||||
timers: {},
|
||||
startedAtMs: nowMs(),
|
||||
pc: null,
|
||||
localStream: null,
|
||||
debugMode: true,
|
||||
debugRunId: String(runId || '').trim(),
|
||||
debugRole: 'responder',
|
||||
};
|
||||
calls.set(cleanCallId, call);
|
||||
}
|
||||
|
||||
activeCallId = cleanCallId;
|
||||
await emitDebug(call, 'info', 'debug_prepare_responder', `peerSessionId=${cleanPeerSessionId}`);
|
||||
setStatus(call, 'debug: responder готов, ждём offer');
|
||||
}
|
||||
|
||||
export async function startDebugConnectionAsInitiator({ runId, callId, peerLogin, peerSessionId }) {
|
||||
const cleanCallId = String(callId || '').trim();
|
||||
const cleanPeerLogin = String(peerLogin || '').trim();
|
||||
const cleanPeerSessionId = String(peerSessionId || '').trim();
|
||||
if (!cleanCallId || !cleanPeerLogin || !cleanPeerSessionId) return;
|
||||
|
||||
const call = {
|
||||
callId: cleanCallId,
|
||||
peerLogin: cleanPeerLogin,
|
||||
direction: 'out',
|
||||
state: 'accepted',
|
||||
remoteSessionId: cleanPeerSessionId,
|
||||
timers: {},
|
||||
startedAtMs: nowMs(),
|
||||
pc: null,
|
||||
localStream: null,
|
||||
debugMode: true,
|
||||
debugRunId: String(runId || '').trim(),
|
||||
debugRole: 'initiator',
|
||||
};
|
||||
|
||||
calls.set(cleanCallId, call);
|
||||
activeCallId = cleanCallId;
|
||||
await emitDebug(call, 'info', 'debug_start_initiator', `peerSessionId=${cleanPeerSessionId}`);
|
||||
try {
|
||||
await onAccept(call);
|
||||
} catch (error) {
|
||||
await emitDebug(call, 'error', 'debug_initiator_start_failed', toErrorText(error));
|
||||
await finishCall(call, `debug start failed: ${toErrorText(error)}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startOutgoingCall(peerLogin) {
|
||||
const cleanPeer = String(peerLogin || '').trim();
|
||||
if (!cleanPeer) return;
|
||||
@@ -153,6 +267,9 @@ export async function startOutgoingCall(peerLogin) {
|
||||
startedAtMs: nowMs(),
|
||||
pc: null,
|
||||
localStream: null,
|
||||
debugMode: false,
|
||||
debugRunId: '',
|
||||
debugRole: '',
|
||||
};
|
||||
calls.set(callId, call);
|
||||
activeCallId = callId;
|
||||
@@ -203,6 +320,9 @@ export async function handleIncomingCallInvite(evt) {
|
||||
startedAtMs: nowMs(),
|
||||
pc: null,
|
||||
localStream: null,
|
||||
debugMode: false,
|
||||
debugRunId: '',
|
||||
debugRole: '',
|
||||
};
|
||||
calls.set(callId, call);
|
||||
}
|
||||
@@ -273,25 +393,43 @@ export async function handleIncomingCallSignal(evt) {
|
||||
}
|
||||
|
||||
if (type === TYPES.OFFER) {
|
||||
const pc = await ensurePeerConnection(call);
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
await sendSignal(call, TYPES.ANSWER, JSON.stringify(answer));
|
||||
setStatus(call, 'получен offer, отправлен answer');
|
||||
try {
|
||||
const pc = await ensurePeerConnection(call);
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
await sendSignal(call, TYPES.ANSWER, JSON.stringify(answer));
|
||||
setStatus(call, 'получен offer, отправлен answer');
|
||||
await emitDebug(call, 'info', 'offer_processed', 'answer sent');
|
||||
} catch (error) {
|
||||
await emitDebug(call, 'error', 'offer_process_failed', toErrorText(error));
|
||||
throw error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === TYPES.ANSWER) {
|
||||
const pc = await ensurePeerConnection(call);
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
||||
setStatus(call, 'получен answer');
|
||||
try {
|
||||
const pc = await ensurePeerConnection(call);
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
||||
setStatus(call, 'получен answer');
|
||||
await emitDebug(call, 'info', 'answer_processed', 'remote description set');
|
||||
} catch (error) {
|
||||
await emitDebug(call, 'error', 'answer_process_failed', toErrorText(error));
|
||||
throw error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === TYPES.ICE) {
|
||||
const pc = await ensurePeerConnection(call);
|
||||
await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(data)));
|
||||
try {
|
||||
const pc = await ensurePeerConnection(call);
|
||||
await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(data)));
|
||||
await emitDebug(call, 'info', 'ice_processed', 'candidate added');
|
||||
} catch (error) {
|
||||
await emitDebug(call, 'error', 'ice_process_failed', toErrorText(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user