Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
- macOS:`~/Library/Application Support/com.codexmanager.desktop/codexmanager.db`
- Linux:`~/.local/share/com.codexmanager.desktop/codexmanager.db`
- 如需调整数据库、代理、监听地址等运行配置,可继续查看 [环境变量与运行配置](docs/zh-CN/report/环境变量与运行配置说明.md)。
- Docker 镜像默认使用 `TZ=Asia/Shanghai`;compose 示例会优先沿用部署环境里的 `TZ`,没有设置时回退到 `Asia/Shanghai`,其他地区部署时请改成对应 IANA 时区。

## 页面展示
### 桌面端
Expand Down
13 changes: 10 additions & 3 deletions apps/src/app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import {
compareEnvOverrideItems,
ensureModelForwardRuleRows,
formatFreeAccountModelLabel,
formatRuntimeTimeZoneLabel,
inferServiceBindPreview,
matchesRecommendedWorkerSettings,
normalizeEnvRiskLevel,
Expand Down Expand Up @@ -2069,9 +2070,15 @@ function AdminSettingsPage() {
</div>
<div className="text-xs text-muted-foreground lg:col-span-2">
<span>
{snapshot.backgroundTasks.warmupCronEnabled
? t("定时账号预热已启用")
: t("定时账号预热未启用。多个计划用 | 分隔。")}
{t(
"计划按服务端时区 {timeZone} 执行。多个计划用 | 分隔。",
{
timeZone: formatRuntimeTimeZoneLabel(
snapshot.runtimeTimeZone,
t("服务端本地时区"),
),
},
)}
</span>
</div>
</div>
Expand Down
16 changes: 16 additions & 0 deletions apps/src/app/settings/settings-page-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
AppSettings,
BackgroundTaskSettings,
EnvOverrideCatalogItem,
RuntimeTimeZone,
} from "@/types";

export const ENV_DESCRIPTION_MAP: Record<string, string> = {
Expand Down Expand Up @@ -227,6 +228,21 @@ export function stringifyNumber(value: number | null | undefined): string {
return value == null ? "" : String(value);
}

export function formatRuntimeTimeZoneLabel(
runtimeTimeZone: RuntimeTimeZone | null | undefined,
localTimeZoneLabel = "服务端本地时区",
): string {
const name = String(runtimeTimeZone?.name || "").trim();
const offset = String(runtimeTimeZone?.offset || "").trim();
const displayName =
name && name !== "Local" ? name : localTimeZoneLabel;
const normalizedOffset =
offset && !offset.toUpperCase().startsWith("UTC")
? `UTC${offset.startsWith("+") || offset.startsWith("-") ? offset : `+${offset}`}`
: offset;
return normalizedOffset ? `${displayName} (${normalizedOffset})` : displayName;
}

export function readNumberField(
source: Record<string, unknown>,
key: string,
Expand Down
17 changes: 17 additions & 0 deletions apps/src/lib/api/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
AppSettings,
BackgroundTaskSettings,
QuotaGuardSettings,
RuntimeTimeZone,
DeviceAuthInfo,
EnvOverrideCatalogItem,
InstalledPluginSummary,
Expand Down Expand Up @@ -83,6 +84,12 @@ const DEFAULT_QUOTA_GUARD: QuotaGuardSettings = {
allowAllLowQuotaFallback: true,
};

const DEFAULT_RUNTIME_TIME_ZONE: RuntimeTimeZone = {
name: "Local",
offset: "",
source: "system",
};

/**
* 函数 `asObject`
*
Expand Down Expand Up @@ -1697,6 +1704,15 @@ export function normalizeQuotaGuard(payload: unknown): QuotaGuardSettings {
};
}

export function normalizeRuntimeTimeZone(payload: unknown): RuntimeTimeZone {
const source = asObject(payload);
return {
name: asString(source.name) || DEFAULT_RUNTIME_TIME_ZONE.name,
offset: asString(source.offset),
source: asString(source.source) || DEFAULT_RUNTIME_TIME_ZONE.source,
};
}

export function normalizeEnvOverrideCatalog(payload: unknown): EnvOverrideCatalogItem[] {
return asArray(payload).reduce<EnvOverrideCatalogItem[]>((result, item) => {
const source = asObject(item);
Expand Down Expand Up @@ -1803,6 +1819,7 @@ export function normalizeAppSettings(payload: unknown): AppSettings {
upstreamTotalTimeoutMs: asInteger(source.upstreamTotalTimeoutMs, 0, 0),
sseKeepaliveIntervalMs: asInteger(source.sseKeepaliveIntervalMs, 15_000, 1),
backgroundTasks: normalizeBackgroundTasks(source.backgroundTasks),
runtimeTimeZone: normalizeRuntimeTimeZone(source.runtimeTimeZone),
envOverrides: normalizeStringRecord(source.envOverrides),
envOverrideCatalog: normalizeEnvOverrideCatalog(source.envOverrideCatalog),
envOverrideReservedKeys: asArray(source.envOverrideReservedKeys).map((item) =>
Expand Down
3 changes: 3 additions & 0 deletions apps/src/lib/i18n/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,9 @@ export const EN_MESSAGES: MessageCatalog = {
网关保活线程: "Gateway keepalive worker",
令牌刷新轮询: "Token refresh polling",
"Worker 并发参数": "Worker concurrency",
"计划按服务端时区 {timeZone} 执行。多个计划用 | 分隔。":
"Schedule runs in server time zone {timeZone}. Separate multiple schedules with |.",
服务端本地时区: "Server local time zone",
当前档位: "Current preset",
自定义: "Custom",
"搜索变量...": "Search variables...",
Expand Down
3 changes: 3 additions & 0 deletions apps/src/lib/i18n/messages/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ export const KO_MESSAGES: MessageCatalog = {
网关策略: "게이트웨이 정책",
后台任务线程: "백그라운드 작업 스레드",
"Worker 并发参数": "Worker 동시성 설정",
"计划按服务端时区 {timeZone} 执行。多个计划用 | 分隔。":
"예약은 서버 시간대 {timeZone} 기준으로 실행됩니다. 여러 예약은 | 로 구분하세요.",
服务端本地时区: "서버 로컬 시간대",
当前档位: "현재 프리셋",
自定义: "사용자 지정",
"搜索变量...": "변수 검색...",
Expand Down
3 changes: 3 additions & 0 deletions apps/src/lib/i18n/messages/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,9 @@ export const RU_MESSAGES: MessageCatalog = {
网关策略: "Политика шлюза",
后台任务线程: "Фоновые рабочие потоки",
"Worker 并发参数": "Параметры конкуренции Worker",
"计划按服务端时区 {timeZone} 执行。多个计划用 | 分隔。":
"Расписание выполняется в часовом поясе сервера {timeZone}. Несколько расписаний разделяйте символом |.",
服务端本地时区: "Локальный часовой пояс сервера",
当前档位: "Текущий профиль",
自定义: "Свой",
"搜索变量...": "Поиск переменных...",
Expand Down
5 changes: 5 additions & 0 deletions apps/src/lib/store/useAppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ export const useAppStore = create<AppState>((set) => ({
warmupCronEnabled: false,
warmupCronExpression: "",
},
runtimeTimeZone: {
name: "Local",
offset: "",
source: "system",
},
envOverrides: {},
envOverrideCatalog: [],
envOverrideReservedKeys: [],
Expand Down
7 changes: 7 additions & 0 deletions apps/src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export interface BackgroundTaskSettings {
warmupCronExpression: string;
}

export interface RuntimeTimeZone {
name: string;
offset: string;
source: string;
}

export interface QuotaGuardSettings {
enabled: boolean;
primaryMinRemainingPercent: number;
Expand Down Expand Up @@ -76,6 +82,7 @@ export interface AppSettings {
upstreamTotalTimeoutMs: number;
sseKeepaliveIntervalMs: number;
backgroundTasks: BackgroundTaskSettings;
runtimeTimeZone: RuntimeTimeZone;
envOverrides: Record<string, string>;
envOverrideCatalog: EnvOverrideCatalogItem[];
envOverrideReservedKeys: string[];
Expand Down
32 changes: 32 additions & 0 deletions apps/tests/settings-page-helpers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,35 @@ test("compareEnvOverrideItems 将高风险请求语义项排在普通项之后",
"CODEXMANAGER_STRICT_REQUEST_PARAM_ALLOWLIST",
]);
});

test("formatRuntimeTimeZoneLabel 显示后端传回的时区和偏移", () => {
assert.equal(
helpers.formatRuntimeTimeZoneLabel({
name: "Asia/Shanghai",
offset: "+08:00",
source: "TZ",
}),
"Asia/Shanghai (UTC+08:00)"
);

assert.equal(
helpers.formatRuntimeTimeZoneLabel({
name: "Local",
offset: "-05:00",
source: "system",
}),
"服务端本地时区 (UTC-05:00)"
);

assert.equal(
helpers.formatRuntimeTimeZoneLabel(
{
name: "Local",
offset: "+01:00",
source: "system",
},
"Server local time zone"
),
"Server local time zone (UTC+01:00)"
);
});
25 changes: 25 additions & 0 deletions crates/service/src/app_settings/api/current.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::app_settings::{list_app_settings_map, listener_bind_addr_for_mode};
use crate::initialize_storage_if_needed;
use crate::{current_web_auth_mode, distribution_enabled, web_access_password_configured};
use chrono::Local;
use codexmanager_core::rpc::types::ModelInfo;
use serde_json::Value;
use std::collections::BTreeMap;
Expand Down Expand Up @@ -78,6 +79,28 @@ fn normalize_service_bind_mode_value(raw: Option<&str>) -> &'static str {
}
}

fn current_runtime_time_zone_value() -> Value {
let env_tz = std::env::var("TZ")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let offset = Local::now().offset().to_string();
let source = if env_tz.is_some() { "TZ" } else { "system" };
let name = env_tz.unwrap_or_else(|| {
if offset == "+00:00" {
"UTC".to_string()
} else {
"Local".to_string()
}
});

serde_json::json!({
"name": name,
"offset": offset,
"source": source,
})
}

/// 函数 `current_app_settings_value`
///
/// 作者: gaohongshun
Expand All @@ -97,6 +120,7 @@ pub(super) fn current_app_settings_value(
initialize_storage_if_needed()?;
sync_runtime_settings_from_storage();
let background_tasks = current_background_tasks_snapshot_value()?;
let runtime_time_zone = current_runtime_time_zone_value();
let update_auto_check = current_update_auto_check_enabled();
let persisted_close_to_tray = current_close_to_tray_on_close_setting();
let close_to_tray = close_to_tray_on_close.unwrap_or(persisted_close_to_tray);
Expand Down Expand Up @@ -259,6 +283,7 @@ pub(super) fn current_app_settings_value(
"webAccessPasswordConfigured": web_access_password_configured(),
});
if let Some(object) = result.as_object_mut() {
object.insert("runtimeTimeZone".to_string(), runtime_time_zone);
object.insert("webAuthMode".to_string(), current_web_auth_mode().into());
object.insert(
"webAuthModeOptions".to_string(),
Expand Down
31 changes: 31 additions & 0 deletions crates/service/tests/app_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,37 @@ fn app_settings_get_defaults_codex_cli_guide_to_false() {
});
}

#[test]
fn app_settings_get_exposes_runtime_time_zone_from_tz_env() {
with_temp_db(|_| {
let _tz = override_env_vars(&[("TZ", Some("Asia/Shanghai"))]);

let snapshot = codexmanager_service::app_settings_get().expect("get app settings");
let runtime_time_zone = snapshot
.get("runtimeTimeZone")
.and_then(|value| value.as_object())
.expect("runtime time zone object");

assert_eq!(
runtime_time_zone.get("name").and_then(|value| value.as_str()),
Some("Asia/Shanghai")
);
assert_eq!(
runtime_time_zone
.get("source")
.and_then(|value| value.as_str()),
Some("TZ")
);
assert!(
runtime_time_zone
.get("offset")
.and_then(|value| value.as_str())
.is_some_and(|value| !value.is_empty()),
"runtime time zone should include an offset: {runtime_time_zone:?}"
);
});
}

/// 函数 `sync_runtime_settings_from_storage_applies_saved_runtime_values`
///
/// 作者: gaohongshun
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ services:
CODEXMANAGER_SERVICE_ADDR: 0.0.0.0:48760
CODEXMANAGER_WEB_ADDR: 0.0.0.0:48761
CODEXMANAGER_WEB_NO_OPEN: "1"
TZ: ${TZ:-Asia/Shanghai}
CODEXMANAGER_DB_PATH: /data/codexmanager.db
CODEXMANAGER_RPC_TOKEN_FILE: /data/codexmanager.rpc-token
CODEXMANAGER_GATEWAY_TRACE_STDOUT: "1"
Expand Down
4 changes: 3 additions & 1 deletion docker/Dockerfile.all-in-one
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ FROM debian:bookworm-slim
ARG APP_USER=codexmanager
ARG APP_UID=10001

ENV TZ=Asia/Shanghai

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates wget \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates wget tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid ${APP_UID} ${APP_USER} \
&& useradd --system --uid ${APP_UID} --gid ${APP_UID} --home-dir /app --create-home --shell /usr/sbin/nologin ${APP_USER} \
Expand Down
4 changes: 3 additions & 1 deletion docker/Dockerfile.service
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ FROM debian:bookworm-slim
ARG APP_USER=codexmanager
ARG APP_UID=10001

ENV TZ=Asia/Shanghai

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates wget \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates wget tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid ${APP_UID} ${APP_USER} \
&& useradd --system --uid ${APP_UID} --gid ${APP_UID} --home-dir /app --create-home --shell /usr/sbin/nologin ${APP_USER} \
Expand Down
4 changes: 3 additions & 1 deletion docker/Dockerfile.service.release
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ FROM debian:bookworm-slim
ARG APP_USER=codexmanager
ARG APP_UID=10001

ENV TZ=Asia/Shanghai

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates wget \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates wget tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid ${APP_UID} ${APP_USER} \
&& useradd --system --uid ${APP_UID} --gid ${APP_UID} --home-dir /app --create-home --shell /usr/sbin/nologin ${APP_USER} \
Expand Down
4 changes: 3 additions & 1 deletion docker/Dockerfile.web
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ FROM debian:bookworm-slim
ARG APP_USER=codexmanager
ARG APP_UID=10001

ENV TZ=Asia/Shanghai

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates wget \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates wget tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid ${APP_UID} ${APP_USER} \
&& useradd --system --uid ${APP_UID} --gid ${APP_UID} --home-dir /app --create-home --shell /usr/sbin/nologin ${APP_USER} \
Expand Down
4 changes: 3 additions & 1 deletion docker/Dockerfile.web.release
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ FROM debian:bookworm-slim
ARG APP_USER=codexmanager
ARG APP_UID=10001

ENV TZ=Asia/Shanghai

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates wget \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates wget tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid ${APP_UID} ${APP_USER} \
&& useradd --system --uid ${APP_UID} --gid ${APP_UID} --home-dir /app --create-home --shell /usr/sbin/nologin ${APP_USER} \
Expand Down
1 change: 1 addition & 0 deletions docker/docker-compose.all-in-one.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
CODEXMANAGER_SERVICE_ADDR: 0.0.0.0:48760
CODEXMANAGER_WEB_ADDR: 0.0.0.0:48761
CODEXMANAGER_WEB_NO_OPEN: "1"
TZ: ${TZ:-Asia/Shanghai}
CODEXMANAGER_DB_PATH: /data/codexmanager.db
CODEXMANAGER_RPC_TOKEN_FILE: /data/codexmanager.rpc-token
CODEXMANAGER_GATEWAY_TRACE_STDOUT: "1"
Expand Down
Loading