fix: resolve unhandled UTC timezone offset for timestamps in conversation records (#5580)

* fix: resolve unhandled UTC timezone offset for timestamps in conversation records

* fix: standardize timezone imports

* fix: unify UTC datetime normalization in dashboard routes

---------

Co-authored-by: 邹永赫 <1259085392@qq.com>
This commit is contained in:
WintryWind
2026-03-01 15:10:35 +08:00
committed by GitHub
parent 451ad685ae
commit fd223bb259
8 changed files with 70 additions and 23 deletions
+5 -2
View File
@@ -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,
+27
View File
@@ -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()
+2 -5
View File
@@ -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:
+6 -3
View File
@@ -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),
}
)
+9 -8
View File
@@ -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
]
+4 -1
View File
@@ -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
),
},
},
)
+6 -3
View File
@@ -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),
}
)
+11 -1
View File
@@ -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)