From 5e6b3cc5503ba21cec25b8f26d86f6845d09a44d Mon Sep 17 00:00:00 2001 From: fdc310 <2213070223@qq.com> Date: Wed, 24 Jun 2026 11:23:02 +0800 Subject: [PATCH] feat(dingtalk): add download link for human input card template and enhance dynamic form configuration --- .../controller/groups/platform/adapters.py | 9 ++ .../pkg/platform/sources/dingtalk.yaml | 29 ++++++- .../home/bots/components/bot-form/BotForm.tsx | 4 + .../dynamic-form/DynamicFormComponent.tsx | 83 ++++++++++++++++++- .../dynamic-form/DynamicFormItemConfig.ts | 8 ++ web/src/app/infra/entities/form/dynamic.ts | 5 ++ web/src/app/wizard/page.tsx | 8 ++ 7 files changed, 140 insertions(+), 6 deletions(-) diff --git a/src/langbot/pkg/api/http/controller/groups/platform/adapters.py b/src/langbot/pkg/api/http/controller/groups/platform/adapters.py index 4356e638d..0e32f9d29 100644 --- a/src/langbot/pkg/api/http/controller/groups/platform/adapters.py +++ b/src/langbot/pkg/api/http/controller/groups/platform/adapters.py @@ -60,6 +60,15 @@ class AdaptersRouterGroup(group.RouterGroup): importutil.read_resource_file_bytes(icon_path), mimetype=mimetypes.guess_type(icon_path)[0] ) + @self.route('/dingtalk/human-input-card-template', methods=['GET'], auth_type=group.AuthType.NONE) + async def _() -> quart.Response: + filename = 'dingtalk_human_input_card.json' + response = quart.Response( + importutil.read_resource_file_bytes(f'templates/{filename}'), mimetype='application/json' + ) + response.headers['Content-Disposition'] = f'attachment; filename={filename}' + return response + # In-memory session store for active registrations _create_app_sessions: dict = {} _SESSION_TTL = 900 # 15 minutes diff --git a/src/langbot/pkg/platform/sources/dingtalk.yaml b/src/langbot/pkg/platform/sources/dingtalk.yaml index dff76216d..261a8bf21 100644 --- a/src/langbot/pkg/platform/sources/dingtalk.yaml +++ b/src/langbot/pkg/platform/sources/dingtalk.yaml @@ -103,15 +103,38 @@ spec: type: string required: true default: "填写你的卡片template_id" + - name: human_input_card_template_download + label: + en_US: Download Human Input Card Template + zh_Hans: 下载人工输入卡片模板 + zh_Hant: 下載人工輸入卡片範本 + description: + en_US: "Used as the only card template ID for the whole conversation turn. Download the built-in template, then import the JSON in DingTalk Open Platform > Card Platform / Card Template Management. After DingTalk creates the template, copy its template ID into the field below. The template already wires `content` (MarkdownBlock) and `btns` (ButtonGroup). Leave empty to fall back to the legacy two-card behavior." + zh_Hans: "用作整个对话回合唯一卡片的模板 ID。先下载内置模板,再到钉钉开放平台 > 卡片平台 / 卡片模板管理中导入该 JSON;钉钉生成模板后,将模板 ID 填到这里。模板已预先连好 `content` (MarkdownBlock) 与 `btns` (ButtonGroup)。留空则降级为旧的双卡行为。" + zh_Hant: "用作整個對話回合唯一卡片的範本 ID。先下載內建範本,再到釘釘開放平台 > 卡片平台 / 卡片範本管理中匯入該 JSON;釘釘產生範本後,將範本 ID 填到這裡。範本已預先連好 `content` (MarkdownBlock) 與 `btns` (ButtonGroup)。留空則降級為舊的雙卡行為。" + type: download-link + required: false + default: "" + url: /api/v1/platform/adapters/dingtalk/human-input-card-template + download_filename: dingtalk_human_input_card.json + help_links: + zh: https://open-dev.dingtalk.com/fe/card + en: https://open-dev.dingtalk.com/fe/card + ja: https://open-dev.dingtalk.com/fe/card + help_label: + en_US: Import Guide + zh_Hans: 导入指引 + zh_Hant: 匯入指引 + ja_JP: インポート手順 - name: human_input_card_template_id label: en_US: Human Input Card Template ID zh_Hans: 人工输入卡片模板ID zh_Hant: 人工輸入卡片範本ID description: - en_US: "Template ID used as the SINGLE card for the whole conversation turn. Streamed LLM text fills the `content` markdown variable; on a Dify human-input pause the `btns` buttonGroup variable is populated so action buttons appear on the SAME card; after the user clicks a button the buttons disappear and resumed streaming continues into the same card. Use the bundled `src/langbot/templates/dingtalk_human_input_card.json` — it ships with `content` (MarkdownBlock) and `btns` (ButtonGroup) already wired. Leave empty to fall back to the legacy two-card behaviour (chat card streaming text + plain-text human-input prompts)." - zh_Hans: "用作整个对话回合**唯一**卡片的模板ID。流式 LLM 文本写入 `content` markdown 变量;Dify 人工输入暂停时同一张卡的 `btns` buttonGroup 变量被填上、按钮浮现;用户点击后按钮消失、恢复的流式内容继续追加到同一张卡。可使用项目附带的 `src/langbot/templates/dingtalk_human_input_card.json`——已经预先连好 `content` (MarkdownBlock) 与 `btns` (ButtonGroup)。留空则降级为旧的双卡行为(聊天卡走流式 + 人工输入走纯文本)。" - zh_Hant: "用作整個對話回合**唯一**卡片的範本ID。流式 LLM 文字寫入 `content` markdown 變數;Dify 人工輸入暫停時同一張卡的 `btns` buttonGroup 變數被填上、按鈕浮現;使用者點擊後按鈕消失、恢復的流式內容繼續追加到同一張卡。可使用專案附帶的 `src/langbot/templates/dingtalk_human_input_card.json`——已經預先連好 `content` (MarkdownBlock) 與 `btns` (ButtonGroup)。留空則降級為舊的雙卡行為(聊天卡走流式 + 人工輸入走純文字)。" + en_US: "Paste the template ID generated after importing the human input card template." + zh_Hans: "填写导入人工输入卡片模板后生成的模板 ID。" + zh_Hant: "填寫匯入人工輸入卡片範本後產生的範本 ID。" type: string required: false default: "" diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index c81225dee..1f48e18e9 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -268,6 +268,10 @@ export default function BotForm({ options: item.options, show_if: item.show_if, login_platform: item.login_platform, + url: item.url, + download_filename: item.download_filename, + help_links: item.help_links, + help_label: item.help_label, }), ), ); diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx index ffea18d6e..7d3af6c23 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -20,9 +20,17 @@ import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { Copy, Check, Globe, QrCode } from 'lucide-react'; +import { + Copy, + Check, + Globe, + QrCode, + Download, + ExternalLink, +} from 'lucide-react'; import { copyToClipboard } from '@/app/utils/clipboard'; import { systemInfo } from '@/app/infra/http'; +import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs'; /** * Resolve the value referenced by a `show_if.field` string. @@ -190,6 +198,50 @@ function WebhookUrlField({ ); } +function DownloadLinkField({ + label, + description, + url, + filename, + helpUrl, + helpLabel, +}: { + label: string; + description?: string; + url: string; + filename?: string; + helpUrl?: string | null; + helpLabel: string; +}) { + const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin; + const downloadUrl = url.startsWith('http') ? url : `${baseUrl}${url}`; + + return ( + + {label} +
+ + {helpUrl && ( + + )} +
+ {description && ( +

{description}

+ )} +
+ ); +} + export default function DynamicFormComponent({ itemConfigList, onSubmit, @@ -215,7 +267,7 @@ export default function DynamicFormComponent({ }) { const isInitialMount = useRef(true); const previousInitialValues = useRef(initialValues); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); // Normalize a form value according to its field type. // This ensures legacy/malformed data (e.g. a plain string for @@ -261,7 +313,8 @@ export default function DynamicFormComponent({ (item) => item.type !== 'webhook-url' && item.type !== 'embed-code' && - item.type !== 'qr-code-login', + item.type !== 'qr-code-login' && + item.type !== 'download-link', ), [itemConfigList], ); @@ -563,6 +616,30 @@ export default function DynamicFormComponent({ ); } + if (config.type === 'download-link') { + if (!config.url) return null; + + return ( + + ); + } + // QR code login button (e.g. Feishu one-click create, WeChat scan login) if (config.type === 'qr-code-login') { return ( diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts index 50ac578ac..c3915a216 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts @@ -17,6 +17,10 @@ export class DynamicFormItemConfig implements IDynamicFormItemSchema { options?: IDynamicFormItemOption[]; show_if?: IShowIfCondition; login_platform?: string; + url?: string; + download_filename?: string; + help_links?: Record; + help_label?: I18nObject; constructor(params: IDynamicFormItemSchema) { this.id = params.id; @@ -29,6 +33,10 @@ export class DynamicFormItemConfig implements IDynamicFormItemSchema { this.options = params.options; this.show_if = params.show_if; this.login_platform = params.login_platform; + this.url = params.url; + this.download_filename = params.download_filename; + this.help_links = params.help_links; + this.help_label = params.help_label; } } diff --git a/web/src/app/infra/entities/form/dynamic.ts b/web/src/app/infra/entities/form/dynamic.ts index 6976b6a48..9a19d1af3 100644 --- a/web/src/app/infra/entities/form/dynamic.ts +++ b/web/src/app/infra/entities/form/dynamic.ts @@ -22,6 +22,10 @@ export interface IDynamicFormItemSchema { scopes?: string[]; accept?: string; // For file type: accepted MIME types login_platform?: string; // For qr-code-login type: platform identifier (e.g. 'feishu', 'weixin') + url?: string; // For download-link type: relative or absolute download URL + download_filename?: string; // Optional filename for download-link type + help_links?: Record; // Optional docs links for display-only fields + help_label?: I18nObject; // Optional label for help_links } export enum DynamicFormItemType { @@ -48,6 +52,7 @@ export enum DynamicFormItemType { WEBHOOK_URL = 'webhook-url', EMBED_CODE = 'embed-code', QR_CODE_LOGIN = 'qr-code-login', + DOWNLOAD_LINK = 'download-link', } export interface IFileConfig { diff --git a/web/src/app/wizard/page.tsx b/web/src/app/wizard/page.tsx index e823fd290..578a29aff 100644 --- a/web/src/app/wizard/page.tsx +++ b/web/src/app/wizard/page.tsx @@ -229,6 +229,10 @@ export default function WizardPage() { options: item.options, show_if: item.show_if, login_platform: item.login_platform, + url: item.url, + download_filename: item.download_filename, + help_links: item.help_links, + help_label: item.help_label, }), ); }, [adapters, selectedAdapter]); @@ -249,6 +253,10 @@ export default function WizardPage() { options: item.options, show_if: item.show_if, login_platform: item.login_platform, + url: item.url, + download_filename: item.download_filename, + help_links: item.help_links, + help_label: item.help_label, }), ); }, [selectedRunnerConfigStage]);