diff --git a/src/langbot/libs/qq_official_api/api.py b/src/langbot/libs/qq_official_api/api.py
index 69ab414a8..0ba1917d9 100644
--- a/src/langbot/libs/qq_official_api/api.py
+++ b/src/langbot/libs/qq_official_api/api.py
@@ -693,7 +693,7 @@ class QQOfficialClient:
target_type: str,
target_id: str,
markdown_content: str,
- keyboard: dict,
+ keyboard: Optional[dict] = None,
msg_id: Optional[str] = None,
event_id: Optional[str] = None,
msg_seq: int = 1,
@@ -734,9 +734,10 @@ class QQOfficialClient:
body: dict[str, Any] = {
'msg_type': 2,
'markdown': {'content': markdown_content},
- 'keyboard': keyboard,
'msg_seq': msg_seq,
}
+ if keyboard and keyboard.get('content', {}).get('rows'):
+ body['keyboard'] = keyboard
if msg_id:
body['msg_id'] = msg_id
if event_id:
diff --git a/src/langbot/libs/wecom_ai_bot_api/api.py b/src/langbot/libs/wecom_ai_bot_api/api.py
index 60a3f45ba..4bbe30222 100644
--- a/src/langbot/libs/wecom_ai_bot_api/api.py
+++ b/src/langbot/libs/wecom_ai_bot_api/api.py
@@ -1129,12 +1129,10 @@ def build_button_interaction_payload(
input_hint_lines = _wecom_input_hint_lines(form_data)
if 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:
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_text = '\n\n'.join(sub_title_parts) or '请选择一个操作以继续。'
+ sub_title_text = '\n\n'.join(sub_title_parts)
button_list = []
for idx, action in enumerate(visible_actions):
@@ -1452,8 +1450,8 @@ def build_button_interaction_update_card(
}
if action_key == clean_action_id:
button['style'] = _wecom_button_style(action, selected=True)
- button['text'] = f'已选择:{button_title}'
- button['replace_text'] = f'已选择:{button_title}'
+ button['text'] = f'✅ {button_title}'
+ button['replace_text'] = f'✅ {button_title}'
matched = True
button_list.append(button)
@@ -1463,7 +1461,7 @@ def build_button_interaction_update_card(
'text': action_title or clean_action_id,
'style': 1,
'key': clean_action_id,
- 'replace_text': f'已选择:{action_title or clean_action_id}',
+ 'replace_text': f'✅ {action_title or clean_action_id}',
}
)
diff --git a/src/langbot/pkg/platform/sources/dingtalk.py b/src/langbot/pkg/platform/sources/dingtalk.py
index e412cf0ae..7e7a7e16a 100644
--- a/src/langbot/pkg/platform/sources/dingtalk.py
+++ b/src/langbot/pkg/platform/sources/dingtalk.py
@@ -296,14 +296,8 @@ def _dingtalk_completed_input_lines(form_data: dict) -> list[str]:
value = inputs.get(field_name)
if value in (None, '', []):
continue
- field_type = _dingtalk_field_type(field)
display_value = _dingtalk_display_input_value(field, 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}')
+ lines.append(f'✅ {field_name}:{display_value}')
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)
if input_hint_lines:
parts.append('Fill these fields in chat before choosing an action:
' + '
'.join(input_hint_lines))
- elif should_show_actions and actions:
- parts.append('Choose an action to continue.')
- display_content = '
'.join(parts) or '请选择一个操作以继续。'
+ display_content = '
'.join(parts)
try:
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)
if input_hint_lines:
parts.append('Fill these fields in chat before choosing an action:
' + '
'.join(input_hint_lines))
- elif should_show_actions and actions:
- parts.append('Choose an action to continue.')
- display_content = '
'.join(parts) or '请选择一个操作以继续。'
+ display_content = '
'.join(parts)
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.
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.
"""
form_template_id = (self.config.get('human_input_card_template_id') or '').strip()
@@ -1349,7 +1339,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
return
# 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
# reply_message_chunk on the synthetic event), so the form card
# 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
# active_turn_card — otherwise create_message_card for the
# 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.
session_key = state.get('session_key', '')
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:
"""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
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):
parts.append('
' + '
'.join(completed_lines))
- parts.append(f'
✅ 已选择:**{action_title}**')
+ parts.append(f'
✅ {action_title}')
content = '
'.join(parts)
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}')
diff --git a/src/langbot/pkg/platform/sources/discord.py b/src/langbot/pkg/platform/sources/discord.py
index 9613e9bcc..3cb13a251 100644
--- a/src/langbot/pkg/platform/sources/discord.py
+++ b/src/langbot/pkg/platform/sources/discord.py
@@ -1396,7 +1396,6 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
body_parts: list[str] = []
if form_content:
body_parts.append(form_content)
- body_parts.append('Please select an option below:')
embed_body = '\n\n'.join(body_parts)
# Discord embed.description has a 4096 char limit — defensive trim.
if len(embed_body) > 4000:
@@ -1589,7 +1588,7 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
stale: bool = False,
) -> None:
"""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:
for child in view.children:
if not isinstance(child, discord_ui.Button):
diff --git a/src/langbot/pkg/platform/sources/lark.py b/src/langbot/pkg/platform/sources/lark.py
index e100bab9f..23dcfd90c 100644
--- a/src/langbot/pkg/platform/sources/lark.py
+++ b/src/langbot/pkg/platform/sources/lark.py
@@ -187,13 +187,7 @@ def _lark_completed_input_lines(form_data: dict) -> list[str]:
if value in (None, '', []):
continue
display_value = _lark_display_input_value(field, value)
- field_type = _dify_field_type(field)
- 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}**')
+ lines.append(f'✅ {field_name}:{display_value}')
return lines
@@ -2014,7 +2008,7 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
Supports Dify form-action resume: when the runner yields a chunk with
``_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.
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):
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)
else:
selected_notice = ''
@@ -2379,7 +2373,7 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
"""Update the entire card layout.
• 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
"""
form_data = form_data or {}
diff --git a/src/langbot/pkg/platform/sources/qqofficial.py b/src/langbot/pkg/platform/sources/qqofficial.py
index f5923bf1c..3369391e8 100644
--- a/src/langbot/pkg/platform/sources/qqofficial.py
+++ b/src/langbot/pkg/platform/sources/qqofficial.py
@@ -842,51 +842,22 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
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')
parts = [f'### {node_title}']
+ plain_parts = [node_title]
if form_content.strip():
parts.append(form_content.strip())
- parts.append('请点击下方按钮选择:')
+ plain_parts.append(form_content.strip())
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
- if is_field_step and not keyboard.get('content', {}).get('rows'):
- field_parts = parts[:-1] if len(parts) > 1 else parts
- text_msg = platform_message.MessageChain([platform_message.Plain(text='\n\n'.join(field_parts))])
- try:
- 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:
+ is_text_field_step = is_field_step and not keyboard.get('content', {}).get('rows')
+ if is_text_field_step:
+ keyboard = None
+ if keyboard is None and not is_text_field_step:
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.
- text_msg = platform_message.MessageChain([platform_message.Plain(text=markdown_content)])
+ text_msg = platform_message.MessageChain([platform_message.Plain(text=plain_content)])
try:
await self.reply_message(message_source, text_msg)
except Exception:
@@ -935,7 +906,7 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter
await self.logger.error(
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:
await self.reply_message(message_source, text_msg)
except Exception:
diff --git a/src/langbot/pkg/platform/sources/telegram.py b/src/langbot/pkg/platform/sources/telegram.py
index afb583674..62d5ee49c 100644
--- a/src/langbot/pkg/platform/sources/telegram.py
+++ b/src/langbot/pkg/platform/sources/telegram.py
@@ -304,7 +304,7 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
action_title = self._form_action_titles[query.data]
try:
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)
except Exception:
# 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
message_thread_id = getattr(effective_message, 'message_thread_id', None) if effective_message else None
- if is_select_field:
- prompt = f'Please select {select_field}:'
- 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}']
+ heading = f'[{node_title}]'
+ text_lines = [heading]
if form_content:
- text_lines.insert(0, form_content)
+ text_lines.append(form_content)
if oversized:
# 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.
'reply_markup': ForceReply(
selective=False,
- input_field_placeholder=f'Enter {current_field}',
+ input_field_placeholder=current_field,
),
}
else:
diff --git a/tests/unit_tests/platform/test_dingtalk_adapter.py b/tests/unit_tests/platform/test_dingtalk_adapter.py
index 0850eab08..bf51cf55a 100644
--- a/tests/unit_tests/platform/test_dingtalk_adapter.py
+++ b/tests/unit_tests/platform/test_dingtalk_adapter.py
@@ -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():
diff --git a/tests/unit_tests/platform/test_lark_adapter.py b/tests/unit_tests/platform/test_lark_adapter.py
index 933d27cf0..59b91ee60 100644
--- a/tests/unit_tests/platform/test_lark_adapter.py
+++ b/tests/unit_tests/platform/test_lark_adapter.py
@@ -109,9 +109,9 @@ def test_lark_completed_input_lines_include_text_select_and_files():
)
assert lines == [
- '✅ 已填写 us_input:**你好**',
- '✅ 已选择 xiala:**or**',
- '✅ 已上传 files:**2 file(s)**',
+ '✅ us_input:你好',
+ '✅ xiala:or',
+ '✅ 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']
diff --git a/tests/unit_tests/platform/test_qqofficial_api.py b/tests/unit_tests/platform/test_qqofficial_api.py
index db86a2483..0f791f612 100644
--- a/tests/unit_tests/platform/test_qqofficial_api.py
+++ b/tests/unit_tests/platform/test_qqofficial_api.py
@@ -75,6 +75,7 @@ def _stream_test_adapter():
adapter.logger = AsyncMock()
adapter.bot = MagicMock()
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._stream_ctx = {}
adapter._stream_ctx_ts = {}
@@ -142,6 +143,36 @@ async def test_qq_non_streaming_fallback_keeps_latest_snapshot_only():
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
async def test_qq_select_click_enqueues_input_progress_query():
import langbot.pkg.core.app # noqa: F401
diff --git a/tests/unit_tests/platform/test_telegram_adapter.py b/tests/unit_tests/platform/test_telegram_adapter.py
index 247d51b16..f3308f16e 100644
--- a/tests/unit_tests/platform/test_telegram_adapter.py
+++ b/tests/unit_tests/platform/test_telegram_adapter.py
@@ -116,5 +116,8 @@ async def test_telegram_text_field_does_not_show_action_buttons():
args = bot.send_message.await_args.kwargs
assert isinstance(args['reply_markup'], ForceReply)
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 == {}
diff --git a/tests/unit_tests/platform/test_wecombot_template_card.py b/tests/unit_tests/platform/test_wecombot_template_card.py
index 7bdb387ca..0fc650aa2 100644
--- a/tests/unit_tests/platform/test_wecombot_template_card.py
+++ b/tests/unit_tests/platform/test_wecombot_template_card.py
@@ -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['button_list'][0] == {'text': 'Approve', 'style': 2, 'key': 'approve'}
assert card['button_list'][1] == {
- 'text': '已选择:Reject',
+ 'text': '✅ Reject',
'style': 1,
'key': 'reject',
- 'replace_text': '已选择:Reject',
+ 'replace_text': '✅ Reject',
}
assert card['source'] == {'desc': 'LangBot'}