mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 20:50:58 +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]
|
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
|
# In-memory session store for active registrations
|
||||||
_create_app_sessions: dict = {}
|
_create_app_sessions: dict = {}
|
||||||
_SESSION_TTL = 900 # 15 minutes
|
_SESSION_TTL = 900 # 15 minutes
|
||||||
|
|||||||
@@ -103,15 +103,38 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: "填写你的卡片template_id"
|
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
|
- name: human_input_card_template_id
|
||||||
label:
|
label:
|
||||||
en_US: Human Input Card Template ID
|
en_US: Human Input Card Template ID
|
||||||
zh_Hans: 人工输入卡片模板ID
|
zh_Hans: 人工输入卡片模板ID
|
||||||
zh_Hant: 人工輸入卡片範本ID
|
zh_Hant: 人工輸入卡片範本ID
|
||||||
description:
|
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)."
|
en_US: "Paste the template ID generated after importing the human input card template."
|
||||||
zh_Hans: "用作整个对话回合**唯一**卡片的模板ID。流式 LLM 文本写入 `content` markdown 变量;Dify 人工输入暂停时同一张卡的 `btns` buttonGroup 变量被填上、按钮浮现;用户点击后按钮消失、恢复的流式内容继续追加到同一张卡。可使用项目附带的 `src/langbot/templates/dingtalk_human_input_card.json`——已经预先连好 `content` (MarkdownBlock) 与 `btns` (ButtonGroup)。留空则降级为旧的双卡行为(聊天卡走流式 + 人工输入走纯文本)。"
|
zh_Hans: "填写导入人工输入卡片模板后生成的模板 ID。"
|
||||||
zh_Hant: "用作整個對話回合**唯一**卡片的範本ID。流式 LLM 文字寫入 `content` markdown 變數;Dify 人工輸入暫停時同一張卡的 `btns` buttonGroup 變數被填上、按鈕浮現;使用者點擊後按鈕消失、恢復的流式內容繼續追加到同一張卡。可使用專案附帶的 `src/langbot/templates/dingtalk_human_input_card.json`——已經預先連好 `content` (MarkdownBlock) 與 `btns` (ButtonGroup)。留空則降級為舊的雙卡行為(聊天卡走流式 + 人工輸入走純文字)。"
|
zh_Hant: "填寫匯入人工輸入卡片範本後產生的範本 ID。"
|
||||||
type: string
|
type: string
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
|||||||
@@ -268,6 +268,10 @@ export default function BotForm({
|
|||||||
options: item.options,
|
options: item.options,
|
||||||
show_if: item.show_if,
|
show_if: item.show_if,
|
||||||
login_platform: item.login_platform,
|
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 { cn } from '@/lib/utils';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { copyToClipboard } from '@/app/utils/clipboard';
|
||||||
import { systemInfo } from '@/app/infra/http';
|
import { systemInfo } from '@/app/infra/http';
|
||||||
|
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the value referenced by a `show_if.field` string.
|
* 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({
|
export default function DynamicFormComponent({
|
||||||
itemConfigList,
|
itemConfigList,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@@ -215,7 +267,7 @@ export default function DynamicFormComponent({
|
|||||||
}) {
|
}) {
|
||||||
const isInitialMount = useRef(true);
|
const isInitialMount = useRef(true);
|
||||||
const previousInitialValues = useRef(initialValues);
|
const previousInitialValues = useRef(initialValues);
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
// Normalize a form value according to its field type.
|
// Normalize a form value according to its field type.
|
||||||
// This ensures legacy/malformed data (e.g. a plain string for
|
// This ensures legacy/malformed data (e.g. a plain string for
|
||||||
@@ -261,7 +313,8 @@ export default function DynamicFormComponent({
|
|||||||
(item) =>
|
(item) =>
|
||||||
item.type !== 'webhook-url' &&
|
item.type !== 'webhook-url' &&
|
||||||
item.type !== 'embed-code' &&
|
item.type !== 'embed-code' &&
|
||||||
item.type !== 'qr-code-login',
|
item.type !== 'qr-code-login' &&
|
||||||
|
item.type !== 'download-link',
|
||||||
),
|
),
|
||||||
[itemConfigList],
|
[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)
|
// QR code login button (e.g. Feishu one-click create, WeChat scan login)
|
||||||
if (config.type === 'qr-code-login') {
|
if (config.type === 'qr-code-login') {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export class DynamicFormItemConfig implements IDynamicFormItemSchema {
|
|||||||
options?: IDynamicFormItemOption[];
|
options?: IDynamicFormItemOption[];
|
||||||
show_if?: IShowIfCondition;
|
show_if?: IShowIfCondition;
|
||||||
login_platform?: string;
|
login_platform?: string;
|
||||||
|
url?: string;
|
||||||
|
download_filename?: string;
|
||||||
|
help_links?: Record<string, string>;
|
||||||
|
help_label?: I18nObject;
|
||||||
|
|
||||||
constructor(params: IDynamicFormItemSchema) {
|
constructor(params: IDynamicFormItemSchema) {
|
||||||
this.id = params.id;
|
this.id = params.id;
|
||||||
@@ -29,6 +33,10 @@ export class DynamicFormItemConfig implements IDynamicFormItemSchema {
|
|||||||
this.options = params.options;
|
this.options = params.options;
|
||||||
this.show_if = params.show_if;
|
this.show_if = params.show_if;
|
||||||
this.login_platform = params.login_platform;
|
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[];
|
scopes?: string[];
|
||||||
accept?: string; // For file type: accepted MIME types
|
accept?: string; // For file type: accepted MIME types
|
||||||
login_platform?: string; // For qr-code-login type: platform identifier (e.g. 'feishu', 'weixin')
|
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 {
|
export enum DynamicFormItemType {
|
||||||
@@ -48,6 +52,7 @@ export enum DynamicFormItemType {
|
|||||||
WEBHOOK_URL = 'webhook-url',
|
WEBHOOK_URL = 'webhook-url',
|
||||||
EMBED_CODE = 'embed-code',
|
EMBED_CODE = 'embed-code',
|
||||||
QR_CODE_LOGIN = 'qr-code-login',
|
QR_CODE_LOGIN = 'qr-code-login',
|
||||||
|
DOWNLOAD_LINK = 'download-link',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFileConfig {
|
export interface IFileConfig {
|
||||||
|
|||||||
@@ -229,6 +229,10 @@ export default function WizardPage() {
|
|||||||
options: item.options,
|
options: item.options,
|
||||||
show_if: item.show_if,
|
show_if: item.show_if,
|
||||||
login_platform: item.login_platform,
|
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]);
|
}, [adapters, selectedAdapter]);
|
||||||
@@ -249,6 +253,10 @@ export default function WizardPage() {
|
|||||||
options: item.options,
|
options: item.options,
|
||||||
show_if: item.show_if,
|
show_if: item.show_if,
|
||||||
login_platform: item.login_platform,
|
login_platform: item.login_platform,
|
||||||
|
url: item.url,
|
||||||
|
download_filename: item.download_filename,
|
||||||
|
help_links: item.help_links,
|
||||||
|
help_label: item.help_label,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}, [selectedRunnerConfigStage]);
|
}, [selectedRunnerConfigStage]);
|
||||||
|
|||||||
Reference in New Issue
Block a user