diff --git a/rbxl-importer/src/app.py b/rbxl-importer/src/app.py index 287f0f7..00a113c 100644 --- a/rbxl-importer/src/app.py +++ b/rbxl-importer/src/app.py @@ -64,6 +64,11 @@ ALLOWED_USER_IDS = [1] # пока только МИН app = Flask(__name__) CORS(app, resources={r'/*': {'origins': '*'}}) +# Devlog для удалённой отладки dev-сессий студии: фронт пушит сюда +# console.error/warn, failed network requests, неожиданные exceptions. +from app_devlog import devlog_bp +app.register_blueprint(devlog_bp) + try: rds = redis.from_url(REDIS_URL, decode_responses=False) rds.ping() diff --git a/rbxl-importer/src/app_devlog.py b/rbxl-importer/src/app_devlog.py new file mode 100644 index 0000000..702a7dd --- /dev/null +++ b/rbxl-importer/src/app_devlog.py @@ -0,0 +1,65 @@ +""" +app_devlog.py — endpoint /devlog для удалённого сбора логов dev-сессии студии. + +Подключается к Flask из app.py через blueprint. + +Эндпоинты: + POST /devlog — приём batch'а событий из браузера + GET /devlog/recent — последние N событий (для меня) + GET /devlog/clear — очистить лог + +Хранение: append-only файл /opt/rbxl-importer/devlog.jsonl. + +CORS открыт для localhost:* (dev режим). +""" +import os +import json +import time +from flask import Blueprint, request, jsonify + +devlog_bp = Blueprint('devlog', __name__) + +DEVLOG_PATH = os.environ.get('DEVLOG_PATH', '/opt/rbxl-importer/devlog.jsonl') + + +@devlog_bp.post('/devlog') +def post_devlog(): + """Принимает массив событий из браузера. + + Каждое событие: {ts, kind, url?, status?, body?, message?, stack?, extra?} + """ + data = request.get_json(silent=True) or {} + events = data.get('events') or [] + if not isinstance(events, list): + return jsonify({'error': 'events must be list'}), 400 + received_at = time.time() + with open(DEVLOG_PATH, 'a', encoding='utf-8') as f: + for ev in events: + if not isinstance(ev, dict): + continue + ev['received_at'] = received_at + f.write(json.dumps(ev, ensure_ascii=False, default=str) + '\n') + return jsonify({'ok': True, 'count': len(events)}) + + +@devlog_bp.get('/devlog/recent') +def get_recent(): + """Последние N событий (по умолчанию 200).""" + n = int(request.args.get('n', 200)) + out = [] + if os.path.exists(DEVLOG_PATH): + with open(DEVLOG_PATH, 'r', encoding='utf-8') as f: + lines = f.readlines()[-n:] + for line in lines: + try: + out.append(json.loads(line)) + except Exception: + pass + return jsonify({'events': out, 'total': len(out)}) + + +@devlog_bp.get('/devlog/clear') +def clear(): + if os.path.exists(DEVLOG_PATH): + os.unlink(DEVLOG_PATH) + return jsonify({'ok': True}) diff --git a/rbxl-importer/src/converter.py b/rbxl-importer/src/converter.py index d7218d4..06ce297 100644 --- a/rbxl-importer/src/converter.py +++ b/rbxl-importer/src/converter.py @@ -31,6 +31,7 @@ CFrame → position + rotationX/Y/Z (Euler XYZ в радианах). """ from dataclasses import dataclass, field, asdict from typing import List, Dict, Any, Optional, Tuple +import json import math import logging @@ -45,8 +46,11 @@ logger = logging.getLogger(__name__) # ────── константы маппинга ────── -# Roblox stud → метры (примерно 0.28). Можно поменять при импорте. -DEFAULT_SCALE = 0.28 +# Roblox stud → unit Rublox-движка. +# R15-персонаж в Rublox ~5.5м, Roblox-персонаж ~5stud высотой. Чтобы карта +# была пропорциональна персонажу — scale 0.35 (платформа 4stud → 1.4 unit, +# как стандартная Rublox-платформа). +DEFAULT_SCALE = 0.35 # Маппинг Material enum → Rublox material strings. # Roblox Enum.Material: @@ -214,7 +218,9 @@ class Converter: 'gui': [], 'inventory': [], 'spawnPoint': {'x': 0, 'y': 2, 'z': 0}, - 'playerModelType': 'default', + # Стандартный R15-скин bacon-hair как во всех новых проектах студии. + # 'default' — невалидный typeId, PlayerController на нём падает. + 'playerModelType': 'skin_bacon-hair', 'worldSize': 100, 'floorEnabled': True, 'jumpPowerMul': 1.0, @@ -615,15 +621,21 @@ class Converter: script_id = f'rbx_{self._next_script_id}' self._next_script_id += 1 + # storys-нормализатор сохраняет только {id,code,target,name} — + # поэтому упаковываем kind+lua_source ВНУТРЬ поля code как комментарий-маркер, + # а GameRuntime/RobloxLuaSandbox распакуют обратно. + # Маркер: первая строка ровно "// @roblox-lua" + JSON-метадата на 2-й строке. + marker_header = '// @roblox-lua\n// ' + json.dumps({ + 'roblox_class': inst.class_name, + 'enabled': bool(props.get('Disabled', False) is False), + }, ensure_ascii=False) + '\n/* lua_source:\n' + marker_footer = '\n*/\n' + packed_code = marker_header + source + marker_footer scene['scripts'].append({ 'id': script_id, 'name': name_or_default(props, inst.class_name), - 'kind': 'roblox-lua', 'target': target, - 'code': '', # JS-эквивалент пока нет (заполнит Lua-runtime в плеере) - 'lua_source': source, - 'roblox_class': inst.class_name, # Script | LocalScript | ModuleScript - 'enabled': bool(props.get('Disabled', False) is False), + 'code': packed_code, }) self.stats.scripts_collected += 1 diff --git a/src/api/rbxlImporterApi.js b/src/api/rbxlImporterApi.js index ba21350..68675f2 100644 --- a/src/api/rbxlImporterApi.js +++ b/src/api/rbxlImporterApi.js @@ -9,6 +9,27 @@ import { RBXL_addres } from './API.js'; +// На dev (localhost) backend ещё не интегрирован с user-service для JWT-парсинга. +// Используем dev override: явно передаём user_id. В prod NPM или user-service +// сами выставят X-User-Id из JWT. +const IS_DEV = typeof window !== 'undefined' + && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); + +function authHeaders() { + const token = localStorage.getItem('Authorization') || ''; + const headers = { 'Authorization': token }; + if (IS_DEV) { + // Достаём user_id из JWT для dev (без validation — backend на dev доверяет). + try { + const payload = JSON.parse(atob(token.split('.')[1] || '')); + if (payload && payload.id != null) headers['X-Auth-Override'] = String(payload.id); + } catch (e) { + // если JWT не парсится — забыли залогиниться, всё равно отправим что-то + } + } + return headers; +} + /** * Анализ .rbxl. Принимает File (из ) или Blob. * Возвращает { preview_hash, report }. @@ -16,14 +37,9 @@ import { RBXL_addres } from './API.js'; export async function analyzeRbxl(file) { const fd = new FormData(); fd.append('file', file, file.name || 'upload.rbxl'); - const token = localStorage.getItem('Authorization') || ''; const resp = await fetch(`${RBXL_addres}/import/rbxl/analyze`, { method: 'POST', - headers: { - 'Authorization': token, - // X-User-Id выставит upstream (NPM → user-service после JWT) - // на dev добавим override (только в LAN): - }, + headers: authHeaders(), body: fd, }); if (!resp.ok) { @@ -37,13 +53,9 @@ export async function analyzeRbxl(file) { * Создаёт проект из preview_hash. Возвращает { project_id, redirect, assets_downloaded, assets_failed }. */ export async function createRbxlProject(previewHash, title) { - const token = localStorage.getItem('Authorization') || ''; const resp = await fetch(`${RBXL_addres}/import/rbxl/create`, { method: 'POST', - headers: { - 'Authorization': token, - 'Content-Type': 'application/json', - }, + headers: { ...authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify({ preview_hash: previewHash, title: title || '' }), }); if (!resp.ok) { diff --git a/src/community/KubikonStudio.jsx b/src/community/KubikonStudio.jsx index 8882cbf..65e4d90 100644 --- a/src/community/KubikonStudio.jsx +++ b/src/community/KubikonStudio.jsx @@ -14,6 +14,7 @@ import useDeviceType from '../hooks/useDeviceType'; import KubikonDesktopOnlyStub from './KubikonDesktopOnlyStub'; import PleeseReg from '../components/PleeseReg/PleeseReg'; import Icon from '../editor/Icon'; +import RbxlImportModal from '../components/RbxlImportModal'; function getCurrentUserId() { try { @@ -131,6 +132,7 @@ const KubikonStudio = () => { const [greetName, setGreetName] = useState(''); // Поиск по своим играм. searchOpen — раскрыт ли инпут в шапке. const [searchQuery, setSearchQuery] = useState(''); + const [rbxlImportOpen, setRbxlImportOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); // Гость МОЖЕТ просматривать студию — видит шаблоны и обучение. @@ -388,8 +390,30 @@ const KubikonStudio = () => { ВИКИ + {/* Импорт Roblox .rbxl — только для МИНа (user_id=1) */} + {getCurrentUserId() === 1 && ( + + )} + setRbxlImportOpen(false)} + currentUserId={getCurrentUserId()} + onCreated={(result) => { + // eslint-disable-next-line no-console + console.log('[rbxl-import] created:', result); + }} + /> +