mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 12:40:59 +00:00
feat(dingtalk): enhance human input card functionality with streaming support and active turn management
- Updated the DingTalk card template to enable streaming mode and multi-update configuration. - Removed the obsolete delete_card method from DingTalkClient to streamline card management. - Enhanced DingTalkAdapter to manage active turn cards and accumulated streaming text, ensuring a seamless user experience during human input prompts. - Modified the create_message_card method to utilize existing active cards for resumed workflows, preventing duplication. - Improved the _paint_form_on_card method to update existing cards with human input prompts and buttons dynamically. - Updated the dingtalk_human_input_card.json template to reflect the new streaming capabilities and configuration options.
This commit is contained in:
@@ -35,7 +35,7 @@ def markdown_block(node_id, variable='content'):
|
|||||||
'valueType': 'fixed',
|
'valueType': 'fixed',
|
||||||
'condition': {'op': 'and', 'conditions': []},
|
'condition': {'op': 'and', 'conditions': []},
|
||||||
},
|
},
|
||||||
'isStreaming': False,
|
'isStreaming': True,
|
||||||
'enableLinkStatPoint': False,
|
'enableLinkStatPoint': False,
|
||||||
'linkStatPoint': {'type': 'dynamicString', 'content': '', 'i18n': False},
|
'linkStatPoint': {'type': 'dynamicString', 'content': '', 'i18n': False},
|
||||||
'linkStatPointParams': [],
|
'linkStatPointParams': [],
|
||||||
@@ -552,6 +552,7 @@ def build_editor_data():
|
|||||||
return {
|
return {
|
||||||
'schemaVersion': '3.0.0',
|
'schemaVersion': '3.0.0',
|
||||||
'schema': {
|
'schema': {
|
||||||
|
'config': {'update_multi': True, 'streaming_mode': True},
|
||||||
'componentsMap': components_map,
|
'componentsMap': components_map,
|
||||||
'componentsTree': [root],
|
'componentsTree': [root],
|
||||||
'i18n': {},
|
'i18n': {},
|
||||||
|
|||||||
@@ -760,36 +760,6 @@ class DingTalkClient:
|
|||||||
await self.logger.error(f'DingTalk update card error: {traceback.format_exc()}')
|
await self.logger.error(f'DingTalk update card error: {traceback.format_exc()}')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def delete_card(self, *, out_track_id: str) -> bool:
|
|
||||||
"""POST /v1.0/card/instances/delete — recall a delivered card.
|
|
||||||
|
|
||||||
Used to retroactively remove the initial streaming chat card when
|
|
||||||
the workflow turns out to be paused for human input — the prompt
|
|
||||||
and buttons then live entirely on the dedicated form card.
|
|
||||||
"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = f'{DINGTALK_OPENAPI_BASE}/v1.0/card/instances/delete'
|
|
||||||
headers = {
|
|
||||||
'x-acs-dingtalk-access-token': self.access_token,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
body = {'outTrackId': out_track_id, 'userIdType': 1}
|
|
||||||
try:
|
|
||||||
_stdout_logger.info('DingTalk delete_card request: out_track_id=%s', out_track_id)
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(url, headers=headers, json=body, timeout=30.0)
|
|
||||||
_stdout_logger.info(
|
|
||||||
'DingTalk delete_card response: status=%d body=%s',
|
|
||||||
response.status_code,
|
|
||||||
response.text[:300],
|
|
||||||
)
|
|
||||||
return response.status_code == 200
|
|
||||||
except Exception:
|
|
||||||
_stdout_logger.exception('DingTalk delete_card error')
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""启动 WebSocket 连接,监听消息"""
|
"""启动 WebSocket 连接,监听消息"""
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
|
|||||||
@@ -181,6 +181,15 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
# user_id_hint, current_text}. Lookup keys for the data-source pull endpoint and
|
# user_id_hint, current_text}. Lookup keys for the data-source pull endpoint and
|
||||||
# the STREAM card-action callback.
|
# the STREAM card-action callback.
|
||||||
card_state: dict
|
card_state: dict
|
||||||
|
# session_key → out_track_id of the currently-active card for the
|
||||||
|
# conversation turn. Lets resumed-workflow chunks (which arrive on a
|
||||||
|
# synthetic event with a fresh resp_message_id) keep updating the same
|
||||||
|
# card the user clicked instead of getting a new one.
|
||||||
|
active_turn_card: dict
|
||||||
|
# session_key → accumulated streaming text for the active turn. Read
|
||||||
|
# by _paint_form_on_card so the post-pause form keeps the streamed
|
||||||
|
# context above the new prompt.
|
||||||
|
active_turn_text: dict
|
||||||
ap: typing.Any = None
|
ap: typing.Any = None
|
||||||
bot_uuid: str = ''
|
bot_uuid: str = ''
|
||||||
|
|
||||||
@@ -208,6 +217,8 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
logger=logger,
|
logger=logger,
|
||||||
card_instance_id_dict={},
|
card_instance_id_dict={},
|
||||||
card_state={},
|
card_state={},
|
||||||
|
active_turn_card={},
|
||||||
|
active_turn_text={},
|
||||||
bot_account_id=bot_account_id,
|
bot_account_id=bot_account_id,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
listeners={},
|
listeners={},
|
||||||
@@ -281,11 +292,14 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
card_instance, card_instance_id = chat_card_entry
|
card_instance, card_instance_id = chat_card_entry
|
||||||
if content:
|
if content:
|
||||||
if form_template_id:
|
if form_template_id:
|
||||||
# The form template's MarkdownBlock has `isStreaming: false`
|
# The card content has already been written via
|
||||||
# — the streaming endpoint (PUT /v1.0/card/streaming) does
|
# update_card_data (in _paint_form_on_card and the
|
||||||
# not propagate to non-streaming components. Use the full
|
# initial card creation). The streaming endpoint
|
||||||
# update_card_data PUT instead so the content actually
|
# (PUT /v1.0/card/streaming) does not propagate
|
||||||
# appears in the card body.
|
# updates on cards whose content was last set via
|
||||||
|
# update_card_data — they take different code paths
|
||||||
|
# on the DingTalk client. Stick with update_card_data
|
||||||
|
# for the whole turn for consistency.
|
||||||
try:
|
try:
|
||||||
await self.bot.update_card_data(
|
await self.bot.update_card_data(
|
||||||
out_track_id=card_instance_id,
|
out_track_id=card_instance_id,
|
||||||
@@ -329,30 +343,54 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
return is_stream
|
return is_stream
|
||||||
|
|
||||||
async def create_message_card(self, message_id, event):
|
async def create_message_card(self, message_id, event):
|
||||||
# When a form template is configured, every card in the conversation
|
|
||||||
# uses it (chat output, form prompts, post-click states). The chat
|
|
||||||
# template fallback only kicks in if no form template is configured.
|
|
||||||
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()
|
||||||
legacy_template_id = self.config.get('card_template_id', '')
|
legacy_template_id = self.config.get('card_template_id', '')
|
||||||
|
|
||||||
# Synthetic events (e.g. card button clicks) have no inbound chatbot
|
# Synthetic events (button clicks): look up the card already in
|
||||||
# message — skip card creation. The lazy-create path in
|
# active_turn_card so reply_message_chunk can stream to it.
|
||||||
# reply_message_chunk will spawn a fresh card when the first
|
|
||||||
# non-empty resume chunk arrives.
|
|
||||||
if event is None or event.source_platform_object is None:
|
if event is None or event.source_platform_object is None:
|
||||||
|
if form_template_id:
|
||||||
|
session_key = self._session_key_from_event(event) if event is not None else ''
|
||||||
|
carry = self.active_turn_card.get(session_key, '') if session_key else ''
|
||||||
|
if carry:
|
||||||
|
self.card_instance_id_dict[message_id] = (None, carry)
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if form_template_id:
|
if form_template_id:
|
||||||
# Defer card creation to the first non-empty chunk. If the Dify
|
# Create one card with the form template, empty buttons,
|
||||||
# workflow pauses immediately for human input without producing
|
# pending state. Streaming writes content to it; form pause
|
||||||
# any LLM text first, no chat card is created at all — only the
|
# paints buttons onto it. One card per turn, no duplication.
|
||||||
# form card gets delivered. Lazy-create lives in
|
incoming_message = event.source_platform_object.incoming_message
|
||||||
# reply_message_chunk → _lazy_create_resume_chat_card.
|
out_track_id = uuid.uuid4().hex
|
||||||
return False
|
is_group = str(incoming_message.conversation_type) == '2'
|
||||||
|
if is_group:
|
||||||
|
open_space_id = f'dtv1.card//IM_GROUP.{incoming_message.conversation_id}'
|
||||||
|
else:
|
||||||
|
open_space_id = f'dtv1.card//IM_ROBOT.{incoming_message.sender_staff_id}'
|
||||||
|
try:
|
||||||
|
await self.bot.create_and_deliver_card(
|
||||||
|
card_template_id=form_template_id,
|
||||||
|
out_track_id=out_track_id,
|
||||||
|
open_space_id=open_space_id,
|
||||||
|
is_group=is_group,
|
||||||
|
card_param_map={'content': '', 'btns': '[]', 'flowStatus': '1'},
|
||||||
|
callback_type='STREAM',
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
if self.ap is not None:
|
||||||
|
self.ap.logger.exception('DingTalk: create form-template card failed')
|
||||||
|
return False
|
||||||
|
self.card_instance_id_dict[message_id] = (None, out_track_id)
|
||||||
|
session_key = self._session_key_from_event(event)
|
||||||
|
if session_key:
|
||||||
|
self.active_turn_card[session_key] = out_track_id
|
||||||
|
self.active_turn_text[session_key] = ''
|
||||||
|
return True
|
||||||
|
|
||||||
# Legacy chat-card path (no form template configured).
|
# Legacy chat-card path (no form template).
|
||||||
incoming_message = event.source_platform_object.incoming_message
|
incoming_message = event.source_platform_object.incoming_message
|
||||||
card_auto_layout = self.config.get('card_ auto_layout', False)
|
card_auto_layout = self.config.get('card_auto_layout', False)
|
||||||
card_instance, card_instance_id = await self.bot.create_and_card(
|
card_instance, card_instance_id = await self.bot.create_and_card(
|
||||||
legacy_template_id, incoming_message, card_auto_layout=card_auto_layout
|
legacy_template_id, incoming_message, card_auto_layout=card_auto_layout
|
||||||
)
|
)
|
||||||
@@ -460,12 +498,12 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
message: platform_message.MessageChain,
|
message: platform_message.MessageChain,
|
||||||
form_data: dict,
|
form_data: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Finalize the current chat card and deliver a new form card.
|
"""Surface human-input prompt + buttons on the active card.
|
||||||
|
|
||||||
Multi-card flow: every Dify pause spawns its own card. The card the
|
In single-card mode (form_template_id configured): update the
|
||||||
chat was streaming into (if any) is closed out via streaming_update
|
EXISTING card with form buttons so it transitions from streaming
|
||||||
with finished=True so its spinner stops; a fresh card is then
|
output to prompt+buttons on the same card. In legacy mode:
|
||||||
delivered carrying the prompt + buttons.
|
finalize the chat card and deliver a separate form card.
|
||||||
"""
|
"""
|
||||||
if self.ap is not None:
|
if self.ap is not None:
|
||||||
self.ap.logger.info(
|
self.ap.logger.info(
|
||||||
@@ -475,9 +513,25 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
message_id = bot_message.resp_message_id
|
message_id = bot_message.resp_message_id
|
||||||
template_id = (self.config.get('human_input_card_template_id') or '').strip()
|
template_id = (self.config.get('human_input_card_template_id') or '').strip()
|
||||||
|
|
||||||
# Finalize the previous chat card so its spinner stops. Use the
|
if template_id:
|
||||||
# already-streamed text as the final content (or zero-width space
|
# Single-card mode: paint prompt + buttons onto the existing card.
|
||||||
# when nothing streamed, to satisfy any non-empty-content guards).
|
session_key = self._session_key_from_event(message_source)
|
||||||
|
entry = self.card_instance_id_dict.get(message_id)
|
||||||
|
out_track_id = entry[1] if entry else None
|
||||||
|
if not out_track_id and session_key:
|
||||||
|
out_track_id = self.active_turn_card.get(session_key, '')
|
||||||
|
if out_track_id:
|
||||||
|
await self._paint_form_on_card(message_source, out_track_id, form_data, session_key)
|
||||||
|
self.card_instance_id_dict.pop(message_id, None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# No existing card (e.g. Dify paused immediately with no LLM
|
||||||
|
# output before the pause). Create a form card directly.
|
||||||
|
await self._send_form_card(message_source, form_data, template_id)
|
||||||
|
self.card_instance_id_dict.pop(message_id, None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Legacy mode: finalize the streaming card with text fallback.
|
||||||
chat_card_entry = self.card_instance_id_dict.pop(message_id, None)
|
chat_card_entry = self.card_instance_id_dict.pop(message_id, None)
|
||||||
if chat_card_entry is not None:
|
if chat_card_entry is not None:
|
||||||
_, chat_out_track_id = chat_card_entry
|
_, chat_out_track_id = chat_card_entry
|
||||||
@@ -489,24 +543,93 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
await self.bot.send_card_message(None, chat_out_track_id, text_content or '', True)
|
await self.bot.send_card_message(None, chat_out_track_id, text_content or '', True)
|
||||||
except Exception:
|
except Exception:
|
||||||
await self.logger.error(f'DingTalk: finalize chat card before form failed: {traceback.format_exc()}')
|
await self.logger.error(f'DingTalk: finalize chat card before form failed: {traceback.format_exc()}')
|
||||||
# When the chat card uses the form template, also flip flowStatus
|
|
||||||
# to 3 so it leaves the pending state visibly.
|
|
||||||
if template_id:
|
|
||||||
try:
|
|
||||||
await self.bot.update_card_data(
|
|
||||||
out_track_id=chat_out_track_id,
|
|
||||||
card_param_map={'flowStatus': '3'},
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not template_id:
|
await self.send_message_text_form(message_source, form_data)
|
||||||
# No form template configured — fall back to plain text so users
|
|
||||||
# can still reply with the option number or title.
|
async def _paint_form_on_card(
|
||||||
|
self,
|
||||||
|
message_source: platform_events.MessageEvent,
|
||||||
|
out_track_id: str,
|
||||||
|
form_data: dict,
|
||||||
|
session_key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Update an existing card's content + buttons for human-input."""
|
||||||
|
actions = list(form_data.get('actions') or [])
|
||||||
|
node_title = form_data.get('node_title', '') or 'Human Input Required'
|
||||||
|
form_content = form_data.get('form_content', '') or ''
|
||||||
|
|
||||||
|
# Record form state for the click-handler.
|
||||||
|
launcher_type, launcher_id, sender_user_id = self._derive_session_descriptor(message_source)
|
||||||
|
self.card_state[out_track_id] = {
|
||||||
|
'session_key': session_key,
|
||||||
|
'launcher_type': launcher_type.value,
|
||||||
|
'launcher_id': launcher_id,
|
||||||
|
'sender_user_id': sender_user_id,
|
||||||
|
'form_token': form_data.get('form_token', ''),
|
||||||
|
'workflow_run_id': form_data.get('workflow_run_id', ''),
|
||||||
|
'actions': actions,
|
||||||
|
'node_title': node_title,
|
||||||
|
'form_content': form_content,
|
||||||
|
}
|
||||||
|
|
||||||
|
btns = self._build_btns(actions, out_track_id)
|
||||||
|
parts: list[str] = []
|
||||||
|
prior = self.active_turn_text.get(session_key, '') if session_key else ''
|
||||||
|
if prior.strip():
|
||||||
|
parts.append(prior.rstrip())
|
||||||
|
parts.append('---')
|
||||||
|
if node_title:
|
||||||
|
parts.append(f'**{node_title}**')
|
||||||
|
if form_content:
|
||||||
|
parts.append(form_content)
|
||||||
|
display_content = '\n\n'.join(parts) or '请选择一个操作以继续。'
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.bot.update_card_data(
|
||||||
|
out_track_id=out_track_id,
|
||||||
|
card_param_map={
|
||||||
|
'content': display_content,
|
||||||
|
'btns': json.dumps(btns, ensure_ascii=False),
|
||||||
|
'flowStatus': '3',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
if self.ap is not None:
|
||||||
|
self.ap.logger.exception('DingTalk: paint form on card failed')
|
||||||
await self.send_message_text_form(message_source, form_data)
|
await self.send_message_text_form(message_source, form_data)
|
||||||
return
|
return
|
||||||
|
|
||||||
await self._send_form_card(message_source, form_data, template_id)
|
if session_key:
|
||||||
|
self.active_turn_text[session_key] = display_content
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_btns(actions: list, out_track_id: str) -> list:
|
||||||
|
btns = []
|
||||||
|
for idx, action in enumerate(actions):
|
||||||
|
action_id = str(action.get('id') or '')
|
||||||
|
title = str(action.get('title') or action_id or f'选项 {idx + 1}')
|
||||||
|
style = (action.get('button_style') or '').lower()
|
||||||
|
if style == 'primary' or (style == '' and idx == 0):
|
||||||
|
color = 'blue'
|
||||||
|
elif style == 'danger':
|
||||||
|
color = 'red'
|
||||||
|
else:
|
||||||
|
color = 'gray'
|
||||||
|
btns.append(
|
||||||
|
{
|
||||||
|
'text': title,
|
||||||
|
'color': color,
|
||||||
|
'status': 'normal',
|
||||||
|
'event': {
|
||||||
|
'type': 'sendCardRequest',
|
||||||
|
'params': {
|
||||||
|
'actionId': action_id,
|
||||||
|
'params': {'action_id': action_id, 'out_track_id': out_track_id},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return btns
|
||||||
|
|
||||||
async def _send_form_card(
|
async def _send_form_card(
|
||||||
self,
|
self,
|
||||||
@@ -601,11 +724,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
) -> typing.Optional[tuple]:
|
) -> typing.Optional[tuple]:
|
||||||
"""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 — no inbound
|
Used after a button click triggers a synthetic event — the form
|
||||||
chatbot message means no card was created upstream, so we spin one
|
card stays put with the "已选择" notice, and a fresh card is
|
||||||
up here when the first non-empty chunk arrives. Prefers the form
|
spawned here for the LLM reply to stream into.
|
||||||
template (so empty `btns` keep the layout consistent across the
|
|
||||||
whole conversation); falls back to the legacy chat template.
|
|
||||||
"""
|
"""
|
||||||
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()
|
||||||
legacy_template_id = (self.config.get('card_template_id') or '').strip()
|
legacy_template_id = (self.config.get('card_template_id') or '').strip()
|
||||||
@@ -614,12 +735,6 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
return None
|
return None
|
||||||
out_track_id = uuid.uuid4().hex
|
out_track_id = uuid.uuid4().hex
|
||||||
open_space_id, is_group = self._derive_open_space(message_source)
|
open_space_id, is_group = self._derive_open_space(message_source)
|
||||||
if self.ap is not None:
|
|
||||||
self.ap.logger.info(
|
|
||||||
f'DingTalk _lazy_create_resume_chat_card: out_track_id={out_track_id} '
|
|
||||||
f'open_space_id={open_space_id} is_group={is_group} '
|
|
||||||
f'using_form_template={bool(form_template_id)}'
|
|
||||||
)
|
|
||||||
if form_template_id:
|
if form_template_id:
|
||||||
card_param_map = {'content': '', 'btns': '[]', 'flowStatus': '1'}
|
card_param_map = {'content': '', 'btns': '[]', 'flowStatus': '1'}
|
||||||
card_data_config = None
|
card_data_config = None
|
||||||
@@ -644,6 +759,12 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
return None
|
return None
|
||||||
entry = (None, out_track_id)
|
entry = (None, out_track_id)
|
||||||
self.card_instance_id_dict[message_id] = entry
|
self.card_instance_id_dict[message_id] = entry
|
||||||
|
# Register as the active card so any further chunks on this turn
|
||||||
|
# (and a subsequent re-pause) land on the same new card.
|
||||||
|
session_key = self._session_key_from_event(message_source)
|
||||||
|
if session_key:
|
||||||
|
self.active_turn_card[session_key] = out_track_id
|
||||||
|
self.active_turn_text[session_key] = ''
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
async def send_message_text_form(
|
async def send_message_text_form(
|
||||||
@@ -813,8 +934,11 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
self.ap.logger.exception('DingTalk: enqueue form action query failed')
|
self.ap.logger.exception('DingTalk: enqueue form action query failed')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Visual feedback: collapse the form card to a "已选择" notice so
|
# Visual feedback on the form card itself: keep the prompt visible,
|
||||||
# the user knows the click registered while the workflow resumes.
|
# add a "已选择" 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.
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self._mark_card_resolved(
|
self._mark_card_resolved(
|
||||||
out_track_id,
|
out_track_id,
|
||||||
@@ -824,6 +948,16 @@ 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
|
||||||
|
# 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:
|
||||||
|
self.active_turn_card.pop(session_key, None)
|
||||||
|
self.active_turn_text.pop(session_key, None)
|
||||||
|
|
||||||
# Once consumed, drop the state — the runner clears _PENDING_FORMS too.
|
# Once consumed, drop the state — the runner clears _PENDING_FORMS too.
|
||||||
self.card_state.pop(out_track_id, None)
|
self.card_state.pop(out_track_id, None)
|
||||||
|
|
||||||
@@ -837,12 +971,9 @@ 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.
|
||||||
|
|
||||||
We rewrite the card content with the original prompt + a green tick
|
Keeps the original prompt visible, adds a "已选择: X" notice, and
|
||||||
marker, and explicitly clear ``btns`` so the buttons are removed
|
clears the buttons. The card stays as a permanent record of the
|
||||||
once chosen. ``flowStatus`` is re-sent because some DingTalk clients
|
choice; the resumed workflow's output goes to a separate new card.
|
||||||
treat the PUT update as a partial *replace* of cardParamMap rather
|
|
||||||
than a merge — without it, the AICardContainer status containers
|
|
||||||
would all gate to ``gone`` and the whole card would blank out.
|
|
||||||
"""
|
"""
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
if node_title:
|
if node_title:
|
||||||
@@ -863,4 +994,5 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
await self.logger.error(f'DingTalk: update form card after click failed: {traceback.format_exc()}')
|
if self.ap is not None:
|
||||||
|
self.ap.logger.exception('DingTalk: mark card resolved failed')
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user