Files
UHD_Media_Tool/apps/web/app/components/settings/browser-providers.tsx
T
rootandClaude Opus 5.5 8071ad33f9 feat(web): 设置页按新分类重排,加搜索、缓存分区与旧锚点跳转
- 左栏三组分区 + 当前分区的节;窄屏为搜索框、分组下拉和节的横排胶囊
- 搜索框按分区、节、节别名和字段名定位,选中字段时滚到该字段并聚焦
- 旧 hash(#automation/cache、#api-keys、#library/scrape 等)replace 到新位置
- 下载器分区放下载盘水位与保种缺省值,名字与实例抽屉统一(暂停线 / 恢复线 / 保种天数)
- 新增获取、整理与刮削、系统、缓存分区;API Key 并入账户与安全,快捷键移到界面
- 补上缺的入口:最小视频体积、子会话并发与层数、搜索站点;日志保留天数(已废弃)不再显示
- 缓存分区按区列出条数、占用、最近写入,单区清空二次确认;TMDB 与图片缓存并入
- 保存提示统一为「已保存「节名」」;按入库口径把相关文案改成「整理」

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 18:01:26 +08:00

208 lines
7.8 KiB
TypeScript

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<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="打开站点网页用(原站视图、智能体)。随部署提供的一台显示为内置。"
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: "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,
}}
/>
</SettingsBlock>
);
}