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)
|
docker_manager = BotDockerManager(host_data_root=BOTS_WORKSPACE_ROOT)
|
||||||
config_manager = BotConfigManager(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):
|
class ChannelConfigRequest(BaseModel):
|
||||||
|
|
@ -1567,6 +1568,14 @@ async def test_provider(payload: dict):
|
||||||
|
|
||||||
@app.post("/api/bots")
|
@app.post("/api/bots")
|
||||||
def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session)):
|
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)
|
image_row = session.get(NanobotImage, payload.image_tag)
|
||||||
if not image_row:
|
if not image_row:
|
||||||
raise HTTPException(status_code=400, detail=f"Image not registered in DB: {payload.image_tag}")
|
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}")
|
raise HTTPException(status_code=400, detail=f"Docker image not found locally: {payload.image_tag}")
|
||||||
|
|
||||||
bot = BotInstance(
|
bot = BotInstance(
|
||||||
id=payload.id,
|
id=normalized_bot_id,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
access_password=str(payload.access_password or ""),
|
access_password=str(payload.access_password or ""),
|
||||||
image_tag=payload.image_tag,
|
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.add(bot)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(bot)
|
session.refresh(bot)
|
||||||
resource_limits = _normalize_resource_limits(payload.cpu_cores, payload.memory_mb, payload.storage_gb)
|
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(
|
_sync_workspace_channels(
|
||||||
session,
|
session,
|
||||||
payload.id,
|
normalized_bot_id,
|
||||||
channels_override=_normalize_initial_channels(payload.id, payload.channels),
|
channels_override=_normalize_initial_channels(normalized_bot_id, payload.channels),
|
||||||
global_delivery_override={
|
global_delivery_override={
|
||||||
"sendProgress": bool(payload.send_progress) if payload.send_progress is not None else False,
|
"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,
|
"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)
|
session.refresh(bot)
|
||||||
_invalidate_bot_detail_cache(payload.id)
|
_invalidate_bot_detail_cache(normalized_bot_id)
|
||||||
return _serialize_bot(bot)
|
return _serialize_bot(bot)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,13 @@ export const wizardEn = {
|
||||||
loadImages: 'Load images',
|
loadImages: 'Load images',
|
||||||
noReady: 'No READY image.',
|
noReady: 'No READY image.',
|
||||||
baseInfo: 'Base Info',
|
baseInfo: 'Base Info',
|
||||||
accessPassword: 'Access Password',
|
|
||||||
accessPasswordPlaceholder: 'Access password (optional)',
|
|
||||||
botIdPlaceholder: 'Bot ID',
|
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',
|
botName: 'Bot Name',
|
||||||
modelAccess: 'Model Access',
|
modelAccess: 'Model Access',
|
||||||
modelNamePlaceholder: 'Model name',
|
modelNamePlaceholder: 'Model name',
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,13 @@ export const wizardZhCn = {
|
||||||
loadImages: '加载镜像列表',
|
loadImages: '加载镜像列表',
|
||||||
noReady: '暂无 READY 镜像。',
|
noReady: '暂无 READY 镜像。',
|
||||||
baseInfo: '基础信息',
|
baseInfo: '基础信息',
|
||||||
accessPassword: '访问密码',
|
|
||||||
accessPasswordPlaceholder: '访问密码(可选)',
|
|
||||||
botIdPlaceholder: 'Bot ID(如 analyst_bot_01)',
|
botIdPlaceholder: 'Bot ID(如 analyst_bot_01)',
|
||||||
|
botIdHint: '只能输入英文字母、数字和下划线。',
|
||||||
|
botIdInvalid: 'Bot ID 只能包含英文字母、数字和下划线。',
|
||||||
|
botIdChecking: '正在检查 Bot ID 是否可用...',
|
||||||
|
botIdAvailable: 'Bot ID 可用。',
|
||||||
|
botIdExists: '该 Bot ID 已存在,请更换。',
|
||||||
|
botIdRequired: '请填写 Bot ID。',
|
||||||
botName: 'Bot 名称',
|
botName: 'Bot 名称',
|
||||||
modelAccess: '模型接入',
|
modelAccess: '模型接入',
|
||||||
modelNamePlaceholder: '模型名(如 qwen-plus)',
|
modelNamePlaceholder: '模型名(如 qwen-plus)',
|
||||||
|
|
|
||||||
|
|
@ -321,14 +321,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-actions .ops-bot-action-stop {
|
.ops-bot-actions .ops-bot-action-stop {
|
||||||
background: #0b1220;
|
background: color-mix(in oklab, #f5af48 30%, var(--panel-soft) 70%);
|
||||||
border-color: color-mix(in oklab, #0b1220 72%, var(--line) 28%);
|
border-color: color-mix(in oklab, #f5af48 58%, var(--line) 42%);
|
||||||
color: #fff;
|
color: #5e3b00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-actions .ops-bot-action-stop:hover {
|
.ops-bot-actions .ops-bot-action-stop:hover {
|
||||||
background: color-mix(in oklab, #0b1220 84%, #1f2937 16%);
|
background: color-mix(in oklab, #f5af48 38%, var(--panel-soft) 62%);
|
||||||
border-color: color-mix(in oklab, #0b1220 82%, white 18%);
|
border-color: color-mix(in oklab, #f5af48 70%, var(--line) 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-actions .ops-bot-action-delete {
|
.ops-bot-actions .ops-bot-action-delete {
|
||||||
|
|
@ -1088,9 +1088,10 @@
|
||||||
width: 34px;
|
width: 34px;
|
||||||
min-width: 34px;
|
min-width: 34px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: color-mix(in oklab, #d14b4b 20%, var(--panel) 80%);
|
background: #0b1220;
|
||||||
color: color-mix(in oklab, var(--text) 86%, white 14%);
|
color: #fff;
|
||||||
border: 1px solid color-mix(in oklab, #d14b4b 50%, var(--line) 50%);
|
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) {
|
.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 { wizardEn } from '../../i18n/wizard.en';
|
||||||
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
||||||
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
||||||
import { setBotAccessPassword } from '../../utils/botAccess';
|
|
||||||
|
|
||||||
type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY';
|
type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY';
|
||||||
type ChannelType = 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack';
|
type ChannelType = 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack';
|
||||||
|
|
@ -97,7 +96,6 @@ const providerPresets: Record<string, { model: string; note: { 'zh-cn': string;
|
||||||
const initialForm = {
|
const initialForm = {
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
access_password: '',
|
|
||||||
llm_provider: 'dashscope',
|
llm_provider: 'dashscope',
|
||||||
llm_model: providerPresets.dashscope.model,
|
llm_model: providerPresets.dashscope.model,
|
||||||
api_key: '',
|
api_key: '',
|
||||||
|
|
@ -153,6 +151,8 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
const [cpuCoresDraft, setCpuCoresDraft] = useState(String(initialForm.cpu_cores));
|
const [cpuCoresDraft, setCpuCoresDraft] = useState(String(initialForm.cpu_cores));
|
||||||
const [memoryMbDraft, setMemoryMbDraft] = useState(String(initialForm.memory_mb));
|
const [memoryMbDraft, setMemoryMbDraft] = useState(String(initialForm.memory_mb));
|
||||||
const [storageGbDraft, setStorageGbDraft] = useState(String(initialForm.storage_gb));
|
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 readyImages = useMemo(() => images.filter((img) => img.status === 'READY'), [images]);
|
||||||
const isZh = locale === 'zh';
|
const isZh = locale === 'zh';
|
||||||
|
|
@ -195,6 +195,40 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
};
|
};
|
||||||
void loadSystemDefaults();
|
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(
|
const configuredChannelsLabel = useMemo(
|
||||||
() => (form.channels.length > 0 ? form.channels.map((c) => c.channel_type).join(', ') : '-'),
|
() => (form.channels.length > 0 ? form.channels.map((c) => c.channel_type).join(', ') : '-'),
|
||||||
[form.channels],
|
[form.channels],
|
||||||
|
|
@ -229,7 +263,23 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
commitCpuCoresDraft(cpuCoresDraft);
|
commitCpuCoresDraft(cpuCoresDraft);
|
||||||
commitMemoryMbDraft(memoryMbDraft);
|
commitMemoryMbDraft(memoryMbDraft);
|
||||||
commitStorageGbDraft(storageGbDraft);
|
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' });
|
notify(ui.requiredBase, { tone: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +326,6 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/bots`, {
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots`, {
|
||||||
id: form.id,
|
id: form.id,
|
||||||
name: form.name,
|
name: form.name,
|
||||||
access_password: form.access_password,
|
|
||||||
llm_provider: form.llm_provider,
|
llm_provider: form.llm_provider,
|
||||||
llm_model: form.llm_model,
|
llm_model: form.llm_model,
|
||||||
api_key: form.api_key,
|
api_key: form.api_key,
|
||||||
|
|
@ -307,10 +356,6 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
env_params: form.env_params,
|
env_params: form.env_params,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (String(form.access_password || '').trim()) {
|
|
||||||
setBotAccessPassword(form.id, form.access_password);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoStart) {
|
if (autoStart) {
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${form.id}/start`);
|
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));
|
setStorageGbDraft(String(initialForm.storage_gb));
|
||||||
setStep(1);
|
setStep(1);
|
||||||
setTestResult('');
|
setTestResult('');
|
||||||
|
setBotIdStatus('idle');
|
||||||
|
setBotIdStatusText('');
|
||||||
notify(ui.created, { tone: 'success' });
|
notify(ui.created, { tone: 'success' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const msg = error?.response?.data?.detail || ui.createFailed;
|
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="grid-2 wizard-step2-grid wizard-step2-grid-3" style={{ gridTemplateColumns: '1fr 1fr 1fr' }}>
|
||||||
<div className="stack card wizard-step2-card">
|
<div className="stack card wizard-step2-card">
|
||||||
<div className="section-mini-title">{ui.baseInfo}</div>
|
<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" 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 }}>
|
<div className="section-mini-title" style={{ marginTop: 10 }}>
|
||||||
{isZh ? '资源配额' : 'Resource Limits'}
|
{isZh ? '资源配额' : 'Resource Limits'}
|
||||||
|
|
@ -768,11 +835,10 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
|
|
||||||
{step === 4 && (
|
{step === 4 && (
|
||||||
<div className="stack">
|
<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>{ui.image}: <span className="mono">{form.image_tag}</span></div>
|
||||||
<div>Bot ID: <span className="mono">{form.id}</span></div>
|
<div>Bot ID: <span className="mono">{form.id}</span></div>
|
||||||
<div>{ui.name}: {form.name}</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>Provider: {form.llm_provider}</div>
|
||||||
<div>{ui.model}: {form.llm_model}</div>
|
<div>{ui.model}: {form.llm_model}</div>
|
||||||
<div>Temperature: {form.temperature.toFixed(2)}</div>
|
<div>Temperature: {form.temperature.toFixed(2)}</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue