Add user-owned IM channel connections

This commit is contained in:
taohe
2026-06-10 21:07:44 +08:00
parent 0fb18e368c
commit dbe3a3bb0d
47 changed files with 4009 additions and 47 deletions
+78
View File
@@ -0,0 +1,78 @@
import { fetch } from "@/core/api/fetcher";
import { getBackendBaseURL } from "@/core/config";
import type {
ChannelConnectResponse,
ChannelConnection,
ChannelConnectionsResponse,
ChannelProviderId,
ChannelProvidersResponse,
} from "./types";
function channelsUrl(path: string): string {
return `${getBackendBaseURL()}/api/channels${path}`;
}
async function throwChannelApiError(
response: Response,
fallback: string,
): Promise<never> {
const body = (await response.json().catch(() => ({}))) as {
detail?: unknown;
};
throw new Error(typeof body.detail === "string" ? body.detail : fallback);
}
export async function listChannelProviders(): Promise<ChannelProvidersResponse> {
const response = await fetch(channelsUrl("/providers"));
if (!response.ok) {
await throwChannelApiError(
response,
`Failed to load channel providers: ${response.statusText}`,
);
}
return response.json() as Promise<ChannelProvidersResponse>;
}
export async function listChannelConnections(): Promise<ChannelConnection[]> {
const response = await fetch(channelsUrl("/connections"));
if (!response.ok) {
await throwChannelApiError(
response,
`Failed to load channel connections: ${response.statusText}`,
);
}
const data = (await response.json()) as ChannelConnectionsResponse;
return data.connections;
}
export async function connectChannelProvider(
provider: ChannelProviderId,
): Promise<ChannelConnectResponse> {
const response = await fetch(
channelsUrl(`/${encodeURIComponent(provider)}/connect`),
{ method: "POST" },
);
if (!response.ok) {
await throwChannelApiError(
response,
`Failed to connect ${provider}: ${response.statusText}`,
);
}
return response.json() as Promise<ChannelConnectResponse>;
}
export async function disconnectChannelConnection(
connectionId: string,
): Promise<void> {
const response = await fetch(
channelsUrl(`/connections/${encodeURIComponent(connectionId)}`),
{ method: "DELETE" },
);
if (!response.ok) {
await throwChannelApiError(
response,
`Failed to disconnect channel: ${response.statusText}`,
);
}
}
+61
View File
@@ -0,0 +1,61 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
connectChannelProvider,
disconnectChannelConnection,
listChannelConnections,
listChannelProviders,
} from "./api";
import type { ChannelProviderId } from "./types";
export const channelProviderQueryKey = ["channelProviders"] as const;
export const channelConnectionsQueryKey = ["channelConnections"] as const;
export function useChannelProviders() {
const { data, isLoading, error } = useQuery({
queryKey: channelProviderQueryKey,
queryFn: () => listChannelProviders(),
});
return {
enabled: data?.enabled ?? false,
providers: data?.providers ?? [],
isLoading,
error,
};
}
export function useChannelConnections() {
const { data, isLoading, error } = useQuery({
queryKey: channelConnectionsQueryKey,
queryFn: () => listChannelConnections(),
});
return { connections: data ?? [], isLoading, error };
}
export function useConnectChannelProvider() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (provider: ChannelProviderId) =>
connectChannelProvider(provider),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: channelProviderQueryKey });
void queryClient.invalidateQueries({
queryKey: channelConnectionsQueryKey,
});
},
});
}
export function useDisconnectChannelConnection() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (connectionId: string) =>
disconnectChannelConnection(connectionId),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: channelProviderQueryKey });
void queryClient.invalidateQueries({
queryKey: channelConnectionsQueryKey,
});
},
});
}
+38
View File
@@ -0,0 +1,38 @@
export type ChannelProviderId = "telegram" | "slack" | "discord" | string;
export interface ChannelProvider {
provider: ChannelProviderId;
display_name: string;
enabled: boolean;
configured: boolean;
auth_mode: string;
connection_status: string;
}
export interface ChannelProvidersResponse {
enabled: boolean;
providers: ChannelProvider[];
}
export interface ChannelConnection {
id: string;
provider: ChannelProviderId;
status: string;
external_account_id?: string | null;
external_account_name?: string | null;
workspace_id?: string | null;
workspace_name?: string | null;
scopes: string[];
metadata: Record<string, unknown>;
}
export interface ChannelConnectionsResponse {
connections: ChannelConnection[];
}
export interface ChannelConnectResponse {
provider: ChannelProviderId;
mode: string;
url: string;
expires_in: number;
}
+30
View File
@@ -170,6 +170,7 @@ export const enUS: Translations = {
sidebar: {
newChat: "New chat",
chats: "Chats",
channels: "Channels",
recentChats: "Recent chats",
demoChats: "Demo chats",
agents: "Agents",
@@ -254,6 +255,27 @@ export const enUS: Translations = {
searchChats: "Search chats",
},
// Channels
channels: {
title: "Channels",
connect: "Connect",
reconnect: "Reconnect",
disconnect: "Disconnect",
connected: "Connected",
notConnected: "Not connected",
pending: "Pending",
revoked: "Disconnected",
disabled: "Disabled",
unconfigured: "Not configured",
unavailable: "Channel connections are unavailable right now.",
descriptions: {
telegram: "Telegram direct messages through your DeerFlow bot.",
slack: "Slack workspace messages and mentions.",
discord: "Discord server messages through your DeerFlow bot.",
},
connectedAs: (name: string) => `Connected as ${name}.`,
},
// Page titles (document title)
pages: {
appName: "DeerFlow",
@@ -354,6 +376,7 @@ export const enUS: Translations = {
sections: {
account: "Account",
appearance: "Appearance",
channels: "Channels",
memory: "Memory",
tools: "Tools",
skills: "Skills",
@@ -456,6 +479,13 @@ export const enUS: Translations = {
title: "Tools",
description: "Manage the configuration and enabled status of MCP tools.",
},
channels: {
title: "Channels",
description:
"Connect IM accounts that can send messages to DeerFlow from outside the browser.",
disabled:
"Channel connections are not enabled on this server. Ask an administrator to enable channel_connections.",
},
skills: {
title: "Agent Skills",
description:
+24
View File
@@ -117,6 +117,7 @@ export interface Translations {
chats: string;
demoChats: string;
agents: string;
channels: string;
};
// Agents
@@ -185,6 +186,23 @@ export interface Translations {
searchChats: string;
};
// Channels
channels: {
title: string;
connect: string;
reconnect: string;
disconnect: string;
connected: string;
notConnected: string;
pending: string;
revoked: string;
disabled: string;
unconfigured: string;
unavailable: string;
descriptions: Record<string, string>;
connectedAs: (name: string) => string;
};
// Page titles (document title)
pages: {
appName: string;
@@ -281,6 +299,7 @@ export interface Translations {
sections: {
account: string;
appearance: string;
channels: string;
memory: string;
tools: string;
skills: string;
@@ -376,6 +395,11 @@ export interface Translations {
title: string;
description: string;
};
channels: {
title: string;
description: string;
disabled: string;
};
skills: {
title: string;
description: string;
+29
View File
@@ -164,6 +164,7 @@ export const zhCN: Translations = {
sidebar: {
newChat: "新对话",
chats: "对话",
channels: "渠道",
recentChats: "最近的对话",
demoChats: "演示对话",
agents: "智能体",
@@ -242,6 +243,27 @@ export const zhCN: Translations = {
searchChats: "搜索对话",
},
// Channels
channels: {
title: "渠道",
connect: "连接",
reconnect: "重新连接",
disconnect: "断开连接",
connected: "已连接",
notConnected: "未连接",
pending: "待完成",
revoked: "已断开",
disabled: "已停用",
unconfigured: "未配置",
unavailable: "当前无法使用渠道连接。",
descriptions: {
telegram: "通过 DeerFlow Bot 接收 Telegram 私聊消息。",
slack: "接收 Slack 工作区消息和提及。",
discord: "通过 DeerFlow Bot 接收 Discord 服务器消息。",
},
connectedAs: (name: string) => `已连接为 ${name}`,
},
// Page titles (document title)
pages: {
appName: "DeerFlow",
@@ -338,6 +360,7 @@ export const zhCN: Translations = {
sections: {
account: "账号",
appearance: "外观",
channels: "渠道",
memory: "记忆",
tools: "工具",
skills: "技能",
@@ -437,6 +460,12 @@ export const zhCN: Translations = {
title: "工具",
description: "管理 MCP 工具的配置和启用状态。",
},
channels: {
title: "渠道",
description: "连接可在浏览器外向 DeerFlow 发送消息的即时通讯账号。",
disabled:
"当前服务器未启用渠道连接。请联系管理员开启 channel_connections。",
},
skills: {
title: "技能",
description: "管理 Agent Skill 配置和启用状态。",