mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-24 06:24:20 +00:00
feat(dingtalk): add download link for human input card template and enhance dynamic form configuration
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<FormItem>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<a href={downloadUrl} download={filename}>
|
||||
<Download className="h-4 w-4" />
|
||||
{label}
|
||||
</a>
|
||||
</Button>
|
||||
{helpUrl && (
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<a href={helpUrl} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
{helpLabel}
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground max-w-2xl">{description}</p>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DownloadLinkField
|
||||
key={config.id}
|
||||
label={extractI18nObject(config.label)}
|
||||
description={
|
||||
config.description
|
||||
? extractI18nObject(config.description)
|
||||
: undefined
|
||||
}
|
||||
url={config.url}
|
||||
filename={config.download_filename}
|
||||
helpUrl={getAdapterDocUrl(config.help_links, i18n.language)}
|
||||
helpLabel={
|
||||
config.help_label
|
||||
? extractI18nObject(config.help_label)
|
||||
: t('bots.viewAdapterDocs')
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// QR code login button (e.g. Feishu one-click create, WeChat scan login)
|
||||
if (config.type === 'qr-code-login') {
|
||||
return (
|
||||
|
||||
@@ -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<string, string>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, string>; // 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 {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user