diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index 6fcb3608c3..2c282867f9 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -11,6 +11,7 @@ from astrbot.core.agent.message import AssistantMessageSegment, UserMessageSegment from astrbot.core.db import BaseDatabase from astrbot.core.db.po import Conversation, ConversationV2 +from astrbot.core.utils.datetime_utils import to_utc_timestamp class ConversationManager: @@ -58,8 +59,10 @@ async def _trigger_session_deleted(self, unified_msg_origin: str) -> None: def _convert_conv_from_v2_to_v1(self, conv_v2: ConversationV2) -> Conversation: """将 ConversationV2 对象转换为 Conversation 对象""" - created_at = int(conv_v2.created_at.timestamp()) - updated_at = int(conv_v2.updated_at.timestamp()) + created_ts = to_utc_timestamp(conv_v2.created_at) + updated_ts = to_utc_timestamp(conv_v2.updated_at) + created_at = int(created_ts) if created_ts is not None else 0 + updated_at = int(updated_ts) if updated_ts is not None else 0 return Conversation( platform_id=conv_v2.platform_id, user_id=conv_v2.user_id, diff --git a/astrbot/core/utils/datetime_utils.py b/astrbot/core/utils/datetime_utils.py new file mode 100644 index 0000000000..97b8196dde --- /dev/null +++ b/astrbot/core/utils/datetime_utils.py @@ -0,0 +1,27 @@ +from datetime import datetime, timezone + + +def normalize_datetime_utc(dt: datetime | None) -> datetime | None: + """Normalize datetime values to UTC. + + Naive datetimes are interpreted as UTC to match SQLite storage behavior. + """ + if dt is None: + return None + if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None: + return dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc) + + +def to_utc_isoformat(dt: datetime | None) -> str | None: + normalized = normalize_datetime_utc(dt) + if normalized is None: + return None + return normalized.isoformat() + + +def to_utc_timestamp(dt: datetime | None) -> float | None: + normalized = normalize_datetime_utc(dt) + if normalized is None: + return None + return normalized.timestamp() diff --git a/astrbot/dashboard/routes/api_key.py b/astrbot/dashboard/routes/api_key.py index 5bc3025795..4b957fe8ea 100644 --- a/astrbot/dashboard/routes/api_key.py +++ b/astrbot/dashboard/routes/api_key.py @@ -5,6 +5,7 @@ from quart import g, request from astrbot.core.db import BaseDatabase +from astrbot.core.utils.datetime_utils import normalize_datetime_utc from .route import Response, Route, RouteContext @@ -25,11 +26,7 @@ def __init__(self, context: RouteContext, db: BaseDatabase) -> None: @staticmethod def _normalize_utc(dt: datetime | None) -> datetime | None: - if dt is None: - return None - if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None: - return dt.replace(tzinfo=timezone.utc) - return dt.astimezone(timezone.utc) + return normalize_datetime_utc(dt) @classmethod def _serialize_datetime(cls, dt: datetime | None) -> str | None: diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 0602cc0745..a914f3cbf0 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -22,6 +22,7 @@ from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr from astrbot.core.utils.active_event_registry import active_event_registry from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.core.utils.datetime_utils import to_utc_isoformat from .route import Response, Route, RouteContext @@ -486,7 +487,9 @@ async def stream(): "type": "message_saved", "data": { "id": saved_record.id, - "created_at": saved_record.created_at.astimezone().isoformat(), + "created_at": to_utc_isoformat( + saved_record.created_at + ), }, } try: @@ -718,8 +721,8 @@ async def get_sessions(self): "creator": session.creator, "display_name": session.display_name, "is_group": session.is_group, - "created_at": session.created_at.astimezone().isoformat(), - "updated_at": session.updated_at.astimezone().isoformat(), + "created_at": to_utc_isoformat(session.created_at), + "updated_at": to_utc_isoformat(session.updated_at), } ) diff --git a/astrbot/dashboard/routes/chatui_project.py b/astrbot/dashboard/routes/chatui_project.py index 5a66dafd36..6ba570f552 100644 --- a/astrbot/dashboard/routes/chatui_project.py +++ b/astrbot/dashboard/routes/chatui_project.py @@ -1,6 +1,7 @@ from quart import g, request from astrbot.core.db import BaseDatabase +from astrbot.core.utils.datetime_utils import to_utc_isoformat from .route import Response, Route, RouteContext @@ -51,8 +52,8 @@ async def create_project(self): "title": project.title, "emoji": project.emoji, "description": project.description, - "created_at": project.created_at.astimezone().isoformat(), - "updated_at": project.updated_at.astimezone().isoformat(), + "created_at": to_utc_isoformat(project.created_at), + "updated_at": to_utc_isoformat(project.updated_at), } ) .__dict__ @@ -70,8 +71,8 @@ async def list_projects(self): "title": project.title, "emoji": project.emoji, "description": project.description, - "created_at": project.created_at.astimezone().isoformat(), - "updated_at": project.updated_at.astimezone().isoformat(), + "created_at": to_utc_isoformat(project.created_at), + "updated_at": to_utc_isoformat(project.updated_at), } for project in projects ] @@ -102,8 +103,8 @@ async def get_project(self): "title": project.title, "emoji": project.emoji, "description": project.description, - "created_at": project.created_at.astimezone().isoformat(), - "updated_at": project.updated_at.astimezone().isoformat(), + "created_at": to_utc_isoformat(project.created_at), + "updated_at": to_utc_isoformat(project.updated_at), } ) .__dict__ @@ -236,8 +237,8 @@ async def get_project_sessions(self): "creator": session.creator, "display_name": session.display_name, "is_group": session.is_group, - "created_at": session.created_at.astimezone().isoformat(), - "updated_at": session.updated_at.astimezone().isoformat(), + "created_at": to_utc_isoformat(session.created_at), + "updated_at": to_utc_isoformat(session.updated_at), } for session in sessions ] diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py index 25438565e1..8d0af938d0 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -21,6 +21,7 @@ ) from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_temp_path +from astrbot.core.utils.datetime_utils import to_utc_isoformat from .route import Route, RouteContext @@ -621,7 +622,9 @@ async def _handle_chat_message( "type": "message_saved", "data": { "id": saved_record.id, - "created_at": saved_record.created_at.astimezone().isoformat(), + "created_at": to_utc_isoformat( + saved_record.created_at + ), }, }, ) diff --git a/astrbot/dashboard/routes/open_api.py b/astrbot/dashboard/routes/open_api.py index 653e22cbfe..9a736b1763 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -15,6 +15,7 @@ webchat_message_parts_have_content, ) from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr +from astrbot.core.utils.datetime_utils import to_utc_isoformat from .api_key import ALL_OPEN_API_SCOPES from .chat import ChatRoute @@ -481,7 +482,9 @@ async def _handle_chat_ws_send(self, post_data: dict) -> None: "type": "message_saved", "data": { "id": saved_record.id, - "created_at": saved_record.created_at.astimezone().isoformat(), + "created_at": to_utc_isoformat( + saved_record.created_at + ), }, "session_id": session_id, } @@ -579,8 +582,8 @@ async def get_chat_sessions(self): "creator": session.creator, "display_name": session.display_name, "is_group": session.is_group, - "created_at": session.created_at.astimezone().isoformat(), - "updated_at": session.updated_at.astimezone().isoformat(), + "created_at": to_utc_isoformat(session.created_at), + "updated_at": to_utc_isoformat(session.updated_at), } ) diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index a9650cd06b..db4a9ae75e 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -3,6 +3,7 @@ import logging import os import socket +from datetime import datetime from pathlib import Path from typing import Protocol, cast @@ -19,6 +20,7 @@ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db import BaseDatabase from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.core.utils.datetime_utils import to_utc_isoformat from astrbot.core.utils.io import get_local_ip_addresses from .routes import * @@ -45,6 +47,13 @@ def _parse_env_bool(value: str | None, default: bool) -> bool: return value.strip().lower() in {"1", "true", "yes", "on"} +class AstrBotJSONProvider(DefaultJSONProvider): + def default(self, obj): + if isinstance(obj, datetime): + return to_utc_isoformat(obj) + return super().default(obj) + + class AstrBotDashboard: def __init__( self, @@ -70,7 +79,8 @@ def __init__( self.app.config["MAX_CONTENT_LENGTH"] = ( 128 * 1024 * 1024 ) # 将 Flask 允许的最大上传文件体大小设置为 128 MB - cast(DefaultJSONProvider, self.app.json).sort_keys = False + self.app.json = AstrBotJSONProvider(self.app) + self.app.json.sort_keys = False self.app.before_request(self.auth_middleware) # token 用于验证请求 logging.getLogger(self.app.name).removeHandler(default_handler)