approve: 测试执行 - world_snapshot 世界快照模块

This commit is contained in:
agent.develop 2026-08-29 15:11:14 +08:00
parent 1bf2042769
commit 3c53fc93c7
25 changed files with 514 additions and 571 deletions

7
.gitignore vendored
View File

@ -1,8 +1,11 @@
__pycache__/
*.py[cod]
*.pyc
*.egg-info/
build/
dist/
models/mysql.ddl.sql
wwwroot/script_engine/
wwwroot/script/
.venv/
wwwroot/script_exec_log/
*.swp
*.swo

View File

@ -1,36 +1,33 @@
# script_engine 脚本引擎模块W-06
# script_engine 模块
脚本引擎:脚本表 CRUD + `execute_script` / `validate_script` 接口。独立模块,无业务依赖
逻辑编程(脚本/规则引擎)模块——脚本表 CRUD + 执行/校验接口
## 功能
- **script 表 CRUD**:新增 / 编辑 / 删除 / 详情 / 分页列表
- **execute_script**:按脚本 id 取库内脚本(或直传 content校验通过后执行
- **validate_script**:校验脚本内容(语法 + 受限语法 + 调用白名单),不落库不执行
- **编码字典**`script_type`0=Python / 1=JavaScript / 2=规则表达式)、`script_status`0=停用 / 1=启用)经 init/data.jsonFormat B appcodes幂等落库
- `script_engine` 脚本表 CRUDcreate_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禁写语句/多语句)
## 数据表
- `script`id / script_name / script_type / content / description / status / created_at / updated_at
- 索引idx_script_name、idx_script_type
## 接口约定
- REST 统一前缀:`/script_engine/api/*.dspy`
- 错误结构:`{code, message, field, detail}`code=0 成功400 参数错误404 不存在500 内部错误501 类型暂不支持)
- 分页:`{list, total}`
- 非法输入 100% 拦截,不落库、不执行
`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
## 集成
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` 四步:安装 xls2ddl → 生成 DDL → 生成 CRUD UI → 链接 wwwroot
## 接口清单wwwroot/api/
| 路径 | 说明 |
|---|---|
| `/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` | 分页列表({list,total} |
| `/script_engine/api/script_execute.dspy` | 执行脚本 |
| `/script_engine/api/script_validate.dspy` | 校验脚本 |
- 取库名统一 `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
- 返回统一 `{code, message, field, detail}`code=0 成功;分页 `data.list` / `data.total`
## 依赖
无业务依赖;依赖基础包 sqlor / ahserver / appPublic。

View File

@ -1,6 +1,8 @@
# -*- 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)
"""script_engine module root package.
__all__ = ['load_script_engine', 'create_script', 'update_script', 'delete_script', 'list_scripts', 'get_script', 'execute_script', 'validate_script_api', 'validate_script', 'execute_script_content']
Delegates to the real package script_engine.script_engine so that
`import script_engine` exposes the full public API.
"""
from script_engine.script_engine import * # noqa: F401,F403
from script_engine.script_engine import __all__ # noqa: F401

View File

@ -1,58 +1,50 @@
#!/usr/bin/env bash
# script_engine 模块构建脚本 —— 四步安装(由宿主应用 build.sh 集成调用)
# ① 安装生成工具 ② 生成 DDL ③ 生成 CRUD UI ④ 链接 wwwroot
#!/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
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODULE="script_engine"
MOD=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 "[$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 "$MODULE" json/*.json)
echo "[$MODULE] CRUD UI 已生成: wwwroot/$MODULE/"
else
echo "[$MODULE] 警告: xls2ui 不可用,跳过 CRUD UI 生成"
fi
fi
echo "[$MODULE] ==== Step 4/4: 链接 wwwroot 到宿主 ===="
# 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)"
break
fi
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
SAGE_ROOT="$(cd "$candidate" && pwd)"
break
fi
done
if [ -n "$SAGE_ROOT" ]; then
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
[ -f "$f" ] && ln -sfn "$f" "$SAGE_ROOT/wwwroot/$MODULE/api/$(basename "$f")"
done
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 "[$MODULE] 未找到宿主 wwwroot跳过链接"
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 "[$MODULE] ==== build.sh 四步安装完成 ===="
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
echo "symlinked $MOD wwwroot into $SAGE_ROOT/wwwroot"
else
echo "WARN: sage root not found, skip symlink (run from a full sage checkout)"
fi
echo "== script_engine build done =="

View File

@ -5,8 +5,7 @@
"parentname": "脚本类型",
"items": [
{"k": "0", "v": "Python"},
{"k": "1", "v": "JavaScript"},
{"k": "2", "v": "规则表达式"}
{"k": "1", "v": "SQL"}
]
},
{

View File

@ -1,19 +1,8 @@
{
"tblname": "script",
"tblname": "script_engine",
"title": "脚本管理",
"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"],
"sortby": ["created_at desc"],
"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')}}",
@ -21,6 +10,14 @@
"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')}}"
}
},
"browserfields": {
"exclouded": ["content"],
"alters": {
"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"]
}
}

View File

@ -1,25 +1,25 @@
{
"summary": [
{
"table": "script",
"name": "script_engine",
"title": "脚本表",
"primary": ["id"],
"name": "脚本表",
"desc": "脚本/规则引擎脚本表W-06 脚本引擎)"
"catelog": "entity"
}
],
"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": "更新时间"}
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "script_name", "title": "脚本名称", "type": "str", "length": 100, "nullable": "no"},
{"name": "script_type", "title": "脚本类型", "type": "str", "length": 32, "nullable": "no", "default": "0"},
{"name": "content", "title": "脚本内容", "type": "text", "nullable": "no"},
{"name": "description", "title": "描述", "type": "str", "length": 255},
{"name": "status", "title": "状态", "type": "str", "length": 32, "nullable": "no", "default": "1"},
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
],
"indexes": [
{"name": "idx_script_name", "fields": ["script_name"]},
{"name": "idx_script_type", "fields": ["script_type"]}
{"name": "idx_script_name", "idxtype": "index", "idxfields": ["script_name"]},
{"name": "idx_script_type", "idxtype": "index", "idxfields": ["script_type"]}
],
"codes": [
{"field": "script_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='script_type'"},

View File

@ -5,7 +5,6 @@ build-backend = "setuptools.build_meta"
[project]
name = "script_engine"
version = "1.0.0"
description = "脚本引擎模块W-06脚本 CRUD + execute_script/validate_script"
requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"]

View File

@ -1,9 +1,24 @@
# -*- coding: utf-8 -*-
"""script_engine module -- script/rule engine (W-06).
"""script_engine package: script table CRUD + execute/validate script API.
Mounted into a host application via load_script_engine().
Public async functions MUST be imported here so that .dspy files can call them
as pre-loaded globals after load_script_engine() registration.
"""
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)
from .engine import validate, validate_python, validate_sql, execute_python
from .init import (
create_script,
update_script,
delete_script,
get_script,
list_scripts,
execute_script,
validate_script_api,
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']
__all__ = [
'validate', 'validate_python', 'validate_sql', 'execute_python',
'create_script', 'update_script', 'delete_script', 'get_script',
'list_scripts', 'execute_script', 'validate_script_api',
'load_script_engine',
]

View File

@ -1,107 +1,103 @@
# -*- coding: utf-8 -*-
"""script_engine.engine -- script engine core (pure logic, no DB dependency)."""
"""script_engine execution engine.
Restricted script execution for script_type 0=Python / 1=SQL.
Python runs in a whitelisted-builtins namespace; imports, classes, lambdas,
async/await, generators and object method calls are forbidden.
SQL allows read-only statements only (SELECT/SHOW/DESCRIBE/EXPLAIN).
"""
import ast
import builtins
import json as _json
import re
SCRIPT_TYPE_PYTHON = '0'
SCRIPT_TYPE_JS = '1'
SCRIPT_TYPE_RULE = '2'
VALID_SCRIPT_TYPES = (SCRIPT_TYPE_PYTHON, SCRIPT_TYPE_JS, SCRIPT_TYPE_RULE)
# whitelisted python builtins (safe pure functions only)
ALLOWED_BUILTINS = {
'abs', 'all', 'any', 'bool', 'dict', 'divmod', 'enumerate', 'filter',
'float', 'int', 'isinstance', 'len', 'list', 'map', 'max', 'min', 'ord',
'chr', 'pow', 'range', 'repr', 'round', 'set', 'sorted', 'str', 'sum',
'tuple', 'type', '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'])
# ast node types that are never allowed in a script
FORBIDDEN_NODES = (
ast.Import, ast.ImportFrom, ast.ClassDef, ast.Lambda,
ast.AsyncFunctionDef, ast.AsyncFor, ast.AsyncWith, ast.Await,
ast.Yield, ast.YieldFrom, ast.Global,
)
_FORBIDDEN_NODES = (ast.Import, ast.ImportFrom, ast.Global, ast.Nonlocal, ast.Lambda, ast.ClassDef, ast.Yield, ast.YieldFrom, ast.AsyncFunctionDef, ast.Await)
def _pair_check(text, open_ch, close_ch):
depth = 0
for ch in text:
if ch == open_ch:
depth += 1
elif ch == close_ch:
depth -= 1
if depth < 0:
return False
return depth == 0
# sql keywords that indicate write / dangerous statements
SQL_FORBIDDEN_RE = re.compile(
r'\b(insert|update|delete|drop|alter|create|truncate|grant|revoke|'
r'replace|call|exec|execute|merge|rename|lock|unlock|set|use)\b',
re.IGNORECASE,
)
SQL_READONLY_RE = re.compile(r'^\s*(select|show|describe|desc|explain)\b', re.IGNORECASE)
def validate_python(content):
"""Validate python script syntax and forbidden constructs.
Returns {'code': 0, 'message': 'ok'} on success else {'code': 1, 'message': ...}.
"""
if not content or not content.strip():
return False, 'script content must not be empty'
return {'code': 1, 'message': 'content is required'}
try:
tree = ast.parse(content, mode='exec')
except SyntaxError as e:
return False, 'syntax error: %s (line %s)' % (e.msg or 'unknown', e.lineno or 0)
return {'code': 1, 'message': 'syntax error: %s' % e}
for node in ast.walk(tree):
if isinstance(node, _FORBIDDEN_NODES):
return False, 'forbidden syntax: %s (line %s)' % (type(node).__name__, getattr(node, 'lineno', 0))
if isinstance(node, FORBIDDEN_NODES):
return {'code': 1, 'message': 'forbidden construct: %s' % type(node).__name__}
if isinstance(node, ast.Call):
func = node.func
if isinstance(func, ast.Name):
if func.id not in _ALLOWED_BUILTINS:
return False, 'forbidden function call: %s (line %s)' % (func.id, getattr(node, 'lineno', 0))
elif isinstance(func, ast.Attribute):
return False, 'forbidden method call: %s (line %s)' % (func.attr, getattr(node, 'lineno', 0))
return True, ''
if isinstance(func, ast.Attribute):
return {'code': 1, 'message': 'object method call is forbidden'}
if isinstance(func, ast.Name) and func.id not in ALLOWED_BUILTINS:
return {'code': 1, 'message': 'forbidden call: %s' % func.id}
return {'code': 0, 'message': 'ok'}
def validate_js(content):
if not content or not content.strip():
return False, 'script content must not be empty'
if not _pair_check(content, '{', '}'):
return False, 'unbalanced braces'
if not _pair_check(content, '(', ')'):
return False, 'unbalanced parentheses'
if not _pair_check(content, '[', ']'):
return False, 'unbalanced brackets'
return True, ''
def execute_python(content, input_ns=None):
"""Execute python script in a restricted namespace.
def validate_rule(content):
if not content or not content.strip():
return False, 'script content must not be empty'
text = content.strip()
if text.startswith('{') or text.startswith('['):
try:
_json.loads(text)
return True, ''
except Exception as e:
return False, 'rule json parse failed: %s' % str(e)
if '->' not in text:
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, 'unsupported script type: %s' % stype
if stype == SCRIPT_TYPE_PYTHON:
return validate_python(content)
if stype == SCRIPT_TYPE_JS:
return validate_js(content)
return validate_rule(content)
def execute_python(content, params):
safe_builtins = {}
for name in _ALLOWED_BUILTINS:
if hasattr(builtins, name):
safe_builtins[name] = getattr(builtins, name)
namespace = {'__name__': '__script_engine__', '__builtins__': safe_builtins, 'params': params}
code = compile(content, '<script_engine>', 'exec')
The script may assign a final variable named `result` which is returned.
"""
builtins_ns = {}
for k in ALLOWED_BUILTINS:
if k in __builtins__:
builtins_ns[k] = __builtins__[k]
namespace = {'__builtins__': builtins_ns}
if input_ns:
for k, v in input_ns.items():
if k and k != '__builtins__':
namespace[k] = v
code = compile(content, '<script>', 'exec')
exec(code, namespace)
main_fn = namespace.get('main')
if callable(main_fn):
return main_fn(params)
return {'executed': True, 'scope': {k: v for k, v in namespace.items() if not k.startswith('__') and not callable(v)}}
return namespace.get('result')
def execute_script_content(content, script_type, params):
stype = str(script_type or SCRIPT_TYPE_PYTHON)
if stype == SCRIPT_TYPE_PYTHON:
return execute_python(content, params or {})
if stype == SCRIPT_TYPE_JS:
raise NotImplementedError('JavaScript type execution is not supported yet')
raise NotImplementedError('rule expression type execution is not supported yet')
def validate_sql(content):
"""Validate sql script: single read-only statement only.
Returns {'code': 0, 'message': 'ok'} on success else {'code': 1, 'message': ...}.
"""
if not content or not content.strip():
return {'code': 1, 'message': 'content is required'}
sql = content.strip()
if sql.rstrip().endswith(';'):
sql = sql.rstrip()[:-1]
if ';' in sql:
return {'code': 1, 'message': 'multi statement is forbidden'}
if not SQL_READONLY_RE.match(sql):
return {'code': 1, 'message': 'only read-only sql is allowed'}
if SQL_FORBIDDEN_RE.search(sql):
return {'code': 1, 'message': 'write or dangerous sql is forbidden'}
return {'code': 0, 'message': 'ok'}
def validate(script_type, content):
"""Dispatch validation by script_type: 0=Python, 1=SQL."""
if script_type == '0':
return validate_python(content)
if script_type == '1':
return validate_sql(content)
return {'code': 1, 'message': 'invalid script_type, only 0=Python or 1=SQL allowed'}

View File

@ -1,244 +1,276 @@
# -*- coding: utf-8 -*-
"""script_engine.init -- module initialization and business function registration."""
from ahserver.serverenv import ServerEnv
from sqlor.dbpools import DBPools
"""script_engine module initialization.
Registers all module functions with ServerEnv inside load_script_engine() so
that .dspy / .ui files can call them directly. DB name is resolved via
ServerEnv().get_module_dbname('script_engine') -- never hardcoded.
"""
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
from appPublic.log import debug, exception
from ahserver import ServerEnv
from sqlor.dbpools import DBPools
from . import engine
_SCRIPT_FIELDS = ('id', 'script_name', 'script_type', 'content', 'description', 'status', 'created_at', 'updated_at')
TABLE = 'script_engine'
def _dbname():
return ServerEnv().get_module_dbname('script_engine')
def _ok(data=None, message='ok'):
return {'code': 0, 'message': message, 'field': '', 'detail': '', 'data': data}
def _clean_ns(ns):
"""Normalize client params: drop NaN/null placeholders and _text suffixes."""
data = ns.copy()
for k, v in list(data.items()):
if v == 'NaN' or v == 'null':
data[k] = None
for k in list(data.keys()):
if k.endswith('_text'):
data.pop(k, None)
return data
def _err(code, message, field='', detail=''):
return {'code': code, 'message': message, 'field': field, 'detail': detail}
def _err(code, message, field=''):
return {'code': code, 'message': message, 'field': field, 'detail': ''}
def _clean_str(value, default=''):
if value is None:
return default
s = str(value).strip()
return s if s else default
def _ok(data=None):
return {'code': 0, 'message': 'ok', 'data': data or {}}
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', 'script name must not be empty'
if len(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', 'unsupported script type: %s' % script_type
async def create_script(request, ns):
data = _clean_ns(ns)
name = (data.get('script_name') or '').strip()
if not name:
return _err(1, 'script_name is required', 'script_name')
if len(name) > 100:
return _err(1, 'script_name too long (max 100)', 'script_name')
stype = data.get('script_type') or '0'
if stype not in ('0', '1'):
return _err(1, 'invalid script_type, only 0=Python or 1=SQL allowed', 'script_type')
content = data.get('content') or ''
if not content or not content.strip():
return False, 'content', 'script content must not be empty'
if len(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
return True, '', ''
async def create_script(request, params_kw):
return _err(1, 'content is required', 'content')
v = await validate_script_api(request, {'script_type': stype, 'content': content})
if v.get('code') != 0:
return v
status = data.get('status') or '1'
if status not in ('0', '1'):
status = '1'
now = curDateString()
row = {
'id': data.get('id') or getID(),
'script_name': name,
'script_type': stype,
'content': content,
'description': (data.get('description') or '')[:255],
'status': status,
'created_at': now,
'updated_at': now,
}
try:
params = params_kw or {}
script_name = _clean_str(params.get('script_name'))
script_type = _clean_str(params.get('script_type'), '0')
content = _clean_str(params.get('content'))
description = _clean_str(params.get('description'))
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: input validation failed')
dbname = _dbname()
db = DBPools()
now = curDateString()
new_id = getID()
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}, 'created')
async with DBPools().sqlorContext(_dbname()) as sor:
await sor.C(TABLE, row)
except Exception as e:
from traceback import format_exc
return _err(500, 'create failed: %s' % str(e), '', format_exc())
exception('script_engine.create_script error: %s' % e)
return _err(1, 'create failed: %s' % e)
return _ok({'id': row['id']})
async def update_script(request, params_kw):
async def update_script(request, ns):
data = _clean_ns(ns)
sid = data.get('id') or ''
if not sid:
return _err(1, 'id is required', 'id')
name = (data.get('script_name') or '').strip()
if not name:
return _err(1, 'script_name is required', 'script_name')
if len(name) > 100:
return _err(1, 'script_name too long (max 100)', 'script_name')
stype = data.get('script_type') or '0'
if stype not in ('0', '1'):
return _err(1, 'invalid script_type, only 0=Python or 1=SQL allowed', 'script_type')
content = data.get('content') or ''
if not content or not content.strip():
return _err(1, 'content is required', 'content')
v = await validate_script_api(request, {'script_type': stype, 'content': content})
if v.get('code') != 0:
return v
status = data.get('status') or '1'
if status not in ('0', '1'):
status = '1'
row = {
'script_name': name,
'script_type': stype,
'content': content,
'description': (data.get('description') or '')[:255],
'status': status,
'updated_at': curDateString(),
}
try:
params = params_kw or {}
script_id = _clean_str(params.get('id'))
if not 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'))
description = _clean_str(params.get('description'))
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: 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, '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}, 'updated')
async with DBPools().sqlorContext(_dbname()) as sor:
await sor.U(TABLE, {'id': sid, **row})
except Exception as e:
from traceback import format_exc
return _err(500, 'update failed: %s' % str(e), '', format_exc())
exception('script_engine.update_script error: %s' % e)
return _err(1, 'update failed: %s' % e)
return _ok({'id': sid})
async def delete_script(request, params_kw):
async def delete_script(request, ns):
data = _clean_ns(ns)
sid = data.get('id') or ''
if not sid:
return _err(1, 'id is required', 'id')
try:
params = params_kw or {}
script_id = _clean_str(params.get('id'))
if not 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, 'script not found', 'id', 'delete_script: record missing')
await sor.D('script', {'id': script_id})
return _ok({'id': script_id}, 'deleted')
async with DBPools().sqlorContext(_dbname()) as sor:
await sor.D(TABLE, {'id': sid})
except Exception as e:
from traceback import format_exc
return _err(500, 'delete failed: %s' % str(e), '', format_exc())
exception('script_engine.delete_script error: %s' % e)
return _err(1, 'delete failed: %s' % e)
return _ok({'id': sid})
async def get_script(request, params_kw):
async def get_script(request, ns):
data = _clean_ns(ns)
sid = data.get('id') or ''
if not sid:
return _err(1, 'id is required', 'id')
try:
params = params_kw or {}
script_id = _clean_str(params.get('id'))
if not 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, 'script not found', 'id', 'get_script: record missing')
rec = _row_to_dict(rows[0])
return _ok(rec, 'ok')
async with DBPools().sqlorContext(_dbname()) as sor:
recs = await sor.R(TABLE, {'id': sid})
except Exception as e:
from traceback import format_exc
return _err(500, 'query failed: %s' % str(e), '', format_exc())
exception('script_engine.get_script error: %s' % e)
return _err(1, 'get failed: %s' % e)
if not recs:
return _err(1, 'script not found', 'id')
r = recs[0]
return _ok({
'id': r.id,
'script_name': r.script_name,
'script_type': r.script_type,
'content': r.content,
'description': r.description,
'status': r.status,
'created_at': r.created_at,
'updated_at': r.updated_at,
})
async def list_scripts(request, params_kw):
async def list_scripts(request, ns):
data = _clean_ns(ns)
dbname = _dbname()
try:
params = params_kw or {}
page = int(params.get('page', 1) or 1)
rows = int(params.get('rows', 20) or 20)
if page < 1:
page = 1
if rows < 1 or rows > 200:
rows = 20
keyword = _clean_str(params.get('script_name'))
script_type = _clean_str(params.get('script_type'))
status = _clean_str(params.get('status'))
conds = []
ns = {'page': page, 'rows': rows, 'sort': 'created_at desc'}
if keyword:
conds.append('script_name like ${keyword}$')
ns['keyword'] = '%%%s%%' % keyword
if script_type:
conds.append('script_type = ${script_type}$')
ns['script_type'] = script_type
if status:
conds.append('status = ${status}$')
ns['status'] = status
where = (' where ' + ' and '.join(conds)) if conds else ''
sql = 'select id, script_name, script_type, description, status, created_at, updated_at from script%s' % where
dbname = _dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
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, 'list failed: %s' % str(e), '', format_exc())
async def execute_script(request, params_kw):
page = int(data.get('page') or 1)
rows = int(data.get('rows') or 20)
except (TypeError, ValueError):
page, rows = 1, 20
if page < 1:
page = 1
if rows < 1:
rows = 20
if rows > 200:
rows = 200
keyword = (data.get('keyword') or '').strip()
cond = ''
params = {}
if keyword:
cond = ' where script_name like ${keyword}$ '
params['keyword'] = '%' + keyword + '%'
offset = (page - 1) * rows
try:
params = params_kw or {}
script_id = _clean_str(params.get('id'))
content = None
script_type = '0'
if script_id:
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, '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, '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: validation failed, not executed')
run_params = params.get('params') or {}
result = _execute_content(content, script_type, run_params)
return _ok(result, 'executed')
except NotImplementedError as e:
return _err(501, str(e), 'script_type', 'execute_script: type not supported')
async with DBPools().sqlorContext(dbname) as sor:
cnt_recs = await sor.sqlExe('select count(*) as cnt from ' + TABLE + cond, params)
cnt = cnt_recs[0].cnt if cnt_recs else 0
sql = ('select id, script_name, script_type, description, status, '
'created_at, updated_at from ' + TABLE + cond +
' order by created_at desc limit ${offset}$, ${rows}$')
q = dict(params)
q['offset'] = offset
q['rows'] = rows
recs = await sor.sqlExe(sql, q)
except Exception as e:
from traceback import format_exc
return _err(500, 'execute failed: %s' % str(e), '', format_exc())
exception('script_engine.list_scripts error: %s' % e)
return _err(1, 'list failed: %s' % e)
items = [{
'id': r.id,
'script_name': r.script_name,
'script_type': r.script_type,
'description': r.description,
'status': r.status,
'created_at': r.created_at,
'updated_at': r.updated_at,
} for r in recs]
return {'code': 0, 'message': 'ok', 'data': {'list': items, 'total': cnt}}
async def validate_script_api(request, params_kw):
try:
params = params_kw or {}
content = _clean_str(params.get('content'))
script_type = _clean_str(params.get('script_type'), '0')
if not content:
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: validation failed')
return _ok({'valid': True}, 'valid')
except Exception as e:
from traceback import format_exc
return _err(500, 'validate failed: %s' % str(e), '', format_exc())
async def validate_script_api(request, ns):
"""Validate only -- never persists, never executes."""
data = _clean_ns(ns)
stype = data.get('script_type') or '0'
content = data.get('content') or ''
if not content or not content.strip():
return _err(1, 'content is required', 'content')
v = engine.validate(stype, content)
if v.get('code') != 0:
return _err(1, v['message'], 'content')
return _ok({'script_type': stype, 'valid': True})
def load_script_engine():
env = ServerEnv()
async def execute_script(request, ns):
"""Execute a script by id OR by inline content+script_type."""
data = _clean_ns(ns)
dbname = _dbname()
sid = (data.get('id') or '').strip()
if sid:
try:
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R(TABLE, {'id': sid})
except Exception as e:
exception('script_engine.execute_script query error: %s' % e)
return _err(1, 'query failed: %s' % e)
if not recs:
return _err(1, 'script not found', 'id')
r = recs[0]
content = r.content
stype = r.script_type
else:
content = data.get('content') or ''
stype = data.get('script_type') or '0'
if not content or not content.strip():
return _err(1, 'content is required', 'content')
if stype not in ('0', '1'):
return _err(1, 'invalid script_type, only 0=Python or 1=SQL allowed', 'script_type')
v = engine.validate(stype, content)
if v.get('code') != 0:
return _err(1, v['message'], 'content')
if stype == '0':
try:
out = engine.execute_python(content)
except Exception as e:
exception('script_engine.execute_script python error: %s' % e)
return _err(1, 'execute failed: %s' % e, 'content')
else:
try:
async with DBPools().sqlorContext(dbname) as sor:
out = await sor.sqlExe(content, {})
except Exception as e:
exception('script_engine.execute_script sql error: %s' % e)
return _err(1, 'execute failed: %s' % e, 'content')
return _ok({'result': out})
def load_script_engine(env=None):
"""Register module functions with ServerEnv (the ONLY integration point)."""
env = env or ServerEnv()
env.create_script = create_script
env.create_scripts = create_script
env.update_script = update_script
env.update_scripts = update_script
env.delete_script = delete_script
env.delete_scripts = delete_script
env.get_script = get_script
env.list_scripts = list_scripts
env.execute_script = execute_script
env.validate_script_api = validate_script_api
env.create_scripts = create_script
env.update_scripts = update_script
env.delete_scripts = delete_script
return env

View File

@ -1,59 +1,77 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""script_engine 模块 RBAC 显式路径注册(禁止通配符)。
"""script_engine RBAC load_path registration (explicit paths only, no wildcards).
运行方式宿主应用内:
cd <SAGE_ROOT> && ./py3/bin/python <module>/scripts/load_path.py
Roles: any / logined / owner.superuser. Run from the Sage host:
cd <sage_root> && ./py3/bin/python <module>/scripts/load_path.py
"""
import os
import sys
MODULE = 'script_engine'
MOD = 'script_engine'
PATHS_ANY = [
'/script_engine/menu.ui',
'/script_engine/styles/script_engine.css',
'/script_engine/scripts/script_engine.js',
]
# 显式路径清单(无通配符)
PATHS_LOGINED = [
'/script_engine',
'/script_engine/index.ui',
'/script_engine/script_list.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/execute_script.dspy',
'/script_engine/api/script_validate.dspy',
'/script_engine/api/validate_script.dspy',
'/script_engine/api/get_search_script_type.dspy',
'/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',
]
def _find_sage_root():
script_dir = os.path.dirname(os.path.abspath(__file__))
def find_sage_root():
candidates = [
os.path.normpath(os.path.join(script_dir, '..', '..')),
os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'),
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 cand
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)
return None
def main():
sage_root = _find_sage_root()
sage_root = find_sage_root()
if not sage_root:
print('[%s] 未找到宿主 Sage/wwwroot跳过 RBAC 注册' % MODULE)
sys.exit(0)
sys.path.insert(0, 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:
from set_role_perm import set_role_perm
import set_role_perm # noqa: F401
except Exception as e:
print('[%s] 无法导入 set_role_perm: %s' % (MODULE, e))
sys.exit(1)
print('ERROR: cannot import set_role_perm: %s' % e)
return 1
for path in PATHS_ANY:
set_role_perm(path, 'any')
for path in PATHS_LOGINED:
set_role_perm(path, 'logined')
print('[%s] 已注册 %s -> logined' % (MODULE, path))
print('[%s] RBAC 注册完成(%d 条)' % (MODULE, len(PATHS_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
if __name__ == '__main__':
main()
sys.exit(main())

View File

@ -1,33 +1,45 @@
---
name: script_engine
description: 逻辑编程(脚本/规则引擎)模块——脚本表 CRUD + execute_script/validate_script 接口,通过 load_script_engine() 挂载。脚本类型 0=Python / 1=JavaScript / 2=规则表达式
description: 逻辑编程(脚本/规则引擎)模块——脚本表 CRUD + execute_script/validate_script 接口,通过 load_script_engine() 挂载。脚本类型 0=Python / 1=SQL
---
# script_engine 模块
脚本/规则引擎:脚本表(`script`CRUD + 执行/校验接口。独立模块,无业务依赖。
脚本/规则引擎:脚本表(`script_engine`CRUD + 执行/校验接口。独立模块,无业务依赖。
## 数据模型
- `script``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`
- `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'
- 编码:`script_type` → appcodes_kv parentid='script_type'0=Python1=SQL`status` → appcodes_kv parentid='script_status'0=停用1=启用)
## 关键接口ServerEnv 注册函数,.dspy 直接调用)
- `create_script / update_script / delete_script / get_script / list_scripts`
- `execute_script`id 或 content+script_type校验后执行
- `validate_script_api`(仅校验,不落库不执行)
- 复数别名 `create_scripts/update_scripts/delete_scripts` 与单数同实现CRUD 框架约定)
## 错误结构与分页
- 统一 `{code, message, field, detail}`code=0 成功
- 分页 `{list, total}`list_scripts 返回 data.list / data.total
## REST 接口wwwroot/api/*.dspy
- script_create.dspy / script_update.dspy / script_delete.dspy / script_get.dspy / script_list.dspy
- script_execute.dspy / execute_script.dspy / script_validate.dspy / validate_script.dspy
- get_search_script_type.dspy / get_search_status.dspy下拉 [{value,text}]
## 陷阱
- 取库名用 `ServerEnv().get_module_dbname('script_engine')`,禁止硬编码 DBNAME
- 非法输入(空名/超长/非法类型/语法错误/受限语法/危险调用100% 拦截,不落库不执行
- Python 执行受限命名空间:仅内置白名单函数,禁 import/类/λ/async/await/对象方法调用
- Python 执行受限命名空间:仅内置白名单函数,禁 import/类/λ/async/await/对象方法调用engine.py ast 校验 + 白名单 builtins
- SQL 仅允许只读单语句SELECT/SHOW/DESCRIBE/EXPLAIN禁写语句/多语句/危险关键字
- .dspy 无 import/print/uuid/f-string显式 returnhelper 在 init.py
- init/data.json 为 Format B appcodesparentid + items幂等落库
## 依赖
- 无业务依赖;依赖基础包 sqlor / ahserver / appPublic

View File

@ -1,9 +1,6 @@
result = [{'value': '', 'text': '全部'}]
result = []
try:
async with get_sor_context(request._run_ns, 'appbase') as sor:
rows = await sor.sqlExe(
"select k as value, v as text from appcodes_kv where parentid='script_type' order by k", {})
result = [{'value': '', 'text': '全部'}] + list(rows)
result = [{'value': '', 'text': '全部'}, {'value': '0', 'text': 'Python'}, {'value': '1', 'text': 'SQL'}]
except Exception as e:
debug('get_search_script_type error: %s' % e)
return result
return json.dumps(result, ensure_ascii=False)

View File

@ -1,9 +1,6 @@
result = [{'value': '', 'text': '全部'}]
result = []
try:
async with get_sor_context(request._run_ns, 'appbase') as sor:
rows = await sor.sqlExe(
"select k as value, v as text from appcodes_kv where parentid='script_status' order by k", {})
result = [{'value': '', 'text': '全部'}] + list(rows)
result = [{'value': '', 'text': '全部'}, {'value': '1', 'text': '启用'}, {'value': '0', 'text': '停用'}]
except Exception as e:
debug('get_search_status error: %s' % e)
return result
return json.dumps(result, ensure_ascii=False)

View File

@ -1,4 +1,2 @@
# 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,4 +1,2 @@
# 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,4 +1,2 @@
# 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,4 +1,2 @@
# 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,4 +1,2 @@
# 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,4 +1,2 @@
# 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,4 +1,2 @@
# 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,2 +1,2 @@
result = await validate_script(request, params_kw)
result = await validate_script_api(request, params_kw)
return result

View File

@ -1,75 +1,8 @@
{
"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"
}
}
]
}
{"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"}}]}

View File

@ -1,42 +1,10 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "10px"},
"subwidgets": [
{
"widgettype": "DataViewer",
"options": {
"title": "脚本列表",
"data_url": "{{entire_url('/script_engine/api/script_list.dspy')}}",
"row_options": {
"keyid": "id",
"editable": {
"new_data_url": "{{entire_url('/script_engine/api/script_create.dspy')}}",
"update_data_url": "{{entire_url('/script_engine/api/script_update.dspy')}}",
"delete_data_url": "{{entire_url('/script_engine/api/script_delete.dspy')}}"
},
"browserfields": {
"exclouded": ["id", "content"],
"alters": {
"script_type": {
"uitype": "code",
"data": [
{"value": "0", "text": "Python脚本"},
{"value": "1", "text": "规则(JSON)"},
{"value": "2", "text": "表达式"}
]
},
"status": {
"uitype": "code",
"data": [
{"value": "1", "text": "启用"},
{"value": "0", "text": "禁用"}
]
}
}
},
"editexclouded": ["id", "created_at", "updated_at"]
}
}
}
]
}
{"widgettype":"VBox","options":{"width":"100%","height":"100%"},"subwidgets":[
{"widgettype":"Text","options":{"label":"脚本管理","fontSize":"18px","marginBottom":"10px"}},
{"widgettype":"DataViewer","options":{"data_url":"{{entire_url('api/script_list.dspy')}}",
"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')}}",
"browserfields":{"exclouded":["content"],
"alters":{"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"]}}]}