Commit Graph

110 Commits

Author SHA1 Message Date
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
Gloridust
0811d463e8 style(panel): 诊断/关于 改为扁平区块,修按钮断行与缺失的 chip 类
这两个区块各自只有单一内容,卡片外框显得厚重;按钮作为 flex 项被压窄导致
"导出诊断包(.tar.gz)" / "查看面板日志" 在按钮内断行。

- 去掉 inst-card 卡片外框,改为扁平 settings-block:标题下直接排描述/控件/操作。
- 主按钮 s-btn 固定内边距 + white-space:nowrap + flex:none,不再被压窄断行;
  次要操作(查看面板日志 / 检查更新 / 发布日志)改 btn-text 文字链接,narrow 下自然换行。
- 修复:时间范围 chip 漏了 .chip 基类(只有 .chip-toggle 无内边距/圆角),补回 'chip chip-toggle'。
- 「导出诊断包(.tar.gz)」按钮文案精简为「导出诊断包」,格式说明移到脚注。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:33:20 +08:00
Gloridust
d42d401f89 chore(docker): Chromium 关闭后台 phone-home(--disable-background-networking)
Chromium 在受限网络(NAS/被墙)下后台不断尝试连 Google 推送(GCM)/组件更新/变体下载
并失败,刷屏 "gcm ConnectionHandler failed net error: -2" 等。加 --disable-background-networking
关掉这些后台流量。

仅影响后台 phone-home,不影响前台网页加载与真实网络错误提示(已验证 Chromium 正常启动、
前台浏览不受影响)。属安全的标准容器化 Chromium 用法。

注:日志里的 gcm/dbus 噪声本就无害,从来不是黑屏/重启的原因;此 flag 主要是减少受限网络下
的无谓后台请求。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:03:42 +08:00
Gloridust
9929a72be1 feat(panel): 全局日志系统 + 一键诊断包导出
单实例「日志」只记录该实例日志,无从排查跨实例/容器层面的问题(首个实例创建卡死、
打开实例黑屏不可用、升级失败等)。新增面板级全局日志 + 一键诊断包:

- logs.ts(新):统一持久化日志(面板数据卷,跨重建保留)。实例日志原语从 docker.ts
  迁来;新增 appendPanelLog/readPanelLog、按时间裁剪 filterSince、一年保留 pruneOldLogs、
  时间范围 24h/7d/30d/1y。无 docker 依赖避免循环。
- 仪表化:实例创建(含镜像拉取前后,定位首次拉取卡死)、删除、启停、重启、升级、
  应用安装/更新、看门狗自愈 均写入面板全局日志(同时回显 stdout)。
- docker.ts buildDiagnostics:打包 system.txt(Docker/镜像/系统)+ panel.log +
  instances/<id>.log(容器状态 inspect + 持久日志 + 实时日志)+ containers.txt(全部
  woc-* 容器清单,含残留),手搓多文件 tar.gz(沿用既有无依赖 tar 风格)。
- 路由:GET /api/admin/diagnostics?range=(导出 tar.gz)、GET /api/admin/panel-log?range=;
  启动 + 每 24h 跑 pruneOldLogs。
- 前端:管理页「诊断与日志」区——时间范围(24h默认/7d/30d/1y) + 导出诊断包 + 查看面板日志。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:48:36 +08:00
Gloridust
0abb5f8bdf fix(panel): 开发版构建的版本展示,不再误标「已是最新」
本地/未发布构建(dev / dev-<sha>)无法与发布版做语义化比较,之前会错误显示
「· 已是最新」。现在:

- 「关于」卡:当前版本非 vX.Y.Z 时标「开发版」,仅把最新发布版作为信息展示
  (当前版本 dev-xxx · 最新发布 v1.2.0),不显示「已是最新」、不触发红点。
- build-local.sh 默认烤入 dev-<短SHA>(而非裸 dev),便于辨识本地构建。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:35:30 +08:00
Gloridust
7293285d1a feat(panel): 显示构建版本号 + 自动检测新版(Docker Hub/GHCR 红点)
管理界面此前看不到面板版本,也无从知道有没有新版可升。现在:

- 构建时把版本号烤进面板镜像:Dockerfile 新增 ARG/ENV WOC_VERSION(放末尾不
  破坏依赖缓存);release.yml 用 git tag 注入(vX.Y.Z,手动触发为 dev-<sha>),
  仅面板镜像消费;build-local.sh 支持 --build-arg(默认 dev)。
- 后端 version.ts:best-effort 查询 Docker Hub 与 GHCR 上 woc-panel 的语义化
  标签取最大值,与当前版本比对;启动后 4s 首检 + 每 6h 复检 + 接口惰性触发,
  失败静默(离线/被墙/私有源不报错、不显红点)。命名空间从 WOC_WECHAT_IMAGE 推断。
- 接口:GET /api/version(任意登录用户读缓存)、POST /api/admin/version/check
  (管理员手动重查)。
- 前端:管理页「关于」卡显示当前版本/最新版/升级提示/检查更新/发布日志链接;
  侧栏「管理」入口在有新版时点红点(仅管理员)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:40:17 +08:00
Gloridust
8cf15923a6 docs(readme): 写入浏览器实例支持 + 精简整理
v1.2.0 起云微是多应用平台(微信 + Chromium 浏览器),README 仍是纯微信
视角,补齐并整理:

- 新增「浏览器实例(登录网页版社媒)」章节:Chromium 随镜像就绪、登录态
  写入数据卷常驻、多端共享、中文输入/文件/剪贴板共用一套;顶部导航加锚点。
- 标题/简介/一句话原理/核心特性/快速开始步骤/「面板能做什么」表 全部从
  「微信」泛化为「实例(微信 / 浏览器)」;新建按钮文案对齐为「新建实例」。
- 精简冗杂:合并过长的「为什么默认 Docker Hub」blockquote 为一行。
- 资源占用补充浏览器实例量级说明;安全须知覆盖社媒账号;
  路线图标记多应用平台完成、日志改为「持久化(跨容器重建保留)」。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:01:37 +08:00
Gloridust
2e201ff058 feat(panel): 持久化实例日志,跨容器重建保留
实例容器日志随重建(重启/升级/看门狗自愈)即丢失,用户看不到"上次为何
重启/崩溃"(浏览器实例常因内存触顶被看门狗重启)。现把生命周期事件 + 重启
原因 + 重建前的容器日志快照,追加到面板数据卷 /data/logs/<id>.log,跨重建保留。

- docker.ts: appendInstanceLog/readInstanceLog/snapshotContainerLog/deleteInstanceLog;
  日志目录与 accounts.json 同卷(宿主 ./data-panel 持久化),单实例上限 ~400KB
  超限截半保留最近;id 十六进制校验防路径注入。
- runInstance 删旧容器前先快照其最后日志、启动后记"容器已启动";
  stopInstance 记"容器已停止";removeInstance 彻底删除时清理日志文件。
- 看门狗 recover() 写入自愈原因(hard/soft/health + 内存明细)。
- 日志接口返回「持久化历史 + 本次容器实时日志」两段。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:53:11 +08:00
Gloridust
167a80a0c3 feat(ci/telegram): issue 新回复也推送(简短)
telegram-notify 增订阅 issue_comment(created),tg-notify.mjs 加 issue_comment 分支:
💬 Issue #N 新回复 · 标题 / 评论人 / 评论摘要(≤400字) / 链接。PR 的评论也走此事件,按 C_PR 跳过;
回复通知不置顶(置顶仍仅限 release)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v1.2.0
2026-06-14 21:25:14 +08:00
Gloridust
1c230316ed feat(v1.2.0): 自定义实例图标——内置精选 + 上传裁剪
- AppIcon:内置精选平台图标(微信/Chromium/Telegram/小红书/抖音/B站/微博/知乎/YouTube/通用)+ ICON_CHOICES。
- 编辑器(管理菜单「图标」):选内置图标 / 上传图片用 react-easy-crop 方形裁剪→128px PNG / 恢复默认。
- 后端 setInstanceIcon + /api/admin/instances/:id/icon(仅 admin;icon=builtin:<key>/data:图片/空,限 ~225KB)。
- 新增依赖 react-easy-crop。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:41:40 +08:00
Gloridust
1ffc6d7e1d feat(v1.2.0): 实例头像由首字母改为按应用类型的图标(自定义图标铺底)
新增 AppIcon.tsx(InstanceIcon + 内置 SVG 图标:微信/Chromium/Telegram/通用),侧栏与主页
卡片头像改用它(按 appType 出默认图标;data: 图片 / builtin:<key> 优先)。
新增 Instance.icon 字段 + publicInstance 下发,为「自定义图标(内置选择 + 上传裁剪)」铺底。
侧栏/主页标题「微信实例」→「实例」,主页副文案按应用名泛化。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:01:03 +08:00
Gloridust
7c9eb82a6f fix(v1.2.0): 旧容器无 app-ctl.sh 时回退 wechat-ctl.sh(修微信实例误报未安装)
多应用分发对所有实例调 /woc/app-ctl.sh,但升级前的旧容器镜像里没有该脚本 → exec 失败 →
wechatStatus 兜底成"未安装",已装微信的老实例全变"待安装"。triggerWechat/wechatStatus 改为
bash -c:有 app-ctl.sh 则按 appType 分发,无则回退老的 wechat-ctl.sh(旧实例皆微信)。
老实例不升级也能正常显示/操作;升级到多应用镜像后自动走 app-ctl 分发。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:52:08 +08:00