feat(wizard): add inbound bot verification

This commit is contained in:
langbot-dev
2026-08-14 23:14:22 +08:00
parent 97b176aef2
commit f2ba540ffb
9 changed files with 333 additions and 15 deletions
+9
View File
@@ -461,6 +461,15 @@ export class BackendClient extends BaseHttpClient {
return this.post(`/api/v1/platform/bots/${botId}/logs`, request);
}
public testHttpBotInbound(
botId: string,
message: string,
): Promise<{ session_id: string; accepted_message_id: string }> {
return this.post(`/api/v1/platform/bots/${botId}/test-inbound`, {
message,
});
}
public getBotSessions(
botId: string,
limit: number = 100,
+125 -14
View File
@@ -14,6 +14,9 @@ import {
Cable,
Settings2,
Blocks,
Copy,
Send,
Webhook,
} from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
@@ -45,6 +48,7 @@ import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import i18n from 'i18next';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Card,
CardContent,
@@ -996,6 +1000,10 @@ function StepBotConfig({
extraWebhookUrl: string;
}) {
const { t } = useTranslation();
const [testMessage, setTestMessage] = useState(
t('wizard.botConfig.httpTestDefaultMessage'),
);
const [isSendingTest, setIsSendingTest] = useState(false);
const adapterLabel = useMemo(() => {
const a = adapters.find((ad) => ad.name === selectedAdapterName);
@@ -1010,6 +1018,29 @@ function StepBotConfig({
[],
);
const copyWebhookUrl = useCallback(async () => {
if (!webhookUrl) return;
await navigator.clipboard.writeText(webhookUrl);
toast.success(t('common.copySuccess'));
}, [t, webhookUrl]);
const sendHttpBotTest = useCallback(async () => {
if (!createdBotUuid || !testMessage.trim()) return;
setIsSendingTest(true);
try {
await httpClient.testHttpBotInbound(createdBotUuid, testMessage.trim());
toast.success(t('wizard.botConfig.httpTestAccepted'));
} catch (error) {
toast.error(
t('wizard.botConfig.httpTestFailed', {
error: error instanceof Error ? error.message : String(error),
}),
);
} finally {
setIsSendingTest(false);
}
}, [createdBotUuid, testMessage, t]);
return (
<div className="max-w-5xl mx-auto space-y-6">
<div className="text-center">
@@ -1019,6 +1050,100 @@ function StepBotConfig({
</p>
</div>
{botSaved && (
<div
className={cn(
'border px-4 py-3',
messageReceived
? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/30'
: 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30',
)}
>
<div className="flex items-start gap-3">
<div
className={cn(
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full',
messageReceived ? 'bg-green-500' : 'bg-amber-500',
)}
>
{messageReceived ? (
<Check className="size-3 text-white" />
) : selectedAdapterName === 'http_bot' ? (
<Send className="size-3 text-white" />
) : webhookUrl ? (
<Webhook className="size-3 text-white" />
) : (
<Loader2 className="size-3 animate-spin text-white" />
)}
</div>
<div className="min-w-0 flex-1">
<p
className={cn(
'text-sm font-medium',
messageReceived
? 'text-green-800 dark:text-green-200'
: 'text-amber-800 dark:text-amber-200',
)}
>
{messageReceived
? t('wizard.botConfig.messageReceived')
: selectedAdapterName === 'http_bot'
? t('wizard.botConfig.httpTestPrompt')
: webhookUrl
? t('wizard.botConfig.webhookTestPrompt')
: t('wizard.botConfig.waitingForMessage')}
</p>
{!messageReceived && webhookUrl && (
<div className="mt-3 space-y-3">
<div className="flex items-center gap-2">
<code className="min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap border bg-background px-2.5 py-2 text-xs">
{webhookUrl}
</code>
<Button
type="button"
variant="outline"
size="icon"
className="size-9 shrink-0"
onClick={copyWebhookUrl}
title={t('common.copy')}
>
<Copy className="size-4" />
</Button>
</div>
{selectedAdapterName === 'http_bot' && (
<div className="flex flex-col gap-2 sm:flex-row">
<Input
value={testMessage}
onChange={(event) => setTestMessage(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') void sendHttpBotTest();
}}
className="bg-background"
/>
<Button
type="button"
onClick={() => void sendHttpBotTest()}
disabled={isSendingTest || !testMessage.trim()}
className="shrink-0"
>
{isSendingTest ? (
<Loader2 className="mr-1.5 size-4 animate-spin" />
) : (
<Send className="mr-1.5 size-4" />
)}
{t('wizard.botConfig.sendHttpTest')}
</Button>
</div>
)}
</div>
)}
</div>
</div>
</div>
)}
<div className="grid gap-6 grid-cols-1 lg:grid-cols-2">
{/* Left column: Adapter config form */}
<div className="space-y-4">
@@ -1082,20 +1207,6 @@ function StepBotConfig({
</CardContent>
</Card>
)}
{/* Bot and inbound-message verification status */}
{botSaved && (
<div className="flex items-center gap-2 px-4 py-3 rounded-lg border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/30">
<div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center shrink-0">
<Check className="w-3 h-3 text-white" />
</div>
<span className="text-sm text-green-700 dark:text-green-300">
{messageReceived
? t('wizard.botConfig.messageReceived')
: t('wizard.botConfig.waitingForMessage')}
</span>
</div>
)}
</div>
{/* Right column: Bot logs */}
+11
View File
@@ -1831,6 +1831,17 @@ const enUS = {
'The bot is enabled. Send it a message from your IM platform to continue.',
messageReceived:
'The bot received an IM message. You can continue to the next step.',
webhookTestPrompt:
'The callback URL is ready. Configure it on the external platform, then send the bot a real message.',
httpTestPrompt:
'HTTP Bot is enabled. Send a real inbound message here to verify the connection.',
httpTestDefaultMessage: 'Hello, this is a connection test message.',
sendHttpTest: 'Send Test Message',
httpTestAccepted:
'The test message was accepted. It will appear in the log shortly.',
httpTestMissingSecret:
'Enter an inbound signing secret and save the configuration first.',
httpTestFailed: 'Failed to send the test message: {{error}}',
logsTitle: 'Bot Logs',
logsDescription:
'Monitor bot activity to verify the platform connection is working.',
+11
View File
@@ -1748,6 +1748,17 @@ const jaJP = {
'ボットが有効になりました。続行するには IM からメッセージを送信してください。',
messageReceived:
'ボットが IM メッセージを受信しました。次のステップに進めます。',
webhookTestPrompt:
'コールバック URL の準備ができました。外部プラットフォームに設定し、ボットへ実際のメッセージを送信してください。',
httpTestPrompt:
'HTTP Bot が有効になりました。実際の受信メッセージを送信して接続を確認できます。',
httpTestDefaultMessage: 'こんにちは。これは接続テストメッセージです。',
sendHttpTest: 'テストメッセージを送信',
httpTestAccepted:
'テストメッセージを受け付けました。まもなくログに表示されます。',
httpTestMissingSecret:
'受信署名シークレットを入力し、先に設定を保存してください。',
httpTestFailed: 'テストメッセージの送信に失敗しました:{{error}}',
logsTitle: 'ボットログ',
logsDescription:
'ボットの活動を監視して、プラットフォーム接続が正常に動作していることを確認します。',
+8
View File
@@ -1751,6 +1751,14 @@ const zhHans = {
botSaved: '机器人配置已保存并启用,请查看日志确认连接正常。',
waitingForMessage: '机器人已启用。请在 IM 中向机器人发送一条消息以继续。',
messageReceived: '机器人已成功收到 IM 消息,可以进入下一步。',
webhookTestPrompt:
'回调地址已就绪。将它配置到外部平台,然后向机器人发送一条真实消息。',
httpTestPrompt: 'HTTP Bot 已启用。可直接发送一条真实入站消息验证连接。',
httpTestDefaultMessage: '你好,这是一条连接测试消息。',
sendHttpTest: '发送测试消息',
httpTestAccepted: '测试消息已被机器人接收,请稍候查看日志。',
httpTestMissingSecret: '请先填写入站签名密钥并重新保存。',
httpTestFailed: '测试消息发送失败:{{error}}',
logsTitle: '机器人日志',
logsDescription: '监控机器人活动,确认平台连接是否正常工作。',
},
+4
View File
@@ -26,6 +26,10 @@ export default defineConfig(({ mode }) => {
target: apiProxyTarget,
changeOrigin: true,
},
'/bots': {
target: apiProxyTarget,
changeOrigin: true,
},
},
},
build: {