first commit
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
"""
|
||||
yida_page_schema.py — 宜搭自定义页面 schema 处理(builder + extractor)。
|
||||
|
||||
公开 API:
|
||||
Builder:
|
||||
- build_schema_content(source_code, compiled_code, form_uuid, existing_data_source=None) -> str
|
||||
- build_default_page_data_source(form_uuid) -> dict
|
||||
- merge_page_data_source(existing_data_source, generated_data_source) -> dict
|
||||
Extractor:
|
||||
- extract_source_code(schema) -> str
|
||||
- extract_compiled_code(schema) -> str
|
||||
- inject_source_code(schema, new_source, new_compiled) -> str
|
||||
|
||||
支持三种 schema 格式:
|
||||
1. 标准格式:actions.module.source / actions.module.compiled
|
||||
2. YidaCodeCanvas 格式:pages[0].componentsTree[*].props.code / runtimeCode
|
||||
3. YidaAICanvas 格式:同 YidaCodeCanvas 布局
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
|
||||
|
||||
# region: schema_builder
|
||||
# ---------------------------------------------------------------------------
|
||||
# 宜搭页面 schema JSON 外壳构建
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _create_node_id_generator():
|
||||
counter = [0]
|
||||
|
||||
def next_node_id():
|
||||
counter[0] += 1
|
||||
ts = _base36(int(time.time() * 1000))
|
||||
return 'node_oc' + ts + _base36(counter[0])
|
||||
|
||||
return next_node_id
|
||||
|
||||
|
||||
def _base36(n):
|
||||
if n == 0:
|
||||
return '0'
|
||||
chars = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
result = []
|
||||
while n > 0:
|
||||
result.append(chars[n % 36])
|
||||
n //= 36
|
||||
return ''.join(reversed(result))
|
||||
|
||||
|
||||
def _generate_suffix():
|
||||
ts = _base36(int(time.time() * 1000))
|
||||
rand = _base36(random.randint(0, 36 ** 6))
|
||||
return ts + rand.ljust(6, '0')[:6]
|
||||
|
||||
|
||||
def _get_global_data_source_fit_config():
|
||||
fit_compiled = (
|
||||
"'use strict';\n\nvar __preParser__ = function fit(response) {\n"
|
||||
" var content = response.content !== undefined ? response.content : response;\n"
|
||||
" var error = {\n"
|
||||
" message: response.errorMsg || response.errors && response.errors[0] "
|
||||
"&& response.errors[0].msg || response.content || "
|
||||
"'远程数据源请求出错,success is false'\n"
|
||||
" };\n var success = true;\n"
|
||||
" if (response.success !== undefined) {\n success = response.success;\n"
|
||||
" } else if (response.hasError !== undefined) {\n success = !response.hasError;\n"
|
||||
" }\n return {\n content: content,\n success: success,\n error: error\n };\n};"
|
||||
)
|
||||
fit_source = (
|
||||
"function fit(response) {\r\n"
|
||||
" const content = (response.content !== undefined) ? response.content : response;\r\n"
|
||||
" const error = {\r\n"
|
||||
" message: response.errorMsg ||\r\n"
|
||||
" (response.errors && response.errors[0] && response.errors[0].msg) ||\r\n"
|
||||
" response.content || '远程数据源请求出错,success is false',\r\n"
|
||||
" };\r\n"
|
||||
" let success = true;\r\n"
|
||||
" if (response.success !== undefined) {\r\n"
|
||||
" success = response.success;\r\n"
|
||||
" } else if (response.hasError !== undefined) {\r\n"
|
||||
" success = !response.hasError;\r\n"
|
||||
" }\r\n"
|
||||
" return {\r\n"
|
||||
" content,\r\n"
|
||||
" success,\r\n"
|
||||
" error,\r\n"
|
||||
" };\r\n"
|
||||
"}"
|
||||
)
|
||||
return {
|
||||
'fit': {
|
||||
'compiled': fit_compiled,
|
||||
'source': fit_source,
|
||||
'type': 'js',
|
||||
'error': {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_default_page_data_source(form_uuid):
|
||||
url_params = {
|
||||
'id': 'VCB660714833IBHEOXK376TA7XJH2AXUWR8MMW',
|
||||
'name': 'urlParams',
|
||||
'description': '当前页面地址的参数:如 aliwork.com/APP_XXX/workbench?id=1&name=宜搭,'
|
||||
'可通过 this.state.urlParams.name 获取到宜搭',
|
||||
'formUuid': form_uuid,
|
||||
'protocal': 'URI',
|
||||
'isReadonly': True,
|
||||
}
|
||||
timestamp = {
|
||||
'id': '',
|
||||
'name': 'timestamp',
|
||||
'description': '',
|
||||
'formUuid': form_uuid,
|
||||
'protocal': 'VALUE',
|
||||
'initialData': '',
|
||||
}
|
||||
return {
|
||||
'offline': [],
|
||||
'globalConfig': _get_global_data_source_fit_config(),
|
||||
'online': [url_params, timestamp],
|
||||
'list': [url_params, timestamp],
|
||||
'sync': True,
|
||||
}
|
||||
|
||||
|
||||
def _is_built_in_page_data_source(item):
|
||||
if not item or not isinstance(item, dict):
|
||||
return False
|
||||
return item.get('name') in ('urlParams', 'timestamp')
|
||||
|
||||
|
||||
def _get_data_source_identity(item):
|
||||
if not item or not isinstance(item, dict):
|
||||
return ''
|
||||
if _is_built_in_page_data_source(item):
|
||||
return 'builtin:' + item['name']
|
||||
if item.get('id'):
|
||||
return 'id:' + item['id']
|
||||
if item.get('name') and item.get('protocal'):
|
||||
return 'name:' + item['name'] + '|protocal:' + item['protocal']
|
||||
if item.get('name'):
|
||||
return 'name:' + item['name']
|
||||
return json.dumps(item, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
|
||||
def _merge_data_source_array(existing_items, generated_items):
|
||||
merged = copy.deepcopy(existing_items) if isinstance(existing_items, list) else []
|
||||
seen = set()
|
||||
for item in merged:
|
||||
identity = _get_data_source_identity(item)
|
||||
if identity:
|
||||
seen.add(identity)
|
||||
|
||||
for item in (generated_items if isinstance(generated_items, list) else []):
|
||||
identity = _get_data_source_identity(item)
|
||||
if not identity or identity not in seen:
|
||||
merged.append(copy.deepcopy(item))
|
||||
if identity:
|
||||
seen.add(identity)
|
||||
return merged
|
||||
|
||||
|
||||
def merge_page_data_source(existing_data_source, generated_data_source):
|
||||
if not existing_data_source or not isinstance(existing_data_source, dict):
|
||||
return copy.deepcopy(generated_data_source)
|
||||
|
||||
existing = copy.deepcopy(existing_data_source)
|
||||
generated = copy.deepcopy(generated_data_source) if generated_data_source else {}
|
||||
merged = {**generated, **existing}
|
||||
|
||||
merged['offline'] = _merge_data_source_array(existing.get('offline'), generated.get('offline'))
|
||||
merged['online'] = _merge_data_source_array(existing.get('online'), generated.get('online'))
|
||||
merged['list'] = _merge_data_source_array(existing.get('list'), generated.get('list'))
|
||||
merged['globalConfig'] = {
|
||||
**(generated.get('globalConfig') or {}),
|
||||
**(existing.get('globalConfig') or {}),
|
||||
}
|
||||
merged['sync'] = existing.get('sync') if existing.get('sync') is not None else generated.get('sync')
|
||||
return merged
|
||||
|
||||
|
||||
def build_schema_content(source_code, compiled_code, form_uuid, existing_data_source=None):
|
||||
"""把 sourceCode + compiledCode 包装成完整的宜搭自定义页面 schema JSON 字符串。"""
|
||||
next_node_id = _create_node_id_generator()
|
||||
|
||||
constructor_code = (
|
||||
"function constructor() {\n"
|
||||
"var module = { exports: {} };\n"
|
||||
"var _this = this;\n"
|
||||
"this.__initMethods__(module.exports, module);\n"
|
||||
"Object.keys(module.exports).forEach(function(item) {\n"
|
||||
" if(typeof module.exports[item] === 'function'){\n"
|
||||
" _this[item] = module.exports[item];\n"
|
||||
" }\n"
|
||||
"});\n\n"
|
||||
"}"
|
||||
)
|
||||
|
||||
page_data_source = merge_page_data_source(
|
||||
existing_data_source,
|
||||
build_default_page_data_source(form_uuid),
|
||||
)
|
||||
|
||||
schema = {
|
||||
'schemaType': 'superform',
|
||||
'schemaVersion': '5.0',
|
||||
'pages': [
|
||||
{
|
||||
'utils': [
|
||||
{
|
||||
'name': 'legaoBuiltin',
|
||||
'type': 'npm',
|
||||
'content': {
|
||||
'package': '@ali/vu-legao-builtin',
|
||||
'version': '3.0.0',
|
||||
'exportName': 'legaoBuiltin',
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': 'yidaPlugin',
|
||||
'type': 'npm',
|
||||
'content': {
|
||||
'package': '@ali/vu-yida-plugin',
|
||||
'version': '1.1.0',
|
||||
'exportName': 'yidaPlugin',
|
||||
},
|
||||
},
|
||||
],
|
||||
'componentsMap': [
|
||||
{'package': '@ali/vc-deep-yida', 'version': '1.5.169', 'componentName': 'RootHeader'},
|
||||
{'package': '@ali/vc-deep-yida', 'version': '1.5.169', 'componentName': 'Jsx'},
|
||||
{'package': '@ali/vc-deep-yida', 'version': '1.5.169', 'componentName': 'RootContent'},
|
||||
{'package': '@ali/vc-deep-yida', 'version': '1.5.169', 'componentName': 'RootFooter'},
|
||||
{'package': '@ali/vc-deep-yida', 'version': '1.5.169', 'componentName': 'Page'},
|
||||
],
|
||||
'componentsTree': [
|
||||
{
|
||||
'componentName': 'Page',
|
||||
'id': next_node_id(),
|
||||
'props': {
|
||||
'contentBgColor': 'white',
|
||||
'pageStyle': {'backgroundColor': '#f2f3f5'},
|
||||
'contentMargin': '0',
|
||||
'contentPadding': '0',
|
||||
'showTitle': False,
|
||||
'contentPaddingMobile': '0',
|
||||
'templateVersion': '1.0.0',
|
||||
'contentMarginMobile': '0',
|
||||
'className': 'page_' + _generate_suffix(),
|
||||
'contentBgColorMobile': 'white',
|
||||
},
|
||||
'condition': True,
|
||||
'css': (
|
||||
'body{background-color:#f2f3f5}'
|
||||
'.vc-page-yida-page{--yida-form-content-padding:0;'
|
||||
'--yida-form-content-margin:0;--yida-layout-padding:0}'
|
||||
'.vc-deep-container-entry.vc-rootcontent{padding:0!important;'
|
||||
'margin-top:0!important;margin-right:0!important;'
|
||||
'margin-bottom:0!important;margin-left:0!important}'
|
||||
),
|
||||
'methods': {
|
||||
'__initMethods__': {
|
||||
'type': 'js',
|
||||
'source': 'function (exports, module) { /*set actions code here*/ }',
|
||||
'compiled': 'function (exports, module) { /*set actions code here*/ }',
|
||||
},
|
||||
},
|
||||
'dataSource': page_data_source,
|
||||
'lifeCycles': {
|
||||
'constructor': {
|
||||
'type': 'js',
|
||||
'compiled': constructor_code,
|
||||
'source': constructor_code,
|
||||
},
|
||||
'componentWillUnmount': {
|
||||
'name': 'didUnmount',
|
||||
'id': 'didUnmount',
|
||||
'type': 'actionRef',
|
||||
'params': {},
|
||||
},
|
||||
'componentDidMount': {
|
||||
'name': 'didMount',
|
||||
'id': 'didMount',
|
||||
'params': {},
|
||||
'type': 'actionRef',
|
||||
},
|
||||
},
|
||||
'hidden': False,
|
||||
'title': '',
|
||||
'isLocked': False,
|
||||
'conditionGroup': '',
|
||||
'children': [
|
||||
{
|
||||
'componentName': 'RootHeader',
|
||||
'id': next_node_id(),
|
||||
'props': {},
|
||||
'condition': True,
|
||||
'hidden': False,
|
||||
'title': '',
|
||||
'isLocked': False,
|
||||
'conditionGroup': '',
|
||||
},
|
||||
{
|
||||
'componentName': 'RootContent',
|
||||
'id': next_node_id(),
|
||||
'props': {},
|
||||
'condition': True,
|
||||
'hidden': False,
|
||||
'title': '',
|
||||
'isLocked': False,
|
||||
'conditionGroup': '',
|
||||
'children': [
|
||||
{
|
||||
'componentName': 'Jsx',
|
||||
'id': next_node_id(),
|
||||
'props': {
|
||||
'render': {
|
||||
'type': 'js',
|
||||
'compiled': (
|
||||
'function main(){\n \n "use strict";\n\n'
|
||||
'var __compiledFunc__ = function render() {\n'
|
||||
' return this.renderJsx();\n};\n'
|
||||
' return __compiledFunc__.apply(this, arguments);\n }'
|
||||
),
|
||||
'source': (
|
||||
'function render() {\n'
|
||||
' return this.renderJsx();\n}'
|
||||
),
|
||||
'error': {},
|
||||
},
|
||||
'__style__': {},
|
||||
'fieldId': 'jsx_' + _generate_suffix(),
|
||||
},
|
||||
'condition': True,
|
||||
'hidden': False,
|
||||
'title': '',
|
||||
'isLocked': False,
|
||||
'conditionGroup': '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'componentName': 'RootFooter',
|
||||
'id': next_node_id(),
|
||||
'props': {},
|
||||
'condition': True,
|
||||
'hidden': False,
|
||||
'title': '',
|
||||
'isLocked': False,
|
||||
'conditionGroup': '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'id': form_uuid,
|
||||
'connectComponent': [],
|
||||
},
|
||||
],
|
||||
'actions': {
|
||||
'module': {
|
||||
'compiled': compiled_code,
|
||||
'source': source_code,
|
||||
},
|
||||
'type': 'FUNCTION',
|
||||
'list': [
|
||||
{'id': 'getCustomState', 'title': 'getCustomState'},
|
||||
{'id': 'setCustomState', 'title': 'setCustomState'},
|
||||
{'id': 'forceUpdate', 'title': 'forceUpdate'},
|
||||
{'id': 'didMount', 'title': 'didMount'},
|
||||
{'id': 'didUnmount', 'title': 'didUnmount'},
|
||||
{'id': 'renderJsx', 'title': 'renderJsx'},
|
||||
],
|
||||
},
|
||||
'config': {
|
||||
'connectComponent': [],
|
||||
},
|
||||
}
|
||||
|
||||
return json.dumps(schema, ensure_ascii=False)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region: schema_extractor
|
||||
# ---------------------------------------------------------------------------
|
||||
# 从 schema 中提取 / 注入源码(兼容标准 / YidaCodeCanvas / YidaAICanvas)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _parse_schema(schema):
|
||||
if isinstance(schema, str):
|
||||
return json.loads(schema)
|
||||
if isinstance(schema, dict):
|
||||
return schema
|
||||
raise TypeError(f"schema must be dict or JSON string, got {type(schema).__name__}")
|
||||
|
||||
|
||||
def _find_code_canvas_node(data: dict):
|
||||
"""在 componentsTree 中查找 YidaCodeCanvas 或 YidaAICanvas 节点"""
|
||||
try:
|
||||
root = data['pages'][0]['componentsTree'][0]
|
||||
found = _search_canvas_recursive(root)
|
||||
if found:
|
||||
return found
|
||||
except (KeyError, IndexError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _search_canvas_recursive(node: dict):
|
||||
if node.get('componentName') in ('YidaCodeCanvas', 'YidaAICanvas'):
|
||||
return node
|
||||
for child in node.get('children', []):
|
||||
found = _search_canvas_recursive(child)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def _detect_format(data: dict) -> str:
|
||||
"""检测 schema 格式:'standard' 或 'code_canvas'"""
|
||||
canvas = _find_code_canvas_node(data)
|
||||
if canvas and canvas.get('props', {}).get('code'):
|
||||
return 'code_canvas'
|
||||
|
||||
try:
|
||||
if data['actions']['module']['source']:
|
||||
return 'standard'
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
raise ValueError(
|
||||
"Cannot detect schema format: neither actions.module.source "
|
||||
"nor YidaCodeCanvas/YidaAICanvas.props.code found."
|
||||
)
|
||||
|
||||
|
||||
def extract_source_code(schema) -> str:
|
||||
"""从宜搭自定义页面 schema 提取原始 JSX 源码。"""
|
||||
data = _parse_schema(schema)
|
||||
fmt = _detect_format(data)
|
||||
|
||||
if fmt == 'code_canvas':
|
||||
canvas = _find_code_canvas_node(data)
|
||||
code = canvas.get('props', {}).get('code')
|
||||
if not code or not isinstance(code, str):
|
||||
raise ValueError("YidaCodeCanvas/YidaAICanvas.props.code is empty or not a string")
|
||||
return code
|
||||
|
||||
try:
|
||||
source = data['actions']['module']['source']
|
||||
except (KeyError, TypeError):
|
||||
raise ValueError("Cannot find actions.module.source")
|
||||
if not isinstance(source, str):
|
||||
raise ValueError(f"actions.module.source is not a string, got {type(source).__name__}")
|
||||
return source
|
||||
|
||||
|
||||
def extract_compiled_code(schema) -> str:
|
||||
"""从宜搭自定义页面 schema 提取编译后代码。"""
|
||||
data = _parse_schema(schema)
|
||||
fmt = _detect_format(data)
|
||||
|
||||
if fmt == 'code_canvas':
|
||||
canvas = _find_code_canvas_node(data)
|
||||
runtime_code = canvas.get('props', {}).get('runtimeCode')
|
||||
if not runtime_code or not isinstance(runtime_code, str):
|
||||
raise ValueError("YidaCodeCanvas/YidaAICanvas.props.runtimeCode is empty or not a string")
|
||||
return runtime_code
|
||||
|
||||
try:
|
||||
compiled = data['actions']['module']['compiled']
|
||||
except (KeyError, TypeError):
|
||||
raise ValueError("Cannot find actions.module.compiled")
|
||||
if not isinstance(compiled, str):
|
||||
raise ValueError(f"actions.module.compiled is not a string, got {type(compiled).__name__}")
|
||||
return compiled
|
||||
|
||||
|
||||
def inject_source_code(schema, new_source: str, new_compiled: str) -> str:
|
||||
"""将新的源码和编译产物注入 schema,返回更新后的 JSON 字符串。"""
|
||||
data = _parse_schema(schema)
|
||||
data = copy.deepcopy(data)
|
||||
fmt = _detect_format(data)
|
||||
|
||||
if fmt == 'code_canvas':
|
||||
canvas = _find_code_canvas_node(data)
|
||||
if not canvas:
|
||||
raise ValueError("Cannot find YidaCodeCanvas/YidaAICanvas node for injection")
|
||||
canvas['props']['code'] = new_source
|
||||
canvas['props']['runtimeCode'] = new_compiled
|
||||
else:
|
||||
if 'actions' not in data or not isinstance(data['actions'], dict):
|
||||
raise ValueError("Invalid schema: missing 'actions' key")
|
||||
if 'module' not in data['actions'] or not isinstance(data['actions']['module'], dict):
|
||||
raise ValueError("Invalid schema: missing 'actions.module' key")
|
||||
data['actions']['module']['source'] = new_source
|
||||
data['actions']['module']['compiled'] = new_compiled
|
||||
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
__all__ = [
|
||||
'build_schema_content',
|
||||
'build_default_page_data_source',
|
||||
'merge_page_data_source',
|
||||
'extract_source_code',
|
||||
'extract_compiled_code',
|
||||
'inject_source_code',
|
||||
]
|
||||
Reference in New Issue
Block a user