fix: message ordering and duplicate messages
Some checks failed
CI/CD Pipeline / Backend — Lint & Test (push) Has been cancelled
CI/CD Pipeline / Frontend — Lint & Build (push) Has been cancelled
CI/CD Pipeline / Docker — Build Check (push) Has been cancelled

Backend:
- Composite ORDER BY (created_at DESC, iteration ASC, id ASC) to fix
  non-deterministic ordering when multiple messages share same second
- Composite key cursor pagination to avoid missing messages at boundary

Android:
- Fix message duplication: guard loadMoreHistory from firing before
  initial scroll completes (isAtTop race → fetchOlderMessages →
  cacheMessagesFromApi with different IDs → Room duplicates)
- Add distinctBy(id) at all message assignment points in ViewModel
- cacheMessagesFromApi: skip insert if same ID or same content exists
- Add relative timestamp display on message bubbles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 22:09:03 +08:00
parent cffe4a52d6
commit e33aa6e0b7
6 changed files with 71 additions and 15 deletions

View File

@@ -604,7 +604,7 @@ async def list_agent_sessions(
db: Session = Depends(get_db),
):
"""获取 Agent 的会话列表,按最近活跃时间排序。"""
from sqlalchemy import func as sa_func, desc
from sqlalchemy import func as sa_func, desc, or_
# 验证 agent 存在或有权限
agent = db.query(Agent).filter(Agent.id == agent_id).first()
@@ -666,6 +666,8 @@ async def get_session_messages(
db: Session = Depends(get_db),
):
"""获取会话的消息历史(分页),从旧到新排序。"""
from sqlalchemy import or_
# limit 限制
limit = min(max(limit, 1), 200)
@@ -674,16 +676,22 @@ async def get_session_messages(
ChatMessage.session_id == session_id,
)
# 游标分页before_id 之前的老消息
# 游标分页before_id 之前的老消息(使用复合键避免 created_at 相同导致遗漏)
if before_id:
cursor_msg = db.query(ChatMessage).filter(ChatMessage.id == before_id).first()
if cursor_msg and cursor_msg.created_at:
base_q = base_q.filter(ChatMessage.created_at < cursor_msg.created_at)
base_q = base_q.filter(
or_(
ChatMessage.created_at < cursor_msg.created_at,
(ChatMessage.created_at == cursor_msg.created_at) & (ChatMessage.id < cursor_msg.id),
)
)
# 取 N+1 条,判断 has_more按时间降序取最新 N 条,再反转)
# 二级排序 iteration + id 避免同一秒内多条消息顺序不确定
batch = (
base_q
.order_by(ChatMessage.created_at.desc())
.order_by(ChatMessage.created_at.desc(), ChatMessage.iteration.asc(), ChatMessage.id.asc())
.limit(limit + 1)
.all()
)