Files
Migu2.0/SimpleLite/model/agv_glb/viewer/remote-bridge.js
T
ArtoriasWu 15405ba114 新增 WMS 搬运规则与任务管理
支持搬运规则维护、候选预览、任务生成、预占、下发、取消和完成,并完善仓储管理前端交互。
2026-06-25 11:02:22 +08:00

240 lines
6.3 KiB
JavaScript

/**
* 远程播放桥接: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();
},
};
}