mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-26 15:34:26 +00:00
feat(adapters): mark EBA-superseded adapters as legacy and collapse them
The 12 old adapters that now have an EBA replacement are tagged `spec.legacy: true` in their source manifests. Principle: don't delete, de-emphasize. - sources/*.yaml (aiocqhttp, dingtalk, discord, kook, lark, officialaccount, qqofficial, slack, telegram, wecom, wecombot, wecomcs): add spec.legacy: true - Adapter / IChooseAdapterEntity types: add optional legacy flag - BotForm adapter Select: split legacy adapters into a collapsed, grayscale group at the bottom with an explanatory hint; auto-expand when the bot already uses a legacy adapter - Wizard platform picker: same collapsed legacy section - i18n: legacyAdapters / legacyAdaptersHint (zh-Hans, en-US) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ metadata:
|
||||
zh_Hant: OneBot v11 適配器,用於接入 QQ 機器人協定端,請查看文件了解使用方式
|
||||
icon: onebot.png
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- protocol
|
||||
help_links:
|
||||
|
||||
@@ -12,6 +12,7 @@ metadata:
|
||||
zh_Hant: 釘釘適配器,請查看文件了解使用方式
|
||||
icon: dingtalk.svg
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- china
|
||||
help_links:
|
||||
|
||||
@@ -20,6 +20,7 @@ metadata:
|
||||
es_ES: Adaptador de Discord, requiere un entorno de red con acceso al servidor de Discord
|
||||
icon: discord.svg
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- popular
|
||||
- global
|
||||
|
||||
@@ -12,6 +12,7 @@ metadata:
|
||||
zh_Hant: KOOK 適配器(原開黑啦),支援頻道訊息和私聊訊息
|
||||
icon: kook.png
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- china
|
||||
help_links:
|
||||
|
||||
@@ -14,6 +14,7 @@ metadata:
|
||||
ja_JP: Lark アダプター、長期接続およびWebhookモードの両方をサポートしています。使用方法の詳細については、ドキュメントを参照してください。
|
||||
icon: lark.svg
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- popular
|
||||
- china
|
||||
|
||||
@@ -12,6 +12,7 @@ metadata:
|
||||
zh_Hant: 微信公眾號適配器,需要公網地址以接收訊息推送,請查看文件了解使用方式
|
||||
icon: officialaccount.png
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- china
|
||||
help_links:
|
||||
|
||||
@@ -12,6 +12,7 @@ metadata:
|
||||
zh_Hant: QQ 官方 API,支援 Webhook 和 WebSocket 兩種連線模式
|
||||
icon: qqofficial.svg
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- china
|
||||
help_links:
|
||||
|
||||
@@ -20,6 +20,7 @@ metadata:
|
||||
es_ES: Adaptador de Slack, requiere una dirección pública para recibir notificaciones de mensajes de Slack, consulte la documentación para obtener instrucciones de uso
|
||||
icon: slack.png
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- popular
|
||||
- global
|
||||
|
||||
@@ -20,6 +20,7 @@ metadata:
|
||||
es_ES: Adaptador de Telegram, consulte la documentación para obtener instrucciones de uso
|
||||
icon: telegram.svg
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- popular
|
||||
- global
|
||||
|
||||
@@ -12,6 +12,7 @@ metadata:
|
||||
zh_Hant: 企業微信內部機器人,請查看文件了解使用方式
|
||||
icon: wecom.png
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- popular
|
||||
- china
|
||||
|
||||
@@ -12,6 +12,7 @@ metadata:
|
||||
zh_Hant: 企業微信智慧機器人,支援長連線和 Webhook 兩種接入方式,請查看文件了解使用方式
|
||||
icon: wecombot.png
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- china
|
||||
help_links:
|
||||
|
||||
@@ -12,6 +12,7 @@ metadata:
|
||||
zh_Hant: 企業微信對外客服機器人,需要公網地址以接收訊息推送,請查看文件了解使用方式
|
||||
icon: wecom.png
|
||||
spec:
|
||||
legacy: true
|
||||
categories:
|
||||
- china
|
||||
help_links:
|
||||
|
||||
@@ -13,7 +13,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { systemInfo } from '@/app/infra/http';
|
||||
import { Agent, Bot } from '@/app/infra/entities/api';
|
||||
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import { ExternalLink, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import EventBindingsEditor from './EventBindingsEditor';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -138,11 +138,35 @@ export default function BotForm({
|
||||
const currentAdapter = form.watch('adapter');
|
||||
const currentAdapterConfig = form.watch('adapter_config');
|
||||
|
||||
// Group adapters by category for the Select dropdown
|
||||
const groupedAdapters = useMemo(
|
||||
() => groupByCategory(adapterNameList),
|
||||
// Group adapters by category for the Select dropdown. Legacy adapters
|
||||
// (those superseded by an EBA implementation) are split out and shown in a
|
||||
// collapsed group at the bottom so they're de-emphasized but still usable.
|
||||
const activeAdapters = useMemo(
|
||||
() => adapterNameList.filter((a) => !a.legacy),
|
||||
[adapterNameList],
|
||||
);
|
||||
const legacyAdapters = useMemo(
|
||||
() => adapterNameList.filter((a) => a.legacy),
|
||||
[adapterNameList],
|
||||
);
|
||||
const groupedAdapters = useMemo(
|
||||
() => groupByCategory(activeAdapters),
|
||||
[activeAdapters],
|
||||
);
|
||||
|
||||
// Whether the collapsed legacy adapter group is expanded in the Select.
|
||||
const [showLegacyAdapters, setShowLegacyAdapters] = useState(false);
|
||||
|
||||
// Auto-expand the legacy group when the selected adapter is itself legacy,
|
||||
// so editing an existing bot on a legacy adapter still reveals the choice.
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentAdapter &&
|
||||
legacyAdapters.some((a) => a.value === currentAdapter)
|
||||
) {
|
||||
setShowLegacyAdapters(true);
|
||||
}
|
||||
}, [currentAdapter, legacyAdapters]);
|
||||
|
||||
// Notify parent when dirty state changes
|
||||
const { isDirty } = form.formState;
|
||||
@@ -207,6 +231,7 @@ export default function BotForm({
|
||||
label: extractI18nObject(item.label),
|
||||
value: item.name,
|
||||
categories: item.spec.categories,
|
||||
legacy: item.spec.legacy,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -514,6 +539,62 @@ export default function BotForm({
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
{legacyAdapters.length > 0 && (
|
||||
<>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowLegacyAdapters((v) => !v);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setShowLegacyAdapters((v) => !v);
|
||||
}
|
||||
}}
|
||||
className="flex cursor-pointer items-center gap-1 px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground border-t mt-1 pt-2"
|
||||
>
|
||||
{showLegacyAdapters ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t('bots.legacyAdapters')}
|
||||
<span className="ml-1 rounded bg-muted px-1.5 py-0.5 text-[10px]">
|
||||
{legacyAdapters.length}
|
||||
</span>
|
||||
</div>
|
||||
{showLegacyAdapters && (
|
||||
<>
|
||||
<p className="px-2 pb-1 text-[11px] leading-snug text-muted-foreground">
|
||||
{t('bots.legacyAdaptersHint')}
|
||||
</p>
|
||||
<SelectGroup>
|
||||
{legacyAdapters.map((item) => (
|
||||
<SelectItem
|
||||
key={`legacy:${item.value}`}
|
||||
value={item.value}
|
||||
>
|
||||
<div className="flex items-center gap-2 opacity-70">
|
||||
<img
|
||||
src={httpClient.getAdapterIconURL(
|
||||
item.value,
|
||||
)}
|
||||
alt=""
|
||||
className="h-5 w-5 rounded grayscale"
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{currentAdapter &&
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface IChooseAdapterEntity {
|
||||
label: string;
|
||||
value: string;
|
||||
categories?: string[];
|
||||
legacy?: boolean;
|
||||
}
|
||||
|
||||
export interface IPipelineEntity {
|
||||
|
||||
@@ -205,6 +205,7 @@ export interface Adapter {
|
||||
icon?: string;
|
||||
spec: {
|
||||
categories?: string[];
|
||||
legacy?: boolean;
|
||||
help_links?: Record<string, string>;
|
||||
supported_events?: string[];
|
||||
supported_apis?: string[];
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Sparkles,
|
||||
PartyPopper,
|
||||
Loader2,
|
||||
@@ -765,14 +767,24 @@ function StepPlatform({
|
||||
onSelect: (name: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [showLegacy, setShowLegacy] = useState(false);
|
||||
|
||||
const activeAdapters = useMemo(
|
||||
() => adapters.filter((a) => !a.spec.legacy),
|
||||
[adapters],
|
||||
);
|
||||
const legacyAdapters = useMemo(
|
||||
() => adapters.filter((a) => a.spec.legacy),
|
||||
[adapters],
|
||||
);
|
||||
|
||||
const groupedAdapters = useMemo(() => {
|
||||
const withCategories = adapters.map((a) => ({
|
||||
const withCategories = activeAdapters.map((a) => ({
|
||||
...a,
|
||||
categories: a.spec.categories,
|
||||
}));
|
||||
return groupByCategory(withCategories);
|
||||
}, [adapters]);
|
||||
}, [activeAdapters]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl mx-auto">
|
||||
@@ -848,6 +860,66 @@ function StepPlatform({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{legacyAdapters.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setShowLegacy((v) => !v)}
|
||||
>
|
||||
{showLegacy ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
{t('bots.legacyAdapters')}
|
||||
<span className="rounded bg-muted px-1.5 py-0.5 text-xs">
|
||||
{legacyAdapters.length}
|
||||
</span>
|
||||
</button>
|
||||
{showLegacy && (
|
||||
<>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('bots.legacyAdaptersHint')}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 opacity-60">
|
||||
{legacyAdapters.map((adapter) => (
|
||||
<Card
|
||||
key={adapter.name}
|
||||
className={cn(
|
||||
'cursor-pointer transition-all hover:shadow-md',
|
||||
selected === adapter.name
|
||||
? 'ring-2 ring-primary shadow-md'
|
||||
: 'hover:border-primary/50',
|
||||
)}
|
||||
onClick={() => onSelect(adapter.name)}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center gap-3 pb-2">
|
||||
<img
|
||||
src={httpClient.getAdapterIconURL(adapter.name)}
|
||||
alt=""
|
||||
className="w-10 h-10 rounded-lg shrink-0 grayscale"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="text-base truncate">
|
||||
{extractI18nObject(adapter.label)}
|
||||
</CardTitle>
|
||||
</div>
|
||||
{selected === adapter.name && (
|
||||
<div className="ml-auto shrink-0">
|
||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-primary-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -339,6 +339,9 @@ const enUS = {
|
||||
deleteConfirmation: 'Are you sure you want to delete this bot?',
|
||||
platformAdapter: 'Platform/Adapter Selection',
|
||||
selectAdapter: 'Select Adapter',
|
||||
legacyAdapters: 'Legacy adapters',
|
||||
legacyAdaptersHint:
|
||||
'These adapters are superseded by their newer (EBA) versions. They are kept only for existing configurations and are not recommended for new bots.',
|
||||
adapterConfig: 'Adapter Configuration',
|
||||
viewAdapterDocs: 'View Docs',
|
||||
bindPipeline: 'Bind Pipeline',
|
||||
|
||||
@@ -324,6 +324,9 @@ const zhHans = {
|
||||
deleteConfirmation: '你确定要删除这个机器人吗?',
|
||||
platformAdapter: '平台/适配器选择',
|
||||
selectAdapter: '选择适配器',
|
||||
legacyAdapters: '旧版适配器',
|
||||
legacyAdaptersHint:
|
||||
'这些适配器已被新版(EBA 架构)取代,仅为兼容存量配置保留,不建议新建机器人时使用。',
|
||||
adapterConfig: '适配器配置',
|
||||
viewAdapterDocs: '查看文档',
|
||||
bindPipeline: '绑定流水线',
|
||||
|
||||
Reference in New Issue
Block a user