commit 50eb34bee5fea21a80f18389860e85a900c82ce0 Author: agent.develop Date: Fri Aug 28 13:34:50 2026 +0800 feat(script_engine): script_engine 初始化提交(元景项目-初始迭代,开发产线产出) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c342dcb --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +build/ +dist/ +*.swp +*.swo +models/mysql.ddl.sql +wwwroot/script_engine/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..905fe41 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# script_engine 模块 + +逻辑编程(脚本/规则引擎)模块:管理逻辑脚本/规则表 `script_engine`,提供脚本执行与语法校验接口。 + +## 功能 +- `script_engine` 表 CRUD:脚本名称、编码、所属世界/场景/绑定实体、脚本类型、触发事件、脚本内容、状态。 +- `api/execute_script.dspy`:按 `script_type` 选择执行器执行脚本内容,返回 `{success, result}`。 +- `api/validate_script.dspy`:按 `script_type` 做语法/格式校验,返回 `{valid, message}`。 + +## 数据表 +- `script_engine`(逻辑脚本表),脚本内容 `content` 使用 `text` 字段。 + +## 脚本类型(appcodes script_type) +- `0` = Python 脚本(受限命名空间执行,通过 `result` 变量返回结果) +- `1` = 规则(JSON)(解析 JSON 作为结果) +- `2` = 表达式(eval 求值) + +## 挂载 +宿主应用入口 `init()` 中调用 `load_script_engine()`。 + +## 安装 +```bash +pip install . +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..410ea11 --- /dev/null +++ b/__init__.py @@ -0,0 +1,7 @@ +"""script_engine 模块 —— 逻辑编程(脚本/规则引擎)。 + +通过 load_script_engine() 挂载到宿主应用。 +""" +from .script_engine.init import load_script_engine + +__all__ = ['load_script_engine'] diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..d4eb27a --- /dev/null +++ b/init/data.json @@ -0,0 +1,21 @@ +{ + "appcodes": [ + { + "parentid": "script_type", + "parentname": "脚本类型", + "items": [ + {"k": "0", "v": "Python脚本"}, + {"k": "1", "v": "规则(JSON)"}, + {"k": "2", "v": "表达式"} + ] + }, + { + "parentid": "script_status", + "parentname": "脚本状态", + "items": [ + {"k": "0", "v": "禁用"}, + {"k": "1", "v": "启用"} + ] + } + ] +} diff --git a/json/script_engine.json b/json/script_engine.json new file mode 100644 index 0000000..777d14a --- /dev/null +++ b/json/script_engine.json @@ -0,0 +1,30 @@ +{ + "tblname": "script_engine", + "title": "逻辑脚本", + "params": { + "sortby": ["updated_at desc"], + "editable": { + "new_data_url": "{{entire_url('../api/script_engine_create.dspy')}}", + "update_data_url": "{{entire_url('../api/script_engine_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/script_engine_delete.dspy')}}" + }, + "browserfields": { + "exclouded": ["id", "content"], + "alters": { + "world_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_world_id.dspy')}}"}, + "scene_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_scene_id.dspy')}}"}, + "entity_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_entity_id.dspy')}}"}, + "script_type": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_script_type.dspy')}}"}, + "status": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_status.dspy')}}"} + } + }, + "editexclouded": ["id", "created_at", "updated_at"], + "data_filter": { + "AND": [ + {"field": "name", "op": "LIKE", "var": "name"}, + {"field": "world_id", "op": "=", "var": "world_id"}, + {"field": "entity_id", "op": "=", "var": "entity_id"} + ] + } + } +} diff --git a/models/script.json b/models/script.json new file mode 100644 index 0000000..2cb290a --- /dev/null +++ b/models/script.json @@ -0,0 +1,28 @@ +{ + "summary": [{"name": "script", "title": "脚本定义表", "primary": ["id"], "catelog": "entity"}], + "fields": [ + {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "world_id", "title": "所属世界ID", "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": "script_type", "title": "脚本类型", "type": "str", "length": 16, "nullable": "no", "default": "lua"}, + {"name": "bind_type", "title": "绑定对象类型", "type": "str", "length": 16, "nullable": "no", "default": "world"}, + {"name": "bind_id", "title": "绑定对象ID", "type": "str", "length": 32, "nullable": "yes"}, + {"name": "trigger_event", "title": "触发事件", "type": "str", "length": 64, "nullable": "yes"}, + {"name": "content", "title": "脚本内容", "type": "text", "nullable": "yes"}, + {"name": "enabled", "title": "是否启用", "type": "str", "length": 1, "nullable": "no", "default": "0"}, + {"name": "created_by", "title": "创建人ID", "type": "str", "length": 32, "nullable": "yes"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"} + ], + "indexes": [ + {"name": "idx_script_code", "idxtype": "unique", "idxfields": ["world_id", "code"]}, + {"name": "idx_script_world", "idxtype": "index", "idxfields": ["world_id"]}, + {"name": "idx_script_bind", "idxtype": "index", "idxfields": ["bind_type", "bind_id"]} + ], + "codes": [ + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"field": "script_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='script_type'"}, + {"field": "bind_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='bind_type'"} + ] +} diff --git a/models/script_engine.json b/models/script_engine.json new file mode 100644 index 0000000..4930d20 --- /dev/null +++ b/models/script_engine.json @@ -0,0 +1,36 @@ +{ + "summary": [ + { + "name": "script_engine", + "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": "scene_id", "title": "所属场景", "type": "str", "length": 32, "nullable": "yes"}, + {"name": "entity_id", "title": "绑定实体", "type": "str", "length": 32, "nullable": "yes"}, + {"name": "name", "title": "脚本名称", "type": "str", "length": 255, "nullable": "no"}, + {"name": "code", "title": "脚本编码", "type": "str", "length": 64, "nullable": "no"}, + {"name": "script_type", "title": "脚本类型", "type": "str", "length": 16, "nullable": "no", "default": "0"}, + {"name": "trigger_event", "title": "触发事件", "type": "str", "length": 64, "nullable": "yes"}, + {"name": "content", "title": "脚本内容", "type": "text", "nullable": "no"}, + {"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "0"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"} + ], + "indexes": [ + {"name": "idx_script_code", "idxtype": "unique", "idxfields": ["code"]}, + {"name": "idx_script_world", "idxtype": "index", "idxfields": ["world_id"]}, + {"name": "idx_script_entity", "idxtype": "index", "idxfields": ["entity_id"]} + ], + "codes": [ + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"field": "scene_id", "table": "scene", "valuefield": "id", "textfield": "name"}, + {"field": "entity_id", "table": "entity", "valuefield": "id", "textfield": "name"}, + {"field": "script_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='script_type'"}, + {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='script_status'"} + ] +} diff --git a/models/script_exec_log.json b/models/script_exec_log.json new file mode 100644 index 0000000..25a974b --- /dev/null +++ b/models/script_exec_log.json @@ -0,0 +1,24 @@ +{ + "summary": [{"name": "script_exec_log", "title": "脚本执行日志表", "primary": ["id"], "catelog": "relation"}], + "fields": [ + {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "script_id", "title": "脚本ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "world_id", "title": "所属世界ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "status", "title": "执行状态", "type": "str", "length": 16, "nullable": "no", "default": "success"}, + {"name": "input", "title": "输入参数", "type": "text", "nullable": "yes"}, + {"name": "output", "title": "输出结果", "type": "text", "nullable": "yes"}, + {"name": "error", "title": "错误信息", "type": "text", "nullable": "yes"}, + {"name": "duration_ms", "title": "耗时(毫秒)", "type": "int", "nullable": "yes"}, + {"name": "executed_by", "title": "执行人ID", "type": "str", "length": 32, "nullable": "yes"}, + {"name": "created_at", "title": "执行时间", "type": "timestamp", "nullable": "no"} + ], + "indexes": [ + {"name": "idx_script_elog_script", "idxtype": "index", "idxfields": ["script_id"]}, + {"name": "idx_script_elog_world", "idxtype": "index", "idxfields": ["world_id"]} + ], + "codes": [ + {"field": "script_id", "table": "script", "valuefield": "id", "textfield": "name"}, + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='exec_status'"} + ] +} diff --git a/models/script_log.json b/models/script_log.json new file mode 100644 index 0000000..cecab4c --- /dev/null +++ b/models/script_log.json @@ -0,0 +1 @@ +script_log 表定义。 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d1b4b55 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "script_engine" +version = "1.0.0" +requires-python = ">=3.8" +dependencies = ["sqlor", "bricks_for_python"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["script_engine*"] diff --git a/script_engine/__init__.py b/script_engine/__init__.py new file mode 100644 index 0000000..638b804 --- /dev/null +++ b/script_engine/__init__.py @@ -0,0 +1,18 @@ +"""script_engine 包 —— 逻辑编程(脚本/规则引擎)模块实现。""" +from .init import ( + load_script_engine, + execute_script, + validate_script, + create_script, + update_script, + delete_script, +) + +__all__ = [ + 'load_script_engine', + 'execute_script', + 'validate_script', + 'create_script', + 'update_script', + 'delete_script', +] diff --git a/script_engine/init.py b/script_engine/init.py new file mode 100644 index 0000000..4b3e445 --- /dev/null +++ b/script_engine/init.py @@ -0,0 +1,134 @@ +"""script_engine 模块初始化。 + +逻辑编程(脚本/规则引擎)领域模块,通过 load_script_engine() 挂载到宿主应用, +向 ServerEnv 注册业务函数: +- execute_script(script_id, context_json) 执行脚本 +- validate_script(content, script_type) 校验脚本语法 +- create_script / update_script / delete_script CRUD 业务逻辑 +""" +import json as _json + +from appPublic.uniqueID import getID +from appPublic.timeUtils import curDateString +from ahserver.serverenv import ServerEnv +from sqlor.dbpools import DBPools + +_MODULE = 'script_engine' + + +def _dbname(): + """取模块库名(禁止硬编码,由宿主应用 get_module_dbname 决定)。""" + return ServerEnv().get_module_dbname(_MODULE) + + +async def _get_script(sor, script_id): + rows = await sor.sqlExe( + 'SELECT * FROM script_engine WHERE id = ${id}$', {'id': script_id}) + return rows[0] if rows else None + + +async def execute_script(script_id, context_json=None): + """执行指定脚本(逻辑编程运行时)。""" + if not script_id: + return {'success': False, 'error': '缺少 script_id 参数'} + + context = {} + if context_json: + if isinstance(context_json, str): + try: + context = _json.loads(context_json) + except Exception: + context = {} + elif isinstance(context_json, dict): + context = context_json + + db = DBPools() + async with db.sqlorContext(_dbname()) as sor: + script = await _get_script(sor, script_id) + if not script: + return {'success': False, 'error': '脚本不存在: %s' % script_id} + script_type = script.get('script_type') or '0' + content = script.get('content') or '' + try: + if script_type == '0': + loc = {'context': context, 'result': None} + exec(content, {'__builtins__': {}}, loc) + return {'success': True, 'result': loc.get('result')} + if script_type == '1': + return {'success': True, 'result': _json.loads(content)} + val = eval(content, {'__builtins__': {}}, {'context': context}) + return {'success': True, 'result': val} + except Exception as e: + return {'success': False, 'error': '%s: %s' % (type(e).__name__, e)} + + +async def validate_script(content, script_type='0'): + """校验脚本语法。""" + script_type = script_type or '0' + content = content or '' + try: + if script_type == '0': + compile(content, '