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:
1 parent
b4c9b1ead2
commit
40dd0e69e4
29 files changed
+1661
-73
No files matched your search
@@ -1,11 +1,12 @@
|
||||
import type { BrowserCapabilities, BrowserCapabilityFlag, BrowserCapabilityKey } from "@umt/contracts";
|
||||
|
||||
/**
|
||||
* camofox-browser 能力声明。依据:jo-inc/camofox-browser master 的 openapi.json(info.version 1.17.0)与 README,
|
||||
* 2026-09-27 取得;用户实际部署可能是其他 fork/版本,运行时以 /openapi.json 探测结果为准(REQ §4.7.4)。
|
||||
* camofox-browser 能力声明。依据:jo-inc/camofox-browser v1.17.0(commit 389c996)的 openapi.json、README 与 server.js;
|
||||
* 用户实际部署可能是其他 fork/版本,运行时以 /openapi.json 探测结果为准(REQ §4.7.4)。
|
||||
*
|
||||
* supported = 本适配器当前能对该上游提供的能力(上游有端点但适配器尚未实现的记 false 并注明 M2);
|
||||
* verified 只能由真实实例原型验证结果置 true(配置注入),探测永远不会把它设为 true。
|
||||
* supported = 本适配器当前能对该上游提供的能力(上游缺端点的记 false 并注明);
|
||||
* verified 只来自人工真机核验:CAMOFOX_VERIFIED_RELEASES(按上游版本登记的核验记录,只在探测到的 info.version
|
||||
* 完全一致时生效)与 browser_provider.config.verified(逐项覆盖,可写 false 撤销)。探测本身永远不会把它设为 true。
|
||||
*/
|
||||
|
||||
/** 每项能力依赖的上游端点(OpenAPI paths 键,小写方法) */
|
||||
@@ -15,11 +16,22 @@ export const CAMOFOX_ENDPOINTS: Record<BrowserCapabilityKey, readonly string[]>
|
||||
navigate: ["post /tabs/{tabId}/navigate"],
|
||||
snapshot: ["get /tabs/{tabId}/snapshot"],
|
||||
screenshot: ["get /tabs/{tabId}/screenshot"],
|
||||
interact: ["post /tabs/{tabId}/click", "post /tabs/{tabId}/type", "post /tabs/{tabId}/select"],
|
||||
// 坐标点击经 evaluate(elementFromPoint)转成选择器:1.17.0 的 click 不接受 {x, y}
|
||||
interact: [
|
||||
"post /tabs/{tabId}/click",
|
||||
"post /tabs/{tabId}/type",
|
||||
"post /tabs/{tabId}/select",
|
||||
"post /tabs/{tabId}/evaluate",
|
||||
],
|
||||
extract: ["post /tabs/{tabId}/extract"],
|
||||
downloadCapture: ["get /tabs/{tabId}/downloads"],
|
||||
// 原站视图接管 = 同一会话的定时截图 + 点击/输入映射(不另起浏览器);VNC 插件未接入
|
||||
takeover: ["get /tabs/{tabId}/screenshot", "post /tabs/{tabId}/click", "post /tabs/{tabId}/type"],
|
||||
takeover: [
|
||||
"get /tabs/{tabId}/screenshot",
|
||||
"post /tabs/{tabId}/click",
|
||||
"post /tabs/{tabId}/type",
|
||||
"post /tabs/{tabId}/evaluate",
|
||||
],
|
||||
// 持久化插件默认开启,无需端点;storage_state 端点属插件,不在 OpenAPI 中
|
||||
persistentProfile: [],
|
||||
};
|
||||
@@ -40,15 +52,47 @@ const ADAPTER_IMPLEMENTED: ReadonlySet<BrowserCapabilityKey> = new Set([
|
||||
|
||||
const STATIC_NOTES: Partial<Record<BrowserCapabilityKey, string>> = {
|
||||
sessions: "上游无显式建会话端点:userId 即会话(BrowserContext),首个 POST /tabs 隐式创建;会话以数据库为准",
|
||||
tabs: "新建/关闭/对账(GET /tabs?userId,上游回收的标 lost)",
|
||||
snapshot: "可访问性树文本 + eN 元素引用,支持 offset 分页",
|
||||
interact: "click/type/select + scroll/press(后两者不在 1.17.0 OpenAPI,待核验);坐标点击待核验",
|
||||
tabs: "新建/关闭/对账(GET /tabs?userId);重启或空闲回收后旧标签页 404/410,对账标 lost 后在同一会话开新标签页",
|
||||
snapshot: "ARIA 快照文本 + [eN] 元素引用,支持 offset 分页;密码框值经 evaluate 取值后遮蔽",
|
||||
interact: "click/type/select/scroll/press/后退/前进/刷新;坐标点击经 evaluate 定位元素,焦点输入用 mode=keyboard",
|
||||
extract: "POST /extract 需先 snapshot 建 ref 表(x-ref)",
|
||||
downloadCapture: "GET /downloads?includeData&consume&maxBytes(README;字段形状待核验)",
|
||||
takeover: "原站视图:同一会话定时截图 + 输入映射;坐标点击与无目标输入待核验,VNC 未接入",
|
||||
persistentProfile: "上游持久化插件默认保存 cookies+localStorage(~/.camofox/profiles),重启恢复待验证",
|
||||
downloadCapture: "GET /downloads?includeData&consume&maxBytes → dataBase64;点击下载链接后捕获",
|
||||
takeover: "原站视图:同一会话同一标签页的截图 + 坐标点击/焦点输入(API 层核验);VNC 插件未接入",
|
||||
persistentProfile:
|
||||
"cookies+localStorage 按 profile 落盘:会话空闲回收、关闭与 SIGTERM 停止时写入;SIGKILL/OOM 丢上次落盘后的新登录",
|
||||
};
|
||||
|
||||
/**
|
||||
* 真机核验记录(docs/runbook/browser.md「真机核验」)。只登记核验通过的能力;新版本必须重新核验后再加一行。
|
||||
*/
|
||||
export const CAMOFOX_VERIFIED_RELEASES: Readonly<
|
||||
Record<string, { verifiedAt: string; source: string; keys: readonly BrowserCapabilityKey[] }>
|
||||
> = {
|
||||
"1.17.0": {
|
||||
verifiedAt: "2026-09-28",
|
||||
source: "jo-inc/camofox-browser v1.17.0 (389c996),docker/camofox/Dockerfile 构建",
|
||||
keys: [
|
||||
"sessions",
|
||||
"tabs",
|
||||
"navigate",
|
||||
"snapshot",
|
||||
"screenshot",
|
||||
"interact",
|
||||
"extract",
|
||||
"downloadCapture",
|
||||
"takeover",
|
||||
"persistentProfile",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/** 某上游版本的已核验能力 + 配置覆盖(配置里显式 false 可撤销) */
|
||||
export function verifiedFor(version: string | null, config: VerifiedMap = {}): VerifiedMap {
|
||||
const rec = version ? CAMOFOX_VERIFIED_RELEASES[version] : undefined;
|
||||
const base: VerifiedMap = Object.fromEntries((rec?.keys ?? []).map((k) => [k, true]));
|
||||
return { ...base, ...config };
|
||||
}
|
||||
|
||||
export type VerifiedMap = Partial<Record<BrowserCapabilityKey, boolean>>;
|
||||
|
||||
const KEYS = Object.keys(CAMOFOX_ENDPOINTS) as BrowserCapabilityKey[];
|
||||
@@ -84,16 +128,18 @@ export function camofoxCapabilitiesFromOpenApi(
|
||||
// 路径参数名不同的 fork 也视为同一端点
|
||||
const norm = (s: string) => s.replace(/\{[^}]+\}/g, "{}");
|
||||
const haveNorm = new Set([...have].map(norm));
|
||||
const version = typeof d.info?.version === "string" ? d.info.version : null;
|
||||
const effective = verifiedFor(version, verified);
|
||||
const flags = Object.fromEntries(
|
||||
KEYS.map((k) => {
|
||||
const missing = CAMOFOX_ENDPOINTS[k].filter((e) => !haveNorm.has(norm(e)));
|
||||
if (missing.length > 0) return [k, flag(k, false, "probed", verified, `上游缺少端点:${missing.join(", ")}`)];
|
||||
return [k, flag(k, ADAPTER_IMPLEMENTED.has(k), CAMOFOX_ENDPOINTS[k].length > 0 ? "probed" : "static", verified)];
|
||||
if (missing.length > 0) return [k, flag(k, false, "probed", effective, `上游缺少端点:${missing.join(", ")}`)];
|
||||
return [k, flag(k, ADAPTER_IMPLEMENTED.has(k), CAMOFOX_ENDPOINTS[k].length > 0 ? "probed" : "static", effective)];
|
||||
}),
|
||||
) as Record<BrowserCapabilityKey, BrowserCapabilityFlag>;
|
||||
return {
|
||||
...flags,
|
||||
upstreamVersion: typeof d.info?.version === "string" ? d.info.version : null,
|
||||
upstreamVersion: version,
|
||||
probedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user