deepseek harness 在 UOS 下报 invalid-time-zone 的排查与修复

问题现象

启动 dsh web@deepseek-ai/dsh)后,浏览器访问 http://127.0.0.1:3080 新建会话并提交消息,对话框返回错误:

1
clientTimeZone must be UTC or a valid IANA Area/Location name

服务端响应 { ok: false, error: { code: "invalid-time-zone", details: { value: "Asia/Beijing" } } }

API 直连复现:

clientTimeZone 结果
Asia/Beijing invalid-time-zone
Asia/Shanghai OK

原因分析

请求链路

1
浏览器 → POST /api/session.prompt →  canonicalClientTimeZone() 校验 → Agent

客户端取时区

dsh-client-runtime/lib/client.js:7009

1
2
3
4
5
6
function resolvedClientTimeZone() {
const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
if (typeof timeZone !== "string" || timeZone.length === 0)
throw new Error("browser time zone is unavailable");
return timeZone;
}

浏览器从 OS 读取时区。本机 /etc/timezone = Asia/Beijing,所以浏览器上报 "Asia/Beijing"

服务端校验

dsh-host-apiproxy/lib/types/api-proxy.js:115-134

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/;

function canonicalClientTimeZone(value) {
if (value.length === 0 || value.trim() !== value
|| (value !== 'UTC' && !IANA_TIME_ZONE.test(value)))
return undefined;
try {
const canonical = new Intl.DateTimeFormat('en-US', { timeZone: value })
.resolvedOptions().timeZone;
if (canonical !== 'UTC' && !IANA_TIME_ZONE.test(canonical))
return undefined;
return canonical;
} catch {
return undefined;
}
}

校验两关:

  1. 字面必须为 "UTC" 或匹配 IANA Area/Location 正则(必须含 /)。
  2. Intl.DateTimeFormat({ timeZone }) 必须接受,并且 resolvedOptions().timeZone 回写也通过同样校验。

根因:ICU 78 移除了 Asia/Beijing 别名

Node v22.23.2
ICU 78.2
支持的时区数 418
1
2
3
4
5
$ node -e 'console.log(Intl.supportedValuesOf("timeZone").filter(z => z.startsWith("Asia/")).length)'
71

$ node -e 'console.log(["Asia/Beijing","Asia/Shanghai","Asia/Chongqing"].map(z => [z, Intl.supportedValuesOf("timeZone").includes(z)]))'
[ [ 'Asia/Beijing', false ], [ 'Asia/Shanghai', true ], [ 'Asia/Chongqing', false ] ]

服务端 ICU 78 不再认识 Asia/Beijing,于是 Intl.DateTimeFormat({ timeZone: 'Asia/Beijing' }) 抛错,canonicalClientTimeZone 返回 undefined,最终被映射成 invalid-time-zone

注意:/usr/share/zoneinfo/Asia/Beijing/Asia/Shanghai 是同一文件的两个软链(都指向 ../PRC),系统层面行为完全等价,仅名字在 ICU 列表中已被规范化。

解决方法

根本修复:改系统时区

把已被 ICU 移除的 Asia/Beijing 改为仍受支持的 Asia/Shanghai(二者指向同一 tzdata 文件,行为等价):

1
2
3
sudo ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
echo "Asia/Shanghai" | sudo tee /etc/timezone
date

然后重启浏览器(让浏览器重新读取系统时区),刷新 http://127.0.0.1:3080 再提交即可。

临时方案(不动系统时区)

Chrome DevTools → ⋮ → More tools → Sensors → Override timezone 选 Asia/Shanghai。仅对当前 tab 生效,刷新或新开 tab 即失效。

其他常见易踩坑的非法值

服务端正则 ^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$ 要求 Area/Location 形式,以下值都会被拒绝:

输入 结果 原因
"" 拒绝
" Asia/Shanghai " 拒绝 首尾空白
"CST" 拒绝 缩写,无 /
"GMT+8" 拒绝 偏移写法,无 /
"+08:00" 拒绝 偏移写法,无 /

经验小结

  • Web 应用跨 ICU/浏览器上报时区时,系统时区字符串与 ICU canonical name 不一致 是常见坑。
  • 本机 OS 虽保留 Asia/Beijing 的 tzdata 别名(指向 PRC),但运行时的 ICU 已统一为 Asia/Shanghai
  • 排查类似 invalid-time-zone 类问题,先在 Node 端跑一次 Intl.supportedValuesOf("timeZone") 看是否包含浏览器上报值,能快速定位。