scense_drag/drag/init.py
2026-08-29 21:10:23 +08:00

539 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""drag 模块——拖拽编程画布。
提供:
- drag_graph 画布表blocks JSON + connections JSON + 编译产物 content
- drag_template 模板表
- 图校验validate_graph编译compile_graph块定义get_block_defs
- 画布 CRUD + graph-save保存即校验+编译) + compile + validate 端点函数
通过 load_drag() 挂载到 ServerEnv宿主应用 load 后 /drag/api/*.dspy 可用。
取库名一律 ServerEnv().get_module_dbname('drag'),禁止硬编码。
注意ahserver/appPublic 依赖全部为函数内惰性导入——保证 blocks/validator/
compiler 核心逻辑可在无宿主环境独立测试,部署时 ahserver 环境可用。
"""
import json
from .blocks import BLOCK_DEFS, get_block_defs
from .validator import validate_graph
from .compiler import compile_graph
def _server_env():
"""惰性获取 ServerEnv 单例(宿主环境)。"""
from ahserver.serverenv import ServerEnv
return ServerEnv()
def _now():
from appPublic.timeutils import curDateString
return curDateString()
def _new_id():
from appPublic.getID import getID
return getID()
def _dumps(obj):
from appPublic.jsonUtils import dumps
return dumps(obj)
async def get_drag_graph_dbname():
"""drag 业务库名(宿主 get_module_dbname 决定)。"""
try:
return _server_env().get_module_dbname('drag')
except Exception:
return 'scense'
# ═══════════════ 块定义(前端画布 + 校验 + 编译的唯一事实源) ═══════════════
async def get_block_defs_api():
"""返回 {categories, blocks} 供前端渲染左侧块面板。"""
return get_block_defs()
# ═══════════════ 画布 CRUD ═══════════════
async def list_drag_graphs(params=None):
"""分页列表:{list, total},支持 name/status 过滤 + sort/order。"""
params = params or {}
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
where, args = [], []
name = params.get('name')
status = params.get('status')
if name:
where.append('name LIKE %s')
args.append('%%%s%%' % name)
if status:
where.append('status = %s')
args.append(status)
wsql = (' WHERE ' + ' AND '.join(where)) if where else ''
ns = {'page': int(params.get('page', 1) or 1),
'rows': int(params.get('rows', 20) or 20),
'sort': params.get('sort', 'updated_at'),
'order': params.get('order', 'desc')}
recs = await sor.sqlPaging(
'SELECT id, name, description, status, created_at, updated_at '
'FROM drag_graph%s' % wsql, ns, args)
rows = [{'id': r.id, 'name': r.name, 'description': getattr(r, 'description', ''),
'status': r.status, 'created_at': str(getattr(r, 'created_at', '')),
'updated_at': str(getattr(r, 'updated_at', ''))} for r in recs]
return {'list': rows, 'total': ns.get('total', len(rows))}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '查询画布列表失败: %s' % e}
async def get_drag_graph(ns):
"""获取画布详情(含 blocks/connections 解析)。"""
gid = (ns or {}).get('id')
if not gid:
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
recs = await sor.R('drag_graph', {'id': gid})
if not recs:
return {'code': 'NOT_FOUND', 'message': '画布不存在: %s' % gid}
r = recs[0]
data = {'id': r.id, 'name': r.name, 'description': getattr(r, 'description', ''),
'status': r.status, 'created_at': str(getattr(r, 'created_at', '')),
'updated_at': str(getattr(r, 'updated_at', ''))}
try:
data['blocks'] = json.loads(r.blocks or '[]')
except Exception:
data['blocks'] = []
try:
data['connections'] = json.loads(r.connections or '[]')
except Exception:
data['connections'] = []
data['content'] = r.content or ''
return {'data': data}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '查询画布失败: %s' % e}
async def create_drag_graph(ns):
"""创建画布:校验 blocks/connections → 编译 → 落库。"""
name = (ns or {}).get('name')
if not name or not str(name).strip():
return {'code': 'PARAM_REQUIRED', 'message': '画布名称必填', 'field': 'name'}
if len(str(name)) > 100:
return {'code': 'FIELD_TOO_LONG', 'message': '画布名称不能超过 100 字符', 'field': 'name'}
blocks = ns.get('blocks')
conns = ns.get('connections')
if isinstance(blocks, str):
try:
blocks = json.loads(blocks)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
if isinstance(conns, str):
try:
conns = json.loads(conns)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
if blocks is None:
blocks = []
if conns is None:
conns = []
graph = {'blocks': blocks, 'connections': conns}
v = validate_graph(graph)
result = compile_graph(graph, {'name': name})
gid = _new_id()
content = result.get('content', '') if result.get('success') else ''
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
await sor.C('drag_graph', {
'id': gid, 'name': str(name).strip(),
'description': str(ns.get('description') or ''),
'blocks': _dumps(blocks), 'connections': _dumps(conns),
'content': content, 'status': '1',
'created_at': _now(), 'updated_at': _now()})
return {'success': True, 'id': gid, 'valid': v['valid'],
'errors': v['errors'], 'content': content}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '保存画布失败: %s' % e}
async def update_drag_graph(ns):
"""更新画布:校验+编译后写回 content。"""
gid = (ns or {}).get('id')
if not gid:
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
blocks = ns.get('blocks')
conns = ns.get('connections')
if isinstance(blocks, str):
try:
blocks = json.loads(blocks)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
if isinstance(conns, str):
try:
conns = json.loads(conns)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
if blocks is None:
blocks = []
if conns is None:
conns = []
graph = {'blocks': blocks, 'connections': conns}
v = validate_graph(graph)
result = compile_graph(graph, {'name': ns.get('name') or gid})
content = result.get('content', '') if result.get('success') else ''
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
recs = await sor.R('drag_graph', {'id': gid})
if not recs:
return {'code': 'NOT_FOUND', 'message': '画布不存在: %s' % gid}
up = {'blocks': _dumps(blocks), 'connections': _dumps(conns),
'content': content, 'updated_at': _now()}
if ns.get('name'):
up['name'] = str(ns['name']).strip()
if ns.get('description') is not None:
up['description'] = str(ns['description'])
if ns.get('status'):
up['status'] = str(ns['status'])
await sor.U('drag_graph', up, {'id': gid})
return {'success': True, 'id': gid, 'valid': v['valid'],
'errors': v['errors'], 'content': content}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '更新画布失败: %s' % e}
async def delete_drag_graph(ns):
"""删除画布。"""
gid = (ns or {}).get('id')
if not gid:
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
await sor.D('drag_graph', {'id': gid})
return {'success': True, 'id': gid}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '删除画布失败: %s' % e}
# ═══════════════ 画布保存(保存即校验+编译) ═══════════════
async def graph_save(ns):
"""保存画布新建或更新blocks/connections 校验+编译后落库。
ns: {id?, name, description?, blocks, connections}
返回 {success, id, valid, errors, content}
"""
gid = (ns or {}).get('id')
if gid:
return await update_drag_graph(ns)
return await create_drag_graph(ns)
# ═══════════════ 编译 / 校验 ═══════════════
async def graph_compile(ns):
"""编译画布 → script_engine 可执行脚本(不落库)。"""
blocks = (ns or {}).get('blocks')
conns = (ns or {}).get('connections')
if isinstance(blocks, str):
try:
blocks = json.loads(blocks)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
if isinstance(conns, str):
try:
conns = json.loads(conns)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
graph = {'blocks': blocks or [], 'connections': conns or []}
return compile_graph(graph, {'name': (ns or {}).get('name')})
async def graph_validate(ns):
"""校验画布 → {valid, errors, warnings}(不落库不编译)。"""
blocks = (ns or {}).get('blocks')
conns = (ns or {}).get('connections')
if isinstance(blocks, str):
try:
blocks = json.loads(blocks)
except Exception:
return {'valid': False, 'errors': [{'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}]}
if isinstance(conns, str):
try:
conns = json.loads(conns)
except Exception:
return {'valid': False, 'errors': [{'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}]}
return validate_graph({'blocks': blocks or [], 'connections': conns or []})
# ═══════════════ 编译产物发布到 script_engine ═══════════════
async def graph_publish(ns):
"""把画布编译产物写入 script_enginescript_type=0 Python 可执行脚本)。
ns: {id 画布id, script_name?}
返回 {success, script_id, script_name}
"""
gid = (ns or {}).get('id')
if not gid:
return {'code': 'PARAM_REQUIRED', 'message': '缺少画布 id', 'field': 'id'}
got = await get_drag_graph({'id': gid})
if got.get('code'):
return got
data = got['data']
result = compile_graph({'blocks': data['blocks'], 'connections': data['connections']},
{'name': data['name'], 'id': gid})
if not result.get('success'):
return {'code': 'COMPILE_ERROR', 'message': '画布校验未通过,无法发布',
'errors': result.get('errors', [])}
script_name = (ns or {}).get('script_name') or ('drag_%s' % data['name'])
se = _server_env()
try:
create_script = getattr(se, 'create_script', None) or getattr(se, 'create_scripts', None)
if create_script is None:
return {'code': 'MODULE_MISSING', 'message': 'script_engine 未挂载,无法发布'}
r = await create_script({'script_name': script_name, 'script_type': '0',
'content': result['content'],
'description': 'drag 画布编译产物 (graph=%s)' % gid})
return {'success': True, 'script_id': r.get('id'), 'script_name': script_name,
'script_result': r}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '发布到 script_engine 失败: %s' % e}
# ═══════════════ 模板 CRUD ═══════════════
async def list_drag_templates(params=None):
"""模板分页列表:{list, total}。"""
params = params or {}
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
where, args = [], []
name = params.get('name')
if name:
where.append('name LIKE %s')
args.append('%%%s%%' % name)
wsql = (' WHERE ' + ' AND '.join(where)) if where else ''
ns = {'page': int(params.get('page', 1) or 1),
'rows': int(params.get('rows', 20) or 20),
'sort': params.get('sort', 'updated_at'),
'order': params.get('order', 'desc')}
recs = await sor.sqlPaging(
'SELECT id, name, category, description, created_at FROM drag_template%s' % wsql,
ns, args)
rows = [{'id': r.id, 'name': r.name, 'category': r.category,
'description': getattr(r, 'description', ''),
'created_at': str(getattr(r, 'created_at', ''))} for r in recs]
return {'list': rows, 'total': ns.get('total', len(rows))}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '查询模板失败: %s' % e}
async def get_drag_template(ns):
"""模板详情(含 blocks/connections 解析)。"""
tid = (ns or {}).get('id')
if not tid:
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
recs = await sor.R('drag_template', {'id': tid})
if not recs:
return {'code': 'NOT_FOUND', 'message': '模板不存在: %s' % tid}
r = recs[0]
data = {'id': r.id, 'name': r.name, 'category': r.category,
'description': getattr(r, 'description', '')}
try:
data['blocks'] = json.loads(r.blocks or '[]')
except Exception:
data['blocks'] = []
try:
data['connections'] = json.loads(r.connections or '[]')
except Exception:
data['connections'] = []
return {'data': data}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '查询模板失败: %s' % e}
async def create_drag_template(ns):
"""创建模板blocks/connections 校验。"""
name = (ns or {}).get('name')
if not name or not str(name).strip():
return {'code': 'PARAM_REQUIRED', 'message': '模板名称必填', 'field': 'name'}
blocks = ns.get('blocks') or []
conns = ns.get('connections') or []
if isinstance(blocks, str):
try:
blocks = json.loads(blocks)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
if isinstance(conns, str):
try:
conns = json.loads(conns)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
v = validate_graph({'blocks': blocks, 'connections': conns})
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
tid = _new_id()
await sor.C('drag_template', {
'id': tid, 'name': str(name).strip(),
'category': str(ns.get('category') or '通用'),
'description': str(ns.get('description') or ''),
'blocks': _dumps(blocks), 'connections': _dumps(conns),
'created_at': _now(), 'updated_at': _now()})
return {'success': True, 'id': tid, 'valid': v['valid'], 'errors': v['errors']}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '保存模板失败: %s' % e}
async def update_drag_template(ns):
"""更新模板。"""
tid = (ns or {}).get('id')
if not tid:
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
blocks = ns.get('blocks')
conns = ns.get('connections')
if isinstance(blocks, str):
try:
blocks = json.loads(blocks)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'blocks 不是合法 JSON', 'field': 'blocks'}
if isinstance(conns, str):
try:
conns = json.loads(conns)
except Exception:
return {'code': 'PARAM_TYPE', 'message': 'connections 不是合法 JSON', 'field': 'connections'}
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
recs = await sor.R('drag_template', {'id': tid})
if not recs:
return {'code': 'NOT_FOUND', 'message': '模板不存在: %s' % tid}
up = {'updated_at': _now()}
if blocks is not None:
up['blocks'] = _dumps(blocks)
if conns is not None:
up['connections'] = _dumps(conns)
if ns.get('name'):
up['name'] = str(ns['name']).strip()
if ns.get('category'):
up['category'] = str(ns['category'])
if ns.get('description') is not None:
up['description'] = str(ns['description'])
await sor.U('drag_template', up, {'id': tid})
return {'success': True, 'id': tid}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '更新模板失败: %s' % e}
async def delete_drag_template(ns):
"""删除模板。"""
tid = (ns or {}).get('id')
if not tid:
return {'code': 'PARAM_REQUIRED', 'message': '缺少 id', 'field': 'id'}
dbname = await get_drag_graph_dbname()
try:
async with _server_env().sqlorContext(dbname) as sor:
await sor.D('drag_template', {'id': tid})
return {'success': True, 'id': tid}
except Exception as e:
return {'code': 'DB_ERROR', 'message': '删除模板失败: %s' % e}
async def template_use(ns):
"""应用模板按模板生成新画布。ns: {template_id, name} → 新画布 id。"""
tid = (ns or {}).get('template_id') or (ns or {}).get('id')
if not tid:
return {'code': 'PARAM_REQUIRED', 'message': '缺少 template_id', 'field': 'template_id'}
got = await get_drag_template({'id': tid})
if got.get('code'):
return got
tpl = got['data']
return await create_drag_graph({'name': ns.get('name') or (tpl['name'] + '_副本'),
'description': '由模板 %s 生成' % tpl['name'],
'blocks': tpl['blocks'], 'connections': tpl['connections']})
# ═══════════════ 实体选择器(下拉数据源) ═══════════════
async def get_entity_options(ns=None):
"""实体下拉选项 [{value, text}]。复用 entity 模块的实体列表。"""
se = _server_env()
list_entities = getattr(se, 'list_entities', None)
if list_entities is None:
return [{'value': 'hero', 'text': 'hero(示例)'}, {'value': 'coin', 'text': 'coin(示例)'}]
try:
r = await list_entities({'page': 1, 'rows': 500})
items = r.get('list') or r.get('data') or []
if isinstance(r, list):
items = r
out = []
for it in items:
if isinstance(it, dict):
out.append({'value': it.get('id'), 'text': it.get('name') or it.get('id')})
else:
out.append({'value': getattr(it, 'id', ''), 'text': getattr(it, 'name', '') or getattr(it, 'id', '')})
return out or [{'value': 'hero', 'text': 'hero(示例)'}, {'value': 'coin', 'text': 'coin(示例)'}]
except Exception:
return [{'value': 'hero', 'text': 'hero(示例)'}, {'value': 'coin', 'text': 'coin(示例)'}]
async def get_template_category_options(ns=None):
"""模板分类下拉。"""
return [{'value': '通用', 'text': '通用'}, {'value': '移动', 'text': '移动'},
{'value': '弹球', 'text': '弹球'}, {'value': '计时', 'text': '计时'}]
# ═══════════════ 辅助:脚本校验白名单(供 script_engine 执行 drag 产物) ═══════════════
def _action_whitelist():
"""drag 编译产物可调用的运行时动作白名单(宿主演绎层实现)。"""
return ['move', 'rotate', 'scale', 'set_property', 'play_animation',
'play_sound', 'show_hide', 'camera', 'wait']
def load_drag():
"""挂载 drag 模块函数到 ServerEnv宿主 init() 调用)。"""
env = _server_env()
env.get_block_defs_api = get_block_defs_api
env.list_drag_graphs = list_drag_graphs
env.get_drag_graph = get_drag_graph
env.create_drag_graph = create_drag_graph
env.update_drag_graph = update_drag_graph
env.delete_drag_graph = delete_drag_graph
env.graph_save = graph_save
env.graph_compile = graph_compile
env.graph_validate = graph_validate
env.graph_publish = graph_publish
env.list_drag_templates = list_drag_templates
env.get_drag_template = get_drag_template
env.create_drag_template = create_drag_template
env.update_drag_template = update_drag_template
env.delete_drag_template = delete_drag_template
env.template_use = template_use
env.get_entity_options = get_entity_options
env.get_template_category_options = get_template_category_options
# CRUD 复数别名CRUD 框架约定create_xxx/update_xxx/delete_xxx
env.create_drag_graphs = create_drag_graph
env.update_drag_graphs = update_drag_graph
env.delete_drag_graphs = delete_drag_graph
env.create_drag_templates = create_drag_template
env.update_drag_templates = update_drag_template
env.delete_drag_templates = delete_drag_template
# 内部导出(测试/扩展用)
env.drag_action_whitelist = _action_whitelist
env.drag_validate_graph = validate_graph
env.drag_compile_graph = compile_graph
print('[drag] load_drag() OK: %d block types registered' % len(BLOCK_DEFS))