diff --git a/apps/server/src/modules/browser/service.ts b/apps/server/src/modules/browser/service.ts index 93b5122f..f8921f7d 100644 --- a/apps/server/src/modules/browser/service.ts +++ b/apps/server/src/modules/browser/service.ts @@ -529,7 +529,10 @@ export class BrowserService { throw new BrowserServiceError("precondition", "站点已停用,浏览器会话已关闭"); } const provider = this.providerFor(prow); - if (!provider.sessionInfo(row.id)) { + // api / worker 各有一份适配器内存状态:另一个进程换了当前标签页(回收后重开)时,按数据库重新载入 + const info = provider.sessionInfo(row.id); + const dbTab = (obj(row.recovery_state) as RecoveryState).currentTabId ?? null; + if (!info || (dbTab !== null && info.currentTabId !== dbTab)) { const tabs = await this.tabRows(row.id); provider.restoreSession({ session: toSession(row), diff --git a/apps/server/test/e2e/e14-vision.test.ts b/apps/server/test/e2e/e14-vision.test.ts index b159298e..9c1f84dd 100644 --- a/apps/server/test/e2e/e14-vision.test.ts +++ b/apps/server/test/e2e/e14-vision.test.ts @@ -6,7 +6,7 @@ import type { FakeScript, FakeToolResult, InvokeContext } from "@umt/agent"; import { createMockCamofox, type MockCamofox } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { browserServiceFor } from "../../src/modules/browser/index.ts"; -import type { BrowserService } from "../../src/modules/browser/service.ts"; +import { BrowserService } from "../../src/modules/browser/service.ts"; import { createE2E, type E2E, pgAvailable, runEvents, runRow, waitFor } from "./harness.ts"; // E14 视觉自动化闭环(VIS-1):fake 视觉模型(脚本返回固定坐标)+ testkit mock camofox(视口 1600×1000、DPR 1, @@ -353,4 +353,20 @@ describe.skipIf(!hasPg)("E14 视觉自动化闭环", () => { expect(ledger.length).toBe(2); for (const l of ledger) expect((l.output as Any).modelImage).toBeNull(); }, 90_000); + + it("另一个进程(api)在标签页被回收后重开,本进程(worker)按数据库换到新标签页", async () => { + await open(ids.site, ids.provider, `${ORIGIN}/torrents.php`); + const worker = new BrowserService(e.booted.ctx, svc.opts); + const auto = { kind: "automation", holder: "agent:e14" } as const; + const snap = (s: BrowserService) => s.run(sid, auto, (p, ref) => p.snapshot(ref), { write: false, persist: false }); + await snap(worker); + const old = (await svc.view(sid)).currentTabId!; + mock.dropTab(old); + await expect(snap(svc)).rejects.toThrow(/回收/); + await svc.run(sid, auto, (p, _ref, x) => + p.openTab(sid, { url: `${ORIGIN}/torrents.php`, budget: x.budget, as: "automation" }), + ); + expect((await svc.view(sid)).currentTabId).not.toBe(old); + await expect(snap(worker)).resolves.toMatchObject({ url: expect.stringContaining("/torrents.php") }); + }, 60_000); });