Support local IM channel connections

This commit is contained in:
taohe
2026-06-10 21:59:33 +08:00
parent 9effa7be6d
commit 92c185b90d
16 changed files with 381 additions and 53 deletions
@@ -1,6 +1,7 @@
"use client";
import { CheckIcon, LoaderCircleIcon } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
@@ -28,12 +29,24 @@ import { ChannelProviderIcon } from "./channel-provider-icon";
function providerCanConnect(provider: ChannelProvider): boolean {
return (
provider.enabled &&
provider.configured &&
(provider.connectable ?? (provider.enabled && provider.configured)) &&
provider.connection_status !== "connected"
);
}
function getProviderDisabledReason(
provider: ChannelProvider,
t: ReturnType<typeof useI18n>["t"],
): string | undefined {
if (!provider.enabled) {
return t.channels.disabled;
}
if (!provider.configured) {
return t.channels.unconfigured;
}
return provider.unavailable_reason ?? undefined;
}
export function WorkspaceChannelsList() {
const { open: isSidebarOpen } = useSidebar();
const { t } = useI18n();
@@ -91,9 +104,7 @@ export function WorkspaceChannelsList() {
isConnected && "gap-1",
)}
disabled={!canConnect || isPending}
title={
!provider.configured ? t.channels.unconfigured : undefined
}
title={getProviderDisabledReason(provider, t)}
onClick={() => {
const connectWindow = prepareConnectWindow();
void connectMutation
@@ -101,7 +112,14 @@ export function WorkspaceChannelsList() {
.then((result) =>
openConnectUrl(result.url, connectWindow),
)
.catch(() => closeConnectWindow(connectWindow));
.catch((error) => {
closeConnectWindow(connectWindow);
toast.error(
error instanceof Error
? error.message
: t.channels.unavailable,
);
});
}}
>
{isPending ? (
@@ -7,6 +7,7 @@ import {
PlugIcon,
UnplugIcon,
} from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -64,6 +65,9 @@ function getStatusLabel(
if (!provider.configured) {
return t.channels.unconfigured;
}
if (provider.unavailable_reason) {
return t.channels.unavailableShort;
}
const status = connection?.status ?? provider.connection_status;
if (status === "connected") {
return t.channels.connected;
@@ -77,6 +81,19 @@ function getStatusLabel(
return t.channels.notConnected;
}
function getProviderDisabledReason(
provider: ChannelProvider,
t: ReturnType<typeof useI18n>["t"],
): string | undefined {
if (!provider.enabled) {
return t.channels.disabled;
}
if (!provider.configured) {
return t.channels.unconfigured;
}
return provider.unavailable_reason ?? undefined;
}
function ChannelProviderItem({
provider,
connection,
@@ -88,7 +105,9 @@ function ChannelProviderItem({
const connectMutation = useConnectChannelProvider();
const disconnectMutation = useDisconnectChannelConnection();
const isConnected = connection?.status === "connected";
const canConnect = provider.enabled && provider.configured && !isConnected;
const canConnect =
(provider.connectable ?? (provider.enabled && provider.configured)) &&
!isConnected;
const isConnecting =
connectMutation.isPending &&
connectMutation.variables === provider.provider;
@@ -97,6 +116,7 @@ function ChannelProviderItem({
disconnectMutation.variables === connection?.id;
const connectionLabel = connection ? getConnectionLabel(connection) : null;
const statusLabel = getStatusLabel(provider, connection, t);
const unavailableReason = getProviderDisabledReason(provider, t);
return (
<Item variant="outline" className="w-full items-start">
@@ -117,6 +137,9 @@ function ChannelProviderItem({
<ItemDescription className="line-clamp-none">
{getProviderDescription(provider, t.channels.descriptions)}
{connectionLabel ? ` ${t.channels.connectedAs(connectionLabel)}` : ""}
{!isConnected && provider.unavailable_reason
? ` ${provider.unavailable_reason}`
: ""}
</ItemDescription>
</ItemContent>
<ItemActions className="ml-auto">
@@ -140,13 +163,20 @@ function ChannelProviderItem({
type="button"
size="sm"
disabled={!canConnect || isConnecting}
title={!provider.configured ? t.channels.unconfigured : undefined}
title={unavailableReason}
onClick={() => {
const connectWindow = prepareConnectWindow();
void connectMutation
.mutateAsync(provider.provider)
.then((result) => openConnectUrl(result.url, connectWindow))
.catch(() => closeConnectWindow(connectWindow));
.catch((error) => {
closeConnectWindow(connectWindow);
toast.error(
error instanceof Error
? error.message
: t.channels.unavailable,
);
});
}}
>
{isConnecting ? (
+2
View File
@@ -5,6 +5,8 @@ export interface ChannelProvider {
display_name: string;
enabled: boolean;
configured: boolean;
connectable?: boolean;
unavailable_reason?: string | null;
auth_mode: string;
connection_status: string;
}
+1
View File
@@ -268,6 +268,7 @@ export const enUS: Translations = {
disabled: "Disabled",
unconfigured: "Not configured",
unavailable: "Channel connections are unavailable right now.",
unavailableShort: "Unavailable",
descriptions: {
telegram: "Telegram direct messages through your DeerFlow bot.",
slack: "Slack workspace messages and mentions.",
+1
View File
@@ -199,6 +199,7 @@ export interface Translations {
disabled: string;
unconfigured: string;
unavailable: string;
unavailableShort: string;
descriptions: Record<string, string>;
connectedAs: (name: string) => string;
};
+1
View File
@@ -256,6 +256,7 @@ export const zhCN: Translations = {
disabled: "已停用",
unconfigured: "未配置",
unavailable: "当前无法使用渠道连接。",
unavailableShort: "不可用",
descriptions: {
telegram: "通过 DeerFlow Bot 接收 Telegram 私聊消息。",
slack: "接收 Slack 工作区消息和提及。",