feat(scense): scense 初始化提交(元景项目-初始迭代,开发产线产出)

This commit is contained in:
agent.develop 2026-08-28 13:34:54 +08:00
commit 93dadab835
6 changed files with 288 additions and 0 deletions

9
.env Normal file
View File

@ -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

57
app/scense.py Normal file
View File

@ -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)

114
build.sh Normal file
View File

@ -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}"

41
conf/config.json Normal file
View File

@ -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"
}

8
scripts/ddl.sql Normal file
View File

@ -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='世界同步';

59
wwwroot/index.ui Normal file
View File

@ -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": "世界 / 场景 / 实体 / 脚本 / 快照 / 同步"}
}
]
}
]
}
]
}
]
}