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:
Junyan Qin
2026-06-26 19:10:07 +08:00
parent 40e7481032
commit ce31fc81b8
18 changed files with 179 additions and 6 deletions
@@ -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 {
+1
View File
@@ -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[];
+74 -2
View File
@@ -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>
);
}
+3
View File
@@ -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',
+3
View File
@@ -324,6 +324,9 @@ const zhHans = {
deleteConfirmation: '你确定要删除这个机器人吗?',
platformAdapter: '平台/适配器选择',
selectAdapter: '选择适配器',
legacyAdapters: '旧版适配器',
legacyAdaptersHint:
'这些适配器已被新版(EBA 架构)取代,仅为兼容存量配置保留,不建议新建机器人时使用。',
adapterConfig: '适配器配置',
viewAdapterDocs: '查看文档',
bindPipeline: '绑定流水线',