feat: consolidate data dashboard and redesign conversations (#9846) · AstrBotDevs/AstrBot@6903085 · GitHub
Skip to content

Commit 6903085

Browse files
authored
feat: consolidate data dashboard and redesign conversations (#9846)
* feat: consolidate data dashboard * feat: redesign conversation workspace * docs: sync conversation OpenAPI artifacts * docs: update data workspace navigation * feat: group conversations by session * fix: optimize mobile conversation workspace * feat: refine log workspace * fix: honor console auto-scroll setting * feat: refine trace workspace
1 parent 8ea8ce6 commit 6903085

58 files changed

Lines changed: 4438 additions & 860 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎astrbot/core/db/__init__.py‎

Lines changed: 9 additions & 0 deletions

‎astrbot/core/db/sqlite.py‎

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99

1010
from deprecated import deprecated
11-
from sqlalchemy import CursorResult, Row, not_
11+
from sqlalchemy import CursorResult, Row, case, not_
1212
from sqlalchemy.dialects.sqlite import dialect as sqlite_dialect
1313
from sqlalchemy.ext.asyncio import AsyncSession
1414
from sqlalchemy.orm import defer
@@ -372,6 +372,19 @@ async def get_filtered_conversations(
372372
col(ConversationV2.content).ilike(f"%{escaped_search_query}%"),
373373
)
374374
)
375+
keyword_query = str(kwargs.get("keyword_query") or "").strip()
376+
if keyword_query:
377+
escaped_keyword_query = json.dumps(
378+
keyword_query,
379+
ensure_ascii=True,
380+
)[1:-1]
381+
conditions.append(
382+
or_(
383+
col(ConversationV2.title).ilike(f"%{keyword_query}%"),
384+
col(ConversationV2.content).ilike(f"%{keyword_query}%"),
385+
col(ConversationV2.content).ilike(f"%{escaped_keyword_query}%"),
386+
)
387+
)
375388
message_types = kwargs.get("message_types") or []
376389
if message_types:
377390
conditions.append(
@@ -395,28 +408,97 @@ async def get_filtered_conversations(
395408
conditions.append(
396409
not_(col(ConversationV2.platform_id).in_(exclude_platforms))
397410
)
411+
umo_query = str(kwargs.get("umo_query") or "").strip()
412+
if umo_query:
413+
conditions.append(col(ConversationV2.user_id).ilike(f"%{umo_query}%"))
398414

399415
if conditions:
400416
base_query = base_query.where(*conditions)
401417

418+
group_by_session = bool(kwargs.get("group_by_session", False))
419+
402420
# Get total count matching the filters
403-
count_query = select(func.count(ConversationV2.inner_conversation_id))
421+
count_target = (
422+
func.distinct(ConversationV2.user_id)
423+
if group_by_session
424+
else ConversationV2.inner_conversation_id
425+
)
426+
count_query = select(func.count(count_target))
404427
if conditions:
405428
count_query = count_query.where(*conditions)
406429
total_count = await session.execute(count_query)
407430
total = total_count.scalar_one()
408431

409432
# Get paginated results
410433
offset = (page - 1) * page_size
411-
result_query = (
412-
base_query.order_by(desc(ConversationV2.created_at))
413-
.order_by(desc(ConversationV2.inner_conversation_id))
414-
.offset(offset)
415-
.limit(page_size)
434+
sort_by = kwargs.get("sort_by", "created_at")
435+
sort_order = kwargs.get("sort_order", "desc")
436+
sort_column = (
437+
ConversationV2.updated_at
438+
if sort_by == "updated_at"
439+
else ConversationV2.created_at
416440
)
441+
order = sort_column.asc if sort_order == "asc" else sort_column.desc
442+
tie_breaker = (
443+
ConversationV2.inner_conversation_id.asc
444+
if sort_order == "asc"
445+
else ConversationV2.inner_conversation_id.desc
446+
)
447+
if group_by_session:
448+
session_sort = func.max(sort_column).label("session_sort")
449+
session_tie_breaker = func.max(
450+
ConversationV2.inner_conversation_id
451+
).label("session_tie_breaker")
452+
session_query = select(
453+
ConversationV2.user_id,
454+
session_sort,
455+
session_tie_breaker,
456+
)
457+
if conditions:
458+
session_query = session_query.where(*conditions)
459+
session_order = (
460+
session_sort.asc if sort_order == "asc" else session_sort.desc
461+
)
462+
session_tie_order = (
463+
session_tie_breaker.asc
464+
if sort_order == "asc"
465+
else session_tie_breaker.desc
466+
)
467+
session_rows = await session.execute(
468+
session_query.group_by(ConversationV2.user_id)
469+
.order_by(session_order())
470+
.order_by(session_tie_order())
471+
.offset(offset)
472+
.limit(page_size)
473+
)
474+
session_ids = [row[0] for row in session_rows.all()]
475+
if not session_ids:
476+
return [], total
477+
session_rank = case(
478+
{session_id: index for index, session_id in enumerate(session_ids)},
479+
value=ConversationV2.user_id,
480+
else_=len(session_ids),
481+
)
482+
result_query = (
483+
base_query.where(col(ConversationV2.user_id).in_(session_ids))
484+
.order_by(session_rank)
485+
.order_by(order())
486+
.order_by(tie_breaker())
487+
)
488+
else:
489+
result_query = (
490+
base_query.order_by(order())
491+
.order_by(tie_breaker())
492+
.offset(offset)
493+
.limit(page_size)
494+
)
417495
if not include_history:
418496
result_query = result_query.options(defer(ConversationV2.content))
419-
if len(platforms) > 1 or len(platform_ids or []) > 1:
497+
if (
498+
not group_by_session
499+
and sort_by == "created_at"
500+
and (len(platforms) > 1 or len(platform_ids or []) > 1)
501+
):
420502
# SQLite may choose the narrow platform index for IN queries and
421503
# then materialize a temporary sort. Force the global ordering
422504
# index for multi-platform pages while keeping ORM row mapping.
@@ -449,6 +531,20 @@ async def get_filtered_conversations(
449531

450532
return conversations, total
451533

534+
async def get_conversation_platform_ids(self) -> list[str]:
535+
"""Return distinct platform IDs referenced by conversation history.
536+
537+
Returns:
538+
Sorted platform IDs that have at least one conversation.
539+
"""
540+
async with self.get_db() as session:
541+
result = await session.execute(
542+
select(ConversationV2.platform_id)
543+
.distinct()
544+
.order_by(ConversationV2.platform_id)
545+
)
546+
return [platform_id for platform_id in result.scalars() if platform_id]
547+
452548
async def create_conversation(
453549
self,
454550
user_id,

‎astrbot/dashboard/api/conversations.py‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Any
3+
from typing import Any, Literal
44

55
from fastapi import APIRouter, Depends, Query, Request
66
from fastapi.responses import StreamingResponse
@@ -94,6 +94,11 @@ async def _list_conversations(
9494
search: str,
9595
exclude_ids: str,
9696
exclude_platforms: str,
97+
keyword: str,
98+
umo: str,
99+
sort_by: Literal["created_at", "updated_at"],
100+
sort_order: Literal["asc", "desc"],
101+
group_by_session: bool,
97102
include_history: bool,
98103
):
99104
return await _run(
@@ -105,6 +110,11 @@ async def _list_conversations(
105110
search_query=search,
106111
exclude_ids=exclude_ids,
107112
exclude_platforms=exclude_platforms,
113+
keyword_query=keyword,
114+
umo_query=umo,
115+
sort_by=sort_by,
116+
sort_order=sort_order,
117+
group_by_session=group_by_session,
108118
include_history=include_history,
109119
)
110120
)
@@ -119,6 +129,11 @@ async def list_conversations(
119129
search: str = Query(default=""),
120130
exclude_ids: str = Query(default=""),
121131
exclude_platforms: str = Query(default=""),
132+
keyword: str = Query(default=""),
133+
umo: str = Query(default=""),
134+
sort_by: Literal["created_at", "updated_at"] = Query(default="created_at"),
135+
sort_order: Literal["asc", "desc"] = Query(default="desc"),
136+
group_by_session: bool = Query(default=False),
122137
include_history: bool = Query(default=True),
123138
_auth: AuthContext = Depends(require_data_scope),
124139
service: ConversationService = Depends(get_service),
@@ -132,10 +147,23 @@ async def list_conversations(
132147
search=search,
133148
exclude_ids=exclude_ids,
134149
exclude_platforms=exclude_platforms,
150+
keyword=keyword,
151+
umo=umo,
152+
sort_by=sort_by,
153+
sort_order=sort_order,
154+
group_by_session=group_by_session,
135155
include_history=include_history,
136156
)
137157

138158

159+
@router.get("/conversations/filter-options")
160+
async def get_conversation_filter_options(
161+
_auth: AuthContext = Depends(require_data_scope),
162+
service: ConversationService = Depends(get_service),
163+
):
164+
return await _run(service.get_filter_options)
165+
166+
139167
@router.post("/conversations/export")
140168
async def export_conversations(
141169
payload: ConversationExportRequest,
@@ -227,6 +255,11 @@ async def list_dashboard_conversations(
227255
search: str = Query(default=""),
228256
exclude_ids: str = Query(default=""),
229257
exclude_platforms: str = Query(default=""),
258+
keyword: str = Query(default=""),
259+
umo: str = Query(default=""),
260+
sort_by: Literal["created_at", "updated_at"] = Query(default="created_at"),
261+
sort_order: Literal["asc", "desc"] = Query(default="desc"),
262+
group_by_session: bool = Query(default=False),
230263
include_history: bool = Query(default=True),
231264
_username: str = Depends(require_dashboard_user),
232265
service: ConversationService = Depends(get_service),
@@ -240,6 +273,11 @@ async def list_dashboard_conversations(
240273
search=search,
241274
exclude_ids=exclude_ids,
242275
exclude_platforms=exclude_platforms,
276+
keyword=keyword,
277+
umo=umo,
278+
sort_by=sort_by,
279+
sort_order=sort_order,
280+
group_by_session=group_by_session,
243281
include_history=include_history,
244282
)
245283

‎astrbot/dashboard/services/conversation_service.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def __init__(
3131
) -> None:
3232
self.db_helper = db_helper
3333
self.conv_mgr = core_lifecycle.conversation_manager
34+
self.core_lifecycle = core_lifecycle
3435

3536
async def list_conversations(
3637
self,
@@ -42,6 +43,11 @@ async def list_conversations(
4243
search_query: str,
4344
exclude_ids: str,
4445
exclude_platforms: str,
46+
keyword_query: str = "",
47+
umo_query: str = "",
48+
sort_by: str = "created_at",
49+
sort_order: str = "desc",
50+
group_by_session: bool = False,
4551
include_history: bool = True,
4652
) -> dict:
4753
platform_list = [item.strip() for item in platforms.split(",") if item.strip()]
@@ -69,6 +75,11 @@ async def list_conversations(
6975
search_query=search_query,
7076
exclude_ids=exclude_id_list,
7177
exclude_platforms=exclude_platform_list,
78+
keyword_query=keyword_query.strip(),
79+
umo_query=umo_query.strip(),
80+
sort_by=sort_by,
81+
sort_order=sort_order,
82+
group_by_session=group_by_session,
7283
include_history=include_history,
7384
)
7485
except Exception as exc:
@@ -95,9 +106,30 @@ async def list_conversations(
95106
"page_size": page_size,
96107
"total": total_count,
97108
"total_pages": total_pages,
109+
"grouped_by_session": group_by_session,
98110
},
99111
}
100112

113+
async def get_filter_options(self) -> dict:
114+
"""Build robot filter options from configured conversation platforms.
115+
116+
Returns:
117+
Robot IDs and adapter types that exist in both configuration and
118+
conversation history.
119+
"""
120+
history_platform_ids = set(await self.db_helper.get_conversation_platform_ids())
121+
configured_platforms = self.core_lifecycle.astrbot_config.get("platform", [])
122+
return {
123+
"bots": [
124+
{
125+
"id": str(platform.get("id", "")),
126+
"type": str(platform.get("type", "")),
127+
}
128+
for platform in configured_platforms
129+
if platform.get("id") in history_platform_ids
130+
]
131+
}
132+
101133
async def get_conversation_detail(self, data: object) -> dict:
102134
payload = self._payload(data)
103135
user_id, cid = self._require_user_and_cid(payload)

‎astrbot/dashboard/services/stat_service.py‎

Lines changed: 27 additions & 7 deletions

‎dashboard/src/api/generated/openapi-v1/sdk.gen.ts‎

Lines changed: 11 additions & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)