262 lines
8.8 KiB
TypeScript
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,
|
|
};
|
|
}
|