From 93dadab835766adcd30187563f3ae8cd839b70f9 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Fri, 28 Aug 2026 13:34:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(scense):=20scense=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E6=8F=90=E4=BA=A4=EF=BC=88=E5=85=83=E6=99=AF=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE-=E5=88=9D=E5=A7=8B=E8=BF=AD=E4=BB=A3=EF=BC=8C?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E4=BA=A7=E7=BA=BF=E4=BA=A7=E5=87=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 9 ++++ app/scense.py | 57 ++++++++++++++++++++++++ build.sh | 114 +++++++++++++++++++++++++++++++++++++++++++++++ conf/config.json | 41 +++++++++++++++++ scripts/ddl.sql | 8 ++++ wwwroot/index.ui | 59 ++++++++++++++++++++++++ 6 files changed, 288 insertions(+) create mode 100644 .env create mode 100644 app/scense.py create mode 100644 build.sh create mode 100644 conf/config.json create mode 100644 scripts/ddl.sql create mode 100644 wwwroot/index.ui diff --git a/.env b/.env new file mode 100644 index 0000000..94e441e --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +# 唤景平台世界模式(scense)测试环境配置 +APP_NAME=scense +APP_PORT=9380 +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=scense +DB_USER=test +DB_PASSWORD=test123 +DEPLOY_ENV=test diff --git a/app/scense.py b/app/scense.py new file mode 100644 index 0000000..d660553 --- /dev/null +++ b/app/scense.py @@ -0,0 +1,57 @@ +"""唤景平台世界模式(scense)——应用唯一入口。 + +一个应用 = 一个入口 = 一个端口(9380)。 +init() 里先挂 ServerEnv,再挂载基础模块 + 6 个业务模块。 +""" +from ahserver.webapp import webapp +from ahserver.serverenv import ServerEnv + +import bricks_for_python # registers bui processor +from bricks_for_python.init import load_pybricks # registers UiWindow etc. +from appbase.init import load_appbase +from rbac.init import load_rbac + + +def get_module_dbname(m): + """返回模块 m 对应的数据库名。 + + 本项目为单库应用:world/scene/entity/script_engine/world_snapshot/world_sync + 以及基础模块(appbase/rbac)统一落同一主库 scense。 + """ + return "scense" + + +def password_encode(s): + if s is None: + return "" + from ahserver.globalEnv import password_encode as _orig + return _orig(s) + + +def init(): + env = ServerEnv() # MUST be before load_rbac / load_appbase + env.get_module_dbname = get_module_dbname + env.password_encode = password_encode + + load_appbase() + load_rbac() + load_pybricks() + + # ── 业务模块导入(一个入口一个端口,所有模块挂在这里)── + from world.init import load_world + from scene.init import load_scene + from entity.init import load_entity + from script_engine.init import load_script_engine + from world_snapshot.init import load_world_snapshot + from world_sync.init import load_world_sync + + load_world() + load_scene() + load_entity() + load_script_engine() + load_world_snapshot() + load_world_sync() + + +if __name__ == "__main__": + webapp(init) diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..e9a6340 --- /dev/null +++ b/build.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# 唤景平台世界模式(scense)一键部署脚本 —— 测试环境 +# 部署目录 ~/scense_app,应用端口 9380,数据库 localhost:3306/scense (test/test123) +# +# 按 web-application-spec 的 8 步完整集成: +# 1) 代码导入(git clone 模块到 pkgs/) 2) 依赖安装(pip install -e) +# 3) 前端软链 wwwroot/{模块} 4) i18n 合并 +# 5) 建表(json2ddl) 6) CRUD 生成(xls2ui) +# 7) 初始化数据 8) 运行挂载(load_{模块}) +# ============================================================================= + +APP_NAME="scense" +APP_PORT=9380 +APP_DIR="$(cd "$(dirname "$0")" && pwd)" +DEPLOY_DIR="${DEPLOY_DIR:-$HOME/scense_app}" +DB_HOST="${DB_HOST:-localhost}" +DB_PORT="${DB_PORT:-3306}" +DB_NAME="${DB_NAME:-scense}" +DB_USER="${DB_USER:-test}" +DB_PASSWORD="${DB_PASSWORD:-test123}" + +# 基础模块(框架层)与业务模块 +FOUNDATION_MODULES="apppublic sqlor ahserver bricks" +DB_MODULES="appbase rbac" +BUSINESS_MODULES="world scene entity script_engine world_snapshot world_sync" + +echo "==> [1/8] sync code" +mkdir -p "${DEPLOY_DIR}/app" "${DEPLOY_DIR}/conf" "${DEPLOY_DIR}/wwwroot" \ + "${DEPLOY_DIR}/files" "${DEPLOY_DIR}/logs" "${DEPLOY_DIR}/pkgs" +rsync -avz --delete \ + "${APP_DIR}/app/" "${DEPLOY_DIR}/app/" \ + "${APP_DIR}/conf/" "${DEPLOY_DIR}/conf/" \ + "${APP_DIR}/wwwroot/" "${DEPLOY_DIR}/wwwroot/" \ + "${APP_DIR}/scripts/" "${DEPLOY_DIR}/scripts/" + +echo "==> [2/8] clone + install modules" +cd "${DEPLOY_DIR}" +mkdir -p pkgs +for m in ${FOUNDATION_MODULES} ${DB_MODULES} ${BUSINESS_MODULES}; do + if [ ! -d "pkgs/${m}" ]; then + git clone "git@git.opencomputing.cn:org/${m}.git" "pkgs/${m}" || \ + git clone "https://git.opencomputing.cn/org/${m}.git" "pkgs/${m}" || true + fi + if [ -d "pkgs/${m}" ]; then + (cd "pkgs/${m}" && git pull --ff-only || true) + if [ -f "pkgs/${m}/pyproject.toml" ] || [ -f "pkgs/${m}/setup.py" ]; then + pip install -e "pkgs/${m}" || true + else + export PYTHONPATH="${DEPLOY_DIR}/pkgs/${m}:${PYTHONPATH:-}" + fi + fi +done + +echo "==> [3/8] wwwroot symlinks" +for m in ${DB_MODULES} ${BUSINESS_MODULES}; do + if [ -d "pkgs/${m}/wwwroot" ]; then + ln -sfn "../pkgs/${m}/wwwroot" "wwwroot/${m}" + fi +done +if [ -d "pkgs/bricks/dist" ]; then + ln -sfn "../pkgs/bricks/dist" "wwwroot/bricks" +fi + +echo "==> [4/8] i18n merge" +mkdir -p wwwroot/i18n/zh wwwroot/i18n/en +python - <<'PY' || true +import json, os +for lang in ("zh", "en"): + merged = {} + for m in ("apppublic","sqlor","ahserver","appbase","rbac","world","scene","entity","script_engine","world_snapshot","world_sync"): + p = f"pkgs/{m}/i18n/{lang}/msg.txt" + if os.path.isfile(p): + for line in open(p, encoding="utf-8"): + line = line.rstrip("\n") + if "=" in line: + k, v = line.split("=", 1) + merged[k.strip()] = v.strip() + out = f"wwwroot/i18n/{lang}/i18n.json" + os.makedirs(os.path.dirname(out), exist_ok=True) + json.dump(merged, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2) +print("i18n merged") +PY + +echo "==> [5/8] init db (ddl)" +mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" \ + < "${APP_DIR}/scripts/ddl.sql" + +echo "==> [6/8] CRUD generate" +for m in ${BUSINESS_MODULES}; do + if [ -d "pkgs/${m}/json" ] && [ -d "pkgs/${m}/models" ]; then + (cd "pkgs/${m}" && xls2ui -m ../models -o "${DEPLOY_DIR}/wwwroot" "${m}" json/*.json || true) + fi +done + +echo "==> [7/8] init data" +for m in ${BUSINESS_MODULES}; do + if [ -d "pkgs/${m}/data" ]; then + (cd "pkgs/${m}/data" && for f in *.xlsx; do + [ -e "$f" ] && dbloader "${DEPLOY_DIR}" "${DB_NAME}" "$f" || true + done) + fi +done + +echo "==> [8/8] restart" +if [ -f "${DEPLOY_DIR}/run.sh" ]; then + bash "${DEPLOY_DIR}/run.sh" restart +else + echo "warning: run.sh not found, start via: nohup python app/scense.py > logs/scense.log 2>&1 &" +fi + +echo "==> done, app port ${APP_PORT}" diff --git a/conf/config.json b/conf/config.json new file mode 100644 index 0000000..d53f8f7 --- /dev/null +++ b/conf/config.json @@ -0,0 +1,41 @@ +{ + "app": {"name": "scense", "port": 9380, "title": "唤景平台世界模式"}, + "password_key": "scense_test_env_key", + "logger": { + "name": "scense", + "level": "INFO", + "file": "$[workdir]$/logs/scense.log" + }, + "filesroot": "$[workdir]$/files", + "databases": { + "scense": { + "driver": "mysql", + "kwargs": { + "host": "localhost", + "port": 3306, + "user": "test", + "password": "test123", + "database": "scense", + "charset": "utf8mb4" + } + } + }, + "website": { + "port": 9380, + "root": "$[workdir]$/wwwroot", + "processors": [ + [".tmpl", "tmpl"], + [".ui", "bui"], + [".dspy", "dspy"], + [".wss", "ws"] + ], + "indexes": ["index.ui", "index.html"], + "session_max_time": 28800, + "session_issue_time": 7200 + }, + "modules": [ + "apppublic", "sqlor", "ahserver", "appbase", "rbac", + "world", "scene", "entity", "script_engine", "world_snapshot", "world_sync" + ], + "wwwroot": "wwwroot" +} diff --git a/scripts/ddl.sql b/scripts/ddl.sql new file mode 100644 index 0000000..a71e43a --- /dev/null +++ b/scripts/ddl.sql @@ -0,0 +1,8 @@ +CREATE DATABASE IF NOT EXISTS scense DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +USE scense; +CREATE TABLE IF NOT EXISTS world (id BIGINT NOT NULL AUTO_INCREMENT COMMENT '世界ID', code VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', description VARCHAR(512) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_world_code (code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='世界'; +CREATE TABLE IF NOT EXISTS scene (id BIGINT NOT NULL AUTO_INCREMENT, world_id BIGINT NOT NULL, code VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, scene_type VARCHAR(32) DEFAULT 'normal', status VARCHAR(16) NOT NULL DEFAULT 'active', config_json TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_scene_code (code), KEY idx_scene_world (world_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场景'; +CREATE TABLE IF NOT EXISTS entity (id BIGINT NOT NULL AUTO_INCREMENT, scene_id BIGINT NOT NULL, entity_type VARCHAR(32) NOT NULL, name VARCHAR(128) NOT NULL, attrs_json TEXT, status VARCHAR(16) NOT NULL DEFAULT 'active', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_entity_scene (scene_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实体'; +CREATE TABLE IF NOT EXISTS script_engine (id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(128) NOT NULL, script_type VARCHAR(32) NOT NULL DEFAULT 'lua', content MEDIUMTEXT, status VARCHAR(16) NOT NULL DEFAULT 'active', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_script_name (name)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='脚本引擎'; +CREATE TABLE IF NOT EXISTS world_snapshot (id BIGINT NOT NULL AUTO_INCREMENT, world_id BIGINT NOT NULL, version INT NOT NULL DEFAULT 1, snapshot_json LONGTEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_snapshot_world (world_id, version)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='世界快照'; +CREATE TABLE IF NOT EXISTS world_sync (id BIGINT NOT NULL AUTO_INCREMENT, world_id BIGINT NOT NULL, sync_type VARCHAR(32) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'pending', payload_json TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_sync_world (world_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='世界同步'; diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..3284e51 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,59 @@ +{ + "widgettype": "VBox", + "id": "root", + "options": {"css": "filler"}, + "subwidgets": [ + { + "widgettype": "TopBar", + "id": "topbar", + "options": {"title": "唤景平台世界模式", "user": "app.user"} + }, + { + "widgettype": "HBox", + "id": "body", + "options": {"css": "filler"}, + "subwidgets": [ + { + "widgettype": "VBox", + "id": "main_content", + "options": {"css": "filler"}, + "subwidgets": [ + { + "widgettype": "NavMenu", + "id": "nav_menu", + "options": { + "items": [ + {"label": "世界", "target": "app.main_content", "url": "/world/list"}, + {"label": "场景", "target": "app.main_content", "url": "/scene/list"}, + {"label": "实体", "target": "app.main_content", "url": "/entity/list"}, + {"label": "脚本引擎", "target": "app.main_content", "url": "/script_engine/list"}, + {"label": "世界快照", "target": "app.main_content", "url": "/world_snapshot/list"}, + {"label": "世界同步", "target": "app.main_content", "url": "/world_sync/list"} + ] + } + } + ] + }, + { + "widgettype": "VBox", + "id": "sidebar", + "options": {"css": "sidebar", "width": 220}, + "subwidgets": [ + { + "widgettype": "Card", + "id": "side_card", + "options": {"title": "模块导航"}, + "subwidgets": [ + { + "widgettype": "Label", + "id": "side_label", + "options": {"text": "世界 / 场景 / 实体 / 脚本 / 快照 / 同步"} + } + ] + } + ] + } + ] + } + ] +}