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; } interface ChannelManagerDeps extends PromptApi { selectedBotId: string; selectedBotDockerStatus: string; t: any; currentGlobalDelivery: { sendProgress: boolean; sendToolHints: boolean }; addableChannelTypes: ChannelType[]; currentNewChannelDraft: BotChannel; refresh: () => Promise; setShowChannelModal: (value: boolean) => void; setChannels: (value: BotChannel[] | ((prev: BotChannel[]) => BotChannel[])) => void; setExpandedChannelByKey: ( value: Record | ((prev: Record) => Record) ) => 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 => 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, 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) => { 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(`${APP_ENDPOINTS.apiBase}/bots/${botId}/channels`); const rows = Array.isArray(res.data) ? res.data : []; setChannels(rows); setExpandedChannelByKey((prev) => { const next: Record = {}; 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) => { 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, }; }