"""历史记录服务 — 检索历史和对话历史持久化到 SQLite 表结构: - search_history: 检索历史(查询词、过滤条件、结果数) - chat_history: 对话历史(问题、模板、thinking、答案、引用条文) """ import sqlite3 import time import json import logging from typing import Optional from app.config import SQLITE_PATH logger = logging.getLogger(__name__) def _get_conn() -> sqlite3.Connection: """获取 SQLite 连接(自动建表)""" SQLITE_PATH.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(SQLITE_PATH), check_same_thread=False) conn.executescript(""" CREATE TABLE IF NOT EXISTS search_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, query TEXT NOT NULL, category TEXT, province TEXT, city TEXT, result_count INTEGER DEFAULT 0, top_k INTEGER DEFAULT 20, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS chat_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, question TEXT NOT NULL, template TEXT DEFAULT 'simple', category TEXT, province TEXT, thinking TEXT, answer TEXT, citations TEXT, thinking_enabled INTEGER DEFAULT 1, status TEXT DEFAULT 'done', created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_search_history_created ON search_history(created_at DESC); CREATE INDEX IF NOT EXISTS idx_chat_history_created ON chat_history(created_at DESC); """) return conn def add_search_history( query: str, category: Optional[str] = None, province: Optional[str] = None, city: Optional[str] = None, result_count: int = 0, top_k: int = 20, ) -> int: """记录检索历史 Returns: 记录 ID,失败返回 0 """ try: conn = _get_conn() ts = time.strftime("%Y-%m-%d %H:%M:%S") cursor = conn.execute( """INSERT INTO search_history (query, category, province, city, result_count, top_k, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)""", (query[:200], category, province, city, result_count, top_k, ts), ) conn.commit() rid = cursor.lastrowid conn.close() return rid except Exception as e: logger.error(f"记录检索历史失败: {e}") return 0 def add_chat_history( question: str, template: str = "simple", category: Optional[str] = None, province: Optional[str] = None, thinking: str = "", answer: str = "", citations: str = "[]", thinking_enabled: bool = True, status: str = "done", ) -> int: """记录对话历史 Args: citations: JSON 字符串,引用条文列表 Returns: 记录 ID,失败返回 0 """ try: conn = _get_conn() ts = time.strftime("%Y-%m-%d %H:%M:%S") cursor = conn.execute( """INSERT INTO chat_history (question, template, category, province, thinking, answer, citations, thinking_enabled, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (question[:1000], template, category, province, thinking, answer, citations, 1 if thinking_enabled else 0, status, ts), ) conn.commit() rid = cursor.lastrowid conn.close() return rid except Exception as e: logger.error(f"记录对话历史失败: {e}") return 0 def update_chat_history( chat_id: int, thinking: str = "", answer: str = "", citations: str = "[]", status: str = "done", ) -> bool: """更新对话历史(流式生成完成后更新完整内容) Returns: True 成功 """ if not chat_id: return False try: conn = _get_conn() conn.execute( """UPDATE chat_history SET thinking = ?, answer = ?, citations = ?, status = ? WHERE id = ?""", (thinking, answer, citations, status, chat_id), ) conn.commit() conn.close() return True except Exception as e: logger.error(f"更新对话历史失败: {e}") return False def get_search_history( page: int = 1, page_size: int = 20, ) -> dict: """获取检索历史列表(分页,最新在前) Returns: {"items": [...], "total": int, "page": int, "page_size": int} """ try: conn = _get_conn() total = conn.execute("SELECT COUNT(*) FROM search_history").fetchone()[0] offset = (page - 1) * page_size rows = conn.execute( """SELECT id, query, category, province, city, result_count, top_k, created_at FROM search_history ORDER BY created_at DESC LIMIT ? OFFSET ?""", (page_size, offset), ).fetchall() conn.close() items = [ { "id": r[0], "query": r[1], "category": r[2], "province": r[3], "city": r[4], "result_count": r[5], "top_k": r[6], "created_at": r[7], } for r in rows ] return {"items": items, "total": total, "page": page, "page_size": page_size} except Exception as e: logger.error(f"获取检索历史失败: {e}") return {"items": [], "total": 0, "page": page, "page_size": page_size} def get_chat_history( page: int = 1, page_size: int = 20, ) -> dict: """获取对话历史列表(分页,最新在前) Returns: {"items": [...], "total": int, "page": int, "page_size": int} """ try: conn = _get_conn() total = conn.execute("SELECT COUNT(*) FROM chat_history").fetchone()[0] offset = (page - 1) * page_size rows = conn.execute( """SELECT id, question, template, category, province, thinking, answer, citations, thinking_enabled, status, created_at FROM chat_history ORDER BY created_at DESC LIMIT ? OFFSET ?""", (page_size, offset), ).fetchall() conn.close() items = [] for r in rows: items.append({ "id": r[0], "question": r[1], "template": r[2], "category": r[3], "province": r[4], "thinking": r[5], "answer": r[6], "citations": json.loads(r[7]) if r[7] else [], "thinking_enabled": bool(r[8]), "status": r[9], "created_at": r[10], }) return {"items": items, "total": total, "page": page, "page_size": page_size} except Exception as e: logger.error(f"获取对话历史失败: {e}") return {"items": [], "total": 0, "page": page, "page_size": page_size} def get_chat_detail(chat_id: int) -> Optional[dict]: """获取单条对话历史详情""" try: conn = _get_conn() row = conn.execute( """SELECT id, question, template, category, province, thinking, answer, citations, thinking_enabled, status, created_at FROM chat_history WHERE id = ?""", (chat_id,), ).fetchone() conn.close() if not row: return None return { "id": row[0], "question": row[1], "template": row[2], "category": row[3], "province": row[4], "thinking": row[5], "answer": row[6], "citations": json.loads(row[7]) if row[7] else [], "thinking_enabled": bool(row[8]), "status": row[9], "created_at": row[10], } except Exception as e: logger.error(f"获取对话详情失败: {e}") return None def delete_search_history(history_id: int) -> bool: """删除单条检索历史""" try: conn = _get_conn() conn.execute("DELETE FROM search_history WHERE id = ?", (history_id,)) conn.commit() conn.close() return True except Exception as e: logger.error(f"删除检索历史失败: {e}") return False def delete_chat_history(history_id: int) -> bool: """删除单条对话历史""" try: conn = _get_conn() conn.execute("DELETE FROM chat_history WHERE id = ?", (history_id,)) conn.commit() conn.close() return True except Exception as e: logger.error(f"删除对话历史失败: {e}") return False def clear_search_history() -> bool: """清空全部检索历史""" try: conn = _get_conn() conn.execute("DELETE FROM search_history") conn.commit() conn.close() return True except Exception as e: logger.error(f"清空检索历史失败: {e}") return False def clear_chat_history() -> bool: """清空全部对话历史""" try: conn = _get_conn() conn.execute("DELETE FROM chat_history") conn.commit() conn.close() return True except Exception as e: logger.error(f"清空对话历史失败: {e}") return False