dashboard-nanobot/frontend/src/modules/dashboard/config-managers/channelManager.ts

262 lines
8.8 KiB
TypeScript

import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env';
import type { BotChannel, ChannelType } from '../types';
type PromptTone = 'info' | 'success' | 'warning' | 'error';
interface NotifyOptions {
title?: string;
tone?: PromptTone;
durationMs?: number;
}
interface ConfirmOptions {
title?: string;
message: string;
tone?: PromptTone;
confirmText?: string;
cancelText?: string;
}
interface PromptApi {
notify: (message: string, options?: NotifyOptions) => void;
confirm: (options: ConfirmOptions) => Promise<boolean>;
}
interface ChannelManagerDeps extends PromptApi {
selectedBotId: string;
selectedBotDockerStatus: string;
t: any;
currentGlobalDelivery: { sendProgress: boolean; sendToolHints: boolean };
addableChannelTypes: ChannelType[];
currentNewChannelDraft: BotChannel;
refresh: () => Promise<void>;
setShowChannelModal: (value: boolean) => void;
setChannels: (value: BotChannel[] | ((prev: BotChannel[]) => BotChannel[])) => void;
setExpandedChannelByKey: (
value: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)
) => void;
setChannelCreateMenuOpen: (value: boolean | ((prev: boolean) => boolean)) => void;
setNewChannelPanelOpen: (value: boolean) => void;
setNewChannelDraft: (value: BotChannel | ((prev: BotChannel) => BotChannel)) => void;
setIsSavingChannel: (value: boolean) => void;
setGlobalDelivery: (
value:
| { sendProgress: boolean; sendToolHints: boolean }
| ((prev: { sendProgress: boolean; sendToolHints: boolean }) => { sendProgress: boolean; sendToolHints: boolean })
) => void;
setIsSavingGlobalDelivery: (value: boolean) => void;
}
export function createChannelManager({
selectedBotId,
selectedBotDockerStatus,
t,
currentGlobalDelivery,
addableChannelTypes,
currentNewChannelDraft,
refresh,
notify,
confirm,
setShowChannelModal,
setChannels,
setExpandedChannelByKey,
setChannelCreateMenuOpen,
setNewChannelPanelOpen,
setNewChannelDraft,
setIsSavingChannel,
setGlobalDelivery,
setIsSavingGlobalDelivery,
}: ChannelManagerDeps) {
const createEmptyChannelExtra = (channelType: ChannelType): Record<string, unknown> =>
channelType === 'weixin' ? {} : {};
const createEmptyChannelDraft = (channelType: ChannelType = 'feishu'): BotChannel => ({
id: 'draft-channel',
bot_id: selectedBotId || '',
channel_type: channelType,
external_app_id: '',
app_secret: '',
internal_port: 8080,
is_active: true,
extra_config: createEmptyChannelExtra(channelType),
});
const channelDraftUiKey = (channel: Pick<BotChannel, 'id' | 'channel_type'>, fallbackIndex: number) => {
const id = String(channel.id || '').trim();
if (id) return id;
const type = String(channel.channel_type || '').trim().toLowerCase();
return type || `channel-${fallbackIndex}`;
};
const resetNewChannelDraft = (channelType: ChannelType = 'feishu') => {
setNewChannelDraft(createEmptyChannelDraft(channelType));
};
const isDashboardChannel = (channel: BotChannel) => String(channel.channel_type).toLowerCase() === 'dashboard';
const sanitizeChannelExtra = (channelType: string, extra: Record<string, unknown>) => {
const type = String(channelType || '').toLowerCase();
if (type === 'dashboard') return extra || {};
if (type === 'weixin') return {};
const next = { ...(extra || {}) };
delete next.sendProgress;
delete next.sendToolHints;
return next;
};
const loadChannels = async (botId: string) => {
if (!botId) return;
const res = await axios.get<BotChannel[]>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/channels`);
const rows = Array.isArray(res.data) ? res.data : [];
setChannels(rows);
setExpandedChannelByKey((prev) => {
const next: Record<string, boolean> = {};
rows
.filter((channel) => !isDashboardChannel(channel))
.forEach((channel, index) => {
const key = channelDraftUiKey(channel, index);
next[key] = typeof prev[key] === 'boolean' ? prev[key] : index === 0;
});
return next;
});
};
const openChannelModal = (botId: string) => {
if (!botId) return;
setExpandedChannelByKey({});
setChannelCreateMenuOpen(false);
setNewChannelPanelOpen(false);
resetNewChannelDraft();
void loadChannels(botId);
setShowChannelModal(true);
};
const beginChannelCreate = (channelType: ChannelType) => {
setExpandedChannelByKey({});
setChannelCreateMenuOpen(false);
setNewChannelPanelOpen(true);
resetNewChannelDraft(channelType);
};
const updateChannelLocal = (index: number, patch: Partial<BotChannel>) => {
setChannels((prev) =>
prev.map((channel, channelIndex) => {
if (channelIndex !== index || channel.locked) return channel;
return { ...channel, ...patch };
}),
);
};
const saveChannel = async (channel: BotChannel) => {
if (!selectedBotId || channel.locked || isDashboardChannel(channel)) return;
setIsSavingChannel(true);
try {
await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/channels/${channel.id}`, {
channel_type: channel.channel_type,
external_app_id: channel.external_app_id,
app_secret: channel.app_secret,
internal_port: Number(channel.internal_port),
is_active: channel.is_active,
extra_config: sanitizeChannelExtra(String(channel.channel_type), channel.extra_config || {}),
});
await loadChannels(selectedBotId);
notify(t.channelSaved, { tone: 'success' });
} catch (error: any) {
const message = error?.response?.data?.detail || t.channelSaveFail;
notify(message, { tone: 'error' });
} finally {
setIsSavingChannel(false);
}
};
const addChannel = async () => {
if (!selectedBotId) return;
const channelType = String(currentNewChannelDraft.channel_type || '').trim().toLowerCase() as ChannelType;
if (!channelType || !addableChannelTypes.includes(channelType)) return;
setIsSavingChannel(true);
try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/channels`, {
channel_type: channelType,
is_active: Boolean(currentNewChannelDraft.is_active),
external_app_id: String(currentNewChannelDraft.external_app_id || ''),
app_secret: String(currentNewChannelDraft.app_secret || ''),
internal_port: Number(currentNewChannelDraft.internal_port) || 8080,
extra_config: sanitizeChannelExtra(channelType, currentNewChannelDraft.extra_config || {}),
});
await loadChannels(selectedBotId);
setNewChannelPanelOpen(false);
resetNewChannelDraft();
} catch (error: any) {
const message = error?.response?.data?.detail || t.channelAddFail;
notify(message, { tone: 'error' });
} finally {
setIsSavingChannel(false);
}
};
const removeChannel = async (channel: BotChannel) => {
if (!selectedBotId || channel.locked || channel.channel_type === 'dashboard') return;
const ok = await confirm({
title: t.channels,
message: t.channelDeleteConfirm(channel.channel_type),
tone: 'warning',
});
if (!ok) return;
setIsSavingChannel(true);
try {
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/channels/${channel.id}`);
await loadChannels(selectedBotId);
notify(t.channelDeleted, { tone: 'success' });
} catch (error: any) {
const message = error?.response?.data?.detail || t.channelDeleteFail;
notify(message, { tone: 'error' });
} finally {
setIsSavingChannel(false);
}
};
const updateGlobalDeliveryFlag = (key: 'sendProgress' | 'sendToolHints', value: boolean) => {
setGlobalDelivery((prev) => ({ ...prev, [key]: value }));
};
const saveGlobalDelivery = async () => {
if (!selectedBotId) return;
setIsSavingGlobalDelivery(true);
try {
await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}`, {
send_progress: Boolean(currentGlobalDelivery.sendProgress),
send_tool_hints: Boolean(currentGlobalDelivery.sendToolHints),
});
if (selectedBotDockerStatus === 'RUNNING') {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/stop`);
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/start`);
}
await refresh();
notify(t.channelSaved, { tone: 'success' });
} catch (error: any) {
const message = error?.response?.data?.detail || t.channelSaveFail;
notify(message, { tone: 'error' });
} finally {
setIsSavingGlobalDelivery(false);
}
};
return {
createEmptyChannelDraft,
channelDraftUiKey,
resetNewChannelDraft,
isDashboardChannel,
loadChannels,
openChannelModal,
beginChannelCreate,
updateChannelLocal,
saveChannel,
addChannel,
removeChannel,
updateGlobalDeliveryFlag,
saveGlobalDelivery,
};
}