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

228 lines
8.2 KiB
JavaScript

/**
* AGV 播放页外部 API:标准化请求/响应格式。
*
* 嵌入 iframe 时父页面:
* await iframe.contentWindow.AgvPlaybackApi.getCatalog()
* await iframe.contentWindow.AgvPlaybackApi.request({ version: 1, method: "playAction", params: { modelId: "英招", actionId: "dongzuo1" } })
*
* 跨窗口 postMessage(父 → 播放页):
* iframe.contentWindow.postMessage({ version: 1, method: "playAction", params: { modelId: "英招", actionId: "dongzuo1" }, id: "req-1" }, "*")
* 播放页回复:{ version: 1, id: "req-1", ok: true, data: { ... } }
*/
export const API_VERSION = 1;
export const METHODS = {
PLAY_ACTION: "playAction",
STOP_ACTION: "stopAction",
CLEAR_ACTIONS: "clearActions",
};
const ALLOWED_METHODS = new Set(Object.values(METHODS));
const ERROR = {
INVALID_REQUEST: "INVALID_REQUEST",
UNKNOWN_METHOD: "UNKNOWN_METHOD",
INTERNAL_ERROR: "INTERNAL_ERROR",
};
function isPlainObject(v) {
return v != null && typeof v === "object" && !Array.isArray(v);
}
function asString(v) {
return typeof v === "string" ? v.trim() : "";
}
function asStringList(v) {
if (Array.isArray(v)) return v.map(asString).filter(Boolean);
if (typeof v === "string") return v.split(/[,+\s]+/).map((s) => s.trim()).filter(Boolean);
return [];
}
async function ensureModel(controller, params) {
const modelId = asString(params.modelId ?? params.model);
if (!modelId) return { ok: false, error: { code: ERROR.INVALID_REQUEST, message: "params.modelId 必填" } };
const cur = controller.getState();
if (cur.modelId === modelId && cur.ready) return { ok: true };
await controller.loadModel(modelId);
return { ok: true };
}
/** @typedef {{ version: number, id?: string, method: string, params?: object }} AgvApiRequest */
/** @typedef {{ version: number, id?: string, ok: boolean, data?: object|null, error?: { code: string, message: string }|null }} AgvApiResponse */
/**
* 校验并规范化入站请求。
* @param {unknown} raw
* @returns {{ ok: true, request: AgvApiRequest } | { ok: false, error: { code: string, message: string } } }
*/
export function parseRequest(raw) {
if (!isPlainObject(raw)) {
return { ok: false, error: { code: ERROR.INVALID_REQUEST, message: "请求体须为 JSON 对象" } };
}
const version = Number(raw.version);
if (version !== API_VERSION) {
return { ok: false, error: { code: ERROR.INVALID_REQUEST, message: `不支持的 version,当前为 ${API_VERSION}` } };
}
const method = asString(raw.method);
if (!method) {
return { ok: false, error: { code: ERROR.INVALID_REQUEST, message: "缺少 method" } };
}
if (!ALLOWED_METHODS.has(method)) {
return { ok: false, error: { code: ERROR.UNKNOWN_METHOD, message: "仅支持 playAction、stopAction、clearActions" } };
}
const params = isPlainObject(raw.params) ? raw.params : {};
const id = raw.id != null ? String(raw.id) : undefined;
return { ok: true, request: { version, method, params, id } };
}
/**
* @param {string|undefined} id
* @param {boolean} ok
* @param {object|null} [data]
* @param {{ code: string, message: string }|null} [error]
* @returns {AgvApiResponse}
*/
export function formatResponse(id, ok, data = null, error = null) {
return { version: API_VERSION, id, ok, data, error };
}
/**
* @param {object} controller
* @param {{ onStateChange?: (state: object) => void, allowedOrigins?: string[]|null }} [options]
*/
export function createPlaybackApi(controller, options = {}) {
const allowedOrigins = options.allowedOrigins ?? null;
let messageBound = false;
function emitState() {
try {
const state = controller.getState();
options.onStateChange?.(state);
dispatchEvent(new CustomEvent("agv-playback", { detail: { type: "state", state } }));
} catch (_) { /* ignore */ }
}
async function dispatch(req) {
const { method, params = {} } = req;
try {
switch (method) {
case METHODS.PLAY_ACTION: {
const ensured = await ensureModel(controller, params);
if (!ensured.ok) return formatResponse(req.id, false, null, ensured.error);
const ids = asStringList(params.actionIds ?? params.actions).length
? asStringList(params.actionIds ?? params.actions)
: [asString(params.actionId ?? params.action)];
const played = [];
for (const id of ids) {
if (!id) continue;
const r = controller.playAction(id);
if (!r.ok) return formatResponse(req.id, false, null, r.error);
played.push(id);
}
if (!played.length) {
return formatResponse(req.id, false, null, { code: ERROR.INVALID_REQUEST, message: "params.actionId 必填" });
}
emitState();
return formatResponse(req.id, true, {
modelId: controller.getState().modelId,
activeActions: controller.getState().activeActions,
played,
});
}
case METHODS.STOP_ACTION: {
const ensured = await ensureModel(controller, params);
if (!ensured.ok) return formatResponse(req.id, false, null, ensured.error);
const ids = asStringList(params.actionIds ?? params.actions).length
? asStringList(params.actionIds ?? params.actions)
: [asString(params.actionId ?? params.action)];
const stopped = [];
for (const id of ids) {
if (!id) continue;
const r = controller.stopAction(id);
if (!r.ok) return formatResponse(req.id, false, null, r.error);
stopped.push(id);
}
if (!stopped.length) {
return formatResponse(req.id, false, null, { code: ERROR.INVALID_REQUEST, message: "params.actionId 必填" });
}
emitState();
return formatResponse(req.id, true, {
modelId: controller.getState().modelId,
activeActions: controller.getState().activeActions,
stopped,
});
}
case METHODS.CLEAR_ACTIONS: {
const ensured = await ensureModel(controller, params);
if (!ensured.ok) return formatResponse(req.id, false, null, ensured.error);
controller.clearActions();
emitState();
return formatResponse(req.id, true, { modelId: controller.getState().modelId, activeActions: [] });
}
default:
return formatResponse(req.id, false, null, { code: ERROR.UNKNOWN_METHOD, message: "仅支持 playAction、stopAction、clearActions" });
}
} catch (e) {
return formatResponse(req.id, false, null, {
code: ERROR.INTERNAL_ERROR,
message: e?.message || String(e),
});
}
}
function isOriginAllowed(origin) {
if (!allowedOrigins || allowedOrigins.includes("*")) return true;
return allowedOrigins.includes(origin);
}
function onMessage(event) {
if (!isOriginAllowed(event.origin)) return;
const parsed = parseRequest(event.data);
if (!parsed.ok) {
const res = formatResponse(undefined, false, null, parsed.error);
event.source?.postMessage?.(res, event.origin);
return;
}
if (!parsed.request.method) return;
Promise.resolve(dispatch(parsed.request)).then((res) => {
if (event.source && typeof event.source.postMessage === "function") {
event.source.postMessage(res, event.origin);
}
});
}
function attach(target = window) {
const api = {
version: API_VERSION,
METHODS,
request: (raw) => {
const parsed = parseRequest({ version: API_VERSION, ...raw });
if (!parsed.ok) return Promise.resolve(formatResponse(raw?.id, false, null, parsed.error));
return Promise.resolve(dispatch(parsed.request));
},
getCatalog: () => fetch("/api/catalog").then((r) => r.json()),
getState: () => controller.getState(),
parseRequest,
formatResponse,
};
target.AgvPlaybackApi = api;
if (!messageBound) {
addEventListener("message", onMessage);
messageBound = true;
}
return api;
}
function detach() {
if (messageBound) {
removeEventListener("message", onMessage);
messageBound = false;
}
delete window.AgvPlaybackApi;
}
return { attach, detach, dispatch, parseRequest, formatResponse };
}