session_routes.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. # -*- coding: utf-8 -*-
  2. """
  3. 会话管理接口 —— 聊天历史查询、会话列表、删除会话、权限模式管理
  4. """
  5. from fastapi import APIRouter, Depends
  6. from pydantic import BaseModel, Field
  7. from api.auth import get_current_user
  8. from db.chat_store import (
  9. get_messages, get_sessions, delete_session,
  10. get_session_mode, set_session_mode, get_latest_context_usage,
  11. pin_session, is_session_pinned, update_session_title,
  12. search_sessions,
  13. )
  14. router = APIRouter()
  15. class ModeRequest(BaseModel):
  16. """权限模式切换请求"""
  17. mode: str = Field(..., description="权限模式", examples=["plan", "full"])
  18. class TitleRequest(BaseModel):
  19. """会话标题编辑请求"""
  20. title: str = Field(..., description="新标题", max_length=100, examples=["15216工作面需风量计算"])
  21. @router.get("/chat/history/{session_id}")
  22. async def get_chat_history(
  23. session_id: str,
  24. limit: int = 100,
  25. offset: int = 0,
  26. user_info: dict = Depends(get_current_user),
  27. ):
  28. """查询指定会话的聊天历史
  29. Args:
  30. session_id: 会话ID
  31. limit: 返回条数上限,默认100
  32. offset: 偏移量,默认0
  33. Returns:
  34. { "session_id": "...", "messages": [...], "total": N }
  35. """
  36. messages = get_messages(session_id, limit=limit, offset=offset)
  37. # 计算总数
  38. all_msgs = get_messages(session_id, limit=10000)
  39. total = len(all_msgs)
  40. return {
  41. "session_id": session_id,
  42. "messages": messages,
  43. "total": total,
  44. }
  45. @router.get("/sessions")
  46. async def list_sessions(
  47. limit: int = 20,
  48. offset: int = 0,
  49. user_info: dict = Depends(get_current_user),
  50. ):
  51. """查询当前用户的会话列表(按更新时间倒序)
  52. Returns:
  53. [{ "session_id": "...", "title": "...", "message_count": N, ... }, ...]
  54. """
  55. user_name = user_info.get("username", "admin")
  56. sessions = get_sessions(limit=limit, offset=offset, user_name=user_name)
  57. return {"sessions": sessions, "total": len(sessions)}
  58. @router.get("/sessions/search")
  59. async def search_session_titles(
  60. q: str = "",
  61. limit: int = 10,
  62. user_info: dict = Depends(get_current_user),
  63. ):
  64. """按标题模糊搜索当前用户的会话(用于插入会话功能)。
  65. Args:
  66. q: 搜索关键词(匹配会话标题)
  67. limit: 最大返回条数,默认 10
  68. Returns:
  69. { "sessions": [...], "total": N }
  70. """
  71. user_name = user_info.get("username", "admin")
  72. if not q or not q.strip():
  73. # 无关键词时返回最近会话
  74. sessions = get_sessions(limit=limit, offset=0, user_name=user_name)
  75. return {"sessions": sessions, "total": len(sessions)}
  76. sessions = search_sessions(user_name, q.strip(), limit=limit)
  77. return {"sessions": sessions, "total": len(sessions)}
  78. @router.delete("/sessions/{session_id}")
  79. async def remove_session(
  80. session_id: str,
  81. user_info: dict = Depends(get_current_user),
  82. ):
  83. """删除指定会话及其所有消息"""
  84. delete_session(session_id)
  85. return {"status": "ok", "message": f"会话 {session_id} 已删除"}
  86. # ── 权限模式管理 ──
  87. @router.get("/sessions/{session_id}/mode")
  88. async def get_mode(
  89. session_id: str,
  90. user_info: dict = Depends(get_current_user),
  91. ):
  92. """获取会话的权限模式"""
  93. return {
  94. "session_id": session_id,
  95. "mode": get_session_mode(session_id),
  96. }
  97. @router.put("/sessions/{session_id}/mode")
  98. async def set_mode(
  99. session_id: str,
  100. req: ModeRequest,
  101. user_info: dict = Depends(get_current_user),
  102. ):
  103. """设置会话的权限模式
  104. mode 取值:
  105. - plan: 计划模式(先生成执行计划,用户确认后再执行)
  106. - full: 完全访问(无限制自动执行)
  107. """
  108. valid = {"plan", "full"}
  109. if req.mode not in valid:
  110. return {
  111. "error": f"无效的模式: {req.mode}",
  112. "valid_modes": sorted(valid),
  113. }
  114. set_session_mode(session_id, req.mode)
  115. return {
  116. "session_id": session_id,
  117. "mode": req.mode,
  118. "message": f"已切换为「{req.mode}」模式",
  119. }
  120. # ── 会话置顶 ──
  121. @router.put("/sessions/{session_id}/pin")
  122. async def toggle_pin(
  123. session_id: str,
  124. user_info: dict = Depends(get_current_user),
  125. ):
  126. """切换会话置顶状态(置顶 ⇄ 取消置顶)。
  127. Returns:
  128. { "session_id": "...", "pinned": true, "message": "..." }
  129. """
  130. pinned = not is_session_pinned(session_id)
  131. pin_session(session_id, pinned)
  132. return {
  133. "session_id": session_id,
  134. "pinned": pinned,
  135. "message": "已置顶" if pinned else "已取消置顶",
  136. }
  137. # ── 会话标题编辑 ──
  138. @router.put("/sessions/{session_id}/title")
  139. async def edit_title(
  140. session_id: str,
  141. req: TitleRequest,
  142. user_info: dict = Depends(get_current_user),
  143. ):
  144. """编辑会话标题。
  145. Args:
  146. req.title: 新标题,最长100字
  147. """
  148. title = req.title.strip()
  149. if not title:
  150. return {"error": "标题不能为空"}
  151. if len(title) > 100:
  152. return {"error": f"标题过长({len(title)}字),最多100字"}
  153. update_session_title(session_id, title)
  154. return {
  155. "session_id": session_id,
  156. "title": title,
  157. "message": "标题已更新",
  158. }
  159. # ── 上下文容量查询 ──
  160. @router.get("/sessions/{session_id}/context")
  161. async def get_context_usage(
  162. session_id: str,
  163. user_info: dict = Depends(get_current_user),
  164. ):
  165. """查询会话最近一次的上下文用量详情。
  166. Returns:
  167. {
  168. "session_id": "...",
  169. "total_limit": 131072,
  170. "current_usage": 12345,
  171. "usage_pct": 9.4,
  172. "breakdown": {
  173. "messages": 5000,
  174. "mcp": 4000,
  175. "skills": 800,
  176. "system_prompt": 600,
  177. "other": 2745
  178. },
  179. "updated_at": "2026-08-05 14:30:00"
  180. }
  181. 若无记录则返回 null 值。
  182. """
  183. row = get_latest_context_usage(session_id)
  184. if not row:
  185. return {
  186. "session_id": session_id,
  187. "total_limit": None,
  188. "current_usage": None,
  189. "usage_pct": None,
  190. "breakdown": None,
  191. "updated_at": None,
  192. }
  193. total_limit = row.get("total_limit", 131072)
  194. current = row.get("current_usage", 0)
  195. pct = round(current / total_limit * 100, 1) if total_limit else 0
  196. return {
  197. "session_id": session_id,
  198. "total_limit": total_limit,
  199. "current_usage": current,
  200. "usage_pct": pct,
  201. "breakdown": {
  202. "messages": row.get("messages_tokens", 0),
  203. "mcp": row.get("mcp_tokens", 0),
  204. "skills": row.get("skills_tokens", 0),
  205. "system_prompt": row.get("system_prompt_tokens", 0),
  206. "other": row.get("other_tokens", 0),
  207. },
  208. "updated_at": row.get("updated_at"),
  209. }