mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-26 14:26:06 +00:00
feat: Refactor input handling and feedback messages across multiple adapters
This commit is contained in:
@@ -693,7 +693,7 @@ class QQOfficialClient:
|
|||||||
target_type: str,
|
target_type: str,
|
||||||
target_id: str,
|
target_id: str,
|
||||||
markdown_content: str,
|
markdown_content: str,
|
||||||
keyboard: dict,
|
keyboard: Optional[dict] = None,
|
||||||
msg_id: Optional[str] = None,
|
msg_id: Optional[str] = None,
|
||||||
event_id: Optional[str] = None,
|
event_id: Optional[str] = None,
|
||||||
msg_seq: int = 1,
|
msg_seq: int = 1,
|
||||||
@@ -734,9 +734,10 @@ class QQOfficialClient:
|
|||||||
body: dict[str, Any] = {
|
body: dict[str, Any] = {
|
||||||
'msg_type': 2,
|
'msg_type': 2,
|
||||||
'markdown': {'content': markdown_content},
|
'markdown': {'content': markdown_content},
|
||||||
'keyboard': keyboard,
|
|
||||||
'msg_seq': msg_seq,
|
'msg_seq': msg_seq,
|
||||||
}
|
}
|
||||||
|
if keyboard and keyboard.get('content', {}).get('rows'):
|
||||||
|
body['keyboard'] = keyboard
|
||||||
if msg_id:
|
if msg_id:
|
||||||
body['msg_id'] = msg_id
|
body['msg_id'] = msg_id
|
||||||
if event_id:
|
if event_id:
|
||||||
|
|||||||
@@ -1129,12 +1129,10 @@ def build_button_interaction_payload(
|
|||||||
input_hint_lines = _wecom_input_hint_lines(form_data)
|
input_hint_lines = _wecom_input_hint_lines(form_data)
|
||||||
if input_hint_lines:
|
if input_hint_lines:
|
||||||
sub_title_parts.append('Fill these fields in chat before choosing an action:\n' + '\n'.join(input_hint_lines))
|
sub_title_parts.append('Fill these fields in chat before choosing an action:\n' + '\n'.join(input_hint_lines))
|
||||||
elif should_show_actions and actions:
|
|
||||||
sub_title_parts.append('Choose an action to continue.')
|
|
||||||
if overflow:
|
if overflow:
|
||||||
extra_lines = [f' - {a.get("title") or a.get("id") or ""} (回复 id: {a.get("id") or ""})' for a in overflow]
|
extra_lines = [f' - {a.get("title") or a.get("id") or ""} (回复 id: {a.get("id") or ""})' for a in overflow]
|
||||||
sub_title_parts.append(f'另有 {len(overflow)} 个选项不在按钮列表中,可直接回复 id:\n' + '\n'.join(extra_lines))
|
sub_title_parts.append(f'另有 {len(overflow)} 个选项不在按钮列表中,可直接回复 id:\n' + '\n'.join(extra_lines))
|
||||||
sub_title_text = '\n\n'.join(sub_title_parts) or '请选择一个操作以继续。'
|
sub_title_text = '\n\n'.join(sub_title_parts)
|
||||||
|
|
||||||
button_list = []
|
button_list = []
|
||||||
for idx, action in enumerate(visible_actions):
|
for idx, action in enumerate(visible_actions):
|
||||||
@@ -1452,8 +1450,8 @@ def build_button_interaction_update_card(
|
|||||||
}
|
}
|
||||||
if action_key == clean_action_id:
|
if action_key == clean_action_id:
|
||||||
button['style'] = _wecom_button_style(action, selected=True)
|
button['style'] = _wecom_button_style(action, selected=True)
|
||||||
button['text'] = f'已选择:{button_title}'
|
button['text'] = f'✅ {button_title}'
|
||||||
button['replace_text'] = f'已选择:{button_title}'
|
button['replace_text'] = f'✅ {button_title}'
|
||||||
matched = True
|
matched = True
|
||||||
button_list.append(button)
|
button_list.append(button)
|
||||||
|
|
||||||
@@ -1463,7 +1461,7 @@ def build_button_interaction_update_card(
|
|||||||
'text': action_title or clean_action_id,
|
'text': action_title or clean_action_id,
|
||||||
'style': 1,
|
'style': 1,
|
||||||
'key': clean_action_id,
|
'key': clean_action_id,
|
||||||
'replace_text': f'已选择:{action_title or clean_action_id}',
|
'replace_text': f'✅ {action_title or clean_action_id}',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -296,14 +296,8 @@ def _dingtalk_completed_input_lines(form_data: dict) -> list[str]:
|
|||||||
value = inputs.get(field_name)
|
value = inputs.get(field_name)
|
||||||
if value in (None, '', []):
|
if value in (None, '', []):
|
||||||
continue
|
continue
|
||||||
field_type = _dingtalk_field_type(field)
|
|
||||||
display_value = _dingtalk_display_input_value(field, value)
|
display_value = _dingtalk_display_input_value(field, value)
|
||||||
if field_type == 'select':
|
lines.append(f'✅ {field_name}:{display_value}')
|
||||||
lines.append(f'✅ 已选择 {field_name}:{display_value}')
|
|
||||||
elif field_type in {'file', 'file-list'}:
|
|
||||||
lines.append(f'✅ 已上传 {field_name}:{display_value}')
|
|
||||||
else:
|
|
||||||
lines.append(f'✅ 已填写 {field_name}:{display_value}')
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
@@ -960,9 +954,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
input_hint_lines = [] if native_field else _dingtalk_input_hint_lines(form_data)
|
input_hint_lines = [] if native_field else _dingtalk_input_hint_lines(form_data)
|
||||||
if input_hint_lines:
|
if input_hint_lines:
|
||||||
parts.append('Fill these fields in chat before choosing an action:<br>' + '<br>'.join(input_hint_lines))
|
parts.append('Fill these fields in chat before choosing an action:<br>' + '<br>'.join(input_hint_lines))
|
||||||
elif should_show_actions and actions:
|
display_content = '<br><br>'.join(parts)
|
||||||
parts.append('Choose an action to continue.')
|
|
||||||
display_content = '<br><br>'.join(parts) or '请选择一个操作以继续。'
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.bot.update_card_data(
|
await self.bot.update_card_data(
|
||||||
@@ -1062,9 +1054,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
input_hint_lines = [] if native_field else _dingtalk_input_hint_lines(form_data)
|
input_hint_lines = [] if native_field else _dingtalk_input_hint_lines(form_data)
|
||||||
if input_hint_lines:
|
if input_hint_lines:
|
||||||
parts.append('Fill these fields in chat before choosing an action:<br>' + '<br>'.join(input_hint_lines))
|
parts.append('Fill these fields in chat before choosing an action:<br>' + '<br>'.join(input_hint_lines))
|
||||||
elif should_show_actions and actions:
|
display_content = '<br><br>'.join(parts)
|
||||||
parts.append('Choose an action to continue.')
|
|
||||||
display_content = '<br><br>'.join(parts) or '请选择一个操作以继续。'
|
|
||||||
|
|
||||||
btns = self._build_btns(actions if should_show_actions else [], out_track_id)
|
btns = self._build_btns(actions if should_show_actions else [], out_track_id)
|
||||||
|
|
||||||
@@ -1100,7 +1090,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
"""Create a new card for resumed-workflow streaming output.
|
"""Create a new card for resumed-workflow streaming output.
|
||||||
|
|
||||||
Used after a button click triggers a synthetic event — the form
|
Used after a button click triggers a synthetic event — the form
|
||||||
card stays put with the "已选择" notice, and a fresh card is
|
card stays put with the selection notice, and a fresh card is
|
||||||
spawned here for the LLM reply to stream into.
|
spawned here for the LLM reply to stream into.
|
||||||
"""
|
"""
|
||||||
form_template_id = (self.config.get('human_input_card_template_id') or '').strip()
|
form_template_id = (self.config.get('human_input_card_template_id') or '').strip()
|
||||||
@@ -1349,7 +1339,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Visual feedback on the form card itself: keep the prompt visible,
|
# Visual feedback on the form card itself: keep the prompt visible,
|
||||||
# add a "已选择" line, remove the buttons. The resumed-workflow
|
# add a selection line, remove the buttons. The resumed-workflow
|
||||||
# output lives on a separate new card (lazy-created in
|
# output lives on a separate new card (lazy-created in
|
||||||
# reply_message_chunk on the synthetic event), so the form card
|
# reply_message_chunk on the synthetic event), so the form card
|
||||||
# stays put as a record of the user's selection.
|
# stays put as a record of the user's selection.
|
||||||
@@ -1367,7 +1357,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
# Crucial: do NOT leave the form card's out_track_id in
|
# Crucial: do NOT leave the form card's out_track_id in
|
||||||
# active_turn_card — otherwise create_message_card for the
|
# active_turn_card — otherwise create_message_card for the
|
||||||
# synthetic event would reuse it for the resume output, painting
|
# synthetic event would reuse it for the resume output, painting
|
||||||
# the LLM reply on top of the "已选择" notice. Clear it so the
|
# the LLM reply on top of the selection notice. Clear it so the
|
||||||
# resume goes through the lazy-create path and spawns a fresh card.
|
# resume goes through the lazy-create path and spawns a fresh card.
|
||||||
session_key = state.get('session_key', '')
|
session_key = state.get('session_key', '')
|
||||||
if session_key and self.active_turn_card.get(session_key) == out_track_id:
|
if session_key and self.active_turn_card.get(session_key) == out_track_id:
|
||||||
@@ -1487,7 +1477,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Update the form card to acknowledge the user's selection.
|
"""Update the form card to acknowledge the user's selection.
|
||||||
|
|
||||||
Keeps the original prompt visible, adds a "已选择: X" notice, and
|
Keeps the original prompt visible, adds a selection notice, and
|
||||||
clears the buttons. The card stays as a permanent record of the
|
clears the buttons. The card stays as a permanent record of the
|
||||||
choice; the resumed workflow's output goes to a separate new card.
|
choice; the resumed workflow's output goes to a separate new card.
|
||||||
"""
|
"""
|
||||||
@@ -1504,7 +1494,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
)
|
)
|
||||||
if completed_lines and not all(line in form_content for line in completed_lines):
|
if completed_lines and not all(line in form_content for line in completed_lines):
|
||||||
parts.append('<hr>' + '<br>'.join(completed_lines))
|
parts.append('<hr>' + '<br>'.join(completed_lines))
|
||||||
parts.append(f'<hr>✅ 已选择:**{action_title}**')
|
parts.append(f'<hr>✅ {action_title}')
|
||||||
content = '<br><br>'.join(parts)
|
content = '<br><br>'.join(parts)
|
||||||
if self.ap is not None:
|
if self.ap is not None:
|
||||||
self.ap.logger.info(f'DingTalk _mark_card_resolved: out_track_id={out_track_id} action={action_title!r}')
|
self.ap.logger.info(f'DingTalk _mark_card_resolved: out_track_id={out_track_id} action={action_title!r}')
|
||||||
|
|||||||
@@ -1396,7 +1396,6 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
body_parts: list[str] = []
|
body_parts: list[str] = []
|
||||||
if form_content:
|
if form_content:
|
||||||
body_parts.append(form_content)
|
body_parts.append(form_content)
|
||||||
body_parts.append('Please select an option below:')
|
|
||||||
embed_body = '\n\n'.join(body_parts)
|
embed_body = '\n\n'.join(body_parts)
|
||||||
# Discord embed.description has a 4096 char limit — defensive trim.
|
# Discord embed.description has a 4096 char limit — defensive trim.
|
||||||
if len(embed_body) > 4000:
|
if len(embed_body) > 4000:
|
||||||
@@ -1589,7 +1588,7 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
stale: bool = False,
|
stale: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Disable all buttons on the form view and annotate the chosen
|
"""Disable all buttons on the form view and annotate the chosen
|
||||||
one — mirrors DingTalk/Lark's in-card 已选择 feedback."""
|
one — mirrors DingTalk/Lark's in-card selection feedback."""
|
||||||
try:
|
try:
|
||||||
for child in view.children:
|
for child in view.children:
|
||||||
if not isinstance(child, discord_ui.Button):
|
if not isinstance(child, discord_ui.Button):
|
||||||
|
|||||||
@@ -187,13 +187,7 @@ def _lark_completed_input_lines(form_data: dict) -> list[str]:
|
|||||||
if value in (None, '', []):
|
if value in (None, '', []):
|
||||||
continue
|
continue
|
||||||
display_value = _lark_display_input_value(field, value)
|
display_value = _lark_display_input_value(field, value)
|
||||||
field_type = _dify_field_type(field)
|
lines.append(f'✅ {field_name}:{display_value}')
|
||||||
if field_type == 'select':
|
|
||||||
lines.append(f'✅ 已选择 {field_name}:**{display_value}**')
|
|
||||||
elif field_type in {'file', 'file-list'}:
|
|
||||||
lines.append(f'✅ 已上传 {field_name}:**{display_value}**')
|
|
||||||
else:
|
|
||||||
lines.append(f'✅ 已填写 {field_name}:**{display_value}**')
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
@@ -2014,7 +2008,7 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
|
|
||||||
Supports Dify form-action resume: when the runner yields a chunk with
|
Supports Dify form-action resume: when the runner yields a chunk with
|
||||||
``_resume_from_form=True``, the card transitions from buttons to a
|
``_resume_from_form=True``, the card transitions from buttons to a
|
||||||
grey "已选择" notice and a new ``streaming_txt_resume`` element is added
|
grey selection notice and a new ``streaming_txt_resume`` element is added
|
||||||
for subsequent resume chunks to stream into.
|
for subsequent resume chunks to stream into.
|
||||||
|
|
||||||
When ``_open_new_card=True`` on the final chunk, the existing card is
|
When ``_open_new_card=True`` on the final chunk, the existing card is
|
||||||
@@ -2051,7 +2045,7 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
)
|
)
|
||||||
if completed_lines and not all(line in stored_form_content for line in completed_lines):
|
if completed_lines and not all(line in stored_form_content for line in completed_lines):
|
||||||
notice_parts.append('---\n' + '\n'.join(completed_lines))
|
notice_parts.append('---\n' + '\n'.join(completed_lines))
|
||||||
notice_parts.append(f'---\n✅ 已选择:**{action_title}**')
|
notice_parts.append(f'---\n✅ {action_title}')
|
||||||
selected_notice = '\n\n'.join(notice_parts)
|
selected_notice = '\n\n'.join(notice_parts)
|
||||||
else:
|
else:
|
||||||
selected_notice = ''
|
selected_notice = ''
|
||||||
@@ -2379,7 +2373,7 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
"""Update the entire card layout.
|
"""Update the entire card layout.
|
||||||
|
|
||||||
• form_data → show interactive buttons (initial Dify pause)
|
• form_data → show interactive buttons (initial Dify pause)
|
||||||
• notice_text → replace buttons with a grey "已选择" notice (resume transition)
|
• notice_text → replace buttons with a grey selection notice (resume transition)
|
||||||
• resume_placeholder_text → add a streaming_txt_resume markdown element
|
• resume_placeholder_text → add a streaming_txt_resume markdown element
|
||||||
"""
|
"""
|
||||||
form_data = form_data or {}
|
form_data = form_data or {}
|
||||||
|
|||||||
@@ -842,51 +842,22 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
|
|||||||
form_content = form_data.get('form_content') or ''
|
form_content = form_data.get('form_content') or ''
|
||||||
is_field_step = bool(form_data.get('_current_input_field')) and not form_data.get('_action_select_only')
|
is_field_step = bool(form_data.get('_current_input_field')) and not form_data.get('_action_select_only')
|
||||||
parts = [f'### {node_title}']
|
parts = [f'### {node_title}']
|
||||||
|
plain_parts = [node_title]
|
||||||
if form_content.strip():
|
if form_content.strip():
|
||||||
parts.append(form_content.strip())
|
parts.append(form_content.strip())
|
||||||
parts.append('请点击下方按钮选择:')
|
plain_parts.append(form_content.strip())
|
||||||
markdown_content = '\n\n'.join(parts)
|
markdown_content = '\n\n'.join(parts)
|
||||||
|
plain_content = '\n\n'.join(plain_parts)
|
||||||
|
|
||||||
keyboard = build_keyboard_from_select_field(form_data) if is_field_step else None
|
keyboard = build_keyboard_from_select_field(form_data) if is_field_step else None
|
||||||
if is_field_step and not keyboard.get('content', {}).get('rows'):
|
is_text_field_step = is_field_step and not keyboard.get('content', {}).get('rows')
|
||||||
field_parts = parts[:-1] if len(parts) > 1 else parts
|
if is_text_field_step:
|
||||||
text_msg = platform_message.MessageChain([platform_message.Plain(text='\n\n'.join(field_parts))])
|
keyboard = None
|
||||||
try:
|
if keyboard is None and not is_text_field_step:
|
||||||
await self.reply_message(message_source, text_msg)
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f'QQ Official: field-step text send failed: {traceback.format_exc()}')
|
|
||||||
return
|
|
||||||
|
|
||||||
sender_id = ''
|
|
||||||
if source is not None:
|
|
||||||
sender_id = (
|
|
||||||
getattr(source, 'user_openid', None)
|
|
||||||
or getattr(source, 'member_openid', None)
|
|
||||||
or getattr(source, 'd_author_id', None)
|
|
||||||
or ''
|
|
||||||
)
|
|
||||||
if not sender_id and message_source.sender is not None:
|
|
||||||
sender_id = str(getattr(message_source.sender, 'id', '') or '')
|
|
||||||
self._pending_forms[session_key] = {
|
|
||||||
'form_data': form_data,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
'sender_id': sender_id,
|
|
||||||
'target_type': target_type,
|
|
||||||
'target_id': target_id,
|
|
||||||
'source_event_t': source.t if source is not None else None,
|
|
||||||
'posted_at': time.time(),
|
|
||||||
}
|
|
||||||
await self.logger.info(
|
|
||||||
f'QQ Official: form field step posted session={session_key} '
|
|
||||||
f'field={form_data.get("_current_input_field")!r}'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if keyboard is None:
|
|
||||||
keyboard = build_keyboard_from_form(form_data, buttons_per_row=2)
|
keyboard = build_keyboard_from_form(form_data, buttons_per_row=2)
|
||||||
if not keyboard.get('content', {}).get('rows'):
|
if keyboard is not None and not keyboard.get('content', {}).get('rows') and not is_text_field_step:
|
||||||
# No actions to render — fall back to plain text.
|
# No actions to render — fall back to plain text.
|
||||||
text_msg = platform_message.MessageChain([platform_message.Plain(text=markdown_content)])
|
text_msg = platform_message.MessageChain([platform_message.Plain(text=plain_content)])
|
||||||
try:
|
try:
|
||||||
await self.reply_message(message_source, text_msg)
|
await self.reply_message(message_source, text_msg)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -935,7 +906,7 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
|
|||||||
await self.logger.error(
|
await self.logger.error(
|
||||||
f'QQ Official: send_markdown_keyboard failed, falling back to text: {traceback.format_exc()}'
|
f'QQ Official: send_markdown_keyboard failed, falling back to text: {traceback.format_exc()}'
|
||||||
)
|
)
|
||||||
text_msg = platform_message.MessageChain([platform_message.Plain(text=markdown_content)])
|
text_msg = platform_message.MessageChain([platform_message.Plain(text=plain_content)])
|
||||||
try:
|
try:
|
||||||
await self.reply_message(message_source, text_msg)
|
await self.reply_message(message_source, text_msg)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
action_title = self._form_action_titles[query.data]
|
action_title = self._form_action_titles[query.data]
|
||||||
try:
|
try:
|
||||||
original_text = query.message.text or ''
|
original_text = query.message.text or ''
|
||||||
selected_text = f'{original_text}\n\n✅ Selected: {action_title}'
|
selected_text = f'{original_text}\n\n✅ {action_title}'
|
||||||
await query.edit_message_text(text=selected_text, reply_markup=None)
|
await query.edit_message_text(text=selected_text, reply_markup=None)
|
||||||
except Exception:
|
except Exception:
|
||||||
# If edit fails (e.g. message too long), just pass
|
# If edit fails (e.g. message too long), just pass
|
||||||
@@ -687,15 +687,10 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
effective_message = update.effective_message
|
effective_message = update.effective_message
|
||||||
message_thread_id = getattr(effective_message, 'message_thread_id', None) if effective_message else None
|
message_thread_id = getattr(effective_message, 'message_thread_id', None) if effective_message else None
|
||||||
|
|
||||||
if is_select_field:
|
heading = f'[{node_title}]'
|
||||||
prompt = f'Please select {select_field}:'
|
text_lines = [heading]
|
||||||
elif is_field_step:
|
|
||||||
prompt = f'Please reply with a value for {current_field}.'
|
|
||||||
else:
|
|
||||||
prompt = 'Please select an action:'
|
|
||||||
text_lines = [f'[{node_title}] {prompt}']
|
|
||||||
if form_content:
|
if form_content:
|
||||||
text_lines.insert(0, form_content)
|
text_lines.append(form_content)
|
||||||
|
|
||||||
if oversized:
|
if oversized:
|
||||||
# callback_data exceeds Telegram's 64-byte limit — fall back to
|
# callback_data exceeds Telegram's 64-byte limit — fall back to
|
||||||
@@ -722,7 +717,7 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
# prompts even when they cannot read ordinary group messages.
|
# prompts even when they cannot read ordinary group messages.
|
||||||
'reply_markup': ForceReply(
|
'reply_markup': ForceReply(
|
||||||
selective=False,
|
selective=False,
|
||||||
input_field_placeholder=f'Enter {current_field}',
|
input_field_placeholder=current_field,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ def test_dingtalk_completed_input_lines_include_text_and_select_values():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert lines == ['✅ 已填写 comment:looks good', '✅ 已选择 choice:B']
|
assert lines == ['✅ comment:looks good', '✅ choice:B']
|
||||||
|
|
||||||
|
|
||||||
def test_dingtalk_clean_form_content_uses_all_input_defs():
|
def test_dingtalk_clean_form_content_uses_all_input_defs():
|
||||||
|
|||||||
@@ -109,9 +109,9 @@ def test_lark_completed_input_lines_include_text_select_and_files():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert lines == [
|
assert lines == [
|
||||||
'✅ 已填写 us_input:**你好**',
|
'✅ us_input:你好',
|
||||||
'✅ 已选择 xiala:**or**',
|
'✅ xiala:or',
|
||||||
'✅ 已上传 files:**2 file(s)**',
|
'✅ files:2 file(s)',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -193,4 +193,4 @@ def test_lark_completed_input_lines_display_select_value_from_object():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert lines == ['✅ 已选择 xiala:**B**']
|
assert lines == ['✅ xiala:B']
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ def _stream_test_adapter():
|
|||||||
adapter.logger = AsyncMock()
|
adapter.logger = AsyncMock()
|
||||||
adapter.bot = MagicMock()
|
adapter.bot = MagicMock()
|
||||||
adapter.bot.send_stream_msg = AsyncMock(return_value={'id': 'stream-1'})
|
adapter.bot.send_stream_msg = AsyncMock(return_value={'id': 'stream-1'})
|
||||||
|
adapter.bot.send_markdown_keyboard = AsyncMock(return_value={'id': 'message-1'})
|
||||||
adapter.ap = None
|
adapter.ap = None
|
||||||
adapter._stream_ctx = {}
|
adapter._stream_ctx = {}
|
||||||
adapter._stream_ctx_ts = {}
|
adapter._stream_ctx_ts = {}
|
||||||
@@ -142,6 +143,36 @@ async def test_qq_non_streaming_fallback_keeps_latest_snapshot_only():
|
|||||||
assert str(sent_chain) == 'Hello'
|
assert str(sent_chain) == 'Hello'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_qq_text_field_prompt_keeps_form_content():
|
||||||
|
from langbot.pkg.platform.sources.qqofficial import QQOfficialAdapter
|
||||||
|
|
||||||
|
adapter = _stream_test_adapter()
|
||||||
|
adapter._pending_forms = {}
|
||||||
|
adapter._session_event_ids = {}
|
||||||
|
adapter._anchor_msg_seq = {}
|
||||||
|
source = MagicMock()
|
||||||
|
source.d_id = 'source-1'
|
||||||
|
source.t = 'C2C_MESSAGE_CREATE'
|
||||||
|
event = MagicMock()
|
||||||
|
event.source_platform_object = source
|
||||||
|
event.sender.id = 'user-1'
|
||||||
|
form_data = {
|
||||||
|
'_current_input_field': 'us_input',
|
||||||
|
'node_title': 'Manual input',
|
||||||
|
'form_content': '1234\nEnter your question',
|
||||||
|
'input_defs': [{'output_variable_name': 'us_input', 'type': 'paragraph'}],
|
||||||
|
'actions': [{'id': 'yes', 'title': 'yes'}],
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(QQOfficialAdapter, '_resolve_target_from_event', return_value=('c2c', 'user-1')):
|
||||||
|
await adapter._handle_form_chunk(event, platform_message.MessageChain([]), form_data)
|
||||||
|
|
||||||
|
send_call = adapter.bot.send_markdown_keyboard.await_args.kwargs
|
||||||
|
assert send_call['markdown_content'] == '### Manual input\n\n1234\nEnter your question'
|
||||||
|
assert send_call['keyboard'] is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_qq_select_click_enqueues_input_progress_query():
|
async def test_qq_select_click_enqueues_input_progress_query():
|
||||||
import langbot.pkg.core.app # noqa: F401
|
import langbot.pkg.core.app # noqa: F401
|
||||||
|
|||||||
@@ -116,5 +116,8 @@ async def test_telegram_text_field_does_not_show_action_buttons():
|
|||||||
args = bot.send_message.await_args.kwargs
|
args = bot.send_message.await_args.kwargs
|
||||||
assert isinstance(args['reply_markup'], ForceReply)
|
assert isinstance(args['reply_markup'], ForceReply)
|
||||||
assert args['reply_markup'].selective is False
|
assert args['reply_markup'].selective is False
|
||||||
assert 'Please reply with a value for us_input.' in args['text']
|
assert args['reply_markup'].input_field_placeholder == 'us_input'
|
||||||
|
assert 'Please reply' not in args['text']
|
||||||
|
assert args['text'].startswith('[人工介入]')
|
||||||
|
assert 'us_input (paragraph)' in args['text']
|
||||||
assert adapter._form_action_titles == {}
|
assert adapter._form_action_titles == {}
|
||||||
|
|||||||
@@ -81,10 +81,10 @@ def test_build_button_interaction_update_card_marks_clicked_button():
|
|||||||
assert card['sub_title_text'] == 'Please choose one action.'
|
assert card['sub_title_text'] == 'Please choose one action.'
|
||||||
assert card['button_list'][0] == {'text': 'Approve', 'style': 2, 'key': 'approve'}
|
assert card['button_list'][0] == {'text': 'Approve', 'style': 2, 'key': 'approve'}
|
||||||
assert card['button_list'][1] == {
|
assert card['button_list'][1] == {
|
||||||
'text': '已选择:Reject',
|
'text': '✅ Reject',
|
||||||
'style': 1,
|
'style': 1,
|
||||||
'key': 'reject',
|
'key': 'reject',
|
||||||
'replace_text': '已选择:Reject',
|
'replace_text': '✅ Reject',
|
||||||
}
|
}
|
||||||
assert card['source'] == {'desc': 'LangBot'}
|
assert card['source'] == {'desc': 'LangBot'}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user