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:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user