import { BROWSER_CAPABILITY_KEYS, type BrowserCapabilityKey, BrowserProviderCreate, BrowserProviderUpdate, type BrowserProviderView, } from "@umt/contracts"; import { useBrowserProviders, useCreateBrowserProvider, useDeleteBrowserProvider, useProbeBrowserProvider, useUpdateBrowserProvider, } from "~/api/browser.ts"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { secretPatch } from "~/lib/secret.ts"; import { Health, TestButton } from "./instances.tsx"; import { type FormValues, optStr, ResourceSection, str } from "./resource-section.tsx"; import { SettingsBlock } from "./settings-block.tsx"; // 浏览器服务(camofox-browser):地址、访问凭据(只写,存凭证库)、启用、测试连通、能力核验状态。 // 部署自带的 camofox 由环境变量登记为 camofox-env,地址与凭据只能改部署配置。 const CAP_LABEL: Record = { sessions: "会话", tabs: "标签页", navigate: "导航", snapshot: "页面快照", screenshot: "截图", interact: "点击与输入", extract: "字段提取", downloadCapture: "下载捕获", takeover: "人工接管", persistentProfile: "登录保持", }; function Verified({ p }: { p: BrowserProviderView }) { const caps = p.capabilities; if (!caps) return 未探测; const keys = BROWSER_CAPABILITY_KEYS.filter((k) => caps[k].supported); const ok = keys.filter((k) => caps[k].verified).length; if (ok === 0) return 未核验; return ( {ok === keys.length ? "已核验" : `已核验 ${ok}/${keys.length}`} {caps.upstreamVersion ? ` · ${caps.upstreamVersion}` : ""} ); } function CapabilityTable({ p }: { p: BrowserProviderView }) { const caps = p.capabilities; if (!caps) return

还没有探测过。点"测试连通"读取能力。

; return (

能力 上游版本 {caps.upstreamVersion ?? "未知"} {p.verifiedAt ? ` · 真机核验 ${p.verifiedAt.slice(0, 10)}` : ""}

"已核验"只来自对真实实例的逐项核验记录(见运行手册),探测只能判断接口在不在。未核验的能力在原站视图里标出。

); } export function BrowserProvidersBlock() { const list = useBrowserProviders(); const create = useCreateBrowserProvider(); const update = useUpdateBrowserProvider(); const remove = useDeleteBrowserProvider(); const probe = useProbeBrowserProvider(); return ( hooks={{ list, create, update, remove } as never} config={{ noun: "浏览器服务", emptyHint: "没有浏览器服务时,原站视图不可用。", deleteHint: "有未关闭会话时不能删除。环境变量登记的只能停用。", drawerExtra: (r) => , columns: [ { key: "name", header: "名称", cell: (r) => (

{r.name} {r.managedBy === "env" ? 环境变量 : null}

{r.baseUrl}

), }, { key: "key", header: "访问凭据", hideBelow: "md", cell: (r) => (r.hasAccessKey ? "已设置" : "未设置"), }, { key: "enabled", header: "状态", hideBelow: "sm", cell: (r) => ( {r.enabled ? "启用" : "停用"} ), }, { key: "health", header: "健康", cell: (r) => }, { key: "verified", header: "核验", hideBelow: "lg", cell: (r) => }, ], rowActions: (r) => ( { const v = await probe.mutateAsync(r.id); const auth = (v.health?.detail as { error?: string } | undefined)?.error === "auth"; return { ok: v.health?.status === "ok", message: auth ? "访问凭据不对" : v.health?.status === "degraded" ? "浏览器服务正在恢复" : "连不上", version: v.capabilities?.upstreamVersion ?? null, }; }} /> ), fields: [ { name: "name", label: "名称", kind: "text", required: true, createOnly: true, placeholder: "例如:家里的 camofox", }, { name: "baseUrl", label: "服务地址", kind: "url", required: true, placeholder: "http://camofox:9377", hint: "环境变量登记的服务改部署配置里的 BROWSER_PROVIDER_URL", wide: true, }, { name: "accessKey", label: "访问凭据", kind: "secret", secret: { set: (v) => v["accessKey:set"] === true, clearable: true }, hint: "camofox 的 CAMOFOX_ACCESS_KEY;没设就留空", }, { name: "enabled", label: "启用", kind: "switch" }, ], initial: { name: "", baseUrl: "", accessKey: "", enabled: true }, fromRow: (r) => ({ name: r.name, baseUrl: r.baseUrl ?? "", accessKey: "", "accessKey:set": r.hasAccessKey, enabled: r.enabled, }), toBody: (v: FormValues, mode) => mode === "create" ? { name: str(v.name), baseUrl: str(v.baseUrl), accessKey: optStr(v.accessKey), enabled: v.enabled === true, } : { baseUrl: str(v.baseUrl), accessKey: secretPatch(v.accessKey), enabled: v.enabled === true }, createSchema: BrowserProviderCreate, updateSchema: BrowserProviderUpdate, }} />
); }