studio/src/utils/remoteDevlog.js
min 412bb2fad9
All checks were successful
CI / Lint (pull_request) Successful in 2m43s
CI / Build (pull_request) Successful in 1m57s
CI / Secret scan (pull_request) Successful in 1m21s
CI / PR size check (pull_request) Successful in 8s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
feat(rbxl-import): студия исполняет импортированные Roblox-Lua скрипты
Сегодня доведены до играбельного состояния:
- UI модалка импорта подключена в KubikonStudio (кнопка для МИНа в навигации)
- Converter: SCALE 0.35 (карта пропорциональна R15-персонажу),
  playerModelType='skin_bacon-hair', Lua упакован в поле code с маркером
  // @roblox-lua (storys API сохраняет только {id,code,target,name})
- vite.config: api+статика через rublox.pro/minecraftia-school.ru
- GameRuntime: распознаёт маркер, запускает через RobloxLuaSandbox
  + wasmoon Worker. Фильтрация: target!=null + lua<2500б +
  лимит 50 sandbox'ов (WASM OOM при >50 VM)
- roblox-shim: nullStub (Proxy с no-op методами) вместо null
  для FindFirstChild/WaitForChild — цепочки не падают
- require() заменён на nullStub
- RobloxLuaSandbox: совместимость с интерфейсом ScriptSandbox
  (sendGlobalEvent/SceneSnapshot/etc — no-op заглушки)
- RobloxLuaWorker: pcall обёртка над user-кодом
- remoteDevlog.js + /devlog endpoint: автосбор browser-логов
- PlayerController._loadSkinManifest: dev-fallback на studio.rublox.pro

Тест на Easy Obby:
- 8205 instances → 2245 primitives + 742 Lua-scripts
- 50/742 Lua-VM запущены (KillBrick handlers и т.п.),
  151 отфильтровано как admin/chat services, 541 пропущено по памяти
- Скин bacon-hair виден, FPS 20-25
- Сцена играется, можно ходить, прыгать

TODO (следующая итерация):
- Single-VM mode для wasmoon (один Lua-state на 742 скрипта,
  убрать WASM OOM)
- Реализовать select/focus в иерархии для импортированных карт
- Touched events от Babylon impostor → Lua-shim сигналы
- Поддержка GUI (ScreenGui/Frame/TextLabel) в конвертере

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 21:13:16 +03:00

169 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* remoteDevlog.js — клиент удалённого dev-логгера.
*
* Перехватывает: console.error/warn, window.onerror, unhandledrejection,
* все fetch/XHR ошибки и не-2xx ответы; батчит и шлёт на бэкенд.
*
* Запускается только в localhost (dev), на проде no-op.
*/
const IS_DEV = typeof window !== 'undefined'
&& (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
const ENDPOINT = '/api-rbxl/devlog';
const FLUSH_INTERVAL_MS = 1500;
const MAX_BATCH = 50;
const queue = [];
let flushTimer = null;
function push(ev) {
if (!IS_DEV) return;
ev.ts = Date.now();
ev.page = location.pathname + location.search;
queue.push(ev);
if (queue.length >= MAX_BATCH) flush();
else scheduleFlush();
}
function scheduleFlush() {
if (flushTimer) return;
flushTimer = setTimeout(flush, FLUSH_INTERVAL_MS);
}
function flush() {
flushTimer = null;
if (queue.length === 0) return;
const events = queue.splice(0, MAX_BATCH);
try {
const blob = new Blob([JSON.stringify({ events })], { type: 'application/json' });
// sendBeacon — не блокирует, переживёт unload
if (navigator.sendBeacon) {
navigator.sendBeacon(ENDPOINT, blob);
} else {
fetch(ENDPOINT, { method: 'POST', body: blob, headers: { 'Content-Type': 'application/json' }, keepalive: true })
.catch(() => {});
}
} catch (e) { /* swallow */ }
}
function truncate(s, max = 4000) {
if (typeof s !== 'string') {
try { s = JSON.stringify(s); } catch { s = String(s); }
}
return s.length > max ? s.slice(0, max) + '...[truncated]' : s;
}
export function installRemoteDevlog() {
if (!IS_DEV) return;
// 1. console.error / console.warn (но НЕ console.log — слишком шумно)
const origError = console.error.bind(console);
const origWarn = console.warn.bind(console);
console.error = (...args) => {
try { push({ kind: 'console.error', message: truncate(args.map(formatArg).join(' ')) }); } catch {}
origError(...args);
};
console.warn = (...args) => {
try { push({ kind: 'console.warn', message: truncate(args.map(formatArg).join(' ')) }); } catch {}
origWarn(...args);
};
// 2. window.onerror
window.addEventListener('error', (ev) => {
push({
kind: 'window.error',
message: ev.message,
filename: ev.filename,
lineno: ev.lineno,
colno: ev.colno,
stack: ev.error?.stack ? truncate(ev.error.stack) : null,
});
});
// 3. Unhandled promise rejection
window.addEventListener('unhandledrejection', (ev) => {
const reason = ev.reason;
push({
kind: 'unhandledrejection',
message: truncate(reason?.message || String(reason)),
stack: reason?.stack ? truncate(reason.stack) : null,
});
});
// 4. fetch wrapper — логируем все не-2xx и failed
const origFetch = window.fetch.bind(window);
window.fetch = async (input, init) => {
const url = typeof input === 'string' ? input : input?.url || '';
const method = (init?.method || 'GET').toUpperCase();
const t0 = performance.now();
try {
const resp = await origFetch(input, init);
if (!resp.ok) {
let body = '';
try {
const cloned = resp.clone();
body = truncate(await cloned.text(), 2000);
} catch {}
push({
kind: 'fetch.bad',
url,
method,
status: resp.status,
duration_ms: Math.round(performance.now() - t0),
body,
});
}
return resp;
} catch (e) {
push({
kind: 'fetch.fail',
url,
method,
duration_ms: Math.round(performance.now() - t0),
message: e?.message || String(e),
});
throw e;
}
};
// 5. XHR wrapper — для axios и т.п.
const XhrOpen = XMLHttpRequest.prototype.open;
const XhrSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
this.__rdl = { method, url, t0: performance.now() };
return XhrOpen.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.send = function (body) {
this.addEventListener('loadend', () => {
const meta = this.__rdl || {};
const status = this.status;
if (status === 0 || status >= 400) {
push({
kind: 'xhr.bad',
url: meta.url,
method: meta.method,
status,
duration_ms: Math.round(performance.now() - (meta.t0 || performance.now())),
body: truncate(this.responseText || '', 2000),
});
}
});
return XhrSend.call(this, body);
};
// Периодически флушим (для долгоживущих логов)
setInterval(() => { if (queue.length) flush(); }, 5000);
window.addEventListener('beforeunload', flush);
// Стартовая отметка чтобы в логе было видно начало сессии
push({ kind: 'session.start', ua: navigator.userAgent });
}
function formatArg(a) {
if (a == null) return String(a);
if (typeof a === 'string') return a;
if (a instanceof Error) return a.message + (a.stack ? '\n' + a.stack : '');
try { return JSON.stringify(a); } catch { return String(a); }
}