import { ArrowRight } from "lucide-react"; import { useState } from "react"; import { Link } from "react-router"; import { useEnrichStats } from "~/api/identity.ts"; import { useSettings } from "~/api/settings.ts"; import { useSites } from "~/api/sites.ts"; import { DataTable } from "~/components/console/data-table.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { ENRICH_SOURCE_KIND } from "~/components/library/labels.ts"; import { SearchDefaultDialog } from "~/components/sites/search-default-dialog.tsx"; import { Button } from "~/components/ui/button.tsx"; import { num } from "./resource-section.tsx"; import { SettingsBlock } from "./settings-block.tsx"; import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx"; // 获取分区:每日下载预算(budget)、搜索站点(search,编辑沿用站点页的对话框)、资源识别(identity)。 // 订阅、规则、站点本身在各自页面;这里只放它们共用的缺省值。 const GiB = 1024 ** 3; export const BUDGET_CARD: CardSpec = { anchor: "budget", title: "每日预算", description: "任一项到上限后不再开始新的下载;前两项按自然日计。", fields: [ { name: "budget.maxNewTitlesPerDay", label: "每日新作品数", kind: "number" }, { name: "budget.maxDownloadGiBPerDay", label: "每日下载量(GiB)", kind: "number" }, { name: "budget.maxConcurrentDownloads", label: "最大并发下载", kind: "number", hint: "1–100" }, ], fromSettings: (s) => ({ "budget.maxNewTitlesPerDay": n(s.budget.maxNewTitlesPerDay), "budget.maxDownloadGiBPerDay": n(Math.round((s.budget.maxDownloadBytesPerDay / GiB) * 100) / 100), "budget.maxConcurrentDownloads": n(s.budget.maxConcurrentDownloads), }), toPatch: (v) => { const gib = num(v["budget.maxDownloadGiBPerDay"]); return { budget: { maxNewTitlesPerDay: num(v["budget.maxNewTitlesPerDay"]), maxDownloadBytesPerDay: gib === undefined ? undefined : Math.round(gib * GiB), maxConcurrentDownloads: num(v["budget.maxConcurrentDownloads"]), }, }; }, }; const SEARCH_MODE_LABEL = { default: "默认站点", all: "全部站点", custom: "指定站点" } as const; /** 搜索站点(settings.search):显示当前范围,修改用站点页同一个对话框 */ function SearchSitesBlock() { const settings = useSettings(); const sites = useSites(); const [open, setOpen] = useState(false); const cfg = settings.data?.search; const mode = cfg?.defaultSiteMode ?? "default"; return ( setOpen(true)}> 修改 } >

{SEARCH_MODE_LABEL[mode]} {mode === "custom" ? ( {cfg?.siteIds.length ?? 0} 个站点 ) : null}

{sites.data ? : null}
); } // 身份识别(DEC-1 惰性识别,对照 MP:RSS / 列表资源只在匹配订阅或用户搜索时识别) export const IDENTITY_CARD: CardSpec = { anchor: "identity", title: "资源识别", description: "默认只在资源命中订阅、作品别名或新作品规则,或你手动识别和搜索时查 TMDB;其余资源留在未识别。", fields: [ { name: "identity.eagerTmdbLookup", label: "每条新资源都查 TMDB", kind: "switch", hint: "RSS 量大时每天可能上千次请求;站点独有的短剧和综艺多半查不到", wide: true, }, { name: "identity.detailIdsPerSiteHour", label: "识别时每站每小时最多抓详情页", kind: "number", hint: "标题认不准时,抓站点详情页取 IMDb、TMDB 或豆瓣 ID 再识别;只在搜索、命中订阅、补全和手动重新识别时抓,RSS 收进来的资源不抓。0 = 不抓", }, { name: "identity.autoEnrich", label: "TMDB 查不到时自动补全元数据", kind: "switch", hint: "资源命中订阅或新作品规则、TMDB 却查不到时,搜索网页出一份补全建议(每条资源一次,不建作品)。每次最多读 3 个网页,调用一次模型服务", wide: true, }, { name: "identity.enrichDailyBudget", label: "元数据补全每日上限", kind: "number", hint: "手动和自动合计;7 天内同一标题直接用上次结果,不计次。0 = 停用", }, { name: "identity.enrichDomains", label: "补全可读取的网站", kind: "textarea", hint: "每行一个域名,含子域名;只放平台和百科类网站。豆瓣只用搜索摘要,不读页面;robots.txt 不允许的页面不读。已登记站点的域名一律不读", wide: true, }, { name: "identity.enrichSnippetAllSites", label: "也用其他网站的搜索摘要", kind: "switch", hint: "短剧多半只有聚合站收录。打开后,白名单以外网站的搜索摘要也用来抽年份、集数、主演、简介(只看搜索引擎给的摘要,不打开这些网页)", wide: true, }, ], fromSettings: (s) => ({ "identity.eagerTmdbLookup": s.identity.eagerTmdbLookup, "identity.autoEnrich": s.identity.autoEnrich, "identity.enrichDailyBudget": n(s.identity.enrichDailyBudget), "identity.enrichDomains": s.identity.enrichDomains.join("\n"), "identity.enrichSnippetAllSites": s.identity.enrichSnippetAllSites ?? true, "identity.detailIdsPerSiteHour": n(s.identity.detailIdsPerSiteHour ?? 10), }), toPatch: (v) => ({ identity: { eagerTmdbLookup: v["identity.eagerTmdbLookup"] === true, autoEnrich: v["identity.autoEnrich"] === true, enrichDailyBudget: Number(v["identity.enrichDailyBudget"]), enrichDomains: String(v["identity.enrichDomains"] ?? "") .split(/[\s,,]+/) .map((d) => d.trim().toLowerCase()) .filter(Boolean), enrichSnippetAllSites: v["identity.enrichSnippetAllSites"] === true, detailIdsPerSiteHour: Number(v["identity.detailIdsPerSiteHour"]), }, }), extra: () => , }; type StatRow = { kind: string; withResults: number; hits: number; rate: number | null }; /** 最近 30 天补全命中率(按来源类别,META-2) */ function EnrichStatsTable() { const q = useEnrichStats(30); const d = q.data; return (

{d ? `最近 ${d.days} 天元数据补全 ${d.total} 次,出了建议 ${d.suggested} 次${d.tracked < d.total ? `;有来源明细的 ${d.tracked} 次` : ""}` : "读取补全命中率…"}

{d && d.tracked > 0 ? ( rows={d.sources} rowKey={(r) => r.kind} empty={{ title: "还没有补全记录" }} columns={[ { key: "kind", header: "来源", cell: (r) => ENRICH_SOURCE_KIND[r.kind] ?? r.kind }, { key: "withResults", header: "有结果", align: "right", cell: (r) => {r.withResults}, }, { key: "hits", header: "命中", align: "right", cell: (r) => {r.hits}, }, { key: "rate", header: "命中率", align: "right", cell: (r) => ( {r.rate === null ? "—" : `${Math.round(r.rate * 100)}%`} ), }, ]} /> ) : null}
); } export function AcquireSection() { return (
{(s) => } {(s) => }
); }