diff --git a/scripts/build_dingtalk_card_template.py b/scripts/build_dingtalk_card_template.py index bb47af3af..c07c73d33 100644 --- a/scripts/build_dingtalk_card_template.py +++ b/scripts/build_dingtalk_card_template.py @@ -220,6 +220,56 @@ def button_group(node_id): } +def avatar(node_id, *, name='LangBot', image_variable='bot_avatar'): + """Avatar component in `userInfo` mode — renders the bot's avatar + image and nickname as a header row above the response content. + Mirrors the layout from `I:\\下载\\dingtalk_1782120006374.json` where + Avatar sits at the top of the done-state AICardContent. + + `imageUrl` is bound to a global variable (default `bot_avatar`) so + the adapter can populate it at runtime with a DingTalk media id + (``@xxx``) obtained from the /media/upload endpoint. DingTalk's + Avatar.imageUrl resolver rejects external URLs — it only accepts + DingTalk-hosted media ids, so this binding is the only path to + a custom avatar. + """ + return { + 'componentName': 'Avatar', + 'id': node_id, + 'props': { + 'imageUrl': { + 'value': '', + 'valueType': 'variable', + 'type': 'dynamicImage', + 'variable': image_variable, + 'variableType': 'global', + }, + 'name': {'i18n': False, 'type': 'dynamicString', 'content': name}, + 'sizeType': 'Standard', + 'size': 'extraSmall', + 'customSize': 48, + 'marginLeft': 12, + 'marginRight': 12, + 'marginTop': 6, + 'marginBottom': 6, + 'visible': { + 'type': 'dynamicVisible', + 'value': True, + 'valueType': 'fixed', + 'condition': {'op': 'and', 'conditions': []}, + }, + 'mode': 'userInfo', + 'margin': -2, + 'innerOffset': 0, + }, + 'title': '头像', + 'hidden': False, + 'isLocked': False, + 'condition': True, + 'conditionGroup': '', + } + + def build_editor_data(): component_names = [ 'AIPending', @@ -229,6 +279,7 @@ def build_editor_data(): 'AICardContainer', 'ButtonGroup', 'MarkdownBlock', + 'Avatar', ] components_map = [ { @@ -344,6 +395,7 @@ def build_editor_data(): 'condition': True, 'conditionGroup': '', 'children': [ + avatar('node_avatar', name='LangBot'), markdown_block('node_text_content', variable='content'), button_group('node_btn_group'), ], @@ -722,6 +774,15 @@ def build_editor_data(): 'disabled': True, 'visible': False, }, + { + 'id': 'bot_avatar', + 'type': 'string', + 'name': 'bot_avatar', + 'description': '机器人头像 DingTalk 媒体 ID(@xxx 格式,启动时由 /media/upload 拿到)', + 'private': False, + 'editorVarType': 'variables', + 'disabled': False, + }, btns_var, ], 'formList': [], diff --git a/src/langbot/libs/dingtalk_api/api.py b/src/langbot/libs/dingtalk_api/api.py index b38fafdd4..552989ed1 100644 --- a/src/langbot/libs/dingtalk_api/api.py +++ b/src/langbot/libs/dingtalk_api/api.py @@ -2,7 +2,9 @@ import asyncio import base64 import json import logging +import os import time +import typing import uuid import urllib.parse from typing import Awaitable, Callable, Optional @@ -57,6 +59,12 @@ class DingTalkClient: self.access_token_expiry_time = '' self.markdown_card = markdown_card self.logger = logger + # Legacy access_token used by the OLD oapi.dingtalk.com endpoints + # (e.g. /media/upload, which is the only documented way to get an + # `@xxx` media_id usable in card Avatar.imageUrl). The new v1.0 + # token doesn't work there — different auth domain. + self.legacy_access_token = '' + self.legacy_access_token_expiry_time: typing.Optional[float] = None self._stopped = False # Flag to control the event loop async def _on_card_action(self, payload: dict) -> None: @@ -760,6 +768,96 @@ class DingTalkClient: await self.logger.error(f'DingTalk update card error: {traceback.format_exc()}') return False + async def get_legacy_access_token(self) -> Optional[str]: + """Fetch the LEGACY (oapi.dingtalk.com) access_token. This is a + different auth domain from the v1.0 token cached in + ``self.access_token`` — only the legacy token authorises the + ``/media/upload`` endpoint that returns an ``@xxx`` media_id + consumable by card components like Avatar.imageUrl. + + Returns the token string on success, None on failure. Caches + with a 60s safety margin before the documented 7200s expiry. + """ + now = time.time() + if ( + self.legacy_access_token + and self.legacy_access_token_expiry_time + and now < self.legacy_access_token_expiry_time + ): + return self.legacy_access_token + + url = 'https://oapi.dingtalk.com/gettoken' + try: + async with httpx.AsyncClient() as client: + response = await client.get(url, params={'appkey': self.key, 'appsecret': self.secret}, timeout=15.0) + data = response.json() if response.status_code == 200 else {} + if data.get('errcode') == 0 and data.get('access_token'): + self.legacy_access_token = data['access_token'] + expires_in = int(data.get('expires_in', 7200)) + self.legacy_access_token_expiry_time = now + expires_in - 60 + return self.legacy_access_token + if self.logger: + await self.logger.error( + f'DingTalk legacy gettoken failed: status={response.status_code} body={response.text[:200]}' + ) + except Exception: + _stdout_logger.exception('DingTalk legacy gettoken error') + if self.logger: + await self.logger.error(f'DingTalk legacy gettoken error: {traceback.format_exc()}') + return None + + async def upload_image_media(self, file_path: str) -> Optional[str]: + """Upload an image file to DingTalk media storage and return the + ``@xxx`` media_id, which can be passed straight into card variables + like Avatar.imageUrl. Endpoint: + + POST https://oapi.dingtalk.com/media/upload?access_token=…&type=image + + Returns the media_id on success, None on any failure (caller + should handle a None gracefully — DingTalk falls back to a + default avatar when imageUrl is empty/unknown). + """ + if not os.path.exists(file_path): + if self.logger: + await self.logger.error(f'DingTalk upload_image_media: file not found {file_path}') + return None + + token = await self.get_legacy_access_token() + if not token: + return None + + url = 'https://oapi.dingtalk.com/media/upload' + try: + with open(file_path, 'rb') as f: + file_bytes = f.read() + file_name = os.path.basename(file_path) + # Best-effort content-type guess; DingTalk accepts the major image + # mime types and otherwise infers from the bytes. + ext = os.path.splitext(file_name)[1].lower().lstrip('.') + mime = {'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif'}.get( + ext, 'application/octet-stream' + ) + async with httpx.AsyncClient() as client: + response = await client.post( + url, + params={'access_token': token, 'type': 'image'}, + files={'media': (file_name, file_bytes, mime)}, + timeout=30.0, + ) + data = response.json() if response.status_code == 200 else {} + if data.get('errcode') == 0 and data.get('media_id'): + _stdout_logger.info('DingTalk upload_image_media OK: media_id=%s', data['media_id']) + return data['media_id'] + if self.logger: + await self.logger.error( + f'DingTalk upload_image_media failed: status={response.status_code} body={response.text[:300]}' + ) + except Exception: + _stdout_logger.exception('DingTalk upload_image_media error') + if self.logger: + await self.logger.error(f'DingTalk upload_image_media error: {traceback.format_exc()}') + return None + async def start(self): """启动 WebSocket 连接,监听消息""" self._stopped = False diff --git a/src/langbot/pkg/platform/sources/dingtalk.py b/src/langbot/pkg/platform/sources/dingtalk.py index 242b94e12..ec7375861 100644 --- a/src/langbot/pkg/platform/sources/dingtalk.py +++ b/src/langbot/pkg/platform/sources/dingtalk.py @@ -1,6 +1,6 @@ import asyncio import json -import time +import pathlib import traceback import typing import uuid @@ -191,14 +191,6 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): # by _paint_form_on_card so the post-pause form keeps the streamed # context above the new prompt. active_turn_text: dict - # user_msg_id → monitoring_message_id, populated by - # on_monitoring_message_created and drained into feedback_state when - # a card is created for that user message. - pending_monitoring_msg: dict - # out_track_id → {monitoring_msg_id, session_id, user_id, voted}. - # Marks a card as feedback-enabled. _on_card_action looks here first - # for the synthetic feedback action ids and emits a FeedbackEvent. - feedback_state: dict # event_type → callback. The abstract base class doesn't declare this, # so we must do it here or pydantic silently drops `listeners={}` in # super().__init__ and any access raises AttributeError. @@ -208,12 +200,17 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): ] ap: typing.Any = None bot_uuid: str = '' + # DingTalk media_id (`@xxx` format) for the bot avatar image, fetched + # on adapter startup by uploading the bundled LangBot logo via the + # legacy /media/upload endpoint. Empty string when the upload hasn't + # run yet or failed — the template's Avatar then falls back to its + # default (initials of `name`). + bot_avatar_media_id: str = '' - # Synthetic action ids the user can't collide with — Dify actions are - # external strings, ours are namespaced. ClassVar so pydantic doesn't - # treat these constants as model fields. - FEEDBACK_ACTION_UP: typing.ClassVar[str] = '__lb_feedback_up__' - FEEDBACK_ACTION_DOWN: typing.ClassVar[str] = '__lb_feedback_down__' + # Path to the LangBot logo bundled in the repo (`res/logo-blue.png`), + # resolved relative to this file. Updated to find the file even when + # LangBot is installed as a package or run from a different cwd. + _LOGO_PATH: typing.ClassVar[pathlib.Path] = pathlib.Path(__file__).resolve().parents[5] / 'res' / 'logo-blue.png' def __init__(self, config: dict, logger: EventLogger): required_keys = [ @@ -241,8 +238,6 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): card_state={}, active_turn_card={}, active_turn_text={}, - pending_monitoring_msg={}, - feedback_state={}, bot_account_id=bot_account_id, bot=bot, listeners={}, @@ -314,14 +309,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): return card_instance, card_instance_id = chat_card_entry - # When this is the final chunk on a feedback-eligible card, - # paint 👍/👎 into the `btns` slot. Skip for non-final chunks - # and for non-form-template (legacy) cards which don't carry a - # `btns` slot in their template. - inject_feedback = is_final and bool(form_template_id) and card_instance_id in self.feedback_state - feedback_btns_json = ( - json.dumps(self._build_feedback_btns(card_instance_id), ensure_ascii=False) if inject_feedback else '[]' - ) + # btns is reserved exclusively for Dify form-action buttons. + # The template renders an Avatar header above the markdown + # content; no feedback buttons get injected here. if content: if form_template_id: # The card content has already been written via @@ -335,19 +325,12 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): try: await self.bot.update_card_data( out_track_id=card_instance_id, - card_param_map={ - 'content': content, - 'btns': feedback_btns_json, - 'flowStatus': '3' if is_final else '1', - }, + card_param_map=self._card_params( + content=content, + btns='[]', + flowStatus='3' if is_final else '1', + ), ) - # Cache the latest written content so the feedback - # click handler can re-send it alongside the - # disabled-buttons update — DingTalk update_card_data - # with a partial cardParamMap clears unspecified - # variables, so a btns-only update wipes the card body. - if inject_feedback: - self.feedback_state[card_instance_id]['last_content'] = content except Exception: if self.ap is not None: self.ap.logger.exception('DingTalk: update card content failed') @@ -356,15 +339,11 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): if is_final: if form_template_id and not content: # Empty final chunk still needs to leave the card with - # flowStatus=3 so the spinner stops. Paint feedback btns - # here too — earlier chunks already wrote the content. - empty_final_params: dict = {'flowStatus': '3'} - if inject_feedback: - empty_final_params['btns'] = feedback_btns_json + # flowStatus=3 so the spinner stops. try: await self.bot.update_card_data( out_track_id=card_instance_id, - card_param_map=empty_final_params, + card_param_map=self._card_params(flowStatus='3'), ) except Exception: pass @@ -417,7 +396,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): out_track_id=out_track_id, open_space_id=open_space_id, is_group=is_group, - card_param_map={'content': '', 'btns': '[]', 'flowStatus': '1'}, + card_param_map=self._card_params(content='', btns='[]', flowStatus='1'), callback_type='STREAM', ) except Exception: @@ -429,11 +408,6 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): if session_key: self.active_turn_card[session_key] = out_track_id self.active_turn_text[session_key] = '' - # Register for feedback so the final streaming chunk can paint - # 👍/👎 buttons. If the turn later pauses for human input, - # _paint_form_on_card un-registers this so the form-prompt card - # doesn't accidentally show feedback buttons. - self._register_feedback_card(out_track_id, event) return True # Legacy chat-card path (no form template). @@ -485,25 +459,38 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): self.bot.on_message('FriendMessage')(on_message) elif event_type == platform_events.GroupMessage: self.bot.on_message('GroupMessage')(on_message) - elif event_type == platform_events.FeedbackEvent: - # Stored only; _on_card_action looks it up by type when a - # feedback action id arrives. - self.listeners[platform_events.FeedbackEvent] = callback - - async def on_monitoring_message_created(self, query, monitoring_message_id: str): - """Pipeline hook: stash monitoring_message_id keyed by the user's - inbound message id so the card created for this turn can carry it - forward as FeedbackEvent.stream_id.""" - try: - user_msg_id = query.message_event.message_chain.message_id - if user_msg_id: - self.pending_monitoring_msg[str(user_msg_id)] = monitoring_message_id - except Exception as exc: - await self.logger.debug(f'DingTalk: failed to map monitoring message: {exc}') async def run_async(self): + # Upload the bundled LangBot logo so the card Avatar can render + # via DingTalk's media CDN — external URLs (e.g. raw.githubusercontent) + # are blocked by DingTalk's Avatar.imageUrl resolver. Non-fatal if + # the upload fails: cards still render without an avatar image. + if self._LOGO_PATH.exists(): + media_id = await self.bot.upload_image_media(str(self._LOGO_PATH)) + if media_id: + self.bot_avatar_media_id = media_id + if self.ap is not None: + self.ap.logger.info(f'DingTalk bot avatar uploaded: media_id={media_id}') + else: + if self.ap is not None: + self.ap.logger.warning('DingTalk bot avatar upload failed; card will use default') + else: + if self.ap is not None: + self.ap.logger.warning(f'DingTalk bot avatar source not found: {self._LOGO_PATH}') await self.bot.start() + def _card_params(self, **extra) -> dict: + """Build a cardParamMap dict that always carries `bot_avatar` + (when uploaded) alongside whatever caller-specific params. The + bot_avatar key gets dropped on every update_card_data call — + DingTalk wipes unspecified template variables, so re-sending it + on each update is mandatory.""" + params = {} + if self.bot_avatar_media_id: + params['bot_avatar'] = self.bot_avatar_media_id + params.update(extra) + return params + async def kill(self) -> bool: await self.bot.stop() return True @@ -634,11 +621,6 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): 'node_title': node_title, 'form_content': form_content, } - # This card is becoming a form-prompt card; thumbs feedback would - # collide visually with the action buttons (same `btns` slot) and - # semantically (feedback is for the answer, not an intermediate - # prompt). Drop the registration. - self.feedback_state.pop(out_track_id, None) btns = self._build_btns(actions, out_track_id) parts: list[str] = [] @@ -659,11 +641,11 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): 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', - }, + card_param_map=self._card_params( + content=display_content, + btns=json.dumps(btns, ensure_ascii=False), + flowStatus='3', + ), ) except Exception: if self.ap is not None: @@ -703,92 +685,6 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): ) return btns - @classmethod - def _build_feedback_btns( - cls, - out_track_id: str, - voted: typing.Optional[str] = None, - ) -> list: - """Two-button feedback pair posted to the `btns` slot. - - The bundled `dingtalk_human_input_card.json` template's btns - ButtonGroup schema only declares ``text/color/status/event`` — - extra fields like ``icon`` are silently dropped by DingTalk's - renderer, so the buttons stay text-only. Emoji is inlined into - ``text`` for a glanceable like/dislike marker. - - * Pre-click (``voted`` is None): 👍 in `blue`, 👎 in `gray`; both - clickable. - * Post-click (``voted`` matches one action id): the clicked button - keeps its color, the other goes flat `gray`; both `disabled`. - """ - items = [ - ('👍 有帮助', cls.FEEDBACK_ACTION_UP, 'blue'), - ('👎 无帮助', cls.FEEDBACK_ACTION_DOWN, 'gray'), - ] - btns = [] - for text, action_id, default_color in items: - if voted is None: - color = default_color - status = 'normal' - else: - color = default_color if action_id == voted else 'gray' - status = 'disabled' - btns.append( - { - 'text': text, - 'color': color, - 'status': status, - 'event': { - 'type': 'sendCardRequest', - 'params': { - 'actionId': action_id, - 'params': {'action_id': action_id, 'out_track_id': out_track_id}, - }, - }, - } - ) - return btns - - def _register_feedback_card( - self, - out_track_id: str, - message_source: platform_events.MessageEvent, - ) -> None: - """Mark a card as eligible for thumbs feedback by capturing session - + user + monitoring info. Pulls the monitoring_message_id from - ``pending_monitoring_msg`` keyed by the inbound DingTalk message id; - falls back to empty for synthetic events (resumed-workflow cards), - in which case the FeedbackEvent still fires but without a stream - id correlation.""" - if not out_track_id or message_source is None: - return - user_msg_id = '' - monitoring_msg_id = '' - spo = getattr(message_source, 'source_platform_object', None) - if spo is not None: - inc = getattr(spo, 'incoming_message', None) - if inc is not None: - user_msg_id = str(getattr(inc, 'message_id', '') or '') - if user_msg_id: - monitoring_msg_id = self.pending_monitoring_msg.pop(user_msg_id, '') - if isinstance(message_source, platform_events.GroupMessage): - session_id = f'group_{message_source.group.id}' - user_id = str(message_source.sender.id) - elif isinstance(message_source, platform_events.FriendMessage): - session_id = f'person_{message_source.sender.id}' - user_id = str(message_source.sender.id) - else: - return - self.feedback_state[out_track_id] = { - 'monitoring_msg_id': monitoring_msg_id, - 'session_id': session_id, - 'user_id': user_id, - 'user_msg_id': user_msg_id, - 'created_at': time.time(), - 'voted': False, - } - async def _send_form_card( self, message_source: platform_events.MessageEvent, @@ -863,11 +759,11 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): out_track_id=out_track_id, open_space_id=open_space_id, is_group=is_group, - card_param_map={ - 'content': display_content, - 'btns': json.dumps(btns, ensure_ascii=False), - 'flowStatus': '3', - }, + card_param_map=self._card_params( + content=display_content, + btns=json.dumps(btns, ensure_ascii=False), + flowStatus='3', + ), callback_type='STREAM', ) except Exception: @@ -894,9 +790,11 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): out_track_id = uuid.uuid4().hex open_space_id, is_group = self._derive_open_space(message_source) if form_template_id: - card_param_map = {'content': '', 'btns': '[]', 'flowStatus': '1'} + card_param_map = self._card_params(content='', btns='[]', flowStatus='1') card_data_config = None else: + # Legacy chat-card template doesn't carry a `bot_avatar` + # variable, so don't decorate the param map here. card_param_map = {'content': '', 'query': '...'} card_data_config = {'autoLayout': self.config.get('card_auto_layout', False)} try: @@ -923,12 +821,6 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): if session_key: self.active_turn_card[session_key] = out_track_id self.active_turn_text[session_key] = '' - # Resumed cards are eligible for feedback too — the synthetic - # message_source has no source_platform_object so the helper - # records empty user_msg_id/monitoring_msg_id; FeedbackEvent fires - # without a stream_id correlation. - if form_template_id: - self._register_feedback_card(out_track_id, message_source) return entry async def send_message_text_form( @@ -1003,13 +895,6 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): await self.logger.warning(f'DingTalk: card action with no action_id, payload={payload}') return - # Feedback path: handle 👍/👎 clicks before form-action lookup so a - # feedback click on a card that also happens to be tracked in - # card_state (it shouldn't, but defensively) is still routed right. - if raw_action_id in (self.FEEDBACK_ACTION_UP, self.FEEDBACK_ACTION_DOWN): - await self._handle_feedback_action(out_track_id, raw_action_id, payload) - return - state = self.card_state.get(out_track_id) if state is None: await self.logger.warning(f'DingTalk: card action received for unknown out_track_id={out_track_id}') @@ -1133,71 +1018,6 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): # Once consumed, drop the state — the runner clears _PENDING_FORMS too. self.card_state.pop(out_track_id, None) - async def _handle_feedback_action( - self, - out_track_id: str, - action_id: str, - payload: dict, - ) -> None: - """Process a 👍/👎 click: dispatch FeedbackEvent to the registered - listener (LangBot's monitoring service records it), then repaint - the card's buttons in `disabled` state so the click is visibly - acknowledged and not re-issued.""" - state = self.feedback_state.get(out_track_id) - if state is None: - await self.logger.warning(f'DingTalk: feedback action {action_id} for unknown out_track_id={out_track_id}') - return - if state.get('voted'): - # Already voted — ignore re-clicks (DingTalk may still deliver - # them despite the disabled status, depending on the client). - return - - feedback_type = 1 if action_id == self.FEEDBACK_ACTION_UP else 2 - feedback_content = '有帮助' if feedback_type == 1 else '无帮助' - - listener = self.listeners.get(platform_events.FeedbackEvent) - if listener is not None: - feedback_event = platform_events.FeedbackEvent( - feedback_id=str(uuid.uuid4()), - feedback_type=feedback_type, - feedback_content=feedback_content, - user_id=state.get('user_id') or payload.get('user_id') or '', - session_id=state.get('session_id', ''), - message_id=state.get('user_msg_id', ''), - stream_id=state.get('monitoring_msg_id', '') or None, - source_platform_object=payload, - ) - try: - await listener(feedback_event, self) - except Exception: - if self.ap is not None: - self.ap.logger.exception('DingTalk: feedback listener raised') - - state['voted'] = action_id - # Repaint the same `btns` slot: clicked button keeps its color + - # disabled (marks the user's choice), other goes gray + disabled. - # CRITICAL: DingTalk update_card_data with a partial cardParamMap - # clears unspecified template variables — sending only `btns` wipes - # the card body. Always re-send content + flowStatus too. - card_param_map: dict = { - 'btns': json.dumps( - self._build_feedback_btns(out_track_id, voted=action_id), - ensure_ascii=False, - ), - 'flowStatus': '3', - } - last_content = state.get('last_content') - if last_content: - card_param_map['content'] = last_content - try: - await self.bot.update_card_data( - out_track_id=out_track_id, - card_param_map=card_param_map, - ) - except Exception: - if self.ap is not None: - self.ap.logger.exception('DingTalk: grey out feedback buttons failed') - async def _mark_card_resolved( self, out_track_id: str, @@ -1224,11 +1044,11 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): try: await self.bot.update_card_data( out_track_id=out_track_id, - card_param_map={ - 'content': content, - 'btns': '[]', - 'flowStatus': '3', - }, + card_param_map=self._card_params( + content=content, + btns='[]', + flowStatus='3', + ), ) except Exception: if self.ap is not None: diff --git a/src/langbot/templates/dingtalk_human_input_card.json b/src/langbot/templates/dingtalk_human_input_card.json index f297e832a..140584f21 100644 --- a/src/langbot/templates/dingtalk_human_input_card.json +++ b/src/langbot/templates/dingtalk_human_input_card.json @@ -1,5 +1,5 @@ { - "editorData": "{\"schemaVersion\":\"3.0.0\",\"schema\":{\"config\":null,\"componentsMap\":[{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AIPending\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AIPending\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardStatusContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardStatusContainer\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"BaseText\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"BaseText\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContent\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContent\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContainer\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"ButtonGroup\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"ButtonGroup\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"MarkdownBlock\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"MarkdownBlock\"}],\"componentsTree\":[{\"componentName\":\"AICardContainer\",\"id\":\"node_root\",\"props\":{\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePending\":true,\"enableWriting\":true,\"enableDoing\":true,\"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\":[{\"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\":\"\"}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_status_writing\",\"props\":{\"status\":2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enableExtend\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"innerOffset\":0,\"enableCollapse\":false,\"margin\":-2},\"title\":\"状态2占位\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_status_writing_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\":[]}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_status_doing\",\"props\":{\"status\":4,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enableExtend\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"innerOffset\":0,\"enableCollapse\":false,\"margin\":-2},\"title\":\"状态4占位\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_status_doing_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\":[]}]},{\"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\":[{\"componentName\":\"MarkdownBlock\",\"id\":\"node_text_content\",\"props\":{\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"content\":{\"variable\":\"content\",\"variableType\":\"global\",\"type\":\"variableValue\",\"varType\":\"markdown\"},\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"isStreaming\":false,\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"marginTop\":6,\"marginBottom\":6,\"marginLeft\":12,\"marginRight\":12},\"title\":\"AI 流式富文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"ButtonGroup\",\"id\":\"node_btn_group\",\"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\":\"\"}]}]},{\"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\":[{\"componentName\":\"BaseText\",\"id\":\"node_failed_text\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"操作失败,请稍后重试。\"},\"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\":10,\"marginRight\":10,\"marginTop\":10,\"marginBottom\":10,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level1_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"custom\",\"styleToken\":\"common_body_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"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\":\"\"}]}]}]}],\"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},{\"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)\"}]}]}]}],\"formList\":[],\"customContextList\":[],\"expList\":[],\"localList\":[],\"hsfList\":[],\"lwpList\":[],\"pageData\":{},\"extension\":{\"extendType\":\"AI\",\"aiStatusList\":[3,1,5,2,4],\"fileTypeList\":[]}}", + "editorData": "{\"schemaVersion\":\"3.0.0\",\"schema\":{\"config\":null,\"componentsMap\":[{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AIPending\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AIPending\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardStatusContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardStatusContainer\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"BaseText\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"BaseText\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContent\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContent\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContainer\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"ButtonGroup\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"ButtonGroup\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"MarkdownBlock\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"MarkdownBlock\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Avatar\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Avatar\"}],\"componentsTree\":[{\"componentName\":\"AICardContainer\",\"id\":\"node_root\",\"props\":{\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePending\":true,\"enableWriting\":true,\"enableDoing\":true,\"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\":[{\"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\":\"\"}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_status_writing\",\"props\":{\"status\":2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enableExtend\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"innerOffset\":0,\"enableCollapse\":false,\"margin\":-2},\"title\":\"状态2占位\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_status_writing_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\":[]}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_status_doing\",\"props\":{\"status\":4,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enableExtend\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"innerOffset\":0,\"enableCollapse\":false,\"margin\":-2},\"title\":\"状态4占位\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_status_doing_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\":[]}]},{\"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\":[{\"componentName\":\"Avatar\",\"id\":\"node_avatar\",\"props\":{\"imageUrl\":{\"value\":\"\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"bot_avatar\",\"variableType\":\"global\"},\"name\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"LangBot\"},\"sizeType\":\"Standard\",\"size\":\"extraSmall\",\"customSize\":48,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"mode\":\"userInfo\",\"margin\":-2,\"innerOffset\":0},\"title\":\"头像\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"MarkdownBlock\",\"id\":\"node_text_content\",\"props\":{\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"content\":{\"variable\":\"content\",\"variableType\":\"global\",\"type\":\"variableValue\",\"varType\":\"markdown\"},\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"isStreaming\":false,\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"marginTop\":6,\"marginBottom\":6,\"marginLeft\":12,\"marginRight\":12},\"title\":\"AI 流式富文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"ButtonGroup\",\"id\":\"node_btn_group\",\"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\":\"\"}]}]},{\"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\":[{\"componentName\":\"BaseText\",\"id\":\"node_failed_text\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"操作失败,请稍后重试。\"},\"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\":10,\"marginRight\":10,\"marginTop\":10,\"marginBottom\":10,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level1_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"custom\",\"styleToken\":\"common_body_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"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\":\"\"}]}]}]}],\"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},{\"id\":\"bot_avatar\",\"type\":\"string\",\"name\":\"bot_avatar\",\"description\":\"机器人头像 DingTalk 媒体 ID(@xxx 格式,启动时由 /media/upload 拿到)\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":false},{\"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)\"}]}]}]}],\"formList\":[],\"customContextList\":[],\"expList\":[],\"localList\":[],\"hsfList\":[],\"lwpList\":[],\"pageData\":{},\"extension\":{\"extendType\":\"AI\",\"aiStatusList\":[3,1,5,2,4],\"fileTypeList\":[]}}", "widgetInfo": "", "type": "im", "mode": "card"