feat(browser): W4-C BrowserProvider contract and camofox-browser adapter skeleton
- contracts/browser.ts: provider kind, per-capability {supported, verified} flags,
session/tab/control-lease rows, NavigationBudget, redacted BrowserSnapshot,
DownloadArtifact with blob/secret refs, BrowserErrorKind
- adapters/browser: BrowserProvider interface, camofox client (health, capabilities
static + /openapi.json probe, openSession/navigate/snapshot/screenshot; rest
not_implemented for M2), none provider, redactSnapshot, profile serial lock,
domain allowlist budget; Bearer access key via BrowserSecretResolver
- testkit/browser: mock camofox server (auth, fixtures with secrets, faults)
- db 0023: browser_provider/session/tab/control_lease with one-active-lease and
one-live-session-per-profile partial unique indexes; types regenerated
- docs/design/browser-provider.md: contract, camofox OpenAPI 1.17.0 mapping,
unverified items and M2 prototype checklist; Playwright is not a product browser
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
269a2269a1
commit
effaa32b70
23 files changed
+2290
No files matched your search
@@ -0,0 +1,95 @@
|
||||
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)。
|
||||
*
|
||||
* supported = 本适配器当前能对该上游提供的能力(上游有端点但适配器尚未实现的记 false 并注明 M2);
|
||||
* verified 只能由真实实例原型验证结果置 true(配置注入),探测永远不会把它设为 true。
|
||||
*/
|
||||
|
||||
/** 每项能力依赖的上游端点(OpenAPI paths 键,小写方法) */
|
||||
export const CAMOFOX_ENDPOINTS: Record<BrowserCapabilityKey, readonly string[]> = {
|
||||
sessions: ["post /tabs", "delete /sessions/{userId}"],
|
||||
tabs: ["post /tabs", "get /tabs", "delete /tabs/{tabId}"],
|
||||
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"],
|
||||
extract: ["post /tabs/{tabId}/extract"],
|
||||
downloadCapture: ["get /tabs/{tabId}/downloads"],
|
||||
// 上游 OpenAPI 无接管端点;VNC 插件(ENABLE_VNC,noVNC 独立端口)是整块虚拟显示,需原型验证
|
||||
takeover: [],
|
||||
// 持久化插件默认开启,无需端点;storage_state 端点属插件,不在 OpenAPI 中
|
||||
persistentProfile: [],
|
||||
};
|
||||
|
||||
/** 适配器已实现的能力;其余上游虽有端点,适配器在 M2 实现 */
|
||||
const ADAPTER_IMPLEMENTED: ReadonlySet<BrowserCapabilityKey> = new Set([
|
||||
"sessions",
|
||||
"tabs",
|
||||
"navigate",
|
||||
"snapshot",
|
||||
"screenshot",
|
||||
"persistentProfile",
|
||||
]);
|
||||
|
||||
const STATIC_NOTES: Partial<Record<BrowserCapabilityKey, string>> = {
|
||||
sessions: "上游无显式建会话端点:userId 即会话(BrowserContext),首个 POST /tabs 隐式创建;关闭会话适配器 M2",
|
||||
tabs: "仅建首个标签页;列举/关闭适配器 M2",
|
||||
snapshot: "可访问性树文本 + eN 元素引用,支持 offset 分页",
|
||||
interact: "上游有 click/type/select;适配器 M2",
|
||||
extract: "上游 POST /extract 需先 snapshot 建 ref 表(x-ref);适配器 M2",
|
||||
downloadCapture: "上游 GET /downloads 支持 includeData/consume/maxBytes;适配器 M2",
|
||||
takeover: "上游无接管 API;VNC 插件画面/输入与同会话连续操作待原型验证",
|
||||
persistentProfile: "上游持久化插件默认保存 cookies+localStorage(~/.camofox/profiles),重启恢复待验证",
|
||||
};
|
||||
|
||||
export type VerifiedMap = Partial<Record<BrowserCapabilityKey, boolean>>;
|
||||
|
||||
const KEYS = Object.keys(CAMOFOX_ENDPOINTS) as BrowserCapabilityKey[];
|
||||
|
||||
function flag(
|
||||
key: BrowserCapabilityKey,
|
||||
supported: boolean,
|
||||
source: "static" | "probed",
|
||||
verified: VerifiedMap,
|
||||
note?: string,
|
||||
): BrowserCapabilityFlag {
|
||||
const n = note ?? STATIC_NOTES[key];
|
||||
return { supported, verified: supported && verified[key] === true, source, ...(n ? { note: n } : {}) };
|
||||
}
|
||||
|
||||
export function camofoxStaticCapabilities(verified: VerifiedMap = {}): BrowserCapabilities {
|
||||
const flags = Object.fromEntries(
|
||||
KEYS.map((k) => [k, flag(k, ADAPTER_IMPLEMENTED.has(k), "static", verified)]),
|
||||
) as Record<BrowserCapabilityKey, BrowserCapabilityFlag>;
|
||||
return { ...flags, upstreamVersion: null, probedAt: null };
|
||||
}
|
||||
|
||||
/** 从上游 openapi.json 文档推导能力:缺端点的能力降为 false 并注明 */
|
||||
export function camofoxCapabilitiesFromOpenApi(
|
||||
doc: unknown,
|
||||
verified: VerifiedMap = {},
|
||||
now = new Date(),
|
||||
): BrowserCapabilities {
|
||||
const d = (doc ?? {}) as { info?: { version?: unknown }; paths?: Record<string, Record<string, unknown>> };
|
||||
const have = new Set<string>();
|
||||
for (const [p, ops] of Object.entries(d.paths ?? {}))
|
||||
for (const m of Object.keys(ops ?? {})) have.add(`${m.toLowerCase()} ${p}`);
|
||||
// 路径参数名不同的 fork 也视为同一端点
|
||||
const norm = (s: string) => s.replace(/\{[^}]+\}/g, "{}");
|
||||
const haveNorm = new Set([...have].map(norm));
|
||||
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)];
|
||||
}),
|
||||
) as Record<BrowserCapabilityKey, BrowserCapabilityFlag>;
|
||||
return {
|
||||
...flags,
|
||||
upstreamVersion: typeof d.info?.version === "string" ? d.info.version : null,
|
||||
probedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user