approve: 复测已修复 Bug(元景项目-初始迭代)

This commit is contained in:
agent.develop 2026-08-29 15:38:23 +08:00
parent 2822693172
commit c7f4d4913d
7 changed files with 173 additions and 110 deletions

16
.gitignore vendored
View File

@ -1,11 +1,17 @@
# Python
__pycache__/ __pycache__/
*.pyc *.py[cod]
*.egg-info/ *.egg-info/
build/ build/
dist/ dist/
models/mysql.ddl.sql .venv/
wwwroot/script_engine/
wwwroot/script/ # generated artifacts
wwwroot/script_exec_log/ mysql.ddl.sql
wwwroot/script_engine_list/
# editors
*.swp *.swp
*.swo *.swo
.idea/
.vscode/

View File

@ -1,33 +1,30 @@
# script_engine 模块 # script_engine 逻辑编程(脚本/规则引擎)模块
逻辑编程(脚本/规则引擎)模块——脚本表 CRUD + 执行/校验接口 脚本/规则引擎:`script_engine` 脚本表 CRUD + 执行/校验接口。独立模块,无业务依赖
## 功能 ## 功能
- 脚本表 CRUD`create_script / update_script / delete_script / get_script / list_scripts`(含复数别名)
- `script_engine` 脚本表 CRUDcreate_script / update_script / delete_script / get_script / list_scripts - 执行接口:`execute_script`(按 id 或 content+script_type校验后执行
- `execute_script`:按 id 或按 content+script_type 校验后执行 - 校验接口:`validate_script_api`(仅校验,不落库不执行)
- `validate_script_api`:仅校验(语法 + 受限语法),不落库不执行 - 脚本类型:`0`=Python`1`=SQL
- 脚本类型:`0`=Python受限命名空间禁 import/类/λ/async/await/对象方法调用),`1`=SQL仅只读 SELECT/SHOW/DESCRIBE/EXPLAIN禁写语句/多语句) - Python 受限执行AST 校验(禁 import/类/λ/async/await/对象方法调用)+ builtins 白名单
- SQL 仅允许只读单语句SELECT/SHOW/DESCRIBE/EXPLAIN禁写语句/多语句/危险关键字
## 数据表 ## 数据表
- `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
`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
索引idx_script_name、idx_script_type - 编码script_type→appcodes_kv parentid='script_type'0=Python1=SQLstatus→parentid='script_status'0=停用1=启用)
编码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
## 集成 ## 集成
```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/*.dspyscript_create / script_update / script_delete / script_get / script_list / script_execute / execute_script / script_validate / validate_script ```bash
- 返回统一 `{code, message, field, detail}`code=0 成功;分页 `data.list` / `data.total` ./build.sh # 1) 装 xls2ddl 2) models→mysql.ddl.sql 3) json→CRUD UI 4) wwwroot 软链
```
## 依赖
无业务依赖;依赖基础包 sqlor / ahserver / appPublic。

View File

@ -1,50 +1,56 @@
#!/bin/bash #!/usr/bin/env bash
# script_engine module build -- four steps: # script_engine module build - four steps:
# 1. install xls2ddl # 1) install xls2ddl
# 2. models/ (json) -> mysql.ddl.sql # 2) models/*.json -> mysql.ddl.sql (json2ddl)
# 3. json/ -> CRUD ui/dspy via xls2ui # 3) json/*.json -> generated CRUD UI (xls2ui)
# 4. symlink module wwwroot into main app wwwroot # 4) symlink module wwwroot into host app wwwroot
set -e set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MOD=script_engine cd "$SCRIPT_DIR"
# locate sage root PYTHON="${PYTHON:-python3}"
SAGE_ROOT=""
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do echo "[1/4] install xls2ddl..."
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then pip install xls2ddl
SAGE_ROOT="$(cd "$candidate" && pwd)"
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 break
fi fi
done done
if [ -n "$HOST_WWWROOT" ]; then
echo "== [1/4] install xls2ddl ==" mkdir -p "$HOST_WWWROOT/script_engine"
pip install xls2ddl >/dev/null 2>&1 || pip3 install xls2ddl >/dev/null 2>&1 || true ln -sfn "$SCRIPT_DIR/wwwroot" "$HOST_WWWROOT/script_engine"
# link generated CRUD subdirs (script_engine_list) individually
echo "== [2/4] models -> DDL ==" for sub in "$SCRIPT_DIR/wwwroot"/*/; do
cd "$SCRIPT_DIR/models" name="$(basename "$sub")"
json2ddl mysql . > mysql.ddl.sql 2>/dev/null || \ case "$name" in
"$SAGE_ROOT/py3/bin/python3" "$SAGE_ROOT/py3/bin/json2ddl" mysql . > mysql.ddl.sql api|styles|scripts) continue ;;
esac
echo "== [3/4] json CRUD -> wwwroot ==" ln -sfn "$sub" "$HOST_WWWROOT/script_engine/$name"
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"
done done
echo "symlinked $MOD wwwroot into $SAGE_ROOT/wwwroot" echo "wwwroot linked to $HOST_WWWROOT/script_engine"
else 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 fi
echo "== script_engine build done ==" echo "script_engine build done"

View File

@ -5,6 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "script_engine" name = "script_engine"
version = "1.0.0" 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" requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"] dependencies = ["sqlor", "bricks_for_python"]

View File

@ -1,25 +1,31 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- 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: Run: python3 scripts/load_path.py (from module repo root)
cd <sage_root> && ./py3/bin/python <module>/scripts/load_path.py Explicit paths only - NO wildcards.
""" """
import os import os
import sys import sys
MOD = 'script_engine' MODULE = 'script_engine'
# paths accessible to any (logged-out) visitor
PATHS_ANY = [ PATHS_ANY = [
'/script_engine/menu.ui', '/script_engine/menu.ui',
'/script_engine/styles/script_engine.css',
'/script_engine/scripts/script_engine.js',
] ]
# paths accessible to any authenticated user
PATHS_LOGINED = [ PATHS_LOGINED = [
'/script_engine', '/script_engine',
'/script_engine/index.ui', '/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_create.dspy',
'/script_engine/api/script_update.dspy', '/script_engine/api/script_update.dspy',
'/script_engine/api/script_delete.dspy', '/script_engine/api/script_delete.dspy',
@ -33,45 +39,51 @@ PATHS_LOGINED = [
'/script_engine/api/get_search_status.dspy', '/script_engine/api/get_search_status.dspy',
] ]
PATHS_SUPERUSER = [ # role-restricted paths
'/script_engine/api/script_delete.dspy', PATHS_ROLE = [
'/script_engine/api/execute_script.dspy', ('/script_engine/api/script_create.dspy', 'owner.operator'),
'/script_engine/api/script_execute.dspy', ('/script_engine/api/script_update.dspy', 'owner.operator'),
('/script_engine/api/script_delete.dspy', 'owner.operator'),
] ]
def find_sage_root(): def find_sage_root():
script_dir = os.path.dirname(os.path.abspath(__file__))
candidates = [ candidates = [
os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'), os.path.join(script_dir, '..', '..'),
os.path.expanduser('~/repos/sage'), os.path.join(os.path.expanduser('~'), 'repos', 'sage'),
os.path.expanduser('~/sage'), os.path.join(os.path.expanduser('~'), 'sage'),
] ]
for c in candidates: for cand in candidates:
if os.path.isdir(os.path.join(c, 'wwwroot')) and os.path.isdir(os.path.join(c, 'py3', 'bin')): cand = os.path.abspath(cand)
return os.path.abspath(c) if os.path.isdir(os.path.join(cand, 'wwwroot')) and os.path.isdir(os.path.join(cand, 'py3', 'bin')):
return cand
return None 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(): def main():
sage_root = find_sage_root() sage_root = find_sage_root()
if not sage_root: if not sage_root:
print('ERROR: sage root not found, skip load_path registration') print('ERROR: sage root not found; skip RBAC registration')
return 1 return
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
for path in PATHS_ANY: for path in PATHS_ANY:
set_role_perm(path, 'any') register(sage_root, path, 'any')
for path in PATHS_LOGINED: for path in PATHS_LOGINED:
set_role_perm(path, 'logined') register(sage_root, path, 'logined')
for path in PATHS_SUPERUSER: for path, role in PATHS_ROLE:
set_role_perm(path, 'owner.superuser') register(sage_root, path, role)
print('script_engine load_path registered: %d paths' % (len(PATHS_ANY) + len(PATHS_LOGINED) + len(PATHS_SUPERUSER))) print('script_engine RBAC registration done')
return 0
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main()) main()

View File

@ -1,8 +1,18 @@
{"widgettype":"VBox","options":{"width":"100%","height":"100%","padding":"20px"},"subwidgets":[ {
{"widgettype":"Text","options":{"label":"脚本引擎","fontSize":"24px"}}, "widgettype": "VBox",
{"widgettype":"ResponsableBox","options":{"gap":"16px","minWidth":"250px"},"subwidgets":[ "options": {"width": "100%", "height": "100%", "padding": "20px"},
{"widgettype":"VBox","options":{"backgroundColor":"#FFFFFF","padding":"20px","cursor":"pointer"}, "subwidgets": [
"binds":[{"wid":"self","event":"click","actiontype":"urlwidget","target":"app.script_engine_content", {"widgettype": "Text", "options": {"label": "逻辑编程(脚本引擎)", "fontSize": "24px"}},
"options":{"url":"{{entire_url('script_list.ui')}}"},"mode":"replace"}], {"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"}, "subwidgets": [
"subwidgets":[{"widgettype":"Text","options":{"label":"脚本管理"}}]}]}, {"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
{"widgettype":"VBox","id":"script_engine_content","options":{"width":"100%","flex":"1","marginTop":"20px"}}]} "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"}}
]
}

View 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"}}
]}
]}
]
}