Commit Graph

138 Commits

Author SHA1 Message Date
Gloridust
9644b31ad3 fix(install): 磁盘写满时明确报「磁盘空间不足」,不再误导查网络
真实案例:某 NAS 盘满 → curl 以退出码 23(本地写失败)反复卡在同一字节、
status.json 也写不进去(面板显示无下载进度),用户误当成网络/代理问题查了半天。
根因是磁盘 ENOSPC(容器日志同时报 errno 28 / 设备上没有空间),与网络无关。

- 下载前 df 预检:可用空间 < 需求(deb 4 倍,不低于 900MB)直接报「磁盘空间不足」并给清理指引
- 下载失败后按 curl rc=23 / df 复查判定磁盘问题,走磁盘专属报错而非「请检查网络」

配合 91a220d 的断点续传:续传解决「网络断」,本次解决「盘写满」——两类「装不完」都对症。
2026-07-19 20:35:25 +08:00
Gloridust
1261e96a2b feat(diag): 诊断包收录每实例 status.json + install.log 尾部(排查装不上无死角)
导出诊断此前看不到微信安装为何失败——只有面板/容器日志,没有
安装进度(status.json)和下载每步(install.log)。现在每个实例段追加:
- status.json:phase/percent/error(面板轮询的那份),一眼看出卡在哪
- install.log 尾 50 行:curl 退出码、已下字节、续传轮次(v1.4.8 起才有)

execCapture 对非 0 退出会抛错,而 tail/cat 不存在的文件退 1,故命令
统一 || true 兜底,文件缺失时走「(无)」而非整段丢失。配合 91a220d
的安装续传修复,形成「装不上→导诊断→定位」闭环。
v1.4.8
2026-07-19 16:40:31 +08:00
Gloridust
91a220d6af fix(install): 微信下载支持断点续传 + 并发锁 + 完整性校验(修反复卡进度退出)
用户反馈安装卡在 ~28% 反复退出、多次重试无效(诊断包实证:panel.log 里短时间连点多次「触发下载安装」)。
do_install 有两处放大网络抖动的缺陷:
- 每次触发都先 rm -rf $WORK_DIR:用户重试时把上一次下到一半的包删掉;
- curl 不带 -C -:每次从 0 重下,网络稍差就永远下不完,且反复停在相近百分比。

修:
- 并发锁($STATE_DIR/.install.lock,mkdir 原子 + pid 存活判定):已有安装在跑则跳过本次触发,
  让它续着下完,不再互相踩。
- curl -C - 断点续传 + --retry-all-errors + 外层最多 6 轮,重试前【绝不删】部分文件 → 从断点继续。
- 下载后 dpkg-deb -f 完整性校验:半包/损坏才删了重下,否则不白费已下的量。
- 全程写 $STATE_DIR/install.log,便于线上定位(此前安装失败在诊断包里完全看不到原因)。

注:待 Docker 恢复后补端到端验证(真拉一次微信 deb + 并发锁)再考虑发版。
2026-07-19 15:46:47 +08:00
Gloridust
896a788a54 fix(image): 区分「连不上仓库」与「版本不存在」,别再回退到过期镜像 (issue #114)
用户面板 v1.4.6 却报「wechat-on-cloud:1.4.6 在本地与镜像仓库均不存在」——但该 tag 在 ghcr 与
Docker Hub 都在(实测 HTTP 200)。#107 同理:面板报 1.2.7 不存在,实测它也在(200)。

根因:probe 拿不到就返回 null,代码把「网络不通」与「tag 不存在」当同一回事 → 回退到本地可能几周前的
:latest → 用户面板是新版、实例却永远停在老镜像,还打出「很可能该版本未成功发布」的误导文案。
国内 NAS 极常见:docker pull 走加速镜像能通,但面板进程直连 registry API 不通。

修:
- probeManifest 返回判别式结果:404=missing(仓库确认没有)/ 其它非2xx与网络错误=unreachable(未知)。
  已实测两个仓库对不存在的 tag 均回 404(非 401),该判据成立。
- resolveInstanceImage:仅 missing 才回退 :latest;unreachable 时保持版本 tag(乐观),把判决权交给
  真正的 docker pull(它有镜像加速配置,多半能拉到)。文案改为网络提示,不再误导。
- ensureImage:真兜底改为基于【真实拉取失败】——拉不到且本地有 :latest 才退而求其次,并明确告知
  可能较旧、网络恢复后重新升级。
v1.4.7
2026-07-16 20:58:15 +08:00
Gloridust
3ce8065f56 fix(self-update): WOC_VERSION 绝不从旧容器继承 —— 修「永久卡在旧版本、更新无效」(issue #107)
用户反馈「一键更新到了 1.2.7」:更新后面板仍自报 v1.2.7。诊断包实锤——他的 panel.log 里同时有
v1.4.3+ 才写的代码日志、却打印「版本 v1.2.7」,即【顶着旧版本号跑新代码】。

根因:buildCreateOpts 用「容器 env != 旧镜像 baked」判定为 compose 注入并保留。但 WOC_VERSION 一旦
偏离(旧镜像 inspect 失败 → oldBaked 为空 → 所有容器 env 都被当成注入而保留),新面板就继承了旧版本号。
更糟的是【自我延续】:偏离后容器值与新镜像 baked 值永远不等,每次自更新都再次保留,用户永远出不来
(只能手删容器重建)。还连累版本耦合去找 wechat-on-cloud:<旧版本>。

修:WOC_VERSION 属【镜像身份】,加入排除名单,永远取新镜像 baked 值;compose 真注入的变量不受影响。
存量受害者可自愈:自更新的 helper 用的是新镜像的代码,故下次点更新即走修复后逻辑逃出循环。
2026-07-16 20:58:15 +08:00
Gloridust
9ea07163e8 feat(disk): 升级后自动清理旧版本镜像(不止悬空)—— 修历史镜像占满磁盘
pruneDanglingImages 只删无 tag 的 <none> 镜像。版本耦合(v1.4.4)后面板拉的是带版本号的
tag(:1.4.5),升级到新版后旧的 :1.4.x 镜像仍带 tag、不是 dangling → 每个历史版本(实例约
4GB、面板约 0.5GB)长期堆积(用户反馈"历史镜像占用太多")。

新增 pruneOldWocImages:删掉"属于 woc 仓库(wechat-on-cloud/woc-panel) + 不被任何容器引用
+ 非当前版本"的镜像。保留集 = 当前实例镜像 + 面板自身镜像 + 所有现存容器(含已停止)在用的镜像,
绝不动正在用/未升级实例的镜像;docker rmi 本身也拒删在用镜像,双重保险。WOC_KEEP_OLD_IMAGES=1 可关。

接入:单实例/一键升级完成后、面板启动 30s 后(回收自更新留下的旧面板镜像 + 周期性兜底)。
本机实测:删掉 4 个孤儿旧面板镜像,6 个在用实例镜像与当前镜像全部保留、实例照常运行。
v1.4.6
2026-07-08 20:24:38 +08:00
Gloridust
d729b80d48 fix(ci): 实例/面板镜像补回 OCI version label(修 v1.4.4 镜像版本显示 876361b9 回归)
native-runner 重写把 metadata-action 挪到 merge job,build job 不再打 labels,
imagetools create 又不写 config label → 最终镜像露出 base 的 876361b9-ls121,
实例卡「镜像版本」角标显示错乱、用户以为没升级成功。
build job 加回 metadata-action 生成 labels + build-push labels,覆盖 base label。
v1.4.5
2026-07-08 14:02:44 +08:00
Gloridust
d29ea66730 docs(p0): 记录 QEMU OOM 的最终解决——改用原生 arm64 runner(v1.4.4 闭环) 2026-07-08 03:57:12 +08:00
Gloridust
5aa0ce0414 fix(ci): 手动构造镜像 ref 前把 owner 转小写(Gloridust→gloridust)
原生 runner 重写里 outputs/srcs 直接用了 github.repository_owner(含大写),
docker 拒绝大写 repo 名。metadata-action 会自动小写、但手动 ref 不会。
加一步 ${GITHUB_REPOSITORY_OWNER,,} 转小写,build/merge 两个 job 统一引用。
v1.4.4
2026-07-08 03:33:18 +08:00
Gloridust
fe964925c1 fix(ci): 改用原生 arm64 runner 分架构构建,根治 QEMU apt OOM
QEMU 模拟下 arm64 的 apt-get update 反复 OOM(getline: Cannot allocate memory),
让实例镜像 1.4.1/1.4.3 都发不出去;源精简只降概率不治本(1.4.3 又栽在 snapshot InRelease)。
公开仓库可用免费 ubuntu-24.04-arm 原生 runner——彻底不走 QEMU:
- 每个 (镜像 × 架构) 在各自原生 runner 上按 digest 推送到 GHCR;
- merge job 从两个 arch digest 合成多架构 manifest,一条 imagetools create 打全部 tag
  (blob 由 GHCR 复制到 Docker Hub,双仓库一并覆盖);
- 冒烟门禁保留在 wechat+amd64(原生 x86 可 docker run)。
actionlint 通过。
2026-07-08 03:25:10 +08:00
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