336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
||
import axios from 'axios';
|
||
|
||
import { APP_ENDPOINTS } from '../../../config/env';
|
||
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
|
||
import type { BotState } from '../../../types/bot';
|
||
import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders';
|
||
import type { DashboardLabels } from '../localeTypes';
|
||
import type { AgentTab, BotEditForm, BotParamDraft, NanobotImage } from '../types';
|
||
import { clampCpuCores, clampMaxTokens, clampMemoryMb, clampStorageGb, clampTemperature } from '../utils';
|
||
|
||
const DEFAULT_EDIT_FORM: BotEditForm = {
|
||
name: '',
|
||
access_password: '',
|
||
llm_provider: '',
|
||
llm_model: '',
|
||
image_tag: '',
|
||
api_key: '',
|
||
api_base: '',
|
||
temperature: 0.2,
|
||
top_p: 1,
|
||
max_tokens: 8192,
|
||
cpu_cores: 1,
|
||
memory_mb: 1024,
|
||
storage_gb: 10,
|
||
system_timezone: '',
|
||
agents_md: '',
|
||
soul_md: '',
|
||
user_md: '',
|
||
tools_md: '',
|
||
identity_md: '',
|
||
};
|
||
|
||
const DEFAULT_PARAM_DRAFT: BotParamDraft = {
|
||
max_tokens: '8192',
|
||
cpu_cores: '1',
|
||
memory_mb: '1024',
|
||
storage_gb: '10',
|
||
};
|
||
|
||
const agentFieldByTab: Record<AgentTab, keyof BotEditForm> = {
|
||
AGENTS: 'agents_md',
|
||
SOUL: 'soul_md',
|
||
USER: 'user_md',
|
||
TOOLS: 'tools_md',
|
||
IDENTITY: 'identity_md',
|
||
};
|
||
|
||
type PromptTone = 'info' | 'success' | 'warning' | 'error';
|
||
|
||
interface NotifyOptions {
|
||
title?: string;
|
||
tone?: PromptTone;
|
||
durationMs?: number;
|
||
}
|
||
|
||
interface UseDashboardBotEditorOptions {
|
||
ensureSelectedBotDetail: () => Promise<BotState | undefined>;
|
||
isZh: boolean;
|
||
notify: (message: string, options?: NotifyOptions) => void;
|
||
refresh: () => Promise<void>;
|
||
selectedBotId: string;
|
||
selectedBot?: BotState;
|
||
setRuntimeMenuOpen: (open: boolean) => void;
|
||
availableImages: NanobotImage[];
|
||
t: DashboardLabels;
|
||
}
|
||
|
||
export function useDashboardBotEditor({
|
||
ensureSelectedBotDetail,
|
||
isZh,
|
||
notify,
|
||
refresh,
|
||
selectedBotId,
|
||
selectedBot,
|
||
setRuntimeMenuOpen,
|
||
availableImages,
|
||
t,
|
||
}: UseDashboardBotEditorOptions) {
|
||
const [agentTab, setAgentTab] = useState<AgentTab>('AGENTS');
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
const [isTestingProvider, setIsTestingProvider] = useState(false);
|
||
const [providerTestResult, setProviderTestResult] = useState('');
|
||
const [editForm, setEditForm] = useState<BotEditForm>(DEFAULT_EDIT_FORM);
|
||
const [paramDraft, setParamDraft] = useState<BotParamDraft>(DEFAULT_PARAM_DRAFT);
|
||
const [showBaseModal, setShowBaseModal] = useState(false);
|
||
const [showParamModal, setShowParamModal] = useState(false);
|
||
const [showAgentModal, setShowAgentModal] = useState(false);
|
||
|
||
const applyEditFormFromBot = useCallback((bot?: BotState) => {
|
||
if (!bot) return;
|
||
const provider = String(bot.llm_provider || '').trim().toLowerCase();
|
||
setProviderTestResult('');
|
||
setEditForm({
|
||
name: bot.name || '',
|
||
access_password: bot.access_password || '',
|
||
llm_provider: provider,
|
||
llm_model: bot.llm_model || '',
|
||
image_tag: bot.image_tag || '',
|
||
api_key: '',
|
||
api_base: bot.api_base || '',
|
||
temperature: clampTemperature(bot.temperature ?? 0.2),
|
||
top_p: bot.top_p ?? 1,
|
||
max_tokens: clampMaxTokens(bot.max_tokens ?? 8192),
|
||
cpu_cores: clampCpuCores(bot.cpu_cores ?? 1),
|
||
memory_mb: clampMemoryMb(bot.memory_mb ?? 1024),
|
||
storage_gb: clampStorageGb(bot.storage_gb ?? 10),
|
||
system_timezone: bot.system_timezone || '',
|
||
agents_md: bot.agents_md || '',
|
||
soul_md: bot.soul_md || '',
|
||
user_md: bot.user_md || '',
|
||
tools_md: bot.tools_md || '',
|
||
identity_md: bot.identity_md || '',
|
||
});
|
||
setParamDraft({
|
||
max_tokens: String(clampMaxTokens(bot.max_tokens ?? 8192)),
|
||
cpu_cores: String(clampCpuCores(bot.cpu_cores ?? 1)),
|
||
memory_mb: String(clampMemoryMb(bot.memory_mb ?? 1024)),
|
||
storage_gb: String(clampStorageGb(bot.storage_gb ?? 10)),
|
||
});
|
||
}, []);
|
||
|
||
const updateEditForm = useCallback((patch: Partial<BotEditForm>) => {
|
||
setEditForm((prev) => ({ ...prev, ...patch }));
|
||
}, []);
|
||
|
||
const updateParamDraft = useCallback((patch: Partial<BotParamDraft>) => {
|
||
setParamDraft((prev) => ({ ...prev, ...patch }));
|
||
}, []);
|
||
|
||
const updateAgentTabValue = useCallback((tab: AgentTab, nextValue: string) => {
|
||
const field = agentFieldByTab[tab];
|
||
setEditForm((prev) => ({ ...prev, [field]: nextValue }));
|
||
}, []);
|
||
|
||
const onBaseProviderChange = useCallback((provider: string) => {
|
||
setEditForm((prev) => {
|
||
const nextProvider = String(provider || '').trim().toLowerCase();
|
||
const nextDefaultApiBase = getLlmProviderDefaultApiBase(nextProvider);
|
||
return {
|
||
...prev,
|
||
llm_provider: nextProvider,
|
||
api_base: nextDefaultApiBase,
|
||
};
|
||
});
|
||
setProviderTestResult('');
|
||
}, []);
|
||
|
||
const closeModals = useCallback(() => {
|
||
setShowBaseModal(false);
|
||
setShowParamModal(false);
|
||
setShowAgentModal(false);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!selectedBotId) return;
|
||
if (showBaseModal || showParamModal || showAgentModal) return;
|
||
applyEditFormFromBot(selectedBot);
|
||
}, [
|
||
applyEditFormFromBot,
|
||
selectedBot,
|
||
selectedBot?.id,
|
||
selectedBot?.updated_at,
|
||
selectedBotId,
|
||
showAgentModal,
|
||
showBaseModal,
|
||
showParamModal,
|
||
]);
|
||
|
||
const openBaseConfigModal = useCallback(async () => {
|
||
setRuntimeMenuOpen(false);
|
||
const detail = await ensureSelectedBotDetail();
|
||
applyEditFormFromBot(detail);
|
||
setShowBaseModal(true);
|
||
}, [applyEditFormFromBot, ensureSelectedBotDetail, setRuntimeMenuOpen]);
|
||
|
||
const openParamConfigModal = useCallback(async () => {
|
||
setRuntimeMenuOpen(false);
|
||
const detail = await ensureSelectedBotDetail();
|
||
applyEditFormFromBot(detail);
|
||
setShowParamModal(true);
|
||
}, [applyEditFormFromBot, ensureSelectedBotDetail, setRuntimeMenuOpen]);
|
||
|
||
const openAgentFilesModal = useCallback(async () => {
|
||
setRuntimeMenuOpen(false);
|
||
const detail = await ensureSelectedBotDetail();
|
||
applyEditFormFromBot(detail);
|
||
setShowAgentModal(true);
|
||
}, [applyEditFormFromBot, ensureSelectedBotDetail, setRuntimeMenuOpen]);
|
||
|
||
const testProviderConnection = useCallback(async () => {
|
||
if (!editForm.llm_provider || !editForm.llm_model || !editForm.api_key.trim()) {
|
||
notify(t.providerRequired, { tone: 'warning' });
|
||
return;
|
||
}
|
||
setIsTestingProvider(true);
|
||
setProviderTestResult('');
|
||
try {
|
||
const res = await axios.post(`${APP_ENDPOINTS.apiBase}/providers/test`, {
|
||
provider: editForm.llm_provider,
|
||
model: editForm.llm_model,
|
||
api_key: editForm.api_key.trim(),
|
||
api_base: editForm.api_base.trim(),
|
||
});
|
||
if (res.data?.ok) {
|
||
const preview = (res.data.models_preview || []).slice(0, 3).join(', ');
|
||
setProviderTestResult(t.connOk(preview));
|
||
} else {
|
||
setProviderTestResult(t.connFail(res.data?.detail || 'unknown error'));
|
||
}
|
||
} catch (error: unknown) {
|
||
const msg = resolveApiErrorMessage(error, 'request failed');
|
||
setProviderTestResult(t.connFail(msg));
|
||
} finally {
|
||
setIsTestingProvider(false);
|
||
}
|
||
}, [editForm.api_base, editForm.api_key, editForm.llm_model, editForm.llm_provider, notify, t]);
|
||
|
||
const saveBot = useCallback(async (mode: 'params' | 'agent' | 'base') => {
|
||
const targetBotId = String(selectedBot?.id || selectedBotId || '').trim();
|
||
if (!targetBotId) {
|
||
notify(isZh ? '未选中 Bot,无法保存。' : 'No bot selected.', { tone: 'warning' });
|
||
return;
|
||
}
|
||
setIsSaving(true);
|
||
try {
|
||
const payload: Record<string, string | number> = {};
|
||
if (mode === 'base') {
|
||
const normalizedSystemTimezone = editForm.system_timezone.trim();
|
||
if (!normalizedSystemTimezone) {
|
||
notify(isZh ? '系统时区不能为空。' : 'System timezone is required.', { tone: 'warning' });
|
||
return;
|
||
}
|
||
payload.name = editForm.name;
|
||
payload.access_password = editForm.access_password;
|
||
payload.image_tag = editForm.image_tag;
|
||
payload.system_timezone = normalizedSystemTimezone;
|
||
const normalizedImageTag = String(editForm.image_tag || '').trim();
|
||
const selectedImage = availableImages.find((row) => String(row.tag || '').trim() === normalizedImageTag);
|
||
const selectedImageStatus = String(selectedImage?.status || '').toUpperCase();
|
||
if (normalizedImageTag && (!selectedImage || selectedImageStatus !== 'READY')) {
|
||
throw new Error(isZh ? '当前镜像不可用,请选择可用镜像。' : 'Selected image is unavailable.');
|
||
}
|
||
const normalizedCpuCores = clampCpuCores(Number(paramDraft.cpu_cores));
|
||
const normalizedMemoryMb = clampMemoryMb(Number(paramDraft.memory_mb));
|
||
const normalizedStorageGb = clampStorageGb(Number(paramDraft.storage_gb));
|
||
payload.cpu_cores = normalizedCpuCores;
|
||
payload.memory_mb = normalizedMemoryMb;
|
||
payload.storage_gb = normalizedStorageGb;
|
||
setEditForm((prev) => ({
|
||
...prev,
|
||
cpu_cores: normalizedCpuCores,
|
||
memory_mb: normalizedMemoryMb,
|
||
storage_gb: normalizedStorageGb,
|
||
}));
|
||
setParamDraft((prev) => ({
|
||
...prev,
|
||
cpu_cores: String(normalizedCpuCores),
|
||
memory_mb: String(normalizedMemoryMb),
|
||
storage_gb: String(normalizedStorageGb),
|
||
}));
|
||
}
|
||
if (mode === 'params') {
|
||
payload.llm_provider = editForm.llm_provider;
|
||
payload.llm_model = editForm.llm_model;
|
||
payload.api_base = editForm.api_base.trim();
|
||
if (editForm.api_key.trim()) payload.api_key = editForm.api_key.trim();
|
||
payload.temperature = clampTemperature(Number(editForm.temperature));
|
||
payload.top_p = Number(editForm.top_p);
|
||
const normalizedMaxTokens = clampMaxTokens(Number(paramDraft.max_tokens));
|
||
payload.max_tokens = normalizedMaxTokens;
|
||
setEditForm((prev) => ({
|
||
...prev,
|
||
max_tokens: normalizedMaxTokens,
|
||
}));
|
||
setParamDraft((prev) => ({ ...prev, max_tokens: String(normalizedMaxTokens) }));
|
||
}
|
||
if (mode === 'agent') {
|
||
payload.agents_md = editForm.agents_md;
|
||
payload.soul_md = editForm.soul_md;
|
||
payload.user_md = editForm.user_md;
|
||
payload.tools_md = editForm.tools_md;
|
||
payload.identity_md = editForm.identity_md;
|
||
}
|
||
|
||
await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${targetBotId}`, payload);
|
||
await refresh();
|
||
closeModals();
|
||
notify(t.configUpdated, { tone: 'success' });
|
||
} catch (error: unknown) {
|
||
const msg = resolveApiErrorMessage(error, t.saveFail);
|
||
notify(msg, { tone: 'error' });
|
||
} finally {
|
||
setIsSaving(false);
|
||
}
|
||
}, [
|
||
availableImages,
|
||
closeModals,
|
||
editForm,
|
||
isZh,
|
||
notify,
|
||
paramDraft,
|
||
refresh,
|
||
selectedBot?.id,
|
||
selectedBotId,
|
||
t,
|
||
]);
|
||
|
||
return {
|
||
agentFieldByTab,
|
||
agentTab,
|
||
applyEditFormFromBot,
|
||
editForm,
|
||
isSaving,
|
||
isTestingProvider,
|
||
onBaseProviderChange,
|
||
openAgentFilesModal,
|
||
openBaseConfigModal,
|
||
openParamConfigModal,
|
||
paramDraft,
|
||
providerTestResult,
|
||
saveBot,
|
||
setAgentTab,
|
||
setShowAgentModal,
|
||
setShowBaseModal,
|
||
setShowParamModal,
|
||
showAgentModal,
|
||
showBaseModal,
|
||
showParamModal,
|
||
testProviderConnection,
|
||
updateAgentTabValue,
|
||
updateEditForm,
|
||
updateParamDraft,
|
||
};
|
||
}
|