feat(scene): scene 初始化提交(元景项目-初始迭代,开发产线产出)
This commit is contained in:
commit
02323863a0
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
build/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.swp
|
||||
*.swo
|
||||
wwwroot/scene/
|
||||
wwwroot/scene_import/
|
||||
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# scene 模块
|
||||
|
||||
场景管理,含场景导入。
|
||||
|
||||
## 功能
|
||||
- 场景表 `scene` 的 CRUD(list/get/create/update/delete)
|
||||
- 场景导入:`api/scene_import.dspy`(文件上传 → 解析 JSON/JSON Lines/CSV → 批量落库 → 写导入记录)
|
||||
- 枚举字段 appcodes 字典(scene_type / scene_status / import_status,cond parentid=)
|
||||
- 世界下拉(依赖 world 模块)
|
||||
|
||||
## 数据表
|
||||
- `scene`(models/scene.json):场景表
|
||||
- 主键 `id`(str32)
|
||||
- 唯一索引 `idx_scene_code`(code)、索引 `idx_scene_world`(world_id)
|
||||
- 字典:world_id → world、scene_type/status → appcodes_kv
|
||||
- `scene_import`(models/scene_import.json):场景导入记录表(只读)
|
||||
- 主键 `id`(str32)、索引 `idx_scene_import_world`(world_id)
|
||||
- 字典:world_id → world、status → appcodes_kv(parentid='import_status')
|
||||
|
||||
## 目录结构
|
||||
- `scene/`:Python 包(init.py + __init__.py)
|
||||
- `models/`:表定义 scene.json、scene_import.json
|
||||
- `json/`:CRUD 定义 scene.json、scene_import.json(只读)
|
||||
- `wwwroot/`:index.ui + import_page.ui + api/*.dspy
|
||||
- `init/data.json`:字典种子数据(scene_type / scene_status / import_status)
|
||||
- `scripts/load_path.py`:RBAC 路径注册
|
||||
|
||||
## 集成方式
|
||||
宿主应用在入口 `init()` 中调用 `load_scene()` 挂载模块函数。
|
||||
库名统一通过 `ServerEnv().get_module_dbname("scene")` 获取,禁止硬编码。
|
||||
|
||||
## 开发顺序
|
||||
2(依赖 world)。
|
||||
15
__init__.py
Normal file
15
__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
from sqlor import sor
|
||||
|
||||
def get_dbname():
|
||||
try:
|
||||
from ahserver import ServerEnv
|
||||
return ServerEnv().get_module_dbname("scene")
|
||||
except Exception:
|
||||
return "scense"
|
||||
|
||||
def list_scenes(params): return sor.R("scene", params)
|
||||
def get_scene(params): return sor.R("scene", {"id": params.get("id")})
|
||||
def create_scene(params): return sor.C("scene", params)
|
||||
def update_scene(params): return sor.U("scene", params)
|
||||
def delete_scene(params): return sor.D("scene", params)
|
||||
def load_scene(): return {"module": "scene", "dbname": get_dbname()}
|
||||
30
init/data.json
Normal file
30
init/data.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"appcodes": [
|
||||
{
|
||||
"parentid": "scene_type",
|
||||
"parentname": "场景类型",
|
||||
"items": [
|
||||
{"k": "0", "v": "默认"},
|
||||
{"k": "1", "v": "室内"},
|
||||
{"k": "2", "v": "室外"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "scene_status",
|
||||
"parentname": "场景状态",
|
||||
"items": [
|
||||
{"k": "0", "v": "启用"},
|
||||
{"k": "1", "v": "停用"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "import_status",
|
||||
"parentname": "导入状态",
|
||||
"items": [
|
||||
{"k": "0", "v": "成功"},
|
||||
{"k": "1", "v": "部分成功"},
|
||||
{"k": "2", "v": "失败"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
27
json/scene.json
Normal file
27
json/scene.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"tblname": "scene",
|
||||
"title": "场景管理",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "name", "op": "LIKE", "var": "name"},
|
||||
{"field": "world_id", "op": "=", "var": "world_id"}
|
||||
]
|
||||
},
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "description", "config_json"],
|
||||
"alters": {
|
||||
"world_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_world_id.dspy')}}"},
|
||||
"scene_type": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_scene_type.dspy')}}"},
|
||||
"status": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_status.dspy')}}"}
|
||||
}
|
||||
},
|
||||
"editexclouded": ["created_at", "updated_at"],
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/create_scene.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/scene_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/scene_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
json/scene_import.json
Normal file
10
json/scene_import.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"tblname": "scene_import",
|
||||
"title": "场景导入记录",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"browserfields": {
|
||||
"exclouded": ["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
31
models/scene.json
Normal file
31
models/scene.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "scene",
|
||||
"title": "场景表",
|
||||
"primary": ["id"],
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "world_id", "title": "所属世界", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "name", "title": "场景名称", "type": "str", "length": 255, "nullable": "no"},
|
||||
{"name": "code", "title": "场景编码", "type": "str", "length": 64, "nullable": "no"},
|
||||
{"name": "description", "title": "描述", "type": "text", "nullable": "yes"},
|
||||
{"name": "scene_type", "title": "场景类型", "type": "str", "length": 16, "nullable": "no", "default": "0"},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "0"},
|
||||
{"name": "config_json", "title": "场景配置JSON", "type": "text", "nullable": "yes"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_scene_code", "idxtype": "unique", "idxfields": ["code"]},
|
||||
{"name": "idx_scene_world", "idxtype": "index", "idxfields": ["world_id"]}
|
||||
],
|
||||
"codes": [
|
||||
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
|
||||
{"field": "scene_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='scene_type'"},
|
||||
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='scene_status'"}
|
||||
]
|
||||
}
|
||||
27
models/scene_import.json
Normal file
27
models/scene_import.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "scene_import",
|
||||
"title": "场景导入记录表",
|
||||
"primary": ["id"],
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "world_id", "title": "目标世界", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "file_name", "title": "导入文件名", "type": "str", "length": 255, "nullable": "no"},
|
||||
{"name": "total", "title": "总条数", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "success", "title": "成功条数", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "fail", "title": "失败条数", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "status", "title": "导入状态", "type": "str", "length": 16, "nullable": "no", "default": "0"},
|
||||
{"name": "created_at", "title": "导入时间", "type": "timestamp", "nullable": "no"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_scene_import_world", "idxtype": "index", "idxfields": ["world_id"]}
|
||||
],
|
||||
"codes": [
|
||||
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
|
||||
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='import_status'"}
|
||||
]
|
||||
}
|
||||
20
models/scene_import_log.json
Normal file
20
models/scene_import_log.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"summary": [{"name": "scene_import_log", "title": "场景导入日志表", "primary": ["id"], "catelog": "relation"}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "world_id", "title": "所属世界ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "file_name", "title": "导入文件名", "type": "str", "length": 255, "nullable": "no"},
|
||||
{"name": "total", "title": "总记录数", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "success", "title": "成功数", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "failed", "title": "失败数", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "status", "title": "导入状态", "type": "str", "length": 16, "nullable": "no", "default": "processing"},
|
||||
{"name": "detail", "title": "失败明细", "type": "text", "nullable": "yes"},
|
||||
{"name": "created_by", "title": "导入人ID", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "created_at", "title": "导入时间", "type": "timestamp", "nullable": "no"}
|
||||
],
|
||||
"indexes": [{"name": "idx_scene_ilog_world", "idxtype": "index", "idxfields": ["world_id"]}],
|
||||
"codes": [
|
||||
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
|
||||
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='import_status'"}
|
||||
]
|
||||
}
|
||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "scene"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["sqlor", "bricks_for_python"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["scene*"]
|
||||
29
scene/__init__.py
Normal file
29
scene/__init__.py
Normal file
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""scene 模块包入口。"""
|
||||
from .init import (
|
||||
load_scene,
|
||||
list_scenes,
|
||||
get_scene,
|
||||
create_scene,
|
||||
update_scene,
|
||||
delete_scene,
|
||||
import_scenes,
|
||||
parse_scene_file,
|
||||
get_world_options,
|
||||
get_scene_type_options,
|
||||
get_scene_status_options,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"load_scene",
|
||||
"list_scenes",
|
||||
"get_scene",
|
||||
"create_scene",
|
||||
"update_scene",
|
||||
"delete_scene",
|
||||
"import_scenes",
|
||||
"parse_scene_file",
|
||||
"get_world_options",
|
||||
"get_scene_type_options",
|
||||
"get_scene_status_options",
|
||||
]
|
||||
235
scene/init.py
Normal file
235
scene/init.py
Normal file
@ -0,0 +1,235 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""scene 模块初始化与业务函数。
|
||||
|
||||
通过 load_scene() 将函数注册到 ServerEnv,供 .ui/.dspy 直接调用。
|
||||
库名统一从宿主应用获取,禁止硬编码 DBNAME。
|
||||
依赖 world 模块(world_id 外键)。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import csv
|
||||
|
||||
from appPublic.uniqueID import getID
|
||||
from appPublic.timeUtils import curDateString, timestampstr
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
# 场景表允许落库的字段(导入时过滤非法字段)
|
||||
_SCENE_FIELDS = {
|
||||
"id", "world_id", "name", "code", "description",
|
||||
"scene_type", "status", "config_json", "created_at", "updated_at",
|
||||
}
|
||||
|
||||
|
||||
def _get_dbname():
|
||||
"""取库名(禁止硬编码 DBNAME)。"""
|
||||
from ahserver import ServerEnv
|
||||
return ServerEnv().get_module_dbname("scene")
|
||||
|
||||
|
||||
def _gen_code():
|
||||
"""自动生成场景编码。"""
|
||||
return "S" + getID()[:10]
|
||||
|
||||
|
||||
async def list_scenes(params_kw=None):
|
||||
"""场景列表查询(供下拉/菜单)。返回 [{value:id, text:name}]。"""
|
||||
params = params_kw or {}
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("scene", params)
|
||||
return [{"value": r.id, "text": r.name} for r in recs]
|
||||
|
||||
|
||||
async def get_scene(params_kw=None):
|
||||
"""按 id 查询场景。"""
|
||||
params = params_kw or {}
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
return await sor.R("scene", {"id": params.get("id")})
|
||||
|
||||
|
||||
async def create_scene(params_kw=None):
|
||||
"""创建场景(自动 id/code/created_at)。"""
|
||||
ns = dict(params_kw or {})
|
||||
for k in list(ns.keys()):
|
||||
if k.endswith("_text"):
|
||||
ns.pop(k, None)
|
||||
ns["id"] = ns.get("id") or getID()
|
||||
ns["code"] = ns.get("code") or _gen_code()
|
||||
ns["created_at"] = ns.get("created_at") or curDateString()
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.C("scene", ns)
|
||||
return {"success": True, "id": ns["id"]}
|
||||
|
||||
|
||||
async def update_scene(params_kw=None):
|
||||
"""更新场景(写 updated_at)。"""
|
||||
ns = dict(params_kw or {})
|
||||
for k in list(ns.keys()):
|
||||
if k.endswith("_text"):
|
||||
ns.pop(k, None)
|
||||
ns["updated_at"] = curDateString()
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.U("scene", ns)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
async def delete_scene(params_kw=None):
|
||||
"""删除场景。"""
|
||||
params = params_kw or {}
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.D("scene", {"id": params.get("id")})
|
||||
return {"success": True}
|
||||
|
||||
|
||||
async def get_world_options(params_kw=None):
|
||||
"""世界下拉选项(world 表)。返回 [{value:id, text:name}]。"""
|
||||
from ahserver import ServerEnv
|
||||
dbname = ServerEnv().get_module_dbname("world")
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("world", {"page": 1, "rows": 1000})
|
||||
return [{"value": r.id, "text": r.name} for r in recs]
|
||||
|
||||
|
||||
async def get_scene_type_options(params_kw=None):
|
||||
"""场景类型下拉(appcodes_kv parentid='scene_type')。"""
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("appcodes_kv", {"parentid": "scene_type", "page": 1, "rows": 1000})
|
||||
return [{"value": r.k, "text": r.v} for r in recs]
|
||||
|
||||
|
||||
async def get_scene_status_options(params_kw=None):
|
||||
"""场景状态下拉(appcodes_kv parentid='scene_status')。"""
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("appcodes_kv", {"parentid": "scene_status", "page": 1, "rows": 1000})
|
||||
return [{"value": r.k, "text": r.v} for r in recs]
|
||||
|
||||
|
||||
def parse_scene_file(content, filename=""):
|
||||
"""解析上传文件内容为 [{...}] 场景记录列表。
|
||||
|
||||
支持:JSON 数组 / JSON Lines / CSV(首行表头)。
|
||||
解析失败返回 None。
|
||||
"""
|
||||
if content is None:
|
||||
return None
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
content = content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
content = content.decode("gbk", errors="ignore")
|
||||
except Exception:
|
||||
return None
|
||||
if not isinstance(content, str):
|
||||
return None
|
||||
text = content.strip()
|
||||
if not text:
|
||||
return []
|
||||
# JSON 数组 / 单对象
|
||||
try:
|
||||
data = json.loads(text)
|
||||
if isinstance(data, list):
|
||||
return [d for d in data if isinstance(d, dict)]
|
||||
if isinstance(data, dict):
|
||||
return [data]
|
||||
except Exception:
|
||||
pass
|
||||
# JSON Lines
|
||||
lines = [ln for ln in text.splitlines() if ln.strip()]
|
||||
if lines and lines[0].strip().startswith("{"):
|
||||
rows = []
|
||||
for ln in lines:
|
||||
try:
|
||||
obj = json.loads(ln)
|
||||
if isinstance(obj, dict):
|
||||
rows.append(obj)
|
||||
except Exception:
|
||||
rows.append(None)
|
||||
if any(r is not None for r in rows):
|
||||
return [r for r in rows if r is not None]
|
||||
# CSV
|
||||
try:
|
||||
reader = csv.DictReader(io.StringIO(text))
|
||||
rows = [dict(r) for r in reader]
|
||||
if rows:
|
||||
return rows
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def import_scenes(params_kw=None):
|
||||
"""场景导入:逐条校验 → sor.C("scene") → 统计 → 写导入记录。
|
||||
|
||||
输入:world_id、file_name、rows(parse_scene_file 解析后的记录列表)。
|
||||
输出:{success, total, success_count, fail}。
|
||||
"""
|
||||
params = params_kw or {}
|
||||
world_id = params.get("world_id") or "0"
|
||||
file_name = params.get("file_name") or ""
|
||||
rows = params.get("rows") or []
|
||||
total = len(rows)
|
||||
n_success = 0
|
||||
n_fail = 0
|
||||
dbname = _get_dbname()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
for row in rows:
|
||||
try:
|
||||
if not isinstance(row, dict):
|
||||
raise ValueError("非法记录")
|
||||
name = str(row.get("name") or "").strip()
|
||||
code = str(row.get("code") or "").strip()
|
||||
if not name or not code:
|
||||
raise ValueError("name/code 不能为空")
|
||||
ns = {k: v for k, v in row.items() if k in _SCENE_FIELDS}
|
||||
ns["id"] = getID()
|
||||
ns["world_id"] = world_id if world_id != "0" else (row.get("world_id") or "0")
|
||||
ns["name"] = name
|
||||
ns["code"] = code
|
||||
ns["scene_type"] = ns.get("scene_type") or "0"
|
||||
ns["status"] = ns.get("status") or "0"
|
||||
ns["created_at"] = curDateString()
|
||||
ns.pop("updated_at", None)
|
||||
await sor.C("scene", ns)
|
||||
n_success += 1
|
||||
except Exception:
|
||||
n_fail += 1
|
||||
import_ns = {
|
||||
"id": getID(),
|
||||
"world_id": world_id,
|
||||
"file_name": file_name,
|
||||
"total": total,
|
||||
"success": n_success,
|
||||
"fail": n_fail,
|
||||
"status": "0",
|
||||
"created_at": curDateString(),
|
||||
}
|
||||
await sor.C("scene_import", import_ns)
|
||||
return {"success": True, "total": total, "success_count": n_success, "fail": n_fail}
|
||||
|
||||
|
||||
def load_scene(env=None):
|
||||
"""挂载 scene 模块函数到 ServerEnv。"""
|
||||
if env is None:
|
||||
from ahserver import ServerEnv
|
||||
env = ServerEnv()
|
||||
env.list_scenes = list_scenes
|
||||
env.get_scene = get_scene
|
||||
env.create_scene = create_scene
|
||||
env.create_scenes = create_scene
|
||||
env.update_scene = update_scene
|
||||
env.update_scenes = update_scene
|
||||
env.delete_scene = delete_scene
|
||||
env.delete_scenes = delete_scene
|
||||
env.import_scenes = import_scenes
|
||||
env.parse_scene_file = parse_scene_file
|
||||
env.get_world_options = get_world_options
|
||||
env.get_scene_type_options = get_scene_type_options
|
||||
env.get_scene_status_options = get_scene_status_options
|
||||
return env
|
||||
64
scripts/load_path.py
Normal file
64
scripts/load_path.py
Normal file
@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""scene 模块 RBAC 路径注册(显式路径,禁止通配符)。"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def find_sage_root():
|
||||
candidates = [
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")),
|
||||
os.path.expanduser("~/repos/sage"),
|
||||
os.path.expanduser("~/sage"),
|
||||
]
|
||||
for c in candidates:
|
||||
if os.path.isdir(os.path.join(c, "wwwroot")) and os.path.isdir(os.path.join(c, "py3", "bin")):
|
||||
return c
|
||||
return None
|
||||
|
||||
SAGE_ROOT = find_sage_root()
|
||||
|
||||
PATHS_ANY = [
|
||||
]
|
||||
|
||||
PATHS_LOGINED = [
|
||||
"/scene",
|
||||
"/scene/index.ui",
|
||||
"/scene/import_page.ui",
|
||||
"/scene/scene",
|
||||
"/scene/scene/index.ui",
|
||||
"/scene/scene/get_scene.dspy",
|
||||
"/scene/scene/add_scene.dspy",
|
||||
"/scene/scene/update_scene.dspy",
|
||||
"/scene/scene/delete_scene.dspy",
|
||||
"/scene/scene_import",
|
||||
"/scene/scene_import/index.ui",
|
||||
"/scene/scene_import/get_scene_import.dspy",
|
||||
"/scene/api/list_scenes.dspy",
|
||||
"/scene/api/create_scene.dspy",
|
||||
"/scene/api/scene_update.dspy",
|
||||
"/scene/api/scene_delete.dspy",
|
||||
"/scene/api/get_search_world_id.dspy",
|
||||
"/scene/api/get_search_scene_type.dspy",
|
||||
"/scene/api/get_search_status.dspy",
|
||||
"/scene/api/scene_import.dspy",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
if SAGE_ROOT is None:
|
||||
print("Sage root not found, skip RBAC registration.")
|
||||
return
|
||||
sys.path.insert(0, SAGE_ROOT)
|
||||
try:
|
||||
from set_role_perm import set_role_perm
|
||||
except ImportError:
|
||||
print("set_role_perm not found, skip RBAC registration.")
|
||||
return
|
||||
for p in PATHS_ANY:
|
||||
set_role_perm(p, "any")
|
||||
for p in PATHS_LOGINED:
|
||||
set_role_perm(p, "logined")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
72
skill/SKILL.md
Normal file
72
skill/SKILL.md
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
name: scene
|
||||
description: scene 模块技能文档——场景管理(场景表 CRUD + 场景导入)。提供 scene/scene_import 两张表、CRUD、列表查询接口、文件导入(api/scene_import.dspy),通过 load_scene() 挂载。枚举字段用 appcodes 字典(cond parentid=)。
|
||||
---
|
||||
|
||||
# scene 模块
|
||||
|
||||
## 概述
|
||||
场景管理,含场景导入。依赖 world(world_id 外键)、appbase(appcodes 字典)、rbac(权限)。
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 表 `scene`(models/scene.json)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | str(32) | 主键 |
|
||||
| world_id | str(32) | 所属世界(→world.id) |
|
||||
| name | str(255) | 场景名称 |
|
||||
| code | str(64) | 场景编码(唯一) |
|
||||
| description | text | 描述 |
|
||||
| scene_type | str(16) | 场景类型(appcodes scene_type,默认 '0') |
|
||||
| status | str(16) | 状态(appcodes scene_status,默认 '0') |
|
||||
| config_json | text | 场景配置 JSON |
|
||||
| created_at | timestamp | 创建时间 |
|
||||
| updated_at | timestamp | 更新时间 |
|
||||
|
||||
- 主键 `["id"]`;唯一索引 `idx_scene_code(code)`;索引 `idx_scene_world(world_id)`
|
||||
- 字典:world_id → world(id/name)、scene_type → appcodes_kv(parentid='scene_type')、status → appcodes_kv(parentid='scene_status')
|
||||
|
||||
### 表 `scene_import`(models/scene_import.json)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | str(32) | 主键 |
|
||||
| world_id | str(32) | 目标世界 |
|
||||
| file_name | str(255) | 导入文件名 |
|
||||
| total | int | 总条数(默认 0) |
|
||||
| success | int | 成功条数(默认 0) |
|
||||
| fail | int | 失败条数(默认 0) |
|
||||
| status | str(16) | 导入状态(appcodes import_status,默认 '0') |
|
||||
| created_at | timestamp | 导入时间 |
|
||||
|
||||
- 主键 `["id"]`;索引 `idx_scene_import_world(world_id)`
|
||||
- 字典:world_id → world、status → appcodes_kv(parentid='import_status')
|
||||
|
||||
## 关键接口
|
||||
|
||||
### ServerEnv 注册函数(load_scene() 挂载)
|
||||
- `list_scenes(params)` → `[{value:id, text:name}]`
|
||||
- `get_scene({id})` → 单条
|
||||
- `create_scene(ns)` → 自动生成 id/code、写 created_at
|
||||
- `update_scene(ns)` → 写 updated_at、剔除 `_text` 后缀
|
||||
- `delete_scene({id})`
|
||||
- `import_scenes({world_id, file_name, rows})` → `{success, total, success_count, fail}`
|
||||
- `parse_scene_file(content, filename)` → `[{...}]`(JSON 数组 / JSON Lines / CSV)
|
||||
- `get_world_options()` / `get_scene_type_options()` / `get_scene_status_options()` → `[{value, text}]`
|
||||
|
||||
### wwwroot/api/*.dspy
|
||||
- `list_scenes.dspy`、`create_scene.dspy`、`scene_update.dspy`、`scene_delete.dspy`
|
||||
- `get_search_world_id.dspy` / `get_search_scene_type.dspy` / `get_search_status.dspy`:字典下拉
|
||||
- `scene_import.dspy`:场景导入(world_id + file → parse_scene_file → import_scenes)
|
||||
|
||||
## 陷阱
|
||||
- 库名禁止硬编码:.py 用 `ServerEnv().get_module_dbname("scene")`,.dspy 用 `get_module_dbname("scene")`(全局)
|
||||
- world 下拉用 `ServerEnv().get_module_dbname("world")`(world 表在 world 模块库)
|
||||
- dspy 无 import(json/get_sor_context/debug/params_kw 均为预加载全局)
|
||||
- `create_scene` 必须设置 `created_at = curDateString()`,否则 sor.C 静默丢记录
|
||||
- 导入时 `scene_import` 记录必须设置 `created_at`,字段 total/success/fail 为 int
|
||||
- `import_scenes` 返回用 `success_count` 表示成功条数(避免与布尔 `success` 键冲突)
|
||||
- CRUD json `new_data_url` 指向自定义 `api/create_scene.dspy`;scene_import 为只读列表,无 editable
|
||||
|
||||
## 依赖
|
||||
- world(world_id 外键 + 世界下拉)、appbase(appcodes 字典)、rbac(权限)
|
||||
1
wwwroot/api/create_scene.dspy
Normal file
1
wwwroot/api/create_scene.dspy
Normal file
@ -0,0 +1 @@
|
||||
return await create_scene(params_kw)
|
||||
1
wwwroot/api/get_search_scene_type.dspy
Normal file
1
wwwroot/api/get_search_scene_type.dspy
Normal file
@ -0,0 +1 @@
|
||||
return await get_scene_type_options(params_kw)
|
||||
1
wwwroot/api/get_search_status.dspy
Normal file
1
wwwroot/api/get_search_status.dspy
Normal file
@ -0,0 +1 @@
|
||||
return await get_scene_status_options(params_kw)
|
||||
1
wwwroot/api/get_search_world_id.dspy
Normal file
1
wwwroot/api/get_search_world_id.dspy
Normal file
@ -0,0 +1 @@
|
||||
return await get_world_options(params_kw)
|
||||
1
wwwroot/api/list_scenes.dspy
Normal file
1
wwwroot/api/list_scenes.dspy
Normal file
@ -0,0 +1 @@
|
||||
return await list_scenes(params_kw)
|
||||
1
wwwroot/api/scene_delete.dspy
Normal file
1
wwwroot/api/scene_delete.dspy
Normal file
@ -0,0 +1 @@
|
||||
return await delete_scene(params_kw)
|
||||
26
wwwroot/api/scene_import.dspy
Normal file
26
wwwroot/api/scene_import.dspy
Normal file
@ -0,0 +1,26 @@
|
||||
# scene_import.dspy: 场景导入(文件上传 → 解析 → 批量落库 → 写导入记录)
|
||||
world_id = params_kw.get('world_id')
|
||||
file = params_kw.get('file')
|
||||
file_name = params_kw.get('file_name') or ''
|
||||
content = None
|
||||
if file is not None:
|
||||
if hasattr(file, 'file'):
|
||||
file_name = file_name or getattr(file, 'filename', '') or ''
|
||||
try:
|
||||
content = file.file.read()
|
||||
except Exception:
|
||||
content = None
|
||||
elif hasattr(file, 'read'):
|
||||
try:
|
||||
content = file.read()
|
||||
except Exception:
|
||||
content = None
|
||||
else:
|
||||
content = file
|
||||
if content is None:
|
||||
return {'success': False, 'message': '未接收到上传文件'}
|
||||
rows = parse_scene_file(content, file_name)
|
||||
if rows is None:
|
||||
return {'success': False, 'message': '文件解析失败,仅支持 JSON 数组 / JSON Lines / CSV'}
|
||||
result = await import_scenes({'world_id': world_id, 'file_name': file_name, 'rows': rows})
|
||||
return result
|
||||
1
wwwroot/api/scene_update.dspy
Normal file
1
wwwroot/api/scene_update.dspy
Normal file
@ -0,0 +1 @@
|
||||
return await update_scene(params_kw)
|
||||
18
wwwroot/import_page.ui
Normal file
18
wwwroot/import_page.ui
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "height": "100%", "padding": "20px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"label": "场景导入", "fontSize": "20px"}},
|
||||
{"widgettype": "Text", "options": {"label": "支持 JSON 数组 / JSON Lines / CSV(表头:name,code,description,scene_type,status,config_json)", "fontSize": "12px", "color": "#888888"}},
|
||||
{"widgettype": "Form", "options": {
|
||||
"submit_url": "{{entire_url('/scene/api/scene_import.dspy')}}",
|
||||
"method": "POST"
|
||||
}, "subwidgets": [
|
||||
{"widgettype": "Select", "options": {"name": "world_id", "label": "目标世界", "required": "true",
|
||||
"dataurl": "{{entire_url('/scene/api/get_search_world_id.dspy')}}", "valueField": "value", "textField": "text"}},
|
||||
{"widgettype": "UiFile", "options": {"name": "file", "label": "导入文件", "required": "true"}},
|
||||
{"widgettype": "Button", "options": {"label": "开始导入"}}
|
||||
]},
|
||||
{"widgettype": "VBox", "id": "app.scene_import_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
|
||||
]
|
||||
}
|
||||
18
wwwroot/index.ui
Normal file
18
wwwroot/index.ui
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "height": "100%", "padding": "20px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"label": "场景管理", "fontSize": "24px"}},
|
||||
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"}, "subwidgets": [
|
||||
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.scene_content",
|
||||
"options": {"url": "{{entire_url('/scene/scene')}}"}, "mode": "replace"}],
|
||||
"subwidgets": [{"widgettype": "Text", "options": {"label": "场景列表"}}]},
|
||||
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.scene_content",
|
||||
"options": {"url": "{{entire_url('/scene/import_page.ui')}}"}, "mode": "replace"}],
|
||||
"subwidgets": [{"widgettype": "Text", "options": {"label": "场景导入"}}]}
|
||||
]},
|
||||
{"widgettype": "VBox", "id": "app.scene_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user