- 下载面板:下载盘只显示「已用 x / 总量」与进度条,暂停线 / 在途 / 预留 / 可清理 / 做种保留收进详情; 阈值文案统一「暂停线 / 恢复线 / 下载盘满暂停」 - RSS 页去掉与站点页重复的站点表,节标题「RSS 地址」;用途只在非通用时显示 - 订阅:一条都没有时不铺筛选;表单里「空 = 24;0 = 关」等等式改成 placeholder + 一句说明 - 设置:浏览器服务、元数据识别、MCP 说明去掉组件名与实现细节;会话目录与 fake 脚本只在开发构建显示; API Key scope 前加中文名;命名模板变量去掉「MP 无」 - 待处理顶部计数卡改成一行筛选按钮 - 智能体悬浮按钮只在没有侧栏的窄屏出现,抽屉打开时不再显示「收起」胶囊 Co-Authored-By: Claude Opus 5.5 <[email protected]>
201 lines
7.6 KiB
TypeScript
201 lines
7.6 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 { 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: "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>
|
|
);
|
|
}
|