-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: 添加分页和搜索功能以获取会话列表,优化前端与后端的数据交互 #2906
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
+260
−100
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Contributor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `astrbot/core/db/sqlite.py:260-265` </location>
<code_context>
+
+
+ @abc.abstractmethod
+ async def get_session_conversations(
+ self,
+ page: int = 1,
</code_context>
<issue_to_address>
**suggestion:** Inconsistent parameter naming for platform filtering.
The interface uses 'platform_ids: list[str] | None', while this implementation uses 'platform: str = None'. Please align the parameter name and type for consistency and to support potential multi-platform filtering.
Suggested implementation:
```python
async def get_session_conversations(
self,
page: int = 1,
page_size: int = 20,
search_query: str = None,
platform_ids: list[str] | None = None,
) -> tuple[list[dict], int]:
```
```python
async with self.get_db() as session:
session: AsyncSession
# Apply platform_ids filtering if provided
platform_filter = None
if platform_ids is not None:
from sqlalchemy import or_
platform_filter = or_(
*[ConversationV2.platform == pid for pid in platform_ids]
)
```
You will need to:
1. Integrate `platform_filter` into your query's `where` clause, replacing any previous usage of `platform`.
2. Update any other references to `platform` in this function to use `platform_ids` and the new filtering logic.
3. Update the interface and any calls to this function elsewhere in your codebase to use `platform_ids: list[str] | None` instead of `platform: str = None`.
</issue_to_address>
### Comment 2
<location> `astrbot/core/db/sqlite.py:307-308` </location>
<code_context>
+ )
+
+ # 平台筛选
+ if platform:
+ platform_pattern = f"{platform}:%"
+ base_query = base_query.where(Preference.scope_id.like(platform_pattern))
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Platform filtering assumes a specific format for scope_id.
If the scope_id format changes or platforms include colons, filtering may fail. Consider enforcing the format or validating platform values.
</issue_to_address>
### Comment 3
<location> `astrbot/core/db/sqlite.py:352-361` </location>
<code_context>
+
+ # 构建列表
+ sessions_data = []
+ for row in rows:
+ sessions_data.append(
+ {
+ "session_id": row.session_id,
</code_context>
<issue_to_address>
**suggestion:** No handling for possible None values in row fields.
Normalize None values in joined fields to empty strings or defaults to prevent issues for API consumers.
```suggestion
for row in rows:
sessions_data.append(
{
"session_id": row.session_id if row.session_id is not None else "",
"conversation_id": row.conversation_id if row.conversation_id is not None else "",
"persona_id": row.persona_id if row.persona_id is not None else "",
"title": row.title if row.title is not None else "",
"persona_name": row.persona_name if row.persona_name is not None else "",
}
)
```
</issue_to_address>
### Comment 4
<location> `dashboard/src/views/SessionManagementPage.vue:440-441` </location>
<code_context>
- // 懒加载过滤会话 - 使用客户端分页
+ // 现在分页由后端处理,前端直接显示后端返回的数据
filteredSessions() {
- let filtered = this.sessions;
-
- // 搜索筛选
- if (this.searchQuery) {
- const query = this.searchQuery.toLowerCase().trim();
- filtered = filtered.filter(session =>
- session.session_name.toLowerCase().includes(query) ||
- session.platform.toLowerCase().includes(query) ||
- session.persona_name?.toLowerCase().includes(query) ||
- session.chat_provider_name?.toLowerCase().includes(query) ||
- session.session_id.toLowerCase().includes(query)
- );
- }
-
- // 平台筛选
- if (this.filterPlatform) {
- filtered = filtered.filter(session => session.platform === this.filterPlatform);
- }
-
- return filtered;
+ return this.sessions;
},
</code_context>
<issue_to_address>
**suggestion:** filteredSessions now just returns sessions, making the computed property redundant.
Since filtering is now server-side, you can remove filteredSessions and reference 'sessions' directly to simplify the code.
Suggested implementation:
```
// 现在分页由后端处理,前端直接显示后端返回的数据
// 已移除 filteredSessions,直接使用 sessions
```
```
<v-data-table-server
:headers="headers"
:items="sessions"
:loading="loading"
:items-per-page="itemsPerPage"
:page="currentPage"
```
</issue_to_address>
### Comment 5
<location> `dashboard/src/views/SessionManagementPage.vue:954-957` </location>
<code_context>
},
+
+ // 处理分页更新事件
+ handlePaginationUpdate(options) {
+ this.currentPage = options.page;
+ this.itemsPerPage = options.itemsPerPage;
+ this.loadSessions();
+ },
+
</code_context>
<issue_to_address>
**suggestion (performance):** No debounce/throttle on pagination, search, or filter changes.
Debouncing these actions will help prevent excessive API calls and reduce backend load.
Suggested implementation:
```
+ // 引入lodash的debounce
+ import debounce from 'lodash/debounce';
+
+ // 在created或data中定义debouncedLoadSessions
+ data() {
+ return {
+ // ...其他data属性
+ debouncedLoadSessions: null,
+ };
+ },
+
+ created() {
+ this.debouncedLoadSessions = debounce(this.loadSessions, 300);
+ },
+
+ // 处理分页更新事件
+ handlePaginationUpdate(options) {
+ this.currentPage = options.page;
+ this.itemsPerPage = options.itemsPerPage;
+ this.debouncedLoadSessions();
+ },
```
```
:items="filteredSessions"
:loading="loading"
:items-per-page="itemsPerPage"
:page="currentPage"
```
1. If you have search or filter change handlers (e.g., `handleSearchUpdate`, `handleFilterUpdate`), replace their `this.loadSessions()` calls with `this.debouncedLoadSessions()`.
2. Make sure lodash is installed in your project (`npm install lodash`).
3. If you use the Options API, move the debounce initialization to `mounted` or `created` as appropriate.
4. If you use a different debounce utility, adjust the import and usage accordingly.
</issue_to_address>
### Comment 6
<location> `astrbot/dashboard/routes/session_management.py:52` </location>
<code_context>
+ sessions_data, total = await self.db_helper.get_session_conversations(
+ page, page_size, search_query, platform
+ )
+ logger.warning(f"{search_query} {platform}")
+
provider_manager = self.core_lifecycle.provider_manager
</code_context>
<issue_to_address>
**suggestion:** Debug logging left in production code.
Please remove or change the log level to prevent excessive logging in production.
```suggestion
logger.debug(f"{search_query} {platform}")
```
</issue_to_address>
### Comment 7
<location> `astrbot/core/db/__init__.py:293-298` </location>
<code_context>
+
+
+ @abc.abstractmethod
+ async def get_session_conversations(
+ self,
+ page: int = 1,
+ page_size: int = 20,
+ search_query: str = "",
+ platform_ids: list[str] | None = None,
+ ) -> tuple[list[dict], int]:
+ """Get paginated session conversations with joined conversation and persona details, support search and platform filter."""
</code_context>
<issue_to_address>
**issue:** Parameter type and name mismatch with implementation.
The implementation in sqlite.py uses 'platform: str = None' instead of 'platform_ids: list[str] | None', which may cause confusion or bugs in future backend implementations. Please align the parameter types and names across all implementations.
</issue_to_address>
### Comment 8
<location> `astrbot/dashboard/routes/session_management.py:40` </location>
<code_context>
async def list_sessions(self):
"""获取所有会话的列表,包括 persona 和 provider 信息"""
try:
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting persona name resolution and session mapping logic into helper functions to simplify the main handler.
```markdown
Extract the “map and enrich” logic out of `list_sessions` into small helpers. For example:
1) Persona‐name resolution
```python
def _resolve_persona_name(self, persona_id: str, persona_name: str) -> str:
if persona_id and persona_id != "[%None]":
return persona_name or next(
(p["name"] for p in self.persona_mgr.personas_v3 if p["name"] == persona_id),
persona_name
)
if persona_id == "[%None]":
return "无人格"
default = self.persona_mgr.selected_default_persona_v3
return default["name"] if default else persona_name
```
2) Single‐row mapping (incl. providers / flags)
```python
async def _map_session_row(self, row: dict) -> dict:
sid = row["session_id"]
info = {
"session_id": sid,
"conversation_id": row["conversation_id"],
"persona_id": self._resolve_persona_name(row["persona_id"], row["persona_name"]),
"chat_provider_id": None,
"stt_provider_id": None,
"tts_provider_id": None,
"session_enabled": SessionServiceManager.is_session_enabled(sid),
"llm_enabled": SessionServiceManager.is_llm_enabled_for_session(sid),
"tts_enabled": SessionServiceManager.is_tts_enabled_for_session(sid),
"message_type": sid.split(":", 1)[1] if ":" in sid else "unknown",
"session_name": SessionServiceManager.get_session_display_name(sid),
"session_raw_name": sid.split(":", 2)[-1],
"title": row["title"],
}
for t in (ProviderType.CHAT_COMPLETION, ProviderType.TEXT_TO_SPEECH, ProviderType.SPEECH_TO_TEXT):
prov = self.core_lifecycle.provider_manager.get_using_provider(t, sid)
if prov:
info[f"{t.name.lower()}_provider_id"] = prov.provider_id
return info
```
3) Slimmed `list_sessions` body
```python
async def list_sessions(self):
page = int(request.args.get("page", 1))
page_size = int(request.args.get("page_size", 20))
data, total = await self.db_helper.get_session_conversations(
page, page_size, request.args.get("search", ""), request.args.get("platform", "")
)
sessions = [await self._map_session_row(r) for r in data]
return Response().ok({
"sessions": sessions,
"pagination": {
"page": page,
"page_size": page_size,
"total": total,
"total_pages": (total + page_size - 1) // page_size if page_size else 0,
},
# ...available_* lists...
}).__dict__
```
This keeps all functionality, but isolates branching in helpers and makes the main handler ≪ 30 lines.
</issue_to_address>
### Comment 9
<location> `astrbot/core/db/sqlite.py:350-362` </location>
<code_context>
async def get_session_conversations(
self,
page: int = 1,
page_size: int = 20,
search_query: str = None,
platform: str = None,
) -> tuple[list[dict], int]:
"""Get paginated session conversations with joined conversation and persona details."""
from sqlalchemy import select, func, or_
from astrbot.core.db.po import Preference, ConversationV2, Persona
async with self.get_db() as session:
session: AsyncSession
offset = (page - 1) * page_size
base_query = (
select(
Preference.scope_id.label("session_id"),
func.json_extract(Preference.value, "$.val").label(
"conversation_id"
),
ConversationV2.persona_id,
ConversationV2.title,
Persona.persona_id.label("persona_name"),
)
.select_from(Preference)
.outerjoin(
ConversationV2,
func.json_extract(Preference.value, "$.val")
== ConversationV2.conversation_id,
)
.outerjoin(Persona, ConversationV2.persona_id == Persona.persona_id)
.where(Preference.scope == "umo", Preference.key == "sel_conv_id")
)
# 搜索筛选
if search_query:
search_pattern = f"%{search_query}%"
base_query = base_query.where(
or_(
Preference.scope_id.ilike(search_pattern),
ConversationV2.title.ilike(search_pattern),
Persona.persona_id.ilike(search_pattern),
)
)
# 平台筛选
if platform:
platform_pattern = f"{platform}:%"
base_query = base_query.where(Preference.scope_id.like(platform_pattern))
# 排序
base_query = base_query.order_by(Preference.scope_id)
# 分页结果
result_query = base_query.offset(offset).limit(page_size)
result = await session.execute(result_query)
rows = result.fetchall()
# 查询总数(应用相同的筛选条件)
count_base_query = (
select(func.count(Preference.scope_id))
.select_from(Preference)
.outerjoin(
ConversationV2,
func.json_extract(Preference.value, "$.val")
== ConversationV2.conversation_id,
)
.outerjoin(Persona, ConversationV2.persona_id == Persona.persona_id)
.where(Preference.scope == "umo", Preference.key == "sel_conv_id")
)
# 应用相同的搜索和平台筛选条件到计数查询
if search_query:
search_pattern = f"%{search_query}%"
count_base_query = count_base_query.where(
or_(
Preference.scope_id.ilike(search_pattern),
ConversationV2.title.ilike(search_pattern),
Persona.persona_id.ilike(search_pattern),
)
)
if platform:
platform_pattern = f"{platform}:%"
count_base_query = count_base_query.where(Preference.scope_id.like(platform_pattern))
total_result = await session.execute(count_base_query)
total = total_result.scalar() or 0
# 构建列表
sessions_data = []
for row in rows:
sessions_data.append(
{
"session_id": row.session_id,
"conversation_id": row.conversation_id,
"persona_id": row.persona_id,
"title": row.title,
"persona_name": row.persona_name,
}
)
return sessions_data, total
</code_context>
<issue_to_address>
**suggestion (code-quality):** Convert for loop into list comprehension ([`list-comprehension`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/list-comprehension/))
```suggestion
sessions_data = [
{
"session_id": row.session_id,
"conversation_id": row.conversation_id,
"persona_id": row.persona_id,
"title": row.title,
"persona_name": row.persona_name,
}
for row in rows
]
```
</issue_to_address>
### Comment 10
<location> `astrbot/dashboard/routes/session_management.py:78-79` </location>
<code_context>
async def list_sessions(self):
"""获取所有会话的列表,包括 persona 和 provider 信息"""
try:
page = int(request.args.get("page", 1))
page_size = int(request.args.get("page_size", 20))
search_query = request.args.get("search", "")
platform = request.args.get("platform", "")
# 使用数据库JOIN查询获取分页会话数据
sessions_data, total = await self.db_helper.get_session_conversations(
page, page_size, search_query, platform
)
logger.warning(f"{search_query} {platform}")
provider_manager = self.core_lifecycle.provider_manager
persona_mgr = self.core_lifecycle.persona_mgr
personas = persona_mgr.personas_v3
sessions = []
# 循环补充非数据库信息,如provider和session状态
for data in sessions_data:
session_id = data["session_id"]
conversation_id = data["conversation_id"]
conv_persona_id = data["persona_id"]
title = data["title"]
persona_name = data["persona_name"]
# 处理 persona 显示
if conv_persona_id and conv_persona_id != "[%None]":
if not persona_name:
for p in personas:
if p["name"] == conv_persona_id:
persona_name = p["name"]
break
elif conv_persona_id == "[%None]":
persona_name = "无人格"
else:
default_persona = persona_mgr.selected_default_persona_v3
if default_persona:
persona_name = default_persona["name"]
session_info = {
"session_id": session_id,
"conversation_id": conversation_id,
"persona_id": persona_name,
"chat_provider_id": None,
"stt_provider_id": None,
"tts_provider_id": None,
"session_enabled": SessionServiceManager.is_session_enabled(
session_id
),
"llm_enabled": SessionServiceManager.is_llm_enabled_for_session(
session_id
),
"tts_enabled": SessionServiceManager.is_tts_enabled_for_session(
session_id
),
"platform": session_id.split(":")[0]
if ":" in session_id
else "unknown",
"message_type": session_id.split(":")[1]
if session_id.count(":") >= 1
else "unknown",
"session_name": SessionServiceManager.get_session_display_name(
session_id
),
"session_raw_name": session_id.split(":")[2]
if session_id.count(":") >= 2
else session_id,
"title": title,
}
# 获取 provider 信息
chat_provider = provider_manager.get_using_provider(
provider_type=ProviderType.CHAT_COMPLETION, umo=session_id
)
tts_provider = provider_manager.get_using_provider(
provider_type=ProviderType.TEXT_TO_SPEECH, umo=session_id
)
stt_provider = provider_manager.get_using_provider(
provider_type=ProviderType.SPEECH_TO_TEXT, umo=session_id
)
if chat_provider:
meta = chat_provider.meta()
session_info["chat_provider_id"] = meta.id
if tts_provider:
meta = tts_provider.meta()
session_info["tts_provider_id"] = meta.id
if stt_provider:
meta = stt_provider.meta()
session_info["stt_provider_id"] = meta.id
sessions.append(session_info)
# 获取可用的 personas 和 providers 列表
available_personas = [
{"name": p["name"], "prompt": p.get("prompt", "")} for p in personas
]
available_chat_providers = []
for provider in provider_manager.provider_insts:
meta = provider.meta()
available_chat_providers.append(
{
"id": meta.id,
"name": meta.id,
"model": meta.model,
"type": meta.type,
}
)
available_stt_providers = []
for provider in provider_manager.stt_provider_insts:
meta = provider.meta()
available_stt_providers.append(
{
"id": meta.id,
"name": meta.id,
"model": meta.model,
"type": meta.type,
}
)
available_tts_providers = []
for provider in provider_manager.tts_provider_insts:
meta = provider.meta()
available_tts_providers.append(
{
"id": meta.id,
"name": meta.id,
"model": meta.model,
"type": meta.type,
}
)
result = {
"sessions": sessions,
"available_personas": available_personas,
"available_chat_providers": available_chat_providers,
"available_stt_providers": available_stt_providers,
"available_tts_providers": available_tts_providers,
"pagination": {
"page": page,
"page_size": page_size,
"total": total,
"total_pages": (total + page_size - 1) // page_size
if page_size > 0
else 0,
},
}
return Response().ok(result).__dict__
except Exception as e:
error_msg = f"获取会话列表失败: {str(e)}\n{traceback.format_exc()}"
logger.error(error_msg)
return Response().error(f"获取会话列表失败: {str(e)}").__dict__
</code_context>
<issue_to_address>
**suggestion (code-quality):** We've found these issues:
- Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
- Low code quality found in SessionManagementRoute.list\_sessions - 13% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))
```suggestion
if default_persona := persona_mgr.selected_default_persona_v3:
```
<br/><details><summary>Explanation</summary>
The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.
How can you solve this?
It might be worth refactoring this function to make it shorter and more readable.
- Reduce the function length by extracting pieces of functionality out into
their own functions. This is the most important thing you can do - ideally a
function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
sits together within the function rather than being scattered.</details>
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
fixes #2905
Motivation / 动机
为会话管理增加分页功能,减少数据库查询次数,将查询时间从最高10秒优化至80毫秒左右。
Modifications / 改动点
修改前端代码,使用分页接口并实现分页。
修改后端代码,实现分页查询、排序、过滤等功能。
Verification Steps / 验证步骤
Screenshots or Test Results / 运行截图或测试结果
Compatibility & Breaking Changes / 兼容性与破坏性变更
Checklist / 检查清单
requirements.txt和pyproject.toml文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations inrequirements.txtandpyproject.toml.Sourcery 总结
为会话管理 API 实现了服务器端分页、搜索和平台过滤功能,并更新了仪表板以消费分页数据,从而大幅提升了查询性能。
新功能:
get_session_conversations方法,以返回带有总数的 paginate 会话记录增强功能:
v-data-table-server和相应事件处理程序的服务器端分页Original summary in English
Summary by Sourcery
Implement server-side pagination, search, and platform filtering for the session management API and update the dashboard to consume paginated data, greatly improving query performance.
New Features:
Enhancements: