feat: show effective AstrBot time in settings (#9581)

This commit is contained in:
Ruochen Pan
2026-08-07 11:25:51 +08:00
committed by GitHub
parent 5259c4b9c5
commit 78214cac5a
6 changed files with 172 additions and 3 deletions
+24 -1
View File
@@ -5,8 +5,10 @@ import copy
import inspect
import os
import traceback
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from astrbot.core import file_token_service, logger
from astrbot.core.config.astrbot_config import AstrBotConfig
@@ -474,7 +476,28 @@ class ConfigProfileService:
}
def get_system_config(self) -> dict:
return self.get_system_schema()
"""Return the system configuration with the server's effective time.
Returns:
System configuration metadata, an aware UTC timestamp, and the
effective UTC offset in minutes.
"""
data = self.get_system_schema()
server_utc_time = datetime.now(timezone.utc)
timezone_name = str(data["config"].get("timezone") or "").strip()
if timezone_name:
try:
configured_time = server_utc_time.astimezone(ZoneInfo(timezone_name))
except (ValueError, ZoneInfoNotFoundError):
configured_time = server_utc_time.astimezone()
else:
configured_time = server_utc_time.astimezone()
utc_offset = configured_time.utcoffset()
data["server_utc_time"] = server_utc_time.isoformat()
data["server_utc_offset_minutes"] = (
int(utc_offset.total_seconds() / 60) if utc_offset else 0
)
return data
def list_profiles(self) -> dict:
return {"info_list": self.acm.get_conf_list()}
@@ -67,6 +67,9 @@
"subtitle": "Rendering strategy, endpoint, and custom templates for text-to-image."
}
},
"timePreview": {
"label": "AstrBot current time"
},
"messages": {
"loadFailed": "Failed to load system config",
"saveSuccess": "System config saved",
@@ -67,6 +67,9 @@
"subtitle": "Стратегия, endpoint и пользовательские шаблоны для рендеринга."
}
},
"timePreview": {
"label": "Текущее время AstrBot"
},
"messages": {
"loadFailed": "Не удалось загрузить системную конфигурацию",
"saveSuccess": "Системная конфигурация сохранена",
@@ -67,6 +67,9 @@
"subtitle": "文本转图像渲染策略、端点和自定义模板。"
}
},
"timePreview": {
"label": "AstrBot 当前时间"
},
"messages": {
"loadFailed": "加载系统配置失败",
"saveSuccess": "系统配置已保存",
+114 -2
View File
@@ -64,7 +64,20 @@
:class="{ 'system-config-group--with-cleanup': group.key === 'tempStorage' }"
@focusout.capture="scheduleSystemConfigAutoSave"
>
<div class="system-config-group__title">{{ group.title }}</div>
<div class="system-config-group__heading">
<div class="system-config-group__title">{{ group.title }}</div>
<div
v-if="group.key === 'runtime' && timezoneTimePreview"
class="timezone-time-preview"
>
<span
class="timezone-time-preview__item"
>
{{ tm('systemConfig.timePreview.label') }} ·
{{ timezoneTimePreview.offsetLabel }} {{ timezoneTimePreview.localTime }}
</span>
</div>
</div>
<AstrBotConfigV4
:metadata="group.metadata"
:iterable="systemConfigData"
@@ -573,6 +586,11 @@ const configSave2faSaving = ref(false);
const configSave2faRotationHint = ref('');
const configSavePendingData = ref(null);
const systemConfigAutoSaveTimer = ref(null);
const serverUtcEpochMs = ref(null);
const serverUtcOffsetMinutes = ref(null);
const serverClockAnchorMs = ref(null);
const serverClockTickMs = ref(null);
const serverClockTimer = ref(null);
const activeSettingsSection = ref('general');
const changelogDialog = ref(false);
@@ -684,6 +702,34 @@ const systemConfigHasChanges = computed(() => (
JSON.stringify(systemConfigData.value || {}) !== systemConfigLastSavedSnapshot.value
));
const timezoneTimePreview = computed(() => {
if (
serverUtcEpochMs.value === null ||
serverUtcOffsetMinutes.value === null ||
serverClockAnchorMs.value === null ||
serverClockTickMs.value === null
) {
return null;
}
const localDate = new Date(
serverUtcEpochMs.value +
Math.max(0, serverClockTickMs.value - serverClockAnchorMs.value) +
serverUtcOffsetMinutes.value * 60_000
);
const localIsoTime = localDate.toISOString();
const absoluteOffset = Math.abs(serverUtcOffsetMinutes.value);
const offsetHours = Math.floor(absoluteOffset / 60);
const offsetMinutes = absoluteOffset % 60;
const offsetLabel = serverUtcOffsetMinutes.value === 0
? 'UTC'
: `UTC${serverUtcOffsetMinutes.value > 0 ? '+' : '-'}${offsetHours}${offsetMinutes ? `:${String(offsetMinutes).padStart(2, '0')}` : ''}`;
return {
localTime: `${localIsoTime.slice(0, 4)}/${localIsoTime.slice(5, 7)}/${localIsoTime.slice(8, 10)} ${localIsoTime.slice(11, 16)}`,
offsetLabel
};
});
const systemConfigGroups = computed(() => {
const systemSection = systemConfigMetadata.value?.system_group?.metadata?.system || {};
const systemItems = systemSection.items || {};
@@ -820,6 +866,12 @@ const loadSystemConfig = async () => {
}
systemConfigData.value = res.data.data?.config || {};
systemConfigMetadata.value = res.data.data?.metadata || {};
const parsedServerTime = Date.parse(res.data.data?.server_utc_time || '');
const parsedUtcOffset = Number(res.data.data?.server_utc_offset_minutes);
serverUtcEpochMs.value = Number.isNaN(parsedServerTime) ? null : parsedServerTime;
serverUtcOffsetMinutes.value = Number.isFinite(parsedUtcOffset) ? parsedUtcOffset : null;
serverClockAnchorMs.value = serverUtcEpochMs.value === null ? null : performance.now();
serverClockTickMs.value = serverClockAnchorMs.value;
systemConfigLastSavedSnapshot.value = JSON.stringify(systemConfigData.value || {});
systemConfigRestartRequired.value = false;
} catch (error) {
@@ -864,6 +916,23 @@ const saveSystemConfig = async (configOverride = null, headers = {}, allow2faPro
configSave2faError.value = '';
systemConfigData.value = configPayload;
systemConfigLastSavedSnapshot.value = JSON.stringify(configPayload);
serverUtcEpochMs.value = null;
serverUtcOffsetMinutes.value = null;
serverClockAnchorMs.value = null;
serverClockTickMs.value = null;
try {
const timeRes = await systemConfigApi.get();
if (timeRes.data.status === 'ok') {
const parsedServerTime = Date.parse(timeRes.data.data?.server_utc_time || '');
const parsedUtcOffset = Number(timeRes.data.data?.server_utc_offset_minutes);
serverUtcEpochMs.value = Number.isNaN(parsedServerTime) ? null : parsedServerTime;
serverUtcOffsetMinutes.value = Number.isFinite(parsedUtcOffset) ? parsedUtcOffset : null;
serverClockAnchorMs.value = serverUtcEpochMs.value === null ? null : performance.now();
serverClockTickMs.value = serverClockAnchorMs.value;
}
} catch {
// Keep the preview hidden until the backend time can be confirmed.
}
systemConfigRestartRequired.value = true;
showToast(res.data.message || tm('systemConfig.messages.saveSuccess'), 'success');
return { success: true };
@@ -1069,6 +1138,9 @@ const resetThemeColors = () => {
onMounted(async () => {
await Promise.all([loadApiKeys(), loadSystemConfig()]);
serverClockTimer.value = window.setInterval(() => {
serverClockTickMs.value = performance.now();
}, 1000);
const hash = window.location.hash;
if (hash.includes('settings-appearance')) {
activeSettingsSection.value = 'appearance';
@@ -1086,6 +1158,10 @@ onMounted(async () => {
});
onUnmounted(() => {
if (serverClockTimer.value) {
clearInterval(serverClockTimer.value);
serverClockTimer.value = null;
}
if (systemConfigAutoSaveTimer.value) {
clearTimeout(systemConfigAutoSaveTimer.value);
systemConfigAutoSaveTimer.value = null;
@@ -1331,8 +1407,15 @@ onUnmounted(() => {
margin-bottom: 0;
}
.system-config-group__title {
.system-config-group__heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.system-config-group__title {
color: rgb(var(--v-theme-on-surface));
font-size: 1.04rem;
font-weight: 760;
@@ -1340,6 +1423,26 @@ onUnmounted(() => {
line-height: 1.32;
}
.timezone-time-preview {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 5px;
font-variant-numeric: tabular-nums;
}
.timezone-time-preview__item {
padding: 3px 7px;
border: 1px solid rgba(var(--v-theme-primary), 0.16);
border-radius: 6px;
color: rgb(var(--v-theme-primary));
background: rgba(var(--v-theme-primary), 0.055);
font-size: 0.72rem;
font-weight: 600;
line-height: 1.3;
white-space: nowrap;
}
.system-config-group :deep(.v-card) {
margin-bottom: 0 !important;
padding-bottom: 0 !important;
@@ -1736,6 +1839,15 @@ onUnmounted(() => {
max-width: none;
}
.system-config-group__heading {
align-items: flex-start;
flex-direction: column;
}
.timezone-time-preview {
justify-content: flex-start;
}
.system-config-group :deep(.config-row) {
padding: 14px 16px;
}
+25
View File
@@ -0,0 +1,25 @@
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import patch
from astrbot.dashboard.services.config_service import ConfigProfileService
def test_get_system_config_includes_effective_server_time() -> None:
"""Verify that the response includes server UTC time and configured offset."""
fixed_time = datetime(2026, 8, 7, 2, 31, tzinfo=timezone.utc)
service = ConfigProfileService(
SimpleNamespace(
astrbot_config_mgr=SimpleNamespace(
confs={"default": {"timezone": "Asia/Shanghai"}}
)
)
)
with patch("astrbot.dashboard.services.config_service.datetime") as mock_datetime:
mock_datetime.now.return_value = fixed_time
result = service.get_system_config()
assert result["server_utc_time"] == "2026-08-07T02:31:00+00:00"
assert result["server_utc_offset_minutes"] == 480
assert result["config"]["timezone"] == "Asia/Shanghai"