approve: 元景项目 小功能点正反用例执行(412条,精确参数)
This commit is contained in:
parent
91bb3be8a6
commit
2822693172
@ -1,20 +1,20 @@
|
||||
{
|
||||
"appcodes": [
|
||||
{
|
||||
"parentid": "script_type",
|
||||
"parentname": "脚本类型",
|
||||
"items": [
|
||||
{"k": "0", "v": "Python"},
|
||||
{"k": "1", "v": "SQL"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "script_status",
|
||||
"parentname": "脚本状态",
|
||||
"items": [
|
||||
{"k": "0", "v": "停用"},
|
||||
{"k": "1", "v": "启用"}
|
||||
]
|
||||
}
|
||||
]
|
||||
"appcodes": [
|
||||
{
|
||||
"parentid": "script_type",
|
||||
"parentname": "脚本类型",
|
||||
"items": [
|
||||
{"k": "0", "v": "Python"},
|
||||
{"k": "1", "v": "SQL"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "script_status",
|
||||
"parentname": "脚本状态",
|
||||
"items": [
|
||||
{"k": "0", "v": "停用"},
|
||||
{"k": "1", "v": "启用"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,23 +1,38 @@
|
||||
{
|
||||
"tblname": "script_engine",
|
||||
"title": "脚本管理",
|
||||
"params": {
|
||||
"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')}}",
|
||||
"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')}}"
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
"tblname": "script_engine",
|
||||
"title": "脚本管理",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "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"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "script_name", "op": "LIKE", "var": "script_name"},
|
||||
{"field": "script_type", "op": "=", "var": "script_type"},
|
||||
{"field": "status", "op": "=", "var": "status"}
|
||||
]
|
||||
},
|
||||
"filter_labels": {
|
||||
"script_name": "脚本名称",
|
||||
"script_type": "脚本类型",
|
||||
"status": "状态"
|
||||
},
|
||||
"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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +1,94 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "script_engine",
|
||||
"title": "脚本表",
|
||||
"primary": ["id"],
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"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", "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'"},
|
||||
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='script_status'"}
|
||||
]
|
||||
"summary": [
|
||||
{
|
||||
"name": "script_engine",
|
||||
"title": "脚本表",
|
||||
"primary": ["id"],
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"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",
|
||||
"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'"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='script_status'"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,24 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""script_engine package: script table CRUD + execute/validate script API.
|
||||
|
||||
Public async functions MUST be imported here so that .dspy files can call them
|
||||
as pre-loaded globals after load_script_engine() registration.
|
||||
"""
|
||||
from .engine import validate, validate_python, validate_sql, execute_python
|
||||
"""script_engine package: logic programming (script/rule engine) module."""
|
||||
from .engine import (
|
||||
execute_python,
|
||||
validate_python_source,
|
||||
validate_sql,
|
||||
)
|
||||
from .init import (
|
||||
create_script,
|
||||
update_script,
|
||||
create_scripts,
|
||||
delete_script,
|
||||
delete_scripts,
|
||||
execute_script,
|
||||
get_script,
|
||||
list_scripts,
|
||||
execute_script,
|
||||
validate_script_api,
|
||||
load_script_engine,
|
||||
update_script,
|
||||
update_scripts,
|
||||
validate_script_api,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'validate', 'validate_python', 'validate_sql', 'execute_python',
|
||||
'create_script', 'update_script', 'delete_script', 'get_script',
|
||||
'list_scripts', 'execute_script', 'validate_script_api',
|
||||
'create_script',
|
||||
'create_scripts',
|
||||
'delete_script',
|
||||
'delete_scripts',
|
||||
'execute_python',
|
||||
'execute_script',
|
||||
'get_script',
|
||||
'list_scripts',
|
||||
'load_script_engine',
|
||||
'update_script',
|
||||
'update_scripts',
|
||||
'validate_python_source',
|
||||
'validate_script_api',
|
||||
'validate_sql',
|
||||
]
|
||||
|
||||
@ -1,183 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""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 unsafe attribute access are forbidden.
|
||||
|
||||
Method calls (ast.Attribute) are allowed ONLY when the method name is on the
|
||||
ALLOWED_METHODS whitelist and is not a dunder (``__x__``) method. This keeps
|
||||
the natural script pattern ``params.get('key')`` working (Bug
|
||||
kGZMpApWK3I1ed4OHpKQ0) while still blocking every other method call and any
|
||||
``__``-prefixed attribute -- sandbox-escape attempts such as
|
||||
``().__class__.__mro__`` / ``x.__globals__`` are rejected.
|
||||
|
||||
SQL allows read-only statements only (SELECT/SHOW/DESCRIBE/EXPLAIN).
|
||||
Python scripts run in a restricted namespace: only whitelisted builtin
|
||||
functions are callable; import / class / lambda / async / await and
|
||||
object method calls are forbidden (AST validation).
|
||||
SQL scripts are read-only single statements (SELECT/SHOW/DESCRIBE/EXPLAIN);
|
||||
write statements, multi-statement and dangerous keywords are rejected.
|
||||
"""
|
||||
import ast
|
||||
import builtins
|
||||
import contextlib
|
||||
import io
|
||||
import re
|
||||
import traceback
|
||||
|
||||
# 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',
|
||||
# whitelisted builtin names callable inside python scripts
|
||||
PY_WHITELIST = {
|
||||
'abs', 'all', 'any', 'bin', 'bool', 'bytearray', 'bytes', 'callable',
|
||||
'chr', 'complex', 'dict', 'divmod', 'enumerate', 'filter', 'float',
|
||||
'format', 'frozenset', 'hash', 'hex', 'int', 'isinstance', 'issubclass',
|
||||
'iter', 'len', 'list', 'map', 'max', 'min', 'next', 'oct', 'ord', 'pow',
|
||||
'print', 'range', 'repr', 'reversed', 'round', 'set', 'slice', 'sorted',
|
||||
'str', 'sum', 'tuple', 'zip',
|
||||
'True', 'False', 'None',
|
||||
}
|
||||
|
||||
# whitelisted object methods. The receiver is always an object that already
|
||||
# lives in the script namespace (a dict/list/str/int/float injected as
|
||||
# ``params`` or created by the script itself); there is no way to reach
|
||||
# os/subprocess/import machinery because dunder access is blocked and the
|
||||
# builtins namespace is restricted to ALLOWED_BUILTINS. Every method NOT in
|
||||
# this list is rejected (100% coverage enforced in validate_python).
|
||||
ALLOWED_METHODS = {
|
||||
# dict / mapping
|
||||
'get', 'keys', 'values', 'items', 'pop', 'setdefault', 'update',
|
||||
'clear', 'copy',
|
||||
# str (pure transformations / queries)
|
||||
'strip', 'lstrip', 'rstrip', 'lower', 'upper', 'title', 'capitalize',
|
||||
'replace', 'split', 'rsplit', 'splitlines', 'join', 'format',
|
||||
'format_map', 'startswith', 'endswith', 'find', 'rfind', 'index',
|
||||
'rindex', 'count', 'isdigit', 'isalpha', 'isalnum', 'isspace',
|
||||
'isupper', 'islower', 'istitle', 'encode', 'decode', 'zfill',
|
||||
'center', 'ljust', 'rjust',
|
||||
# list
|
||||
'append', 'extend', 'insert', 'remove', 'pop', 'sort', 'reverse',
|
||||
'index', 'count', 'copy', 'clear',
|
||||
# int / float
|
||||
'bit_length', 'is_integer',
|
||||
# builtins that are never allowed even if listed elsewhere
|
||||
PY_FORBIDDEN_BUILTINS = {
|
||||
'__import__', 'eval', 'exec', 'compile', 'open', 'input', 'globals',
|
||||
'locals', 'vars', 'dir', 'getattr', 'setattr', 'delattr', 'hasattr',
|
||||
'memoryview', 'breakpoint', 'exit', 'quit', 'help', 'type', 'object',
|
||||
}
|
||||
|
||||
# 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, ast.Nonlocal,
|
||||
# AST node types that are forbidden in python scripts
|
||||
PY_FORBIDDEN_NODE_TYPES = (
|
||||
ast.Import,
|
||||
ast.ImportFrom,
|
||||
ast.ClassDef,
|
||||
ast.Lambda,
|
||||
ast.AsyncFunctionDef,
|
||||
ast.AsyncFor,
|
||||
ast.AsyncWith,
|
||||
ast.Await,
|
||||
ast.Global,
|
||||
ast.Nonlocal,
|
||||
ast.Delete,
|
||||
ast.Yield,
|
||||
ast.YieldFrom,
|
||||
)
|
||||
|
||||
# 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 statement starters that are read-only
|
||||
SQL_READONLY_FIRST = ('SELECT', 'SHOW', 'DESCRIBE', 'DESC', 'EXPLAIN')
|
||||
|
||||
# SQL keywords that indicate write / execution / privilege operations
|
||||
SQL_FORBIDDEN_KEYWORDS = (
|
||||
'INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'CREATE', 'TRUNCATE',
|
||||
'GRANT', 'REVOKE', 'MERGE', 'REPLACE', 'RENAME', 'CALL', 'LOAD',
|
||||
'LOCK', 'UNLOCK', 'SET', 'USE', 'BEGIN', 'COMMIT', 'ROLLBACK',
|
||||
'SAVEPOINT', 'INTO', 'OUTFILE', 'INFILE', 'PREPARE', 'EXECUTE',
|
||||
'DEALLOCATE', 'PROCEDURE', 'TRIGGER', 'EVENT', 'TEMPORARY', 'PARTITION',
|
||||
)
|
||||
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.
|
||||
def _has_keyword(text, kw):
|
||||
return re.search(r'\b' + kw + r'\b', text, re.IGNORECASE) is not None
|
||||
|
||||
Safety rules (in order of evaluation):
|
||||
1. forbidden AST node kinds: import/import-from/class/lambda/async/
|
||||
await/yield/global/nonlocal
|
||||
2. dunder (``__x__``) attribute access is ALWAYS rejected -- this is
|
||||
the sandbox-escape guard (``().__class__``, ``x.__globals__``, ...)
|
||||
3. calls:
|
||||
- bare name -> the name must be in ALLOWED_BUILTINS
|
||||
- object method (ast.Attribute) -> the method name must be in
|
||||
ALLOWED_METHODS (and, per rule 2, never dunder); anything else is
|
||||
rejected (100% interception of non-whitelisted methods)
|
||||
- any other call expression (subscript call, call result call, ...)
|
||||
is rejected
|
||||
4. attribute assignment / deletion (``obj.attr = ...``,
|
||||
``del obj.attr``) is rejected -- scripts must not mutate object
|
||||
attributes. Subscript assignment on plain dicts/lists stays allowed
|
||||
because it only touches objects already in the script namespace.
|
||||
|
||||
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'}
|
||||
def validate_python_source(content):
|
||||
"""AST-validate python source. Returns (ok, message)."""
|
||||
if not content or not str(content).strip():
|
||||
return False, 'python content is empty'
|
||||
try:
|
||||
tree = ast.parse(content, mode='exec')
|
||||
tree = ast.parse(str(content))
|
||||
except SyntaxError as e:
|
||||
return {'code': 1, 'message': 'syntax error: %s' % e}
|
||||
|
||||
# rule 1 + 2: forbidden node kinds and dunder attribute access
|
||||
return False, 'syntax error: %s' % str(e)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, FORBIDDEN_NODES):
|
||||
return {'code': 1, 'message': 'forbidden construct: %s' % type(node).__name__}
|
||||
if isinstance(node, ast.Attribute) and node.attr.startswith('__'):
|
||||
return {'code': 1, 'message': 'forbidden attribute access: %s' % node.attr}
|
||||
if isinstance(node, PY_FORBIDDEN_NODE_TYPES):
|
||||
return False, 'forbidden python syntax: ' + type(node).__name__
|
||||
if isinstance(node, ast.Call):
|
||||
fn = node.func
|
||||
if isinstance(fn, ast.Name):
|
||||
if fn.id in PY_FORBIDDEN_BUILTINS:
|
||||
return False, 'forbidden builtin: ' + fn.id
|
||||
if fn.id not in PY_WHITELIST:
|
||||
return False, 'unknown function: ' + fn.id
|
||||
elif isinstance(fn, ast.Attribute):
|
||||
return False, 'object method call is not allowed'
|
||||
return True, ''
|
||||
|
||||
# rule 3: calls
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
|
||||
def execute_python(content, params=None):
|
||||
"""Execute python source in a restricted namespace.
|
||||
|
||||
Script may read `params` dict and set `result`; stdout is captured.
|
||||
Returns {code, message, field, detail, data}.
|
||||
"""
|
||||
ok, msg = validate_python_source(content)
|
||||
if not ok:
|
||||
return {'code': 1, 'message': msg, 'field': 'content', 'detail': ''}
|
||||
safe_builtins = {}
|
||||
for name in PY_WHITELIST:
|
||||
if name in ('True', 'False', 'None'):
|
||||
continue
|
||||
func = node.func
|
||||
if isinstance(func, ast.Name):
|
||||
if func.id not in ALLOWED_BUILTINS:
|
||||
return {'code': 1, 'message': 'forbidden call: %s' % func.id}
|
||||
elif isinstance(func, ast.Attribute):
|
||||
if func.attr not in ALLOWED_METHODS:
|
||||
return {'code': 1, 'message': 'forbidden method call: %s' % func.attr}
|
||||
else:
|
||||
return {'code': 1, 'message': 'forbidden call expression'}
|
||||
|
||||
# rule 4: attribute assignment / deletion
|
||||
for node in ast.walk(tree):
|
||||
targets = []
|
||||
if isinstance(node, ast.Assign):
|
||||
targets = node.targets
|
||||
elif isinstance(node, (ast.AnnAssign, ast.AugAssign)):
|
||||
targets = [node.target]
|
||||
elif isinstance(node, ast.Delete):
|
||||
targets = node.targets
|
||||
for t in targets:
|
||||
for sub in ast.walk(t):
|
||||
if isinstance(sub, ast.Attribute):
|
||||
return {'code': 1, 'message': 'attribute assignment is forbidden'}
|
||||
|
||||
return {'code': 0, 'message': 'ok'}
|
||||
if hasattr(builtins, name):
|
||||
safe_builtins[name] = getattr(builtins, name)
|
||||
safe_builtins['__builtins__'] = safe_builtins
|
||||
ns = {'params': dict(params or {})}
|
||||
ns.update(safe_builtins)
|
||||
buf = io.StringIO()
|
||||
try:
|
||||
with contextlib.redirect_stdout(buf):
|
||||
exec(compile(str(content), '<script_engine>', 'exec'), ns)
|
||||
return {
|
||||
'code': 0,
|
||||
'message': 'ok',
|
||||
'field': '',
|
||||
'detail': '',
|
||||
'data': {'result': ns.get('result'), 'output': buf.getvalue()},
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'code': 1,
|
||||
'message': str(e),
|
||||
'field': '',
|
||||
'detail': traceback.format_exc(),
|
||||
}
|
||||
|
||||
|
||||
def execute_python(content, input_ns=None):
|
||||
"""Execute python script in a restricted namespace.
|
||||
|
||||
The script may assign a final variable named `result` which is returned.
|
||||
``input_ns`` (e.g. {'params': {...}}) is injected into the namespace so
|
||||
scripts can read business inputs with ``params.get('key')``.
|
||||
"""
|
||||
blt = __builtins__ if isinstance(__builtins__, dict) else vars(__builtins__)
|
||||
builtins_ns = {}
|
||||
for k in ALLOWED_BUILTINS:
|
||||
if k in blt:
|
||||
builtins_ns[k] = blt[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)
|
||||
return namespace.get('result')
|
||||
|
||||
|
||||
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'}
|
||||
def validate_sql(sql):
|
||||
"""Validate SQL source. Returns (ok, message)."""
|
||||
if not sql or not str(sql).strip():
|
||||
return False, 'sql content is empty'
|
||||
text = str(sql).strip().rstrip(';').strip()
|
||||
if not text:
|
||||
return False, 'sql content is empty'
|
||||
parts = [p.strip() for p in text.split(';') if p.strip()]
|
||||
if len(parts) > 1:
|
||||
return False, 'multi-statement sql is not allowed'
|
||||
first = text.split(None, 1)[0].upper()
|
||||
if first not in SQL_READONLY_FIRST:
|
||||
return False, 'only SELECT/SHOW/DESCRIBE/EXPLAIN statements are allowed'
|
||||
for kw in SQL_FORBIDDEN_KEYWORDS:
|
||||
if _has_keyword(text, kw):
|
||||
return False, 'forbidden sql keyword: ' + kw
|
||||
return True, ''
|
||||
|
||||
@ -1,283 +1,327 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""script_engine module initialization.
|
||||
"""script_engine module init.
|
||||
|
||||
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.
|
||||
Register script table CRUD + execute/validate interfaces to ServerEnv.
|
||||
Functions:
|
||||
create_script / update_script / delete_script / get_script / list_scripts
|
||||
execute_script / validate_script_api
|
||||
plural aliases: create_scripts / update_scripts / delete_scripts
|
||||
Entry point: load_script_engine()
|
||||
"""
|
||||
from appPublic.uniqueID import getID
|
||||
from appPublic.timeUtils import curDateString
|
||||
from appPublic.log import debug, exception
|
||||
from ahserver import ServerEnv
|
||||
from sqlor.dbpools import DBPools
|
||||
from . import engine
|
||||
try:
|
||||
from ahserver import ServerEnv
|
||||
except ImportError: # pragma: no cover - alternate package layout
|
||||
try:
|
||||
from ahserver.serverEnv import ServerEnv
|
||||
except ImportError: # pragma: no cover - alternate package layout
|
||||
from sqlor.dbpools import ServerEnv
|
||||
|
||||
from appPublic.log import debug
|
||||
from appPublic.timeUtils import curDateString
|
||||
from appPublic.uniqueID import getID
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
from .engine import (
|
||||
execute_python,
|
||||
validate_python_source,
|
||||
validate_sql,
|
||||
)
|
||||
|
||||
MODULE = 'script_engine'
|
||||
TABLE = 'script_engine'
|
||||
|
||||
SCRIPT_TYPE_PYTHON = '0'
|
||||
SCRIPT_TYPE_SQL = '1'
|
||||
STATUS_ENABLED = '1'
|
||||
|
||||
|
||||
def _dbname():
|
||||
return ServerEnv().get_module_dbname('script_engine')
|
||||
return ServerEnv().get_module_dbname(MODULE)
|
||||
|
||||
|
||||
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 _clean_params(params_kw):
|
||||
if hasattr(params_kw, 'copy'):
|
||||
return params_kw.copy()
|
||||
return dict(params_kw or {})
|
||||
|
||||
|
||||
def _err(code, message, field=''):
|
||||
return {'code': code, 'message': message, 'field': field, 'detail': ''}
|
||||
|
||||
|
||||
def _ok(data=None):
|
||||
return {'code': 0, 'message': 'ok', 'data': data or {}}
|
||||
|
||||
|
||||
async def create_script(request, ns):
|
||||
data = _clean_ns(ns)
|
||||
name = (data.get('script_name') or '').strip()
|
||||
def _check_name(name):
|
||||
if not name:
|
||||
return _err(1, 'script_name is required', 'script_name')
|
||||
return 'script_name is required'
|
||||
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'
|
||||
return 'script_name is too long (max 100)'
|
||||
return ''
|
||||
|
||||
|
||||
def _check_type(stype):
|
||||
if stype not in (SCRIPT_TYPE_PYTHON, SCRIPT_TYPE_SQL):
|
||||
return 'invalid script_type (0=python, 1=sql)'
|
||||
return ''
|
||||
|
||||
|
||||
def _validate_content(stype, content):
|
||||
if stype == SCRIPT_TYPE_PYTHON:
|
||||
return validate_python_source(content)
|
||||
return validate_sql(content)
|
||||
|
||||
|
||||
async def create_script(request, params_kw):
|
||||
ns = _clean_params(params_kw)
|
||||
name = str(ns.get('script_name') or '').strip()
|
||||
err = _check_name(name)
|
||||
if err:
|
||||
return {'code': 1, 'message': err, 'field': 'script_name', 'detail': ''}
|
||||
stype = str(ns.get('script_type') or SCRIPT_TYPE_PYTHON)
|
||||
err = _check_type(stype)
|
||||
if err:
|
||||
return {'code': 1, 'message': err, 'field': 'script_type', 'detail': ''}
|
||||
content = str(ns.get('content') or '')
|
||||
if not content.strip():
|
||||
return {'code': 1, 'message': 'content is required', 'field': 'content', 'detail': ''}
|
||||
ok, msg = _validate_content(stype, content)
|
||||
if not ok:
|
||||
return {'code': 1, 'message': msg, 'field': 'content', 'detail': ''}
|
||||
now = curDateString()
|
||||
row = {
|
||||
'id': data.get('id') or getID(),
|
||||
rec = {
|
||||
'id': getID(),
|
||||
'script_name': name,
|
||||
'script_type': stype,
|
||||
'content': content,
|
||||
'description': (data.get('description') or '')[:255],
|
||||
'status': status,
|
||||
'description': str(ns.get('description') or '')[:255],
|
||||
'status': str(ns.get('status') or STATUS_ENABLED),
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
}
|
||||
dbname = _dbname()
|
||||
try:
|
||||
async with DBPools().sqlorContext(_dbname()) as sor:
|
||||
await sor.C(TABLE, row)
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.C(TABLE, rec)
|
||||
except Exception as e:
|
||||
exception('script_engine.create_script error: %s' % e)
|
||||
return _err(1, 'create failed: %s' % e)
|
||||
return _ok({'id': row['id']})
|
||||
debug('script_engine create_script error: %s' % str(e))
|
||||
return {'code': 1, 'message': str(e), 'field': '', 'detail': ''}
|
||||
return {'code': 0, 'message': 'ok', 'data': {'id': rec['id']}}
|
||||
|
||||
|
||||
async def update_script(request, ns):
|
||||
data = _clean_ns(ns)
|
||||
sid = data.get('id') or ''
|
||||
async def update_script(request, params_kw):
|
||||
ns = _clean_params(params_kw)
|
||||
sid = str(ns.get('id') or '').strip()
|
||||
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(),
|
||||
}
|
||||
return {'code': 1, 'message': 'id is required', 'field': 'id', 'detail': ''}
|
||||
upd = {'id': sid, 'updated_at': curDateString()}
|
||||
if ns.get('script_name') is not None:
|
||||
name = str(ns.get('script_name') or '').strip()
|
||||
err = _check_name(name)
|
||||
if err:
|
||||
return {'code': 1, 'message': err, 'field': 'script_name', 'detail': ''}
|
||||
upd['script_name'] = name
|
||||
if ns.get('script_type') is not None:
|
||||
stype = str(ns.get('script_type'))
|
||||
err = _check_type(stype)
|
||||
if err:
|
||||
return {'code': 1, 'message': err, 'field': 'script_type', 'detail': ''}
|
||||
upd['script_type'] = stype
|
||||
if ns.get('content') is not None:
|
||||
stype = str(ns.get('script_type') or upd.get('script_type') or SCRIPT_TYPE_PYTHON)
|
||||
content = str(ns.get('content') or '')
|
||||
if not content.strip():
|
||||
return {'code': 1, 'message': 'content is required', 'field': 'content', 'detail': ''}
|
||||
ok, msg = _validate_content(stype, content)
|
||||
if not ok:
|
||||
return {'code': 1, 'message': msg, 'field': 'content', 'detail': ''}
|
||||
upd['content'] = content
|
||||
if ns.get('description') is not None:
|
||||
upd['description'] = str(ns.get('description'))[:255]
|
||||
if ns.get('status') is not None:
|
||||
upd['status'] = str(ns.get('status'))
|
||||
dbname = _dbname()
|
||||
try:
|
||||
async with DBPools().sqlorContext(_dbname()) as sor:
|
||||
await sor.U(TABLE, {'id': sid, **row})
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.U(TABLE, upd)
|
||||
except Exception as e:
|
||||
exception('script_engine.update_script error: %s' % e)
|
||||
return _err(1, 'update failed: %s' % e)
|
||||
return _ok({'id': sid})
|
||||
debug('script_engine update_script error: %s' % str(e))
|
||||
return {'code': 1, 'message': str(e), 'field': '', 'detail': ''}
|
||||
return {'code': 0, 'message': 'ok', 'data': {'id': sid}}
|
||||
|
||||
|
||||
async def delete_script(request, ns):
|
||||
data = _clean_ns(ns)
|
||||
sid = data.get('id') or ''
|
||||
async def delete_script(request, params_kw):
|
||||
ns = _clean_params(params_kw)
|
||||
sid = str(ns.get('id') or '').strip()
|
||||
if not sid:
|
||||
return _err(1, 'id is required', 'id')
|
||||
return {'code': 1, 'message': 'id is required', 'field': 'id', 'detail': ''}
|
||||
dbname = _dbname()
|
||||
try:
|
||||
async with DBPools().sqlorContext(_dbname()) as sor:
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.D(TABLE, {'id': sid})
|
||||
except Exception as e:
|
||||
exception('script_engine.delete_script error: %s' % e)
|
||||
return _err(1, 'delete failed: %s' % e)
|
||||
return _ok({'id': sid})
|
||||
debug('script_engine delete_script error: %s' % str(e))
|
||||
return {'code': 1, 'message': str(e), 'field': '', 'detail': ''}
|
||||
return {'code': 0, 'message': 'ok', 'data': {'id': sid}}
|
||||
|
||||
|
||||
async def get_script(request, ns):
|
||||
data = _clean_ns(ns)
|
||||
sid = data.get('id') or ''
|
||||
async def get_script(request, params_kw):
|
||||
ns = _clean_params(params_kw)
|
||||
sid = str(ns.get('id') or '').strip()
|
||||
if not sid:
|
||||
return _err(1, 'id is required', 'id')
|
||||
return {'code': 1, 'message': 'id is required', 'field': 'id', 'detail': ''}
|
||||
dbname = _dbname()
|
||||
try:
|
||||
async with DBPools().sqlorContext(_dbname()) as sor:
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
recs = await sor.R(TABLE, {'id': sid})
|
||||
except Exception as e:
|
||||
exception('script_engine.get_script error: %s' % e)
|
||||
return _err(1, 'get failed: %s' % e)
|
||||
debug('script_engine get_script error: %s' % str(e))
|
||||
return {'code': 1, 'message': str(e), 'field': '', 'detail': ''}
|
||||
if not recs:
|
||||
return _err(1, 'script not found', 'id')
|
||||
return {'code': 1, 'message': 'script not found', 'field': 'id', 'detail': ''}
|
||||
r = recs[0]
|
||||
return _ok({
|
||||
return {'code': 0, 'message': 'ok', 'data': {
|
||||
'id': r.id,
|
||||
'script_name': r.script_name,
|
||||
'script_type': r.script_type,
|
||||
'content': r.content,
|
||||
'description': r.description,
|
||||
'description': getattr(r, 'description', ''),
|
||||
'status': r.status,
|
||||
'created_at': r.created_at,
|
||||
'updated_at': r.updated_at,
|
||||
})
|
||||
}}
|
||||
|
||||
|
||||
async def list_scripts(request, ns):
|
||||
data = _clean_ns(ns)
|
||||
dbname = _dbname()
|
||||
async def list_scripts(request, params_kw):
|
||||
ns = _clean_params(params_kw)
|
||||
try:
|
||||
page = int(data.get('page') or 1)
|
||||
rows = int(data.get('rows') or 20)
|
||||
page = int(ns.get('page') or 1)
|
||||
except (TypeError, ValueError):
|
||||
page, rows = 1, 20
|
||||
if page < 1:
|
||||
page = 1
|
||||
if rows < 1:
|
||||
try:
|
||||
rows = int(ns.get('rows') or ns.get('pagerows') or 20)
|
||||
except (TypeError, ValueError):
|
||||
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 + '%'
|
||||
page = max(page, 1)
|
||||
rows = min(max(rows, 1), 500)
|
||||
offset = (page - 1) * rows
|
||||
conds = []
|
||||
vals = {}
|
||||
name = str(ns.get('script_name') or '').strip()
|
||||
if name:
|
||||
conds.append('script_name like ${script_name}$')
|
||||
vals['script_name'] = '%' + name + '%'
|
||||
stype = str(ns.get('script_type') or '').strip()
|
||||
if stype:
|
||||
conds.append('script_type = ${script_type}$')
|
||||
vals['script_type'] = stype
|
||||
status = str(ns.get('status') or '').strip()
|
||||
if status:
|
||||
conds.append('status = ${status}$')
|
||||
vals['status'] = status
|
||||
where = ''
|
||||
if conds:
|
||||
where = ' where ' + ' and '.join(conds)
|
||||
dbname = _dbname()
|
||||
try:
|
||||
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)
|
||||
cnts = await sor.sqlExe('select count(*) as cnt from ' + TABLE + where, vals)
|
||||
total = int(cnts[0].cnt) if cnts else 0
|
||||
recs = await sor.sqlExe(
|
||||
'select id, script_name, script_type, description, status, '
|
||||
'created_at, updated_at from ' + TABLE + where +
|
||||
' order by created_at desc limit %d offset %d' % (rows, offset),
|
||||
vals)
|
||||
items = [{
|
||||
'id': r.id,
|
||||
'script_name': r.script_name,
|
||||
'script_type': r.script_type,
|
||||
'description': getattr(r, 'description', ''),
|
||||
'status': r.status,
|
||||
'created_at': r.created_at,
|
||||
'updated_at': r.updated_at,
|
||||
} for r in recs]
|
||||
except Exception as e:
|
||||
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}}
|
||||
debug('script_engine list_scripts error: %s' % str(e))
|
||||
return {'code': 1, 'message': str(e), 'field': '', 'detail': ''}
|
||||
return {'code': 0, 'message': 'ok', 'data': {'list': items, 'total': total}}
|
||||
|
||||
|
||||
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})
|
||||
|
||||
|
||||
async def execute_script(request, ns):
|
||||
"""Execute a script by id OR by inline content+script_type.
|
||||
|
||||
Python scripts receive the business params (everything except control
|
||||
fields) as a ``params`` dict in the namespace, so scripts can read input
|
||||
via ``params.get('key')``.
|
||||
"""
|
||||
data = _clean_ns(ns)
|
||||
async def _execute_sql(sql, params):
|
||||
dbname = _dbname()
|
||||
sid = (data.get('id') or '').strip()
|
||||
try:
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
recs = await sor.sqlExe(sql, dict(params or {}))
|
||||
rows = list(recs)
|
||||
except Exception as e:
|
||||
debug('script_engine execute_sql error: %s' % str(e))
|
||||
return {'code': 1, 'message': str(e), 'field': '', 'detail': ''}
|
||||
return {'code': 0, 'message': 'ok', 'data': {'rows': rows, 'total': len(rows)}}
|
||||
|
||||
|
||||
async def execute_script(request, params_kw):
|
||||
"""Execute a script by id, or by content + script_type (validated first)."""
|
||||
ns = _clean_params(params_kw)
|
||||
sid = str(ns.get('id') or '').strip()
|
||||
content = ns.get('content')
|
||||
stype = str(ns.get('script_type') or SCRIPT_TYPE_PYTHON)
|
||||
params = ns.get('params') or {}
|
||||
if sid:
|
||||
dbname = _dbname()
|
||||
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)
|
||||
debug('script_engine execute_script load error: %s' % str(e))
|
||||
return {'code': 1, 'message': str(e), 'field': '', 'detail': ''}
|
||||
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:
|
||||
ctrl = {'id', 'content', 'script_type', 'page', 'rows', 'keyword'}
|
||||
params = {k: v for k, v in data.items() if k not in ctrl}
|
||||
out = engine.execute_python(content, input_ns={'params': params})
|
||||
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})
|
||||
return {'code': 1, 'message': 'script not found', 'field': 'id', 'detail': ''}
|
||||
rec = recs[0]
|
||||
if str(rec.status) != STATUS_ENABLED:
|
||||
return {'code': 1, 'message': 'script is disabled', 'field': 'status', 'detail': ''}
|
||||
content = rec.content
|
||||
stype = str(rec.script_type)
|
||||
err = _check_type(stype)
|
||||
if err:
|
||||
return {'code': 1, 'message': err, 'field': 'script_type', 'detail': ''}
|
||||
if content is None or not str(content).strip():
|
||||
return {'code': 1, 'message': 'content is required', 'field': 'content', 'detail': ''}
|
||||
ok, msg = _validate_content(stype, str(content))
|
||||
if not ok:
|
||||
return {'code': 1, 'message': msg, 'field': 'content', 'detail': ''}
|
||||
if stype == SCRIPT_TYPE_PYTHON:
|
||||
return execute_python(str(content), params)
|
||||
return await _execute_sql(str(content), params)
|
||||
|
||||
|
||||
async def validate_script_api(request, params_kw):
|
||||
"""Validate only - does not save to db and does not execute."""
|
||||
ns = _clean_params(params_kw)
|
||||
content = str(ns.get('content') or '')
|
||||
if not content.strip():
|
||||
return {'code': 1, 'message': 'content is required', 'field': 'content', 'detail': ''}
|
||||
stype = str(ns.get('script_type') or SCRIPT_TYPE_PYTHON)
|
||||
err = _check_type(stype)
|
||||
if err:
|
||||
return {'code': 1, 'message': err, 'field': 'script_type', 'detail': ''}
|
||||
ok, msg = _validate_content(stype, content)
|
||||
if not ok:
|
||||
return {'code': 1, 'message': msg, 'field': 'content', 'detail': ''}
|
||||
return {'code': 0, 'message': 'ok', 'data': {'valid': True}}
|
||||
|
||||
|
||||
# plural aliases (CRUD framework convention: dspy wrappers use plural names)
|
||||
create_scripts = create_script
|
||||
update_scripts = update_script
|
||||
delete_scripts = delete_script
|
||||
|
||||
|
||||
def load_script_engine(env=None):
|
||||
"""Register module functions with ServerEnv (the ONLY integration point)."""
|
||||
env = env or ServerEnv()
|
||||
"""Register all script_engine functions to ServerEnv."""
|
||||
if env is None:
|
||||
env = ServerEnv()
|
||||
env.create_script = create_script
|
||||
env.create_scripts = create_script
|
||||
env.create_scripts = create_scripts
|
||||
env.update_script = update_script
|
||||
env.update_scripts = update_script
|
||||
env.update_scripts = update_scripts
|
||||
env.delete_script = delete_script
|
||||
env.delete_scripts = delete_script
|
||||
env.delete_scripts = delete_scripts
|
||||
env.get_script = get_script
|
||||
env.list_scripts = list_scripts
|
||||
env.execute_script = execute_script
|
||||
env.validate_script_api = validate_script_api
|
||||
debug('script_engine module loaded')
|
||||
return env
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
# execute_script.dspy - alias endpoint for script execution
|
||||
debug('execute_script.dspy: START params_kw=%s' % dict(params_kw))
|
||||
result = await execute_script(request, params_kw)
|
||||
return result
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
result = []
|
||||
# get_search_script_type.dspy - dropdown for script_type (appcodes_kv parentid=script_type)
|
||||
debug('get_search_script_type.dspy: START')
|
||||
try:
|
||||
result = [{'value': '', 'text': '全部'}, {'value': '0', 'text': 'Python'}, {'value': '1', 'text': 'SQL'}]
|
||||
async with get_sor_context(request._run_ns, 'appbase') as sor:
|
||||
recs = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='script_type' order by k", {})
|
||||
return json.dumps([{'value': '', 'text': '全部'}] + list(recs), ensure_ascii=False)
|
||||
except Exception as e:
|
||||
debug('get_search_script_type error: %s' % e)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
debug('get_search_script_type.dspy error: %s' % str(e))
|
||||
return json.dumps([{'value': '', 'text': '全部'}], ensure_ascii=False)
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
result = []
|
||||
# get_search_status.dspy - dropdown for status (appcodes_kv parentid=script_status)
|
||||
debug('get_search_status.dspy: START')
|
||||
try:
|
||||
result = [{'value': '', 'text': '全部'}, {'value': '1', 'text': '启用'}, {'value': '0', 'text': '停用'}]
|
||||
async with get_sor_context(request._run_ns, 'appbase') as sor:
|
||||
recs = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='script_status' order by k", {})
|
||||
return json.dumps([{'value': '', 'text': '全部'}] + list(recs), ensure_ascii=False)
|
||||
except Exception as e:
|
||||
debug('get_search_status error: %s' % e)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
debug('get_search_status.dspy error: %s' % str(e))
|
||||
return json.dumps([{'value': '', 'text': '全部'}], ensure_ascii=False)
|
||||
|
||||
@ -1,2 +1,6 @@
|
||||
# script_create.dspy - create script (validates syntax before save)
|
||||
debug('script_create.dspy: START params_kw=%s' % dict(params_kw))
|
||||
result = await create_script(request, params_kw)
|
||||
return result
|
||||
if result.get('code') == 0:
|
||||
return {'status': 'ok', 'message': 'ok', 'data': result.get('data', {})}
|
||||
return {'status': 'error', 'message': result.get('message', 'create failed'), 'field': result.get('field', ''), 'data': result}
|
||||
|
||||
@ -1,2 +1,6 @@
|
||||
# script_delete.dspy - delete script by id
|
||||
debug('script_delete.dspy: START params_kw=%s' % dict(params_kw))
|
||||
result = await delete_script(request, params_kw)
|
||||
return result
|
||||
if result.get('code') == 0:
|
||||
return {'status': 'ok', 'message': 'ok', 'data': result.get('data', {})}
|
||||
return {'status': 'error', 'message': result.get('message', 'delete failed'), 'field': result.get('field', ''), 'data': result}
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
# script_execute.dspy - execute script by id or content+script_type
|
||||
debug('script_execute.dspy: START params_kw=%s' % dict(params_kw))
|
||||
result = await execute_script(request, params_kw)
|
||||
return result
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
# script_get.dspy - get script detail by id
|
||||
debug('script_get.dspy: START params_kw=%s' % dict(params_kw))
|
||||
result = await get_script(request, params_kw)
|
||||
return result
|
||||
|
||||
@ -1,2 +1,7 @@
|
||||
# script_list.dspy - paginated script list (returns DataViewer rows/total)
|
||||
debug('script_list.dspy: START params_kw=%s' % dict(params_kw))
|
||||
result = await list_scripts(request, params_kw)
|
||||
return result
|
||||
if result.get('code') == 0:
|
||||
data = result.get('data', {})
|
||||
return {'status': 'ok', 'rows': data.get('list', []), 'total': data.get('total', 0)}
|
||||
return {'status': 'error', 'message': result.get('message', 'list failed'), 'rows': [], 'total': 0}
|
||||
|
||||
@ -1,2 +1,6 @@
|
||||
# script_update.dspy - update script (validates syntax before save)
|
||||
debug('script_update.dspy: START params_kw=%s' % dict(params_kw))
|
||||
result = await update_script(request, params_kw)
|
||||
return result
|
||||
if result.get('code') == 0:
|
||||
return {'status': 'ok', 'message': 'ok', 'data': result.get('data', {})}
|
||||
return {'status': 'error', 'message': result.get('message', 'update failed'), 'field': result.get('field', ''), 'data': result}
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
# script_validate.dspy - validate script content only (no save, no execute)
|
||||
debug('script_validate.dspy: START params_kw=%s' % dict(params_kw))
|
||||
result = await validate_script_api(request, params_kw)
|
||||
return result
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
# validate_script.dspy - alias endpoint for script validation
|
||||
debug('validate_script.dspy: START params_kw=%s' % dict(params_kw))
|
||||
result = await validate_script_api(request, params_kw)
|
||||
return result
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user