mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-11 04:17:14 +00:00
fix(bots): streamline detail layout and event listening
This commit is contained in:
@@ -13,7 +13,7 @@ src/langbot/pkg/platform/adapters/aiocqhttp/
|
||||
├── message_converter.py
|
||||
├── platform_api.py
|
||||
├── types.py
|
||||
└── onebot.svg
|
||||
└── onebot.png
|
||||
```
|
||||
|
||||
The EBA adapter is registered as `aiocqhttp-omni`. The legacy adapter remains at `src/langbot/pkg/platform/sources/aiocqhttp.py`.
|
||||
|
||||
@@ -11,7 +11,7 @@ metadata:
|
||||
en_US: OneBot v11 adapter for QQ-compatible protocol endpoints with event-driven orchestration support
|
||||
zh_Hans: OneBot v11 适配器,用于接入 QQ 兼容协议端,支持事件驱动编排
|
||||
zh_Hant: OneBot v11 適配器,用於接入 QQ 相容協定端,支援事件驅動編排
|
||||
icon: onebot.svg
|
||||
icon: onebot.png
|
||||
|
||||
spec:
|
||||
categories:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="96" height="96" rx="20" fill="#16A34A"/>
|
||||
<path d="M24 33C24 25.268 30.268 19 38 19H58C65.732 19 72 25.268 72 33V51C72 58.732 65.732 65 58 65H41.5L29 77V64.059C26.024 61.514 24 57.729 24 51V33Z" fill="white"/>
|
||||
<circle cx="39" cy="42" r="5" fill="#16A34A"/>
|
||||
<circle cx="57" cy="42" r="5" fill="#16A34A"/>
|
||||
<path d="M39 53C44.5 57 51.5 57 57 53" stroke="#16A34A" stroke-width="5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 527 B |
@@ -4,13 +4,6 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -36,6 +29,7 @@ import { Bot } from '@/app/infra/entities/api';
|
||||
import EntityBasicInfoDialog, {
|
||||
EntityBasicInfoValues,
|
||||
} from '@/app/home/components/entity-basic-info/EntityBasicInfoDialog';
|
||||
import AdapterEventDebugDialog from './components/bot-form/AdapterEventDebugDialog';
|
||||
import EntityTitleEditButton from '@/app/home/components/entity-basic-info/EntityTitleEditButton';
|
||||
|
||||
export default function BotDetailContent({ id }: { id: string }) {
|
||||
@@ -61,6 +55,7 @@ export default function BotDetailContent({ id }: { id: string }) {
|
||||
}, [id, isCreateMode, bots, setDetailEntityName, t]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState('config');
|
||||
const [adapterLabel, setAdapterLabel] = useState('');
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [basicInfoOpen, setBasicInfoOpen] = useState(false);
|
||||
const [bot, setBot] = useState<Bot | null>(null);
|
||||
@@ -191,7 +186,7 @@ export default function BotDetailContent({ id }: { id: string }) {
|
||||
<>
|
||||
<div className="flex h-full min-w-0 flex-col">
|
||||
{/* Sticky Header: title + enable switch + save button */}
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-between gap-3 pb-4">
|
||||
<div className="flex min-w-0 items-center gap-4">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<h1 className="truncate text-xl font-semibold">
|
||||
@@ -219,14 +214,29 @@ export default function BotDetailContent({ id }: { id: string }) {
|
||||
)}
|
||||
</div>
|
||||
{canManage && (
|
||||
<Button
|
||||
type="submit"
|
||||
form="bot-form"
|
||||
disabled={!formDirty}
|
||||
className={activeTab !== 'config' ? 'invisible' : ''}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<AdapterEventDebugDialog
|
||||
key={id}
|
||||
botId={id}
|
||||
adapterLabel={adapterLabel}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
form="bot-form"
|
||||
disabled={!formDirty}
|
||||
className={activeTab !== 'config' ? 'invisible' : ''}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -286,9 +296,9 @@ export default function BotDetailContent({ id }: { id: string }) {
|
||||
{/* Tab: Configuration */}
|
||||
<TabsContent
|
||||
value="config"
|
||||
className="mt-4 min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden"
|
||||
className="mt-4 min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden lg:overflow-hidden"
|
||||
>
|
||||
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-6 pb-8">
|
||||
<div className="min-h-0 min-w-0 pb-4 lg:h-full lg:pb-0">
|
||||
<fieldset className="contents" disabled={!canManage}>
|
||||
<BotForm
|
||||
ref={botFormRef}
|
||||
@@ -296,43 +306,9 @@ export default function BotDetailContent({ id }: { id: string }) {
|
||||
onFormSubmit={handleFormSubmit}
|
||||
onNewBotCreated={handleNewBotCreated}
|
||||
onDirtyChange={setFormDirty}
|
||||
onAdapterLabelChange={setAdapterLabel}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
{/* Card: Danger Zone */}
|
||||
{canManage && (
|
||||
<Card className="border-destructive/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-destructive">
|
||||
{t('bots.dangerZone')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('bots.dangerZoneDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{t('bots.deleteBotAction')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('bots.deleteBotHint')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
>
|
||||
<Trash2 className="size-4 mr-1.5" />
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -192,12 +192,11 @@ export default function AdapterEventDebugDialog({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!botId}
|
||||
onClick={() => setOpen(true)}
|
||||
title={!botId ? t('bots.adapterEventNeedsSavedBot') : undefined}
|
||||
>
|
||||
<RadioTower className="mr-1 h-4 w-4" />
|
||||
<RadioTower className="size-4" />
|
||||
{t('bots.adapterEventDebugAction')}
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import { systemInfo } from '@/app/infra/http';
|
||||
import { Agent, Bot } from '@/app/infra/entities/api';
|
||||
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||
import { ExternalLink, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import EventBindingsEditor from './EventBindingsEditor';
|
||||
import AdapterEventDebugDialog from './AdapterEventDebugDialog';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -101,10 +101,17 @@ interface BotFormProps {
|
||||
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
|
||||
onNewBotCreated: (botId: string) => void;
|
||||
onDirtyChange?: (dirty: boolean) => void;
|
||||
onAdapterLabelChange?: (label: string) => void;
|
||||
}
|
||||
|
||||
const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
{ initBotId, onFormSubmit, onNewBotCreated, onDirtyChange },
|
||||
{
|
||||
initBotId,
|
||||
onFormSubmit,
|
||||
onNewBotCreated,
|
||||
onDirtyChange,
|
||||
onAdapterLabelChange,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
@@ -153,6 +160,12 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
|
||||
// Watch adapter and adapter_config for filtering
|
||||
const currentAdapter = form.watch('adapter');
|
||||
const adapterLabel =
|
||||
adapterNameList.find((adapter) => adapter.value === currentAdapter)
|
||||
?.label ?? '';
|
||||
useEffect(() => {
|
||||
onAdapterLabelChange?.(adapterLabel);
|
||||
}, [adapterLabel, onAdapterLabelChange]);
|
||||
const currentAdapterConfig = form.watch('adapter_config');
|
||||
|
||||
// Group adapters by category for the Select dropdown. Legacy adapters are
|
||||
@@ -440,10 +453,15 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
id="bot-form"
|
||||
onSubmit={form.handleSubmit(onDynamicFormSubmit)}
|
||||
aria-busy={isLoading}
|
||||
className="w-full min-w-0 max-w-full"
|
||||
className={cn('w-full min-w-0 max-w-full', initBotId && 'lg:h-full')}
|
||||
>
|
||||
<fieldset
|
||||
className="w-full min-w-0 max-w-full space-y-6"
|
||||
className={cn(
|
||||
'w-full min-w-0 max-w-full',
|
||||
initBotId
|
||||
? 'grid gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] lg:grid-rows-[minmax(0,1fr)]'
|
||||
: 'space-y-6',
|
||||
)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{!initBotId && (
|
||||
@@ -489,14 +507,24 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
)}
|
||||
|
||||
{/* Card 2: Adapter Configuration */}
|
||||
<Card>
|
||||
<Card
|
||||
className={cn(
|
||||
'min-w-0',
|
||||
initBotId && 'lg:min-h-0 lg:overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('bots.adapterConfig')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('bots.adapterConfigDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent
|
||||
className={cn(
|
||||
'min-w-0 space-y-4',
|
||||
initBotId && 'lg:min-h-0 lg:flex-1 lg:overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="adapter"
|
||||
@@ -507,7 +535,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
@@ -515,7 +543,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-[240px] overflow-hidden">
|
||||
<SelectTrigger className="w-full min-w-0 overflow-hidden sm:w-[240px]">
|
||||
{field.value ? (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<img
|
||||
@@ -693,40 +721,29 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentAdapter && initBotId && (
|
||||
<div className="flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">
|
||||
{t('bots.adapterConfigurationTest')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t('bots.adapterConfigurationTestDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<AdapterEventDebugDialog
|
||||
botId={initBotId}
|
||||
adapterLabel={
|
||||
adapterNameList.find(
|
||||
(adapter) => adapter.value === currentAdapter,
|
||||
)?.label ?? currentAdapter
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Card 3: Event Routing */}
|
||||
{currentAdapter && (
|
||||
<Card>
|
||||
<Card
|
||||
className={cn(
|
||||
'min-w-0',
|
||||
initBotId && 'lg:min-h-0 lg:overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('bots.eventRouting')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('bots.eventRoutingDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent
|
||||
className={cn(
|
||||
'min-w-0',
|
||||
initBotId && 'lg:min-h-0 lg:flex-1 lg:overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
<EventBindingsEditor
|
||||
form={form}
|
||||
botId={initBotId}
|
||||
|
||||
@@ -450,7 +450,7 @@ const enUS = {
|
||||
routeFallbackIgnored:
|
||||
'Events that match no route are ignored. Add a catch-all route only when every event needs an explicit outcome.',
|
||||
testRoute: 'Check route',
|
||||
adapterEventDebugAction: 'Listen for platform events',
|
||||
adapterEventDebugAction: 'Test listener',
|
||||
adapterEventDebugTitle: 'Platform event debugging',
|
||||
adapterEventDebugDescription:
|
||||
'Trigger an event in {{platform}}. It will appear here when the adapter receives it.',
|
||||
|
||||
@@ -348,6 +348,7 @@ const esES = {
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
adapterEventDebugAction: 'Probar escucha',
|
||||
title: 'Bots',
|
||||
description:
|
||||
'Crea y gestiona Bots, que son los puntos de entrada para que LangBot se conecte con diversas plataformas',
|
||||
|
||||
@@ -458,7 +458,7 @@ const jaJP = {
|
||||
routeFallbackIgnored:
|
||||
'どのルートにも一致しないイベントは無視されます。すべてのイベントに明示的な結果が必要な場合のみ、フォールバックを追加してください。',
|
||||
testRoute: 'ルートを確認',
|
||||
adapterEventDebugAction: 'プラットフォームイベントを監視',
|
||||
adapterEventDebugAction: 'イベント監視テスト',
|
||||
adapterEventDebugTitle: 'プラットフォームイベントのデバッグ',
|
||||
adapterEventDebugDescription:
|
||||
'{{platform}} でイベントを発生させると、アダプターの受信後にここへ表示されます。',
|
||||
|
||||
@@ -346,6 +346,7 @@ const ruRU = {
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
adapterEventDebugAction: 'Тест прослушивания',
|
||||
title: 'Боты',
|
||||
description:
|
||||
'Создание и управление ботами — точками входа LangBot для подключения к различным платформам',
|
||||
|
||||
@@ -333,6 +333,7 @@ const thTH = {
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
adapterEventDebugAction: 'ทดสอบการรับเหตุการณ์',
|
||||
title: 'บอท',
|
||||
description:
|
||||
'สร้างและจัดการ Bot ซึ่งเป็นจุดเชื่อมต่อของ LangBot กับแพลตฟอร์มต่างๆ',
|
||||
|
||||
@@ -342,6 +342,7 @@ const viVN = {
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
adapterEventDebugAction: 'Kiểm tra lắng nghe',
|
||||
title: 'Bot',
|
||||
description:
|
||||
'Tạo và quản lý Bot, là điểm kết nối của LangBot với các nền tảng khác nhau',
|
||||
|
||||
@@ -428,7 +428,7 @@ const zhHans = {
|
||||
routeFallbackIgnored:
|
||||
'未命中任何路由的事件会被忽略。只有需要为每个事件指定结果时,才添加全局兜底路由。',
|
||||
testRoute: '检查路由',
|
||||
adapterEventDebugAction: '监听平台事件',
|
||||
adapterEventDebugAction: '测试监听',
|
||||
adapterEventDebugTitle: '平台事件调试',
|
||||
adapterEventDebugDescription:
|
||||
'在 {{platform}} 中触发事件,适配器收到后会显示在这里。',
|
||||
|
||||
@@ -322,6 +322,7 @@ const zhHant = {
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
adapterEventDebugAction: '測試監聽',
|
||||
title: '機器人',
|
||||
description: '建立和管理機器人,這是 LangBot 與各個平台連接的入口',
|
||||
createBot: '建立機器人',
|
||||
|
||||
@@ -501,16 +501,15 @@ test.describe('bot advanced flows', () => {
|
||||
const adapterCard = page.locator('[data-slot="card"]').filter({
|
||||
has: page.getByText('Adapter Configuration', { exact: true }),
|
||||
});
|
||||
const dangerCard = page
|
||||
.locator('[data-slot="card"]')
|
||||
.filter({ has: page.getByText('Danger Zone', { exact: true }) });
|
||||
const routingBox = await routingCard.boundingBox();
|
||||
const dangerBox = await dangerCard.boundingBox();
|
||||
const adapterBox = await adapterCard.boundingBox();
|
||||
expect(routingBox).not.toBeNull();
|
||||
expect(dangerBox).not.toBeNull();
|
||||
expect(
|
||||
dangerBox!.y - (routingBox!.y + routingBox!.height),
|
||||
).toBeGreaterThanOrEqual(20);
|
||||
expect(adapterBox).not.toBeNull();
|
||||
expect(routingBox!.x).toBeGreaterThan(adapterBox!.x + adapterBox!.width);
|
||||
expect(Math.abs(routingBox!.y - adapterBox!.y)).toBeLessThan(2);
|
||||
await expect(
|
||||
page.getByRole('button', { name: /^Delete$/ }),
|
||||
).toBeInViewport();
|
||||
|
||||
await routingCard.getByRole('button', { name: 'View all' }).click();
|
||||
await expect(
|
||||
@@ -588,15 +587,22 @@ test.describe('bot advanced flows', () => {
|
||||
expect(dialogBox!.height).toBeLessThan(500);
|
||||
|
||||
await routeDialog.getByRole('button', { name: 'Close' }).first().click();
|
||||
await expect(
|
||||
routingCard.getByRole('button', { name: 'Listen for platform events' }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
adapterCard.getByText('Test adapter configuration'),
|
||||
).toBeVisible();
|
||||
await adapterCard
|
||||
.getByRole('button', { name: 'Listen for platform events' })
|
||||
.click();
|
||||
).toHaveCount(0);
|
||||
const listenButton = page.getByRole('button', {
|
||||
name: 'Test listener',
|
||||
exact: true,
|
||||
});
|
||||
const listenBox = await listenButton.boundingBox();
|
||||
const saveBox = await page
|
||||
.getByRole('button', { name: /^Save$/ })
|
||||
.boundingBox();
|
||||
expect(listenBox).not.toBeNull();
|
||||
expect(saveBox).not.toBeNull();
|
||||
expect(listenBox!.x + listenBox!.width).toBeLessThan(saveBox!.x);
|
||||
expect(Math.abs(listenBox!.y - saveBox!.y)).toBeLessThan(2);
|
||||
await listenButton.click();
|
||||
const adapterDialog = page.getByRole('dialog');
|
||||
await expect(
|
||||
adapterDialog.getByText('Platform event debugging', { exact: true }),
|
||||
@@ -1338,6 +1344,7 @@ test.describe('cross-resource flows', () => {
|
||||
|
||||
const firstHandle = page.getByRole('button', { name: 'Drag route 1' });
|
||||
const secondCard = routeCards.nth(1);
|
||||
await firstHandle.scrollIntoViewIfNeeded();
|
||||
const handleBox = await firstHandle.boundingBox();
|
||||
const targetBox = await secondCard.boundingBox();
|
||||
expect(handleBox).not.toBeNull();
|
||||
|
||||
Reference in New Issue
Block a user