Commit Graph

128 Commits

Author SHA1 Message Date
Gloridust
9981e9a5dc feat(design): 设计系统文档 doc/dev/design.md + 无障碍基线(焦点环/动效降级)
- doc/dev/design.md:把牛奶布艺设计语言正式沉淀为规范——设计哲学、令牌表(色彩/圆角/
  阴影高光/动效曲线,含深浅色)、布局与三 Tab 信息架构、逐组件规范(卡片/按钮三级/输入框/
  徽标体系/分段 Tab/折叠菜单/空状态/Toast/进度/模态)、动效、无障碍、文案、反例、落地清单。
  令牌与 class 名与 styles.css 一一对应,是可执行的活规范而非抽象理论。
- 无障碍基线(此前为 0):全局 :focus-visible 绿色焦点环(仅键盘导航出现,鼠标不触发;
  无边框拟物 UI 尤其需要);prefers-reduced-motion 降级(去装饰动效、保留 spinner/进度的
  必要循环)。已用真实构建 CSS 截图验证焦点环不被按钮 overflow:hidden 裁剪、贴合胶囊圆角。
v1.4.3
2026-07-08 03:13:09 +08:00
Gloridust
06a96f3588 fix(ui): 实例卡片布局拥挤 —— 标题行只留状态徽标,可升级/镜像版本移到独立元数据行
加了「镜像 vX.Y.Z」角标后,标题行挤了名字+在线+可升级+镜像版本四样,导致名字被截成
「Ethan...」、徽标竖排换行成「可升\n级」「在\n线」。重构:
- 标题行只保留 名字 + 状态徽标;名字 flex:1 吃满宽度,仅真的过长才省略号(并加 title 悬浮全名)。
- 可升级 / 镜像版本 下沉到独立的 .inst-meta 行,可自然换行。
- .tag 全局加 white-space:nowrap,杜绝徽标字内换行;标题行状态徽标 flex:none 不被挤压。
- 卡片网格最小列宽 220→248px,给内容更多呼吸空间。
已用真实构建 CSS 渲染 mock 卡片截图验证:深浅色下名字完整显示、超长名优雅省略、徽标不再竖排。
2026-07-08 03:05:41 +08:00
Gloridust
f3b3219cdb feat: 公网访问加固(登录限速/会话滑动续期)+ 管理页三 Tab 信息架构重构
公网/NAS 稳定性专项扫描的产出:

① 登录限速:面板常被直接暴露公网,/api/auth/login 原先无任何限速可被无限爆破。
双键计数(IP+用户名 5 次、IP 20 次 / 15 分钟窗口),触发回 429 + 面板日志告警;
用 socket 直连地址而非可伪造的 XFF;成功登录清零。已实测:5×401 → 第 6 次 429。

② 会话滑动续期:固定 30 天硬过期在"常年挂着"的 NAS 场景表现为"隔三差五要重登"。
现剩余不足一半自动续满(服务端),鉴权层顺手刷新 cookie maxAge(浏览器侧同步)——
活跃用户永不掉线,闲置超时才需重登。

③ DNS-rebind 钩子补 return reply(async 钩子已 send 后的规范终止写法)。

④ 运维文档新增「公网访问 / 反代部署」章节:nginx 完整示例(ws 升级头 +
proxy_read_timeout 3600s——默认 60s 是"桌面挂着不动 1 分钟就断线"的元凶 +
client_max_body_size)、Caddy/frp/Cloudflare 注意事项、PANEL_ALLOWED_HOSTS 提醒。

⑤ 管理页重构:7 个区块一条长滚动 → 「实例 / 用户 / 系统」三 Tab
(实例=日常操作;用户=子账号+本账号;系统=残留容器+数据卷+诊断日志+关于)。
牛奶布艺分段选择器(凹槽+浮起胶囊),Tab 角标点提示待处理事项(可升级/有残留),
记住上次停留 Tab;子账号(非管理员)视图不变。
2026-07-08 02:57:10 +08:00
Gloridust
c54d3ff18e fix(ci): 构建期精简 apt 源,根治 QEMU arm64 下 apt-get update OOM
v1.4.2 的「合并两层为一次 update」只降低了 OOM 概率,未除根因:单次 update 仍要解析
一堆构建用不到的源(deb-src 源码索引 / backports / docker / nodesource),QEMU 内存峰值仍可能爆
(download.docker.com InRelease 正是触发点)。

对策:构建期把 apt 源精简到只剩 debian main/updates/security(纯二进制)+ chromium 锚定快照,
装完原样恢复(docker.list/nodesource.list/backports 均还原,不改变最终镜像的源)。索引集从 ~13 降到 ~4。
本机原生 arm64 全新构建验证:Chromium 149 装成、包全解析、源已恢复。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 12:19:00 +08:00
Gloridust
fda9522b78 fix(critical): 实例镜像 arm64 构建 OOM + 版本耦合无兜底致面板锁死(v1.4.1 连环 P0)
v1.4.1 只发布了面板、实例镜像 CI 失败,叠加版本耦合无兜底 → 用户升级指示器消失、
实例卡在坏的 1.4.0、浏览器仍黑屏。两处修复:

① Dockerfile arm64 OOM:CI 的 QEMU arm64 下第二个 apt 层 OOM(两次 apt-get update +
snapshot 源被后续层反复拉取)。合并为单层一次 update+install、装完即移除 snapshot 源、
Acquire::Languages=none + Retries=3,砍掉内存/网络峰值。本机原生 arm64 构建 + 全套探针
(Chromium 149 / DPI 98304 / CEF 连跑两次 / 30s 零崩溃 / 窗口可见)已验证通过。

② 版本耦合安全兜底:面板偏好「与自身同版本」的实例镜像 tag,但该 tag 在本地和 registry
都不存在时(CI 部分失败/发布时序),原先会指向不存在的 tag → 检测恒空、升级拉不到。
新增 resolveInstanceImage():不可达则回退 :latest,功能永不因版本错配而瘫痪(已运行时验证
9.9.9→latest)。启动时与 ensureImage 前各解析一次。

P0 复盘记录连环成因;待办:改用原生 arm64 runner 替代 QEMU、matrix 失败联动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.4.2
2026-07-07 12:11:16 +08:00
Gloridust
d523204c6b feat(#112): 实例卡显示镜像版本 + 升级结果诚实汇报(拉取失败不再假装成功)
issue #112 复盘出两个产品缺陷(用户"以为升级了其实没有"、且无从自查):
- 实例卡新增「镜像 vX.Y.Z」角标(读 CI 的 org.opencontainers.image.version label;
  自构建镜像显示短 id),用户一眼可见每个实例到底跑的哪版。
- upgradeInstance 记录升级前后镜像 id:拉取失败且镜像未变 → 抛错按失败处理并说明原因
  (原先静默回退本地旧镜像还报"完成");镜像更换/已最新也分别写入实例日志。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.4.1
2026-07-07 01:01:22 +08:00
Gloridust
4d9cc1015a fix(critical): Chromium 版本锚定(snapshot) + CI 产物门禁 + 面板/实例版本耦合 + 声音重连修复
v1.4.0 三项反馈的修复:

① 浏览器实例仍黑屏(P0):CI 构建时 apt 拉到 Chromium 150(本地验证的是 149 缓存层)。
实测 150 在本栈上「非首次启动」路径(--no-first-run / First Run 哨兵 / 同 profile 第二次启动)
与 --start-maximized 均直接 SIGTRAP —— 首跑正常、之后必崩,无任何 flag 可绕。修复:
- Dockerfile 从 snapshot.debian.org 固定快照(20260704T180435Z)锚定安装 149.0.7827.196(R1);
  升级 chromium = 显式改快照+版本并过全套门禁。已验证:锚定镜像 DPI ✓、CEF 连跑两次 ✓、
  30s 零崩溃 ✓、窗口可见 ✓。
- release.yml 产物门禁:先构建 amd64 在 runner 上跑四项探针(DPI/CEF两次启动/崩溃循环/微信模式),
  全过才推送——「验证的东西和发布的东西必须是同一个 artifact」(v1.4.0 的流程教训)。
- 本地门禁 CEF 探针升级为连跑两次(单次恰好漏掉非首跑崩溃)。

② 面板/实例版本耦合(R1):正式版面板把实例镜像 :latest 改写为与自身相同的版本 tag,
同版本一起验证发布,杜绝「低版本面板 × 高版本实例」未验证组合;env 显式锁其它 tag 则尊重、
dev 面板保持 latest。配套:本地缺该版本镜像但远端存在时,升级引导也会亮起。

③ VNC 声音大概率无声:vncAudio 的 opened 标志断线不复位 → socket 重连后跳过 emit('open'),
kclient 永不重新推流 → 实例升级/重启/面板更新/网络抖动后全程静音。修:disconnect 时复位。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 00:55:08 +08:00
Gloridust
9ec3ea2d73 docs+fix: 稳定性设计文档(远程桌面/升级链路决策全记录)+ 重置设备ID 也走 keepImage
- doc/dev/稳定性设计.md:两条命脉链路的现行设计、每个决策的原因(含故意不做的事:
  单会话互踢、页内软重连、激进健康探测)、已知单点(KasmVNC 进程级卡死只能重启容器)。
- regenInstanceMachineId 补上 keepImage(恢复类操作幂等,R10 的最后一处漏网)。
- 发布门禁全项通过:DPI=98304、CEF 探针存活、30s 零崩溃、微信模式就绪、
  升级路径(keepImage 保版本 → 升级换 latest → LogConfig 20m×2 生效)、面板部署 0 错误。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.4.0
2026-07-07 00:10:03 +08:00
Gloridust
7dc2053c0b fix(stability): 重启/自愈幂等(keepImage)+ 代理双侧 TCP keepalive —— 远程桌面稳定性审查
重大逻辑问题:「重启」「卡死自愈」「看门狗自愈」都走 runInstance 按本地 :latest 重建——
只要本地镜像被任何一次拉取更新过(新建实例/升级任一实例),下一次重启/自愈就把该实例
【隐式换镜像】。若本地新镜像恰好是坏的(如 v1.3.0 时期),一次看门狗自愈就能弄坏一个
从没点过升级的实例("我什么都没动它怎么坏了")。修:runInstance 增加 keepImage 选项,
重启/自愈沿用容器当前镜像重建(且跳过 ensureImage,离线也能重启);换镜像只走显式升级。
已实测:keepImage 重建后镜像不变;不带 keepImage(升级)换到 latest;新容器 LogConfig 生效。
守则新增 R10(重启必须幂等)。

代理加固:面板↔实例 双侧 ws 套接字启用 TCP keepalive(30s)。客户端断网/切网(WiFi→4G、
NAS 休眠)时 TCP 不会主动通知,半开死连接可挂数小时——对 KasmVNC 表现为幽灵会话占坑,
与新连接并存正是历史上"Xvnc 卡死需重启容器"的诱因之一;30s 探测让死连接分钟级回收。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 00:05:06 +08:00
Gloridust
4a6d71844b fix(update): 更新机制专项审查 —— 远端新版检测盲区 + 单实例升级异步化 + 自更新加固
对面板自更新/单实例升级/一键升级三条链路审查后逐项修复:

盲区(最重要):实例"可升级"检测只比「容器镜像 vs 本地镜像」,而用户更新面板后本地实例
镜像通常还是旧的(没人主动 pull)→ 永远检测不到可升级、升级引导形同虚设。新增远端检测:
registry manifest HEAD digest(不下载)对比本地 RepoDigests,30 分钟缓存、后台刷新、
离线/自构建镜像返回未知不打扰。横幅与关于页提示均纳入该信号。

单实例升级异步化:原同步等待拉取(受限网络下数分钟)会被反代 ~60s 掐断 → 前端误报失败
而后台还在跑,再点一次就并发重建。改为登记 upgradingIds 后立即返回,前端轮询直至完成,
按"是否仍落后"给出结论;与一键升级互斥(409)。

一键升级顺序修正:先拉镜像再判定落后清单(原先反了——拉取带来更新后,"本来等于旧最新版"
的实例才变落后,点完全部升级横幅却还在);拉取阶段进度显示 phase;页面刷新后自动恢复轮询。

自更新加固:拉取加无进度停滞超时(原来会无限卡死且 updateInFlight 永久锁死);标志 10 分钟
自动复位(helper 静默失败后可重试);版本锚定(R1)——优先拉更新检查宣告的具体版本 tag,
失败回退 :latest,保证"更新到 v1.3.1"拿到的就是 v1.3.1。

其他:并发 pullImage 合并为单一拉取;升级保留实例原有停止状态(不再悄悄拉起);
单实例升级完成后也回收悬空镜像。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:56:03 +08:00
Gloridust
7ec79314bb docs(dev): 架构守则 + 发布门禁 + P0复盘簿 —— 把历次 P0 的学费固化成硬约束
从 v1.2.6 自更新黑屏、v1.2.9~v1.3.1 DPI 单位事故、磁盘暴涨、升级卡死等真实 P0 中
蒸馏出 9 条架构不变式(R1~R9):版本锚定可回滚、声明式自重建、资源必有边界、
长任务异步化、面板×实例兼容矩阵、根因=解释全部症状+A/B实证、AI PR 审语义、
发布节奏(特性不当天发)、冒烟测用户动作。发布门禁给出逐条可执行的命令清单
(含微信 CEF 等价探针、升级路径测试);P0 复盘簿记录每次事故的流程性成因。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:37:48 +08:00
Gloridust
94b591e0b2 fix(critical): xsettingsd Xft/DPI 单位错误 —— v1.2.9~v1.3.1 微信公众号/附件秒关、浏览器黑屏的总根因
XSETTINGS 规范中 Xft/DPI 的单位是「DPI×1024」(96 DPI 应写 98304),#101 引入的 xsettingsd
配置误写成 96 → 所有 Chromium 内核应用把 DPI 读成 96/1024≈0.09 → 缩放因子≈0 → 显示变换
矩阵 [0 0 0 0;…] 不可逆(transform.cc NOTREACHED)→ GPU/viz 进程连崩 3 次 → 窗口/应用退出:
- 微信内嵌 CEF:点公众号/附件/小程序 → 窗口秒关/白屏(issue #111,日志与本地复现完全吻合)
- 系统 Chromium:整个浏览器崩溃循环 → 黑屏(v1.3.1 的 --force-device-scale-factor=1 只救了
  能加 flag 的 Chromium,是治症状;微信 CEF 无法加 flag,必须修 XSETTINGS 源头)
A/B 实证:Xft/DPI 96 → 裸 chromium 立崩(同款堆栈);改 98304 → 存活。两处写入点均已修
(docker/autostart 每次开机重写 + 面板 applyXsettingsFont 实时下发)。

配套修复本轮反馈的其余问题:
- 磁盘暴涨("1TB 没了"):实例容器注入 LogConfig 20m×2(默认 json-file 无上限,崩溃循环
  每 2s 刷错顶满磁盘);compose 面板同配;微信 crashinfo 崩溃转储 1 天过期清理;
  一键升级完成后清理悬空(<none>)旧镜像并记录释放空间。
- 一键升级"卡死":改异步——立即返回、后台顺序执行、前端轮询进度(n/总数·当前实例);
  镜像统一只拉一次(原每实例各拉一次,受限网络下每次都等停滞超时)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:31:24 +08:00
Gloridust
bdb53065ca Update .gitignore 2026-07-05 16:56:23 +08:00
Gloridust
72a34ac7b0 fix(chromium): 升级实例后黑屏崩溃循环 —— 强制 device-scale-factor=1
回归根因(v1.3.0 实例镜像重建时 apt 拉到 Chromium 149 引入,非某个 PR 的代码 bug):
较新 Chromium(≥149)在 KasmVNC 尚无客户端连接、Xvnc 分辨率/DPI 为退化值时,
device-scale-factor 读成 0 → 合成器算出的显示变换矩阵不可逆(transform.cc NOTREACHED)
→ GPU/viz 进程连崩 3 次 → "GPU process isn't usable. Goodbye." → 浏览器退出 →
autostart 每 2s 重启 → 死循环黑屏(只有鼠标、无窗口)。旧版 Chromium 容忍退化分辨率,149 变严格才暴露。

修复:Chromium 启动加 --force-device-scale-factor=1,强制缩放为 1 使变换矩阵可逆,从根上消除崩溃。
仅影响 Chromium 实例;微信(自带 CEF)与旧镜像不受影响。

验证:新镜像一次性容器(最坏情形:无客户端连接、分辨率退化)下 chromium 30s 零退出、
窗口正常显示(修复前每 2s 崩一次、诊断包里 173/58 次退出)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.3.1
2026-07-05 16:33:41 +08:00
Gloridust
db894cfcff feat(update): 实例镜像落后检测 + 一键升级全部 + 壁纸缺工具友好报错
面板与实例是两套镜像:更新面板不会动实例,用户常"更新了面板、实例还旧"(如设壁纸报 127)。补齐:
- 后端:latestInstanceImageId + instanceOutdated(运行镜像 != 本地最新即落后);
  GET /instances/upgrade-status(落后清单)、POST /instances/upgrade-all(逐个升级落后实例)。
- 前端:管理页顶部横幅「有 N 个实例可升级 + 一键升级全部实例」,每张实例卡加「可升级」角标;
  关于页面板更新后也提示"另有 N 个实例镜像可升级"(更新面板≠更新实例)。
- 壁纸:applyBackground 前置检查 xwallpaper,旧镜像缺失时提示"请先在管理升级实例"而非退出码 127。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 01:15:30 +08:00
Gloridust
e880f9d2fc fix(壁纸): resize 后壁纸被平铺 —— 改为分辨率变化即重设(issue 反馈)
autostart 在 Xvnc 默认分辨率下设一次壁纸;浏览器连上后 KasmVNC resize-remote 把屏幕变大,
X 就把当时的小 root pixmap 平铺填满(表现为壁纸 2×2 平铺)。改为后台看守:每 2s 检查
「壁纸名@分辨率」签名,一变就 --zoom 重设,跨 resize/换壁纸都保持正确。冒烟验证不阻断启动。
v1.3.0
2026-07-05 00:55:01 +08:00
Gloridust
ed45957b4a feat(update): 开发版允许一键「升级到正式版」
旧逻辑 hasUpdate 需当前版本能解析为语义化 vX.Y.Z,dev-<sha> 开发版永远拿不到更新入口。
现:version.ts 增 isDev(当前非正式版),开发版只要查到任一正式发布即 hasUpdate=true;
About 面板对开发版把按钮/提示改为「升级到正式版」(自更新拉 :latest 即最新发布)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 00:16:19 +08:00
Gloridust
97de987cc0 fix(#101): 字体/xsettingsd 路径 /home/abc → /config(会话 HOME),修 Permission denied
冒烟测试发现 #101 的字体功能写 /home/abc/.xsettingsd 报 Permission denied——linuxserver kasm 会话
HOME 是 /config(数据卷),/home/abc 不可写。全部改为 /config:xsettingsd 配置、用户 fontconfig、
~/.local/share/fonts→/config/.fonts 软链都落到 /config,既可写、又随卷持久化、上传字体也能被 fontconfig
发现。Dockerfile 去掉无用的 /home/abc 预建(/config 是运行时卷)。已冒烟验证:无报错、autostart 正常到
达应用阶段、软链正确。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.2.9
2026-07-05 00:05:53 +08:00
Gloridust
2741066cf9 Merge #101: 桌面壁纸 + 自定义字体(by d1y,修正 blockers 后并入) 2026-07-04 23:53:16 +08:00
Gloridust
f115cf21c1 fix(#101): 修正壁纸/字体 PR 的 blockers 后并入
在 d1y 的壁纸/字体功能基础上修掉不可合并的点:
- Dockerfile:移除硬编码的清华 apt 源(会拖垮海外/CI 构建)、还原基础镜像 lscr.io、去掉 imagemagick
  (只为缩略图凭空胖 ~100MB)。缩略图改为直接回原图、前端缩放。
- docker.ts:新增 safeMediaName(在 safeName 基础上拒绝 shell 元字符),壁纸/字体的文件名/字体名都改用它,
  杜绝拼进 sh -c 的注入/坏名(含 getFontFamily 漏校验的一处)。移除 convert 缩略图代码。
- index.ts:bgHandler 404 时误 return reply(truthy) 导致 `if(!inst)` 拦不住 + 类型错误;改为 return null。
- build-local.sh:还原多余的 --progress=plain。

功能:管理页可给实例设桌面壁纸、上传/应用自定义字体(跨容器重建持久化)。实例镜像新增
xwallpaper/xsettingsd(已验证构建通过、工具就位、无 imagemagick)。

Co-Authored-By: d1y <deep@d1y.dev>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:53:16 +08:00
Gloridust
fb8c02cf9e Merge PR #101 (壁纸/字体 by d1y) into main —— 待随后修正 blockers 2026-07-04 23:45:31 +08:00
Gloridust
010bc4d0f8 Merge PR #98: 无 body 的 POST(心跳)补 Content-Type 兜底,修 415
by chenshu007。controlBeat 等无 body 的 POST 不带 Content-Type,Fastify 白名单解析器会 415、
静默打断交互心跳。补一个通配 content-type 解析器兜底(精确匹配优先,安全)。
2026-07-04 23:42:58 +08:00
Gloridust
28ac781c93 Merge PR #90: WOC_ENABLE_GPU=1 时把宿主 /dev/dri 直通进实例(完成 #36)
by mvanhorn。ENABLE_GPU 此前只抑制 DISABLE_DRI、从不直通 /dev/dri,故设了也没效果(#36 根因)。
本 PR 补齐:仅 WOC_ENABLE_GPU=1 时把宿主 /dev/dri 渲染节点加入 HostConfig.Devices,并把设备真实
数字 GID 加进 GroupAdd(render 组 GID 各发行版不一,仅靠组名可能仍打不开)。找不到则优雅降级。
默认关闭,零影响既有用户。
2026-07-04 23:38:47 +08:00
Ethan Zou
57c38975e3 Merge pull request #78 from chenshu007/feat/docker-socket-proxy-hardening
feat: optional docker-socket-proxy hardening overlay (不改变默认行为)
2026-07-04 23:37:05 +08:00
Gloridust
64633a8147 fix: 会话持久化(#95) + 深色剪贴板可读(#96) + 粘贴图片到桌面(#91) + 拉取超时(#99)
- #95 会话改落盘持久化(sessions.json,与 accounts.json 同目录)+ 时长可配置 WOC_SESSION_DAYS(默认30天),
  cookie maxAge 同步。根因:旧版会话只在内存,面板重启/自更新/看门狗重建都会清空 → 每次都要重登。
- #96 .clip-area / .security-status 误用不存在的 var(--mf-trough) → 深色下退回硬编码亮灰、配浅色文字成"白底白字"。
  改回 var(--trough)。
- #91 桌面打开时监听 paste:剪贴板含图片则上传到桌面为文件(RFB 剪贴板仅文本,图片走文件通道),应用里可取用。
- #99 pullImage 加"无进度"超时(WOC_PULL_STALL_MIN,默认5分钟):NAS 直连 docker.io 卡死时快速失败而非无限
  hang 住创建请求/前端转圈。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.2.8
2026-07-04 23:30:11 +08:00
d1y
c9ef07eb56 feat(panel): 桌面壁纸与字体管理
- 上传/应用/清除壁纸,缩略图预览
- 上传/安装/删除字体,fc-cache 刷新
- fontconfig 别名配置 + xsettingsd 实时生效
- 移除 hook-fontconfig,改为重启提示
- 切换 tsinghua 安全更新源
- 安装 xwallpaper/imagemagick/xsettingsd 依赖
2026-06-30 00:17:41 +08:00
d1y
5bb94d88e1 chore(build): docker build 默认输出 --progress=plain 方便调试
本地构建时加 --progress=plain,让 apt 等步骤的输出可见,
避免卡住时用户看不到真实进度。
2026-06-29 18:55:06 +08:00
d1y
8d276c51c3 fix(docker): 换 ghcr.io 源 + 清华 apt 镜像加速中国用户构建
- FROM lscr.io 改为 ghcr.io(linuxserver 镜像国内直连更稳)
- 新增 apt sources.list 层,替换为 mirrors.tuna.tsinghua.edu.cn
- apt-get 下载从 deb.debian.org 走清华源,构建速度大幅提升
2026-06-29 18:55:02 +08:00
chenshu007
784367e9fb fix: accept missing/unrecognized content-type on heartbeat beat endpoint
The /api/instances/:id/control/beat endpoint is a no-body POST used as a
keep-alive heartbeat. The frontend sends it without a Content-Type header.

Fastify's content-type parser is whitelist-based: any request whose
Content-Type does not match a registered parser (including requests with
no Content-Type at all) is rejected with 415 Unsupported Media Type.
The panel already registered parsers for application/json (built-in) and
application/octet-stream, but nothing for the missing-header case.

Fix: register a '*' wildcard parser as a catch-all. Fastify resolves
parsers with exact matches first, so the two existing parsers are
unaffected; the wildcard only fires when nothing else matches. The beat
handler ignores the body entirely, so returning null is correct.

Verified: POST /control/beat without Content-Type now returns 403 (auth
check passes the content-type layer) instead of 415; existing JSON and
octet-stream routes continue to return 200 as before.
2026-06-28 13:36:17 +08:00
Gloridust
8e15556e6f feat(panel): 子账号改用户名(#89)
管理页每个子账号新增「改名」:弹窗输入新用户名(3-20 位字母/数字/下划线,查重),
保存即改登录名;会话以 userId 为准,改名后保持登录、下次用新名登录。

- store.renameUser(查重 + 写入)
- POST /api/admin/users/:id/rename(管理员,格式校验)
- api.renameUser + Admin 的 RenameUser 弹窗

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.2.7
2026-06-26 10:25:05 +08:00
Matt Van Horn
e63c491709 fix: pass through /dev/dri when WOC_ENABLE_GPU is set 2026-06-25 04:28:47 -07:00
Gloridust
7e21aca7bb fix(self-update): 修一键更新后实例落错网络致黑屏(502)的根因
根因:自更新重建面板时 buildCreateOpts 复刻了旧容器的 Hostname(=旧短ID)。新面板 os.hostname() 因此指向
【已删除的旧容器】,ensureNetwork 的 docker.getContainer(hostname()) 404 → 探测不到面板网络 → 返回 null →
此后新建/重启/升级/看门狗软重启的实例都以 NetworkMode=undefined 落到默认 bridge 网络,而反代按容器名
(http://woc-wx-xxx:3000) 跨网络解析不到 → 502「桌面服务暂不可用」黑屏。直接 docker compose pull 的用户
不受影响(compose 重建的面板 hostname 正确),与线上现象一致。

两处修复:
- self-update.ts:重建面板不再复刻旧 Hostname,让 docker 用新容器自身短 ID 作 hostname。
- docker.ts ensureNetwork:探测面板网络时,hostname 候选失败再按容器名(woc-panel)兜底查找,彻底不依赖
  hostname 与容器 ID 一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.2.6
2026-06-25 19:14:09 +08:00
Gloridust
da87d1fce4 feat(diag): 诊断包补宿主级信号——内存/cgroup/安全选项/实例实时内存
便于定位 Ubuntu server 等宿主上的"黑屏/502/反复重启"是否是资源/宿主限制问题:
- system.txt 增:内存&Swap限制支持、cgroup 版本/驱动、存储驱动、SecurityOptions(apparmor/userns/seccomp);
  面板实例配置(SHM、docker硬内存上限是否设、GPU)、内存自愈 soft/hard 阈值。
- 每实例增:运行中实例的当前内存占用 MiB(配合宿主总内存/OOMKilled 一眼判断是否内存不足)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.2.5
2026-06-25 01:37:20 +08:00
Gloridust
0fe35103ab fix(proxy): 上游暂不可用回「自动重连」页,取代死的纯文本 502(修黑屏"桌面服务暂不可用")
实例在创建初始化/升级/重启/内存软重启、以及面板自更新(代理短暂中断)时会短暂 502;旧版回纯文本
"桌面服务暂时不可用",且 iframe 一旦载入它就 frameLoaded=true、不再触发重试,用户卡在黑屏死页。
现对浏览器导航请求回一个带 spinner、每 3s 自动重载的友好页:实例一就绪即自动连上;约 30s 仍不行
才转手动重试(20s 间隔重置计数以区分新一轮故障)。子资源(JS/CSS/XHR)仍回纯文本,不误喂 HTML。

诊断依据:两个 v1.2.4 用户的诊断包显示实例其实健康(KasmVNC 正常 accept),502 都出现在各类重启/初始化
的瞬时窗口,并非实例真的挂了。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:49:11 +08:00
Gloridust
60e53425e3 fix(audio): 点桌面即可解挂起出声 + 声音默认关每次打开都关
微信内声音播不出的根因:浏览器自动播放策略挂起 AudioContext 需用户手势恢复,但恢复手势监听绑在父窗口,
用户点的是同源 iframe 内的桌面画面、事件不冒泡到父窗口 → "点画面没用、得重开声音开关"才出声。
- VncAudio 加 resumePlayback(),Desktop 在 iframe 内补 pointerdown/keydown 手势监听转调它,点桌面即出声。
- 声音开关改为每次打开实例都默认【关】、不持久化 on(用户要求;蓝牙外放场景默认关最可预期)。
- 同步修正加载页文案(不再说"声音自动开启")。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.2.4
2026-06-23 21:50:51 +08:00
Gloridust
8bb721d3ba fix(input): 转发模式回车改"补 Return 直接发送",不再切焦点回虚拟机(#81)
上一版 #81 修复在发送后把焦点切回 VM,但转发模式下 VM 无 IME,用户接着打的拼音会以原始
keysym 直灌微信输入框(出现 "nniih'h你好啊" 串码)。改为打完文本后补一个 Return 直接发送,
焦点始终留在底部输入条,下一条仍用本机输入法安全输入。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:40:19 +08:00
Gloridust
c871a70b5a fix: 安装用户未就绪重试(#74) + 关窗清理音频桥(#82) + 输入条发送后回焦点(#81)
- #74 安装失败「unable to find user abc: no matching entries in passwd」:容器 init 未完成时
  abc 用户还没建好,docker 创建 exec 即报 400。加 execCreate 重试包装(对该错误重试 ~12s),
  超时给清晰中文提示,而非透传难懂的 docker 400。
- #82 开音频后关窗再进必须重启实例:React 清理在直接关窗时不一定执行,残留 audio socket.io
  (开麦时还占着麦克风)与下次新连接并存把实例顶到"需重启"。加 pagehide(非 bfcache)即断开音频桥。
- #81 输入条回车只落字、还得点发送:发送后把键盘焦点交回 VM iframe,用户再按一次回车即在微信里发出。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:15:04 +08:00
chenshu007
a032d17d71 feat: add optional docker-socket-proxy hardening overlay
Adds docker-compose.secure.yml and doc/安全加固.md as fully opt-in
security hardening for any WOC deployment.

The raw docker.sock mount in the panel is root-equivalent: a compromised
panel process can create privileged containers, mount the host filesystem,
or stop/delete arbitrary containers. This PR interposes a filtered proxy
(tecnativo/docker-socket-proxy) between the panel and the daemon so that
only the API endpoints the panel actually calls are reachable.

Changes:
- docker-compose.secure.yml: compose overlay that adds the proxy service,
  wires the panel to it via DOCKER_HOST, and shadows /var/run/docker.sock
  with /dev/null to block direct socket access at the filesystem level.
  Allowed: CONTAINERS, EXEC (required for app install / file ops / xdotool),
  IMAGES, VOLUMES, POST, INFO. Everything else is explicitly denied.
- doc/安全加固.md: explains the threat model, how the proxy mitigates it,
  residual gaps (endpoint-level filtering cannot inspect request bodies),
  usage instructions, and image digest pinning as a complementary practice.

Default docker-compose.yml is not modified.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 00:30:19 +08:00
Gloridust
ba32cc90ae fix(dark): 撤掉不稳定的容器 portal 方案,浏览器深色回到 --force-dark-mode
实测极简 KasmVNC 容器里 portal 方案不可靠:autostart 起的 session dbus 会死
(gsettings 提交报 Connection refused),微信内核也不订阅 portal 的 color-scheme 信号,
所以微信不会变深、浏览器的"实时切换"也落空。故回到简单可靠的做法:
- 浏览器(Chromium):WOC_DARK=1 时 autostart 加 --force-dark-mode(开机即深,重启实例生效)。
- 微信:本开关无效,深色请在微信「设置→通用→深色模式」手动选。
- 移除:Dockerfile 的 xdg-desktop-portal/dconf 等包、autostart 的 portal 段、woc-dark.sh、
  docker.ts 的 setInstanceDark、index.ts 的在线切换循环。面板开关仅持久化 desktopDark(驱动开机明暗)。

去掉每实例常驻的 dbus/portal,也消除其对稳定性的潜在影响。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.2.3
2026-06-19 02:11:47 +08:00
Gloridust
eefce1e9ba fix(dark): woc-dark.sh 复用 autostart 真实总线 + 浏览器改纯 portal 跟随(修实时切换)
两个实测出的 bug:
1) 总线不一致:base 镜像里 XDG_RUNTIME_DIR=/config/.XDG,autostart 的 portal 总线在
   /config/.XDG/woc-bus;但面板 docker exec woc-dark.sh 不继承会话环境,原先默认 /tmp/woc-run-<uid>,
   连错总线 → 改了 dconf 也通知不到运行中的 portal → 微信/浏览器不实时变。改为 autostart 把真实总线
   地址写入 /config/.woc-dark-env,woc-dark.sh source 它。
2) 浏览器恒深色:之前给 Chromium 加 --force-dark-mode 是启动期固定开关,切回浅色无效、必须重启。
   去掉它,改为同微信一样纯靠 portal 的 color-scheme/gtk-theme,实现双向实时切换。
另:autostart 不再 export 静态 GTK_THEME(会让 GTK 恒深色),明暗统一走 gsettings。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 02:11:47 +08:00
Gloridust
698f767f82 fix(build): build-local 用 --provenance=false 出单一镜像,避免经典 API 仍解析到旧镜像
Docker 29 + containerd 镜像存储下,plain docker build -t :tag 会生成带 attestation 的
manifest list;docker image inspect / docker run / 面板用的 dockerode 解析 :tag 时不跟到
新 manifest list,仍指向同名旧镜像,导致重建实例还是用旧镜像。加 --provenance/--sbom=false
出单一镜像即可被经典 API 正确解析。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 02:11:47 +08:00
Gloridust
335b839e9a feat(dark): 面板/实例统一深色开关(portal 实时切换)+ 修复 KasmVNC 致命崩溃自愈
实例深色不再靠 GTK_THEME(微信 Chromium 内核不吃),改用 freedesktop 桌面 portal:
每个实例常驻 session D-Bus + xdg-desktop-portal(gtk 后端) + dconf(固定总线地址),
面板顶栏主题开关(管理员)统一控制面板 + 实例深色,持久化进 accounts.json,并对运行中实例
docker exec woc-dark.sh 实时切换 color-scheme(微信据 SettingChanged 实时重绘),无需改 env。

- docker/Dockerfile: 装 xdg-desktop-portal(+gtk) / dconf / gsettings / dbus-x11(单独成层)
- docker/autostart: woc_setup_portal 常驻 portal,初始明暗由 WOC_DARK 决定
- docker/woc-dark.sh: 面板经 exec 调用,实时改 gsettings color-scheme
- store.ts: 持久化 desktopDark;docker.ts: envList 读它 + setInstanceDark;index.ts: /api/desktop-theme
- AppShell 主题开关接 setDesktopTheme(含 auto 跟随系统联动);移除 WOC_INSTANCE_DARK env

KasmVNC 客户端 bundle 长跑空闲后偶发 'lastActiveAt' 致命崩溃 → 仅在其致命错误浮层出现时
做一次干净整页重载(安全重连路径,非激进 churn),5min 限 4 次防死循环。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 02:11:47 +08:00
Gloridust
60dd6d018f feat(panel): 深色模式(跟随系统/亮色/深色),主页顶栏图标切换
- 主页顶栏最右加主题图标按钮,循环切换 跟随系统 / 亮色 / 深色,存浏览器 localStorage。
- index.html 内联脚本首屏前置 data-theme,避免亮/暗闪烁。
- styles.css 加牛奶布艺·暗色变量(亮蓝黑 base、surface 更高一档、近纯黑阴影、极淡顶面高光),
  data-theme=dark 强制深色、=auto 跟随 prefers-color-scheme、=light 维持亮色。
  界面基于 CSS 变量,覆盖核心变量即整体适配。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 02:11:47 +08:00
Gloridust
bf99cc2116 fix(self-update): 防误报失败 + 防并发重入
- helper 重建前先等 2.5s,让面板把 HTTP 响应回给前端再停它,避免前端误报"更新失败"。
- triggerSelfUpdate 加 in-flight 守卫,两次点击不会派生两个 helper 互相打架。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 02:11:47 +08:00
Gloridust
f939538113 feat(panel): 面板内「一键更新面板」(自更新 + 失败回滚)
面板本身的更新此前必须去 docker/飞牛手动 pull + 重建。现支持在「关于」里一键更新(仅在检测到
新版时出现,管理员可见):

- 机制:面板先拉新镜像,再派生一个临时 helper 容器(用新镜像 + docker.sock)重建 woc-panel:
  停旧 → 按旧容器配置(env 用「新镜像 baked + compose 注入」合并,保留 Labels/HostConfig/网络)
  用新镜像重建 → 起来 → 稳定运行检查;失败则回滚旧镜像。helper 干完退出。
- 最坏情况(helper 也挂)用户 docker compose up -d 即可恢复(与手动一致)。
- 已本地实测:成功路径(重建后稳定、env/网络/label 保留)+ 回滚路径(坏镜像→回滚救活),
  且全程不影响真 woc-panel。
- 新增 POST /api/admin/version/self-update + api.selfUpdatePanel + 「关于」按钮。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 02:11:47 +08:00
Gloridust
998a57ca25 fix(build): 加 panel/.dockerignore,禁止把宿主 node_modules 拷进镜像
Dockerfile 的 COPY web/ ./ 与 COPY server/ ./ 会把构建上下文里的 node_modules 一并拷入,
若宿主先前在本地装过依赖(如 macOS),其平台特定二进制(@esbuild/darwin-arm64)会覆盖镜像内
npm install 的 Linux 依赖 → tsx/esbuild 报 "Host version ... does not match binary version" 起不来。
加 .dockerignore 排除 **/node_modules(及 web/dist 等),保证本地构建与 CI(干净 checkout)一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 02:11:47 +08:00
Ethan Zou
9b8adb2c2a Update README.md 2026-06-17 20:18:42 +08:00
Gloridust
23aa65d25c fix(desktop): 回退到 1.1.7 的稳定连接行为,杜绝自动重连/重启 churn
回归分析(对比 v1.1.7,当时很稳):1.1.7 没有音频桥、没有 VNC 响应性看门狗、没有任何面板侧
自动重连/重启 —— 连接很稳,靠 noVNC 自带重连 + 手动刷新兜底。之后陆续加的东西反而制造了
"频繁卡死/重启":

- VNC 响应性看门狗自动重启 → 已默认关闭(f5e5bb5)。
- 本会话加的客户端自动重载(健康监测)+ 自动重启(heal)+ 回前台重载 → 本提交全部撤掉:
  连接态监测改为「仅记录 [client] 日志、不自动动作」;恢复交给 noVNC 自带重连 + 用户手动
  「重新连接/重启」(与 1.1.7 一致,不再有重载/重启 churn)。
- 音频桥(1.1.7 没有,是每实例一条额外 socket.io)→ 改为「声音」开关,默认关,回到 1.1.7 的
  无音频连接行为;想听声音再开。「麦克风」开关仅在「声音」开启时出现。

诊断用的连接日志([vnc] 服务端 + [client] 前端状态)保留,便于继续定位。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.2.2
2026-06-17 02:51:11 +08:00
Gloridust
d33bba69b9 feat(desktop): 麦克风开关(默认关),不再一打开实例就抢占麦克风
之前打开实例 = 焦点即 getUserMedia 采集麦克风,导致 AirPods 等被切到低音质通话模式。
新增 nav「麦克风:开/关」开关,默认关:默认不占用麦克风(AirPods 保持高质输出),
需要语音/通话时再开。扬声器(听实例声音)始终可用、不受影响。状态存 localStorage。

- VncAudio 增加 micEnabled(默认 false)+ setMicEnabled();startMic 关闭时直接跳过 getUserMedia。
- Desktop 顶栏加开关,切换即时生效(setMicEnabled,不重建音频连接)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 00:25:36 +08:00
Gloridust
94e3400c93 feat: 客户端连接日志 + 自愈放宽预热窗口(避免误打断刚启动的实例)
诊断包实锤:刚创建/重启的实例 KasmVNC 需约 10-15s 预热,期间 nginx 已能回 101 但 ws 接收器
还没就绪(容器日志 0 条 got client connection)→ noVNC 拿到 ws 却收不到 RFB,反复 connecting→
掉线→重连。之前 15s 阈值会在预热期就整页重连/请求重启,反而拖长"正在连接"。

- 自愈阈值 15s→30s,先让预热 + noVNC 自带重连自行连上,不过早打断。
- 新增客户端连接日志(回传服务端,[client] 前缀,与 [vnc] 服务端日志对齐):
  VNC 状态变化(connected/connecting/reconnecting/disconnected)、整页重连、请求重启、回前台重连、
  iframe 加载 —— 下次卡死即可看清"到底是 noVNC 在重连、还是我们的逻辑在重载、还是服务端没起来"。
- 新增 POST /api/instances/:id/clientlog(需访问权,限长)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:55:44 +08:00