commit 02323863a0619f6b00119dd655458582caf23a0d Author: agent.develop Date: Fri Aug 28 13:34:47 2026 +0800 feat(scene): scene 初始化提交(元景项目-初始迭代,开发产线产出) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8da8c1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +build/ +*.egg-info/ +__pycache__/ +*.pyc +*.swp +*.swo +wwwroot/scene/ +wwwroot/scene_import/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c6cb25 --- /dev/null +++ b/README.md @@ -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)。 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..82f254c --- /dev/null +++ b/__init__.py @@ -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()} diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..332d49d --- /dev/null +++ b/init/data.json @@ -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": "失败"} + ] + } + ] +} diff --git a/json/scene.json b/json/scene.json new file mode 100644 index 0000000..d49031f --- /dev/null +++ b/json/scene.json @@ -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')}}" + } + } +} diff --git a/json/scene_import.json b/json/scene_import.json new file mode 100644 index 0000000..e4d52c9 --- /dev/null +++ b/json/scene_import.json @@ -0,0 +1,10 @@ +{ + "tblname": "scene_import", + "title": "场景导入记录", + "params": { + "sortby": ["created_at desc"], + "browserfields": { + "exclouded": ["id"] + } + } +} diff --git a/models/scene.json b/models/scene.json new file mode 100644 index 0000000..ace4c72 --- /dev/null +++ b/models/scene.json @@ -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'"} + ] +} diff --git a/models/scene_import.json b/models/scene_import.json new file mode 100644 index 0000000..8aa50c0 --- /dev/null +++ b/models/scene_import.json @@ -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'"} + ] +} diff --git a/models/scene_import_log.json b/models/scene_import_log.json new file mode 100644 index 0000000..38468c7 --- /dev/null +++ b/models/scene_import_log.json @@ -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'"} + ] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c5b46e2 --- /dev/null +++ b/pyproject.toml @@ -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*"] diff --git a/scene/__init__.py b/scene/__init__.py new file mode 100644 index 0000000..a1975ba --- /dev/null +++ b/scene/__init__.py @@ -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", +] diff --git a/scene/init.py b/scene/init.py new file mode 100644 index 0000000..0a648a6 --- /dev/null +++ b/scene/init.py @@ -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 diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..0b9d0d2 --- /dev/null +++ b/scripts/load_path.py @@ -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() diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..6858824 --- /dev/null +++ b/skill/SKILL.md @@ -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(权限) diff --git a/wwwroot/api/create_scene.dspy b/wwwroot/api/create_scene.dspy new file mode 100644 index 0000000..1e18dbc --- /dev/null +++ b/wwwroot/api/create_scene.dspy @@ -0,0 +1 @@ +return await create_scene(params_kw) diff --git a/wwwroot/api/get_search_scene_type.dspy b/wwwroot/api/get_search_scene_type.dspy new file mode 100644 index 0000000..1500d8d --- /dev/null +++ b/wwwroot/api/get_search_scene_type.dspy @@ -0,0 +1 @@ +return await get_scene_type_options(params_kw) diff --git a/wwwroot/api/get_search_status.dspy b/wwwroot/api/get_search_status.dspy new file mode 100644 index 0000000..b7ab3e2 --- /dev/null +++ b/wwwroot/api/get_search_status.dspy @@ -0,0 +1 @@ +return await get_scene_status_options(params_kw) diff --git a/wwwroot/api/get_search_world_id.dspy b/wwwroot/api/get_search_world_id.dspy new file mode 100644 index 0000000..863387d --- /dev/null +++ b/wwwroot/api/get_search_world_id.dspy @@ -0,0 +1 @@ +return await get_world_options(params_kw) diff --git a/wwwroot/api/list_scenes.dspy b/wwwroot/api/list_scenes.dspy new file mode 100644 index 0000000..d4e4d62 --- /dev/null +++ b/wwwroot/api/list_scenes.dspy @@ -0,0 +1 @@ +return await list_scenes(params_kw) diff --git a/wwwroot/api/scene_delete.dspy b/wwwroot/api/scene_delete.dspy new file mode 100644 index 0000000..95986a3 --- /dev/null +++ b/wwwroot/api/scene_delete.dspy @@ -0,0 +1 @@ +return await delete_scene(params_kw) diff --git a/wwwroot/api/scene_import.dspy b/wwwroot/api/scene_import.dspy new file mode 100644 index 0000000..d987746 --- /dev/null +++ b/wwwroot/api/scene_import.dspy @@ -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 diff --git a/wwwroot/api/scene_update.dspy b/wwwroot/api/scene_update.dspy new file mode 100644 index 0000000..77f7cce --- /dev/null +++ b/wwwroot/api/scene_update.dspy @@ -0,0 +1 @@ +return await update_scene(params_kw) diff --git a/wwwroot/import_page.ui b/wwwroot/import_page.ui new file mode 100644 index 0000000..3cb2abf --- /dev/null +++ b/wwwroot/import_page.ui @@ -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"}} + ] +} diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..4c2d18e --- /dev/null +++ b/wwwroot/index.ui @@ -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"}} + ] +}