feat: persistent chat message storage + Android pull-to-load history
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:
- Add ChatMessage model + Alembic migration 024
- Add on_message callback to AgentRuntime for persisting messages during SSE streaming
- Plumb session_id from ChatRequest to AgentContext in all 4 chat endpoints
- Add GET /agent-chat/{id}/sessions and /sessions/{sid}/messages with cursor pagination

Android:
- Add DTOs/ApiService/MessageDao for server-side chat history
- ChatRepository: fetchOlderMessages (API + Room cache), offline fallback
- ChatViewModel: loadMoreHistory with isLoadingMore/hasMoreMessages state
- ChatScreen: scroll-to-top detection + top loading indicator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 00:07:26 +08:00
parent 569e3ab7df
commit a06082480a
12 changed files with 705 additions and 18 deletions

View File

@@ -105,6 +105,7 @@ class AgentRuntime:
execution_logger: Optional[Any] = None,
on_tool_executed: Optional[Callable[[str], Any]] = None,
on_llm_call: Optional[Callable[[Dict[str, Any]], Any]] = None,
on_message: Optional[Callable[[Dict[str, Any]], Any]] = None,
hook_manager: Optional[HookManager] = None,
streamlined: bool = False,
):
@@ -141,6 +142,7 @@ class AgentRuntime:
self.execution_logger = execution_logger
self.on_tool_executed = on_tool_executed
self.on_llm_call = on_llm_call
self.on_message = on_message
self._memory_context_loaded = False
self._llm_invocations = 0
# 自主学习作用域bare 聊天用 "bare"Agent 用 "agent"
@@ -808,6 +810,12 @@ class AgentRuntime:
# 2. 追加用户消息
self.context.add_user_message(user_input)
if self.on_message:
self.on_message({
"role": "user", "content": user_input,
"session_id": self.context.session_id,
"iteration": 0,
})
# 2.5 计划模式 (P2) — 流式生成执行计划
plan: Optional[Plan] = None
@@ -969,6 +977,12 @@ class AgentRuntime:
# LLM 直接返回文本 → 结束
self.context.add_assistant_message(content)
final_text = content or "(模型未返回有效内容)"
if self.on_message:
self.on_message({
"role": "assistant", "content": final_text,
"session_id": self.context.session_id,
"iteration": self.context.iteration,
})
review_score = 0.0
# 输出质量自检(默认关闭)
@@ -1027,6 +1041,12 @@ class AgentRuntime:
# 有工具调用 → 先记录 assistant 消息
self.context.add_assistant_message(content or "", tool_calls, reasoning)
if self.on_message:
self.on_message({
"role": "assistant", "content": content or "",
"session_id": self.context.session_id,
"iteration": self.context.iteration,
})
# yield think 事件
tc_names = [tc["function"]["name"] for tc in tool_calls]
@@ -1087,6 +1107,15 @@ class AgentRuntime:
result = json.dumps({"error": hook_res.reason}, ensure_ascii=False)
yield {"type": "tool_result", "name": tname, "result": result, "iteration": self.context.iteration}
self.context.add_tool_result(tcid, tname, result)
if self.on_message:
self.on_message({
"role": "tool", "content": result,
"session_id": self.context.session_id,
"iteration": self.context.iteration,
"tool_name": tname,
"tool_input": json.dumps(targs, ensure_ascii=False) if targs else None,
"tool_output": result,
})
continue
if hook_res.modified_input:
targs = hook_res.modified_input
@@ -1119,11 +1148,29 @@ class AgentRuntime:
result = f"[审批拒绝] 工具 {tname} 需要人工审批但被拒绝。"
yield {"type": "tool_result", "name": tname, "result": result, "iteration": self.context.iteration}
self.context.add_tool_result(tcid, tname, result)
if self.on_message:
self.on_message({
"role": "tool", "content": result,
"session_id": self.context.session_id,
"iteration": self.context.iteration,
"tool_name": tname,
"tool_input": json.dumps(targs, ensure_ascii=False) if targs else None,
"tool_output": result,
})
continue
elif decision == "skip":
result = f"[审批跳过] 工具 {tname} 被跳过。"
yield {"type": "tool_result", "name": tname, "result": result, "iteration": self.context.iteration}
self.context.add_tool_result(tcid, tname, result)
if self.on_message:
self.on_message({
"role": "tool", "content": result,
"session_id": self.context.session_id,
"iteration": self.context.iteration,
"tool_name": tname,
"tool_input": json.dumps(targs, ensure_ascii=False) if targs else None,
"tool_output": result,
})
continue
# decision == "approved" → 继续执行
@@ -1154,6 +1201,15 @@ class AgentRuntime:
))
self.context.add_tool_result(tcid, tname, result)
if self.on_message:
self.on_message({
"role": "tool", "content": result,
"session_id": self.context.session_id,
"iteration": self.context.iteration,
"tool_name": tname,
"tool_input": json.dumps(targs, ensure_ascii=False) if targs else None,
"tool_output": result,
})
self.context.tool_calls_made += 1
# Hook: PostToolUse — 工具执行后处理 (流式)