feat(browser): ship camofox v1.17.0 in compose, verified on a real instance

- compose: camofox service on by default (internal only, volume umt-camofox,
  healthcheck, CAMOFOX_REPLICAS=0 to skip); app depends on it with
  service_started/required=false and auto-registers camofox-env from
  BROWSER_PROVIDER_URL / BROWSER_ACCESS_KEY / BROWSER_ENABLED
- docker/camofox/Dockerfile: builds upstream tag v1.17.0 (389c996); fixes the
  upstream Dockerfile (missing postinstall.js before npm ci, sh -c not
  forwarding SIGTERM so logins were never persisted on restart)
- adapter fixes from the real container: coordinate click via evaluate +
  selector, focus typing with mode=keyboard, password values redacted from
  ARIA snapshots/extract, downloads field names, 410 after browser restart
  treated as lost tab; navigate reopens a tab in the same session
- verified-capability table keyed by upstream version (1.17.0 all ten);
  other versions stay unverified; probe reports auth failures
- settings: browser providers block (create/edit/delete/probe, capability
  table); API POST/PATCH/DELETE /browser/providers, POST .../probe
- scripts/browser-verify: umtbr stack + mock NexusPHP run of REQ 4.7.4 items
- docs: runbook browser §1/§7 (real-instance record), deploy §2.1

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 00:05:21 +08:00
1 parent b4c9b1ead2
commit 40dd0e69e4
29 files changed
+1661 -73

No files matched your search

+24 -2
View File
@@ -10,7 +10,10 @@ import { afterEach, describe, expect, it } from "vitest";
import {
BrowserError,
type BrowserLogger,
CAMOFOX_ENDPOINTS,
CAMOFOX_VERIFIED_RELEASES,
type CamofoxConfig,
camofoxCapabilitiesFromOpenApi,
camofoxStaticCapabilities,
classifyHttpStatus,
createCamofoxBrowserProvider,
@@ -89,7 +92,7 @@ describe("能力声明", () => {
expect(c.navigate).toMatchObject({ supported: true, verified: false, source: "static" });
expect(c.snapshot).toMatchObject({ supported: true, verified: true });
expect(c.interact).toMatchObject({ supported: true, verified: true });
expect(c.interact.note).toContain("待核验");
expect(c.interact.note).toContain("evaluate");
expect(c.takeover).toMatchObject({ supported: true, verified: false });
expect(c.upstreamVersion).toBeNull();
});
@@ -97,13 +100,31 @@ describe("能力声明", () => {
it("探测 /openapi.json:读取上游版本,缺端点的能力降级", async () => {
const { provider } = await setup({ omitPaths: ["get /tabs/{tabId}/snapshot"] });
const c = await provider.capabilities({ probe: true });
expect(c.upstreamVersion).toBe("1.17.0");
expect(c.upstreamVersion).toBe("1.17.0-mock");
expect(c.probedAt).not.toBeNull();
expect(c.navigate).toMatchObject({ supported: true, source: "probed", verified: false });
expect(c.snapshot).toMatchObject({ supported: false, source: "probed" });
expect(c.snapshot.note).toContain("/tabs/{tabId}/snapshot");
});
it("真机核验表:探测到已核验的上游版本才置 verified;配置可逐项撤销;其它版本不继承", () => {
const paths: Record<string, Record<string, unknown>> = {};
for (const e of Object.values(CAMOFOX_ENDPOINTS).flat()) {
const [m, p] = e.split(" ");
paths[p!] = { ...paths[p!], [m!]: {} };
}
const doc = (version: string) => ({ info: { version }, paths });
const ok = camofoxCapabilitiesFromOpenApi(doc("1.17.0"));
expect(Object.keys(CAMOFOX_VERIFIED_RELEASES)).toContain("1.17.0");
for (const k of CAMOFOX_VERIFIED_RELEASES["1.17.0"]!.keys)
expect(ok[k]).toMatchObject({ supported: true, verified: true });
expect(camofoxCapabilitiesFromOpenApi(doc("1.17.0"), { takeover: false }).takeover.verified).toBe(false);
const other = camofoxCapabilitiesFromOpenApi(doc("1.18.0"));
expect(other.snapshot.verified).toBe(false);
// 静态声明不知道上游版本:一律未核验
expect(camofoxStaticCapabilities().snapshot.verified).toBe(false);
});
it("none 实现:能力全关,动作报 unavailable", async () => {
const none = createNoneBrowserProvider();
const c = await none.capabilities();
@@ -300,6 +321,7 @@ describe("错误分类", () => {
expect(classifyHttpStatus(401)).toBe("auth");
expect(classifyHttpStatus(403)).toBe("auth");
expect(classifyHttpStatus(404)).toBe("not_found");
expect(classifyHttpStatus(410)).toBe("not_found");
expect(classifyHttpStatus(409)).toBe("bad_request");
expect(classifyHttpStatus(429)).toBe("rate_limited");
expect(classifyHttpStatus(500)).toBe("upstream");