approve: 测试执行 - world 世界管理模块

This commit is contained in:
agent.develop 2026-08-29 12:44:23 +08:00
parent 58a27e52b4
commit 1bf2042769
18 changed files with 302 additions and 188 deletions

View File

@ -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/
| 路径 | 说明 |

View File

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

View File

@ -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 四步安装完成 ===="

View File

@ -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": "启用"}]}
]
}
"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": "启用"}
]
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,65 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""script_engine 模块 RBAC 路径登记load_path.py"""
"""script_engine 模块 RBAC 显式路径注册(禁止通配符)。
运行方式宿主应用内:
cd <SAGE_ROOT> && ./py3/bin/python <module>/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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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