☰
Star-Office-UI 2026-03-05 更新深度解读:CDN 缓存 404、异步生图防 524 超时与移动端侧边栏的稳定性修复实践
2026/9/28 2:55:28 网站建设 项目流程
  • 前端
  • 后端
  • AI 应用
  • 数据可视化

【免费下载链接】Star-Office-UI

A pixel office for your OpenClaw: turn invisible work states into a cozy little space with characters, daily notes, and guest agents. Code under MIT; art assets for non-commercial learning only.

项目地址:https://gitcode.com/gh_mirrors/st/Star-Office-UI
点击查看免费下载

本篇技术指南围绕 Star-Office-UI 2026-03-05 的 8 个 commit 展开,核心聚焦「稳定性修复 + 移动端体验 + 安全收尾」三大主题:如何修复 CDN 缓存 404 导致整站无法加载的故障、如何通过后台线程 + 前端轮询规避 Cloudflare 524 生图超时、如何用遮罩层 +100dvh解决移动端抽屉侧边栏滚动穿透,以及如何为 join key 增加过期时间与并发上限。读完本文,你将掌握这套像素风办公室项目(由 Flask 后端 backend/app.py 与单页前端 frontend/index.html 构成)在生产环境中的缓存策略、异步任务模式、移动端滚动锁定与接入密钥治理的完整实现方案。


变更概览:一次「小而全」的稳定性收尾

本次更新共覆盖 8 个 commit,可以归类为三组:

#Commit分类说明
1878793d🐛 fix修复 CDN 缓存 404 导致页面无法加载
2cc22403🐛 fix修复fetchStatus()中多余的else块导致 JS 语法错误
3103f944🐛 fix生图接口改为异步任务模式,避免 Cloudflare 524 超时
4ee141de🧹 chore清理本地测试时意外提交的文件
583e61ff🧹 chore将join-keys.json加入.gitignore(运行时数据不入库)
6899f27e🐛 fix移动端/iPad 侧边栏修复(遮罩层 + body 滚动锁定 +100dvh)
75aef430🐛 fix移动端 drawer 关闭时完全移出屏幕(right: -100vw)
802a731e✨ feat新增 join key 级别过期时间 + 并发上限支持

下文逐条拆解实现细节,并给出当前仓库源码中的可验证证据。


1. 修复 CDN 缓存 404:一个缓存头策略引发的「整站宕机」

问题根因

Flask 后端对/static/路径下的所有响应(包括 404)都设置了一年长缓存头。Cloudflare 于是把phaser.js的 404 响应也缓存了长达 2.7 天,导致office.hyacinth.im的 HTML 能加载、静态资源却全部 404,页面完全无法启动。

修复方案

缓存策略改为「按响应状态码区分」

在 backend/app.py 中,add_no_cache_headers作为全局@app.after_request钩子,现在按路径与状态码双重判断:

@app.after_request def add_no_cache_headers(response): """Apply cache policy by path: - HTML/API/state: no-cache (always fresh) - /static assets (2xx only): long cache (filenames are versioned with ?v=VERSION_TIMESTAMP) - /static assets (non-2xx, e.g. 404): no-cache to prevent CDN from caching errors """ path = (request.path or "") if path.startswith('/static/') and 200 <= response.status_code < 300: response.headers["Cache-Control"] = "public, max-age=31536000, immutable" response.headers.pop("Pragma", None) response.headers.pop("Expires", None) else: response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" return response

关键改动点:

  • 只有 2xx 响应才允许一年长缓存(max-age=31536000, immutable),非 2xx(尤其是 404/500)一律no-cache, no-store, must-revalidate,从源头杜绝 CDN 缓存错误页;
  • 同时清掉Pragma/Expires遗留头,避免与Cache-Control语义冲突;
  • HTML、API、状态类接口始终走 no-cache,保证每次刷新都能拿到最新数据。
静态资源加版本化查询参数

仅靠缓存头还不够——文件名不变的前提下,CDN 仍可能命中旧缓存。修复同时在服务端生成了一个进程级的版本时间戳:

# Generate a version timestamp once at server startup for cache busting VERSION_TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S")

对应实现位于 backend/app.py,服务启动时生成一次。随后在返回 HTML 时把模板占位符替换为真实时间戳(见 backend/app.py):

_INDEX_HTML_CACHE = raw_html.replace("{{VERSION_TIMESTAMP}}", VERSION_TIMESTAMP)

而 frontend/index.html 中对phaser.js等静态脚本的引用均携带?v={{VERSION_TIMESTAMP}}。这样每次部署(服务重启)后时间戳变化,CDN 就会重新回源拉取新资源,实现了「长缓存 + 部署即失效」的组合效果。

运维要点:此方案要求每次发布都重启服务(或至少保证VERSION_TIMESTAMP更新)。这也是为什么项目把静态资源长期缓存与版本化参数配套使用——两者缺一,都可能复现本次的 CDN 404 事故。


2. 修复fetchStatus()语法错误:一个孤立的else块卡死整个页面

问题根因

frontend/index.html 的fetchStatus()是前端轮询/status的核心函数,其内部try/catch之间残留了一个孤立的} else { ... }块,破坏了 JS 语法结构。浏览器解析时报Missing catch or finally after try,脚本整体失效,页面永远卡在 loading。

修复方案

移除多余的else块——其中的打字机逻辑已被前面的if/else分支完整覆盖。修复后的函数结构(源码证据)为:

function fetchStatus() { return fetch('/status', { cache: 'no-store' }) .then(response => response.json()) .then(data => { try { if (data.officeName) { window.officeNameFromServer = data.officeName; ... } const nextState = normalizeState(data.state); const stateInfo = STATES[nextState] || STATES.idle; const changed = (pendingDesiredState === null) && (nextState !== currentState); ... if (changed) { typewriterTarget = nextLine; typewriterText = ''; typewriterIndex = 0; } else { if (!typewriterTarget || typewriterTarget !== nextLine) { typewriterTarget = nextLine; typewriterText = ''; typewriterIndex = 0; } } } catch (err) { console.error('fetchStatus apply error', err); typewriterTarget = '状态更新异常,正在恢复...'; typewriterText = ''; typewriterIndex = 0; } }); }

修复后try块内部是「如果状态变化则重置打字机、否则仅在文本不同时重置」的完整分支,catch兜底恢复提示,结构自洽。

值得注意的是,这份更新报告明确标注:此 bug 是 GitHub 上 PR #49、#51、#52 同时在修的问题,本次修复后三个 PR 均可以关闭。这也提醒我们:单页应用的「整页卡 loading」类故障,优先级最高的排查项是浏览器控制台里的 JS 语法错误,语法级 bug 往往比逻辑 bug 更容易被快速定位。


3. 生图接口异步化:后台线程 + 前端轮询绕开 Cloudflare 524

问题根因

原POST /assets/generate-rpg-background是同步接口,生图通常耗时 30~120 秒,而 Cloudflare 的代理超时限制为 100 秒。公网用户一旦生图超过 100 秒,就会触发 HTTP 524 超时,请求被断开,但后端可能仍在继续生成,前端也无法得知结果。

后端改动:拆分为「提交任务 + 轮询结果」

源码证据位于 backend/app.py 的异步任务注册表:

# Async background task registry for long-running operations (e.g. image generation) # Avoids Cloudflare 524 timeout (100s limit) by letting frontend poll for completion. _bg_tasks = {} # task_id -> {"status": "pending"|"done"|"error", "result": ..., "error": ..., "created_at": ...} _bg_tasks_lock = threading.Lock()
任务提交(POST /assets/generate-rpg-background)

见 backend/app.py,核心流程:

  1. 校验资产编辑器权限(_require_asset_editor_auth);
  2. 检查生图脚本环境(GEMINI_PYTHON/GEMINI_SCRIPT是否存在),缺失则直接返回 500 提示「gemini-image-generate 未安装」;
  3. 防重入:在锁内扫描_bg_tasks,若已有status == "pending"的任务,直接返回已有task_id,提示「已有生图任务进行中,请等待完成」;
  4. 生成唯一task_id(gen_+ 毫秒时间戳 + 4 位随机小写字母数字),登记为pending;
  5. 启动守护线程执行_bg_generate_worker,立即返回{ok, async, task_id}。

任务 ID 生成方式:

task_id = "gen_" + str(int(datetime.now().timestamp() * 1000)) + "_" + "".join(random.choices(_string.ascii_lowercase + _string.digits, k=4))
后台 worker(_bg_generate_worker)

见 backend/app.py。worker 在后台线程中完成真正的生图逻辑:调用 Gemini 生成底图、产出office_bg_small.webp、把历史版本快照备份到assets/bg-history/目录,然后在锁内把任务标记为done并写入结果(含path)。异常分支会把任务标记为error,并将常见错误归类为MISSING_API_KEY/MODEL_NOT_AVAILABLE等可识别 code,附带detail供前端精确提示。

结果轮询(GET /assets/generate-rpg-background/poll)

见 backend/app.py:

  • status == "pending":返回「生图进行中...」,前端继续轮询;
  • status == "done":返回结果,并在锁内_bg_tasks.pop(task_id, None)立即清理任务;
  • status == "error":同样 pop 清理,并根据是否携带code决定返回 400 还是 500。

「poll 消费即清理」正是本次风险评估中「异步任务内存泄漏风险为低」的底气所在。

前端改动:_startAndPollGeneration()统一轮询

见 frontend/index.html:

async function _startAndPollGeneration(body, out, progressMsg) { const res = await fetch('/assets/generate-rpg-background', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); ... // 每 3 秒轮询一次 while (true) { await sleep(3000); const poll = await fetch('/assets/generate-rpg-background/poll?task_id=' + taskId, ...); if (poll.status === 'done') { ... return result; } if (poll.status === 'error') { ... return {ok:false, ...}; } } return { ok: false, msg: '生图超时(超过5分钟),请重试' }; }

要点:

  • 提交任务后每 3 秒轮询一次,实时更新等待进度文案;
  • 超时上限 5 分钟,超过则提示重试;
  • 同时抽取了统一的_handleGenError()(见 frontend/index.html 附近)做错误处理,按MISSING_API_KEY、MODEL_NOT_AVAILABLE等 code 给出针对性提示,属于典型的 DRY 优化。

「搬家」(moveRoom)与「中介方案生图」(broker 生图)两条业务路径均已切换到该异步模式,调用示例见 frontend/index.html。


4-5. 清理与 gitignore:运行时密钥数据不入库

  • ee141de清理了本地测试时意外提交的文件;
  • 83e61ff将join-keys.json加入.gitignore。

join-keys.json是项目的运行时接入密钥库,包含每个 key 的明文与使用状态(见示例 join-keys.sample.json),属于典型的「敏感但非配置」数据——一旦入库,任何 clone 仓库的人都能拿到密钥。将其加入.gitignore后,仓库只保留.sample模板,实际密钥在服务器本地维护。

该改动对应的风险评估在报告中单独列为「🟢 已解决」,但仍强调:如果历史上曾有 commit 包含join-keys.json,Git 历史中依然存在该文件,需要确认远端历史是否干净(必要时需改写历史)。


6-7. 移动端/iPad 侧边栏修复:遮罩层 + 滚动锁定 +100dvh

问题根因

在移动端/iPad 上打开资产侧边栏(drawer)时:

  • 背后的页面仍可滚动(滚动穿透);
  • 关闭后 drawer 只偏移-320px,在视口宽度更大的移动设备上仍能看到抽屉露出,体验割裂。

修复方案

遮罩层 + 点击关闭

新增#asset-drawer-backdrop遮罩层(见 frontend/index.html),固定覆盖全屏(inset: 0),半透明黑背景,点击即调用toggleAssetDrawer(false)关闭:

#asset-drawer-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); display: none; -webkit-tap-highlight-color: transparent; } #asset-drawer-backdrop.open { display: block; }

对应 DOM 结构位于 frontend/index.html。

body 滚动锁定与滚动位置恢复

打开 drawer 时给body加drawer-openclass,配合overflow:hidden; position:fixed锁定背景滚动;关闭时恢复scrollY,避免页面跳回顶部。JS 实现见 frontend/index.html:

async function toggleAssetDrawer(force) { const drawer = document.getElementById('asset-drawer'); const backdrop = document.getElementById('asset-drawer-backdrop'); const next = (typeof force === 'boolean') ? force : !assetDrawerOpen; assetDrawerOpen = next; drawer.classList.toggle('open', next); if (next) { _drawerScrollY = window.scrollY; document.body.style.top = `-${_drawerScrollY}px`; } document.body.classList.toggle('drawer-open', next); if (!next) { document.body.style.top = ''; window.scrollTo(0, _drawerScrollY); } ... }

移动端媒体查询内的锁定样式(见 frontend/index.html):

body.drawer-open { overflow: hidden !important; position: fixed; width: 100%; ... }
完全移出视口 +100dvh+ 滚动穿透抑制
  • 关闭状态改为right: -100vw:无论设备视口多宽,抽屉都能完全移出屏幕,从根本上解决「-320px 偏移在宽屏设备上仍可见」的问题;
  • 高度改用100dvh:适配移动端动态视口(iOS Safari 地址栏收起/展开时视口高度变化),桌面端保留100vh作为兜底(见 frontend/index.html 的height: 100vh; height: 100dvh;双声明写法);
  • overscroll-behavior: contain:阻止抽屉内部滚动穿透到背景页面(见 frontend/index.html 及抽屉 body 的 L1094-L1098);
  • 同时配合#main-stage的视口动态左移(body.drawer-open #main-stage规则,见 frontend/index.html),确保抽屉与主舞台之间至少保留 20px 间隔。

8. 新功能:Join Key 级别过期时间 + 并发上限

这是本次更新中唯一的 feature 类改动,目标是让「邀请 guest agent 参与活动」具备更强的治理能力。

数据结构:expiresAt+maxConcurrent

在 join-keys.sample.json 中可以看到 key 的完整字段:

{ "keys": [ { "key": "ocj_example_team_01", "used": false, "reusable": true, "maxConcurrent": 3, "usedBy": null, "usedByAgentId": null, "usedAt": null } ] }

本次更新为每个 key 新增两个可选字段:

字段类型含义默认值
expiresAtstring(ISO 8601 时间戳)该 key 的绝对过期时间,过期后拒绝一切接入不设置则永不过期
maxConcurrentint同一个 key 同时在线(approved 且在 5 分钟内有心跳)的 agent 数上限3(源码中的默认值)

过期检查:join-agent与agent-push双端点

两个端点在执行前都会读取key_item.get("expiresAt")并与当前时间比对,过期则返回 403 与友好提示:

key_expires_at_str = key_item.get("expiresAt") if key_expires_at_str: try: key_expires_at = datetime.fromisoformat(key_expires_at_str) if datetime.now() > key_expires_at: return jsonify({"ok": False, "msg": "该接入密钥已过期,活动已结束 🎉"}), 403 except Exception: pass
  • 接入端点join_agent:见 backend/app.py;
  • 状态推送端点agent_push:见 backend/app.py。

并发上限:锁内二次读取 + 在线判定

并发控制的关键设计(见 backend/app.py):

  1. join_lock全局互斥锁 + 锁内重新读取:避免多个并发请求基于同一旧快照同时通过校验(check-then-act 竞态);
  2. 在线判定基于 5 分钟窗口:lastPushAt/updated_at距今超过 300 秒的 agent 被标记为offline,不占用并发额度(_age_seconds辅助函数实现);
  3. 同类 agent 去重:同一name已存在时只更新记录,不重复计数;
  4. 超过上限返回429:"该接入密钥当前并发已达上限({max_concurrent}),请稍后或换另一个 key"。
max_concurrent = int(key_item.get("maxConcurrent", 3)) ... if active_count >= max_concurrent: save_agents_state(agents) return jsonify({"ok": False, "msg": f"该接入密钥当前并发已达上限({max_concurrent}),请稍后或换另一个 key"}), 429

配套说明:agent_push端点允许offline(过期的在线状态,而非撤销的授权)状态的 agent 恢复推送并自动升级回approved,见 backend/app.py,这保证了「guest agent 短暂离线后重连」不会被误拒。

密钥文件的加载与保存

join-keys.json的读写统一走 backend/store_utils.py 中的 JSON 加载/保存工具(UTF-8 +indent=2持久化),密钥文件的路径常量JOIN_KEYS_FILE定义于 backend/app.py。


潜在风险评估与结论

原报告对四个风险点做了明确评级,这里结合源码逐一给出可验证结论:

风险点等级说明与源码依据
异步任务内存泄漏🟡 低_bg_tasks在任务完成并被 poll 消费后即pop清理(见 backend/app.py);若前端从不 poll(如用户中途关页),任务对象会残留。当前生图频率低、风险极低,后续可考虑加定期清理。
join-keys.json历史泄露🟢 已解决已加入.gitignore,但若历史 commit 曾包含该文件,Git 历史中仍存在,建议确认远端历史是否干净。
前端fetchStatus修复🟢 已验证修复后的try/catch结构完整(见 frontend/index.html),本地运行正常。
移动端 drawerposition:fixed🟢 低iOS Safari 下position:fixed+100dvh组合偶有兼容问题,但已是业界最佳实践(双声明100vh; 100dvh做了降级兜底)。

结论:无新增 bug 风险,可以安全推送。


文件变更统计

.gitignore | 1 + backend/app.py | 166 ++++++++++++++++++------ frontend/index.html | 162 ++++++++++++++++-------- frontend/join-office-skill.md | 102 +++++++++------ frontend/office-agent-push.py | 286 ++++++++++++++++++++++++++++++++++++++++++ office-agent-push.py | 2 +- 共 6 个文件,+589 行,-130 行

变更分布符合本次主题:改动高度集中在后端 API 层(backend/app.py的缓存头、异步任务、join key 校验)与前端单页(frontend/index.html的移动端 drawer、轮询逻辑、fetchStatus);frontend/office-agent-push.py与根目录 office-agent-push.py 的变动则与 join key 过期/并发特性的对接有关。


给维护者的实践清单

结合本次更新的全部经验,可以沉淀出四条可直接复用的运维/开发规则:

  1. CDN 场景下,缓存头必须按状态码区分:永远不要对 404/5xx 设置长缓存;静态资源务必配合版本化参数(如?v={{VERSION_TIMESTAMP}}),否则「长缓存」会成为故障放大器;
  2. 长耗时接口一律异步化:凡是可能超过网关超时(本例为 Cloudflare 100 秒)的操作,都应以「提交任务 + task_id + 轮询」模式实现,并在消费后清理任务对象,防止内存泄漏;
  3. 移动端抽屉/侧边栏的标准三件套:遮罩层点击关闭、body滚动锁定(记录并恢复scrollY)、right: -100vw+100dvh+overscroll-behavior: contain;
  4. 接入密钥要具备生命周期:expiresAt让活动密钥自动失效,maxConcurrent防止单一密钥被并发滥用;涉及计数校验的临界区务必加锁并在锁内重新读取数据。
  • 前端
  • 后端
  • AI 应用
  • 数据可视化

【免费下载链接】Star-Office-UI

A pixel office for your OpenClaw: turn invisible work states into a cozy little space with characters, daily notes, and guest agents. Code under MIT; art assets for non-commercial learning only.

项目地址:https://gitcode.com/gh_mirrors/st/Star-Office-UI
点击查看免费下载

相关推荐

上一篇:Yuxi-Know技术选型分析:为什么选择LangGraph和LightRAG
下一篇:Open-AutoGLM终极指南:如何用AI自动化完成日常手机操作

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询