From 78b27d9c360bef54cf3e803bdec1ae1528627033 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Tue, 10 Mar 2026 17:18:17 +0800 Subject: [PATCH] v0.1.4 --- backend/main.py | 21 +++-- frontend/src/i18n/wizard.en.ts | 8 +- frontend/src/i18n/wizard.zh-cn.ts | 8 +- .../modules/dashboard/BotDashboardModule.css | 17 ++-- .../modules/onboarding/BotWizardModule.tsx | 90 ++++++++++++++++--- 5 files changed, 114 insertions(+), 30 deletions(-) diff --git a/backend/main.py b/backend/main.py index dad3d72..704cac0 100644 --- a/backend/main.py +++ b/backend/main.py @@ -55,6 +55,7 @@ os.makedirs(DATA_ROOT, exist_ok=True) docker_manager = BotDockerManager(host_data_root=BOTS_WORKSPACE_ROOT) config_manager = BotConfigManager(host_data_root=BOTS_WORKSPACE_ROOT) +BOT_ID_PATTERN = re.compile(r"^[A-Za-z0-9_]+$") class ChannelConfigRequest(BaseModel): @@ -1567,6 +1568,14 @@ async def test_provider(payload: dict): @app.post("/api/bots") def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session)): + normalized_bot_id = str(payload.id or "").strip() + if not normalized_bot_id: + raise HTTPException(status_code=400, detail="Bot ID is required") + if not BOT_ID_PATTERN.fullmatch(normalized_bot_id): + raise HTTPException(status_code=400, detail="Bot ID can only contain letters, numbers, and underscores") + if session.get(BotInstance, normalized_bot_id): + raise HTTPException(status_code=409, detail=f"Bot ID already exists: {normalized_bot_id}") + image_row = session.get(NanobotImage, payload.image_tag) if not image_row: raise HTTPException(status_code=400, detail=f"Image not registered in DB: {payload.image_tag}") @@ -1576,22 +1585,22 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session raise HTTPException(status_code=400, detail=f"Docker image not found locally: {payload.image_tag}") bot = BotInstance( - id=payload.id, + id=normalized_bot_id, name=payload.name, access_password=str(payload.access_password or ""), image_tag=payload.image_tag, - workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, payload.id), + workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, normalized_bot_id), ) session.add(bot) session.commit() session.refresh(bot) resource_limits = _normalize_resource_limits(payload.cpu_cores, payload.memory_mb, payload.storage_gb) - _write_env_store(payload.id, _normalize_env_params(payload.env_params)) + _write_env_store(normalized_bot_id, _normalize_env_params(payload.env_params)) _sync_workspace_channels( session, - payload.id, - channels_override=_normalize_initial_channels(payload.id, payload.channels), + normalized_bot_id, + channels_override=_normalize_initial_channels(normalized_bot_id, payload.channels), global_delivery_override={ "sendProgress": bool(payload.send_progress) if payload.send_progress is not None else False, "sendToolHints": bool(payload.send_tool_hints) if payload.send_tool_hints is not None else False, @@ -1618,7 +1627,7 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session }, ) session.refresh(bot) - _invalidate_bot_detail_cache(payload.id) + _invalidate_bot_detail_cache(normalized_bot_id) return _serialize_bot(bot) diff --git a/frontend/src/i18n/wizard.en.ts b/frontend/src/i18n/wizard.en.ts index 0c614c8..c6c3405 100644 --- a/frontend/src/i18n/wizard.en.ts +++ b/frontend/src/i18n/wizard.en.ts @@ -21,9 +21,13 @@ export const wizardEn = { loadImages: 'Load images', noReady: 'No READY image.', baseInfo: 'Base Info', - accessPassword: 'Access Password', - accessPasswordPlaceholder: 'Access password (optional)', botIdPlaceholder: 'Bot ID', + botIdHint: 'Only letters, numbers, and underscores are allowed.', + botIdInvalid: 'Bot ID can only contain letters, numbers, and underscores.', + botIdChecking: 'Checking whether the Bot ID is available...', + botIdAvailable: 'Bot ID is available.', + botIdExists: 'This Bot ID already exists. Please choose another one.', + botIdRequired: 'Please enter a Bot ID.', botName: 'Bot Name', modelAccess: 'Model Access', modelNamePlaceholder: 'Model name', diff --git a/frontend/src/i18n/wizard.zh-cn.ts b/frontend/src/i18n/wizard.zh-cn.ts index 802406d..4c64f69 100644 --- a/frontend/src/i18n/wizard.zh-cn.ts +++ b/frontend/src/i18n/wizard.zh-cn.ts @@ -21,9 +21,13 @@ export const wizardZhCn = { loadImages: '加载镜像列表', noReady: '暂无 READY 镜像。', baseInfo: '基础信息', - accessPassword: '访问密码', - accessPasswordPlaceholder: '访问密码(可选)', botIdPlaceholder: 'Bot ID(如 analyst_bot_01)', + botIdHint: '只能输入英文字母、数字和下划线。', + botIdInvalid: 'Bot ID 只能包含英文字母、数字和下划线。', + botIdChecking: '正在检查 Bot ID 是否可用...', + botIdAvailable: 'Bot ID 可用。', + botIdExists: '该 Bot ID 已存在,请更换。', + botIdRequired: '请填写 Bot ID。', botName: 'Bot 名称', modelAccess: '模型接入', modelNamePlaceholder: '模型名(如 qwen-plus)', diff --git a/frontend/src/modules/dashboard/BotDashboardModule.css b/frontend/src/modules/dashboard/BotDashboardModule.css index 89f4b13..1e553bb 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.css +++ b/frontend/src/modules/dashboard/BotDashboardModule.css @@ -321,14 +321,14 @@ } .ops-bot-actions .ops-bot-action-stop { - background: #0b1220; - border-color: color-mix(in oklab, #0b1220 72%, var(--line) 28%); - color: #fff; + background: color-mix(in oklab, #f5af48 30%, var(--panel-soft) 70%); + border-color: color-mix(in oklab, #f5af48 58%, var(--line) 42%); + color: #5e3b00; } .ops-bot-actions .ops-bot-action-stop:hover { - background: color-mix(in oklab, #0b1220 84%, #1f2937 16%); - border-color: color-mix(in oklab, #0b1220 82%, white 18%); + background: color-mix(in oklab, #f5af48 38%, var(--panel-soft) 62%); + border-color: color-mix(in oklab, #f5af48 70%, var(--line) 30%); } .ops-bot-actions .ops-bot-action-delete { @@ -1088,9 +1088,10 @@ width: 34px; min-width: 34px; padding: 0; - background: color-mix(in oklab, #d14b4b 20%, var(--panel) 80%); - color: color-mix(in oklab, var(--text) 86%, white 14%); - border: 1px solid color-mix(in oklab, #d14b4b 50%, var(--line) 50%); + background: #0b1220; + color: #fff; + border: 1px solid color-mix(in oklab, #0b1220 72%, var(--line) 28%); + box-shadow: 0 8px 18px rgba(9, 15, 28, 0.22); } .ops-composer-submit-btn:hover:not(:disabled) { diff --git a/frontend/src/modules/onboarding/BotWizardModule.tsx b/frontend/src/modules/onboarding/BotWizardModule.tsx index 5cd7417..3847e8d 100644 --- a/frontend/src/modules/onboarding/BotWizardModule.tsx +++ b/frontend/src/modules/onboarding/BotWizardModule.tsx @@ -10,7 +10,6 @@ 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'; @@ -97,7 +96,6 @@ const providerPresets: Record('idle'); + const [botIdStatusText, setBotIdStatusText] = useState(''); const readyImages = useMemo(() => images.filter((img) => img.status === 'READY'), [images]); const isZh = locale === 'zh'; @@ -195,6 +195,40 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro }; void loadSystemDefaults(); }, []); + + useEffect(() => { + const raw = String(form.id || '').trim(); + if (!raw) { + setBotIdStatus('idle'); + setBotIdStatusText(''); + return; + } + if (!/^[A-Za-z0-9_]+$/.test(raw)) { + setBotIdStatus('invalid'); + setBotIdStatusText(ui.botIdInvalid); + return; + } + + setBotIdStatus('checking'); + setBotIdStatusText(ui.botIdChecking); + const timer = window.setTimeout(async () => { + try { + await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(raw)}`); + setBotIdStatus('exists'); + setBotIdStatusText(ui.botIdExists); + } catch (error: any) { + if (error?.response?.status === 404) { + setBotIdStatus('available'); + setBotIdStatusText(ui.botIdAvailable); + return; + } + setBotIdStatus('idle'); + setBotIdStatusText(''); + } + }, 300); + + return () => window.clearTimeout(timer); + }, [form.id, ui.botIdAvailable, ui.botIdChecking, ui.botIdExists, ui.botIdInvalid]); const configuredChannelsLabel = useMemo( () => (form.channels.length > 0 ? form.channels.map((c) => c.channel_type).join(', ') : '-'), [form.channels], @@ -229,7 +263,23 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro commitCpuCoresDraft(cpuCoresDraft); commitMemoryMbDraft(memoryMbDraft); commitStorageGbDraft(storageGbDraft); - if (!form.id || !form.name || !form.api_key || !form.image_tag || !form.llm_model) { + if (!form.id) { + notify(ui.botIdRequired, { tone: 'warning' }); + return; + } + if (botIdStatus === 'invalid') { + notify(ui.botIdInvalid, { tone: 'warning' }); + return; + } + if (botIdStatus === 'exists') { + notify(ui.botIdExists, { tone: 'warning' }); + return; + } + if (botIdStatus === 'checking') { + notify(ui.botIdChecking, { tone: 'warning' }); + return; + } + if (!form.name || !form.api_key || !form.image_tag || !form.llm_model) { notify(ui.requiredBase, { tone: 'warning' }); return; } @@ -276,7 +326,6 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro 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, @@ -307,10 +356,6 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro 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`); } @@ -323,6 +368,8 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro setStorageGbDraft(String(initialForm.storage_gb)); setStep(1); setTestResult(''); + setBotIdStatus('idle'); + setBotIdStatusText(''); notify(ui.created, { tone: 'success' }); } catch (error: any) { const msg = error?.response?.data?.detail || ui.createFailed; @@ -594,9 +641,29 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
{ui.baseInfo}
- setForm((p) => ({ ...p, id: e.target.value }))} /> + { + const normalized = e.target.value.replace(/[^A-Za-z0-9_]/g, ''); + setForm((p) => ({ ...p, id: normalized })); + }} + /> +
+ {botIdStatusText || ui.botIdHint} +
setForm((p) => ({ ...p, name: e.target.value }))} /> - setForm((p) => ({ ...p, access_password: e.target.value }))} />
{isZh ? '资源配额' : 'Resource Limits'} @@ -768,11 +835,10 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro {step === 4 && (
-
+
{ui.image}: {form.image_tag}
Bot ID: {form.id}
{ui.name}: {form.name}
-
{ui.accessPassword}: {form.access_password ? (isZh ? '已设置' : 'Configured') : (isZh ? '未设置' : 'Not set')}
Provider: {form.llm_provider}
{ui.model}: {form.llm_model}
Temperature: {form.temperature.toFixed(2)}