fix(web): desktop view ensures session on open and auto-reconnects after 5s blank (INT-3)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ed868f166e
commit
82df5662f0
2 files changed
+54
-2
No files matched your search
@@ -3,7 +3,12 @@ import type { SiteListItem } from "@umt/contracts";
|
|||||||
import { Hand, Image as ImageIcon, LoaderCircle, Monitor, Power, RotateCw, Undo2 } from "lucide-react";
|
import { Hand, Image as ImageIcon, LoaderCircle, Monitor, Power, RotateCw, Undo2 } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useBrowserControl, useCloseBrowserSession, useSiteBrowserSession } from "~/api/browser.ts";
|
import {
|
||||||
|
useBrowserControl,
|
||||||
|
useBrowserReconnect,
|
||||||
|
useCloseBrowserSession,
|
||||||
|
useSiteBrowserSession,
|
||||||
|
} from "~/api/browser.ts";
|
||||||
import { LoadingState } from "~/components/console/empty-state.tsx";
|
import { LoadingState } from "~/components/console/empty-state.tsx";
|
||||||
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
||||||
import { Button } from "~/components/ui/button.tsx";
|
import { Button } from "~/components/ui/button.tsx";
|
||||||
@@ -46,6 +51,19 @@ const CONTROLLER_TONE: Record<string, StatusTone> = {
|
|||||||
|
|
||||||
type Mode = "auto" | "screenshot";
|
type Mode = "auto" | "screenshot";
|
||||||
|
|
||||||
|
/** 画面加载后多久还没出图算黑屏 */
|
||||||
|
const BLANK_TIMEOUT_MS = 5_000;
|
||||||
|
|
||||||
|
/** 同源时看 Selkies 的视频流有没有出帧;跨源 / 拿不到文档时不判黑屏(只按 onLoad 判断) */
|
||||||
|
function desktopBlank(frame: HTMLIFrameElement | null): boolean {
|
||||||
|
try {
|
||||||
|
const video = frame?.contentDocument?.querySelector("video");
|
||||||
|
return !!video && (video.readyState < 2 || video.videoWidth === 0);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 原站视图 Tab:能用桌面模式就用,否则截图模式 */
|
/** 原站视图 Tab:能用桌面模式就用,否则截图模式 */
|
||||||
export function SiteOriginPanel({ site }: { site: SiteListItem }) {
|
export function SiteOriginPanel({ site }: { site: SiteListItem }) {
|
||||||
const session = useSiteBrowserSession(site.id);
|
const session = useSiteBrowserSession(site.id);
|
||||||
@@ -99,13 +117,45 @@ function DesktopView({
|
|||||||
const refreshRole = () => void qc.invalidateQueries({ queryKey: qk.sub("browser", "desktop") });
|
const refreshRole = () => void qc.invalidateQueries({ queryKey: qk.sub("browser", "desktop") });
|
||||||
const [reloadKey, setReloadKey] = useState(0);
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const frame = useRef<HTMLIFrameElement>(null);
|
||||||
|
/** 打开时确认失败 / 黑屏超时:交给 useAutoReconnect 自动重连一次 */
|
||||||
|
const [stuck, setStuck] = useState(false);
|
||||||
|
|
||||||
const reloadDesktop = () => {
|
const reloadDesktop = () => {
|
||||||
setLoaded(false);
|
setLoaded(false);
|
||||||
setReloadKey((k) => k + 1);
|
setReloadKey((k) => k + 1);
|
||||||
};
|
};
|
||||||
// BR-3:浏览器服务(同一容器里的 Selkies)重启过 → 自动重连会话一次,成功后重建桌面连接
|
// BR-3:浏览器服务(同一容器里的 Selkies)重启过 → 自动重连会话一次,成功后重建桌面连接
|
||||||
const rc = useAutoReconnect(site.id, s, false, reloadDesktop);
|
const rc = useAutoReconnect(site.id, s, stuck, () => {
|
||||||
|
setStuck(false);
|
||||||
|
reloadDesktop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// INT-3(VER-3b):桌面 iframe 直连 Selkies,不经过截图 / 操作接口,不会触发服务端的被动重连 →
|
||||||
|
// 打开桌面时先调一次幂等的 reconnect(上游上下文还在时服务端什么都不做),失败再走自动重连
|
||||||
|
const ensure = useBrowserReconnect(site.id);
|
||||||
|
const { mutate: ensureMutate } = ensure;
|
||||||
|
const sid = s && s.controller !== "closed" ? s.id : null;
|
||||||
|
const ensured = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sid || ensured.current === sid) return;
|
||||||
|
ensured.current = sid;
|
||||||
|
ensureMutate(sid, { onError: () => setStuck(true) });
|
||||||
|
}, [sid, ensureMutate]);
|
||||||
|
|
||||||
|
// 画面加载失败或黑屏超过 5 秒:每个会话自动重连一次(之后靠「重新连接」按钮)
|
||||||
|
const blankTried = useRef<string | null>(null);
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: reloadKey 变化 = 新一轮加载,重新计时
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sid || blankTried.current === sid) return;
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
if (loaded && !desktopBlank(frame.current)) return;
|
||||||
|
blankTried.current = sid;
|
||||||
|
console.debug("[browser] 桌面画面 5 秒未出图,自动重新连接一次", sid);
|
||||||
|
setStuck(true);
|
||||||
|
}, BLANK_TIMEOUT_MS);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [sid, loaded, reloadKey]);
|
||||||
// 别处(Agent 操作、截图)触发的重连成功后也重建桌面连接
|
// 别处(Agent 操作、截图)触发的重连成功后也重建桌面连接
|
||||||
const reconnectedAt = s?.reconnect?.status === "ok" ? s.reconnect.at : null;
|
const reconnectedAt = s?.reconnect?.status === "ok" ? s.reconnect.at : null;
|
||||||
const prevReconnect = useRef(reconnectedAt);
|
const prevReconnect = useRef(reconnectedAt);
|
||||||
@@ -230,6 +280,7 @@ function DesktopView({
|
|||||||
) : null}
|
) : null}
|
||||||
<iframe
|
<iframe
|
||||||
key={reloadKey}
|
key={reloadKey}
|
||||||
|
ref={frame}
|
||||||
title="浏览器桌面"
|
title="浏览器桌面"
|
||||||
src={status.path}
|
src={status.path}
|
||||||
onLoad={() => setLoaded(true)}
|
onLoad={() => setLoaded(true)}
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ pixelflux + labwc Wayland 桌面 + Selkies 2.0 Web 客户端)+ camofox 1.17.0 **
|
|||||||
| 初始化 | `init-umt-desktop` | 由 `CAMOFOX_ACCESS_KEY` 派生 Selkies 控制 / 只读口令;覆盖 labwc autostart;把 `/root/.camofox` 交给 `abc` |
|
| 初始化 | `init-umt-desktop` | 由 `CAMOFOX_ACCESS_KEY` 派生 Selkies 控制 / 只读口令;覆盖 labwc autostart;把 `/root/.camofox` 交给 `abc` |
|
||||||
| 反代 | `apps/server/src/modules/desktop` | `/api/v1/browser/desktop/{sessionId}/*` → 容器 `:3000`(静态客户端 + `api/websockets`);只接受控制台会话 |
|
| 反代 | `apps/server/src/modules/desktop` | `/api/v1/browser/desktop/{sessionId}/*` → 容器 `:3000`(静态客户端 + `api/websockets`);只接受控制台会话 |
|
||||||
| 前端 | `apps/web/app/components/browser/desktop-view.tsx` | 原站视图 Tab:桌面可用且是安全上下文 → 桌面模式;否则截图模式并说明原因;可手动切换 |
|
| 前端 | `apps/web/app/components/browser/desktop-view.tsx` | 原站视图 Tab:桌面可用且是安全上下文 → 桌面模式;否则截图模式并说明原因;可手动切换 |
|
||||||
|
| 自愈(INT-3) | 同上 | 打开桌面先调一次幂等 `POST /browser/sessions/{id}/reconnect`(iframe 直连 Selkies 不触发被动重连);画面 5 秒未加载或视频流未出帧 → 每个会话自动重连一次并重建桌面连接,之后靠「重新连接」按钮 |
|
||||||
| HTTPS | compose `caddy`(profile `tls`)+ `docker/browser-desktop/Caddyfile` | 可选;已有反代终结 TLS 时不用 |
|
| HTTPS | compose `caddy`(profile `tls`)+ `docker/browser-desktop/Caddyfile` | 可选;已有反代终结 TLS 时不用 |
|
||||||
|
|
||||||
窗口:无标题栏、最大化铺满 1280×900(`BROWSER_DESKTOP_WIDTH/HEIGHT`),`HARDEN_DESKTOP` 去掉终端 / sudo / xdg-open,
|
窗口:无标题栏、最大化铺满 1280×900(`BROWSER_DESKTOP_WIDTH/HEIGHT`),`HARDEN_DESKTOP` 去掉终端 / sudo / xdg-open,
|
||||||
|
|||||||
Reference in new issue
Block a user