mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-16 10:46:03 +00:00
feat(dingtalk): implement human input card support and card action handling
- Add a new module `card_callback.py` to handle card action button clicks from DingTalk. - Introduce `DingTalkCardActionHandler` to process card action callbacks and extract parameters. - Update `DingTalkAdapter` to manage card state and handle form input through a single card template. - Add configuration for `human_input_card_template_id` in `dingtalk.yaml` to specify the template for human input. - Create a new card template `dingtalk_human_input_card.json` for rendering human input prompts and buttons.
This commit is contained in:
648
scripts/build_dingtalk_card_template.py
Normal file
648
scripts/build_dingtalk_card_template.py
Normal file
@@ -0,0 +1,648 @@
|
||||
"""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.
|
||||
|
||||
Unlike BaseText (whose `text` field requires editor-side manual binding),
|
||||
MarkdownBlock's `content` accepts `variableValue` binding at JSON load
|
||||
time — the imported template renders the variable straight away.
|
||||
"""
|
||||
return {
|
||||
'componentName': 'MarkdownBlock',
|
||||
'id': node_id,
|
||||
'props': {
|
||||
'mdVer': 0,
|
||||
'icon': {'type': 'icon', 'icon': '', 'iconType': 'emoji'},
|
||||
'content': {'variable': variable, 'variableType': 'global', 'type': 'variableValue'},
|
||||
'visible': {
|
||||
'type': 'dynamicVisible',
|
||||
'value': True,
|
||||
'valueType': 'fixed',
|
||||
'condition': {'op': 'and', 'conditions': []},
|
||||
},
|
||||
'isStreaming': False,
|
||||
'enableLinkStatPoint': False,
|
||||
'linkStatPoint': {'type': 'dynamicString', 'content': '', 'i18n': False},
|
||||
'linkStatPointParams': [],
|
||||
'marginTop': 6,
|
||||
'marginBottom': 6,
|
||||
'marginLeft': 12,
|
||||
'marginRight': 12,
|
||||
},
|
||||
'title': 'AI 流式富文本',
|
||||
'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 build_editor_data():
|
||||
component_names = [
|
||||
'AIPending',
|
||||
'AICardStatusContainer',
|
||||
'BaseText',
|
||||
'AICardContent',
|
||||
'AICardContainer',
|
||||
'ButtonGroup',
|
||||
'MarkdownBlock',
|
||||
]
|
||||
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': [
|
||||
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,
|
||||
)
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
root = {
|
||||
'componentName': 'AICardContainer',
|
||||
'id': 'node_root',
|
||||
'props': {
|
||||
'marginLeft': 0,
|
||||
'marginRight': 0,
|
||||
'marginTop': 0,
|
||||
'marginBottom': 0,
|
||||
'enablePending': True,
|
||||
'enableWriting': False,
|
||||
'enableDoing': False,
|
||||
'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, 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': {
|
||||
'componentsMap': components_map,
|
||||
'componentsTree': [root],
|
||||
'i18n': {},
|
||||
'version': '1.0.0',
|
||||
},
|
||||
'mockData': {
|
||||
'cardData': {
|
||||
'flowStatus': 3,
|
||||
'content': '请审核以下报销申请:\n\n- 申请人:张三\n- 金额:¥1,200\n- 类别:差旅',
|
||||
'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,
|
||||
},
|
||||
btns_var,
|
||||
],
|
||||
'formList': [],
|
||||
'customContextList': [],
|
||||
'expList': [],
|
||||
'localList': [],
|
||||
'hsfList': [],
|
||||
'lwpList': [],
|
||||
'pageData': {},
|
||||
'extension': {'extendType': 'AI', 'aiStatusList': [3, 1, 5], '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()
|
||||
Reference in New Issue
Block a user