mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-25 23:14:20 +00:00
feat(qqofficial): implement one-click QR binding and enhance localization support
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -1383,6 +1383,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',
|
||||
|
||||
@@ -1426,6 +1426,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',
|
||||
|
||||
@@ -1385,6 +1385,19 @@
|
||||
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: '無効なプラグインページ',
|
||||
|
||||
@@ -1395,6 +1395,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: 'Недопустимая страница плагина',
|
||||
|
||||
@@ -1361,6 +1361,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: 'หน้าปลั๊กอินไม่ถูกต้อง',
|
||||
|
||||
@@ -1385,6 +1385,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ệ',
|
||||
|
||||
@@ -1319,6 +1319,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: '无效的插件页面',
|
||||
|
||||
@@ -1319,6 +1319,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