From 6881e813bd83e37af91f88312c72e6f5282fd0cf Mon Sep 17 00:00:00 2001 From: WintryWind7 <2272142104@qq.com> Date: Sun, 1 Mar 2026 00:51:23 +0800 Subject: [PATCH 1/3] fix: resolve unhandled UTC timezone offset for timestamps in conversation records --- astrbot/core/conversation_mgr.py | 5 +++-- astrbot/dashboard/routes/chat.py | 6 +++--- astrbot/dashboard/routes/chatui_project.py | 16 ++++++++-------- astrbot/dashboard/routes/live_chat.py | 2 +- astrbot/dashboard/routes/open_api.py | 6 +++--- astrbot/dashboard/server.py | 14 +++++++++++++- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index 6fcb3608c..c5b54948c 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -58,8 +58,9 @@ 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()) + from datetime import timezone + created_at = int(conv_v2.created_at.replace(tzinfo=timezone.utc).timestamp()) if conv_v2.created_at else 0 + updated_at = int(conv_v2.updated_at.replace(tzinfo=timezone.utc).timestamp()) if conv_v2.updated_at else 0 return Conversation( platform_id=conv_v2.platform_id, user_id=conv_v2.user_id, diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 0602cc074..4aa76187a 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -486,7 +486,7 @@ async def stream(): "type": "message_saved", "data": { "id": saved_record.id, - "created_at": saved_record.created_at.astimezone().isoformat(), + "created_at": saved_record.created_at.replace(tzinfo=timezone.utc).isoformat(), }, } try: @@ -718,8 +718,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": session.created_at.replace(tzinfo=timezone.utc).isoformat(), + "updated_at": session.updated_at.replace(tzinfo=timezone.utc).isoformat(), } ) diff --git a/astrbot/dashboard/routes/chatui_project.py b/astrbot/dashboard/routes/chatui_project.py index 5a66dafd3..d98356d0f 100644 --- a/astrbot/dashboard/routes/chatui_project.py +++ b/astrbot/dashboard/routes/chatui_project.py @@ -51,8 +51,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": project.created_at.replace(tzinfo=timezone.utc).isoformat(), + "updated_at": project.updated_at.replace(tzinfo=timezone.utc).isoformat(), } ) .__dict__ @@ -70,8 +70,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": project.created_at.replace(tzinfo=timezone.utc).isoformat(), + "updated_at": project.updated_at.replace(tzinfo=timezone.utc).isoformat(), } for project in projects ] @@ -102,8 +102,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": project.created_at.replace(tzinfo=timezone.utc).isoformat(), + "updated_at": project.updated_at.replace(tzinfo=timezone.utc).isoformat(), } ) .__dict__ @@ -236,8 +236,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": session.created_at.replace(tzinfo=timezone.utc).isoformat(), + "updated_at": session.updated_at.replace(tzinfo=timezone.utc).isoformat(), } for session in sessions ] diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py index 25438565e..21f51ef14 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -621,7 +621,7 @@ async def _handle_chat_message( "type": "message_saved", "data": { "id": saved_record.id, - "created_at": saved_record.created_at.astimezone().isoformat(), + "created_at": saved_record.created_at.replace(tzinfo=timezone.utc).isoformat(), }, }, ) diff --git a/astrbot/dashboard/routes/open_api.py b/astrbot/dashboard/routes/open_api.py index 653e22cbf..b60da531c 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -481,7 +481,7 @@ 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": saved_record.created_at.replace(tzinfo=timezone.utc).isoformat(), }, "session_id": session_id, } @@ -579,8 +579,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": session.created_at.replace(tzinfo=timezone.utc).isoformat(), + "updated_at": session.updated_at.replace(tzinfo=timezone.utc).isoformat(), } ) diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index a9650cd06..7133ab0ad 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -70,7 +70,19 @@ def __init__( self.app.config["MAX_CONTENT_LENGTH"] = ( 128 * 1024 * 1024 ) # 将 Flask 允许的最大上传文件体大小设置为 128 MB - cast(DefaultJSONProvider, self.app.json).sort_keys = False + + from datetime import datetime, timezone + class AstrBotJSONProvider(DefaultJSONProvider): + def default(self, obj): + if isinstance(obj, datetime): + if obj.tzinfo is None: + # 默认为 UTC + return obj.replace(tzinfo=timezone.utc).isoformat() + return obj.isoformat() + return super().default(obj) + + 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) From 9aa46376dea6eaba680fcb87feefb256825d8689 Mon Sep 17 00:00:00 2001 From: WintryWind7 <2272142104@qq.com> Date: Sun, 1 Mar 2026 01:45:38 +0800 Subject: [PATCH 2/3] fix: standardize timezone imports --- astrbot/core/conversation_mgr.py | 2 +- astrbot/dashboard/routes/chat.py | 1 + astrbot/dashboard/routes/chatui_project.py | 1 + astrbot/dashboard/routes/live_chat.py | 1 + astrbot/dashboard/routes/open_api.py | 1 + astrbot/dashboard/server.py | 2 +- 6 files changed, 6 insertions(+), 2 deletions(-) diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index c5b54948c..25b78bbbf 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -6,6 +6,7 @@ import json from collections.abc import Awaitable, Callable +from datetime import timezone from astrbot.core import sp from astrbot.core.agent.message import AssistantMessageSegment, UserMessageSegment @@ -58,7 +59,6 @@ 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 对象""" - from datetime import timezone created_at = int(conv_v2.created_at.replace(tzinfo=timezone.utc).timestamp()) if conv_v2.created_at else 0 updated_at = int(conv_v2.updated_at.replace(tzinfo=timezone.utc).timestamp()) if conv_v2.updated_at else 0 return Conversation( diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 4aa76187a..a13b66313 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -4,6 +4,7 @@ import re import uuid from contextlib import asynccontextmanager +from datetime import timezone from typing import cast from quart import Response as QuartResponse diff --git a/astrbot/dashboard/routes/chatui_project.py b/astrbot/dashboard/routes/chatui_project.py index d98356d0f..dc95a773c 100644 --- a/astrbot/dashboard/routes/chatui_project.py +++ b/astrbot/dashboard/routes/chatui_project.py @@ -1,3 +1,4 @@ +from datetime import timezone from quart import g, request from astrbot.core.db import BaseDatabase diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py index 21f51ef14..743a349c4 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -5,6 +5,7 @@ import time import uuid import wave +from datetime import timezone from typing import Any import jwt diff --git a/astrbot/dashboard/routes/open_api.py b/astrbot/dashboard/routes/open_api.py index b60da531c..9c380a5da 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -1,6 +1,7 @@ import asyncio import hashlib import json +from datetime import timezone from uuid import uuid4 from quart import g, request, websocket diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 7133ab0ad..345844b48 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -7,6 +7,7 @@ from typing import Protocol, cast import jwt +from datetime import datetime, timezone import psutil from flask.json.provider import DefaultJSONProvider from hypercorn.asyncio import serve @@ -71,7 +72,6 @@ def __init__( 128 * 1024 * 1024 ) # 将 Flask 允许的最大上传文件体大小设置为 128 MB - from datetime import datetime, timezone class AstrBotJSONProvider(DefaultJSONProvider): def default(self, obj): if isinstance(obj, datetime): From 0ba06f060ae09a529846ba2c9b7ff91b79fbef45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Sun, 1 Mar 2026 16:08:02 +0900 Subject: [PATCH 3/3] fix: unify UTC datetime normalization in dashboard routes --- astrbot/core/conversation_mgr.py | 8 ++++--- astrbot/core/utils/datetime_utils.py | 27 ++++++++++++++++++++++ astrbot/dashboard/routes/api_key.py | 7 ++---- astrbot/dashboard/routes/chat.py | 10 ++++---- astrbot/dashboard/routes/chatui_project.py | 18 +++++++-------- astrbot/dashboard/routes/live_chat.py | 6 +++-- astrbot/dashboard/routes/open_api.py | 10 ++++---- astrbot/dashboard/server.py | 20 ++++++++-------- 8 files changed, 68 insertions(+), 38 deletions(-) create mode 100644 astrbot/core/utils/datetime_utils.py diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index 25b78bbbf..2c282867f 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -6,12 +6,12 @@ import json from collections.abc import Awaitable, Callable -from datetime import timezone 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: @@ -59,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.replace(tzinfo=timezone.utc).timestamp()) if conv_v2.created_at else 0 - updated_at = int(conv_v2.updated_at.replace(tzinfo=timezone.utc).timestamp()) if conv_v2.updated_at else 0 + 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 000000000..97b8196dd --- /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 5bc302579..4b957fe8e 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 a13b66313..a914f3cbf 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -4,7 +4,6 @@ import re import uuid from contextlib import asynccontextmanager -from datetime import timezone from typing import cast from quart import Response as QuartResponse @@ -23,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 @@ -487,7 +487,9 @@ async def stream(): "type": "message_saved", "data": { "id": saved_record.id, - "created_at": saved_record.created_at.replace(tzinfo=timezone.utc).isoformat(), + "created_at": to_utc_isoformat( + saved_record.created_at + ), }, } try: @@ -719,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.replace(tzinfo=timezone.utc).isoformat(), - "updated_at": session.updated_at.replace(tzinfo=timezone.utc).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 dc95a773c..6ba570f55 100644 --- a/astrbot/dashboard/routes/chatui_project.py +++ b/astrbot/dashboard/routes/chatui_project.py @@ -1,7 +1,7 @@ -from datetime import timezone 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 @@ -52,8 +52,8 @@ async def create_project(self): "title": project.title, "emoji": project.emoji, "description": project.description, - "created_at": project.created_at.replace(tzinfo=timezone.utc).isoformat(), - "updated_at": project.updated_at.replace(tzinfo=timezone.utc).isoformat(), + "created_at": to_utc_isoformat(project.created_at), + "updated_at": to_utc_isoformat(project.updated_at), } ) .__dict__ @@ -71,8 +71,8 @@ async def list_projects(self): "title": project.title, "emoji": project.emoji, "description": project.description, - "created_at": project.created_at.replace(tzinfo=timezone.utc).isoformat(), - "updated_at": project.updated_at.replace(tzinfo=timezone.utc).isoformat(), + "created_at": to_utc_isoformat(project.created_at), + "updated_at": to_utc_isoformat(project.updated_at), } for project in projects ] @@ -103,8 +103,8 @@ async def get_project(self): "title": project.title, "emoji": project.emoji, "description": project.description, - "created_at": project.created_at.replace(tzinfo=timezone.utc).isoformat(), - "updated_at": project.updated_at.replace(tzinfo=timezone.utc).isoformat(), + "created_at": to_utc_isoformat(project.created_at), + "updated_at": to_utc_isoformat(project.updated_at), } ) .__dict__ @@ -237,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.replace(tzinfo=timezone.utc).isoformat(), - "updated_at": session.updated_at.replace(tzinfo=timezone.utc).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 743a349c4..8d0af938d 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -5,7 +5,6 @@ import time import uuid import wave -from datetime import timezone from typing import Any import jwt @@ -22,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 @@ -622,7 +622,9 @@ async def _handle_chat_message( "type": "message_saved", "data": { "id": saved_record.id, - "created_at": saved_record.created_at.replace(tzinfo=timezone.utc).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 9c380a5da..9a736b176 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -1,7 +1,6 @@ import asyncio import hashlib import json -from datetime import timezone from uuid import uuid4 from quart import g, request, websocket @@ -16,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 @@ -482,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.replace(tzinfo=timezone.utc).isoformat(), + "created_at": to_utc_isoformat( + saved_record.created_at + ), }, "session_id": session_id, } @@ -580,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.replace(tzinfo=timezone.utc).isoformat(), - "updated_at": session.updated_at.replace(tzinfo=timezone.utc).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 345844b48..db4a9ae75 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -3,11 +3,11 @@ import logging import os import socket +from datetime import datetime from pathlib import Path from typing import Protocol, cast import jwt -from datetime import datetime, timezone import psutil from flask.json.provider import DefaultJSONProvider from hypercorn.asyncio import serve @@ -20,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 * @@ -46,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, @@ -71,16 +79,6 @@ def __init__( self.app.config["MAX_CONTENT_LENGTH"] = ( 128 * 1024 * 1024 ) # 将 Flask 允许的最大上传文件体大小设置为 128 MB - - class AstrBotJSONProvider(DefaultJSONProvider): - def default(self, obj): - if isinstance(obj, datetime): - if obj.tzinfo is None: - # 默认为 UTC - return obj.replace(tzinfo=timezone.utc).isoformat() - return obj.isoformat() - return super().default(obj) - self.app.json = AstrBotJSONProvider(self.app) self.app.json.sort_keys = False self.app.before_request(self.auth_middleware)