From 2676927c1aeb00433db05fe85130c737b271bc42 Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:28:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(webui):=20=E4=B8=AD=E6=96=87=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=90=8D=E7=99=BB=E5=BD=95=E5=90=8E=E6=89=80=E6=9C=89?= =?UTF-8?q?=E5=86=99=E6=93=8D=E4=BD=9C=E9=9D=99=E9=BB=98=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为什么:headers() 把用户名塞进 X-Farm-Id 请求头,而 HTTP 头只允许 Latin-1 字符——中文用户名(如"阳")会让 fetch 在发送前直接抛 TypeError(String contains non ISO-8859-1 code point)。登录时 me 还是 null(头为 ASCII 的 default)所以能登录成功,登录之后 me 变成中文,所有走 api() 的写操作(播种/浇水/收获/卖出)全部 静默失败;页面靠 SSE 推送照常渲染,看起来一切正常,用户表现为 "点播种没反应"。 - headers():非 ASCII 的牧场名/令牌不再塞头。登录态本来就由 Cookie 鉴权,X-Farm-Id 只是匿名脚本模式的备用通道;ASCII 名 (default、bot 等)行为不变。 - api():捕获 fetch 网络异常并 toast 提示。此前 Promise 拒绝 无人接住,这类故障完全不可见,正是本 bug 难排查的原因。 - localStorage 读写加 try/catch 兜底:sandbox iframe 若不带 allow-same-origin,裸访问即抛 SecurityError,会炸掉整个启动 脚本(启动段就有三处裸调用)。 验证:headless Chrome 真机复现 → 修复 → UI 全流程回归(登录 "阳" → 点播种 → 选小麦 → toast 与地块状态正确更新);ruff / pyright / 528 项测试全绿。 --- farm/server/webui.py | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/farm/server/webui.py b/farm/server/webui.py index bda662f..37fc79d 100644 --- a/farm/server/webui.py +++ b/farm/server/webui.py @@ -212,6 +212,15 @@ function goodsName(key) { return cropNames[key] || key; } +/* sandbox iframe 可能禁用 localStorage(无 allow-same-origin 时访问即抛 + SecurityError),任何一处裸调用都会炸掉整个启动脚本。 */ +function lsGet(key) { + try { return localStorage.getItem(key); } catch (e) { return null; } +} +function lsSet(key, val) { + try { localStorage.setItem(key, val); } catch (e) {} +} + function toast(text) { const el = $("toast"); el.textContent = text; @@ -228,14 +237,32 @@ function setStatus(text, cls) { function farmName() { return me || "default"; } function token() { return ($("token").value || "").trim(); } +/* HTTP 头只允许 Latin-1 字符:中文用户名塞进 X-Farm-Id 会让 fetch 在发送前 + 就抛 TypeError,登录后所有写操作静默失败(表现为“点播种没反应”)。 + 登录态由 Cookie 鉴权,非 ASCII 的值一律不塞头。 */ +function latin1(s) { + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if (c < 32 || c > 126) return false; + } + return true; +} + function headers() { - const h = { "Content-Type": "application/json", "X-Farm-Id": farmName() }; - if (token()) h["X-API-Token"] = token(); + const h = { "Content-Type": "application/json" }; + if (latin1(farmName())) h["X-Farm-Id"] = farmName(); + if (token() && latin1(token())) h["X-API-Token"] = token(); return h; } async function api(path, options) { - const res = await fetch(path, options); + let res; + try { + res = await fetch(path, options); + } catch (e) { + toast("网络错误:" + e.message); + return null; + } let data = null; try { data = await res.json(); } catch (e) { data = { ok: false, message: "响应不是 JSON" }; } if (!res.ok || data.ok === false) { @@ -507,7 +534,7 @@ function applyIdentity() { $("logout").classList.toggle("hidden", !me); $("loginmask").classList.toggle("show", !me); if (me) { - localStorage.setItem("sf.farm", me); + lsSet("sf.farm", me); connect(); } else if (es) { try { es.close(); } catch (e) {} @@ -534,9 +561,9 @@ $("logout").addEventListener("click", async () => { $("loginname").focus(); }); /* ---- 启动 ---- */ -$("token").value = localStorage.getItem("sf.token") || ""; +$("token").value = lsGet("sf.token") || ""; $("token").addEventListener("change", () => { - localStorage.setItem("sf.token", token()); + lsSet("sf.token", token()); if (me) connect(); }); whoami();