v0.1.4
parent
415d0078fb
commit
78b27d9c36
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<string, { model: string; note: { 'zh-cn': string;
|
|||
const initialForm = {
|
||||
id: '',
|
||||
name: '',
|
||||
access_password: '',
|
||||
llm_provider: 'dashscope',
|
||||
llm_model: providerPresets.dashscope.model,
|
||||
api_key: '',
|
||||
|
|
@ -153,6 +151,8 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
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 [botIdStatus, setBotIdStatus] = useState<'idle' | 'checking' | 'available' | 'exists' | 'invalid'>('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
|
|||
<div className="grid-2 wizard-step2-grid wizard-step2-grid-3" style={{ gridTemplateColumns: '1fr 1fr 1fr' }}>
|
||||
<div className="stack card wizard-step2-card">
|
||||
<div className="section-mini-title">{ui.baseInfo}</div>
|
||||
<input className="input" placeholder={ui.botIdPlaceholder} value={form.id} onChange={(e) => setForm((p) => ({ ...p, id: e.target.value }))} />
|
||||
<input
|
||||
className="input"
|
||||
placeholder={ui.botIdPlaceholder}
|
||||
value={form.id}
|
||||
onChange={(e) => {
|
||||
const normalized = e.target.value.replace(/[^A-Za-z0-9_]/g, '');
|
||||
setForm((p) => ({ ...p, id: normalized }));
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="field-label"
|
||||
style={{
|
||||
color:
|
||||
botIdStatus === 'invalid' || botIdStatus === 'exists'
|
||||
? 'var(--err)'
|
||||
: botIdStatus === 'available'
|
||||
? 'var(--ok)'
|
||||
: 'var(--muted)',
|
||||
}}
|
||||
>
|
||||
{botIdStatusText || ui.botIdHint}
|
||||
</div>
|
||||
<input className="input" placeholder={ui.botName} value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} />
|
||||
<input className="input" type="password" placeholder={ui.accessPasswordPlaceholder} value={form.access_password} onChange={(e) => setForm((p) => ({ ...p, access_password: e.target.value }))} />
|
||||
|
||||
<div className="section-mini-title" style={{ marginTop: 10 }}>
|
||||
{isZh ? '资源配额' : 'Resource Limits'}
|
||||
|
|
@ -768,11 +835,10 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
|
||||
{step === 4 && (
|
||||
<div className="stack">
|
||||
<div className="card summary-grid">
|
||||
<div className="card summary-grid">
|
||||
<div>{ui.image}: <span className="mono">{form.image_tag}</span></div>
|
||||
<div>Bot ID: <span className="mono">{form.id}</span></div>
|
||||
<div>{ui.name}: {form.name}</div>
|
||||
<div>{ui.accessPassword}: {form.access_password ? (isZh ? '已设置' : 'Configured') : (isZh ? '未设置' : 'Not set')}</div>
|
||||
<div>Provider: {form.llm_provider}</div>
|
||||
<div>{ui.model}: {form.llm_model}</div>
|
||||
<div>Temperature: {form.temperature.toFixed(2)}</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue