mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 09:26:07 +00:00
feat: add supports for dify hitl (#2226)
* feat: Implement workflow form handling for paused workflows - Added module-level storage for pending forms to manage state across sessions. - Introduced functions to set, get, and clear pending forms with expiration handling. - Enhanced DifyServiceAPIRunner to support resuming paused workflows via form actions. - Implemented logic to yield human input requests and display appropriate messages. - Updated workflow submission methods to handle paused states and resume actions. - Ensured proper merging of pending form actions with user inputs for seamless interaction. * feat: Add '_routed_by_rule' variable to form action in Lark and Telegram adapters * feat: Enhance Lark and Telegram adapters with new form handling for paused workflows * feat: Enhance TelegramAdapter to handle form action buttons and message threading * feat: Improve TelegramAdapter message handling with enhanced error management and draft message support * feat: Add the function for formatting human input text to support adapters without rich UI. * feat(dingtalk): implement human input card support and card action handling - Add a new module `card_callback.py` to handle card action button clicks from DingTalk. - Introduce `DingTalkCardActionHandler` to process card action callbacks and extract parameters. - Update `DingTalkAdapter` to manage card state and handle form input through a single card template. - Add configuration for `human_input_card_template_id` in `dingtalk.yaml` to specify the template for human input. - Create a new card template `dingtalk_human_input_card.json` for rendering human input prompts and buttons. * feat(dingtalk): enhance human input card functionality with streaming support and active turn management - Updated the DingTalk card template to enable streaming mode and multi-update configuration. - Removed the obsolete delete_card method from DingTalkClient to streamline card management. - Enhanced DingTalkAdapter to manage active turn cards and accumulated streaming text, ensuring a seamless user experience during human input prompts. - Modified the create_message_card method to utilize existing active cards for resumed workflows, preventing duplication. - Improved the _paint_form_on_card method to update existing cards with human input prompts and buttons dynamically. - Updated the dingtalk_human_input_card.json template to reflect the new streaming capabilities and configuration options. * feat(wecom): implement Dify human input pause handling with button interaction support * feat(qqofficial): implement Dify human input button interaction handling and markdown keyboard support * feat(qqofficial): implement one-click QR binding and enhance localization support * feat(discord): implement Discord form view with button interactions for Dify actions * fix(telegram): correct group chat type check and handle oversized callback data for Telegram actions fix(difysvapi): ensure safe access to remove-think configuration in pipeline settings * feat(dify): add support for chatflow app type and enhance human input handling * feat(telegram): add action title feedback for user selections in Telegram messages * feat(lark): enhance LarkAdapter to store form content for resume notices * feat(dingtalk): update display formatting for card content with HTML line breaks * feat(dingtalk): add feedback functionality to cards with 👍/👎 buttons - Implemented feedback state management for cards, allowing users to provide feedback via thumbs up/down buttons. - Enhanced card rendering to include feedback buttons when appropriate. - Registered feedback listeners to handle feedback events and update card states accordingly. - Updated the card template to support dynamic button rendering for feedback actions. - Improved error handling and logging for feedback actions and card updates. * fix: add Avatar component to dingtalk_human_input_card.json for enhanced user interaction * feat(wecom): add optional source block to interactive template cards for enhanced branding * feat(wecom): add functions for template card action extraction and update, enhance button interaction handling * feat(qqofficial): synchronize passive-reply counter with inbound message sequence * feat(qqofficial): add method to identify invisible form placeholder chunks in messages * feat(dingtalk): add download link for human input card template and enhance dynamic form configuration * feat(telegram): enhance message handling with group stream deletion and form placeholder detection * Add unit tests for DingTalk, Lark, WeComBot, and Dify service API runners - Implement tests for DingTalk adapter helper functions including form content cleaning, input extraction, and completed input lines. - Create unit tests for Lark adapter helper functions focusing on input extraction and completed input lines. - Add tests for WeComBot template card functionalities, including event extraction and payload building for human input. - Enhance Dify service API runner tests to cover human input forms, including input collection, action handling, and form snapshot extraction. * feat: Enhance Telegram and QQ Official adapters with select field handling and form action processing - Added support for select fields in Telegram adapter, including option extraction and callback handling. - Implemented form action processing for Telegram callbacks, improving user interaction feedback. - Introduced new helper functions for building keyboards and resolving select button actions in QQ Official adapter. - Enhanced DifyServiceAPIRunner to handle cumulative streaming responses and improve error handling during workflow resumes. - Added unit tests for new functionalities in Telegram and QQ Official adapters, ensuring robust behavior for select fields and form actions. * feat(lark): add functions for current input definitions and visible form content handling feat(qqofficial): update fallback text handling for non-streaming scenarios feat(difysvapi): enhance form content processing for interactive fields and actions test: add unit tests for Lark and QQ Official adapter functionalities * Add tests for DingTalk adapter content processing and markdown formatting - Updated the assertion in `test_dingtalk_completed_input_lines_include_text_and_select_values` to remove unnecessary markdown formatting. - Added new tests to verify that `_dingtalk_clean_form_content` maintains the order of prompts and completed values in various scenarios. - Introduced `test_dingtalk_card_markdown_preserves_internal_line_breaks` to ensure internal line breaks are correctly converted to HTML line breaks. * feat: Refactor input handling and feedback messages across multiple adapters * feat: Update the human-computer interaction template cards, and optimize the prompt information and content display. * feat: Refactor pending form handling to isolate by bot and pipeline * feat: Enhance error handling and caching for Dify and WeCom interactions * feat: Enhance select input handling and validation in Dify API runner and Telegram adapter * feat: Add missing completed input lines handling in DingTalk adapter * feat: Add pipeline_uuid handling across multiple adapters and update related tests
This commit is contained in:
@@ -269,6 +269,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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,15 @@ 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, Info, QrCode } from 'lucide-react';
|
||||
import {
|
||||
Copy,
|
||||
Check,
|
||||
Globe,
|
||||
Info,
|
||||
QrCode,
|
||||
Download,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { copyToClipboard } from '@/app/utils/clipboard';
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -33,6 +41,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { systemInfo } from '@/app/infra/http';
|
||||
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||
|
||||
/**
|
||||
* Resolve the value referenced by a `show_if.field` string.
|
||||
@@ -291,6 +300,52 @@ 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 className="min-w-0">
|
||||
<FormLabel className="break-words">{label}</FormLabel>
|
||||
<div className="flex min-w-0 flex-wrap 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="max-w-2xl text-sm break-words text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display-only component for `__system.*` fields (e.g. the deployment's
|
||||
* outbound IPs that the operator must add to a platform's trusted-IP list).
|
||||
@@ -405,7 +460,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
|
||||
@@ -460,6 +515,7 @@ export default function DynamicFormComponent({
|
||||
item.type !== 'webhook-url' &&
|
||||
item.type !== 'embed-code' &&
|
||||
item.type !== 'qr-code-login' &&
|
||||
item.type !== 'download-link' &&
|
||||
!item.name.startsWith(SYSTEM_FIELD_PREFIX),
|
||||
),
|
||||
[itemConfigList],
|
||||
@@ -777,6 +833,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 (
|
||||
|
||||
@@ -18,6 +18,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;
|
||||
@@ -30,6 +34,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,12 @@ import {
|
||||
} from 'lucide-react';
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
export type QrLoginPlatform = 'feishu' | 'weixin' | 'dingtalk' | 'wecombot';
|
||||
export type QrLoginPlatform =
|
||||
| 'feishu'
|
||||
| 'weixin'
|
||||
| 'dingtalk'
|
||||
| 'wecombot'
|
||||
| 'qqofficial';
|
||||
|
||||
interface PlatformConfig {
|
||||
titleKey: string;
|
||||
@@ -29,6 +34,7 @@ interface PlatformConfig {
|
||||
apiBase: string;
|
||||
extractSuccess: (data: Record<string, string>) => Record<string, string>;
|
||||
successNoteKey?: string;
|
||||
boundByKey?: string;
|
||||
}
|
||||
|
||||
const PLATFORM_CONFIGS: Record<QrLoginPlatform, PlatformConfig> = {
|
||||
@@ -92,6 +98,22 @@ const PLATFORM_CONFIGS: Record<QrLoginPlatform, PlatformConfig> = {
|
||||
}),
|
||||
successNoteKey: 'wecombot.robotNameNote',
|
||||
},
|
||||
qqofficial: {
|
||||
titleKey: 'qqofficial.createBinding',
|
||||
connectingKey: 'qqofficial.connecting',
|
||||
scanQRCodeKey: 'qqofficial.scanQRCode',
|
||||
waitingKey: 'qqofficial.waitingForScan',
|
||||
successKey: 'qqofficial.bindSuccess',
|
||||
failedKey: 'qqofficial.bindFailed',
|
||||
retryKey: 'qqofficial.retry',
|
||||
apiBase: '/api/v1/platform/adapters/qqofficial/bind',
|
||||
extractSuccess: (data) => ({
|
||||
appid: data.appid,
|
||||
secret: data.secret,
|
||||
}),
|
||||
successNoteKey: 'qqofficial.tokenNote',
|
||||
boundByKey: 'qqofficial.boundBy',
|
||||
},
|
||||
};
|
||||
|
||||
interface QrCodeLoginDialogProps {
|
||||
@@ -118,6 +140,7 @@ export default function QrCodeLoginDialog({
|
||||
const [qrDataUrl, setQrDataUrl] = useState('');
|
||||
const [expireIn, setExpireIn] = useState(0);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [successMeta, setSuccessMeta] = useState('');
|
||||
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const checkExpiredRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
@@ -178,6 +201,7 @@ export default function QrCodeLoginDialog({
|
||||
setQrDataUrl('');
|
||||
setExpireIn(0);
|
||||
setErrorMessage('');
|
||||
setSuccessMeta('');
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
@@ -275,6 +299,13 @@ export default function QrCodeLoginDialog({
|
||||
sessionIdRef.current = null;
|
||||
cleanup();
|
||||
setState('success');
|
||||
// Platform may return extra audit metadata (e.g. QQ Official returns
|
||||
// the scanner's user_openid) — surface it briefly before the dialog closes.
|
||||
if (rest.user_openid && cfg.boundByKey) {
|
||||
setSuccessMeta(
|
||||
tRef.current(cfg.boundByKey, { openid: rest.user_openid }),
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
onSuccessRef.current(cfg.extractSuccess(rest));
|
||||
onOpenChangeRef.current(false);
|
||||
@@ -395,6 +426,11 @@ export default function QrCodeLoginDialog({
|
||||
<p className="text-sm text-green-600 font-medium">
|
||||
{t(platformConfig.successKey)}
|
||||
</p>
|
||||
{successMeta && (
|
||||
<p className="text-xs text-muted-foreground text-center max-w-xs break-all">
|
||||
{successMeta}
|
||||
</p>
|
||||
)}
|
||||
{platformConfig.successNoteKey && (
|
||||
<p className="text-xs text-muted-foreground text-center max-w-xs">
|
||||
{t(platformConfig.successNoteKey)}
|
||||
|
||||
@@ -44,6 +44,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 {
|
||||
@@ -72,6 +76,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]);
|
||||
|
||||
@@ -1822,6 +1822,19 @@ const enUS = {
|
||||
robotNameNote:
|
||||
'Robot Name cannot be obtained automatically. Please fill it in manually.',
|
||||
},
|
||||
qqofficial: {
|
||||
createBinding: 'One-Click QR Binding for QQ Official Bot',
|
||||
scanQRCode:
|
||||
'Scan the QR code below with mobile QQ and authorize the binding in QQ Bot Assistant',
|
||||
waitingForScan: 'Waiting for scan',
|
||||
bindSuccess: 'Bound successfully! AppID and Secret have been filled in',
|
||||
bindFailed: 'Binding failed',
|
||||
connecting: 'Connecting to QQ service...',
|
||||
retry: 'Retry',
|
||||
tokenNote:
|
||||
'The Token field is not used by the current adapter — you can leave it blank.',
|
||||
boundBy: 'Bound by QQ user {{openid}}',
|
||||
},
|
||||
pluginPages: {
|
||||
selectFromSidebar: 'Select a plugin page from the sidebar',
|
||||
invalidPage: 'Invalid plugin page',
|
||||
|
||||
@@ -1740,6 +1740,20 @@ const esES = {
|
||||
robotNameNote:
|
||||
'El nombre del robot no puede obtenerse automáticamente. Introdúcelo manualmente.',
|
||||
},
|
||||
qqofficial: {
|
||||
createBinding: 'Vinculación QR con un clic para el bot oficial de QQ',
|
||||
scanQRCode:
|
||||
'Escanea el código QR siguiente con QQ móvil y autoriza la vinculación en «QQ Bot Assistant»',
|
||||
waitingForScan: 'Esperando escaneo',
|
||||
bindSuccess:
|
||||
'¡Vinculación correcta! AppID y Secret se han rellenado automáticamente',
|
||||
bindFailed: 'Error en la vinculación',
|
||||
connecting: 'Conectando con el servicio de QQ...',
|
||||
retry: 'Reintentar',
|
||||
tokenNote:
|
||||
'El campo Token no es utilizado por el adaptador actual; puedes dejarlo vacío.',
|
||||
boundBy: 'Vinculado por el usuario QQ {{openid}}',
|
||||
},
|
||||
pluginPages: {
|
||||
selectFromSidebar: 'Selecciona una página de plugin en la barra lateral',
|
||||
invalidPage: 'Página de plugin no válida',
|
||||
|
||||
@@ -1733,6 +1733,19 @@ const jaJP = {
|
||||
retry: '再試行',
|
||||
robotNameNote: 'ロボット名は自動取得できません。手動で入力してください。',
|
||||
},
|
||||
qqofficial: {
|
||||
createBinding: 'ワンクリックで QQ 公式ボットを QR バインド',
|
||||
scanQRCode:
|
||||
'以下の QR コードをモバイル QQ でスキャンし、「QQ ボットアシスタント」でバインドを承認してください',
|
||||
waitingForScan: 'スキャン待ち',
|
||||
bindSuccess: 'バインド成功!AppID と Secret が自動入力されました',
|
||||
bindFailed: 'バインド失敗',
|
||||
connecting: 'QQ サービスに接続中...',
|
||||
retry: '再試行',
|
||||
tokenNote:
|
||||
'Token フィールドは現行アダプターでは使用しません。空欄のままで構いません。',
|
||||
boundBy: 'QQ ユーザー {{openid}} によりバインドされました',
|
||||
},
|
||||
pluginPages: {
|
||||
selectFromSidebar: 'サイドバーからプラグインページを選択してください',
|
||||
invalidPage: '無効なプラグインページ',
|
||||
|
||||
@@ -1711,6 +1711,19 @@ const ruRU = {
|
||||
robotNameNote:
|
||||
'Имя бота нельзя получить автоматически. Пожалуйста, введите его вручную.',
|
||||
},
|
||||
qqofficial: {
|
||||
createBinding: 'Привязка официального бота QQ по QR-коду',
|
||||
scanQRCode:
|
||||
'Отсканируйте QR-код ниже мобильным QQ и подтвердите привязку в «QQ Bot Assistant»',
|
||||
waitingForScan: 'Ожидание сканирования',
|
||||
bindSuccess: 'Привязка успешна! AppID и Secret заполнены автоматически',
|
||||
bindFailed: 'Не удалось выполнить привязку',
|
||||
connecting: 'Подключение к сервису QQ...',
|
||||
retry: 'Повторить',
|
||||
tokenNote:
|
||||
'Поле Token не используется текущим адаптером — его можно оставить пустым.',
|
||||
boundBy: 'Привязано пользователем QQ {{openid}}',
|
||||
},
|
||||
pluginPages: {
|
||||
selectFromSidebar: 'Выберите страницу плагина на боковой панели',
|
||||
invalidPage: 'Недопустимая страница плагина',
|
||||
|
||||
@@ -1672,6 +1672,18 @@ const thTH = {
|
||||
retry: 'ลองใหม่',
|
||||
robotNameNote: 'ไม่สามารถดึงชื่อบอตได้โดยอัตโนมัติ กรุณากรอกด้วยตนเอง',
|
||||
},
|
||||
qqofficial: {
|
||||
createBinding: 'ผูกบอต QQ Official ด้วย QR คลิกเดียว',
|
||||
scanQRCode:
|
||||
'สแกนคิวอาร์โค้ดด้านล่างด้วย QQ มือถือ แล้วอนุญาตการผูกใน «QQ Bot Assistant»',
|
||||
waitingForScan: 'กำลังรอสแกน',
|
||||
bindSuccess: 'ผูกสำเร็จ! AppID และ Secret ถูกกรอกอัตโนมัติแล้ว',
|
||||
bindFailed: 'การผูกล้มเหลว',
|
||||
connecting: 'กำลังเชื่อมต่อบริการ QQ...',
|
||||
retry: 'ลองใหม่',
|
||||
tokenNote: 'อะแดปเตอร์ปัจจุบันไม่ได้ใช้ฟิลด์ Token จึงเว้นว่างไว้ได้',
|
||||
boundBy: 'ผูกโดยผู้ใช้ QQ {{openid}}',
|
||||
},
|
||||
pluginPages: {
|
||||
selectFromSidebar: 'เลือกหน้าปลั๊กอินจากแถบด้านข้าง',
|
||||
invalidPage: 'หน้าปลั๊กอินไม่ถูกต้อง',
|
||||
|
||||
@@ -1701,6 +1701,19 @@ const viVN = {
|
||||
retry: 'Thử lại',
|
||||
robotNameNote: 'Không thể tự động lấy tên bot. Vui lòng điền thủ công.',
|
||||
},
|
||||
qqofficial: {
|
||||
createBinding: 'Liên kết bot QQ Official bằng QR một chạm',
|
||||
scanQRCode:
|
||||
'Quét mã QR bên dưới bằng QQ trên di động và xác nhận liên kết trong «QQ Bot Assistant»',
|
||||
waitingForScan: 'Đang chờ quét',
|
||||
bindSuccess: 'Liên kết thành công! AppID và Secret đã được điền tự động',
|
||||
bindFailed: 'Liên kết thất bại',
|
||||
connecting: 'Đang kết nối tới dịch vụ QQ...',
|
||||
retry: 'Thử lại',
|
||||
tokenNote:
|
||||
'Bộ chuyển đổi hiện tại không dùng trường Token; có thể để trống.',
|
||||
boundBy: 'Được liên kết bởi người dùng QQ {{openid}}',
|
||||
},
|
||||
pluginPages: {
|
||||
selectFromSidebar: 'Chọn một trang plugin từ thanh bên',
|
||||
invalidPage: 'Trang plugin không hợp lệ',
|
||||
|
||||
@@ -1741,6 +1741,17 @@ const zhHans = {
|
||||
retry: '重试',
|
||||
robotNameNote: '机器人名称无法自动获取,请手动填写。',
|
||||
},
|
||||
qqofficial: {
|
||||
createBinding: '一键扫码绑定 QQ 机器人',
|
||||
scanQRCode: '请使用手机 QQ 扫描以下二维码,在「QQ 机器人助手」中授权绑定',
|
||||
waitingForScan: '等待扫码中',
|
||||
bindSuccess: '绑定成功!AppID 与密钥已自动填入',
|
||||
bindFailed: '绑定失败',
|
||||
connecting: '正在连接 QQ 服务...',
|
||||
retry: '重试',
|
||||
tokenNote: 'Token 字段当前适配器未使用,留空即可。',
|
||||
boundBy: '由 QQ 用户 {{openid}} 扫码绑定',
|
||||
},
|
||||
pluginPages: {
|
||||
selectFromSidebar: '从侧边栏选择一个插件页面',
|
||||
invalidPage: '无效的插件页面',
|
||||
|
||||
@@ -1650,6 +1650,17 @@ const zhHant = {
|
||||
retry: '重試',
|
||||
robotNameNote: '機器人名稱無法自動取得,請手動填寫。',
|
||||
},
|
||||
qqofficial: {
|
||||
createBinding: '一鍵掃碼綁定 QQ 機器人',
|
||||
scanQRCode: '請使用手機 QQ 掃描以下 QR Code,在「QQ 機器人助手」中授權綁定',
|
||||
waitingForScan: '等待掃碼中',
|
||||
bindSuccess: '綁定成功!AppID 與密鑰已自動填入',
|
||||
bindFailed: '綁定失敗',
|
||||
connecting: '正在連線 QQ 服務...',
|
||||
retry: '重試',
|
||||
tokenNote: 'Token 欄位目前介面卡未使用,留空即可。',
|
||||
boundBy: '由 QQ 用戶 {{openid}} 掃碼綁定',
|
||||
},
|
||||
pluginPages: {
|
||||
selectFromSidebar: '從側邊欄選擇一個插件頁面',
|
||||
invalidPage: '無效的插件頁面',
|
||||
|
||||
Reference in New Issue
Block a user