Merge origin/main into main

main
AlanPaine 2026-04-17 17:10:47 +08:00
commit 73b9afa037
23 changed files with 53 additions and 92 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 / 平台参数:
# - page_size
# - chat_pull_page_size
# - command_auto_unlock_seconds
# - upload_max_mb
# - allowed_attachment_extensions
# - workspace_download_extensions

View File

@ -18,6 +18,7 @@ from services.bot_management_service import (
list_bots_with_cache,
update_bot_record,
)
from services.image_service import list_registered_images
from services.provider_service import test_provider_connection
router = APIRouter()
@ -28,6 +29,22 @@ async def test_provider(payload: dict):
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")
def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session)):
return create_bot_record(session, payload=payload)

View File

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

View File

@ -45,7 +45,6 @@ REQUIRED_TABLES = (
REQUIRED_SYS_SETTING_KEYS = (
"page_size",
"chat_pull_page_size",
"command_auto_unlock_seconds",
"auth_token_ttl_hours",
"auth_token_max_active",
"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_PAGE_SIZE: Final[int] = 10
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_MAX_ACTIVE: Final[int] = _env_int("AUTH_TOKEN_MAX_ACTIVE", 2, 1, 20)
DEFAULT_BOT_SYSTEM_TIMEZONE: Final[str] = str(

View File

@ -6,7 +6,6 @@ from pydantic import BaseModel, Field
class PlatformSettingsPayload(BaseModel):
page_size: int = Field(default=10, ge=1, le=100)
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_max_active: int = Field(default=2, ge=1, le=20)
upload_max_mb: int = Field(default=100, ge=1, le=2048)

View File

@ -9,7 +9,6 @@ from core.settings import (
DEFAULT_AUTH_TOKEN_MAX_ACTIVE,
DEFAULT_AUTH_TOKEN_TTL_HOURS,
DEFAULT_CHAT_PULL_PAGE_SIZE,
DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
DEFAULT_PAGE_SIZE,
DEFAULT_UPLOAD_MAX_MB,
DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS,
@ -24,7 +23,6 @@ ACTIVITY_EVENT_RETENTION_SETTING_KEY = "activity_event_retention_days"
SETTING_KEYS = (
"page_size",
"chat_pull_page_size",
"command_auto_unlock_seconds",
"auth_token_ttl_hours",
"auth_token_max_active",
"upload_max_mb",
@ -52,15 +50,6 @@ SYSTEM_SETTING_DEFINITIONS: Dict[str, Dict[str, Any]] = {
"is_public": True,
"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": {
"name": "认证 Token 过期小时数",
"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"]))),
"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_max_active": max(1, min(20, int(data["auth_token_max_active"]))),
"upload_max_mb": int(data["upload_max_mb"]),
@ -70,7 +69,6 @@ def save_platform_settings(session: Session, payload: PlatformSettingsPayload) -
normalized = PlatformSettingsPayload(
page_size=max(1, min(100, int(payload.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_max_active=max(1, min(20, int(payload.auth_token_max_active))),
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())
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:
_prune_deprecated_system_settings(session)
stmt = select(PlatformSetting.key).where(PlatformSetting.key.in_(REQUIRED_SYSTEM_SETTING_KEYS))
present = {
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]:
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, {})
row = _upsert_setting_row(
session,

View File

@ -266,7 +266,7 @@ export const dashboardZhCn = {
modelName: '模型名称',
modelNamePlaceholder: '如 qwen-plus',
newApiKey: 'API Key',
newApiKeyPlaceholder: '输入 API Key',
newApiKeyPlaceholder: '输入 API Key',
testing: '测试中...',
testModelConnection: '测试模型连接',
cancel: '取消',

View File

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

View File

@ -193,10 +193,14 @@ export function useDashboardBotEditor({
notify(t.providerRequired, { tone: 'warning' });
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);
setProviderTestResult('');
try {
const res = await axios.post(`${APP_ENDPOINTS.apiBase}/providers/test`, {
const res = await axios.post(endpoint, {
provider: editForm.llm_provider,
model: editForm.llm_model,
api_key: editForm.api_key.trim(),
@ -214,7 +218,7 @@ export function useDashboardBotEditor({
} finally {
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 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 { APP_ENDPOINTS } from '../../../config/env';
@ -42,7 +42,6 @@ interface UseDashboardChatCommandDispatchOptions {
selectedBot?: { id: string } | null;
canChat: boolean;
isTaskRunningExternally: boolean;
commandAutoUnlockSeconds: number;
command: string;
pendingAttachments: string[];
quotedReply: QuotedReply | null;
@ -62,7 +61,6 @@ export function useDashboardChatCommandDispatch({
selectedBot,
canChat,
isTaskRunningExternally,
commandAutoUnlockSeconds,
command,
pendingAttachments,
quotedReply,
@ -78,40 +76,15 @@ export function useDashboardChatCommandDispatch({
t,
}: UseDashboardChatCommandDispatchOptions) {
const [sendingByBot, setSendingByBot] = useState<Record<string, number>>({});
const [commandAutoUnlockDeadlineByBot, setCommandAutoUnlockDeadlineByBot] = useState<Record<string, number>>({});
const [interruptingByBot, setInterruptingByBot] = useState<Record<string, boolean>>({});
const [controlCommandByBot, setControlCommandByBot] = useState<Record<string, string>>({});
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 isSending = selectedBotSendingCount > 0;
const isTaskRunning = Boolean(selectedBot && (isSending || isTaskRunningExternally));
const isCommandAutoUnlockWindowActive = selectedBotAutoUnlockDeadline > Date.now();
const isSendingBlocked = isSending && isCommandAutoUnlockWindowActive;
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 ({
commandRaw,
attachmentsRaw,
@ -137,10 +110,6 @@ export function useDashboardChatCommandDispatch({
try {
requestAnimationFrame(() => scrollConversationToBottom('auto'));
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>(
`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/command`,
{ command: payloadText, attachments },
@ -168,11 +137,6 @@ export function useDashboardChatCommandDispatch({
return true;
} catch (error: unknown) {
const msg = resolveApiErrorMessage(error, t.sendFail);
setCommandAutoUnlockDeadlineByBot((prev) => {
const next = { ...prev };
delete next[selectedBot.id];
return next;
});
addBotMessage(selectedBot.id, {
role: 'assistant',
text: t.sendFailMsg(msg),
@ -196,7 +160,6 @@ export function useDashboardChatCommandDispatch({
}, [
addBotMessage,
canChat,
commandAutoUnlockSeconds,
completeLeadingStagedSubmission,
notify,
scrollConversationToBottom,
@ -302,7 +265,6 @@ export function useDashboardChatCommandDispatch({
interruptExecution,
isInterrupting,
isSending,
isSendingBlocked,
isTaskRunning,
sendControlCommand,
sendCurrentDraft,

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ interface UseDashboardSystemDefaultsOptions {
notify: (message: string, options?: { tone?: 'error' | 'success' | 'warning' | 'info' }) => void;
setBotListPageSize: Dispatch<SetStateAction<number>>;
setChatPullPageSize?: Dispatch<SetStateAction<number>>;
setCommandAutoUnlockSeconds?: 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)));
}
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({
isZh,
notify,
setBotListPageSize,
setChatPullPageSize,
setCommandAutoUnlockSeconds,
setVoiceCountdown,
}: UseDashboardSystemDefaultsOptions) {
const [botListPageSizeReady, setBotListPageSizeReady] = useState(false);
@ -66,7 +58,6 @@ export function useDashboardSystemDefaults({
setBotListPageSize(normalizePlatformPageSize(data?.chat?.page_size, 10));
setChatPullPageSize?.(resolveChatPullPageSize(data?.chat?.pull_page_size));
setCommandAutoUnlockSeconds?.(resolveCommandAutoUnlockSeconds(data?.chat?.command_auto_unlock_seconds));
setAllowedAttachmentExtensions(
parseAllowedAttachmentExtensions(data?.workspace?.allowed_attachment_extensions),
);
@ -85,7 +76,7 @@ export function useDashboardSystemDefaults({
setVoiceMaxSeconds(nextVoiceMaxSeconds);
setVoiceCountdown?.(nextVoiceMaxSeconds);
}
}, [setBotListPageSize, setChatPullPageSize, setCommandAutoUnlockSeconds, setVoiceCountdown]);
}, [setBotListPageSize, setChatPullPageSize, setVoiceCountdown]);
useEffect(() => {
let alive = true;

View File

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

View File

@ -1,7 +1,6 @@
export interface PlatformSettings {
page_size: number;
chat_pull_page_size: number;
command_auto_unlock_seconds: number;
auth_token_ttl_hours: number;
auth_token_max_active: 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),
llm_provider: preferDefined(bot.llm_provider, prev?.llm_provider),
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),
temperature: preferDefined(bot.temperature, prev?.temperature),
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),
created_at: preferDefined(bot.created_at, prev?.created_at),
updated_at: preferDefined(bot.updated_at, prev?.updated_at),
api_key: preferDefined(bot.api_key, prev?.api_key),
logs: prev?.logs ?? bot.logs ?? [],
messages: prev?.messages ?? bot.messages ?? [],
events: prev?.events ?? bot.events ?? [],

View File

@ -150,7 +150,6 @@ docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T \
PAGE_SIZE_JSON="10"
CHAT_PULL_PAGE_SIZE_JSON="60"
COMMAND_AUTO_UNLOCK_SECONDS_JSON="10"
AUTH_TOKEN_TTL_HOURS_JSON="24"
AUTH_TOKEN_MAX_ACTIVE_JSON="2"
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 page_size_json="$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_max_active_json="$AUTH_TOKEN_MAX_ACTIVE_JSON" \
-v upload_max_mb_json="$UPLOAD_MAX_MB_JSON" \

View File

@ -17,7 +17,6 @@ INSERT INTO sys_setting (
VALUES
('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()),
('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_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()),
@ -35,6 +34,8 @@ SET
sort_order = EXCLUDED.sort_order,
updated_at = NOW();
DELETE FROM sys_setting WHERE key = 'command_auto_unlock_seconds';
INSERT INTO skill_market_item (
skill_key,
display_name,