v0.1.4-p6

main
mula.liu 2026-04-17 13:53:47 +08:00
parent 4c99826863
commit 02a4000416
26 changed files with 61 additions and 98 deletions

View File

@ -34,7 +34,6 @@ DEFAULT_BOT_SYSTEM_TIMEZONE=Asia/Shanghai
# The following platform-level items are initialized by SQL and managed in sys_setting / 平台参数: # The following platform-level items are initialized by SQL and managed in sys_setting / 平台参数:
# - page_size # - page_size
# - chat_pull_page_size # - chat_pull_page_size
# - command_auto_unlock_seconds
# - upload_max_mb # - upload_max_mb
# - allowed_attachment_extensions # - allowed_attachment_extensions
# - workspace_download_extensions # - workspace_download_extensions

View File

@ -18,6 +18,7 @@ from services.bot_management_service import (
list_bots_with_cache, list_bots_with_cache,
update_bot_record, update_bot_record,
) )
from services.image_service import list_registered_images
from services.provider_service import test_provider_connection from services.provider_service import test_provider_connection
router = APIRouter() router = APIRouter()
@ -28,6 +29,22 @@ async def test_provider(payload: dict):
return await test_provider_connection(payload) return await test_provider_connection(payload)
@router.post("/api/bots/{bot_id}/providers/test")
async def test_bot_provider(bot_id: str, payload: dict, session: Session = Depends(get_session)):
bot = session.get(BotInstance, bot_id)
if not bot:
raise HTTPException(status_code=404, detail="Bot not found")
return await test_provider_connection(payload)
@router.get("/api/bots/{bot_id}/images")
def list_bot_images(bot_id: str, session: Session = Depends(get_session)):
bot = session.get(BotInstance, bot_id)
if not bot:
raise HTTPException(status_code=404, detail="Bot not found")
return list_registered_images(session)
@router.post("/api/bots") @router.post("/api/bots")
def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session)): def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session)):
return create_bot_record(session, payload=payload) return create_bot_record(session, payload=payload)

View File

@ -34,7 +34,6 @@ def get_system_defaults():
"chat": { "chat": {
"pull_page_size": platform_settings.chat_pull_page_size, "pull_page_size": platform_settings.chat_pull_page_size,
"page_size": platform_settings.page_size, "page_size": platform_settings.page_size,
"command_auto_unlock_seconds": platform_settings.command_auto_unlock_seconds,
}, },
"topic_presets": get_topic_presets()["presets"], "topic_presets": get_topic_presets()["presets"],
"speech": { "speech": {

View File

@ -45,7 +45,6 @@ REQUIRED_TABLES = (
REQUIRED_SYS_SETTING_KEYS = ( REQUIRED_SYS_SETTING_KEYS = (
"page_size", "page_size",
"chat_pull_page_size", "chat_pull_page_size",
"command_auto_unlock_seconds",
"auth_token_ttl_hours", "auth_token_ttl_hours",
"auth_token_max_active", "auth_token_max_active",
"upload_max_mb", "upload_max_mb",

View File

@ -187,7 +187,6 @@ DATABASE_POOL_RECYCLE: Final[int] = _env_int("DATABASE_POOL_RECYCLE", 1800, 30,
DEFAULT_UPLOAD_MAX_MB: Final[int] = 100 DEFAULT_UPLOAD_MAX_MB: Final[int] = 100
DEFAULT_PAGE_SIZE: Final[int] = 10 DEFAULT_PAGE_SIZE: Final[int] = 10
DEFAULT_CHAT_PULL_PAGE_SIZE: Final[int] = 60 DEFAULT_CHAT_PULL_PAGE_SIZE: Final[int] = 60
DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS: Final[int] = 10
DEFAULT_AUTH_TOKEN_TTL_HOURS: Final[int] = _env_int("AUTH_TOKEN_TTL_HOURS", 24, 1, 720) DEFAULT_AUTH_TOKEN_TTL_HOURS: Final[int] = _env_int("AUTH_TOKEN_TTL_HOURS", 24, 1, 720)
DEFAULT_AUTH_TOKEN_MAX_ACTIVE: Final[int] = _env_int("AUTH_TOKEN_MAX_ACTIVE", 2, 1, 20) DEFAULT_AUTH_TOKEN_MAX_ACTIVE: Final[int] = _env_int("AUTH_TOKEN_MAX_ACTIVE", 2, 1, 20)
DEFAULT_BOT_SYSTEM_TIMEZONE: Final[str] = str( DEFAULT_BOT_SYSTEM_TIMEZONE: Final[str] = str(

View File

@ -6,7 +6,6 @@ from pydantic import BaseModel, Field
class PlatformSettingsPayload(BaseModel): class PlatformSettingsPayload(BaseModel):
page_size: int = Field(default=10, ge=1, le=100) page_size: int = Field(default=10, ge=1, le=100)
chat_pull_page_size: int = Field(default=60, ge=10, le=500) chat_pull_page_size: int = Field(default=60, ge=10, le=500)
command_auto_unlock_seconds: int = Field(default=10, ge=1, le=600)
auth_token_ttl_hours: int = Field(default=24, ge=1, le=720) auth_token_ttl_hours: int = Field(default=24, ge=1, le=720)
auth_token_max_active: int = Field(default=2, ge=1, le=20) auth_token_max_active: int = Field(default=2, ge=1, le=20)
upload_max_mb: int = Field(default=100, ge=1, le=2048) upload_max_mb: int = Field(default=100, ge=1, le=2048)

View File

@ -424,6 +424,7 @@ def serialize_bot_detail(bot: BotInstance) -> Dict[str, Any]:
"image_tag": bot.image_tag, "image_tag": bot.image_tag,
"llm_provider": runtime["llm_provider"], "llm_provider": runtime["llm_provider"],
"llm_model": runtime["llm_model"], "llm_model": runtime["llm_model"],
"api_key": runtime["api_key"],
"api_base": runtime["api_base"], "api_base": runtime["api_base"],
"temperature": runtime["temperature"], "temperature": runtime["temperature"],
"top_p": runtime["top_p"], "top_p": runtime["top_p"],

View File

@ -9,7 +9,6 @@ from core.settings import (
DEFAULT_AUTH_TOKEN_MAX_ACTIVE, DEFAULT_AUTH_TOKEN_MAX_ACTIVE,
DEFAULT_AUTH_TOKEN_TTL_HOURS, DEFAULT_AUTH_TOKEN_TTL_HOURS,
DEFAULT_CHAT_PULL_PAGE_SIZE, DEFAULT_CHAT_PULL_PAGE_SIZE,
DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
DEFAULT_UPLOAD_MAX_MB, DEFAULT_UPLOAD_MAX_MB,
DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS, DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS,
@ -24,7 +23,6 @@ ACTIVITY_EVENT_RETENTION_SETTING_KEY = "activity_event_retention_days"
SETTING_KEYS = ( SETTING_KEYS = (
"page_size", "page_size",
"chat_pull_page_size", "chat_pull_page_size",
"command_auto_unlock_seconds",
"auth_token_ttl_hours", "auth_token_ttl_hours",
"auth_token_max_active", "auth_token_max_active",
"upload_max_mb", "upload_max_mb",
@ -52,15 +50,6 @@ SYSTEM_SETTING_DEFINITIONS: Dict[str, Dict[str, Any]] = {
"is_public": True, "is_public": True,
"sort_order": 8, "sort_order": 8,
}, },
"command_auto_unlock_seconds": {
"name": "发送按钮自动恢复秒数",
"category": "chat",
"description": "对话发送后按钮保持停止态的最长秒数,超时后自动恢复为可发送状态。",
"value_type": "integer",
"value": DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
"is_public": True,
"sort_order": 9,
},
"auth_token_ttl_hours": { "auth_token_ttl_hours": {
"name": "认证 Token 过期小时数", "name": "认证 Token 过期小时数",
"category": "auth", "category": "auth",

View File

@ -50,7 +50,6 @@ def get_platform_settings(session: Session) -> PlatformSettingsPayload:
{ {
"page_size": max(1, min(100, int(data["page_size"]))), "page_size": max(1, min(100, int(data["page_size"]))),
"chat_pull_page_size": max(10, min(500, int(data["chat_pull_page_size"]))), "chat_pull_page_size": max(10, min(500, int(data["chat_pull_page_size"]))),
"command_auto_unlock_seconds": max(1, min(600, int(data["command_auto_unlock_seconds"]))),
"auth_token_ttl_hours": max(1, min(720, int(data["auth_token_ttl_hours"]))), "auth_token_ttl_hours": max(1, min(720, int(data["auth_token_ttl_hours"]))),
"auth_token_max_active": max(1, min(20, int(data["auth_token_max_active"]))), "auth_token_max_active": max(1, min(20, int(data["auth_token_max_active"]))),
"upload_max_mb": int(data["upload_max_mb"]), "upload_max_mb": int(data["upload_max_mb"]),
@ -70,7 +69,6 @@ def save_platform_settings(session: Session, payload: PlatformSettingsPayload) -
normalized = PlatformSettingsPayload( normalized = PlatformSettingsPayload(
page_size=max(1, min(100, int(payload.page_size))), page_size=max(1, min(100, int(payload.page_size))),
chat_pull_page_size=max(10, min(500, int(payload.chat_pull_page_size))), chat_pull_page_size=max(10, min(500, int(payload.chat_pull_page_size))),
command_auto_unlock_seconds=max(1, min(600, int(payload.command_auto_unlock_seconds))),
auth_token_ttl_hours=max(1, min(720, int(payload.auth_token_ttl_hours))), auth_token_ttl_hours=max(1, min(720, int(payload.auth_token_ttl_hours))),
auth_token_max_active=max(1, min(20, int(payload.auth_token_max_active))), auth_token_max_active=max(1, min(20, int(payload.auth_token_max_active))),
upload_max_mb=payload.upload_max_mb, upload_max_mb=payload.upload_max_mb,

View File

@ -15,9 +15,23 @@ from services.platform_settings_core import (
) )
REQUIRED_SYSTEM_SETTING_KEYS = tuple(SYSTEM_SETTING_DEFINITIONS.keys()) REQUIRED_SYSTEM_SETTING_KEYS = tuple(SYSTEM_SETTING_DEFINITIONS.keys())
DEPRECATED_SYSTEM_SETTING_KEYS = ("command_auto_unlock_seconds",)
def _prune_deprecated_system_settings(session: Session) -> None:
removed = False
for key in DEPRECATED_SYSTEM_SETTING_KEYS:
row = session.get(PlatformSetting, key)
if row is None:
continue
session.delete(row)
removed = True
if removed:
session.commit()
def validate_required_system_settings(session: Session) -> None: def validate_required_system_settings(session: Session) -> None:
_prune_deprecated_system_settings(session)
stmt = select(PlatformSetting.key).where(PlatformSetting.key.in_(REQUIRED_SYSTEM_SETTING_KEYS)) stmt = select(PlatformSetting.key).where(PlatformSetting.key.in_(REQUIRED_SYSTEM_SETTING_KEYS))
present = { present = {
str(key or "").strip() str(key or "").strip()
@ -53,6 +67,8 @@ def list_system_settings(session: Session, search: str = "") -> List[Dict[str, A
def create_or_update_system_setting(session: Session, payload: SystemSettingPayload) -> Dict[str, Any]: def create_or_update_system_setting(session: Session, payload: SystemSettingPayload) -> Dict[str, Any]:
normalized_key = _normalize_setting_key(payload.key) normalized_key = _normalize_setting_key(payload.key)
if normalized_key in DEPRECATED_SYSTEM_SETTING_KEYS:
raise ValueError("Setting key has been removed")
definition = SYSTEM_SETTING_DEFINITIONS.get(normalized_key, {}) definition = SYSTEM_SETTING_DEFINITIONS.get(normalized_key, {})
row = _upsert_setting_row( row = _upsert_setting_row(
session, session,

View File

@ -69,7 +69,7 @@ export const dashboardEn = {
feedbackSaveFail: 'Failed to save feedback.', feedbackSaveFail: 'Failed to save feedback.',
feedbackMessagePending: 'Message is not synced yet. Please retry in a moment.', feedbackMessagePending: 'Message is not synced yet. Please retry in a moment.',
sendFailMsg: (msg: string) => `Command delivery failed: ${msg}`, sendFailMsg: (msg: string) => `Command delivery failed: ${msg}`,
providerRequired: 'Set provider/model/new API key before testing.', providerRequired: 'Set provider/model/API key before testing.',
connOk: (preview: string) => (preview ? `Connection passed, models: ${preview}` : 'Connection passed'), connOk: (preview: string) => (preview ? `Connection passed, models: ${preview}` : 'Connection passed'),
connFail: (msg: string) => `Failed: ${msg}`, connFail: (msg: string) => `Failed: ${msg}`,
configUpdated: 'Configuration updated (effective after bot restart).', configUpdated: 'Configuration updated (effective after bot restart).',
@ -265,8 +265,8 @@ export const dashboardEn = {
baseImageReadonly: 'Base Image', baseImageReadonly: 'Base Image',
modelName: 'Model Name', modelName: 'Model Name',
modelNamePlaceholder: 'e.g. qwen-plus', modelNamePlaceholder: 'e.g. qwen-plus',
newApiKey: 'New API Key (optional)', newApiKey: 'API Key',
newApiKeyPlaceholder: 'Only updated when filled', newApiKeyPlaceholder: 'Enter API Key',
testing: 'Testing...', testing: 'Testing...',
testModelConnection: 'Test model connection', testModelConnection: 'Test model connection',
cancel: 'Cancel', cancel: 'Cancel',

View File

@ -69,7 +69,7 @@ export const dashboardZhCn = {
feedbackSaveFail: '反馈保存失败。', feedbackSaveFail: '反馈保存失败。',
feedbackMessagePending: '消息尚未同步,暂不可反馈。', feedbackMessagePending: '消息尚未同步,暂不可反馈。',
sendFailMsg: (msg: string) => `指令发送失败:${msg}`, sendFailMsg: (msg: string) => `指令发送失败:${msg}`,
providerRequired: '请填写 Provider、模型和 API Key 后再测试。', providerRequired: '请填写 Provider、模型和 API Key 后再测试。',
connOk: (preview: string) => (preview ? `连接成功,模型: ${preview}` : '连接成功'), connOk: (preview: string) => (preview ? `连接成功,模型: ${preview}` : '连接成功'),
connFail: (msg: string) => `连接失败: ${msg}`, connFail: (msg: string) => `连接失败: ${msg}`,
configUpdated: '配置已更新(重启 Bot 后生效)。', configUpdated: '配置已更新(重启 Bot 后生效)。',
@ -265,8 +265,8 @@ export const dashboardZhCn = {
baseImageReadonly: '基础镜像', baseImageReadonly: '基础镜像',
modelName: '模型名称', modelName: '模型名称',
modelNamePlaceholder: '如 qwen-plus', modelNamePlaceholder: '如 qwen-plus',
newApiKey: '新的 API Key(留空不更新)', newApiKey: 'API Key',
newApiKeyPlaceholder: '输入新 Key 才会更新', newApiKeyPlaceholder: '请输入 API Key',
testing: '测试中...', testing: '测试中...',
testModelConnection: '测试模型连接', testModelConnection: '测试模型连接',
cancel: '取消', cancel: '取消',

View File

@ -44,7 +44,6 @@ export function useBotDashboardModule({
const fileNotPreviewableLabel = locale === 'zh' ? '当前文件类型不支持预览' : 'This file type is not previewable'; const fileNotPreviewableLabel = locale === 'zh' ? '当前文件类型不支持预览' : 'This file type is not previewable';
const [botListPageSize, setBotListPageSize] = useState(10); const [botListPageSize, setBotListPageSize] = useState(10);
const [chatPullPageSize, setChatPullPageSize] = useState(60); const [chatPullPageSize, setChatPullPageSize] = useState(60);
const [commandAutoUnlockSeconds, setCommandAutoUnlockSeconds] = useState(10);
const isZh = locale === 'zh'; const isZh = locale === 'zh';
const botSearchInputName = `nbot-search-${useId().replace(/:/g, '-')}`; const botSearchInputName = `nbot-search-${useId().replace(/:/g, '-')}`;
const workspaceSearchInputName = `nbot-workspace-search-${useId().replace(/:/g, '-')}`; const workspaceSearchInputName = `nbot-workspace-search-${useId().replace(/:/g, '-')}`;
@ -63,7 +62,6 @@ export function useBotDashboardModule({
notify, notify,
setBotListPageSize, setBotListPageSize,
setChatPullPageSize, setChatPullPageSize,
setCommandAutoUnlockSeconds,
}); });
const { const {
botListMenuOpen, botListMenuOpen,
@ -435,7 +433,6 @@ export function useBotDashboardModule({
filePickerRef, filePickerRef,
interruptExecution, interruptExecution,
isInterrupting, isInterrupting,
isSendingBlocked,
jumpConversationToDate, jumpConversationToDate,
loadInitialChatPage, loadInitialChatPage,
onChatScroll, onChatScroll,
@ -466,7 +463,6 @@ export function useBotDashboardModule({
canChat, canChat,
isTaskRunningExternally: isBotThinking, isTaskRunningExternally: isBotThinking,
chatPullPageSize, chatPullPageSize,
commandAutoUnlockSeconds,
pendingAttachments, pendingAttachments,
setPendingAttachments, setPendingAttachments,
isUploadingAttachments, isUploadingAttachments,
@ -500,7 +496,6 @@ export function useBotDashboardModule({
isChatEnabled, isChatEnabled,
} = useDashboardInteractionState({ } = useDashboardInteractionState({
canChat, canChat,
isSendingBlocked,
isVoiceRecording, isVoiceRecording,
isVoiceTranscribing, isVoiceTranscribing,
selectedBot, selectedBot,

View File

@ -97,7 +97,7 @@ export function useDashboardBotEditor({
llm_provider: provider, llm_provider: provider,
llm_model: bot.llm_model || '', llm_model: bot.llm_model || '',
image_tag: bot.image_tag || '', image_tag: bot.image_tag || '',
api_key: '', api_key: bot.api_key || '',
api_base: bot.api_base || '', api_base: bot.api_base || '',
temperature: clampTemperature(bot.temperature ?? 0.2), temperature: clampTemperature(bot.temperature ?? 0.2),
top_p: bot.top_p ?? 1, top_p: bot.top_p ?? 1,
@ -193,10 +193,14 @@ export function useDashboardBotEditor({
notify(t.providerRequired, { tone: 'warning' }); notify(t.providerRequired, { tone: 'warning' });
return; return;
} }
const targetBotId = String(selectedBot?.id || selectedBotId || '').trim();
const endpoint = targetBotId
? `${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetBotId)}/providers/test`
: `${APP_ENDPOINTS.apiBase}/providers/test`;
setIsTestingProvider(true); setIsTestingProvider(true);
setProviderTestResult(''); setProviderTestResult('');
try { try {
const res = await axios.post(`${APP_ENDPOINTS.apiBase}/providers/test`, { const res = await axios.post(endpoint, {
provider: editForm.llm_provider, provider: editForm.llm_provider,
model: editForm.llm_model, model: editForm.llm_model,
api_key: editForm.api_key.trim(), api_key: editForm.api_key.trim(),
@ -214,7 +218,7 @@ export function useDashboardBotEditor({
} finally { } finally {
setIsTestingProvider(false); setIsTestingProvider(false);
} }
}, [editForm.api_base, editForm.api_key, editForm.llm_model, editForm.llm_provider, notify, t]); }, [editForm.api_base, editForm.api_key, editForm.llm_model, editForm.llm_provider, notify, selectedBot?.id, selectedBotId, t]);
const saveBot = useCallback(async (mode: 'params' | 'agent' | 'base') => { const saveBot = useCallback(async (mode: 'params' | 'agent' | 'base') => {
const targetBotId = String(selectedBot?.id || selectedBotId || '').trim(); const targetBotId = String(selectedBot?.id || selectedBotId || '').trim();

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState, type Dispatch, type SetStateAction } from 'react'; import { useCallback, useState, type Dispatch, type SetStateAction } from 'react';
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
@ -42,7 +42,6 @@ interface UseDashboardChatCommandDispatchOptions {
selectedBot?: { id: string } | null; selectedBot?: { id: string } | null;
canChat: boolean; canChat: boolean;
isTaskRunningExternally: boolean; isTaskRunningExternally: boolean;
commandAutoUnlockSeconds: number;
command: string; command: string;
pendingAttachments: string[]; pendingAttachments: string[];
quotedReply: QuotedReply | null; quotedReply: QuotedReply | null;
@ -62,7 +61,6 @@ export function useDashboardChatCommandDispatch({
selectedBot, selectedBot,
canChat, canChat,
isTaskRunningExternally, isTaskRunningExternally,
commandAutoUnlockSeconds,
command, command,
pendingAttachments, pendingAttachments,
quotedReply, quotedReply,
@ -78,40 +76,15 @@ export function useDashboardChatCommandDispatch({
t, t,
}: UseDashboardChatCommandDispatchOptions) { }: UseDashboardChatCommandDispatchOptions) {
const [sendingByBot, setSendingByBot] = useState<Record<string, number>>({}); const [sendingByBot, setSendingByBot] = useState<Record<string, number>>({});
const [commandAutoUnlockDeadlineByBot, setCommandAutoUnlockDeadlineByBot] = useState<Record<string, number>>({});
const [interruptingByBot, setInterruptingByBot] = useState<Record<string, boolean>>({}); const [interruptingByBot, setInterruptingByBot] = useState<Record<string, boolean>>({});
const [controlCommandByBot, setControlCommandByBot] = useState<Record<string, string>>({}); const [controlCommandByBot, setControlCommandByBot] = useState<Record<string, string>>({});
const selectedBotSendingCount = selectedBot ? Number(sendingByBot[selectedBot.id] || 0) : 0; const selectedBotSendingCount = selectedBot ? Number(sendingByBot[selectedBot.id] || 0) : 0;
const selectedBotAutoUnlockDeadline = selectedBot ? Number(commandAutoUnlockDeadlineByBot[selectedBot.id] || 0) : 0;
const activeControlCommand = selectedBot ? controlCommandByBot[selectedBot.id] || '' : ''; const activeControlCommand = selectedBot ? controlCommandByBot[selectedBot.id] || '' : '';
const isSending = selectedBotSendingCount > 0; const isSending = selectedBotSendingCount > 0;
const isTaskRunning = Boolean(selectedBot && (isSending || isTaskRunningExternally)); const isTaskRunning = Boolean(selectedBot && (isSending || isTaskRunningExternally));
const isCommandAutoUnlockWindowActive = selectedBotAutoUnlockDeadline > Date.now();
const isSendingBlocked = isSending && isCommandAutoUnlockWindowActive;
const isInterrupting = Boolean(selectedBot && interruptingByBot[selectedBot.id]); const isInterrupting = Boolean(selectedBot && interruptingByBot[selectedBot.id]);
useEffect(() => {
if (!selectedBot?.id || selectedBotAutoUnlockDeadline <= 0) return;
const remaining = selectedBotAutoUnlockDeadline - Date.now();
if (remaining <= 0) {
setCommandAutoUnlockDeadlineByBot((prev) => {
const next = { ...prev };
delete next[selectedBot.id];
return next;
});
return;
}
const timer = window.setTimeout(() => {
setCommandAutoUnlockDeadlineByBot((prev) => {
const next = { ...prev };
delete next[selectedBot.id];
return next;
});
}, remaining + 20);
return () => window.clearTimeout(timer);
}, [selectedBot?.id, selectedBotAutoUnlockDeadline]);
const submitChatPayload = useCallback(async ({ const submitChatPayload = useCallback(async ({
commandRaw, commandRaw,
attachmentsRaw, attachmentsRaw,
@ -137,10 +110,6 @@ export function useDashboardChatCommandDispatch({
try { try {
requestAnimationFrame(() => scrollConversationToBottom('auto')); requestAnimationFrame(() => scrollConversationToBottom('auto'));
setSendingByBot((prev) => ({ ...prev, [selectedBot.id]: Number(prev[selectedBot.id] || 0) + 1 })); setSendingByBot((prev) => ({ ...prev, [selectedBot.id]: Number(prev[selectedBot.id] || 0) + 1 }));
setCommandAutoUnlockDeadlineByBot((prev) => ({
...prev,
[selectedBot.id]: Date.now() + (commandAutoUnlockSeconds * 1000),
}));
const res = await axios.post<CommandResponse>( const res = await axios.post<CommandResponse>(
`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/command`, `${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/command`,
{ command: payloadText, attachments }, { command: payloadText, attachments },
@ -168,11 +137,6 @@ export function useDashboardChatCommandDispatch({
return true; return true;
} catch (error: unknown) { } catch (error: unknown) {
const msg = resolveApiErrorMessage(error, t.sendFail); const msg = resolveApiErrorMessage(error, t.sendFail);
setCommandAutoUnlockDeadlineByBot((prev) => {
const next = { ...prev };
delete next[selectedBot.id];
return next;
});
addBotMessage(selectedBot.id, { addBotMessage(selectedBot.id, {
role: 'assistant', role: 'assistant',
text: t.sendFailMsg(msg), text: t.sendFailMsg(msg),
@ -196,7 +160,6 @@ export function useDashboardChatCommandDispatch({
}, [ }, [
addBotMessage, addBotMessage,
canChat, canChat,
commandAutoUnlockSeconds,
completeLeadingStagedSubmission, completeLeadingStagedSubmission,
notify, notify,
scrollConversationToBottom, scrollConversationToBottom,
@ -302,7 +265,6 @@ export function useDashboardChatCommandDispatch({
interruptExecution, interruptExecution,
isInterrupting, isInterrupting,
isSending, isSending,
isSendingBlocked,
isTaskRunning, isTaskRunning,
sendControlCommand, sendControlCommand,
sendCurrentDraft, sendCurrentDraft,

View File

@ -16,7 +16,6 @@ interface UseDashboardChatComposerOptions {
selectedBot?: { id: string } | null; selectedBot?: { id: string } | null;
canChat: boolean; canChat: boolean;
isTaskRunningExternally: boolean; isTaskRunningExternally: boolean;
commandAutoUnlockSeconds: number;
pendingAttachments: string[]; pendingAttachments: string[];
setPendingAttachments: Dispatch<SetStateAction<string[]>>; setPendingAttachments: Dispatch<SetStateAction<string[]>>;
isUploadingAttachments: boolean; isUploadingAttachments: boolean;
@ -41,7 +40,6 @@ export function useDashboardChatComposer({
selectedBot, selectedBot,
canChat, canChat,
isTaskRunningExternally, isTaskRunningExternally,
commandAutoUnlockSeconds,
pendingAttachments, pendingAttachments,
setPendingAttachments, setPendingAttachments,
isUploadingAttachments, isUploadingAttachments,
@ -111,7 +109,6 @@ export function useDashboardChatComposer({
interruptExecution, interruptExecution,
isInterrupting, isInterrupting,
isSending, isSending,
isSendingBlocked,
isTaskRunning, isTaskRunning,
sendControlCommand, sendControlCommand,
sendCurrentDraft, sendCurrentDraft,
@ -120,7 +117,6 @@ export function useDashboardChatComposer({
selectedBot, selectedBot,
canChat, canChat,
isTaskRunningExternally, isTaskRunningExternally,
commandAutoUnlockSeconds,
command, command,
pendingAttachments, pendingAttachments,
quotedReply, quotedReply,
@ -281,7 +277,6 @@ export function useDashboardChatComposer({
interruptExecution, interruptExecution,
isInterrupting, isInterrupting,
isSending, isSending,
isSendingBlocked,
onComposerKeyDown, onComposerKeyDown,
quoteAssistantReply, quoteAssistantReply,
quotedReply, quotedReply,

View File

@ -14,7 +14,6 @@ interface UseDashboardConversationOptions {
canChat: boolean; canChat: boolean;
isTaskRunningExternally: boolean; isTaskRunningExternally: boolean;
chatPullPageSize: number; chatPullPageSize: number;
commandAutoUnlockSeconds: number;
pendingAttachments: string[]; pendingAttachments: string[];
setPendingAttachments: Dispatch<SetStateAction<string[]>>; setPendingAttachments: Dispatch<SetStateAction<string[]>>;
isUploadingAttachments: boolean; isUploadingAttachments: boolean;
@ -50,7 +49,6 @@ export function useDashboardConversation(options: UseDashboardConversationOption
selectedBot: options.selectedBot, selectedBot: options.selectedBot,
canChat: options.canChat, canChat: options.canChat,
isTaskRunningExternally: options.isTaskRunningExternally, isTaskRunningExternally: options.isTaskRunningExternally,
commandAutoUnlockSeconds: options.commandAutoUnlockSeconds,
pendingAttachments: options.pendingAttachments, pendingAttachments: options.pendingAttachments,
setPendingAttachments: options.setPendingAttachments, setPendingAttachments: options.setPendingAttachments,
isUploadingAttachments: options.isUploadingAttachments, isUploadingAttachments: options.isUploadingAttachments,

View File

@ -30,7 +30,6 @@ interface UseDashboardBaseStateOptions {
interface UseDashboardInteractionStateOptions { interface UseDashboardInteractionStateOptions {
canChat: boolean; canChat: boolean;
isSendingBlocked?: boolean;
isVoiceRecording?: boolean; isVoiceRecording?: boolean;
isVoiceTranscribing?: boolean; isVoiceTranscribing?: boolean;
selectedBot?: BotState; selectedBot?: BotState;
@ -227,12 +226,11 @@ export function useDashboardBaseState({
export function useDashboardInteractionState({ export function useDashboardInteractionState({
canChat, canChat,
isSendingBlocked = false,
isVoiceRecording = false, isVoiceRecording = false,
isVoiceTranscribing = false, isVoiceTranscribing = false,
selectedBot, selectedBot,
}: UseDashboardInteractionStateOptions) { }: UseDashboardInteractionStateOptions) {
const isChatEnabled = Boolean(canChat && !isSendingBlocked); const isChatEnabled = Boolean(canChat);
const canSendControlCommand = Boolean( const canSendControlCommand = Boolean(
selectedBot && canChat && !isVoiceRecording && !isVoiceTranscribing, selectedBot && canChat && !isVoiceRecording && !isVoiceTranscribing,
); );

View File

@ -71,15 +71,19 @@ export function useDashboardRuntimeControl({
); );
const loadImageOptions = useCallback(async () => { const loadImageOptions = useCallback(async () => {
const targetBotId = String(selectedBotId || forcedBotId || '').trim();
const endpoint = targetBotId
? `${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetBotId)}/images`
: `${APP_ENDPOINTS.apiBase}/images`;
const [imagesRes] = await Promise.allSettled([ const [imagesRes] = await Promise.allSettled([
axios.get<NanobotImage[]>(`${APP_ENDPOINTS.apiBase}/images`), axios.get<NanobotImage[]>(endpoint),
]); ]);
if (imagesRes.status === 'fulfilled') { if (imagesRes.status === 'fulfilled') {
setAvailableImages(Array.isArray(imagesRes.value.data) ? imagesRes.value.data : []); setAvailableImages(Array.isArray(imagesRes.value.data) ? imagesRes.value.data : []);
} else { } else {
setAvailableImages([]); setAvailableImages([]);
} }
}, []); }, [forcedBotId, selectedBotId]);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const forced = String(forcedBotId || '').trim(); const forced = String(forcedBotId || '').trim();

View File

@ -12,7 +12,6 @@ interface UseDashboardSystemDefaultsOptions {
notify: (message: string, options?: { tone?: 'error' | 'success' | 'warning' | 'info' }) => void; notify: (message: string, options?: { tone?: 'error' | 'success' | 'warning' | 'info' }) => void;
setBotListPageSize: Dispatch<SetStateAction<number>>; setBotListPageSize: Dispatch<SetStateAction<number>>;
setChatPullPageSize?: Dispatch<SetStateAction<number>>; setChatPullPageSize?: Dispatch<SetStateAction<number>>;
setCommandAutoUnlockSeconds?: Dispatch<SetStateAction<number>>;
setVoiceCountdown?: Dispatch<SetStateAction<number>>; setVoiceCountdown?: Dispatch<SetStateAction<number>>;
} }
@ -34,18 +33,11 @@ function resolveChatPullPageSize(raw: unknown) {
return Math.max(10, Math.min(500, Math.floor(configured))); return Math.max(10, Math.min(500, Math.floor(configured)));
} }
function resolveCommandAutoUnlockSeconds(raw: unknown) {
const configured = Number(raw);
if (!Number.isFinite(configured) || configured <= 0) return 10;
return Math.max(1, Math.min(600, Math.floor(configured)));
}
export function useDashboardSystemDefaults({ export function useDashboardSystemDefaults({
isZh, isZh,
notify, notify,
setBotListPageSize, setBotListPageSize,
setChatPullPageSize, setChatPullPageSize,
setCommandAutoUnlockSeconds,
setVoiceCountdown, setVoiceCountdown,
}: UseDashboardSystemDefaultsOptions) { }: UseDashboardSystemDefaultsOptions) {
const [botListPageSizeReady, setBotListPageSizeReady] = useState(false); const [botListPageSizeReady, setBotListPageSizeReady] = useState(false);
@ -66,7 +58,6 @@ export function useDashboardSystemDefaults({
setBotListPageSize(normalizePlatformPageSize(data?.chat?.page_size, 10)); setBotListPageSize(normalizePlatformPageSize(data?.chat?.page_size, 10));
setChatPullPageSize?.(resolveChatPullPageSize(data?.chat?.pull_page_size)); setChatPullPageSize?.(resolveChatPullPageSize(data?.chat?.pull_page_size));
setCommandAutoUnlockSeconds?.(resolveCommandAutoUnlockSeconds(data?.chat?.command_auto_unlock_seconds));
setAllowedAttachmentExtensions( setAllowedAttachmentExtensions(
parseAllowedAttachmentExtensions(data?.workspace?.allowed_attachment_extensions), parseAllowedAttachmentExtensions(data?.workspace?.allowed_attachment_extensions),
); );
@ -85,7 +76,7 @@ export function useDashboardSystemDefaults({
setVoiceMaxSeconds(nextVoiceMaxSeconds); setVoiceMaxSeconds(nextVoiceMaxSeconds);
setVoiceCountdown?.(nextVoiceMaxSeconds); setVoiceCountdown?.(nextVoiceMaxSeconds);
} }
}, [setBotListPageSize, setChatPullPageSize, setCommandAutoUnlockSeconds, setVoiceCountdown]); }, [setBotListPageSize, setChatPullPageSize, setVoiceCountdown]);
useEffect(() => { useEffect(() => {
let alive = true; let alive = true;

View File

@ -280,7 +280,6 @@ export interface SystemDefaultsResponse {
chat?: { chat?: {
page_size?: number; page_size?: number;
pull_page_size?: number; pull_page_size?: number;
command_auto_unlock_seconds?: number;
}; };
topic_presets?: unknown; topic_presets?: unknown;
speech?: { speech?: {

View File

@ -1,7 +1,6 @@
export interface PlatformSettings { export interface PlatformSettings {
page_size: number; page_size: number;
chat_pull_page_size: number; chat_pull_page_size: number;
command_auto_unlock_seconds: number;
auth_token_ttl_hours: number; auth_token_ttl_hours: number;
auth_token_max_active: number; auth_token_max_active: number;
upload_max_mb: number; upload_max_mb: number;

View File

@ -71,6 +71,7 @@ export const useAppStore = create<AppStore>((set) => ({
image_tag: preferDefined(bot.image_tag, prev?.image_tag), image_tag: preferDefined(bot.image_tag, prev?.image_tag),
llm_provider: preferDefined(bot.llm_provider, prev?.llm_provider), llm_provider: preferDefined(bot.llm_provider, prev?.llm_provider),
llm_model: preferDefined(bot.llm_model, prev?.llm_model), llm_model: preferDefined(bot.llm_model, prev?.llm_model),
api_key: preferDefined(bot.api_key, prev?.api_key),
api_base: preferDefined(bot.api_base, prev?.api_base), api_base: preferDefined(bot.api_base, prev?.api_base),
temperature: preferDefined(bot.temperature, prev?.temperature), temperature: preferDefined(bot.temperature, prev?.temperature),
top_p: preferDefined(bot.top_p, prev?.top_p), top_p: preferDefined(bot.top_p, prev?.top_p),
@ -108,6 +109,7 @@ export const useAppStore = create<AppStore>((set) => ({
enabled: preferDefined(bot.enabled, prev?.enabled), enabled: preferDefined(bot.enabled, prev?.enabled),
created_at: preferDefined(bot.created_at, prev?.created_at), created_at: preferDefined(bot.created_at, prev?.created_at),
updated_at: preferDefined(bot.updated_at, prev?.updated_at), updated_at: preferDefined(bot.updated_at, prev?.updated_at),
api_key: preferDefined(bot.api_key, prev?.api_key),
logs: prev?.logs ?? bot.logs ?? [], logs: prev?.logs ?? bot.logs ?? [],
messages: prev?.messages ?? bot.messages ?? [], messages: prev?.messages ?? bot.messages ?? [],
events: prev?.events ?? bot.events ?? [], events: prev?.events ?? bot.events ?? [],

View File

@ -28,6 +28,7 @@ export interface BotState {
image_tag?: string; image_tag?: string;
llm_provider?: string; llm_provider?: string;
llm_model?: string; llm_model?: string;
api_key?: string;
api_base?: string; api_base?: string;
temperature?: number; temperature?: number;
top_p?: number; top_p?: number;

View File

@ -150,7 +150,6 @@ docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T \
PAGE_SIZE_JSON="10" PAGE_SIZE_JSON="10"
CHAT_PULL_PAGE_SIZE_JSON="60" CHAT_PULL_PAGE_SIZE_JSON="60"
COMMAND_AUTO_UNLOCK_SECONDS_JSON="10"
AUTH_TOKEN_TTL_HOURS_JSON="24" AUTH_TOKEN_TTL_HOURS_JSON="24"
AUTH_TOKEN_MAX_ACTIVE_JSON="2" AUTH_TOKEN_MAX_ACTIVE_JSON="2"
UPLOAD_MAX_MB_JSON="$UPLOAD_MAX_MB" UPLOAD_MAX_MB_JSON="$UPLOAD_MAX_MB"
@ -171,7 +170,6 @@ docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T \
-v ON_ERROR_STOP=1 \ -v ON_ERROR_STOP=1 \
-v page_size_json="$PAGE_SIZE_JSON" \ -v page_size_json="$PAGE_SIZE_JSON" \
-v chat_pull_page_size_json="$CHAT_PULL_PAGE_SIZE_JSON" \ -v chat_pull_page_size_json="$CHAT_PULL_PAGE_SIZE_JSON" \
-v command_auto_unlock_seconds_json="$COMMAND_AUTO_UNLOCK_SECONDS_JSON" \
-v auth_token_ttl_hours_json="$AUTH_TOKEN_TTL_HOURS_JSON" \ -v auth_token_ttl_hours_json="$AUTH_TOKEN_TTL_HOURS_JSON" \
-v auth_token_max_active_json="$AUTH_TOKEN_MAX_ACTIVE_JSON" \ -v auth_token_max_active_json="$AUTH_TOKEN_MAX_ACTIVE_JSON" \
-v upload_max_mb_json="$UPLOAD_MAX_MB_JSON" \ -v upload_max_mb_json="$UPLOAD_MAX_MB_JSON" \

View File

@ -17,7 +17,6 @@ INSERT INTO sys_setting (
VALUES VALUES
('page_size', '分页大小', 'ui', '平台各类列表默认每页条数。', 'integer', :'page_size_json', TRUE, 5, NOW(), NOW()), ('page_size', '分页大小', 'ui', '平台各类列表默认每页条数。', 'integer', :'page_size_json', TRUE, 5, NOW(), NOW()),
('chat_pull_page_size', '对话懒加载条数', 'chat', 'Bot 对话区向上懒加载时每次读取的消息条数。', 'integer', :'chat_pull_page_size_json', TRUE, 8, NOW(), NOW()), ('chat_pull_page_size', '对话懒加载条数', 'chat', 'Bot 对话区向上懒加载时每次读取的消息条数。', 'integer', :'chat_pull_page_size_json', TRUE, 8, NOW(), NOW()),
('command_auto_unlock_seconds', '发送按钮自动恢复秒数', 'chat', '对话发送后按钮保持停止态的最长秒数,超时后自动恢复为可发送状态。', 'integer', :'command_auto_unlock_seconds_json', TRUE, 9, NOW(), NOW()),
('auth_token_ttl_hours', '认证 Token 过期小时数', 'auth', 'Panel 与 Bot 登录 Token 的统一有效时长,单位小时。', 'integer', :'auth_token_ttl_hours_json', FALSE, 10, NOW(), NOW()), ('auth_token_ttl_hours', '认证 Token 过期小时数', 'auth', 'Panel 与 Bot 登录 Token 的统一有效时长,单位小时。', 'integer', :'auth_token_ttl_hours_json', FALSE, 10, NOW(), NOW()),
('auth_token_max_active', '认证 Token 最大并发数', 'auth', '同一主体允许同时活跃的 Token 数量,超过时自动撤销最旧 Token。', 'integer', :'auth_token_max_active_json', FALSE, 11, NOW(), NOW()), ('auth_token_max_active', '认证 Token 最大并发数', 'auth', '同一主体允许同时活跃的 Token 数量,超过时自动撤销最旧 Token。', 'integer', :'auth_token_max_active_json', FALSE, 11, NOW(), NOW()),
('upload_max_mb', '上传大小限制', 'upload', '单文件上传大小限制,单位 MB。', 'integer', :'upload_max_mb_json', FALSE, 20, NOW(), NOW()), ('upload_max_mb', '上传大小限制', 'upload', '单文件上传大小限制,单位 MB。', 'integer', :'upload_max_mb_json', FALSE, 20, NOW(), NOW()),
@ -35,6 +34,8 @@ SET
sort_order = EXCLUDED.sort_order, sort_order = EXCLUDED.sort_order,
updated_at = NOW(); updated_at = NOW();
DELETE FROM sys_setting WHERE key = 'command_auto_unlock_seconds';
INSERT INTO skill_market_item ( INSERT INTO skill_market_item (
skill_key, skill_key,
display_name, display_name,