feat: Update the human-computer interaction template cards, and optimize the prompt information and content display.

This commit is contained in:
fdc310
2026-07-11 20:05:05 +08:00
parent 6cae954a67
commit d8c14fc969
2 changed files with 131 additions and 43 deletions
+55 -35
View File
@@ -797,22 +797,12 @@ def _wecom_input_hint_lines(form_data: dict) -> list[str]:
continue
if not field_name:
continue
if field_type == 'select':
source = field.get('option_source') or {}
options = source.get('value') if isinstance(source, dict) else []
if isinstance(options, list) and options:
option_text = ', '.join(f'{idx}. {option}' for idx, option in enumerate(options, start=1))
lines.append(f'- {field_label}: {option_text}')
else:
lines.append(f'- {field_label}: choose one option')
elif field_type in {'file', 'file-list'}:
if field_type in {'file', 'file-list'}:
limit = field.get('number_limits') if field_type == 'file-list' else 1
allowed_types = ', '.join(field.get('allowed_file_types') or [])
suffix = f', up to {limit}' if field_type == 'file-list' and limit else ''
allowed = f' ({allowed_types})' if allowed_types else ''
lines.append(f'- {field_label}: upload file(s){allowed}{suffix} or reply `{field_name}: <url>`')
else:
lines.append(f'请直接回复:{field_label}')
return lines
@@ -861,18 +851,8 @@ def _wecom_field_title(field: dict, fallback: str) -> str:
def _wecom_form_desc(form_data: dict) -> str:
current_field = str(form_data.get('_current_input_field') or '').strip()
if current_field:
for field in form_data.get('input_defs') or form_data.get('all_input_defs') or []:
if str(field.get('output_variable_name') or '').strip() != current_field:
continue
field_type = str(field.get('type') or 'text').strip().lower()
if field_type != 'select':
return f'请直接回复:{_wecom_field_display_name(field, current_field)}'
form_content = _wecom_clean_form_content(form_data)
if form_content:
return form_content[:512]
return 'Please choose an option to continue.'
return form_content[:512] if form_content else ''
def build_human_input_text_prompt(form_data: dict) -> Optional[str]:
@@ -887,8 +867,11 @@ def build_human_input_text_prompt(form_data: dict) -> Optional[str]:
field_type = str(field.get('type') or 'text').strip().lower()
if field_type == 'select':
return None
label = _wecom_field_display_name(field, current_field)
return f'{str(form_data.get("node_title") or "人工介入").strip()}\n\n请直接回复:{label}'
form_content = _wecom_clean_form_content(form_data)
if not form_content:
form_content = _wecom_field_display_name(field, current_field)
node_title = str(form_data.get('node_title') or '人工介入').strip()
return f'{node_title}\n\n{form_content}' if form_content else node_title
return None
@@ -1010,13 +993,11 @@ def build_select_button_interaction_payload(
overflow_options = options[6:]
node_title = (form_data.get('node_title') or '').strip() or 'Human Input'
field_title = _wecom_field_title(field, field_name or 'Select') if field else 'Select'
form_content = _wecom_clean_form_content(form_data)
sub_title_parts: list[str] = []
if form_content:
sub_title_parts.append(form_content)
sub_title_parts.append(f'Choose {field_title}.')
if overflow_options:
extra_lines = [f' - {idx + 7}. {option}' for idx, option in enumerate(overflow_options)]
sub_title_parts.append(
@@ -1066,21 +1047,57 @@ def build_human_input_template_card_payload(
def _wecom_clean_form_content(form_data: dict) -> str:
content = form_data.get('raw_form_content') or form_data.get('form_content') or ''
field_names = {
str(field.get('output_variable_name') or '').strip()
for field in form_data.get('input_defs') or []
is_field_step = bool(form_data.get('_current_input_field')) and not form_data.get('_action_select_only')
raw_content = str(form_data.get('raw_form_content') or '')
content = form_data.get('form_content') or raw_content
input_defs = list(form_data.get('all_input_defs') or form_data.get('input_defs') or [])
fields = {
str(field.get('output_variable_name') or '').strip(): field
for field in input_defs
if str(field.get('output_variable_name') or '').strip()
}
if form_data.get('_action_select_only') and raw_content:
placeholders = [
match
for match in re.finditer(r'\{\{#\$output\.([^#{}]+)#\}\}', raw_content)
if match.group(1).strip() in fields
]
if placeholders:
content = raw_content[placeholders[-1].end() :]
if is_field_step:
inputs = form_data.get('inputs') or {}
def replace_placeholder(match: re.Match[str]) -> str:
field_name = match.group(1).strip()
field = fields.get(field_name)
value = inputs.get(field_name)
if not field or value in (None, '', []):
return ''
return f'{field_name}{_wecom_display_input_value(field, value)}'
content = re.sub(r'\{\{#\$output\.([^#{}]+)#\}\}', replace_placeholder, str(content))
kept_lines: list[str] = []
for line in str(content).splitlines():
placeholder = re.fullmatch(r'\s*\{\{#\$output\.([^#{}]+)#\}\}\s*', line)
if placeholder and placeholder.group(1) in field_names:
if placeholder and placeholder.group(1).strip() in fields:
continue
kept_lines.append(line)
return re.sub(r'\n{3,}', '\n\n', '\n'.join(kept_lines).strip())
def _wecom_display_input_value(field: dict, value: Any) -> str:
field_type = str(field.get('type') or 'text').strip().lower()
if field_type == 'file':
if isinstance(value, dict):
return str(value.get('url') or value.get('upload_file_id') or '1 file')
elif field_type == 'file-list' and isinstance(value, list):
return f'{len(value)} file(s)'
return str(value)
def build_button_interaction_payload(
form_data: dict,
task_id: str,
@@ -1150,7 +1167,6 @@ def build_button_interaction_payload(
'card_type': 'button_interaction',
'main_title': {
'title': node_title,
'desc': _wecom_form_desc(form_data),
},
'sub_title_text': sub_title_text,
'button_list': button_list,
@@ -1434,7 +1450,7 @@ def build_button_interaction_update_card(
"""Build the template_card body used to update a clicked form card."""
node_title = str(form_data.get('node_title') or '').strip() or '人工介入'
form_content = str(form_data.get('form_content') or '').strip()
form_content = _wecom_clean_form_content(form_data)
action_title = resolve_form_action_title(form_data, action_id)
clean_action_id = str(action_id or '').strip()
@@ -1528,15 +1544,19 @@ def build_multiple_interaction_update_card(
}
)
display_form_data = dict(form_data)
display_form_data['inputs'] = selected_values
form_content = _wecom_clean_form_content(display_form_data)
card: dict[str, Any] = {
'card_type': 'multiple_interaction',
'main_title': {
'title': node_title,
'desc': 'Submitted',
'desc': form_content,
},
'select_list': select_list,
'submit_button': {
'text': 'Submitted',
'text': '',
'key': 'submit_human_input',
},
'task_id': task_id,
@@ -169,11 +169,12 @@ def test_build_payload_can_emulate_select_as_buttons_for_wecombot_ws():
assert parse_select_button_action(card['button_list'][1]['key'], form_data) == {'choice': 'B'}
def test_text_input_card_shows_direct_reply_prompt():
def test_text_input_card_uses_current_stage_content_without_direct_reply_prompt():
payload = build_human_input_template_card_payload(
{
'node_title': '人工介入',
'form_content': '请输入你的问题\n\n{{#$output.us_input#}}',
'form_content': '11\n请输入你的问题\n\n{{#$output.us_input#}}',
'raw_form_content': ('11\n请输入你的问题\n{{#$output.us_input#}}\n请选择你的答案\n{{#$output.xiala#}}'),
'input_defs': [
{
'output_variable_name': 'us_input',
@@ -189,8 +190,8 @@ def test_text_input_card_shows_direct_reply_prompt():
)
card = payload['template_card']
assert card['main_title']['desc'] == '请直接回复:请输入你的问题'
assert '请直接回复:请输入你的问题' in card['sub_title_text']
assert 'desc' not in card['main_title']
assert card['sub_title_text'] == '11\n请输入你的问题'
assert card['button_list'] == []
@@ -198,6 +199,8 @@ def test_build_human_input_text_prompt_for_current_text_field():
prompt = build_human_input_text_prompt(
{
'node_title': '人工介入',
'form_content': '11\n请输入你的问题\n{{#$output.us_input#}}',
'raw_form_content': ('11\n请输入你的问题\n{{#$output.us_input#}}\n请选择你的答案\n{{#$output.xiala#}}'),
'input_defs': [
{
'output_variable_name': 'us_input',
@@ -209,7 +212,7 @@ def test_build_human_input_text_prompt_for_current_text_field():
}
)
assert prompt == '人工介入\n\n请直接回复:请输入你的问题'
assert prompt == '人工介入\n\n11\n请输入你的问题'
@pytest.mark.asyncio
@@ -229,6 +232,8 @@ async def test_ws_push_form_pause_sends_text_prompt_without_empty_card():
'msg-1',
{
'node_title': '人工介入',
'form_content': '11\n请输入你的问题\n{{#$output.us_input#}}',
'raw_form_content': ('11\n请输入你的问题\n{{#$output.us_input#}}\n请选择你的答案\n{{#$output.xiala#}}'),
'input_defs': [
{
'output_variable_name': 'us_input',
@@ -243,7 +248,7 @@ async def test_ws_push_form_pause_sends_text_prompt_without_empty_card():
assert ok is True
assert stream_id == 'stream-1'
assert task_id is None
assert sent == [('req-1', '人工介入\n\n请直接回复:请输入你的问题')]
assert sent == [('req-1', '人工介入\n\n11\n请输入你的问题')]
assert client._pending_forms_by_task == {}
assert 'msg-1' not in client._stream_ids
@@ -363,10 +368,12 @@ def test_extract_template_card_selections_reads_response_data_direct_mapping():
assert selections == {'choice': 'B'}
def test_build_multiple_interaction_update_card_disables_submitted_select():
def test_build_multiple_interaction_update_card_disables_selected_value_without_submitted_text():
card = build_multiple_interaction_update_card(
{
'node_title': 'Manual Review',
'form_content': 'Choose a label\n{{#$output.choice#}}',
'raw_form_content': 'Choose a label\n{{#$output.choice#}}',
'input_defs': [
{
'output_variable_name': 'choice',
@@ -374,12 +381,73 @@ def test_build_multiple_interaction_update_card_disables_submitted_select():
'option_source': {'type': 'constant', 'value': ['A', 'B']},
}
],
'_current_input_field': 'choice',
},
task_id='task-1',
selections={'choice': 'B'},
)
assert card['card_type'] == 'multiple_interaction'
assert card['main_title']['desc'] == 'Submitted'
assert card['main_title']['desc'] == 'Choose a label\n✅ choiceB'
assert card['submit_button']['text'] == ''
assert card['select_list'][0]['disable'] is True
assert card['select_list'][0]['selected_id'] == 'opt_2'
def test_select_stage_only_shows_current_prompt_in_a_separate_message():
raw_content = '11\n请输入你的问题\n{{#$output.us_input#}}\n请选择你的答案\n{{#$output.xiala#}}'
payload = build_human_input_template_card_payload(
{
'node_title': '人工介入',
'form_content': '请选择你的答案\n{{#$output.xiala#}}',
'raw_form_content': raw_content,
'input_defs': [
{
'output_variable_name': 'xiala',
'type': 'select',
'option_source': {'type': 'constant', 'value': ['1', '2']},
}
],
'all_input_defs': [
{'output_variable_name': 'us_input', 'type': 'paragraph'},
{
'output_variable_name': 'xiala',
'type': 'select',
'option_source': {'type': 'constant', 'value': ['1', '2']},
},
],
'inputs': {'us_input': '你叫啥'},
'_current_input_field': 'xiala',
},
task_id='task-1',
)
assert payload['template_card']['main_title']['desc'] == '请选择你的答案'
def test_action_stage_only_shows_content_after_fields_without_placeholders():
raw_content = '11\n请输入你的问题\n{{#$output.us_input#}}\n请选择你的答案\n{{#$output.xiala#}}\n请选择操作'
payload = build_human_input_template_card_payload(
{
'node_title': '人工介入',
'form_content': raw_content,
'raw_form_content': raw_content,
'input_defs': [],
'all_input_defs': [
{'output_variable_name': 'us_input', 'type': 'paragraph'},
{'output_variable_name': 'xiala', 'type': 'select'},
],
'inputs': {'us_input': '你叫啥', 'xiala': '2'},
'actions': [
{'id': 'yes', 'title': 'yes'},
{'id': 'no', 'title': 'no'},
],
'_action_select_only': True,
},
task_id='task-1',
)
card = payload['template_card']
assert card['sub_title_text'] == '请选择操作'
assert '{{#$output.' not in card['sub_title_text']
assert [button['text'] for button in card['button_list']] == ['yes', 'no']