diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index fd1a9aeb8c..e522ce5453 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -114,6 +114,8 @@ ) from astrbot.core.utils.string_utils import normalize_and_dedupe_strings +LLM_ERROR_MESSAGE_EXTRA_KEY = "_llm_error_message" + @dataclass(slots=True) class MainAgentBuildConfig: @@ -183,6 +185,10 @@ class MainAgentBuildResult: reset_coro: Coroutine | None = None +def _set_llm_error_message(event: AstrMessageEvent, message: str) -> None: + event.set_extra(LLM_ERROR_MESSAGE_EXTRA_KEY, message) + + def _select_provider( event: AstrMessageEvent, plugin_context: Context ) -> Provider | None: @@ -190,18 +196,28 @@ def _select_provider( sel_provider = event.get_extra("selected_provider") if sel_provider and isinstance(sel_provider, str): provider = plugin_context.get_provider_by_id(sel_provider) - if not provider: + if provider is None: logger.error("未找到指定的提供商: %s。", sel_provider) + _set_llm_error_message( + event, + f"LLM 请求失败:未找到指定的提供商 `{sel_provider}`。请检查提供商配置或重新选择可用模型。", + ) + return None if not isinstance(provider, Provider): logger.error( "选择的提供商类型无效(%s),跳过 LLM 请求处理。", type(provider) ) + _set_llm_error_message( + event, + f"LLM 请求失败:选择的提供商类型无效({type(provider).__name__}),已跳过本次请求。", + ) return None return provider try: return plugin_context.get_using_provider(umo=event.unified_msg_origin) except ValueError as exc: logger.error("Error occurred while selecting provider: %s", exc) + _set_llm_error_message(event, f"LLM 请求失败:{exc}") return None @@ -1192,6 +1208,11 @@ async def build_main_agent( provider = provider or _select_provider(event, plugin_context) if provider is None: logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。") + if not event.get_extra(LLM_ERROR_MESSAGE_EXTRA_KEY): + _set_llm_error_message( + event, + "LLM 请求失败:未找到任何可用的对话模型(提供商)。请先在 WebUI 中配置并启用可用模型。", + ) return None if req is None: diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py index fee641c192..2c200ec262 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py @@ -14,6 +14,7 @@ ) from astrbot.core.agent.response import AgentStats from astrbot.core.astr_main_agent import ( + LLM_ERROR_MESSAGE_EXTRA_KEY, MainAgentBuildConfig, MainAgentBuildResult, build_main_agent, @@ -151,6 +152,11 @@ async def initialize(self, ctx: PipelineContext) -> None: max_quoted_fallback_images=settings.get("max_quoted_fallback_images", 20), ) + async def _send_llm_error_message( + self, event: AstrMessageEvent, message: object + ) -> None: + await event.send(MessageChain().message(str(message))) + async def process( self, event: AstrMessageEvent, provider_wake_prefix: str ) -> AsyncGenerator[None, None]: @@ -219,6 +225,13 @@ async def process( ) if build_result is None: + if llm_error_message := event.get_extra( + LLM_ERROR_MESSAGE_EXTRA_KEY + ): + await self._send_llm_error_message( + event, + llm_error_message, + ) return agent_runner = build_result.agent_runner @@ -229,10 +242,12 @@ async def process( api_base = provider.provider_config.get("api_base", "") for host in decoded_blocked: if host in api_base: - logger.error( - "Provider API base %s is blocked due to security reasons. Please use another ai provider.", - api_base, + error_message = ( + f"LLM 请求失败:Provider API base `{api_base}` " + "因安全原因被拦截,请更换可用的 AI 提供商。" ) + logger.error(error_message) + await self._send_llm_error_message(event, error_message) return stream_to_general = ( diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index e5cae3da3b..7e88480f1c 100644 --- a/dashboard/src/components/chat/Chat.vue +++ b/dashboard/src/components/chat/Chat.vue @@ -841,6 +841,7 @@ async function startNewChat() { replyTarget.value = null; newChat(); closeMobileSidebar(); + await focusChatInput(); } function openCreateProjectDialog() { @@ -975,6 +976,7 @@ async function selectSession(sessionId: string, pushRoute = true) { } scrollToBottom(); closeMobileSidebar(); + await focusChatInput(); } async function sendCurrentMessage() { @@ -1032,6 +1034,7 @@ async function sendCurrentMessage() { console.error("Failed to send message:", error); } finally { sending.value = false; + await focusChatInput(); } } @@ -1326,6 +1329,13 @@ function scrollToBottom() { }); } +async function focusChatInput() { + await nextTick(); + window.requestAnimationFrame(() => { + inputRef.value?.focusInput(); + }); +} + async function stopCurrentSession() { if (!currSessionId.value) return; try { @@ -1487,6 +1497,9 @@ function toggleTheme() { align-items: center; gap: 8px; padding: 8px 12px; + padding-right: 68px; + position: relative; + box-sizing: border-box; cursor: pointer; text-align: left; } @@ -1511,15 +1524,24 @@ function toggleTheme() { } .session-actions { - display: none; + display: flex; align-items: center; gap: 2px; flex-shrink: 0; + opacity: 0; + pointer-events: none; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + visibility: hidden; } .session-item:hover .session-actions, .session-item:focus-within .session-actions { - display: flex; + opacity: 1; + pointer-events: auto; + visibility: visible; } .session-action-btn { diff --git a/dashboard/src/components/chat/ChatInput.vue b/dashboard/src/components/chat/ChatInput.vue index c966d13777..e4657c6f6c 100644 --- a/dashboard/src/components/chat/ChatInput.vue +++ b/dashboard/src/components/chat/ChatInput.vue @@ -108,14 +108,23 @@ +