Skip to content

Commit fd223bb

Browse files
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>
1 parent 451ad68 commit fd223bb

File tree

8 files changed

+70
-23
lines changed

8 files changed

+70
-23
lines changed

astrbot/core/conversation_mgr.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from astrbot.core.agent.message import AssistantMessageSegment, UserMessageSegment
1212
from astrbot.core.db import BaseDatabase
1313
from astrbot.core.db.po import Conversation, ConversationV2
14+
from astrbot.core.utils.datetime_utils import to_utc_timestamp
1415

1516

1617
class ConversationManager:
@@ -58,8 +59,10 @@ async def _trigger_session_deleted(self, unified_msg_origin: str) -> None:
5859

5960
def _convert_conv_from_v2_to_v1(self, conv_v2: ConversationV2) -> Conversation:
6061
"""将 ConversationV2 对象转换为 Conversation 对象"""
61-
created_at = int(conv_v2.created_at.timestamp())
62-
updated_at = int(conv_v2.updated_at.timestamp())
62+
created_ts = to_utc_timestamp(conv_v2.created_at)
63+
updated_ts = to_utc_timestamp(conv_v2.updated_at)
64+
created_at = int(created_ts) if created_ts is not None else 0
65+
updated_at = int(updated_ts) if updated_ts is not None else 0
6366
return Conversation(
6467
platform_id=conv_v2.platform_id,
6568
user_id=conv_v2.user_id,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from datetime import datetime, timezone
2+
3+
4+
def normalize_datetime_utc(dt: datetime | None) -> datetime | None:
5+
"""Normalize datetime values to UTC.
6+
7+
Naive datetimes are interpreted as UTC to match SQLite storage behavior.
8+
"""
9+
if dt is None:
10+
return None
11+
if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None:
12+
return dt.replace(tzinfo=timezone.utc)
13+
return dt.astimezone(timezone.utc)
14+
15+
16+
def to_utc_isoformat(dt: datetime | None) -> str | None:
17+
normalized = normalize_datetime_utc(dt)
18+
if normalized is None:
19+
return None
20+
return normalized.isoformat()
21+
22+
23+
def to_utc_timestamp(dt: datetime | None) -> float | None:
24+
normalized = normalize_datetime_utc(dt)
25+
if normalized is None:
26+
return None
27+
return normalized.timestamp()

astrbot/dashboard/routes/api_key.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from quart import g, request
66

77
from astrbot.core.db import BaseDatabase
8+
from astrbot.core.utils.datetime_utils import normalize_datetime_utc
89

910
from .route import Response, Route, RouteContext
1011

@@ -25,11 +26,7 @@ def __init__(self, context: RouteContext, db: BaseDatabase) -> None:
2526

2627
@staticmethod
2728
def _normalize_utc(dt: datetime | None) -> datetime | None:
28-
if dt is None:
29-
return None
30-
if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None:
31-
return dt.replace(tzinfo=timezone.utc)
32-
return dt.astimezone(timezone.utc)
29+
return normalize_datetime_utc(dt)
3330

3431
@classmethod
3532
def _serialize_datetime(cls, dt: datetime | None) -> str | None:

astrbot/dashboard/routes/chat.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
2323
from astrbot.core.utils.active_event_registry import active_event_registry
2424
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
25+
from astrbot.core.utils.datetime_utils import to_utc_isoformat
2526

2627
from .route import Response, Route, RouteContext
2728

@@ -486,7 +487,9 @@ async def stream():
486487
"type": "message_saved",
487488
"data": {
488489
"id": saved_record.id,
489-
"created_at": saved_record.created_at.astimezone().isoformat(),
490+
"created_at": to_utc_isoformat(
491+
saved_record.created_at
492+
),
490493
},
491494
}
492495
try:
@@ -718,8 +721,8 @@ async def get_sessions(self):
718721
"creator": session.creator,
719722
"display_name": session.display_name,
720723
"is_group": session.is_group,
721-
"created_at": session.created_at.astimezone().isoformat(),
722-
"updated_at": session.updated_at.astimezone().isoformat(),
724+
"created_at": to_utc_isoformat(session.created_at),
725+
"updated_at": to_utc_isoformat(session.updated_at),
723726
}
724727
)
725728

astrbot/dashboard/routes/chatui_project.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from quart import g, request
22

33
from astrbot.core.db import BaseDatabase
4+
from astrbot.core.utils.datetime_utils import to_utc_isoformat
45

56
from .route import Response, Route, RouteContext
67

@@ -51,8 +52,8 @@ async def create_project(self):
5152
"title": project.title,
5253
"emoji": project.emoji,
5354
"description": project.description,
54-
"created_at": project.created_at.astimezone().isoformat(),
55-
"updated_at": project.updated_at.astimezone().isoformat(),
55+
"created_at": to_utc_isoformat(project.created_at),
56+
"updated_at": to_utc_isoformat(project.updated_at),
5657
}
5758
)
5859
.__dict__
@@ -70,8 +71,8 @@ async def list_projects(self):
7071
"title": project.title,
7172
"emoji": project.emoji,
7273
"description": project.description,
73-
"created_at": project.created_at.astimezone().isoformat(),
74-
"updated_at": project.updated_at.astimezone().isoformat(),
74+
"created_at": to_utc_isoformat(project.created_at),
75+
"updated_at": to_utc_isoformat(project.updated_at),
7576
}
7677
for project in projects
7778
]
@@ -102,8 +103,8 @@ async def get_project(self):
102103
"title": project.title,
103104
"emoji": project.emoji,
104105
"description": project.description,
105-
"created_at": project.created_at.astimezone().isoformat(),
106-
"updated_at": project.updated_at.astimezone().isoformat(),
106+
"created_at": to_utc_isoformat(project.created_at),
107+
"updated_at": to_utc_isoformat(project.updated_at),
107108
}
108109
)
109110
.__dict__
@@ -236,8 +237,8 @@ async def get_project_sessions(self):
236237
"creator": session.creator,
237238
"display_name": session.display_name,
238239
"is_group": session.is_group,
239-
"created_at": session.created_at.astimezone().isoformat(),
240-
"updated_at": session.updated_at.astimezone().isoformat(),
240+
"created_at": to_utc_isoformat(session.created_at),
241+
"updated_at": to_utc_isoformat(session.updated_at),
241242
}
242243
for session in sessions
243244
]

astrbot/dashboard/routes/live_chat.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
)
2222
from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
2323
from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_temp_path
24+
from astrbot.core.utils.datetime_utils import to_utc_isoformat
2425

2526
from .route import Route, RouteContext
2627

@@ -621,7 +622,9 @@ async def _handle_chat_message(
621622
"type": "message_saved",
622623
"data": {
623624
"id": saved_record.id,
624-
"created_at": saved_record.created_at.astimezone().isoformat(),
625+
"created_at": to_utc_isoformat(
626+
saved_record.created_at
627+
),
625628
},
626629
},
627630
)

astrbot/dashboard/routes/open_api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
webchat_message_parts_have_content,
1616
)
1717
from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
18+
from astrbot.core.utils.datetime_utils import to_utc_isoformat
1819

1920
from .api_key import ALL_OPEN_API_SCOPES
2021
from .chat import ChatRoute
@@ -481,7 +482,9 @@ async def _handle_chat_ws_send(self, post_data: dict) -> None:
481482
"type": "message_saved",
482483
"data": {
483484
"id": saved_record.id,
484-
"created_at": saved_record.created_at.astimezone().isoformat(),
485+
"created_at": to_utc_isoformat(
486+
saved_record.created_at
487+
),
485488
},
486489
"session_id": session_id,
487490
}
@@ -579,8 +582,8 @@ async def get_chat_sessions(self):
579582
"creator": session.creator,
580583
"display_name": session.display_name,
581584
"is_group": session.is_group,
582-
"created_at": session.created_at.astimezone().isoformat(),
583-
"updated_at": session.updated_at.astimezone().isoformat(),
585+
"created_at": to_utc_isoformat(session.created_at),
586+
"updated_at": to_utc_isoformat(session.updated_at),
584587
}
585588
)
586589

astrbot/dashboard/server.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import os
55
import socket
6+
from datetime import datetime
67
from pathlib import Path
78
from typing import Protocol, cast
89

@@ -19,6 +20,7 @@
1920
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
2021
from astrbot.core.db import BaseDatabase
2122
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
23+
from astrbot.core.utils.datetime_utils import to_utc_isoformat
2224
from astrbot.core.utils.io import get_local_ip_addresses
2325

2426
from .routes import *
@@ -45,6 +47,13 @@ def _parse_env_bool(value: str | None, default: bool) -> bool:
4547
return value.strip().lower() in {"1", "true", "yes", "on"}
4648

4749

50+
class AstrBotJSONProvider(DefaultJSONProvider):
51+
def default(self, obj):
52+
if isinstance(obj, datetime):
53+
return to_utc_isoformat(obj)
54+
return super().default(obj)
55+
56+
4857
class AstrBotDashboard:
4958
def __init__(
5059
self,
@@ -70,7 +79,8 @@ def __init__(
7079
self.app.config["MAX_CONTENT_LENGTH"] = (
7180
128 * 1024 * 1024
7281
) # 将 Flask 允许的最大上传文件体大小设置为 128 MB
73-
cast(DefaultJSONProvider, self.app.json).sort_keys = False
82+
self.app.json = AstrBotJSONProvider(self.app)
83+
self.app.json.sort_keys = False
7484
self.app.before_request(self.auth_middleware)
7585
# token 用于验证请求
7686
logging.getLogger(self.app.name).removeHandler(default_handler)

0 commit comments

Comments
 (0)