/** * 远程播放桥接:SSE 订阅 + 命令队列 + 本地状态回传。 */ import { API_VERSION } from "./api.js"; const API_ROOT = "/api/agv"; const REMOTE_METHODS = new Set(["playAction", "stopAction", "clearActions"]); const CLIENT_ID = globalThis.crypto?.randomUUID?.() || `client-${Date.now()}`; export function connectRemotePlayback(playbackApi, { onStatus } = {}) { let lastRevision = 0; let connected = false; let es = null; const queue = []; let draining = false; let applyingRemote = 0; function setStatus(text, kind = "idle") { onStatus?.({ text, kind, connected, revision: lastRevision }); } function shouldSkipEcho(msg) { return msg?.sourceClientId && msg.sourceClientId === CLIENT_ID; } async function waitUntilReady(maxMs = 20000) { const start = Date.now(); while (Date.now() - start < maxMs) { if (playbackApi.getState?.().ready) return true; await new Promise((r) => setTimeout(r, 150)); } return playbackApi.getState?.().ready; } async function withRemoteApply(fn) { applyingRemote += 1; try { return await fn(); } finally { applyingRemote -= 1; } } async function ensureModel(modelId) { if (!modelId) return true; const cur = playbackApi.getState?.() || {}; if (cur.modelId === modelId && cur.ready) return true; const res = await playbackApi.request({ version: API_VERSION, method: "clearActions", params: { modelId }, }); if (!res.ok) { setStatus(`远程加载失败: ${res.error?.message || "unknown"}`, "error"); return false; } return waitUntilReady(); } async function applySnapshot(state) { if (!state) return false; const revision = Number(state.revision) || 0; if (revision <= lastRevision) return true; return withRemoteApply(async () => { const modelId = state.modelId; const desired = state.activeActions || []; if (!(await ensureModel(modelId))) return false; const cur = playbackApi.getState?.() || {}; const current = new Set(cur.activeActions || []); const want = new Set(desired); for (const id of current) { if (!want.has(id)) { const res = await playbackApi.request({ version: API_VERSION, method: "stopAction", params: { modelId: cur.modelId || modelId, actionId: id }, }); if (!res.ok) { setStatus(`远程动作同步失败: ${res.error?.message || "unknown"}`, "error"); return false; } } } for (const id of want) { if (!current.has(id)) { const res = await playbackApi.request({ version: API_VERSION, method: "playAction", params: { modelId: cur.modelId || modelId, actionId: id }, }); if (!res.ok) { setStatus(`远程动作同步失败: ${res.error?.message || "unknown"}`, "error"); return false; } } } lastRevision = revision; return true; }); } async function runCommand(msg) { if (msg.type === "connected") { connected = true; setStatus("远程已连接", "ok"); if (msg.state) await applySnapshot(msg.state); return; } if (msg.type === "ping") return; if (!msg.method) return; const revision = Number(msg.revision) || 0; if (revision && revision <= lastRevision) return; if (shouldSkipEcho(msg)) { if (revision) lastRevision = Math.max(lastRevision, revision); return; } if (msg.method === "syncState") { await applySnapshot(msg.state); return; } if (!REMOTE_METHODS.has(msg.method)) return; await withRemoteApply(async () => { const p = msg.params || {}; if (!(await ensureModel(p.modelId || p.model))) return; await waitUntilReady(); const res = await playbackApi.request({ version: API_VERSION, id: msg.id, method: msg.method, params: p, }); if (!res.ok) { setStatus(`远程命令失败: ${res.error?.message || "unknown"}`, "error"); return; } if (revision) lastRevision = Math.max(lastRevision, revision); }); } function enqueue(msg) { queue.push(msg); drain(); } async function drain() { if (draining) return; draining = true; while (queue.length) { const msg = queue.shift(); try { await runCommand(msg); } catch (e) { console.warn("[remote-bridge]", e); setStatus(`远程执行异常: ${e.message}`, "error"); } } draining = false; } function connectSse() { if (es) es.close(); es = new EventSource(`${API_ROOT}/events`); es.onmessage = (ev) => { try { enqueue(JSON.parse(ev.data)); } catch (e) { console.warn("[remote-bridge] bad event", e); } }; es.onerror = () => { connected = false; setStatus("远程连接断开,自动重连…", "warn"); }; } async function fetchServerState() { const res = await fetch(`${API_ROOT}/state?t=${Date.now()}`); const body = await res.json(); return body.ok ? body.data : null; } async function syncFromServer() { try { const state = await fetchServerState(); if (!state || (state.revision || 0) <= 0) return state; await applySnapshot(state); return state; } catch (e) { setStatus("远程状态同步失败", "warn"); return null; } } let reportTimer = null; function reportLocalState() { if (applyingRemote > 0) return; clearTimeout(reportTimer); reportTimer = setTimeout(async () => { const s = playbackApi.getState?.(); if (!s?.ready || applyingRemote > 0) return; try { await fetch(`${API_ROOT}/report`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId: CLIENT_ID, modelId: s.modelId, activeActions: s.activeActions, }), }); } catch (_) { /* ignore */ } }, 250); } connectSse(); setStatus("等待远程连接…", "idle"); return { clientId: CLIENT_ID, fetchServerState, syncFromServer, reportLocalState, close: () => { clearTimeout(reportTimer); es?.close(); }, }; }