2026-03-31 04:31:47 +00:00
|
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
|
|
|
|
|
|
import { APP_ENDPOINTS } from '../../../config/env';
|
2026-04-13 13:03:07 +00:00
|
|
|
|
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
|
|
|
|
|
|
import type { BotState } from '../../../types/bot';
|
2026-03-31 04:31:47 +00:00
|
|
|
|
import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders';
|
2026-04-13 13:03:07 +00:00
|
|
|
|
import type { DashboardLabels } from '../localeTypes';
|
2026-03-31 04:31:47 +00:00
|
|
|
|
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 {
|
2026-04-13 13:03:07 +00:00
|
|
|
|
ensureSelectedBotDetail: () => Promise<BotState | undefined>;
|
2026-03-31 04:31:47 +00:00
|
|
|
|
isZh: boolean;
|
|
|
|
|
|
notify: (message: string, options?: NotifyOptions) => void;
|
|
|
|
|
|
refresh: () => Promise<void>;
|
|
|
|
|
|
selectedBotId: string;
|
2026-04-13 13:03:07 +00:00
|
|
|
|
selectedBot?: BotState;
|
2026-03-31 04:31:47 +00:00
|
|
|
|
setRuntimeMenuOpen: (open: boolean) => void;
|
|
|
|
|
|
availableImages: NanobotImage[];
|
2026-04-13 13:03:07 +00:00
|
|
|
|
t: DashboardLabels;
|
2026-03-31 04:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-04-13 13:03:07 +00:00
|
|
|
|
const applyEditFormFromBot = useCallback((bot?: BotState) => {
|
2026-03-31 04:31:47 +00:00
|
|
|
|
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: '',
|
2026-04-14 02:04:12 +00:00
|
|
|
|
api_base: bot.api_base || '',
|
2026-03-31 04:31:47 +00:00
|
|
|
|
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 || '',
|
2026-04-14 02:04:12 +00:00
|
|
|
|
soul_md: bot.soul_md || '',
|
2026-03-31 04:31:47 +00:00
|
|
|
|
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(),
|
2026-04-14 02:04:12 +00:00
|
|
|
|
api_base: editForm.api_base.trim(),
|
2026-03-31 04:31:47 +00:00
|
|
|
|
});
|
|
|
|
|
|
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'));
|
|
|
|
|
|
}
|
2026-04-13 13:03:07 +00:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
const msg = resolveApiErrorMessage(error, 'request failed');
|
2026-03-31 04:31:47 +00:00
|
|
|
|
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') {
|
2026-04-14 02:04:12 +00:00
|
|
|
|
const normalizedSystemTimezone = editForm.system_timezone.trim();
|
|
|
|
|
|
if (!normalizedSystemTimezone) {
|
|
|
|
|
|
notify(isZh ? '系统时区不能为空。' : 'System timezone is required.', { tone: 'warning' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-31 04:31:47 +00:00
|
|
|
|
payload.name = editForm.name;
|
|
|
|
|
|
payload.access_password = editForm.access_password;
|
|
|
|
|
|
payload.image_tag = editForm.image_tag;
|
2026-04-14 02:04:12 +00:00
|
|
|
|
payload.system_timezone = normalizedSystemTimezone;
|
2026-03-31 04:31:47 +00:00
|
|
|
|
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;
|
2026-04-14 02:04:12 +00:00
|
|
|
|
payload.api_base = editForm.api_base.trim();
|
2026-03-31 04:31:47 +00:00
|
|
|
|
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' });
|
2026-04-13 13:03:07 +00:00
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
|
const msg = resolveApiErrorMessage(error, t.saveFail);
|
2026-03-31 04:31:47 +00:00
|
|
|
|
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,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|