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
@@ -0,0 +1,200 @@
|
||||
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 { 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<BrowserCapabilityKey, string> = {
|
||||
sessions: "会话",
|
||||
tabs: "标签页",
|
||||
navigate: "导航",
|
||||
snapshot: "页面快照",
|
||||
screenshot: "截图",
|
||||
interact: "点击与输入",
|
||||
extract: "字段提取",
|
||||
downloadCapture: "下载捕获",
|
||||
takeover: "人工接管",
|
||||
persistentProfile: "登录保持",
|
||||
};
|
||||
|
||||
function Verified({ p }: { p: BrowserProviderView }) {
|
||||
const caps = p.capabilities;
|
||||
if (!caps) return <StatusBadge>未探测</StatusBadge>;
|
||||
const keys = BROWSER_CAPABILITY_KEYS.filter((k) => caps[k].supported);
|
||||
const ok = keys.filter((k) => caps[k].verified).length;
|
||||
if (ok === 0) return <StatusBadge tone="warning">未核验</StatusBadge>;
|
||||
return (
|
||||
<StatusBadge tone={ok === keys.length ? "success" : "warning"}>
|
||||
{ok === keys.length ? "已核验" : `已核验 ${ok}/${keys.length}`}
|
||||
{caps.upstreamVersion ? ` · ${caps.upstreamVersion}` : ""}
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
|
||||
function CapabilityTable({ p }: { p: BrowserProviderView }) {
|
||||
const caps = p.capabilities;
|
||||
if (!caps) return <p className="mt-4 text-caption text-muted-foreground">还没有探测过。点"测试连通"读取能力。</p>;
|
||||
return (
|
||||
<div className="mt-5 space-y-2">
|
||||
<h3 className="text-caption font-semibold">
|
||||
能力
|
||||
<span className="ml-2 font-normal text-muted-foreground">
|
||||
上游版本 {caps.upstreamVersion ?? "未知"}
|
||||
{p.verifiedAt ? ` · 真机核验 ${p.verifiedAt.slice(0, 10)}` : ""}
|
||||
</span>
|
||||
</h3>
|
||||
<ul className="divide-y rounded-lg border">
|
||||
{BROWSER_CAPABILITY_KEYS.map((k) => {
|
||||
const f = caps[k];
|
||||
return (
|
||||
<li key={k} className="flex items-start justify-between gap-3 px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-caption font-medium">{CAP_LABEL[k]}</p>
|
||||
{f.note ? <p className="text-meta text-muted-foreground">{f.note}</p> : null}
|
||||
</div>
|
||||
{!f.supported ? (
|
||||
<StatusBadge tone="destructive">不可用</StatusBadge>
|
||||
) : f.verified ? (
|
||||
<StatusBadge tone="success">已核验</StatusBadge>
|
||||
) : (
|
||||
<StatusBadge tone="warning">未核验</StatusBadge>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<p className="text-meta text-muted-foreground">
|
||||
"已核验"只来自对真实实例的逐项核验记录(见运行手册),探测只能判断接口在不在。未核验的能力在原站视图里标出。
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BrowserProvidersBlock() {
|
||||
const list = useBrowserProviders();
|
||||
const create = useCreateBrowserProvider();
|
||||
const update = useUpdateBrowserProvider();
|
||||
const remove = useDeleteBrowserProvider();
|
||||
const probe = useProbeBrowserProvider();
|
||||
return (
|
||||
<SettingsBlock
|
||||
section="sites"
|
||||
anchor="browser"
|
||||
title="浏览器服务"
|
||||
description="原站视图和 Agent 打开站点网页用的 camofox-browser。部署自带的一台由环境变量登记(camofox-env)。"
|
||||
plain
|
||||
>
|
||||
<ResourceSection<BrowserProviderView>
|
||||
hooks={{ list, create, update, remove } as never}
|
||||
config={{
|
||||
noun: "浏览器服务",
|
||||
emptyHint: "没有浏览器服务时,原站视图不可用。",
|
||||
deleteHint: "有未关闭会话时不能删除。环境变量登记的只能停用。",
|
||||
drawerExtra: (r) => <CapabilityTable p={r} />,
|
||||
columns: [
|
||||
{
|
||||
key: "name",
|
||||
header: "名称",
|
||||
cell: (r) => (
|
||||
<div className="min-w-0">
|
||||
<p className="flex items-center gap-2 font-medium">
|
||||
{r.name}
|
||||
{r.managedBy === "env" ? <StatusBadge>环境变量</StatusBadge> : null}
|
||||
</p>
|
||||
<p className="truncate text-meta text-muted-foreground">{r.baseUrl}</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "key",
|
||||
header: "访问凭据",
|
||||
hideBelow: "md",
|
||||
cell: (r) => (r.hasAccessKey ? "已设置" : "未设置"),
|
||||
},
|
||||
{
|
||||
key: "enabled",
|
||||
header: "状态",
|
||||
hideBelow: "sm",
|
||||
cell: (r) => (
|
||||
<StatusBadge tone={r.enabled ? "success" : "neutral"}>{r.enabled ? "启用" : "停用"}</StatusBadge>
|
||||
),
|
||||
},
|
||||
{ key: "health", header: "健康", cell: (r) => <Health status={r.healthStatus} at={r.lastHealthAt} /> },
|
||||
{ key: "verified", header: "核验", hideBelow: "lg", cell: (r) => <Verified p={r} /> },
|
||||
],
|
||||
rowActions: (r) => (
|
||||
<TestButton
|
||||
label="测试连通"
|
||||
pending={probe.isPending}
|
||||
run={async () => {
|
||||
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: "password",
|
||||
hint: "camofox 的 CAMOFOX_ACCESS_KEY;没设就留空",
|
||||
editHint: "留空不修改",
|
||||
},
|
||||
{ name: "enabled", label: "启用", kind: "switch" },
|
||||
],
|
||||
initial: { name: "", baseUrl: "", accessKey: "", enabled: true },
|
||||
fromRow: (r) => ({ name: r.name, baseUrl: r.baseUrl ?? "", accessKey: "", 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: optStr(v.accessKey), enabled: v.enabled === true },
|
||||
createSchema: BrowserProviderCreate,
|
||||
updateSchema: BrowserProviderUpdate,
|
||||
}}
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
}
|
||||
@@ -2,10 +2,11 @@ import { ArrowRight } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { BrowserProvidersBlock } from "./browser-providers.tsx";
|
||||
import { num, str } from "./resource-section.tsx";
|
||||
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
||||
|
||||
// 站点默认(site-center §8):站点没单独设置时用的限流、快照周期、UA、代理。
|
||||
// 站点默认(site-center §8):站点没单独设置时用的限流、快照周期、UA、代理;浏览器服务(BR-1)。
|
||||
// 站点、凭证、账号在「站点」页管理;站点级覆盖在站点的"编辑"里。
|
||||
|
||||
const DEFAULTS_CARD: CardSpec = {
|
||||
@@ -91,6 +92,7 @@ export function SitesSection() {
|
||||
</Button>
|
||||
</div>
|
||||
<SettingsGate>{(s) => <SettingsCard section="sites" spec={DEFAULTS_CARD} settings={s} />}</SettingsGate>
|
||||
<BrowserProvidersBlock />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user