diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index 6fcb3608..2c282867 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -11,6 +11,7 @@ from astrbot.core import sp 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 @@ class ConversationManager: 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 00000000..97b8196d --- /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 5bc30257..4b957fe8 100644 --- a/astrbot/dashboard/routes/api_key.py +++ b/astrbot/dashboard/routes/api_key.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta, timezone 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 @@ class ApiKeyRoute(Route): @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 0602cc07..a914f3cb 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -22,6 +22,7 @@ from astrbot.core.platform.sources.webchat.message_parts_helper import ( 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 @@ class ChatRoute(Route): "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 @@ class ChatRoute(Route): "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 5a66dafd..6ba570f5 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 @@ class ChatUIProjectRoute(Route): "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 @@ class ChatUIProjectRoute(Route): "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 @@ class ChatUIProjectRoute(Route): "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 @@ class ChatUIProjectRoute(Route): "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 25438565..8d0af938 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.message_parts_helper import ( ) 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 @@ class LiveChatRoute(Route): "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 653e22cb..9a736b17 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -15,6 +15,7 @@ from astrbot.core.platform.sources.webchat.message_parts_helper import ( 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 @@ class OpenApiRoute(Route): "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 @@ class OpenApiRoute(Route): "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 a9650cd0..db4a9ae7 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -3,6 +3,7 @@ import hashlib 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.config.default import VERSION 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 @@ class AstrBotDashboard: 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)