approve: 复测已修复 Bug(元景项目-初始迭代)
This commit is contained in:
parent
2822693172
commit
c7f4d4913d
16
.gitignore
vendored
16
.gitignore
vendored
@ -1,11 +1,17 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
models/mysql.ddl.sql
|
||||
wwwroot/script_engine/
|
||||
wwwroot/script/
|
||||
wwwroot/script_exec_log/
|
||||
.venv/
|
||||
|
||||
# generated artifacts
|
||||
mysql.ddl.sql
|
||||
wwwroot/script_engine_list/
|
||||
|
||||
# editors
|
||||
*.swp
|
||||
*.swo
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
47
README.md
47
README.md
@ -1,33 +1,30 @@
|
||||
# script_engine 模块
|
||||
# script_engine 逻辑编程(脚本/规则引擎)模块
|
||||
|
||||
逻辑编程(脚本/规则引擎)模块——脚本表 CRUD + 执行/校验接口。
|
||||
脚本/规则引擎:`script_engine` 脚本表 CRUD + 执行/校验接口。独立模块,无业务依赖。
|
||||
|
||||
## 功能
|
||||
|
||||
- `script_engine` 脚本表 CRUD:create_script / update_script / delete_script / get_script / list_scripts
|
||||
- `execute_script`:按 id 或按 content+script_type 校验后执行
|
||||
- `validate_script_api`:仅校验(语法 + 受限语法),不落库不执行
|
||||
- 脚本类型:`0`=Python(受限命名空间,禁 import/类/λ/async/await/对象方法调用),`1`=SQL(仅只读 SELECT/SHOW/DESCRIBE/EXPLAIN,禁写语句/多语句)
|
||||
- 脚本表 CRUD:`create_script / update_script / delete_script / get_script / list_scripts`(含复数别名)
|
||||
- 执行接口:`execute_script`(按 id 或 content+script_type,校验后执行)
|
||||
- 校验接口:`validate_script_api`(仅校验,不落库不执行)
|
||||
- 脚本类型:`0`=Python,`1`=SQL
|
||||
- Python 受限执行:AST 校验(禁 import/类/λ/async/await/对象方法调用)+ builtins 白名单
|
||||
- SQL 仅允许只读单语句(SELECT/SHOW/DESCRIBE/EXPLAIN),禁写语句/多语句/危险关键字
|
||||
|
||||
## 数据表
|
||||
|
||||
`script_engine`:id(str32 PK)、script_name(str100 not null)、script_type(str32 default '0')、content(text not null)、description(str255)、status(str32 default '1')、created_at、updated_at
|
||||
索引:idx_script_name、idx_script_type
|
||||
编码:script_type→appcodes_kv(parentid='script_type'),status→appcodes_kv(parentid='script_status')
|
||||
|
||||
## 安装(挂载到宿主应用)
|
||||
|
||||
1. `pip install .`(或加入宿主 build.sh 模块安装循环)
|
||||
2. 宿主应用 `app/{app}.py`:`from script_engine.init import load_script_engine` + `load_script_engine()`(init() 内)
|
||||
3. `scripts/load_path.py` 注册 RBAC(显式路径,无通配符)
|
||||
4. `build.sh` 四步:安装 xls2ddl → models→DDL → json→CRUD UI → 链接 wwwroot
|
||||
- `script_engine`:id(str32 PK)、script_name(str100)、script_type(str32 default '0')、content(text)、description(str255)、status(str32 default '1')、created_at、updated_at
|
||||
- 索引:idx_script_name、idx_script_type
|
||||
- 编码:script_type→appcodes_kv parentid='script_type'(0=Python,1=SQL);status→parentid='script_status'(0=停用,1=启用)
|
||||
|
||||
## 集成
|
||||
```python
|
||||
from script_engine.init import load_script_engine
|
||||
load_script_engine() # 注册全部函数到 ServerEnv
|
||||
```
|
||||
- 取库名:`ServerEnv().get_module_dbname('script_engine')`(.py)/ `get_module_dbname('script_engine')`(.dspy 全局)
|
||||
- REST:`wwwroot/api/script_create.dspy` 等 11 个端点
|
||||
- RBAC:`scripts/load_path.py` 显式注册(无通配符)
|
||||
|
||||
- 取库名统一 `ServerEnv().get_module_dbname('script_engine')`(.py)/ `get_module_dbname('script_engine')`(.dspy),禁止硬编码 DBNAME
|
||||
- REST 接口(wwwroot/api/*.dspy):script_create / script_update / script_delete / script_get / script_list / script_execute / execute_script / script_validate / validate_script
|
||||
- 返回统一 `{code, message, field, detail}`;code=0 成功;分页 `data.list` / `data.total`
|
||||
|
||||
## 依赖
|
||||
|
||||
无业务依赖;依赖基础包 sqlor / ahserver / appPublic。
|
||||
## 构建
|
||||
```bash
|
||||
./build.sh # 1) 装 xls2ddl 2) models→mysql.ddl.sql 3) json→CRUD UI 4) wwwroot 软链
|
||||
```
|
||||
|
||||
86
build.sh
86
build.sh
@ -1,50 +1,56 @@
|
||||
#!/bin/bash
|
||||
# script_engine module build -- four steps:
|
||||
# 1. install xls2ddl
|
||||
# 2. models/ (json) -> mysql.ddl.sql
|
||||
# 3. json/ -> CRUD ui/dspy via xls2ui
|
||||
# 4. symlink module wwwroot into main app wwwroot
|
||||
set -e
|
||||
#!/usr/bin/env bash
|
||||
# script_engine module build - four steps:
|
||||
# 1) install xls2ddl
|
||||
# 2) models/*.json -> mysql.ddl.sql (json2ddl)
|
||||
# 3) json/*.json -> generated CRUD UI (xls2ui)
|
||||
# 4) symlink module wwwroot into host app wwwroot
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MOD=script_engine
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# locate sage root
|
||||
SAGE_ROOT=""
|
||||
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
|
||||
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
|
||||
SAGE_ROOT="$(cd "$candidate" && pwd)"
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
|
||||
echo "[1/4] install xls2ddl..."
|
||||
pip install xls2ddl
|
||||
|
||||
echo "[2/4] generate DDL from models/*.json..."
|
||||
if [ -d models ]; then
|
||||
$PYTHON -m xls2ddl.json2ddl mysql . > mysql.ddl.sql
|
||||
fi
|
||||
|
||||
echo "[3/4] generate CRUD UI from json/*.json..."
|
||||
if [ -d json ]; then
|
||||
$PYTHON -m xls2ddl.xls2ui -m models -o wwwroot script_engine json/*.json
|
||||
fi
|
||||
|
||||
echo "[4/4] symlink module wwwroot into host app wwwroot..."
|
||||
# locate host app wwwroot: prefer sibling app dir under the same workspace
|
||||
HOST_WWWROOT=""
|
||||
for candidate in \
|
||||
"$SCRIPT_DIR/../../apps/yuanjing/wwwroot" \
|
||||
"$SCRIPT_DIR/../../yuanjing/wwwroot" \
|
||||
"$SCRIPT_DIR/../yuanjing/wwwroot" \
|
||||
"$HOME/repos/yuanjing/wwwroot"; do
|
||||
if [ -d "$candidate" ]; then
|
||||
HOST_WWWROOT="$(cd "$candidate" && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "== [1/4] install xls2ddl =="
|
||||
pip install xls2ddl >/dev/null 2>&1 || pip3 install xls2ddl >/dev/null 2>&1 || true
|
||||
|
||||
echo "== [2/4] models -> DDL =="
|
||||
cd "$SCRIPT_DIR/models"
|
||||
json2ddl mysql . > mysql.ddl.sql 2>/dev/null || \
|
||||
"$SAGE_ROOT/py3/bin/python3" "$SAGE_ROOT/py3/bin/json2ddl" mysql . > mysql.ddl.sql
|
||||
|
||||
echo "== [3/4] json CRUD -> wwwroot =="
|
||||
cd "$SCRIPT_DIR"
|
||||
if [ -d json ] && ls json/*.json >/dev/null 2>&1; then
|
||||
PYTHONPATH="$SAGE_ROOT/py3/bin" python3 -m xls2ddl.xls2crud -m models -o wwwroot "$MOD" json/*.json || \
|
||||
"$SAGE_ROOT/py3/bin/xls2ui" -m models -o wwwroot "$MOD" json/*.json || true
|
||||
fi
|
||||
|
||||
echo "== [4/4] symlink module wwwroot -> sage wwwroot =="
|
||||
if [ -n "$SAGE_ROOT" ]; then
|
||||
ln -sfn "$SCRIPT_DIR/wwwroot" "$SAGE_ROOT/wwwroot/$MOD"
|
||||
for d in "$SCRIPT_DIR"/wwwroot/*/; do
|
||||
[ -d "$d" ] || continue
|
||||
base="$(basename "$d")"
|
||||
case "$base" in api|styles|scripts) continue ;; esac
|
||||
ln -sfn "$d" "$SAGE_ROOT/wwwroot/$base"
|
||||
if [ -n "$HOST_WWWROOT" ]; then
|
||||
mkdir -p "$HOST_WWWROOT/script_engine"
|
||||
ln -sfn "$SCRIPT_DIR/wwwroot" "$HOST_WWWROOT/script_engine"
|
||||
# link generated CRUD subdirs (script_engine_list) individually
|
||||
for sub in "$SCRIPT_DIR/wwwroot"/*/; do
|
||||
name="$(basename "$sub")"
|
||||
case "$name" in
|
||||
api|styles|scripts) continue ;;
|
||||
esac
|
||||
ln -sfn "$sub" "$HOST_WWWROOT/script_engine/$name"
|
||||
done
|
||||
echo "symlinked $MOD wwwroot into $SAGE_ROOT/wwwroot"
|
||||
echo "wwwroot linked to $HOST_WWWROOT/script_engine"
|
||||
else
|
||||
echo "WARN: sage root not found, skip symlink (run from a full sage checkout)"
|
||||
echo "WARN: host app wwwroot not found - skip symlink (deploy step will link)"
|
||||
fi
|
||||
|
||||
echo "== script_engine build done =="
|
||||
echo "script_engine build done"
|
||||
|
||||
@ -5,6 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "script_engine"
|
||||
version = "1.0.0"
|
||||
description = "Logic programming (script/rule engine) module: script table CRUD + execute/validate interfaces (0=Python, 1=SQL)"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["sqlor", "bricks_for_python"]
|
||||
|
||||
|
||||
@ -1,25 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""script_engine RBAC load_path registration (explicit paths only, no wildcards).
|
||||
"""script_engine RBAC path registration.
|
||||
|
||||
Roles: any / logined / owner.superuser. Run from the Sage host:
|
||||
cd <sage_root> && ./py3/bin/python <module>/scripts/load_path.py
|
||||
Run: python3 scripts/load_path.py (from module repo root)
|
||||
Explicit paths only - NO wildcards.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
MOD = 'script_engine'
|
||||
MODULE = 'script_engine'
|
||||
|
||||
# paths accessible to any (logged-out) visitor
|
||||
PATHS_ANY = [
|
||||
'/script_engine/menu.ui',
|
||||
'/script_engine/styles/script_engine.css',
|
||||
'/script_engine/scripts/script_engine.js',
|
||||
]
|
||||
|
||||
# paths accessible to any authenticated user
|
||||
PATHS_LOGINED = [
|
||||
'/script_engine',
|
||||
'/script_engine/index.ui',
|
||||
'/script_engine/script_list.ui',
|
||||
'/script_engine/script_execute_page.ui',
|
||||
'/script_engine/script_engine_list',
|
||||
'/script_engine/script_engine_list/index.ui',
|
||||
'/script_engine/script_engine_list/get_script_engine_list.dspy',
|
||||
'/script_engine/script_engine_list/add_script_engine.dspy',
|
||||
'/script_engine/script_engine_list/update_script_engine.dspy',
|
||||
'/script_engine/script_engine_list/delete_script_engine.dspy',
|
||||
'/script_engine/api/script_create.dspy',
|
||||
'/script_engine/api/script_update.dspy',
|
||||
'/script_engine/api/script_delete.dspy',
|
||||
@ -33,45 +39,51 @@ PATHS_LOGINED = [
|
||||
'/script_engine/api/get_search_status.dspy',
|
||||
]
|
||||
|
||||
PATHS_SUPERUSER = [
|
||||
'/script_engine/api/script_delete.dspy',
|
||||
'/script_engine/api/execute_script.dspy',
|
||||
'/script_engine/api/script_execute.dspy',
|
||||
# role-restricted paths
|
||||
PATHS_ROLE = [
|
||||
('/script_engine/api/script_create.dspy', 'owner.operator'),
|
||||
('/script_engine/api/script_update.dspy', 'owner.operator'),
|
||||
('/script_engine/api/script_delete.dspy', 'owner.operator'),
|
||||
]
|
||||
|
||||
|
||||
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.expanduser('~/repos/sage'),
|
||||
os.path.expanduser('~/sage'),
|
||||
os.path.join(script_dir, '..', '..'),
|
||||
os.path.join(os.path.expanduser('~'), 'repos', 'sage'),
|
||||
os.path.join(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 os.path.abspath(c)
|
||||
for cand in candidates:
|
||||
cand = os.path.abspath(cand)
|
||||
if os.path.isdir(os.path.join(cand, 'wwwroot')) and os.path.isdir(os.path.join(cand, 'py3', 'bin')):
|
||||
return cand
|
||||
return None
|
||||
|
||||
|
||||
def register(sage_root, path, role):
|
||||
set_role_perm = os.path.join(sage_root, 'py3', 'bin', 'set_role_perm.py')
|
||||
if not os.path.exists(set_role_perm):
|
||||
print('set_role_perm.py not found: %s' % set_role_perm)
|
||||
return
|
||||
cmd = '%s %s %s %s' % (sys.executable, set_role_perm, role, path)
|
||||
print('RUN: %s' % cmd)
|
||||
os.system(cmd)
|
||||
|
||||
|
||||
def main():
|
||||
sage_root = find_sage_root()
|
||||
if not sage_root:
|
||||
print('ERROR: sage root not found, skip load_path registration')
|
||||
return 1
|
||||
sys.path.insert(0, os.path.join(sage_root, 'py3', 'bin'))
|
||||
try:
|
||||
import set_role_perm # noqa: F401
|
||||
except Exception as e:
|
||||
print('ERROR: cannot import set_role_perm: %s' % e)
|
||||
return 1
|
||||
print('ERROR: sage root not found; skip RBAC registration')
|
||||
return
|
||||
for path in PATHS_ANY:
|
||||
set_role_perm(path, 'any')
|
||||
register(sage_root, path, 'any')
|
||||
for path in PATHS_LOGINED:
|
||||
set_role_perm(path, 'logined')
|
||||
for path in PATHS_SUPERUSER:
|
||||
set_role_perm(path, 'owner.superuser')
|
||||
print('script_engine load_path registered: %d paths' % (len(PATHS_ANY) + len(PATHS_LOGINED) + len(PATHS_SUPERUSER)))
|
||||
return 0
|
||||
register(sage_root, path, 'logined')
|
||||
for path, role in PATHS_ROLE:
|
||||
register(sage_root, path, role)
|
||||
print('script_engine RBAC registration done')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
main()
|
||||
|
||||
@ -1,8 +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.script_engine_content",
|
||||
"options":{"url":"{{entire_url('script_list.ui')}}"},"mode":"replace"}],
|
||||
"subwidgets":[{"widgettype":"Text","options":{"label":"脚本管理"}}]}]},
|
||||
{"widgettype":"VBox","id":"script_engine_content","options":{"width":"100%","flex":"1","marginTop":"20px"}}]}
|
||||
{
|
||||
"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_list')}}", "mode": "replace"}}],
|
||||
"subwidgets": [{"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_execute_page.ui')}}", "mode": "replace"}}],
|
||||
"subwidgets": [{"widgettype": "Text", "options": {"label": "脚本执行 / 校验"}}]}
|
||||
]},
|
||||
{"widgettype": "VBox", "id": "app.script_engine_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
|
||||
]
|
||||
}
|
||||
|
||||
31
wwwroot/script_execute_page.ui
Normal file
31
wwwroot/script_execute_page.ui
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "height": "100%", "padding": "20px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"label": "脚本执行 / 校验", "fontSize": "20px"}},
|
||||
{"widgettype": "HBox", "options": {"width": "100%", "gap": "16px"}, "subwidgets": [
|
||||
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "16px", "flex": "1"}, "subwidgets": [
|
||||
{"widgettype": "Text", "options": {"label": "脚本类型"}},
|
||||
{"widgettype": "Select", "id": "script_type", "options": {
|
||||
"data": [{"value": "0", "text": "Python"}, {"value": "1", "text": "SQL"}],
|
||||
"value": "0"}},
|
||||
{"widgettype": "Text", "options": {"label": "脚本内容"}},
|
||||
{"widgettype": "Textarea", "id": "script_content", "options": {"width": "100%", "height": "220px", "placeholder": "Python: 设置 result 变量输出;SQL: 只读单条 SELECT/SHOW/DESCRIBE/EXPLAIN"}},
|
||||
{"widgettype": "HBox", "options": {"gap": "12px"}, "subwidgets": [
|
||||
{"widgettype": "Button", "id": "btn_validate", "options": {"label": "校验"},
|
||||
"binds": [{"wid": "btn_validate", "event": "click", "actiontype": "urlwidget", "target": "script_result",
|
||||
"options": {"method": "POST", "url": "{{entire_url('api/script_validate.dspy')}}",
|
||||
"params": {"script_type": "{{params_kw.script_type}}", "content": "{{params_kw.script_content}}"}}}]},
|
||||
{"widgettype": "Button", "id": "btn_execute", "options": {"label": "执行"},
|
||||
"binds": [{"wid": "btn_execute", "event": "click", "actiontype": "urlwidget", "target": "script_result",
|
||||
"options": {"method": "POST", "url": "{{entire_url('api/script_execute.dspy')}}",
|
||||
"params": {"script_type": "{{params_kw.script_type}}", "content": "{{params_kw.script_content}}"}}}]}
|
||||
]}
|
||||
]},
|
||||
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "16px", "flex": "1"}, "subwidgets": [
|
||||
{"widgettype": "Text", "options": {"label": "执行结果"}},
|
||||
{"widgettype": "VBox", "id": "script_result", "options": {"width": "100%", "minHeight": "260px", "backgroundColor": "#F7F7F7"}}
|
||||
]}
|
||||
]}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user