886 lines
32 KiB
Python
886 lines
32 KiB
Python
|
|
"""
|
|||
|
|
消息存储服务 - 将每次查询的消息记录存储到 MySQL
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
from typing import Any, Dict, List, Optional
|
|||
|
|
|
|||
|
|
import pymysql
|
|||
|
|
|
|||
|
|
from config import Config
|
|||
|
|
from services.common.datetime_utils import DateTimeGenerator
|
|||
|
|
from schemas.messages import MessagesDTO
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MessageStorage:
|
|||
|
|
"""消息存储服务"""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
cfg = Config.get_section("logging_mysql")
|
|||
|
|
self.enabled = str(cfg.get("enabled", "false")).lower() in ("1", "true", "yes")
|
|||
|
|
self.entity_debug_enabled = str(cfg.get("entity_debug_enabled", "false")).lower() in ("1", "true", "yes")
|
|||
|
|
self.host = cfg.get("host", "127.0.0.1")
|
|||
|
|
self.port = int(cfg.get("port", 3306))
|
|||
|
|
self.user = cfg.get("user", "root")
|
|||
|
|
self.password = cfg.get("password", "")
|
|||
|
|
self.database = cfg.get("database", "more_dots")
|
|||
|
|
# 消息落库与结构化日志分表,避免误用 logging_mysql.table=structured_logs
|
|||
|
|
self.table = cfg.get("messages_table", "ipc_apbo.messages")
|
|||
|
|
self.conversation_table = cfg.get("conversation_table", "ipc_apbo.conversations")
|
|||
|
|
self.connect_timeout = int(cfg.get("connect_timeout", 5))
|
|||
|
|
self._inited = False
|
|||
|
|
self._conversation_schema_checked = False
|
|||
|
|
|
|||
|
|
def _get_conn(self):
|
|||
|
|
"""获取数据库连接"""
|
|||
|
|
return pymysql.connect(
|
|||
|
|
host=self.host,
|
|||
|
|
port=self.port,
|
|||
|
|
user=self.user,
|
|||
|
|
password=self.password,
|
|||
|
|
database=self.database,
|
|||
|
|
charset="utf8mb4",
|
|||
|
|
autocommit=True,
|
|||
|
|
connect_timeout=self.connect_timeout,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _log_local(event: str, payload: Optional[Dict[str, Any]] = None) -> None:
|
|||
|
|
print(json.dumps({
|
|||
|
|
"level": "ERROR",
|
|||
|
|
"event": event,
|
|||
|
|
"payload": payload or {},
|
|||
|
|
"created_at": DateTimeGenerator.now().epoch_millis,
|
|||
|
|
}, ensure_ascii=False))
|
|||
|
|
|
|||
|
|
def _log_entity_debug(self, event: str, payload: Optional[Dict[str, Any]] = None) -> None:
|
|||
|
|
if not self.entity_debug_enabled:
|
|||
|
|
return
|
|||
|
|
print(json.dumps({
|
|||
|
|
"level": "DEBUG",
|
|||
|
|
"event": event,
|
|||
|
|
"payload": payload or {},
|
|||
|
|
"created_at": DateTimeGenerator.now().epoch_millis,
|
|||
|
|
}, ensure_ascii=False))
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _now_ms() -> int:
|
|||
|
|
return DateTimeGenerator.now().epoch_millis
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _build_audit_fields(
|
|||
|
|
*,
|
|||
|
|
random_code: str,
|
|||
|
|
user: Optional[str],
|
|||
|
|
created_value: Any = None,
|
|||
|
|
updated_value: Any = None,
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
created_ms = MessageStorage._resolve_epoch_millis(created_value)
|
|||
|
|
updated_ms = MessageStorage._resolve_epoch_millis(updated_value, fallback=created_ms)
|
|||
|
|
created_bundle = DateTimeGenerator.bundle(created_ms, default_to_now=True)
|
|||
|
|
updated_bundle = DateTimeGenerator.bundle(updated_ms, default_to_now=True)
|
|||
|
|
operator = (user or "system").strip() if isinstance(user, str) else "system"
|
|||
|
|
return {
|
|||
|
|
"random_code": random_code,
|
|||
|
|
"create_user": operator,
|
|||
|
|
"create_date": created_bundle.db_datetime,
|
|||
|
|
"update_user": operator,
|
|||
|
|
"update_date": updated_bundle.db_datetime,
|
|||
|
|
"create_user_name": operator,
|
|||
|
|
"update_user_name": operator,
|
|||
|
|
"created_at": created_ms,
|
|||
|
|
"updated_at": updated_ms,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _resolve_epoch_millis(value: Any, fallback: Optional[int] = None) -> int:
|
|||
|
|
if value is None:
|
|||
|
|
return fallback if fallback is not None else DateTimeGenerator.now().epoch_millis
|
|||
|
|
|
|||
|
|
if isinstance(value, (int, float)):
|
|||
|
|
raw = int(value)
|
|||
|
|
digits = len(str(abs(raw)))
|
|||
|
|
if digits == 10:
|
|||
|
|
return raw * 1000
|
|||
|
|
return raw
|
|||
|
|
|
|||
|
|
if isinstance(value, str):
|
|||
|
|
text = value.strip()
|
|||
|
|
if text.isdigit():
|
|||
|
|
raw = int(text)
|
|||
|
|
digits = len(text)
|
|||
|
|
if digits == 10:
|
|||
|
|
return raw * 1000
|
|||
|
|
if digits == 13:
|
|||
|
|
return raw
|
|||
|
|
|
|||
|
|
parsed = DateTimeGenerator.bundle(value, default_to_now=True)
|
|||
|
|
return parsed.epoch_millis
|
|||
|
|
|
|||
|
|
parsed = DateTimeGenerator.bundle(value, default_to_now=True)
|
|||
|
|
return parsed.epoch_millis
|
|||
|
|
|
|||
|
|
def _log_entity_stage(self, entity: str, action: str, stage: str, started_at: int, payload: Optional[Dict[str, Any]] = None) -> None:
|
|||
|
|
debug_payload = dict(payload or {})
|
|||
|
|
debug_payload.update({
|
|||
|
|
"entity": entity,
|
|||
|
|
"action": action,
|
|||
|
|
"stage": stage,
|
|||
|
|
"elapsed_ms": max(0, self._now_ms() - started_at),
|
|||
|
|
})
|
|||
|
|
self._log_entity_debug(f"message_storage.{entity}.{action}.{stage}", debug_payload)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _split_table_reference(table_name: str, default_schema: str) -> tuple[str, str]:
|
|||
|
|
cleaned = str(table_name or "").strip()
|
|||
|
|
if "." in cleaned:
|
|||
|
|
schema_name, physical_table_name = cleaned.split(".", 1)
|
|||
|
|
else:
|
|||
|
|
schema_name, physical_table_name = default_schema, cleaned
|
|||
|
|
return schema_name.strip().strip("`"), physical_table_name.strip().strip("`")
|
|||
|
|
|
|||
|
|
def _ensure_conversation_schema(self, conn) -> None:
|
|||
|
|
if self._conversation_schema_checked or not self.enabled:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
started_at = self._now_ms()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"ensure_schema",
|
|||
|
|
"start",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_table": self.conversation_table},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
schema_name, table_name = self._split_table_reference(self.conversation_table, self.database)
|
|||
|
|
probe_sql = """
|
|||
|
|
SELECT 1
|
|||
|
|
FROM information_schema.columns
|
|||
|
|
WHERE table_schema = %s AND table_name = %s AND column_name = %s
|
|||
|
|
LIMIT 1
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
with conn.cursor() as cur:
|
|||
|
|
cur.execute(probe_sql, (schema_name, table_name, "name"))
|
|||
|
|
if cur.fetchone() is None:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"ensure_schema",
|
|||
|
|
"alter_needed",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_table": self.conversation_table, "missing_column": "name"},
|
|||
|
|
)
|
|||
|
|
alter_sql = f"""
|
|||
|
|
ALTER TABLE {self.conversation_table}
|
|||
|
|
ADD COLUMN name VARCHAR(255) NULL COMMENT '会话名称' AFTER user
|
|||
|
|
"""
|
|||
|
|
cur.execute(alter_sql)
|
|||
|
|
|
|||
|
|
self._conversation_schema_checked = True
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"ensure_schema",
|
|||
|
|
"success",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_table": self.conversation_table, "schema_checked": True},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _ensure_table(self) -> None:
|
|||
|
|
"""确保消息表存在"""
|
|||
|
|
if self._inited or not self.enabled:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
started_at = self._now_ms()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"storage",
|
|||
|
|
"ensure_table",
|
|||
|
|
"start",
|
|||
|
|
started_at,
|
|||
|
|
{"messages_table": self.table, "conversation_table": self.conversation_table},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
message_sql = f"""
|
|||
|
|
CREATE TABLE IF NOT EXISTS {self.table} (
|
|||
|
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|||
|
|
random_code VARCHAR(100) NULL COMMENT '业务主键',
|
|||
|
|
create_user VARCHAR(100) NULL COMMENT '创建人',
|
|||
|
|
create_date DATETIME NULL COMMENT '创建时间',
|
|||
|
|
update_user VARCHAR(100) NULL COMMENT '修改人',
|
|||
|
|
update_date DATETIME NULL COMMENT '修改时间',
|
|||
|
|
create_user_name VARCHAR(255) NULL COMMENT '创建人姓名',
|
|||
|
|
update_user_name VARCHAR(255) NULL COMMENT '修改人姓名',
|
|||
|
|
message_id VARCHAR(255) NULL COMMENT '消息 ID',
|
|||
|
|
conversation_id VARCHAR(255) NULL COMMENT '会话 ID',
|
|||
|
|
user VARCHAR(255) NULL COMMENT '用户标识',
|
|||
|
|
query LONGTEXT NULL COMMENT '用户查询',
|
|||
|
|
answer LONGTEXT NULL COMMENT '回答消息内容',
|
|||
|
|
feedback VARCHAR(255) NULL COMMENT '点赞 like / 点踩 dislike',
|
|||
|
|
feedback_content TEXT NULL COMMENT '点踩内容',
|
|||
|
|
created_at BIGINT NULL COMMENT '创建时间(毫秒时间戳)',
|
|||
|
|
updated_at BIGINT NULL COMMENT '更新时间(毫秒时间戳)',
|
|||
|
|
`log` JSON NULL COMMENT '当前对话日志(JSON字符串)',
|
|||
|
|
UNIQUE KEY uk_random_code (random_code),
|
|||
|
|
UNIQUE KEY uk_message_id (message_id),
|
|||
|
|
INDEX idx_conversation_id (conversation_id),
|
|||
|
|
INDEX idx_created_at (created_at)
|
|||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息记录表';
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
conversation_sql = f"""
|
|||
|
|
CREATE TABLE IF NOT EXISTS {self.conversation_table} (
|
|||
|
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|||
|
|
random_code VARCHAR(100) NULL COMMENT '业务主键',
|
|||
|
|
create_user VARCHAR(100) NULL COMMENT '创建人',
|
|||
|
|
create_date DATETIME NULL COMMENT '创建时间',
|
|||
|
|
update_user VARCHAR(100) NULL COMMENT '修改人',
|
|||
|
|
update_date DATETIME NULL COMMENT '修改时间',
|
|||
|
|
create_user_name VARCHAR(255) NULL COMMENT '创建人姓名',
|
|||
|
|
update_user_name VARCHAR(255) NULL COMMENT '修改人姓名',
|
|||
|
|
conversation_id VARCHAR(255) NULL COMMENT '会话 ID',
|
|||
|
|
user VARCHAR(255) NULL COMMENT '用户',
|
|||
|
|
name VARCHAR(512) NULL COMMENT '会话名称',
|
|||
|
|
status VARCHAR(255) NULL COMMENT '状态',
|
|||
|
|
introduction VARCHAR(255) NULL COMMENT '开场白',
|
|||
|
|
created_at BIGINT NULL COMMENT '创建时间(毫秒时间戳)',
|
|||
|
|
updated_at BIGINT NULL COMMENT '更新时间(毫秒时间戳)',
|
|||
|
|
UNIQUE KEY uk_conversation_random_code (random_code),
|
|||
|
|
UNIQUE KEY uk_conversation_id (conversation_id),
|
|||
|
|
INDEX idx_conversation_updated_at (updated_at)
|
|||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话记录表';
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with self._get_conn() as conn:
|
|||
|
|
with conn.cursor() as cur:
|
|||
|
|
cur.execute(message_sql)
|
|||
|
|
cur.execute(conversation_sql)
|
|||
|
|
self._ensure_conversation_schema(conn)
|
|||
|
|
self._inited = True
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"storage",
|
|||
|
|
"ensure_table",
|
|||
|
|
"success",
|
|||
|
|
started_at,
|
|||
|
|
{"messages_table": self.table, "conversation_table": self.conversation_table},
|
|||
|
|
)
|
|||
|
|
except Exception as exc:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"storage",
|
|||
|
|
"ensure_table",
|
|||
|
|
"failed",
|
|||
|
|
started_at,
|
|||
|
|
{"messages_table": self.table, "conversation_table": self.conversation_table, "error": str(exc)},
|
|||
|
|
)
|
|||
|
|
self._log_local("message_storage.ensure_table_failed", {
|
|||
|
|
"error": str(exc),
|
|||
|
|
"messages_table": self.table,
|
|||
|
|
"conversation_table": self.conversation_table,
|
|||
|
|
})
|
|||
|
|
# 开发阶段容错,避免初始化失败影响主流程
|
|||
|
|
self.enabled = False
|
|||
|
|
|
|||
|
|
def create_conversation(
|
|||
|
|
self,
|
|||
|
|
conversation_id: str,
|
|||
|
|
user: Optional[str],
|
|||
|
|
name: Optional[str],
|
|||
|
|
status: str,
|
|||
|
|
introduction: Optional[str],
|
|||
|
|
created_at: int,
|
|||
|
|
updated_at: int,
|
|||
|
|
) -> bool:
|
|||
|
|
started_at = self._now_ms()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"create",
|
|||
|
|
"start",
|
|||
|
|
started_at,
|
|||
|
|
{
|
|||
|
|
"conversation_id": conversation_id,
|
|||
|
|
"user": user,
|
|||
|
|
"name_len": len(name or ""),
|
|||
|
|
"status": status,
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"create",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "reason": "storage_disabled"},
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
self._ensure_table()
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"create",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "reason": "storage_disabled_after_init"},
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
insert_sql = f"""
|
|||
|
|
INSERT INTO {self.conversation_table}(
|
|||
|
|
random_code, create_user, create_date, update_user, update_date,
|
|||
|
|
create_user_name, update_user_name,
|
|||
|
|
conversation_id, user, name, status, introduction, created_at, updated_at
|
|||
|
|
) VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
audit = self._build_audit_fields(
|
|||
|
|
random_code=conversation_id,
|
|||
|
|
user=user,
|
|||
|
|
created_value=created_at,
|
|||
|
|
updated_value=updated_at,
|
|||
|
|
)
|
|||
|
|
with self._get_conn() as conn:
|
|||
|
|
self._ensure_conversation_schema(conn)
|
|||
|
|
with conn.cursor() as cur:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"create",
|
|||
|
|
"sql_execute",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "table": self.conversation_table},
|
|||
|
|
)
|
|||
|
|
cur.execute(
|
|||
|
|
insert_sql,
|
|||
|
|
(
|
|||
|
|
audit["random_code"],
|
|||
|
|
audit["create_user"],
|
|||
|
|
audit["create_date"],
|
|||
|
|
audit["update_user"],
|
|||
|
|
audit["update_date"],
|
|||
|
|
audit["create_user_name"],
|
|||
|
|
audit["update_user_name"],
|
|||
|
|
conversation_id,
|
|||
|
|
user,
|
|||
|
|
name,
|
|||
|
|
status,
|
|||
|
|
introduction,
|
|||
|
|
audit["created_at"],
|
|||
|
|
audit["updated_at"],
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"create",
|
|||
|
|
"success",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "table": self.conversation_table},
|
|||
|
|
)
|
|||
|
|
return True
|
|||
|
|
except Exception as exc:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"create",
|
|||
|
|
"failed",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "table": self.conversation_table, "error": str(exc)},
|
|||
|
|
)
|
|||
|
|
self._log_local("message_storage.create_conversation_failed", {
|
|||
|
|
"error": str(exc),
|
|||
|
|
"conversation_table": self.conversation_table,
|
|||
|
|
"conversation_id": conversation_id,
|
|||
|
|
})
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def get_conversation_by_id(self, conversation_id: str) -> Optional[Dict[str, Any]]:
|
|||
|
|
started_at = self._now_ms()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"get_by_id",
|
|||
|
|
"start",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id},
|
|||
|
|
)
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"get_by_id",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "reason": "storage_disabled"},
|
|||
|
|
)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
self._ensure_table()
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"get_by_id",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "reason": "storage_disabled_after_init"},
|
|||
|
|
)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
select_sql = f"""
|
|||
|
|
SELECT conversation_id, user, name, status, introduction, created_at, updated_at
|
|||
|
|
FROM {self.conversation_table}
|
|||
|
|
WHERE conversation_id = %s
|
|||
|
|
LIMIT 1
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
with self._get_conn() as conn:
|
|||
|
|
self._ensure_conversation_schema(conn)
|
|||
|
|
with conn.cursor(pymysql.cursors.DictCursor) as cur:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"get_by_id",
|
|||
|
|
"sql_execute",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "table": self.conversation_table},
|
|||
|
|
)
|
|||
|
|
cur.execute(select_sql, (conversation_id,))
|
|||
|
|
result = cur.fetchone()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"get_by_id",
|
|||
|
|
"success",
|
|||
|
|
started_at,
|
|||
|
|
{
|
|||
|
|
"conversation_id": conversation_id,
|
|||
|
|
"table": self.conversation_table,
|
|||
|
|
"found": bool(result),
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
return dict(result) if result else None
|
|||
|
|
except Exception as exc:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"get_by_id",
|
|||
|
|
"failed",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "table": self.conversation_table, "error": str(exc)},
|
|||
|
|
)
|
|||
|
|
self._log_local("message_storage.get_conversation_failed", {
|
|||
|
|
"error": str(exc),
|
|||
|
|
"conversation_table": self.conversation_table,
|
|||
|
|
"conversation_id": conversation_id,
|
|||
|
|
})
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def update_conversation_updated_at(self, conversation_id: str, updated_at: int) -> bool:
|
|||
|
|
started_at = self._now_ms()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"update_updated_at",
|
|||
|
|
"start",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "updated_at": updated_at},
|
|||
|
|
)
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"update_updated_at",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "reason": "storage_disabled"},
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
self._ensure_table()
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"update_updated_at",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "reason": "storage_disabled_after_init"},
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
update_sql = f"""
|
|||
|
|
UPDATE {self.conversation_table}
|
|||
|
|
SET updated_at = %s,
|
|||
|
|
update_date = %s
|
|||
|
|
WHERE conversation_id = %s
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
update_ms = self._resolve_epoch_millis(updated_at)
|
|||
|
|
update_bundle = DateTimeGenerator.bundle(update_ms, default_to_now=True)
|
|||
|
|
with self._get_conn() as conn:
|
|||
|
|
self._ensure_conversation_schema(conn)
|
|||
|
|
with conn.cursor() as cur:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"update_updated_at",
|
|||
|
|
"sql_execute",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "table": self.conversation_table},
|
|||
|
|
)
|
|||
|
|
affected_rows = cur.execute(
|
|||
|
|
update_sql,
|
|||
|
|
(update_ms, update_bundle.db_datetime, conversation_id),
|
|||
|
|
)
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"update_updated_at",
|
|||
|
|
"success",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "affected_rows": int(affected_rows or 0)},
|
|||
|
|
)
|
|||
|
|
return bool(affected_rows)
|
|||
|
|
except Exception as exc:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"conversations",
|
|||
|
|
"update_updated_at",
|
|||
|
|
"failed",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "table": self.conversation_table, "error": str(exc)},
|
|||
|
|
)
|
|||
|
|
self._log_local("message_storage.update_conversation_failed", {
|
|||
|
|
"error": str(exc),
|
|||
|
|
"conversation_table": self.conversation_table,
|
|||
|
|
"conversation_id": conversation_id,
|
|||
|
|
})
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def save_message(
|
|||
|
|
self,
|
|||
|
|
conversation_id: str,
|
|||
|
|
message_id: str,
|
|||
|
|
query: str,
|
|||
|
|
answer: Optional[str] = None,
|
|||
|
|
workflow_type: Optional[str] = None,
|
|||
|
|
user: Optional[str] = None,
|
|||
|
|
sql_query: Optional[str] = None,
|
|||
|
|
execution_result: Optional[Dict[str, Any]] = None,
|
|||
|
|
metadata: Optional[Dict[str, Any]] = None,
|
|||
|
|
created_at: Optional[int] = None,
|
|||
|
|
updated_at: Optional[int] = None,
|
|||
|
|
logs: Optional[List[str]] = None,
|
|||
|
|
) -> bool:
|
|||
|
|
started_at = self._now_ms()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"create",
|
|||
|
|
"start",
|
|||
|
|
started_at,
|
|||
|
|
{
|
|||
|
|
"conversation_id": conversation_id,
|
|||
|
|
"message_id": message_id,
|
|||
|
|
"query_len": len(query or ""),
|
|||
|
|
"answer_len": len(answer or ""),
|
|||
|
|
"workflow_type": workflow_type,
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
"""
|
|||
|
|
保存消息记录
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
conversation_id: 会话 ID
|
|||
|
|
message_id: 消息 ID
|
|||
|
|
query: 用户查询
|
|||
|
|
answer: AI 回复
|
|||
|
|
workflow_type: 工作流类型 (conversation/tool_using)
|
|||
|
|
user: 用户标识
|
|||
|
|
sql_query: 生成的 SQL
|
|||
|
|
execution_result: SQL 执行结果
|
|||
|
|
metadata: 其他元数据
|
|||
|
|
logs: 过程日志(兼容 Java saveMessageToDB)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
bool: 是否保存成功
|
|||
|
|
"""
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"create",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "message_id": message_id, "reason": "storage_disabled"},
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
self._ensure_table()
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"create",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "message_id": message_id, "reason": "storage_disabled_after_init"},
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
audit = self._build_audit_fields(
|
|||
|
|
random_code=message_id,
|
|||
|
|
user=user,
|
|||
|
|
created_value=created_at,
|
|||
|
|
updated_value=updated_at,
|
|||
|
|
)
|
|||
|
|
log_payload = {
|
|||
|
|
"workflow_type": workflow_type,
|
|||
|
|
"sql_query": sql_query,
|
|||
|
|
"execution_result": execution_result or {},
|
|||
|
|
"metadata": metadata or {},
|
|||
|
|
}
|
|||
|
|
normalized_logs = [str(item) for item in (logs or []) if str(item).strip()]
|
|||
|
|
if normalized_logs:
|
|||
|
|
log_payload["data"] = "\n".join(normalized_logs)
|
|||
|
|
message_record = MessagesDTO(
|
|||
|
|
message_id=message_id,
|
|||
|
|
conversation_id=conversation_id,
|
|||
|
|
user=user,
|
|||
|
|
query=query,
|
|||
|
|
answer=answer,
|
|||
|
|
feedback=None,
|
|||
|
|
feedback_content=None,
|
|||
|
|
created_at=audit["created_at"],
|
|||
|
|
updated_at=audit["updated_at"],
|
|||
|
|
log=log_payload,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
insert_sql = f"""
|
|||
|
|
INSERT INTO {self.table}(
|
|||
|
|
random_code, create_user, create_date, update_user, update_date,
|
|||
|
|
create_user_name, update_user_name,
|
|||
|
|
message_id, conversation_id, user, query, answer,
|
|||
|
|
feedback, feedback_content, created_at, updated_at, `log`
|
|||
|
|
) VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with self._get_conn() as conn:
|
|||
|
|
with conn.cursor() as cur:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"create",
|
|||
|
|
"sql_execute",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "message_id": message_id, "table": self.table},
|
|||
|
|
)
|
|||
|
|
cur.execute(
|
|||
|
|
insert_sql,
|
|||
|
|
(
|
|||
|
|
audit["random_code"],
|
|||
|
|
audit["create_user"],
|
|||
|
|
audit["create_date"],
|
|||
|
|
audit["update_user"],
|
|||
|
|
audit["update_date"],
|
|||
|
|
audit["create_user_name"],
|
|||
|
|
audit["update_user_name"],
|
|||
|
|
message_record.message_id,
|
|||
|
|
message_record.conversation_id,
|
|||
|
|
message_record.user,
|
|||
|
|
message_record.query,
|
|||
|
|
message_record.answer,
|
|||
|
|
message_record.feedback,
|
|||
|
|
message_record.feedback_content,
|
|||
|
|
message_record.created_at,
|
|||
|
|
message_record.updated_at,
|
|||
|
|
json.dumps(message_record.log, ensure_ascii=False),
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"create",
|
|||
|
|
"success",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "message_id": message_id, "table": self.table},
|
|||
|
|
)
|
|||
|
|
return True
|
|||
|
|
except Exception as exc:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"create",
|
|||
|
|
"failed",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "message_id": message_id, "table": self.table, "error": str(exc)},
|
|||
|
|
)
|
|||
|
|
self._log_local("message_storage.save_failed", {
|
|||
|
|
"error": str(exc),
|
|||
|
|
"messages_table": self.table,
|
|||
|
|
"message_id": message_id,
|
|||
|
|
"conversation_id": conversation_id,
|
|||
|
|
})
|
|||
|
|
# 开发阶段容错,避免日志失败影响主流程
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def get_conversation_history(
|
|||
|
|
self,
|
|||
|
|
conversation_id: str,
|
|||
|
|
limit: int = 20
|
|||
|
|
) -> List[Dict[str, Any]]:
|
|||
|
|
"""
|
|||
|
|
获取会话历史消息
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
conversation_id: 会话 ID
|
|||
|
|
limit: 返回消息数量
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
List[Dict[str, Any]]: 消息列表
|
|||
|
|
"""
|
|||
|
|
started_at = self._now_ms()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"get_history",
|
|||
|
|
"start",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "limit": limit},
|
|||
|
|
)
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"get_history",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "reason": "storage_disabled"},
|
|||
|
|
)
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
self._ensure_table()
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"get_history",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "reason": "storage_disabled_after_init"},
|
|||
|
|
)
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
select_sql = f"""
|
|||
|
|
SELECT * FROM {self.table}
|
|||
|
|
WHERE conversation_id = %s
|
|||
|
|
ORDER BY created_at DESC
|
|||
|
|
LIMIT %s
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with self._get_conn() as conn:
|
|||
|
|
with conn.cursor(pymysql.cursors.DictCursor) as cur:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"get_history",
|
|||
|
|
"sql_execute",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "limit": limit, "table": self.table},
|
|||
|
|
)
|
|||
|
|
cur.execute(select_sql, (conversation_id, limit))
|
|||
|
|
results = cur.fetchall()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"get_history",
|
|||
|
|
"success",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "count": len(results or [])},
|
|||
|
|
)
|
|||
|
|
return list(results)
|
|||
|
|
except Exception as exc:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"get_history",
|
|||
|
|
"failed",
|
|||
|
|
started_at,
|
|||
|
|
{"conversation_id": conversation_id, "error": str(exc)},
|
|||
|
|
)
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
def update_feedback_by_message_id(
|
|||
|
|
self,
|
|||
|
|
message_id: str,
|
|||
|
|
feedback: str,
|
|||
|
|
feedback_content: Optional[str] = None,
|
|||
|
|
) -> bool:
|
|||
|
|
"""按 message_id 回写点赞/点踩反馈。"""
|
|||
|
|
started_at = self._now_ms()
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"update_feedback",
|
|||
|
|
"start",
|
|||
|
|
started_at,
|
|||
|
|
{"message_id": message_id, "feedback": feedback},
|
|||
|
|
)
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"update_feedback",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"message_id": message_id, "reason": "storage_disabled"},
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
self._ensure_table()
|
|||
|
|
if not self.enabled:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"update_feedback",
|
|||
|
|
"skipped",
|
|||
|
|
started_at,
|
|||
|
|
{"message_id": message_id, "reason": "storage_disabled_after_init"},
|
|||
|
|
)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
update_sql = f"""
|
|||
|
|
UPDATE {self.table}
|
|||
|
|
SET feedback = %s,
|
|||
|
|
feedback_content = %s,
|
|||
|
|
updated_at = %s,
|
|||
|
|
update_date = %s
|
|||
|
|
WHERE message_id = %s
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
normalized_feedback_content = (feedback_content or "").strip() or None
|
|||
|
|
now_bundle = DateTimeGenerator.now()
|
|||
|
|
try:
|
|||
|
|
with self._get_conn() as conn:
|
|||
|
|
with conn.cursor() as cur:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"update_feedback",
|
|||
|
|
"sql_execute",
|
|||
|
|
started_at,
|
|||
|
|
{"message_id": message_id, "table": self.table},
|
|||
|
|
)
|
|||
|
|
affected_rows = cur.execute(
|
|||
|
|
update_sql,
|
|||
|
|
(
|
|||
|
|
feedback,
|
|||
|
|
normalized_feedback_content,
|
|||
|
|
now_bundle.epoch_millis,
|
|||
|
|
now_bundle.db_datetime,
|
|||
|
|
message_id,
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"update_feedback",
|
|||
|
|
"success",
|
|||
|
|
started_at,
|
|||
|
|
{"message_id": message_id, "affected_rows": int(affected_rows or 0)},
|
|||
|
|
)
|
|||
|
|
return bool(affected_rows)
|
|||
|
|
except Exception as exc:
|
|||
|
|
self._log_entity_stage(
|
|||
|
|
"messages",
|
|||
|
|
"update_feedback",
|
|||
|
|
"failed",
|
|||
|
|
started_at,
|
|||
|
|
{"message_id": message_id, "error": str(exc)},
|
|||
|
|
)
|
|||
|
|
# 开发阶段容错,避免日志失败影响主流程
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 全局单例
|
|||
|
|
_GLOBAL_MESSAGE_STORAGE: Optional[MessageStorage] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_message_storage() -> MessageStorage:
|
|||
|
|
"""获取消息存储服务实例"""
|
|||
|
|
global _GLOBAL_MESSAGE_STORAGE
|
|||
|
|
if _GLOBAL_MESSAGE_STORAGE is None:
|
|||
|
|
_GLOBAL_MESSAGE_STORAGE = MessageStorage()
|
|||
|
|
return _GLOBAL_MESSAGE_STORAGE
|