diff --git a/Dockerfile b/Dockerfile index d340750..d2c7712 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,11 @@ FROM python:3.11-slim +# Set timezone to Asia/Shanghai (UTC+8) by default +# Can be overridden with -e TZ= when running container +ENV TZ=Asia/Shanghai \ + TIMEZONE_OFFSET=8 +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + WORKDIR /app COPY requirements.txt . diff --git a/config/setting.toml b/config/setting.toml index 6250d6b..d86b465 100644 --- a/config/setting.toml +++ b/config/setting.toml @@ -52,3 +52,10 @@ at_auto_refresh_enabled = false [call_logic] call_mode = "default" + +[timezone] +# 时区偏移小时数,默认为东八区(中国标准时间) +# 可选值:-12 到 +14 的整数 +# 常用时区:中国/新加坡 +8, 日本/韩国 +9, 印度 +5.5, 伦敦 0, 纽约 -5, 洛杉矶 -8 +timezone_offset = 8 + diff --git a/docker-compose.yml b/docker-compose.yml index de86ed1..36af7a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,4 +11,6 @@ services: - ./config/setting.toml:/app/config/setting.toml environment: - PYTHONUNBUFFERED=1 + - TZ=Asia/Shanghai + - TIMEZONE_OFFSET=8 restart: unless-stopped diff --git a/src/api/admin.py b/src/api/admin.py index 641fd37..baf0915 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -928,9 +928,16 @@ async def get_stats(token: str = Depends(verify_admin_token)): @router.get("/api/logs") async def get_logs(limit: int = 100, token: str = Depends(verify_admin_token)): """Get recent logs with token email and task progress""" + from src.utils.timezone import convert_utc_to_local + logs = await db.get_recent_logs(limit) result = [] for log in logs: + # Convert UTC time to local timezone + created_at = log.get("created_at") + if created_at: + created_at = convert_utc_to_local(created_at) + log_data = { "id": log.get("id"), "token_id": log.get("token_id"), @@ -939,7 +946,7 @@ async def get_logs(limit: int = 100, token: str = Depends(verify_admin_token)): "operation": log.get("operation"), "status_code": log.get("status_code"), "duration": log.get("duration"), - "created_at": log.get("created_at"), + "created_at": created_at, "request_body": log.get("request_body"), "response_body": log.get("response_body"), "task_id": log.get("task_id") diff --git a/src/services/generation_handler.py b/src/services/generation_handler.py index d32a047..17fa606 100644 --- a/src/services/generation_handler.py +++ b/src/services/generation_handler.py @@ -813,7 +813,7 @@ class GenerationHandler: # Send retry notification to user if streaming if stream: yield self._format_stream_chunk( - reasoning_content=f"**生成失败,正在重试**\\n\\n第 {retry_count} 次重试(共 {max_retries} 次)...\\n\\n失败原因:{str(e)}\\n\\n" + reasoning_content=f"**生成失败,正在重试**\n\n第 {retry_count} 次重试(共 {max_retries} 次)...\n\n失败原因:{str(e)}\n\n" ) # Small delay before retry diff --git a/src/utils/timezone.py b/src/utils/timezone.py new file mode 100644 index 0000000..7eb8c44 --- /dev/null +++ b/src/utils/timezone.py @@ -0,0 +1,95 @@ +"""Timezone utilities for consistent time handling across the application""" + +from datetime import datetime, timezone, timedelta +from typing import Optional +import os + + +def get_timezone_offset() -> int: + """Get timezone offset in hours from environment variable or default to UTC+8 + + Returns: + int: Timezone offset in hours (default: 8 for China/Asia/Shanghai) + """ + try: + return int(os.getenv("TIMEZONE_OFFSET", "8")) + except ValueError: + return 8 + + +def get_timezone() -> timezone: + """Get timezone object based on configured offset + + Returns: + timezone: Timezone object with configured offset + """ + offset_hours = get_timezone_offset() + return timezone(timedelta(hours=offset_hours)) + + +def convert_utc_to_local(utc_time_str: Optional[str]) -> Optional[str]: + """Convert UTC timestamp string to local timezone with ISO format + + Args: + utc_time_str: UTC timestamp string (e.g., "2024-01-24 10:30:45") + + Returns: + str: ISO formatted timestamp with timezone info (e.g., "2024-01-24T18:30:45+08:00") + None: If conversion fails or input is None + """ + if not utc_time_str: + return None + + try: + # Parse SQLite timestamp (UTC) - handle both with and without 'Z' suffix + dt = datetime.fromisoformat(utc_time_str.replace('Z', '+00:00')) + + # If no timezone info, assume UTC + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + + # Convert to local timezone + local_tz = get_timezone() + dt_local = dt.astimezone(local_tz) + + # Return ISO format with timezone + return dt_local.isoformat() + except Exception as e: + # If conversion fails, return original value + print(f"Warning: Failed to convert timestamp '{utc_time_str}': {e}") + return utc_time_str + + +def get_current_local_time() -> datetime: + """Get current time in local timezone + + Returns: + datetime: Current datetime with local timezone + """ + return datetime.now(get_timezone()) + + +def format_local_time(dt: Optional[datetime], fmt: str = "%Y-%m-%d %H:%M:%S") -> str: + """Format datetime to string in local timezone + + Args: + dt: Datetime object to format + fmt: Format string (default: "%Y-%m-%d %H:%M:%S") + + Returns: + str: Formatted time string, or "-" if dt is None + """ + if not dt: + return "-" + + try: + # Convert to local timezone if needed + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + + local_tz = get_timezone() + dt_local = dt.astimezone(local_tz) + return dt_local.strftime(fmt) + except Exception as e: + print(f"Warning: Failed to format datetime: {e}") + return str(dt)