fix(sites): normalize HHClub RSS items, complete detail page parsing (HH-2)

- RSS: split NexusPHP "[category]name[subtitle][size][uploader]" titles in the
  site adapter layer (category id/name/media type, clean title, subtitle,
  size); snapshot fields win on merge; lazily renormalize old RSS rows and
  re-emit release.observed; unknown list fields show an em dash
- Detail: HHClub section layout (douban info block as description,
  screenshots section, IMDb/TMDB from MediaInfo lines, upload time field,
  on-demand viewfilelist.php once); site CDN screenshots get a Referer
- Site request count now includes site-bound RSS fetches
- Origin view shows "自动化" when idle and "Agent" only after recent
  automation actions (lastAutomationAt); agent step titles no longer get
  squeezed to a sliver by long notes

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 17:30:25 +08:00
1 parent 4a212ed19e
commit cabd772243
32 files changed
+816 -65

No files matched your search

@@ -217,7 +217,7 @@ export function ReleaseDetailSheet({
{ label: "促销截止", value: formatDateTime(detail.data.volatile.promotionExpiresAt) },
{
label: "做种 / 下载",
value: `${r.seeders ?? "?"} / ${r.leechers ?? "?"}(${relTime(detail.data.volatile.peersObservedAt)}观察)`,
value: `${r.seeders ?? "—"} / ${r.leechers ?? "—"}(${relTime(detail.data.volatile.peersObservedAt)}观察)`,
},
{ label: "详情抓取于", value: formatDateTime(detail.data.volatile.detailFetchedAt) },
]}
@@ -16,9 +16,23 @@ export function hrOf(r: Pick<SourceRelease, "raw">): "yes" | "no" | "unknown" {
return hr === true ? "yes" : hr === false ? "no" : "unknown";
}
/** 站点 RSS 规范化后的字段(raw.rss):分类名、媒体大类 */
function rssExtra(r: Pick<SourceRelease, "raw">): { categoryName?: unknown } {
const s = (r.raw as { rss?: { categoryName?: unknown } }).rss;
return s && typeof s === "object" ? s : {};
}
/** 分类名:列表快照的站点分类名 → RSS 标题前缀里的分类名 → 站点分类 ID */
export function categoryLabel(r: Pick<SourceRelease, "raw" | "categoryRaw">): string | null {
const name = siteExtra(r).categoryName;
return typeof name === "string" && name ? name : r.categoryRaw;
if (typeof name === "string" && name) return name;
const rss = rssExtra(r).categoryName;
return typeof rss === "string" && rss ? rss : r.categoryRaw;
}
/** 列表里的数字:未知显示「—」(未知 ≠ 0) */
export function countText(n: number | null | undefined): string {
return typeof n === "number" && Number.isFinite(n) ? String(n) : "—";
}
const PROMO_LABEL: Record<string, string> = {
@@ -6,6 +6,7 @@ import { cn, formatBytes } from "~/lib/utils.ts";
import {
absTime,
categoryLabel,
countText,
hrOf,
ORIGIN_LABEL,
promotionLabel,
@@ -15,8 +16,11 @@ import {
snatchedOf,
} from "./release-fields.ts";
/** 促销 / H&R 徽章(列表与详情共用):促销带剩余时间,过期的写明 */
export function ReleaseBadges({ r }: { r: WorkbenchRelease }) {
/**
* 促销 / H&R 徽章(列表与详情共用):促销带剩余时间,过期的写明。
* H&R 未知只在 showUnknownHr 时标出(详情里);列表行不为未知字段挂徽章,避免 RSS 条目满屏「未知」。
*/
export function ReleaseBadges({ r, showUnknownHr = false }: { r: WorkbenchRelease; showUnknownHr?: boolean }) {
const promo = promotionLabel(r);
const hr = hrOf(r);
const left = promo && !promo.expired ? remainingLabel(r.promotionExpiresAt) : null;
@@ -31,7 +35,7 @@ export function ReleaseBadges({ r }: { r: WorkbenchRelease }) {
</span>
) : null}
{hr === "yes" ? <StatusBadge tone="warning">H&R</StatusBadge> : null}
{hr === "unknown" ? <StatusBadge>H&R 未知</StatusBadge> : null}
{hr === "unknown" && showUnknownHr ? <StatusBadge>H&R 未知</StatusBadge> : null}
</>
);
}
@@ -159,19 +163,17 @@ export function ReleaseRow({
{r.subtitle ?? ""}
</span>
<span className="flex flex-wrap items-center gap-x-3 gap-y-1 tabular-nums sm:shrink-0 sm:flex-nowrap sm:gap-x-4">
<span className="sm:w-14 sm:truncate" title={r.categoryRaw ? `站点分类 ${r.categoryRaw}` : undefined}>
{cat ?? "分类未知"}
</span>
<span className="sm:w-16 sm:text-right">
{r.sizeBytes === null ? "大小未知" : formatBytes(r.sizeBytes, 1)}
<span className="sm:w-14 sm:truncate" title={r.categoryRaw ? `站点分类 ${r.categoryRaw}` : "分类未知"}>
{cat ?? "—"}
</span>
<span className="sm:w-16 sm:text-right">{r.sizeBytes === null ? "—" : formatBytes(r.sizeBytes, 1)}</span>
<span className="inline-flex items-center gap-0.5 sm:w-32" title="做种 / 下载 / 完成">
<ArrowUp className="h-3 w-3" aria-label="做种" />
<span className={cn(r.seeders === 0 && "text-warning")}>{r.seeders ?? "?"}</span>
<span className={cn(r.seeders === 0 && "text-warning")}>{countText(r.seeders)}</span>
<ArrowDown className="ml-1.5 h-3 w-3" aria-label="下载" />
{r.leechers ?? "?"}
{countText(r.leechers)}
<Check className="ml-1.5 h-3 w-3" aria-label="完成" />
{snatched ?? "?"}
{countText(snatched)}
</span>
<span className="sm:w-20 sm:text-right" title={timeTitle}>
{relTime(r.sourcePublishedAt)}