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
Gloridust
342a2f22a2
feat(panel): 记录每次远程桌面 ws 连接尝试/建立/失败/关闭
...
在面板反代层为每条 VNC ws 连接打点到实例持久日志:
- [vnc] 连接尝试 user/ip(server upgrade 处)
- [vnc] 上游已接受(101) · 桌面连接建立(proxyReqWs 收到上游 101)
- [vnc] 连接失败:<err>(proxy.ws 错误回调)
- [vnc] 连接关闭(持续 Ns)(client socket close)
与实例容器内 KasmVNC 的 "got client connection/accepted" 按时间对齐,即可精确定位卡死发生在
"面板→实例之间"(有尝试无 101)还是"实例 KasmVNC 内部"(有 101 但无 got client connection)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-15 22:31:34 +08:00
Gloridust
c0af1d766a
feat: 实例卡死自愈——客户端多次干净重连失败即请求重启容器
...
实测确认:KasmVNC 的 ws 接收器会偶发卡死(nginx 仍能serve 静态页让 noVNC 显示"正在连接",
但新 ws 永远 accept 不了;强刷新、重启面板均无效,只有重启实例容器能恢复)。这是 KasmVNC
基础镜像层的问题,面板无法直接修复,故改为可靠检测 + 自动重启兜底。
- 客户端健康监测(7d9b5ac,依赖可靠的 noVNC_connected class)干净重载 2 次仍连不上后,
调 POST /api/instances/:id/heal 请求重启该实例(数据保留),再整页重载连到全新容器。
客户端 + 服务端各 3 分钟限频,防重启风暴。
- heal 接口:需对该实例有访问权(让被卡住的用户能自助恢复,不必等管理员)。
- 放弃 /websockify 合成 WebSocket 探活:KasmVNC 握手挑剔(返回 404),且无法在健康态验证卡死探测,
改用客户端可靠的连接态信号驱动恢复。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-15 16:23:35 +08:00
Gloridust
7d9b5ac80d
feat(desktop): VNC 连接健康监测 + 卡住自动干净重连
...
读取 kasmweb 在同源 iframe <html> 上的连接态 class(noVNC_connected 等),iframe 已加载
但 VNC 持续 ~15s 不在 connected(卡在"正在连接"/noVNC 自动重连反复失败)→ 自动整页重载干净
重连,不再把用户晾在"正在连接…超时…无响应"。sessionStorage 限流:2 分钟内最多自动重连 2 次,
避免实例服务端真卡死时无限重载(那种需重启实例)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-15 16:09:29 +08:00
Gloridust
b0511f5f11
fix(desktop): 切实例不再重挂 iframe,根治"A→B→A 卡死(刷新无效)"
...
iframe 之前带 key={id},切换实例时 React 会重挂它(删旧元素+建新元素)。删旧元素时
VNC ws 未必干净关闭 → 实例服务端残留半开连接;回到该实例再开新 ws 时新旧并存把 Xvnc
卡死(服务端卡死,刷新都救不了,只能重启容器)。这正是"依次开 A、B,再切回 A 即超时"的根因,
且是长期存在的 bug。
- 去掉 iframe 的 key:切实例只改 src,浏览器“导航”iframe —— 旧文档 unload 干净关闭旧 ws,
再加载新实例,不留半开连接。重连仍走整页重载。
- 与 7fecc09(回前台干净重连)互补:分别覆盖“切实例”与“切走App/锁屏回来”两个并存触发点。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-15 15:36:42 +08:00
Gloridust
7fecc099bc
fix(desktop): 回到前台时干净重连,堵住 noVNC 自动重连的并存卡死
...
855997f 只把"手动"重连(重新连接/重启/切模式)改成整页重载,但没管 noVNC 自带的
reconnect=true 自动重连——切走 App / 锁屏挂起 ws 后回到前台,noVNC 会立刻开新 ws,
而服务端旧连接尚未释放 → 新旧 ws 并存把 Xvnc 卡死("开一段时间断联→卡死→重启"的主因,
手机最严重)。
- 新增 visibilitychange 监听:离开 ≥8s 后回到前台,直接整页重载干净重连(先彻底关旧 ws
再连),从机制上避开并存。阈值可按手机实测微调。
- 仍保留 noVNC reconnect=true 兜前台瞬时抖动(<8s 切回不重载)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-15 15:04:34 +08:00
Gloridust
855997fc70
fix(desktop): 重连/重启走整页重载,根治"重连后 Xvnc 卡死、第二次连接超时"
...
「桌面无响应」的「重新连接」按钮与「重启实例」后的重连此前用页内 bump vncNonce 重挂
iframe——而 setMode 的注释早已指出:页内重挂会让新旧两条 ws 短暂并存、概率性把实例的
Xvnc 卡死(需重启容器才恢复)。于是用户点「重新连接」反而把实例搞死,必须重启;重启后
第一次能连,再触发一次重连就又卡死、第二次连接超时。
- 「重新连接」按钮、restartInstance 重连统一改为 window.location.reload()(与 setMode 同,
整页卸载彻底关闭旧 ws 再干净重连,杜绝 ws 并存)。
- 退休已无用的 vncNonce 页内重挂机制(state/iframe key/effect 依赖一并清理)。
- 「桌面无响应」面板文案不再承诺"自动检测重启自愈"(响应性探测已默认关闭)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-15 14:22:29 +08:00
Gloridust
f5e5bb5bcb
fix(watchdog): VNC 响应性探测默认关闭,可用 env 重新开启
...
实测健康实例 VNC 探测 ~1ms 响应;但宿主级 CPU/IO 偶发争用(如同机重 docker build)
会让 8s 探测超时被误判为 stall,从而重启正常实例(用户反馈"浏览器经常重启")。
- 新增 WOC_WATCHDOG_HEALTH_FAILS(默认 0=关闭该探测);正整数 N = 连续 N 次无响应才重启。
- 内存阈值自愈(soft/hard)不受影响,照常工作。
- .env.example 补充说明。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
v1.2.1
2026-06-15 03:53:59 +08:00