diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py
index d4174ffb58..986570ac34 100644
--- a/astrbot/dashboard/routes/chat.py
+++ b/astrbot/dashboard/routes/chat.py
@@ -766,6 +766,44 @@ async def stream():
message_accumulator = BotMessageAccumulator()
agent_stats = {}
refs = {}
+
+ async def flush_pending_bot_message():
+ nonlocal message_accumulator, agent_stats, refs
+ if not (message_accumulator.has_content() or refs or agent_stats):
+ return None
+
+ message_parts_to_save = message_accumulator.build_message_parts(
+ include_pending_tool_calls=True
+ )
+ plain_text = collect_plain_text_from_message_parts(
+ message_parts_to_save
+ )
+
+ try:
+ extracted_refs = self._extract_web_search_refs(
+ plain_text,
+ message_parts_to_save,
+ )
+ except Exception as e:
+ logger.exception(
+ f"Failed to extract web search refs: {e}",
+ exc_info=True,
+ )
+ extracted_refs = refs
+
+ saved_record = await self._save_bot_message(
+ webchat_conv_id,
+ message_parts_to_save,
+ agent_stats,
+ extracted_refs,
+ llm_checkpoint_id,
+ platform_history_id,
+ )
+ message_accumulator = BotMessageAccumulator()
+ agent_stats = {}
+ refs = {}
+ return saved_record
+
try:
# Emit session_id first so clients can bind the stream immediately.
session_info = {
@@ -885,35 +923,7 @@ async def stream():
should_save = True
if should_save:
- message_parts_to_save = (
- message_accumulator.build_message_parts(
- include_pending_tool_calls=True
- )
- )
- plain_text = collect_plain_text_from_message_parts(
- message_parts_to_save
- )
-
- # 提取 web_search_tavily 引用
- try:
- refs = self._extract_web_search_refs(
- plain_text,
- message_parts_to_save,
- )
- except Exception as e:
- logger.exception(
- f"Failed to extract web search refs: {e}",
- exc_info=True,
- )
-
- saved_record = await self._save_bot_message(
- webchat_conv_id,
- message_parts_to_save,
- agent_stats,
- refs,
- llm_checkpoint_id,
- platform_history_id,
- )
+ saved_record = await flush_pending_bot_message()
# 发送保存的消息信息给前端
if saved_record and not client_disconnected:
saved_info = {
@@ -930,15 +940,18 @@ async def stream():
yield f"data: {json.dumps(saved_info, ensure_ascii=False)}\n\n"
except Exception:
pass
- message_accumulator = BotMessageAccumulator()
- agent_stats = {}
- refs = {}
-
if msg_type == "end":
break
except BaseException as e:
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
finally:
+ try:
+ await flush_pending_bot_message()
+ except Exception as e:
+ logger.exception(
+ f"Failed to persist pending webchat message: {e}",
+ exc_info=True,
+ )
webchat_queue_mgr.remove_back_queue(message_id)
# 将消息放入会话特定的队列
diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py
index 8f4dc26fab..8fe113fcbe 100644
--- a/astrbot/dashboard/routes/live_chat.py
+++ b/astrbot/dashboard/routes/live_chat.py
@@ -453,6 +453,7 @@ async def _handle_chat_message(
llm_checkpoint_id = str(uuid.uuid4())
try:
+ pending_bot_message_flusher = None
chat_queue = webchat_queue_mgr.get_or_create_queue(session_id)
await chat_queue.put(
(
@@ -499,9 +500,47 @@ async def _handle_chat_message(
agent_stats = {}
refs = {}
+ async def flush_pending_bot_message():
+ nonlocal message_accumulator, agent_stats, refs
+ if not (message_accumulator.has_content() or refs or agent_stats):
+ return None
+
+ message_parts_to_save = message_accumulator.build_message_parts(
+ include_pending_tool_calls=True
+ )
+ plain_text = collect_plain_text_from_message_parts(
+ message_parts_to_save
+ )
+ try:
+ extracted_refs = self._extract_web_search_refs(
+ plain_text,
+ message_parts_to_save,
+ )
+ except Exception as e:
+ logger.exception(
+ f"[Live Chat] Failed to extract web search refs: {e}",
+ exc_info=True,
+ )
+ extracted_refs = refs
+
+ saved_record = await self._save_bot_message(
+ session_id,
+ message_parts_to_save,
+ agent_stats,
+ extracted_refs,
+ llm_checkpoint_id,
+ )
+ message_accumulator = BotMessageAccumulator()
+ agent_stats = {}
+ refs = {}
+ return saved_record
+
+ pending_bot_message_flusher = flush_pending_bot_message
+
while True:
if session.should_interrupt:
session.should_interrupt = False
+ await flush_pending_bot_message()
break
try:
@@ -574,30 +613,7 @@ async def _handle_chat_message(
should_save = True
if should_save:
- message_parts_to_save = message_accumulator.build_message_parts(
- include_pending_tool_calls=True
- )
- plain_text = collect_plain_text_from_message_parts(
- message_parts_to_save
- )
- try:
- refs = self._extract_web_search_refs(
- plain_text,
- message_parts_to_save,
- )
- except Exception as e:
- logger.exception(
- f"[Live Chat] Failed to extract web search refs: {e}",
- exc_info=True,
- )
-
- saved_record = await self._save_bot_message(
- session_id,
- message_parts_to_save,
- agent_stats,
- refs,
- llm_checkpoint_id,
- )
+ saved_record = await flush_pending_bot_message()
if saved_record:
await self._send_chat_payload(
session,
@@ -614,10 +630,6 @@ async def _handle_chat_message(
},
)
- message_accumulator = BotMessageAccumulator()
- agent_stats = {}
- refs = {}
-
if msg_type == "end":
break
@@ -633,6 +645,14 @@ async def _handle_chat_message(
},
)
finally:
+ try:
+ if pending_bot_message_flusher is not None:
+ await pending_bot_message_flusher()
+ except Exception as e:
+ logger.exception(
+ f"[Live Chat] Failed to persist pending chat message: {e}",
+ exc_info=True,
+ )
session.is_processing = False
webchat_queue_mgr.remove_back_queue(message_id)
diff --git a/dashboard/index.html b/dashboard/index.html
index 771f72affe..5ffa2c1178 100644
--- a/dashboard/index.html
+++ b/dashboard/index.html
@@ -9,7 +9,7 @@
diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css
index 54785df237..3e1ffbc326 100644
--- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css
+++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css
@@ -1,4 +1,4 @@
-/* Auto-generated MDI subset – 247 icons */
+/* Auto-generated MDI subset – 261 icons */
/* Do not edit manually. Run: pnpm run subset-icons */
@font-face {
@@ -236,6 +236,10 @@
content: "\F0167";
}
+.mdi-code-braces::before {
+ content: "\F0169";
+}
+
.mdi-code-json::before {
content: "\F0626";
}
@@ -324,6 +328,10 @@
content: "\F1634";
}
+.mdi-database-search-outline::before {
+ content: "\F1636";
+}
+
.mdi-delete::before {
content: "\F01B4";
}
@@ -396,6 +404,10 @@
content: "\F022E";
}
+.mdi-file-delimited-outline::before {
+ content: "\F0EA5";
+}
+
.mdi-file-document::before {
content: "\F0219";
}
@@ -416,6 +428,10 @@
content: "\F021C";
}
+.mdi-file-music-outline::before {
+ content: "\F0E2A";
+}
+
.mdi-file-outline::before {
content: "\F0224";
}
@@ -436,6 +452,10 @@
content: "\F0A4D";
}
+.mdi-file-video-outline::before {
+ content: "\F0E2C";
+}
+
.mdi-file-word-box::before {
content: "\F022D";
}
@@ -536,6 +556,10 @@
content: "\F0EFE";
}
+.mdi-image-outline::before {
+ content: "\F0976";
+}
+
.mdi-import::before {
content: "\F02FA";
}
@@ -564,10 +588,38 @@
content: "\F0315";
}
+.mdi-language-css3::before {
+ content: "\F031C";
+}
+
+.mdi-language-html5::before {
+ content: "\F031D";
+}
+
+.mdi-language-java::before {
+ content: "\F0B37";
+}
+
+.mdi-language-javascript::before {
+ content: "\F031E";
+}
+
.mdi-language-markdown::before {
content: "\F0354";
}
+.mdi-language-markdown-outline::before {
+ content: "\F0F5B";
+}
+
+.mdi-language-python::before {
+ content: "\F0320";
+}
+
+.mdi-language-typescript::before {
+ content: "\F06E6";
+}
+
.mdi-layers-outline::before {
content: "\F09FE";
}
@@ -688,6 +740,10 @@
content: "\F03D6";
}
+.mdi-package-variant-closed::before {
+ content: "\F03D7";
+}
+
.mdi-page-first::before {
content: "\F0600";
}
@@ -812,10 +868,6 @@
content: "\F167A";
}
-.mdi-send::before {
- content: "\F048A";
-}
-
.mdi-server::before {
content: "\F048B";
}
@@ -1004,6 +1056,10 @@
content: "\F05B7";
}
+.mdi-wrench-outline::before {
+ content: "\F0BE0";
+}
+
.mdi-zip-box::before {
content: "\F05C4";
}
diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff
index b5c8cefd74..02f37fd732 100644
Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff differ
diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2
index 74e05e5454..a26ce1678c 100644
Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 differ
diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue
index 1e465887d3..e5cae3da3b 100644
--- a/dashboard/src/components/chat/Chat.vue
+++ b/dashboard/src/components/chat/Chat.vue
@@ -1,5 +1,6 @@
@@ -34,6 +35,25 @@
+
+
-
-