import { useEffect, useMemo, useState } from 'react'; import axios from 'axios'; import { Eye, EyeOff, Plus, Settings2, Trash2 } from 'lucide-react'; import { APP_ENDPOINTS } from '../../config/env'; import { useAppStore } from '../../store/appStore'; import { channelsZhCn } from '../../i18n/channels.zh-cn'; import { channelsEn } from '../../i18n/channels.en'; import { pickLocale } from '../../i18n'; import { wizardZhCn } from '../../i18n/wizard.zh-cn'; import { wizardEn } from '../../i18n/wizard.en'; import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; import { LucentIconButton } from '../../components/lucent/LucentIconButton'; import { setBotAccessPassword } from '../../utils/botAccess'; type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY'; type ChannelType = 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack'; const FALLBACK_SOUL_MD = '# Soul\n\n你是专业的企业数字员工,表达清晰、可执行。'; const FALLBACK_AGENTS_MD = '# Agent Instructions\n\n- 优先完成任务目标\n- 操作前先说明意图\n- 输出必须可执行\n\n## 默认输出规范\n\n- 每次执行任务时,在 workspace 中创建新目录保存本次输出。\n- 输出内容默认采用 Markdown(.md)格式。'; const FALLBACK_USER_MD = '# User\n\n- 语言: 中文\n- 风格: 专业\n- 偏好: 简明且有步骤'; const FALLBACK_TOOLS_MD = '# Tools\n\n- 谨慎使用 shell\n- 修改文件后复核\n- 失败时说明原因并重试策略'; const FALLBACK_IDENTITY_MD = '# Identity\n\n- 角色: 企业数字员工\n- 领域: 运维与任务执行'; interface WizardChannelConfig { channel_type: ChannelType; is_active: boolean; external_app_id: string; app_secret: string; internal_port: number; extra_config: Record; } interface NanobotImage { tag: string; status: string; } interface SystemDefaultsResponse { templates?: { soul_md?: string; agents_md?: string; user_md?: string; tools_md?: string; identity_md?: string; }; } const providerPresets: Record = { openrouter: { model: 'openai/gpt-4o-mini', note: { 'zh-cn': 'OpenRouter 网关,模型名示例 openai/gpt-4o-mini。', en: 'OpenRouter gateway, model example: openai/gpt-4o-mini.', }, apiBase: 'https://openrouter.ai/api/v1', }, dashscope: { model: 'qwen-plus', note: { 'zh-cn': '阿里云 DashScope(千问),模型示例 qwen-plus / qwen-max。', en: 'Alibaba DashScope (Qwen), model example: qwen-plus / qwen-max.', }, apiBase: 'https://dashscope.aliyuncs.com/compatible-mode/v1', }, openai: { model: 'gpt-4o-mini', note: { 'zh-cn': 'OpenAI 原生模型。', en: 'OpenAI native models.', }, }, deepseek: { model: 'deepseek-chat', note: { 'zh-cn': 'DeepSeek 原生模型。', en: 'DeepSeek native models.', }, }, kimi: { model: 'moonshot-v1-8k', note: { 'zh-cn': 'Kimi(Moonshot)接口,模型示例 moonshot-v1-8k。', en: 'Kimi (Moonshot) endpoint, model example: moonshot-v1-8k.', }, apiBase: 'https://api.moonshot.cn/v1', }, minimax: { model: 'MiniMax-Text-01', note: { 'zh-cn': 'MiniMax 接口,模型示例 MiniMax-Text-01。', en: 'MiniMax endpoint, model example: MiniMax-Text-01.', }, apiBase: 'https://api.minimax.chat/v1', }, }; const initialForm = { id: '', name: '', access_password: '', llm_provider: 'dashscope', llm_model: providerPresets.dashscope.model, api_key: '', api_base: providerPresets.dashscope.apiBase ?? '', image_tag: '', temperature: 0.2, top_p: 1.0, max_tokens: 8192, cpu_cores: 1, memory_mb: 1024, storage_gb: 10, soul_md: FALLBACK_SOUL_MD, agents_md: FALLBACK_AGENTS_MD, user_md: FALLBACK_USER_MD, tools_md: FALLBACK_TOOLS_MD, identity_md: FALLBACK_IDENTITY_MD, env_params: {} as Record, send_progress: false, send_tool_hints: false, channels: [] as WizardChannelConfig[], }; const optionalChannelTypes: ChannelType[] = ['feishu', 'qq', 'dingtalk', 'telegram', 'slack']; interface BotWizardModuleProps { onCreated?: () => void; onGoDashboard?: () => void; } export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModuleProps) { const { locale } = useAppStore(); const { notify } = useLucentPrompt(); const [step, setStep] = useState(1); const [images, setImages] = useState([]); const [isLoadingImages, setIsLoadingImages] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [autoStart, setAutoStart] = useState(true); const [isTestingProvider, setIsTestingProvider] = useState(false); const [testResult, setTestResult] = useState(''); const [agentTab, setAgentTab] = useState('AGENTS'); const [showChannelModal, setShowChannelModal] = useState(false); const [showToolsConfigModal, setShowToolsConfigModal] = useState(false); const [envDraftKey, setEnvDraftKey] = useState(''); const [envDraftValue, setEnvDraftValue] = useState(''); const [envDraftVisible, setEnvDraftVisible] = useState(false); const [envVisibleByKey, setEnvVisibleByKey] = useState>({}); const [newChannelType, setNewChannelType] = useState('feishu'); const [form, setForm] = useState(initialForm); const [defaultAgentsTemplate, setDefaultAgentsTemplate] = useState(FALLBACK_AGENTS_MD); const [maxTokensDraft, setMaxTokensDraft] = useState(String(initialForm.max_tokens)); const [cpuCoresDraft, setCpuCoresDraft] = useState(String(initialForm.cpu_cores)); const [memoryMbDraft, setMemoryMbDraft] = useState(String(initialForm.memory_mb)); const [storageGbDraft, setStorageGbDraft] = useState(String(initialForm.storage_gb)); const readyImages = useMemo(() => images.filter((img) => img.status === 'READY'), [images]); const isZh = locale === 'zh'; const ui = pickLocale(locale, { 'zh-cn': wizardZhCn, en: wizardEn }); const lc = isZh ? channelsZhCn : channelsEn; const noteLocale = pickLocale(locale, { 'zh-cn': 'zh-cn' as const, en: 'en' as const }); const activeChannelTypes = useMemo(() => new Set(form.channels.map((c) => c.channel_type)), [form.channels]); const addableChannelTypes = useMemo( () => optionalChannelTypes.filter((t) => !activeChannelTypes.has(t)), [activeChannelTypes], ); const envEntries = useMemo( () => Object.entries(form.env_params || {}) .filter(([k]) => String(k || '').trim().length > 0) .sort(([a], [b]) => a.localeCompare(b)), [form.env_params], ); useEffect(() => { const loadSystemDefaults = async () => { try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/system/defaults`); const tpl = res.data?.templates || {}; const agentsTemplate = String(tpl.agents_md || '').trim() || FALLBACK_AGENTS_MD; setDefaultAgentsTemplate(agentsTemplate); setForm((prev) => { return { ...prev, soul_md: String(tpl.soul_md || '').trim() || prev.soul_md, agents_md: agentsTemplate, user_md: String(tpl.user_md || '').trim() || prev.user_md, tools_md: String(tpl.tools_md || '').trim() || prev.tools_md, identity_md: String(tpl.identity_md || '').trim() || prev.identity_md, }; }); } catch { // keep fallback templates } }; void loadSystemDefaults(); }, []); const configuredChannelsLabel = useMemo( () => (form.channels.length > 0 ? form.channels.map((c) => c.channel_type).join(', ') : '-'), [form.channels], ); const loadImages = async () => { setIsLoadingImages(true); try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/images`); setImages(res.data); const ready = res.data.filter((img) => img.status === 'READY'); if (!form.image_tag && ready.length > 0) { setForm((prev) => ({ ...prev, image_tag: ready[0].tag })); } return ready; } finally { setIsLoadingImages(false); } }; const next = async () => { if (step === 1) { const ready = await loadImages(); if (ready.length === 0) { notify(ui.noReadyImage, { tone: 'warning' }); return; } } if (step === 2) { commitMaxTokensDraft(maxTokensDraft); commitCpuCoresDraft(cpuCoresDraft); commitMemoryMbDraft(memoryMbDraft); commitStorageGbDraft(storageGbDraft); if (!form.id || !form.name || !form.api_key || !form.image_tag || !form.llm_model) { notify(ui.requiredBase, { tone: 'warning' }); return; } } if (step < 4) { setStep((s) => s + 1); } }; const testProvider = async () => { if (!form.llm_provider || !form.api_key || !form.llm_model) { notify(ui.providerRequired, { tone: 'warning' }); return; } setIsTestingProvider(true); setTestResult(''); try { const res = await axios.post(`${APP_ENDPOINTS.apiBase}/providers/test`, { provider: form.llm_provider, model: form.llm_model, api_key: form.api_key, api_base: form.api_base || undefined, }); if (res.data?.ok) { const preview = (res.data.models_preview || []).slice(0, 3).join(', '); setTestResult(ui.connOk(preview)); } else { setTestResult(ui.connFailed(res.data?.detail || 'unknown error')); } } catch (error: any) { const msg = error?.response?.data?.detail || error?.message || 'request failed'; setTestResult(ui.connFailed(msg)); } finally { setIsTestingProvider(false); } }; const createBot = async () => { setIsSubmitting(true); try { await axios.post(`${APP_ENDPOINTS.apiBase}/bots`, { id: form.id, name: form.name, access_password: form.access_password, llm_provider: form.llm_provider, llm_model: form.llm_model, api_key: form.api_key, api_base: form.api_base || undefined, image_tag: form.image_tag, system_prompt: form.soul_md, temperature: clampTemperature(Number(form.temperature)), top_p: Number(form.top_p), max_tokens: Number(form.max_tokens), cpu_cores: Number(form.cpu_cores), memory_mb: Number(form.memory_mb), storage_gb: Number(form.storage_gb), soul_md: form.soul_md, agents_md: form.agents_md, user_md: form.user_md, tools_md: form.tools_md, identity_md: form.identity_md, send_progress: Boolean(form.send_progress), send_tool_hints: Boolean(form.send_tool_hints), channels: form.channels.map((c) => ({ channel_type: c.channel_type, is_active: c.is_active, external_app_id: c.external_app_id, app_secret: c.app_secret, internal_port: c.internal_port, extra_config: sanitizeChannelExtra(c.channel_type, c.extra_config), })), env_params: form.env_params, }); if (String(form.access_password || '').trim()) { setBotAccessPassword(form.id, form.access_password); } if (autoStart) { await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${form.id}/start`); } onCreated?.(); onGoDashboard?.(); setForm(initialForm); setMaxTokensDraft(String(initialForm.max_tokens)); setCpuCoresDraft(String(initialForm.cpu_cores)); setMemoryMbDraft(String(initialForm.memory_mb)); setStorageGbDraft(String(initialForm.storage_gb)); setStep(1); setTestResult(''); notify(ui.created, { tone: 'success' }); } catch (error: any) { const msg = error?.response?.data?.detail || ui.createFailed; notify(msg, { tone: 'error' }); } finally { setIsSubmitting(false); } }; const onProviderChange = (provider: string) => { const preset = providerPresets[provider] ?? { model: '' }; setForm((p) => ({ ...p, llm_provider: provider, llm_model: preset.model || p.llm_model, api_base: preset.apiBase ?? '', })); setTestResult(''); }; const tabMap: Record = { AGENTS: 'agents_md', SOUL: 'soul_md', USER: 'user_md', TOOLS: 'tools_md', IDENTITY: 'identity_md', }; const addChannel = () => { if (!addableChannelTypes.includes(newChannelType)) return; setForm((prev) => ({ ...prev, channels: [ ...prev.channels, { channel_type: newChannelType, is_active: true, external_app_id: '', app_secret: '', internal_port: 8080, extra_config: {}, }, ], })); const rest = addableChannelTypes.filter((t) => t !== newChannelType); if (rest.length > 0) setNewChannelType(rest[0]); }; const upsertEnvParam = (key: string, value: string) => { const normalized = String(key || '').trim().toUpperCase(); if (!normalized) return; setForm((prev) => ({ ...prev, env_params: { ...(prev.env_params || {}), [normalized]: String(value || ''), }, })); }; const removeEnvParam = (key: string) => { const normalized = String(key || '').trim().toUpperCase(); if (!normalized) return; setForm((prev) => { const next = { ...(prev.env_params || {}) }; delete next[normalized]; return { ...prev, env_params: next }; }); }; const updateChannel = (index: number, patch: Partial) => { setForm((prev) => ({ ...prev, channels: prev.channels.map((c, i) => (i === index ? { ...c, ...patch } : c)), })); }; const removeChannel = (index: number) => { setForm((prev) => ({ ...prev, channels: prev.channels.filter((_, i) => i !== index), })); }; const clampMaxTokens = (value: number) => { if (Number.isNaN(value)) return 8192; return Math.min(32768, Math.max(256, Math.round(value))); }; const clampTemperature = (value: number) => { if (Number.isNaN(value)) return 0.2; return Math.min(1, Math.max(0, value)); }; const clampCpuCores = (value: number) => { if (Number.isNaN(value)) return 1; if (value === 0) return 0; return Math.min(16, Math.max(0.1, Math.round(value * 10) / 10)); }; const clampMemoryMb = (value: number) => { if (Number.isNaN(value)) return 1024; if (value === 0) return 0; return Math.min(65536, Math.max(256, Math.round(value))); }; const clampStorageGb = (value: number) => { if (Number.isNaN(value)) return 10; if (value === 0) return 0; return Math.min(1024, Math.max(1, Math.round(value))); }; const commitMaxTokensDraft = (raw: string) => { const next = clampMaxTokens(Number(raw)); setForm((p) => ({ ...p, max_tokens: next })); setMaxTokensDraft(String(next)); }; const commitCpuCoresDraft = (raw: string) => { const next = clampCpuCores(Number(raw)); setForm((p) => ({ ...p, cpu_cores: next })); setCpuCoresDraft(String(next)); }; const commitMemoryMbDraft = (raw: string) => { const next = clampMemoryMb(Number(raw)); setForm((p) => ({ ...p, memory_mb: next })); setMemoryMbDraft(String(next)); }; const commitStorageGbDraft = (raw: string) => { const next = clampStorageGb(Number(raw)); setForm((p) => ({ ...p, storage_gb: next })); setStorageGbDraft(String(next)); }; const updateGlobalDeliveryFlag = (key: 'sendProgress' | 'sendToolHints', value: boolean) => { setForm((prev) => { if (key === 'sendProgress') return { ...prev, send_progress: value }; return { ...prev, send_tool_hints: value }; }); }; const sanitizeChannelExtra = (_channelType: string, extra: Record) => { const next = { ...(extra || {}) }; delete next.sendProgress; delete next.sendToolHints; return next; }; const renderChannelFields = (channel: WizardChannelConfig, idx: number) => { if (channel.channel_type === 'telegram') { return ( <> updateChannel(idx, { app_secret: e.target.value })} /> updateChannel(idx, { extra_config: { ...(channel.extra_config || {}), proxy: e.target.value } }) } /> ); } if (channel.channel_type === 'feishu') { return ( <> updateChannel(idx, { external_app_id: e.target.value })} /> updateChannel(idx, { app_secret: e.target.value })} /> updateChannel(idx, { extra_config: { ...(channel.extra_config || {}), encryptKey: e.target.value } }) } /> updateChannel(idx, { extra_config: { ...(channel.extra_config || {}), verificationToken: e.target.value } }) } /> ); } if (channel.channel_type === 'dingtalk') { return ( <> updateChannel(idx, { external_app_id: e.target.value })} /> updateChannel(idx, { app_secret: e.target.value })} /> ); } if (channel.channel_type === 'slack') { return ( <> updateChannel(idx, { external_app_id: e.target.value })} /> updateChannel(idx, { app_secret: e.target.value })} /> ); } if (channel.channel_type === 'qq') { return ( <> updateChannel(idx, { external_app_id: e.target.value })} /> updateChannel(idx, { app_secret: e.target.value })} /> ); } return null; }; return (

{ui.title}

{ui.sub}

{ui.s1}
{ui.s2}
{ui.s3}
{ui.s4}
{step === 1 && (
{readyImages.map((img) => ( ))} {readyImages.length === 0 &&
{ui.noReady}
}
)} {step === 2 && (
{ui.baseInfo}
setForm((p) => ({ ...p, id: e.target.value }))} /> setForm((p) => ({ ...p, name: e.target.value }))} /> setForm((p) => ({ ...p, access_password: e.target.value }))} />
{isZh ? '资源配额' : 'Resource Limits'}
setCpuCoresDraft(e.target.value)} onBlur={(e) => commitCpuCoresDraft(e.target.value)} /> setMemoryMbDraft(e.target.value)} onBlur={(e) => commitMemoryMbDraft(e.target.value)} /> setStorageGbDraft(e.target.value)} onBlur={(e) => commitStorageGbDraft(e.target.value)} />
{isZh ? '提示:填写 0 表示不限制。' : 'Tip: value 0 means unlimited.'}
{ui.modelAccess}
setForm((p) => ({ ...p, llm_model: e.target.value }))} /> setForm((p) => ({ ...p, api_key: e.target.value }))} /> setForm((p) => ({ ...p, api_base: e.target.value }))} />
{providerPresets[form.llm_provider]?.note[noteLocale]}
{testResult &&
{testResult}
}
{ui.modelParams}
setForm((p) => ({ ...p, temperature: clampTemperature(Number(e.target.value)) }))} />
setForm((p) => ({ ...p, top_p: Number(e.target.value) }))} />
setMaxTokensDraft(e.target.value)} onBlur={(e) => commitMaxTokensDraft(e.target.value)} />
{ui.tokenRange}
{[4096, 8192, 16384, 32768].map((value) => ( ))}
{lc.wizardSectionTitle}
{lc.wizardSectionDesc}
{configuredChannelsLabel}
setShowChannelModal(true)} tooltip={lc.openManager} aria-label={lc.openManager}>
{ui.toolsConfig}
{ui.toolsDesc}
{envEntries.length > 0 ? envEntries.map(([k]) => k).join(', ') : ui.noEnvParams}
setShowToolsConfigModal(true)} tooltip={ui.openToolsManager} aria-label={ui.openToolsManager} >
)} {step === 3 && (
{(['AGENTS', 'SOUL', 'USER', 'TOOLS', 'IDENTITY'] as AgentTab[]).map((tab) => ( ))}
{agentTab === 'AGENTS' ? (
{isZh ? '建议:将“创建新目录并以 Markdown 输出”写入 AGENTS.md' : 'Tip: Put "create output directory + markdown output" in AGENTS.md'}
) : null}