From 1bf204276951c3b0b11cb8e0cb98abb15d41b1f0 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Sat, 29 Aug 2026 12:44:23 +0800 Subject: [PATCH] =?UTF-8?q?approve:=20=E6=B5=8B=E8=AF=95=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=20-=20world=20=E4=B8=96=E7=95=8C=E7=AE=A1=E7=90=86=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +-- __init__.py | 11 ++-- build.sh | 39 ++++++++++---- init/data.json | 25 +++++++-- json/script_engine.json | 50 +++++++++--------- models/script_engine.json | 60 ++++++++++----------- script_engine/__init__.py | 9 ++-- script_engine/engine.py | 36 ++++++------- script_engine/init.py | 89 ++++++++++++++++++-------------- scripts/load_path.py | 74 ++++++++++++-------------- wwwroot/api/script_create.dspy | 2 + wwwroot/api/script_delete.dspy | 2 + wwwroot/api/script_execute.dspy | 2 + wwwroot/api/script_get.dspy | 2 + wwwroot/api/script_list.dspy | 2 + wwwroot/api/script_update.dspy | 2 + wwwroot/api/script_validate.dspy | 2 + wwwroot/index.ui | 76 ++++++++++++++++++++++++++- 18 files changed, 302 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index 6c35ad3..ae4f861 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # script_engine 脚本引擎模块(W-06) -脚本引擎:脚本表 CRUD + `execute_script` / `validate_script` 接口。独立模块,无业务依赖,可与 world / scene / entity 链路并行开发。 +脚本引擎:脚本表 CRUD + `execute_script` / `validate_script` 接口。独立模块,无业务依赖。 ## 功能 - **script 表 CRUD**:新增 / 编辑 / 删除 / 详情 / 分页列表 @@ -10,9 +10,10 @@ ## 数据表 - `script`:id / script_name / script_type / content / description / status / created_at / updated_at +- 索引:idx_script_name、idx_script_type ## 接口约定 -- REST 统一前缀:`/api/*`(宿主自动路由 `/script_engine/api/xxx.dspy`) +- REST 统一前缀:`/script_engine/api/*.dspy` - 错误结构:`{code, message, field, detail}`(code=0 成功,400 参数错误,404 不存在,500 内部错误,501 类型暂不支持) - 分页:`{list, total}` - 非法输入 100% 拦截,不落库、不执行 @@ -21,7 +22,7 @@ 1. `pip install .` 2. 宿主应用 `from script_engine.init import load_script_engine` → `init()` 内调用 `load_script_engine()` 3. `scripts/load_path.py` 登记 RBAC 路径;宿主 `load_path.py` 兜底 -4. `build.sh` 生成 DDL / CRUD UI / 链接 wwwroot +4. `build.sh` 四步:安装 xls2ddl → 生成 DDL → 生成 CRUD UI → 链接 wwwroot ## 接口清单(wwwroot/api/) | 路径 | 说明 | diff --git a/__init__.py b/__init__.py index 410ea11..8330b6c 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,6 @@ -"""script_engine 模块 —— 逻辑编程(脚本/规则引擎)。 +# -*- coding: utf-8 -*- +"""script_engine module -- script/rule engine (W-06).""" +from script_engine.engine import validate_script as validate_script, execute_script_content as execute_script_content +from script_engine.init import (load_script_engine as load_script_engine, create_script as create_script, update_script as update_script, delete_script as delete_script, list_scripts as list_scripts, get_script as get_script, execute_script as execute_script, validate_script_api as validate_script_api) -通过 load_script_engine() 挂载到宿主应用。 -""" -from .script_engine.init import load_script_engine - -__all__ = ['load_script_engine'] +__all__ = ['load_script_engine', 'create_script', 'update_script', 'delete_script', 'list_scripts', 'get_script', 'execute_script', 'validate_script_api', 'validate_script', 'execute_script_content'] diff --git a/build.sh b/build.sh index d31f2d0..14821c3 100644 --- a/build.sh +++ b/build.sh @@ -1,24 +1,36 @@ #!/usr/bin/env bash -# script_engine 模块构建脚本(由宿主应用 build.sh 集成调用) +# script_engine 模块构建脚本 —— 四步安装(由宿主应用 build.sh 集成调用) +# ① 安装生成工具 ② 生成 DDL ③ 生成 CRUD UI ④ 链接 wwwroot set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE="script_engine" +echo "[$MODULE] ==== Step 1/4: 安装生成工具 xls2ddl ====" +if ! command -v json2ddl >/dev/null 2>&1 || ! command -v xls2ui >/dev/null 2>&1; then + pip install xls2ddl >/dev/null 2>&1 || echo "[$MODULE] 警告: xls2ddl 安装失败(宿主已装则忽略)" +fi + +echo "[$MODULE] ==== Step 2/4: 生成 DDL ====" if [ -d "$SCRIPT_DIR/models" ]; then if command -v json2ddl >/dev/null 2>&1; then json2ddl mysql "$SCRIPT_DIR/models" > "$SCRIPT_DIR/models/mysql.ddl.sql" + echo "[$MODULE] DDL 已生成: models/mysql.ddl.sql" else - echo "[script_engine] json2ddl 不可用,跳过 DDL 生成" + echo "[$MODULE] 警告: json2ddl 不可用,跳过 DDL 生成" fi fi +echo "[$MODULE] ==== Step 3/4: 生成 CRUD UI ====" if [ -d "$SCRIPT_DIR/json" ]; then if command -v xls2ui >/dev/null 2>&1; then - (cd "$SCRIPT_DIR" && xls2ui -m models -o wwwroot script_engine json/*.json) + (cd "$SCRIPT_DIR" && xls2ui -m models -o wwwroot "$MODULE" json/*.json) + echo "[$MODULE] CRUD UI 已生成: wwwroot/$MODULE/" else - echo "[script_engine] xls2ui 不可用,跳过 CRUD UI 生成" + echo "[$MODULE] 警告: xls2ui 不可用,跳过 CRUD UI 生成" fi fi +echo "[$MODULE] ==== Step 4/4: 链接 wwwroot 到宿主 ====" SAGE_ROOT="" for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then @@ -27,15 +39,20 @@ for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do fi done if [ -n "$SAGE_ROOT" ]; then - mkdir -p "$SAGE_ROOT/wwwroot/script_engine" - ln -sfn "$SCRIPT_DIR/wwwroot/index.ui" "$SAGE_ROOT/wwwroot/script_engine/index.ui" - mkdir -p "$SAGE_ROOT/wwwroot/script_engine/api" + mkdir -p "$SAGE_ROOT/wwwroot/$MODULE/api" + ln -sfn "$SCRIPT_DIR/wwwroot/index.ui" "$SAGE_ROOT/wwwroot/$MODULE/index.ui" for f in "$SCRIPT_DIR"/wwwroot/api/*.dspy; do - ln -sfn "$f" "$SAGE_ROOT/wwwroot/script_engine/api/$(basename "$f")" + [ -f "$f" ] && ln -sfn "$f" "$SAGE_ROOT/wwwroot/$MODULE/api/$(basename "$f")" done - echo "[script_engine] wwwroot 已链接到 $SAGE_ROOT/wwwroot/script_engine" + for d in "$SCRIPT_DIR"/wwwroot/*/; do + [ -d "$d" ] || continue + name="$(basename "$d")" + case "$name" in api|styles|scripts) continue ;; esac + ln -sfn "$d" "$SAGE_ROOT/wwwroot/$MODULE/$name" + done + echo "[$MODULE] wwwroot 已链接到 $SAGE_ROOT/wwwroot/$MODULE" else - echo "[script_engine] 未找到宿主 wwwroot,跳过链接" + echo "[$MODULE] 未找到宿主 wwwroot,跳过链接" fi -echo "[script_engine] build.sh 完成" +echo "[$MODULE] ==== build.sh 四步安装完成 ====" diff --git a/init/data.json b/init/data.json index fff09c5..9496401 100644 --- a/init/data.json +++ b/init/data.json @@ -1,6 +1,21 @@ { - "appcodes": [ - {"parentid": "script_type", "parentname": "脚本类型", "items": [{"k": "0", "v": "Python"}, {"k": "1", "v": "JavaScript"}, {"k": "2", "v": "规则表达式"}]}, - {"parentid": "script_status", "parentname": "脚本状态", "items": [{"k": "0", "v": "停用"}, {"k": "1", "v": "启用"}]} - ] -} \ No newline at end of file + "appcodes": [ + { + "parentid": "script_type", + "parentname": "脚本类型", + "items": [ + {"k": "0", "v": "Python"}, + {"k": "1", "v": "JavaScript"}, + {"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 index 777d14a..67d1e66 100644 --- a/json/script_engine.json +++ b/json/script_engine.json @@ -1,30 +1,26 @@ { - "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"} - ] - } + "tblname": "script", + "params": { + "listname": "脚本列表", + "browserfields": { + "gridwidth": "100%", + "fields": [ + {"field": "script_name", "label": "脚本名称", "width": 150, "searchable": true}, + {"field": "script_type", "label": "脚本类型", "width": 100}, + {"field": "status", "label": "状态", "width": 80}, + {"field": "description", "label": "描述", "width": 220}, + {"field": "created_at", "label": "创建时间", "width": 150}, + {"field": "updated_at", "label": "更新时间", "width": 150} + ] + }, + "editexclouded": ["script_name", "script_type", "content", "description", "status"], + "new_data_url": "{{entire_url('../api/script_create.dspy')}}", + "update_data_url": "{{entire_url('../api/script_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/script_delete.dspy')}}", + "editable": { + "new_data_url": "{{entire_url('../api/script_create.dspy')}}", + "update_data_url": "{{entire_url('../api/script_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/script_delete.dspy')}}" } + } } diff --git a/models/script_engine.json b/models/script_engine.json index 4930d20..d364a8b 100644 --- a/models/script_engine.json +++ b/models/script_engine.json @@ -1,36 +1,28 @@ { - "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'"} - ] + "summary": [ + { + "table": "script", + "primary": ["id"], + "name": "脚本表", + "desc": "脚本/规则引擎脚本表(W-06 脚本引擎)" + } + ], + "fields": [ + {"name": "id", "type": "str32", "notnull": true, "comment": "脚本ID(主键)"}, + {"name": "script_name", "type": "str100", "notnull": true, "comment": "脚本名称"}, + {"name": "script_type", "type": "str32", "default": "0", "comment": "脚本类型: 0=Python/1=JavaScript/2=规则表达式"}, + {"name": "content", "type": "text", "notnull": true, "comment": "脚本内容"}, + {"name": "description", "type": "str255", "comment": "脚本描述"}, + {"name": "status", "type": "str32", "default": "1", "comment": "状态: 0=停用/1=启用"}, + {"name": "created_at", "type": "datetime", "comment": "创建时间"}, + {"name": "updated_at", "type": "datetime", "comment": "更新时间"} + ], + "indexes": [ + {"name": "idx_script_name", "fields": ["script_name"]}, + {"name": "idx_script_type", "fields": ["script_type"]} + ], + "codes": [ + {"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/script_engine/__init__.py b/script_engine/__init__.py index f96ec9d..6b6760e 100644 --- a/script_engine/__init__.py +++ b/script_engine/__init__.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- -"""script_engine 模块——脚本引擎(W-06)。""" -from script_engine.engine import (validate_script as validate_script, execute_script_content as execute_script_content) +"""script_engine module -- script/rule engine (W-06). + +Mounted into a host application via load_script_engine(). +""" +from script_engine.engine import validate_script as validate_script, execute_script_content as execute_script_content from script_engine.init import (load_script_engine as load_script_engine, create_script as create_script, update_script as update_script, delete_script as delete_script, list_scripts as list_scripts, get_script as get_script, execute_script as execute_script, validate_script_api as validate_script_api) -__all__ = ['load_script_engine','create_script','update_script','delete_script','list_scripts','get_script','execute_script','validate_script_api','validate_script','execute_script_content'] +__all__ = ['load_script_engine', 'create_script', 'update_script', 'delete_script', 'list_scripts', 'get_script', 'execute_script', 'validate_script_api', 'validate_script', 'execute_script_content'] diff --git a/script_engine/engine.py b/script_engine/engine.py index c912d1c..d8c2b1b 100644 --- a/script_engine/engine.py +++ b/script_engine/engine.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- -"""script_engine.engine —— 脚本引擎核心(纯逻辑,无 DB 依赖)。""" +"""script_engine.engine -- script engine core (pure logic, no DB dependency).""" import ast import builtins +import json as _json SCRIPT_TYPE_PYTHON = '0' SCRIPT_TYPE_JS = '1' SCRIPT_TYPE_RULE = '2' VALID_SCRIPT_TYPES = (SCRIPT_TYPE_PYTHON, SCRIPT_TYPE_JS, SCRIPT_TYPE_RULE) -_ALLOWED_BUILTINS = frozenset(['abs','all','any','bool','dict','divmod','enumerate','filter','float','format','frozenset','int','isinstance','issubclass','iter','len','list','map','max','min','next','object','pow','range','repr','reversed','round','set','slice','sorted','str','sum','tuple','zip']) +_ALLOWED_BUILTINS = frozenset(['abs', 'all', 'any', 'bool', 'dict', 'divmod', 'enumerate', 'filter', 'float', 'format', 'frozenset', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'list', 'map', 'max', 'min', 'next', 'object', 'pow', 'range', 'repr', 'reversed', 'round', 'set', 'slice', 'sorted', 'str', 'sum', 'tuple', 'zip']) _FORBIDDEN_NODES = (ast.Import, ast.ImportFrom, ast.Global, ast.Nonlocal, ast.Lambda, ast.ClassDef, ast.Yield, ast.YieldFrom, ast.AsyncFunctionDef, ast.Await) @@ -27,56 +28,55 @@ def _pair_check(text, open_ch, close_ch): def validate_python(content): if not content or not content.strip(): - return False, '脚本内容不能为空' + return False, 'script content must not be empty' try: tree = ast.parse(content, mode='exec') except SyntaxError as e: - return False, '语法错误: %s (第 %s 行)' % (e.msg or 'unknown', e.lineno or 0) + return False, 'syntax error: %s (line %s)' % (e.msg or 'unknown', e.lineno or 0) for node in ast.walk(tree): if isinstance(node, _FORBIDDEN_NODES): - return False, '禁止使用的语法: %s (第 %s 行)' % (type(node).__name__, getattr(node, 'lineno', 0)) + return False, 'forbidden syntax: %s (line %s)' % (type(node).__name__, getattr(node, 'lineno', 0)) if isinstance(node, ast.Call): func = node.func if isinstance(func, ast.Name): if func.id not in _ALLOWED_BUILTINS: - return False, '禁止调用的函数: %s (第 %s 行)' % (func.id, getattr(node, 'lineno', 0)) + return False, 'forbidden function call: %s (line %s)' % (func.id, getattr(node, 'lineno', 0)) elif isinstance(func, ast.Attribute): - return False, '禁止调用对象方法: %s (第 %s 行)' % (func.attr, getattr(node, 'lineno', 0)) + return False, 'forbidden method call: %s (line %s)' % (func.attr, getattr(node, 'lineno', 0)) return True, '' def validate_js(content): if not content or not content.strip(): - return False, '脚本内容不能为空' + return False, 'script content must not be empty' if not _pair_check(content, '{', '}'): - return False, '大括号不配对' + return False, 'unbalanced braces' if not _pair_check(content, '(', ')'): - return False, '圆括号不配对' + return False, 'unbalanced parentheses' if not _pair_check(content, '[', ']'): - return False, '方括号不配对' + return False, 'unbalanced brackets' return True, '' def validate_rule(content): if not content or not content.strip(): - return False, '脚本内容不能为空' + return False, 'script content must not be empty' text = content.strip() if text.startswith('{') or text.startswith('['): try: - import json as _json _json.loads(text) return True, '' except Exception as e: - return False, '规则 JSON 解析失败: %s' % str(e) + return False, 'rule json parse failed: %s' % str(e) if '->' not in text: - return False, '规则格式应为 JSON 或 "条件 -> 动作"' + return False, 'rule format should be json or "condition -> action"' return True, '' def validate_script(content, script_type): stype = str(script_type or SCRIPT_TYPE_PYTHON) if stype not in VALID_SCRIPT_TYPES: - return False, '不支持的脚本类型: %s' % stype + return False, 'unsupported script type: %s' % stype if stype == SCRIPT_TYPE_PYTHON: return validate_python(content) if stype == SCRIPT_TYPE_JS: @@ -103,5 +103,5 @@ def execute_script_content(content, script_type, params): if stype == SCRIPT_TYPE_PYTHON: return execute_python(content, params or {}) if stype == SCRIPT_TYPE_JS: - raise NotImplementedError('JavaScript 类型暂不支持执行') - raise NotImplementedError('规则表达式类型暂不支持执行') + raise NotImplementedError('JavaScript type execution is not supported yet') + raise NotImplementedError('rule expression type execution is not supported yet') diff --git a/script_engine/init.py b/script_engine/init.py index fc263d0..18033b9 100644 --- a/script_engine/init.py +++ b/script_engine/init.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- -"""script_engine.init —— 模块初始化与业务函数注册。""" +"""script_engine.init -- module initialization and business function registration.""" from ahserver.serverenv import ServerEnv from sqlor.dbpools import DBPools from appPublic.uniqueID import getID from appPublic.timeUtils import curDateString from script_engine.engine import validate_script as _validate_content, execute_script_content as _execute_content, VALID_SCRIPT_TYPES +_SCRIPT_FIELDS = ('id', 'script_name', 'script_type', 'content', 'description', 'status', 'created_at', 'updated_at') + def _dbname(): return ServerEnv().get_module_dbname('script_engine') @@ -26,17 +28,24 @@ def _clean_str(value, default=''): return s if s else default +def _row_to_dict(row): + out = {} + for f in _SCRIPT_FIELDS: + out[f] = getattr(row, f, None) + return out + + def _validate_input(script_name, script_type, content): if not script_name: - return False, 'script_name', '脚本名称不能为空' + return False, 'script_name', 'script name must not be empty' if len(script_name) > 100: - return False, 'script_name', '脚本名称长度不能超过 100' + return False, 'script_name', 'script name length must not exceed 100' if script_type not in VALID_SCRIPT_TYPES: - return False, 'script_type', '不支持的脚本类型: %s' % script_type + return False, 'script_type', 'unsupported script type: %s' % script_type if not content or not content.strip(): - return False, 'content', '脚本内容不能为空' + return False, 'content', 'script content must not be empty' if len(content) > 65535: - return False, 'content', '脚本内容长度不能超过 65535' + return False, 'content', 'script content length must not exceed 65535' ok, err = _validate_content(content, script_type) if not ok: return False, 'content', err @@ -53,7 +62,7 @@ async def create_script(request, params_kw): status = _clean_str(params.get('status'), '1') ok, field, msg = _validate_input(script_name, script_type, content) if not ok: - return _err(400, msg, field, 'create_script: 参数校验失败') + return _err(400, msg, field, 'create_script: input validation failed') dbname = _dbname() db = DBPools() now = curDateString() @@ -61,10 +70,10 @@ async def create_script(request, params_kw): ns = {'id': new_id, 'script_name': script_name, 'script_type': script_type, 'content': content, 'description': description, 'status': status, 'created_at': now, 'updated_at': now} async with db.sqlorContext(dbname) as sor: await sor.C('script', ns) - return _ok({'id': new_id}, '创建成功') + return _ok({'id': new_id}, 'created') except Exception as e: from traceback import format_exc - return _err(500, '创建失败: %s' % str(e), '', format_exc()) + return _err(500, 'create failed: %s' % str(e), '', format_exc()) async def update_script(request, params_kw): @@ -72,7 +81,7 @@ async def update_script(request, params_kw): params = params_kw or {} script_id = _clean_str(params.get('id')) if not script_id: - return _err(400, '缺少脚本ID', 'id', 'update_script: id 必填') + return _err(400, 'script id is required', 'id', 'update_script: id required') script_name = _clean_str(params.get('script_name')) script_type = _clean_str(params.get('script_type'), '0') content = _clean_str(params.get('content')) @@ -80,20 +89,19 @@ async def update_script(request, params_kw): status = _clean_str(params.get('status'), '1') ok, field, msg = _validate_input(script_name, script_type, content) if not ok: - return _err(400, msg, field, 'update_script: 参数校验失败') + return _err(400, msg, field, 'update_script: input validation failed') dbname = _dbname() db = DBPools() async with db.sqlorContext(dbname) as sor: rows = await sor.R('script', {'id': script_id}) if not rows: - return _err(404, '脚本不存在', 'id', 'update_script: 记录不存在') - upd = {'script_name': script_name, 'script_type': script_type, 'content': content, 'description': description, 'status': status, 'updated_at': curDateString()} - upd['id'] = script_id + return _err(404, 'script not found', 'id', 'update_script: record missing') + upd = {'id': script_id, 'script_name': script_name, 'script_type': script_type, 'content': content, 'description': description, 'status': status, 'updated_at': curDateString()} await sor.U('script', upd) - return _ok({'id': script_id}, '更新成功') + return _ok({'id': script_id}, 'updated') except Exception as e: from traceback import format_exc - return _err(500, '更新失败: %s' % str(e), '', format_exc()) + return _err(500, 'update failed: %s' % str(e), '', format_exc()) async def delete_script(request, params_kw): @@ -101,18 +109,18 @@ async def delete_script(request, params_kw): params = params_kw or {} script_id = _clean_str(params.get('id')) if not script_id: - return _err(400, '缺少脚本ID', 'id', 'delete_script: id 必填') + return _err(400, 'script id is required', 'id', 'delete_script: id required') dbname = _dbname() db = DBPools() async with db.sqlorContext(dbname) as sor: rows = await sor.R('script', {'id': script_id}) if not rows: - return _err(404, '脚本不存在', 'id', 'delete_script: 记录不存在') + return _err(404, 'script not found', 'id', 'delete_script: record missing') await sor.D('script', {'id': script_id}) - return _ok({'id': script_id}, '删除成功') + return _ok({'id': script_id}, 'deleted') except Exception as e: from traceback import format_exc - return _err(500, '删除失败: %s' % str(e), '', format_exc()) + return _err(500, 'delete failed: %s' % str(e), '', format_exc()) async def get_script(request, params_kw): @@ -120,18 +128,18 @@ async def get_script(request, params_kw): params = params_kw or {} script_id = _clean_str(params.get('id')) if not script_id: - return _err(400, '缺少脚本ID', 'id', 'get_script: id 必填') + return _err(400, 'script id is required', 'id', 'get_script: id required') dbname = _dbname() db = DBPools() async with db.sqlorContext(dbname) as sor: rows = await sor.R('script', {'id': script_id}) if not rows: - return _err(404, '脚本不存在', 'id', 'get_script: 记录不存在') - rec = dict(rows[0]) - return _ok(rec, '查询成功') + return _err(404, 'script not found', 'id', 'get_script: record missing') + rec = _row_to_dict(rows[0]) + return _ok(rec, 'ok') except Exception as e: from traceback import format_exc - return _err(500, '查询失败: %s' % str(e), '', format_exc()) + return _err(500, 'query failed: %s' % str(e), '', format_exc()) async def list_scripts(request, params_kw): @@ -162,13 +170,14 @@ async def list_scripts(request, params_kw): dbname = _dbname() db = DBPools() async with db.sqlorContext(dbname) as sor: - result = await sor.sqlExe(sql, ns) - total = result.get('total', 0) if isinstance(result, dict) else len(result or []) - data = result.get('rows', []) if isinstance(result, dict) else (result or []) - return _ok({'list': data, 'total': total}, '查询成功') + result = await sor.sqlPaging(sql, ns) + total = result.get('total', 0) + rows_data = result.get('rows', []) or [] + data = [_row_to_dict(r) for r in rows_data] + return _ok({'list': data, 'total': total}, 'ok') except Exception as e: from traceback import format_exc - return _err(500, '列表查询失败: %s' % str(e), '', format_exc()) + return _err(500, 'list failed: %s' % str(e), '', format_exc()) async def execute_script(request, params_kw): @@ -183,25 +192,25 @@ async def execute_script(request, params_kw): async with db.sqlorContext(dbname) as sor: rows = await sor.R('script', {'id': script_id}) if not rows: - return _err(404, '脚本不存在', 'id', 'execute_script: 记录不存在') + return _err(404, 'script not found', 'id', 'execute_script: record missing') content = rows[0].content script_type = str(rows[0].script_type or '0') else: content = _clean_str(params.get('content')) script_type = _clean_str(params.get('script_type'), '0') if not content: - return _err(400, '脚本内容不能为空', 'content', 'execute_script: 无脚本内容') + return _err(400, 'script content must not be empty', 'content', 'execute_script: no content') ok, field, msg = _validate_input('t', script_type, content) if not ok: - return _err(400, msg, field, 'execute_script: 校验失败,不执行') + return _err(400, msg, field, 'execute_script: validation failed, not executed') run_params = params.get('params') or {} result = _execute_content(content, script_type, run_params) - return _ok(result, '执行成功') + return _ok(result, 'executed') except NotImplementedError as e: - return _err(501, str(e), 'script_type', 'execute_script: 该类型暂不支持执行') + return _err(501, str(e), 'script_type', 'execute_script: type not supported') except Exception as e: from traceback import format_exc - return _err(500, '执行失败: %s' % str(e), '', format_exc()) + return _err(500, 'execute failed: %s' % str(e), '', format_exc()) async def validate_script_api(request, params_kw): @@ -210,14 +219,14 @@ async def validate_script_api(request, params_kw): content = _clean_str(params.get('content')) script_type = _clean_str(params.get('script_type'), '0') if not content: - return _err(400, '脚本内容不能为空', 'content', 'validate_script: 无脚本内容') + return _err(400, 'script content must not be empty', 'content', 'validate_script: no content') ok, field, msg = _validate_input('t', script_type, content) if not ok: - return _err(400, msg, field, 'validate_script: 校验失败') - return _ok({'valid': True}, '校验通过') + return _err(400, msg, field, 'validate_script: validation failed') + return _ok({'valid': True}, 'valid') except Exception as e: from traceback import format_exc - return _err(500, '校验失败: %s' % str(e), '', format_exc()) + return _err(500, 'validate failed: %s' % str(e), '', format_exc()) def load_script_engine(): diff --git a/scripts/load_path.py b/scripts/load_path.py index 0de1920..2778456 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -1,65 +1,59 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""script_engine 模块 RBAC 路径登记(load_path.py)。""" +"""script_engine 模块 RBAC 显式路径注册(禁止通配符)。 + +运行方式(宿主应用内): + cd && ./py3/bin/python /scripts/load_path.py +""" import os import sys -MOD = 'script_engine' - -PATHS_ANY = ['/%s/menu.ui' % MOD] +MODULE = 'script_engine' +# 显式路径清单(无通配符) PATHS_LOGINED = [ - '/%s' % MOD, - '/%s/index.ui' % MOD, - '/%s/script' % MOD, - '/%s/script/index.ui' % MOD, - '/%s/script/get_script.dspy' % MOD, - '/%s/script/add_script.dspy' % MOD, - '/%s/script/update_script.dspy' % MOD, - '/%s/script/delete_script.dspy' % MOD, - '/%s/api/script_create.dspy' % MOD, - '/%s/api/script_update.dspy' % MOD, - '/%s/api/script_delete.dspy' % MOD, - '/%s/api/script_get.dspy' % MOD, - '/%s/api/script_list.dspy' % MOD, - '/%s/api/script_execute.dspy' % MOD, - '/%s/api/script_validate.dspy' % MOD, + '/script_engine', + '/script_engine/index.ui', + '/script_engine/api/script_create.dspy', + '/script_engine/api/script_update.dspy', + '/script_engine/api/script_delete.dspy', + '/script_engine/api/script_get.dspy', + '/script_engine/api/script_list.dspy', + '/script_engine/api/script_execute.dspy', + '/script_engine/api/script_validate.dspy', ] -def find_sage_root(): +def _find_sage_root(): + script_dir = os.path.dirname(os.path.abspath(__file__)) candidates = [ - os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..'), + os.path.normpath(os.path.join(script_dir, '..', '..')), os.path.expanduser('~/repos/sage'), os.path.expanduser('~/sage'), ] for cand in candidates: if os.path.isdir(os.path.join(cand, 'wwwroot')) and os.path.isdir(os.path.join(cand, 'py3', 'bin')): - return os.path.abspath(cand) + return cand return None def main(): - sage_root = find_sage_root() + sage_root = _find_sage_root() if not sage_root: - print('[script_engine] 未找到 Sage 根目录,跳过 RBAC 登记(由宿主 load_path.py 兜底)') - return 0 - set_role_perm = os.path.join(sage_root, 'set_role_perm.py') - if not os.path.exists(set_role_perm): - print('[script_engine] 未找到 set_role_perm.py,跳过') - return 0 + print('[%s] 未找到宿主 Sage/wwwroot,跳过 RBAC 注册' % MODULE) + sys.exit(0) sys.path.insert(0, sage_root) - import importlib.util - spec = importlib.util.spec_from_file_location('set_role_perm', set_role_perm) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - for p in PATHS_ANY: - mod.set_role_perm(p, 'any') - for p in PATHS_LOGINED: - mod.set_role_perm(p, 'logined') - print('[script_engine] RBAC 路径登记完成: any=%d, logined=%d' % (len(PATHS_ANY), len(PATHS_LOGINED))) - return 0 + sys.path.insert(0, os.path.join(sage_root, 'py3', 'bin')) + try: + from set_role_perm import set_role_perm + except Exception as e: + print('[%s] 无法导入 set_role_perm: %s' % (MODULE, e)) + sys.exit(1) + for path in PATHS_LOGINED: + set_role_perm(path, 'logined') + print('[%s] 已注册 %s -> logined' % (MODULE, path)) + print('[%s] RBAC 注册完成(%d 条)' % (MODULE, len(PATHS_LOGINED))) if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/wwwroot/api/script_create.dspy b/wwwroot/api/script_create.dspy index de868dd..a70b87e 100644 --- a/wwwroot/api/script_create.dspy +++ b/wwwroot/api/script_create.dspy @@ -1,2 +1,4 @@ +# script_create.dspy -- create a script record +# @api: POST /script_engine/api/script_create.dspy result = await create_script(request, params_kw) return result diff --git a/wwwroot/api/script_delete.dspy b/wwwroot/api/script_delete.dspy index 92114f3..b566b22 100644 --- a/wwwroot/api/script_delete.dspy +++ b/wwwroot/api/script_delete.dspy @@ -1,2 +1,4 @@ +# script_delete.dspy -- delete a script record +# @api: POST /script_engine/api/script_delete.dspy result = await delete_script(request, params_kw) return result diff --git a/wwwroot/api/script_execute.dspy b/wwwroot/api/script_execute.dspy index ba16d2c..c6723e9 100644 --- a/wwwroot/api/script_execute.dspy +++ b/wwwroot/api/script_execute.dspy @@ -1,2 +1,4 @@ +# script_execute.dspy -- execute a script by id or inline content +# @api: POST /script_engine/api/script_execute.dspy result = await execute_script(request, params_kw) return result diff --git a/wwwroot/api/script_get.dspy b/wwwroot/api/script_get.dspy index 818ae59..b6a8191 100644 --- a/wwwroot/api/script_get.dspy +++ b/wwwroot/api/script_get.dspy @@ -1,2 +1,4 @@ +# script_get.dspy -- get one script record detail +# @api: GET/POST /script_engine/api/script_get.dspy result = await get_script(request, params_kw) return result diff --git a/wwwroot/api/script_list.dspy b/wwwroot/api/script_list.dspy index a3eb9e4..20bc4da 100644 --- a/wwwroot/api/script_list.dspy +++ b/wwwroot/api/script_list.dspy @@ -1,2 +1,4 @@ +# script_list.dspy -- paged script list +# @api: GET/POST /script_engine/api/script_list.dspy result = await list_scripts(request, params_kw) return result diff --git a/wwwroot/api/script_update.dspy b/wwwroot/api/script_update.dspy index 2428d77..24dc225 100644 --- a/wwwroot/api/script_update.dspy +++ b/wwwroot/api/script_update.dspy @@ -1,2 +1,4 @@ +# script_update.dspy -- update a script record +# @api: POST /script_engine/api/script_update.dspy result = await update_script(request, params_kw) return result diff --git a/wwwroot/api/script_validate.dspy b/wwwroot/api/script_validate.dspy index dbd9503..2f885dc 100644 --- a/wwwroot/api/script_validate.dspy +++ b/wwwroot/api/script_validate.dspy @@ -1,2 +1,4 @@ +# script_validate.dspy -- validate script content only (no persist, no execute) +# @api: POST /script_engine/api/script_validate.dspy result = await validate_script_api(request, params_kw) return result diff --git a/wwwroot/index.ui b/wwwroot/index.ui index ea8740d..685d366 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -1 +1,75 @@ -{"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.script_engine_content","options":{"url":"{{entire_url('/script_engine/script/index.ui')}}"},"mode":"replace"}],"subwidgets":[{"widgettype":"Text","options":{"label":"脚本管理"}},{"widgettype":"Text","options":{"label":"脚本 CRUD:新增/编辑/删除/查询"}}]},{"widgettype":"VBox","options":{"backgroundColor":"#FFFFFF","padding":"20px","cursor":"pointer"},"binds":[{"wid":"self","event":"click","actiontype":"urlwidget","target":"app.script_engine_content","options":{"url":"{{entire_url('/script_engine/api/script_execute.dspy')}}"},"mode":"replace"}],"subwidgets":[{"widgettype":"Text","options":{"label":"脚本执行"}},{"widgettype":"Text","options":{"label":"execute_script 接口"}}]},{"widgettype":"VBox","options":{"backgroundColor":"#FFFFFF","padding":"20px","cursor":"pointer"},"binds":[{"wid":"self","event":"click","actiontype":"urlwidget","target":"app.script_engine_content","options":{"url":"{{entire_url('/script_engine/api/script_validate.dspy')}}"},"mode":"replace"}],"subwidgets":[{"widgettype":"Text","options":{"label":"脚本校验"}},{"widgettype":"Text","options":{"label":"validate_script 接口"}}]}]},{"widgettype":"VBox","id":"app.script_engine_content","options":{"width":"100%","flex":"1","marginTop":"20px"}}]} \ No newline at end of file +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "padding": "20px", + "backgroundColor": "#F5F6FA" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "脚本引擎(W-06)", + "fontSize": "24px", + "fontWeight": "bold" + } + }, + { + "widgettype": "ResponsableBox", + "options": { + "gap": "16px", + "minWidth": "250px" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer", + "borderRadius": "8px" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.script_engine_content", + "options": { + "url": "{{entire_url('/script_engine/script_engine_list')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "脚本管理", + "fontSize": "16px" + } + }, + { + "widgettype": "Text", + "options": { + "label": "脚本 CRUD / 执行 / 校验", + "fontSize": "13px", + "color": "#888888" + } + } + ] + } + ] + }, + { + "widgettype": "VBox", + "id": "script_engine_content", + "options": { + "width": "100%", + "flex": "1", + "marginTop": "20px" + } + } + ] +}