feat(platforms): auto-discover channels for agent cards

This commit is contained in:
xmanrui
2026-03-07 02:54:40 +08:00
parent 17e1490c83
commit e69afd0340
3 changed files with 95 additions and 88 deletions
+41 -44
View File
@@ -207,7 +207,7 @@ function getFeishuUserOpenIds(agentIds: string[], sessionsMap: Map<string, any>)
function getChannelDirectPeerIds(
agentIds: string[],
sessionsMap: Map<string, any>,
channel: "telegram" | "whatsapp"
channel: string
): Record<string, string> {
const map: Record<string, string> = {};
const pattern = new RegExp(`^agent:[^:]+:${channel}:direct:(.+)$`);
@@ -328,16 +328,21 @@ export async function GET() {
// 从预读的 sessions 数据获取飞书用户 open_id
const feishuUserOpenIds = getFeishuUserOpenIds(agentIds, sessionsMap);
const telegramDirectPeerIds = getChannelDirectPeerIds(agentIds, sessionsMap, "telegram");
const whatsappDirectPeerIds = getChannelDirectPeerIds(agentIds, sessionsMap, "whatsapp");
const discordDmAllowFrom = channels.discord?.dm?.allowFrom || [];
// Build a set of agent IDs that have explicit feishu bindings
const boundFeishuAgentIds = new Set(
const enabledChannelNames: string[] = Object.entries(channels)
.filter(([, cfg]) => cfg && typeof cfg === "object" && (cfg as any).enabled !== false)
.map(([channelName]) => channelName);
const boundChannelNames: string[] = Array.from(new Set(
bindings
.filter((b: any) => b.match?.channel === "feishu")
.map((b: any) => b.agentId)
);
.map((b: any) => b.match?.channel)
.filter((v: any): v is string => typeof v === "string" && v.length > 0)
));
const discoverChannelNames: string[] = Array.from(new Set([...enabledChannelNames, ...boundChannelNames]));
const directPeerIdsByChannel: Record<string, Record<string, string>> = {};
for (const channelName of discoverChannelNames) {
if (channelName === "feishu") continue;
directPeerIdsByChannel[channelName] = getChannelDirectPeerIds(agentIds, sessionsMap, channelName);
}
const discordDmAllowFrom = channels.discord?.dm?.allowFrom || [];
// 构建 agent 详情
const agents = await Promise.all(agentList.map(async (agent: any) => {
@@ -349,6 +354,11 @@ export async function GET() {
// 查找绑定的平台
const platforms: { name: string; accountId?: string; appId?: string; botOpenId?: string; botUserId?: string }[] = [];
const addPlatform = (platform: { name: string; accountId?: string; appId?: string; botOpenId?: string; botUserId?: string }) => {
if (!platform?.name) return;
const exists = platforms.some((p) => p.name === platform.name && (p.accountId || "") === (platform.accountId || ""));
if (!exists) platforms.push(platform);
};
// 检查飞书绑定 (explicit binding)
const feishuBinding = bindings.find(
@@ -359,7 +369,7 @@ export async function GET() {
const acc = feishuAccounts[accountId];
const appId = acc?.appId;
const userOpenId = feishuUserOpenIds[id] || null;
platforms.push({ name: "feishu", accountId, appId, ...(userOpenId && { botOpenId: userOpenId }) });
addPlatform({ name: "feishu", accountId, appId, ...(userOpenId && { botOpenId: userOpenId }) });
}
// If no explicit binding, check if there's a feishu account matching this agent id
@@ -367,7 +377,7 @@ export async function GET() {
const acc = feishuAccounts[id];
const appId = acc?.appId;
const userOpenId = feishuUserOpenIds[id] || null;
platforms.push({ name: "feishu", accountId: id, appId, ...(userOpenId && { botOpenId: userOpenId }) });
addPlatform({ name: "feishu", accountId: id, appId, ...(userOpenId && { botOpenId: userOpenId }) });
}
// main agent 特殊处理:默认绑定所有未显式绑定的 channel
@@ -378,43 +388,30 @@ export async function GET() {
const acc = feishuAccounts["main"];
const appId = acc?.appId || channels.feishu?.appId;
const userOpenId = feishuUserOpenIds["main"] || null;
platforms.push({ name: "feishu", accountId: "main", appId, ...(userOpenId && { botOpenId: userOpenId }) });
addPlatform({ name: "feishu", accountId: "main", appId, ...(userOpenId && { botOpenId: userOpenId }) });
}
if (channels.discord && channels.discord.enabled !== false) {
const botUserId = discordDmAllowFrom[0] || null;
platforms.push({ name: "discord", ...(botUserId && { botUserId }) });
}
if (channels.telegram && channels.telegram.enabled !== false) {
const botUserId = telegramDirectPeerIds[id] || null;
platforms.push({ name: "telegram", ...(botUserId && { botUserId }) });
}
if (channels.whatsapp && channels.whatsapp.enabled !== false) {
const botUserId = whatsappDirectPeerIds[id] || null;
platforms.push({ name: "whatsapp", ...(botUserId && { botUserId }) });
// main agent 默认展示所有已启用 channel(feishu 已单独处理)
for (const channelName of enabledChannelNames) {
if (channelName === "feishu") continue;
const botUserId = directPeerIdsByChannel[channelName]?.[id]
|| (channelName === "discord" ? (discordDmAllowFrom[0] || null) : null);
addPlatform({ name: channelName, ...(botUserId && { botUserId }) });
}
}
// Also detect discord for non-main agents if they have discord bindings
// 非 main agent:按显式 bindings 展示 channel(自动支持新增 channel
if (id !== "main") {
const discordBinding = bindings.find(
(b: any) => b.agentId === id && b.match?.channel === "discord"
);
if (discordBinding) {
platforms.push({ name: "discord" });
}
const telegramBinding = bindings.find(
(b: any) => b.agentId === id && b.match?.channel === "telegram"
);
if (telegramBinding) {
const botUserId = telegramDirectPeerIds[id] || null;
platforms.push({ name: "telegram", ...(botUserId && { botUserId }) });
}
const whatsappBinding = bindings.find(
(b: any) => b.agentId === id && b.match?.channel === "whatsapp"
);
if (whatsappBinding) {
const botUserId = whatsappDirectPeerIds[id] || null;
platforms.push({ name: "whatsapp", ...(botUserId && { botUserId }) });
const seenBindingChannels = new Set<string>();
for (const binding of bindings) {
if (binding?.agentId !== id) continue;
const channelName = binding?.match?.channel;
if (!channelName || channelName === "feishu") continue;
if (seenBindingChannels.has(channelName)) continue;
seenBindingChannels.add(channelName);
const botUserId = directPeerIdsByChannel[channelName]?.[id] || null;
const accountId = typeof binding?.match?.accountId === "string" ? binding.match.accountId : undefined;
addPlatform({ name: channelName, ...(accountId && { accountId }), ...(botUserId && { botUserId }) });
}
}
+52 -44
View File
@@ -154,50 +154,52 @@ function PlatformBadge({
}) {
const pName = platform.name;
const badgeWidthClass = "w-[8.25rem]";
const remoteLogoSrc = pName === "feishu"
? "https://cdn.simpleicons.org/lark/2E5BFF"
: pName === "telegram"
? "https://cdn.simpleicons.org/telegram/26A5E4"
: pName === "whatsapp"
? "https://cdn.simpleicons.org/whatsapp/25D366"
: "https://cdn.simpleicons.org/discord/5865F2";
const logoFallbackSrc = pName === "feishu"
? "/assets/platform-logos/feishu-favicon.png?v=1"
: pName === "telegram"
? "/assets/platform-logos/telegram.svg"
: pName === "whatsapp"
? "/assets/platform-logos/whatsapp.svg"
: "/assets/platform-logos/discord.svg";
const logoSizeClass = pName === "feishu" ? "w-[1.09375rem] h-[1.09375rem]" : "w-3.5 h-3.5";
const knownMeta: Record<string, { remoteLogoSrc: string; logoFallbackSrc: string; badgeStyle: string; logoSizeClass?: string }> = {
feishu: {
remoteLogoSrc: "https://cdn.simpleicons.org/lark/2E5BFF",
logoFallbackSrc: "/assets/platform-logos/feishu-favicon.png?v=1",
badgeStyle: "bg-blue-500/20 text-blue-300 border border-blue-500/30 hover:bg-blue-500/40 hover:border-blue-400",
logoSizeClass: "w-[1.09375rem] h-[1.09375rem]",
},
discord: {
remoteLogoSrc: "https://cdn.simpleicons.org/discord/5865F2",
logoFallbackSrc: "/assets/platform-logos/discord.svg",
badgeStyle: "bg-purple-500/20 text-purple-300 border border-purple-500/30 hover:bg-purple-500/40 hover:border-purple-400",
},
telegram: {
remoteLogoSrc: "https://cdn.simpleicons.org/telegram/26A5E4",
logoFallbackSrc: "/assets/platform-logos/telegram.svg",
badgeStyle: "bg-sky-500/20 text-sky-300 border border-sky-500/30 hover:bg-sky-500/40 hover:border-sky-400",
},
whatsapp: {
remoteLogoSrc: "https://cdn.simpleicons.org/whatsapp/25D366",
logoFallbackSrc: "/assets/platform-logos/whatsapp.svg",
badgeStyle: "bg-green-500/20 text-green-300 border border-green-500/30 hover:bg-green-500/40 hover:border-green-400",
},
qqbot: {
remoteLogoSrc: "https://cdn.simpleicons.org/tencentqq/12B7F5",
logoFallbackSrc: "/assets/platform-logos/telegram.svg",
badgeStyle: "bg-blue-500/20 text-blue-300 border border-blue-500/30 hover:bg-blue-500/40 hover:border-blue-400",
},
};
const meta = knownMeta[pName];
const logoSizeClass = meta?.logoSizeClass || "w-3.5 h-3.5";
let sessionKey: string;
if (pName === "feishu" && platform.botOpenId) {
sessionKey = `agent:${agentId}:feishu:direct:${platform.botOpenId}`;
} else if (pName === "discord" && platform.botUserId) {
sessionKey = `agent:${agentId}:discord:direct:${platform.botUserId}`;
} else if (pName === "telegram" && platform.botUserId) {
sessionKey = `agent:${agentId}:telegram:direct:${platform.botUserId}`;
} else if (pName === "whatsapp" && platform.botUserId) {
sessionKey = `agent:${agentId}:whatsapp:direct:${platform.botUserId}`;
} else if (platform.botUserId) {
sessionKey = `agent:${agentId}:${pName}:direct:${platform.botUserId}`;
} else {
sessionKey = `agent:${agentId}:main`;
}
let sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey }, gatewayHost);
if (gatewayToken) sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey, token: gatewayToken }, gatewayHost);
const badgeStyle = pName === "feishu"
? "bg-blue-500/20 text-blue-300 border border-blue-500/30 hover:bg-blue-500/40 hover:border-blue-400"
: pName === "telegram"
? "bg-sky-500/20 text-sky-300 border border-sky-500/30 hover:bg-sky-500/40 hover:border-sky-400"
: pName === "whatsapp"
? "bg-green-500/20 text-green-300 border border-green-500/30 hover:bg-green-500/40 hover:border-green-400"
: "bg-purple-500/20 text-purple-300 border border-purple-500/30 hover:bg-purple-500/40 hover:border-purple-400";
const labelRaw = pName === "feishu" ? t("platform.feishu")
: pName === "telegram" ? t("platform.telegram")
: pName === "whatsapp" ? t("platform.whatsapp")
: t("platform.discord");
const label = labelRaw.replace(/^[^\p{L}\p{N}]+/u, "").trim();
const badgeStyle = meta?.badgeStyle || "bg-gray-500/20 text-gray-300 border border-gray-500/30 hover:bg-gray-500/40 hover:border-gray-400";
const translated = t(`platform.${pName}`);
const labelRaw = translated !== `platform.${pName}` ? translated : pName;
const label = labelRaw.replace(/^[^\p{L}\p{N}]+/u, "").trim() || pName;
return (
<div className="inline-flex items-center gap-1.5 max-w-full">
@@ -209,16 +211,22 @@ function PlatformBadge({
title={t("agent.openChat")}
className={`inline-flex items-center gap-0.5 px-2 py-0.5 rounded-full text-xs font-medium cursor-pointer transition-all hover:scale-105 hover:shadow-md min-w-0 ${badgeWidthClass} ${badgeStyle}`}
>
<img
src={remoteLogoSrc}
alt={`${label} logo`}
className={`${logoSizeClass} shrink-0`}
onError={(e) => {
if (e.currentTarget.dataset.fallbackApplied === "1") return;
e.currentTarget.dataset.fallbackApplied = "1";
e.currentTarget.src = logoFallbackSrc;
}}
/>
{meta ? (
<img
src={meta.remoteLogoSrc}
alt={`${label} logo`}
className={`${logoSizeClass} shrink-0`}
onError={(e) => {
if (e.currentTarget.dataset.fallbackApplied === "1") return;
e.currentTarget.dataset.fallbackApplied = "1";
e.currentTarget.src = meta.logoFallbackSrc;
}}
/>
) : (
<span className="inline-flex w-3.5 h-3.5 items-center justify-center rounded bg-white/10 text-[9px] font-semibold shrink-0">
{pName.slice(0, 2).toUpperCase()}
</span>
)}
<span className="shrink-0">{label}</span>
{pName === "feishu" && platform.accountId && (
<span className="opacity-60 truncate max-w-[4.5rem]">({platform.accountId})</span>
+2
View File
@@ -117,6 +117,7 @@ const translations: Record<Locale, Record<string, string>> = {
"platform.discord": "🎮 Discord",
"platform.telegram": "✈️ Telegram",
"platform.whatsapp": "💬 WhatsApp",
"platform.qqbot": "🐧 QQBot",
// time range
"range.daily": "按天",
@@ -373,6 +374,7 @@ const translations: Record<Locale, Record<string, string>> = {
"platform.discord": "🎮 Discord",
"platform.telegram": "✈️ Telegram",
"platform.whatsapp": "💬 WhatsApp",
"platform.qqbot": "🐧 QQBot",
// time range
"range.daily": "Daily",