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);
+ }}
+ />
+