mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-15 08:56:07 +00:00
0cde4c71cd
- Added support for select fields in Telegram adapter, including option extraction and callback handling. - Implemented form action processing for Telegram callbacks, improving user interaction feedback. - Introduced new helper functions for building keyboards and resolving select button actions in QQ Official adapter. - Enhanced DifyServiceAPIRunner to handle cumulative streaming responses and improve error handling during workflow resumes. - Added unit tests for new functionalities in Telegram and QQ Official adapters, ensuring robust behavior for select fields and form actions.
1112 lines
38 KiB
Python
1112 lines
38 KiB
Python
"""Generate the DingTalk human-input card template JSON.
|
||
|
||
The output is wrapped in the {editorData, widgetInfo, type, mode} envelope
|
||
the DingTalk card builder expects on import. editorData is itself a JSON
|
||
string (NOT a nested object), matching real exports from the builder.
|
||
|
||
Run from the repo root: python scripts/build_dingtalk_card_template.py
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
OUTPUT = Path('src/langbot/templates/dingtalk_human_input_card.json')
|
||
|
||
|
||
def markdown_block(node_id, variable='content'):
|
||
"""A MarkdownBlock whose content is bound to a global variable.
|
||
|
||
Critical: `content.varType: "markdown"` must be set, otherwise DingTalk
|
||
silently fails to render the bound variable (the card body stays blank
|
||
even though the variable is supplied via cardParamMap). The working
|
||
reference template in I:\\下载\\dingtalk_1782055283543.json confirms
|
||
this — its MarkdownBlock has the same varType marker.
|
||
|
||
isStreaming is left `false` because the adapter writes the variable via
|
||
`update_card_data` (the full-card PUT endpoint), not the streaming
|
||
`card/streaming` endpoint. Setting `isStreaming: true` here conflicts
|
||
with that path and can suppress the rendered body.
|
||
"""
|
||
return {
|
||
'componentName': 'MarkdownBlock',
|
||
'id': node_id,
|
||
'props': {
|
||
'mdVer': 0,
|
||
'icon': {'type': 'icon', 'icon': '', 'iconType': 'emoji'},
|
||
'content': {
|
||
'variable': variable,
|
||
'variableType': 'global',
|
||
'type': 'variableValue',
|
||
'varType': 'markdown',
|
||
},
|
||
'visible': {
|
||
'type': 'dynamicVisible',
|
||
'value': True,
|
||
'valueType': 'fixed',
|
||
'condition': {'op': 'and', 'conditions': []},
|
||
},
|
||
'isStreaming': False,
|
||
'enableLinkStatPoint': False,
|
||
'linkStatPoint': {
|
||
'type': 'dynamicString',
|
||
'content': 'Page_InteractiveCard__Click_markdownOpenlink',
|
||
'i18n': False,
|
||
},
|
||
'linkStatPointParams': [],
|
||
'marginTop': 6,
|
||
'marginBottom': 6,
|
||
'marginLeft': 12,
|
||
'marginRight': 12,
|
||
},
|
||
'title': 'AI 流式富文本',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
}
|
||
|
||
|
||
def _dynamic_string_var(variable):
|
||
return {'type': 'dynamicString', 'content': '', 'i18n': False, 'variable': variable, 'variableType': 'global'}
|
||
|
||
|
||
def _dynamic_visible_var(variable):
|
||
return {
|
||
'type': 'dynamicVisible',
|
||
'value': True,
|
||
'valueType': 'variable',
|
||
'variable': variable,
|
||
'variableType': 'global',
|
||
'condition': {'op': 'and', 'conditions': []},
|
||
}
|
||
|
||
|
||
SELECT_OPTION_LOCALES = (
|
||
'zh_CN',
|
||
'zh_TW',
|
||
'en_US',
|
||
'ja_JP',
|
||
'vi_VN',
|
||
'th_TH',
|
||
'id_ID',
|
||
'ne_NP',
|
||
'ms_MY',
|
||
'ko_KR',
|
||
'ru_RU',
|
||
'es_EA',
|
||
'tr_TR',
|
||
'fr_FR',
|
||
'pt_BR',
|
||
)
|
||
|
||
|
||
def _empty_select_option():
|
||
return {'value': '', 'text': {locale: '' for locale in SELECT_OPTION_LOCALES}}
|
||
|
||
|
||
def _select_options_variable():
|
||
return {
|
||
'name': 'index_o',
|
||
'private': False,
|
||
'type': 'selectOptions',
|
||
'id': 'index_o',
|
||
'description': 'Select options',
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
'schema': [
|
||
{
|
||
'id': 'index_o.value',
|
||
'type': 'string',
|
||
'name': 'value',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '',
|
||
},
|
||
{
|
||
'id': 'index_o.text',
|
||
'type': 'object',
|
||
'name': 'text',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '',
|
||
'schema': [
|
||
{
|
||
'id': f'index_o.{locale}',
|
||
'type': 'string',
|
||
'name': locale,
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '',
|
||
}
|
||
for locale in SELECT_OPTION_LOCALES
|
||
],
|
||
},
|
||
],
|
||
}
|
||
|
||
|
||
def input_block(node_id):
|
||
return {
|
||
'componentName': 'Input',
|
||
'id': node_id,
|
||
'props': {
|
||
'placeholder': _dynamic_string_var('input_placeholder'),
|
||
'currentValue': _dynamic_string_var('input_value'),
|
||
'message': _dynamic_string_var('input_placeholder'),
|
||
'title': _dynamic_string_var('input_title'),
|
||
'id': {'type': 'dynamicString', 'content': 'input', 'i18n': False},
|
||
'params': [
|
||
{
|
||
'type': 'builtIn',
|
||
'variable': '',
|
||
'value': '',
|
||
'name': 'input',
|
||
'variableType': 'global',
|
||
'id': '__built_in_inputResult__',
|
||
}
|
||
],
|
||
'visible': _dynamic_visible_var('input_visible'),
|
||
'status': {
|
||
'type': 'dynamicSelect',
|
||
'valueType': 'fixed',
|
||
'value': 'normal',
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'actionType': 'request',
|
||
'localVarAction': {'type': 'variableValue', 'variableType': 'global', 'variable': ''},
|
||
'keyOfDynamicObject': {'type': 'dynamicString', 'content': '', 'i18n': False},
|
||
'inlineMode': False,
|
||
'textArea': True,
|
||
'minRows': {
|
||
'type': 'dynamicNumber',
|
||
'valueType': 'fixed',
|
||
'value': 2,
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'maxRows': {
|
||
'type': 'dynamicNumber',
|
||
'valueType': 'fixed',
|
||
'value': 6,
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'marginLeft': 12,
|
||
'marginRight': 12,
|
||
'marginTop': 6,
|
||
'marginBottom': 6,
|
||
'margin': 12,
|
||
'innerOffset': 0,
|
||
},
|
||
'title': 'Text input',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
}
|
||
|
||
|
||
def select_block(node_id):
|
||
return {
|
||
'componentName': 'SelectBlock',
|
||
'id': node_id,
|
||
'props': {
|
||
'id': {'type': 'dynamicString', 'content': 'select', 'i18n': False},
|
||
'placeholder': _dynamic_string_var('select_placeholder'),
|
||
'currentIndex': {
|
||
'type': 'dynamicNumber',
|
||
'valueType': 'variable',
|
||
'value': -1,
|
||
'variable': 'select_index',
|
||
'variableType': 'global',
|
||
},
|
||
'options': {
|
||
'type': 'dynamicSelectOptions',
|
||
'valueType': 'variable',
|
||
'value': [],
|
||
'variable': 'index_o',
|
||
'variableType': 'global',
|
||
},
|
||
'optionLabelMaxLines': 3,
|
||
'params': [
|
||
{
|
||
'type': 'builtIn',
|
||
'variable': '',
|
||
'value': '{"index": ${index}, "value": "${value}"}',
|
||
'name': 'select',
|
||
'variableType': 'global',
|
||
'id': '__built_in_selectResult__',
|
||
}
|
||
],
|
||
'actionType': 'request',
|
||
'localVarAction': {'type': 'variableValue', 'variableType': 'global', 'variable': ''},
|
||
'keyOfDynamicObject': {'type': 'dynamicString', 'content': '', 'i18n': False},
|
||
'status': {
|
||
'type': 'dynamicSelect',
|
||
'valueType': 'fixed',
|
||
'value': 'normal',
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'visible': _dynamic_visible_var('select_visible'),
|
||
'marginLeft': 12,
|
||
'marginRight': 12,
|
||
'marginTop': 6,
|
||
'marginBottom': 6,
|
||
'pullOptionsWhileOpen': False,
|
||
'pullOptionsRequestParams': [],
|
||
'margin': 12,
|
||
'innerOffset': 0,
|
||
},
|
||
'title': 'Select',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
}
|
||
|
||
|
||
def text_block(
|
||
node_id,
|
||
text,
|
||
*,
|
||
bold=False,
|
||
gravity='left',
|
||
font_size=14,
|
||
line_height=22,
|
||
max_lines=20,
|
||
ml=12,
|
||
mr=12,
|
||
mt=4,
|
||
mb=4,
|
||
color_token='common_level1_base_color',
|
||
style_token='common_body_text_style',
|
||
):
|
||
return {
|
||
'componentName': 'BaseText',
|
||
'id': node_id,
|
||
'props': {
|
||
'text': {'i18n': False, 'type': 'dynamicString', 'content': text},
|
||
'hoverText': {'type': 'dynamicString', 'content': '', 'i18n': False},
|
||
'iconType': 'iconCode',
|
||
'iconFont': {'type': 'icon', 'icon': '', 'iconType': 'ddIcon'},
|
||
'icon': {
|
||
'type': 'dynamicLink',
|
||
'value': '',
|
||
'valueType': 'fixed',
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'darkIcon': {
|
||
'type': 'dynamicLink',
|
||
'value': '',
|
||
'valueType': 'fixed',
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'autoWidth': False,
|
||
'maxWidth': {
|
||
'type': 'dynamicNumber',
|
||
'valueType': 'fixed',
|
||
'value': 0,
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'fixedWidth': {
|
||
'type': 'dynamicNumber',
|
||
'valueType': 'fixed',
|
||
'value': 0,
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'marginLeft': ml,
|
||
'marginRight': mr,
|
||
'marginTop': mt,
|
||
'marginBottom': mb,
|
||
'fontColorType': 'Standard',
|
||
'enableHighlight': False,
|
||
'maxLine': {
|
||
'type': 'dynamicNumber',
|
||
'valueType': 'fixed',
|
||
'value': max_lines,
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'color': {
|
||
'type': 'dynamicColor',
|
||
'valueType': 'fixed',
|
||
'value': color_token,
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'customLightColor': {
|
||
'type': 'dynamicColor',
|
||
'valueType': 'fixed',
|
||
'value': '#35404b',
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'customDarkColor': {
|
||
'type': 'dynamicColor',
|
||
'valueType': 'fixed',
|
||
'value': '#f6f6f6',
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'gravity': gravity,
|
||
'fontSizeType': 'Standard',
|
||
'styleType': 'custom',
|
||
'styleToken': style_token,
|
||
'size': 'middle',
|
||
'customFontSize': font_size,
|
||
'customFontLineHeight': line_height,
|
||
'bold': bold,
|
||
'italic': False,
|
||
'strikeThrough': False,
|
||
'lineHeight': 'normal',
|
||
'visible': {
|
||
'type': 'dynamicVisible',
|
||
'value': True,
|
||
'valueType': 'fixed',
|
||
'condition': {'op': 'and', 'conditions': []},
|
||
},
|
||
'autoMaxWidth': False,
|
||
'innerOffset': 0,
|
||
'enableIcon': False,
|
||
'widthMode': 'match_parent',
|
||
'margin': -2,
|
||
},
|
||
'title': '基础文本',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
}
|
||
|
||
|
||
def button_group(node_id):
|
||
return {
|
||
'componentName': 'ButtonGroup',
|
||
'id': node_id,
|
||
'props': {
|
||
'dynamicButtons': {'type': 'variableValue', 'variableType': 'global', 'variable': 'btns'},
|
||
'marginLeft': 12,
|
||
'marginRight': 12,
|
||
'marginTop': 6,
|
||
'marginBottom': 12,
|
||
'visible': {
|
||
'type': 'dynamicVisible',
|
||
'value': True,
|
||
'valueType': 'fixed',
|
||
'condition': {'op': 'and', 'conditions': []},
|
||
},
|
||
'responsiveLayoutWidth': 350,
|
||
'buttonsSource': 'variable',
|
||
'fixedButtonIds': [],
|
||
'fixedButtons': [],
|
||
'enableResponsiveLayout': False,
|
||
'matchContent': False,
|
||
'buttonSpacing': 8,
|
||
'margin': -2,
|
||
'innerOffset': 0,
|
||
},
|
||
'title': '按钮组',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
}
|
||
|
||
|
||
def avatar(node_id, *, name='LangBot', image_variable='bot_avatar'):
|
||
"""Avatar component in `userInfo` mode — renders the bot's avatar
|
||
image and nickname as a header row above the response content.
|
||
Mirrors the layout from `I:\\下载\\dingtalk_1782120006374.json` where
|
||
Avatar sits at the top of the done-state AICardContent.
|
||
|
||
`imageUrl` is bound to a global variable (default `bot_avatar`) so
|
||
the adapter can populate it at runtime with a DingTalk media id
|
||
(``@xxx``) obtained from the /media/upload endpoint. DingTalk's
|
||
Avatar.imageUrl resolver rejects external URLs — it only accepts
|
||
DingTalk-hosted media ids, so this binding is the only path to
|
||
a custom avatar.
|
||
"""
|
||
return {
|
||
'componentName': 'Avatar',
|
||
'id': node_id,
|
||
'props': {
|
||
'imageUrl': {
|
||
'value': '',
|
||
'valueType': 'variable',
|
||
'type': 'dynamicImage',
|
||
'variable': image_variable,
|
||
'variableType': 'global',
|
||
},
|
||
'name': {'i18n': False, 'type': 'dynamicString', 'content': name},
|
||
'sizeType': 'Standard',
|
||
'size': 'extraSmall',
|
||
'customSize': 48,
|
||
'marginLeft': 12,
|
||
'marginRight': 12,
|
||
'marginTop': 6,
|
||
'marginBottom': 6,
|
||
'visible': {
|
||
'type': 'dynamicVisible',
|
||
'value': True,
|
||
'valueType': 'fixed',
|
||
'condition': {'op': 'and', 'conditions': []},
|
||
},
|
||
'mode': 'userInfo',
|
||
'margin': -2,
|
||
'innerOffset': 0,
|
||
},
|
||
'title': '头像',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
}
|
||
|
||
|
||
def build_editor_data():
|
||
component_names = [
|
||
'AIPending',
|
||
'AICardStatusContainer',
|
||
'BaseText',
|
||
'AICardContent',
|
||
'AICardContainer',
|
||
'ButtonGroup',
|
||
'MarkdownBlock',
|
||
'Avatar',
|
||
'Input',
|
||
'SelectBlock',
|
||
]
|
||
components_map = [
|
||
{
|
||
'package': '@ali/dxComponent',
|
||
'version': '1.0.0',
|
||
'exportName': n,
|
||
'main': './src/index.tsx',
|
||
'destructuring': False,
|
||
'subName': '',
|
||
'componentName': n,
|
||
}
|
||
for n in component_names
|
||
]
|
||
|
||
pending_state = {
|
||
'componentName': 'AICardStatusContainer',
|
||
'id': 'node_status_pending',
|
||
'props': {
|
||
'status': 1,
|
||
'marginLeft': 0,
|
||
'marginRight': 0,
|
||
'marginTop': 0,
|
||
'marginBottom': 0,
|
||
'enableExtend': False,
|
||
'autoFoldConfig': {
|
||
'needFold': True,
|
||
'heightLimit': 480,
|
||
'foldStatusLocalDataKey': '_cardFoldStatusLocalDataKey',
|
||
},
|
||
'innerOffset': 0,
|
||
'enableCollapse': False,
|
||
'margin': -2,
|
||
},
|
||
'title': '处理中状态',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
'children': [
|
||
{
|
||
'componentName': 'AIPending',
|
||
'id': 'node_pending_inner',
|
||
'props': {
|
||
'marginLeft': 0,
|
||
'marginRight': 0,
|
||
'marginTop': 0,
|
||
'marginBottom': 0,
|
||
'pendingTip': {'type': 'dynamicString', 'content': '处理中...', 'i18n': False},
|
||
'style': 'embed',
|
||
'hideIcon': False,
|
||
},
|
||
'hidden': False,
|
||
'title': '',
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
}
|
||
],
|
||
}
|
||
|
||
done_state = {
|
||
'componentName': 'AICardStatusContainer',
|
||
'id': 'node_status_done',
|
||
'props': {
|
||
'status': 3,
|
||
'marginLeft': 0,
|
||
'marginRight': 0,
|
||
'marginTop': 0,
|
||
'marginBottom': 0,
|
||
'enableExtend': False,
|
||
'autoFoldConfig': {
|
||
'needFold': True,
|
||
'heightLimit': 480,
|
||
'foldStatusLocalDataKey': '_cardFoldStatusLocalDataKey',
|
||
},
|
||
'innerOffset': 0,
|
||
'enableCollapse': False,
|
||
'margin': -2,
|
||
},
|
||
'title': '完成状态',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
'children': [
|
||
{
|
||
'componentName': 'AICardContent',
|
||
'id': 'node_done_content',
|
||
'props': {
|
||
'marginLeft': 0,
|
||
'marginRight': 0,
|
||
'marginTop': 0,
|
||
'marginBottom': 0,
|
||
'visible': {
|
||
'type': 'dynamicVisible',
|
||
'value': True,
|
||
'valueType': 'fixed',
|
||
'condition': {'op': 'and', 'conditions': []},
|
||
},
|
||
'innerOffset': 0,
|
||
'disabledWhileForward': False,
|
||
'statPoint': {'type': 'dynamicString', 'content': '', 'i18n': False},
|
||
'statPointParams': [
|
||
{'type': 'fixed', 'variable': '', 'value': '', 'name': '', 'variableType': 'global', 'id': '1'}
|
||
],
|
||
'margin': -2,
|
||
'transformToEventChain': False,
|
||
'enableStatPoint': False,
|
||
},
|
||
'hidden': False,
|
||
'title': '',
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
'children': [
|
||
avatar('node_avatar', name='LangBot'),
|
||
input_block('node_input'),
|
||
select_block('node_select'),
|
||
markdown_block('node_text_content', variable='content'),
|
||
button_group('node_btn_group'),
|
||
],
|
||
}
|
||
],
|
||
}
|
||
|
||
failed_state = {
|
||
'componentName': 'AICardStatusContainer',
|
||
'id': 'node_status_failed',
|
||
'props': {
|
||
'status': 5,
|
||
'marginLeft': 0,
|
||
'marginRight': 0,
|
||
'marginTop': 0,
|
||
'marginBottom': 0,
|
||
'enableExtend': False,
|
||
'autoFoldConfig': {
|
||
'needFold': True,
|
||
'heightLimit': 480,
|
||
'foldStatusLocalDataKey': '_cardFoldStatusLocalDataKey',
|
||
},
|
||
'innerOffset': 0,
|
||
'enableCollapse': False,
|
||
'margin': -2,
|
||
},
|
||
'title': '失败状态',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
'children': [
|
||
{
|
||
'componentName': 'AICardContent',
|
||
'id': 'node_failed_content',
|
||
'props': {
|
||
'visible': {
|
||
'type': 'dynamicVisible',
|
||
'value': True,
|
||
'valueType': 'fixed',
|
||
'condition': {'op': 'and', 'conditions': []},
|
||
},
|
||
'marginLeft': 0,
|
||
'marginRight': 0,
|
||
'marginTop': 0,
|
||
'marginBottom': 0,
|
||
'innerOffset': 0,
|
||
'disabledWhileForward': False,
|
||
'statPoint': {'type': 'dynamicString', 'content': '', 'i18n': False},
|
||
'statPointParams': [
|
||
{'type': 'fixed', 'variable': '', 'value': '', 'name': '', 'variableType': 'global', 'id': '1'}
|
||
],
|
||
'margin': -2,
|
||
'transformToEventChain': False,
|
||
'enableStatPoint': False,
|
||
},
|
||
'hidden': False,
|
||
'title': '',
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
'children': [
|
||
text_block(
|
||
'node_failed_text',
|
||
'操作失败,请稍后重试。',
|
||
gravity='center',
|
||
mt=10,
|
||
mb=10,
|
||
ml=10,
|
||
mr=10,
|
||
max_lines=2,
|
||
font_size=15,
|
||
)
|
||
],
|
||
}
|
||
],
|
||
}
|
||
|
||
# Empty containers for flowStatus=2 (writing) and flowStatus=4 (doing).
|
||
# AICardContainer expects placeholders to exist for every enabled state;
|
||
# without them, the renderer can refuse to advance to flowStatus=3 (done)
|
||
# and the card body stays empty. They render nothing visible because
|
||
# they have no content children, but their presence satisfies the
|
||
# state-machine validation.
|
||
def _empty_status_container(node_id, status):
|
||
return {
|
||
'componentName': 'AICardStatusContainer',
|
||
'id': node_id,
|
||
'props': {
|
||
'status': status,
|
||
'marginLeft': 0,
|
||
'marginRight': 0,
|
||
'marginTop': 0,
|
||
'marginBottom': 0,
|
||
'enableExtend': False,
|
||
'autoFoldConfig': {
|
||
'needFold': True,
|
||
'heightLimit': 480,
|
||
'foldStatusLocalDataKey': '_cardFoldStatusLocalDataKey',
|
||
},
|
||
'innerOffset': 0,
|
||
'enableCollapse': False,
|
||
'margin': -2,
|
||
},
|
||
'title': f'状态{status}占位',
|
||
'hidden': False,
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
'children': [
|
||
{
|
||
'componentName': 'AICardContent',
|
||
'id': f'{node_id}_content',
|
||
'props': {
|
||
'marginLeft': 0,
|
||
'marginRight': 0,
|
||
'marginTop': 0,
|
||
'marginBottom': 0,
|
||
'visible': {
|
||
'type': 'dynamicVisible',
|
||
'value': True,
|
||
'valueType': 'fixed',
|
||
'condition': {'op': 'and', 'conditions': []},
|
||
},
|
||
'innerOffset': 0,
|
||
'disabledWhileForward': False,
|
||
'statPoint': {'type': 'dynamicString', 'content': '', 'i18n': False},
|
||
'statPointParams': [
|
||
{
|
||
'type': 'fixed',
|
||
'variable': '',
|
||
'value': '',
|
||
'name': '',
|
||
'variableType': 'global',
|
||
'id': '1',
|
||
}
|
||
],
|
||
'margin': -2,
|
||
'transformToEventChain': False,
|
||
'enableStatPoint': False,
|
||
},
|
||
'hidden': False,
|
||
'title': '',
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
'children': [],
|
||
}
|
||
],
|
||
}
|
||
|
||
writing_state = _empty_status_container('node_status_writing', 2)
|
||
doing_state = _empty_status_container('node_status_doing', 4)
|
||
|
||
root = {
|
||
'componentName': 'AICardContainer',
|
||
'id': 'node_root',
|
||
'props': {
|
||
'marginLeft': 0,
|
||
'marginRight': 0,
|
||
'marginTop': 0,
|
||
'marginBottom': 0,
|
||
'enablePending': True,
|
||
# writing/doing must be enabled so AICardContainer recognises
|
||
# flowStatus transitions through 2/4 — without this, the
|
||
# working reference template (I:\\下载\\dingtalk_1782055283543.json)
|
||
# never reaches the done state and the body stays empty.
|
||
'enableWriting': True,
|
||
'enableDoing': True,
|
||
'enableFailed': True,
|
||
'summaryContent': {'type': 'variableValue', 'variableType': 'global', 'variable': ''},
|
||
'enableTitle': False,
|
||
'flowStatusVar': {'type': 'variableValue', 'variableType': 'global', 'variable': 'flowStatus'},
|
||
'operationPenalType': 'custom',
|
||
'enableFlowAbort': True,
|
||
'innerOffset': 0,
|
||
'enableGradientBorder': True,
|
||
'cardSizeMode': 'adaptive',
|
||
'cardSizeHeightMode': 'adaptive',
|
||
'cardSizeWidthMode': 'adaptive',
|
||
'cardSizeHeight': {
|
||
'type': 'dynamicNumber',
|
||
'valueType': 'fixed',
|
||
'value': 226,
|
||
'variable': '',
|
||
'variableType': 'global',
|
||
},
|
||
'hasBackground': False,
|
||
'backgroundType': 'Standard',
|
||
'standardBackgroundColor': 'gray',
|
||
'backgroundColor': '#F6F6F6',
|
||
'darkModeBackgroundColor': '#3C3C3C',
|
||
'enableEngineUpgrade': False,
|
||
'enableExposeStatPoint': False,
|
||
'enableDebugTool': False,
|
||
},
|
||
'hidden': False,
|
||
'title': '',
|
||
'isLocked': False,
|
||
'condition': True,
|
||
'conditionGroup': '',
|
||
'children': [pending_state, writing_state, doing_state, done_state, failed_state],
|
||
}
|
||
|
||
btns_var = {
|
||
'name': 'btns',
|
||
'private': False,
|
||
'type': 'buttonGroup',
|
||
'id': 'btns',
|
||
'description': '动态按钮列表(Dify actions)',
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
'schema': [
|
||
{
|
||
'id': 'btns.text',
|
||
'type': 'string',
|
||
'name': 'text',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '按钮文案',
|
||
},
|
||
{
|
||
'id': 'btns.color',
|
||
'type': 'string',
|
||
'name': 'color',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '按钮颜色',
|
||
},
|
||
{
|
||
'id': 'btns.status',
|
||
'type': 'string',
|
||
'name': 'status',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '按钮状态',
|
||
},
|
||
{
|
||
'id': 'btns.event',
|
||
'type': 'dynamicEvent',
|
||
'name': 'event',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '按钮点击事件',
|
||
'schema': [
|
||
{
|
||
'id': 'btns.type',
|
||
'type': 'string',
|
||
'name': 'type',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '事件类型:openLink / sendCardRequest',
|
||
},
|
||
{
|
||
'id': 'btns.params',
|
||
'type': 'object',
|
||
'name': 'params',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '事件参数',
|
||
'schema': [
|
||
{
|
||
'id': 'btns.url',
|
||
'type': 'string',
|
||
'name': 'url',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '点击跳转链接(type=openLink)',
|
||
},
|
||
{
|
||
'id': 'btns.actionId',
|
||
'type': 'string',
|
||
'name': 'actionId',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '回传请求 id(type=sendCardRequest)',
|
||
},
|
||
{
|
||
'id': 'btns.params',
|
||
'type': 'object',
|
||
'name': 'params',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'description': '回传请求参数(type=sendCardRequest)',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
}
|
||
|
||
return {
|
||
'schemaVersion': '3.0.0',
|
||
'schema': {
|
||
# Match the working reference template — leaving config null lets
|
||
# DingTalk pick defaults. Explicit `streaming_mode: true` would
|
||
# make the renderer wait for chunks on the streaming endpoint
|
||
# (PUT /v1.0/card/streaming), which our adapter does NOT use —
|
||
# it pushes content via update_card_data, so streaming_mode=true
|
||
# leaves the body empty.
|
||
'config': None,
|
||
'componentsMap': components_map,
|
||
'componentsTree': [root],
|
||
'i18n': {},
|
||
'version': '1.0.0',
|
||
},
|
||
'mockData': {
|
||
'cardData': {
|
||
'flowStatus': 3,
|
||
'content': '请审核以下报销申请:\n\n- 申请人:张三\n- 金额:¥1,200\n- 类别:差旅',
|
||
'input_visible': '',
|
||
'input_title': '',
|
||
'input_placeholder': '',
|
||
'input_value': '',
|
||
'select_visible': '',
|
||
'select_placeholder': '',
|
||
'index_o': [_empty_select_option()],
|
||
'select_options': [],
|
||
'select_index': -1,
|
||
'btns': [
|
||
{
|
||
'text': '通过',
|
||
'color': 'blue',
|
||
'status': 'normal',
|
||
'event': {
|
||
'type': 'sendCardRequest',
|
||
'params': {'actionId': 'approve', 'params': {'action_id': 'approve'}},
|
||
},
|
||
},
|
||
{
|
||
'text': '驳回',
|
||
'color': 'gray',
|
||
'status': 'normal',
|
||
'event': {
|
||
'type': 'sendCardRequest',
|
||
'params': {'actionId': 'reject', 'params': {'action_id': 'reject'}},
|
||
},
|
||
},
|
||
{
|
||
'text': '补充资料',
|
||
'color': 'gray',
|
||
'status': 'normal',
|
||
'event': {
|
||
'type': 'sendCardRequest',
|
||
'params': {'actionId': 'more_info', 'params': {'action_id': 'more_info'}},
|
||
},
|
||
},
|
||
],
|
||
},
|
||
'cardPrivateData': {},
|
||
'localData': {'flowStatus': '', '_cardFoldStatusLocalDataKey': ''},
|
||
'richTextData': {},
|
||
},
|
||
'renderContext': {'regenerateEnabled': '1', 'regenerateIndex': '2', 'regenerateTotal': '5'},
|
||
'editVersion': 0,
|
||
'customWidgetInfo': '',
|
||
'useCustomWidgetInfo': False,
|
||
'variableList': [
|
||
{
|
||
'id': 'content',
|
||
'type': 'markdown',
|
||
'name': 'content',
|
||
'description': '人工输入提示词(Dify form_content 含可选 node_title 前缀)',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
{
|
||
'id': 'flowStatus',
|
||
'type': 'string',
|
||
'name': 'flowStatus',
|
||
'description': 'AI卡片状态:pending(1)、writing(2)、done(3)、failed(5)',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': True,
|
||
'visible': False,
|
||
},
|
||
{
|
||
'id': 'bot_avatar',
|
||
'type': 'string',
|
||
'name': 'bot_avatar',
|
||
'description': '机器人头像 DingTalk 媒体 ID(@xxx 格式,启动时由 /media/upload 拿到)',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
{
|
||
'id': 'input_visible',
|
||
'type': 'string',
|
||
'name': 'input_visible',
|
||
'description': 'Whether to show the text input component',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
{
|
||
'id': 'input_title',
|
||
'type': 'string',
|
||
'name': 'input_title',
|
||
'description': 'Text input title',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
{
|
||
'id': 'input_placeholder',
|
||
'type': 'string',
|
||
'name': 'input_placeholder',
|
||
'description': 'Text input placeholder',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
{
|
||
'id': 'input_value',
|
||
'type': 'string',
|
||
'name': 'input_value',
|
||
'description': 'Text input current value',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
{
|
||
'id': 'select_visible',
|
||
'type': 'string',
|
||
'name': 'select_visible',
|
||
'description': 'Whether to show the select component',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
{
|
||
'id': 'select_placeholder',
|
||
'type': 'string',
|
||
'name': 'select_placeholder',
|
||
'description': 'Select placeholder',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
_select_options_variable(),
|
||
{
|
||
'id': 'select_options',
|
||
'type': 'array',
|
||
'name': 'select_options',
|
||
'description': 'Legacy select options',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
{
|
||
'id': 'select_index',
|
||
'type': 'number',
|
||
'name': 'select_index',
|
||
'description': 'Current select index',
|
||
'private': False,
|
||
'editorVarType': 'variables',
|
||
'disabled': False,
|
||
},
|
||
btns_var,
|
||
],
|
||
'formList': [],
|
||
'customContextList': [],
|
||
'expList': [],
|
||
'localList': [],
|
||
'hsfList': [],
|
||
'lwpList': [],
|
||
'pageData': {},
|
||
'extension': {
|
||
'extendType': 'AI',
|
||
# All 5 statuses listed — must mirror the enableX flags on
|
||
# AICardContainer. The working reference template's extension
|
||
# includes 2 (writing) and 4 (doing); omitting them while
|
||
# enableWriting/enableDoing are true makes the renderer reject
|
||
# transitions and leaves the card body empty.
|
||
'aiStatusList': [3, 1, 5, 2, 4],
|
||
'fileTypeList': [],
|
||
},
|
||
}
|
||
|
||
|
||
def main():
|
||
editor_data = build_editor_data()
|
||
wrapper = {
|
||
'editorData': json.dumps(editor_data, ensure_ascii=False, separators=(',', ':')),
|
||
'widgetInfo': '',
|
||
'type': 'im',
|
||
'mode': 'card',
|
||
}
|
||
OUTPUT.write_text(json.dumps(wrapper, ensure_ascii=False, indent=2), encoding='utf-8')
|
||
print(f'wrote {OUTPUT}')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|