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}
+
+ {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]);