feat(browser): self-heal sessions after camofox restarts (BR-3)
- Any session operation that hits 404/410 (or finds no open tab) reconciles
tabs; if none survive it re-injects the account cookies (BR-2), opens a new
tab under the same userId/sessionKey, returns to the recorded URL (site home
otherwise), marks old tabs lost and retries the operation once. Failure
returns 503 browser_reconnect_failed with the reason, with a 10s backoff.
- Restart detection: umt plugin GET /umt/instance (fallback /health fields);
a changed instance id marks every open session needsReconnect.
- POST /browser/sessions/{id}/reconnect (idempotent); session view exposes
needsReconnect and reconnect {status, trigger, at, reason, humanNotice}.
- Controller and lease are kept; reconnecting during takeover asks the human
to double-check the reloaded page.
- Web: screenshot and desktop modes show "reconnecting", auto-reconnect once,
pause polling, offer a reconnect button with the reason on failure; desktop
mode rebuilds the Selkies connection after a reconnect.
- Mock camofox restart(), tests, smoke step and runbook section 10.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
da24380870
commit
838aea3ebd
19 files changed
+977
-45
No files matched your search
@@ -10,6 +10,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { apiData, errorMessage } from "~/lib/api.ts";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { CONTROLLER_LABEL, credentialBadge, holderName, waitingText } from "./coords.ts";
|
||||
import { ReconnectPanel, ReloadNotice, useAutoReconnect } from "./reconnect.tsx";
|
||||
import { SiteOriginView } from "./site-origin-view.tsx";
|
||||
|
||||
// 原站视图桌面模式(docs/design/webtop-remote-browser.md §5.7):iframe 嵌浏览器桌面(Selkies),
|
||||
@@ -99,6 +100,25 @@ function DesktopView({
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const reloadDesktop = () => {
|
||||
setLoaded(false);
|
||||
setReloadKey((k) => k + 1);
|
||||
};
|
||||
// BR-3:浏览器服务(同一容器里的 Selkies)重启过 → 自动重连会话一次,成功后重建桌面连接
|
||||
const rc = useAutoReconnect(site.id, s, false, reloadDesktop);
|
||||
// 别处(Agent 操作、截图)触发的重连成功后也重建桌面连接
|
||||
const reconnectedAt = s?.reconnect?.status === "ok" ? s.reconnect.at : null;
|
||||
const prevReconnect = useRef(reconnectedAt);
|
||||
useEffect(() => {
|
||||
if (prevReconnect.current !== reconnectedAt) {
|
||||
prevReconnect.current = reconnectedAt;
|
||||
if (reconnectedAt) {
|
||||
setLoaded(false);
|
||||
setReloadKey((k) => k + 1);
|
||||
}
|
||||
}
|
||||
}, [reconnectedAt]);
|
||||
|
||||
// 控制权变化(接管 / 交还 / 过期)后重连,Selkies 按新口令认证;服务端也会主动断开旧连接
|
||||
const role = status.role;
|
||||
const prevRole = useRef(role);
|
||||
@@ -117,11 +137,16 @@ function DesktopView({
|
||||
const waiting = waitingText(s.waitingReason);
|
||||
const cred = credentialBadge(s.credential);
|
||||
const leaseHolder = s.lease ? holderName(s.lease.holder) : null;
|
||||
const conn: { tone: StatusTone; text: string } = loaded
|
||||
? mine
|
||||
? { tone: "success", text: "已连接,可操作" }
|
||||
: { tone: "success", text: "已连接,只读" }
|
||||
: { tone: "neutral", text: "连接中" };
|
||||
const conn: { tone: StatusTone; text: string } =
|
||||
rc.state.phase === "reconnecting"
|
||||
? { tone: "warning", text: "正在重新连接" }
|
||||
: rc.state.phase === "failed"
|
||||
? { tone: "destructive", text: "连接中断" }
|
||||
: loaded
|
||||
? mine
|
||||
? { tone: "success", text: "已连接,可操作" }
|
||||
: { tone: "success", text: "已连接,只读" }
|
||||
: { tone: "neutral", text: "连接中" };
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -167,14 +192,7 @@ function DesktopView({
|
||||
接管
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setLoaded(false);
|
||||
setReloadKey((k) => k + 1);
|
||||
}}
|
||||
>
|
||||
<Button variant="outline" size="sm" disabled={rc.busy} onClick={reloadDesktop}>
|
||||
<RotateCw />
|
||||
重新连接
|
||||
</Button>
|
||||
@@ -194,6 +212,8 @@ function DesktopView({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReloadNotice session={s} />
|
||||
|
||||
<p className="text-caption text-muted-foreground">
|
||||
{mine
|
||||
? "直接在画面里点击和输入。中文、日文用本机输入法输入;复制粘贴用 Ctrl+C / Ctrl+V。"
|
||||
@@ -201,6 +221,11 @@ function DesktopView({
|
||||
</p>
|
||||
|
||||
<div className="pg-slab relative min-w-0 overflow-hidden rounded-xl">
|
||||
{rc.state.phase !== "idle" ? (
|
||||
<div className="absolute inset-0 z-10 grid place-items-center bg-background/85">
|
||||
<ReconnectPanel state={rc.state} onRetry={rc.retry} />
|
||||
</div>
|
||||
) : null}
|
||||
<iframe
|
||||
key={reloadKey}
|
||||
title="浏览器桌面"
|
||||
|
||||
Reference in new issue
Block a user