文案(资源与 RSS):去掉候选 / Feed / 轮询等术语外用词,复核改刷新详情
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
775d1589e4
commit
4fe4679efc
6 files changed
+50
-50
No files matched your search
@@ -122,8 +122,8 @@ export function FeedFormDialog({
|
||||
const parsed = feed ? FeedSourceUpdate.safeParse(raw) : FeedSourceCreate.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
const e = zodFieldErrors(parsed.error.issues);
|
||||
if (e.pollIntervalSec) e.pollMin = "轮询间隔至少 1 分钟";
|
||||
if (e.url) e.url = feed ? "URL 格式不对" : "填写完整的 RSS 地址";
|
||||
if (e.pollIntervalSec) e.pollMin = "抓取间隔至少 1 分钟";
|
||||
if (e.url) e.url = feed ? "RSS 地址格式不对" : "填写完整的 RSS 地址";
|
||||
setErrors(e);
|
||||
return;
|
||||
}
|
||||
@@ -144,7 +144,7 @@ export function FeedFormDialog({
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{feed ? "编辑 RSS" : "新建 RSS"}</DialogTitle>
|
||||
<DialogDescription>URL 加密保存,之后只显示脱敏地址。</DialogDescription>
|
||||
<DialogDescription>地址加密保存,之后只显示脱敏后的地址。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
className="grid gap-1"
|
||||
@@ -157,7 +157,7 @@ export function FeedFormDialog({
|
||||
label="名称"
|
||||
htmlFor="feed-name"
|
||||
error={errors.name}
|
||||
hint={feed ? undefined : "可选;留空时用「站点名 RSS」或地址主机名"}
|
||||
hint={feed ? undefined : "留空时用「站点名 RSS」或地址的主机名"}
|
||||
>
|
||||
<Input
|
||||
id="feed-name"
|
||||
@@ -167,11 +167,11 @@ export function FeedFormDialog({
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="URL"
|
||||
label="RSS 地址"
|
||||
htmlFor="feed-url"
|
||||
required={!feed}
|
||||
error={errors.url}
|
||||
hint={feed ? `当前:${feed.urlDisplay};留空表示不变` : "RSS / Atom 地址"}
|
||||
hint={feed ? `当前:${feed.urlDisplay}。留空不修改` : "支持 RSS 和 Atom"}
|
||||
>
|
||||
<Input
|
||||
id="feed-url"
|
||||
@@ -198,7 +198,7 @@ export function FeedFormDialog({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormField>
|
||||
<FormField label="轮询间隔(分钟)" htmlFor="feed-poll" error={errors.pollMin}>
|
||||
<FormField label="抓取间隔(分钟)" htmlFor="feed-poll" error={errors.pollMin}>
|
||||
<Input
|
||||
id="feed-poll"
|
||||
type="number"
|
||||
@@ -257,7 +257,7 @@ export function FeedFormDialog({
|
||||
<FormField
|
||||
label="站点账号"
|
||||
htmlFor="feed-account"
|
||||
hint={d.siteId && !siteAccounts.length ? "该站点没有账号" : undefined}
|
||||
hint={d.siteId && !siteAccounts.length ? "这个站点没有账号" : undefined}
|
||||
>
|
||||
<Select
|
||||
value={d.siteAccountId || NONE}
|
||||
@@ -295,7 +295,7 @@ export function FeedFormDialog({
|
||||
/>
|
||||
</FormField>
|
||||
<div className="flex items-center justify-between gap-3 py-1 text-caption text-foreground">
|
||||
<label htmlFor="feed-enabled">启用(关闭后停止轮询)</label>
|
||||
<label htmlFor="feed-enabled">启用,关闭后停止抓取</label>
|
||||
<Switch id="feed-enabled" checked={d.enabled} onCheckedChange={(v) => set("enabled", v)} />
|
||||
</div>
|
||||
<DialogFooter className="mt-2 gap-2">
|
||||
|
||||
@@ -36,7 +36,7 @@ const lastErrorText = (f: FeedSource) =>
|
||||
/** 游标窗口摘要:服务端 cursor.window 的条目数与上次抓取时间 */
|
||||
function cursorSummary(f: FeedSource): string {
|
||||
const w = f.cursor.window as { lastFetchedAt?: string | null; lastGuidSet?: unknown[] } | undefined;
|
||||
if (!w) return "尚无游标";
|
||||
if (!w) return "还没有游标";
|
||||
const n = Array.isArray(w.lastGuidSet) ? w.lastGuidSet.length : null;
|
||||
return `${n !== null ? `窗口 ${n} 条` : "窗口"}${w.lastFetchedAt ? ` · ${relTime(w.lastFetchedAt)}` : ""}`;
|
||||
}
|
||||
@@ -62,14 +62,14 @@ function FeedActions({ f }: { f: FeedSource }) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
title="立即抓取(受站点限流)"
|
||||
title="立即抓取,按站点限流排队"
|
||||
aria-label="立即抓取"
|
||||
disabled={!f.enabled || fetchNow.isPending}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
fetchNow.mutate(f.id, {
|
||||
onSuccess: () => toast.success("抓取已排队"),
|
||||
onSuccess: () => toast.success("已加入抓取队列"),
|
||||
onError: (err) => toast.error(errorMessage(err)),
|
||||
});
|
||||
}}
|
||||
@@ -81,7 +81,7 @@ function FeedActions({ f }: { f: FeedSource }) {
|
||||
}
|
||||
|
||||
const OBS_STATUS: Record<Observation["status"], { label: string; tone: StatusTone } | null> = {
|
||||
new: { label: "待识别", tone: "neutral" },
|
||||
new: { label: "未处理", tone: "neutral" },
|
||||
linked: null,
|
||||
ignored: { label: "已忽略", tone: "neutral" },
|
||||
error: { label: "出错", tone: "warning" },
|
||||
@@ -188,7 +188,7 @@ export function FeedTable({
|
||||
},
|
||||
{
|
||||
key: "health",
|
||||
header: "健康",
|
||||
header: "状态",
|
||||
cell: (f) => {
|
||||
const gap = feedGapHint(f);
|
||||
return (
|
||||
@@ -230,19 +230,19 @@ export function FeedTable({
|
||||
]),
|
||||
{
|
||||
key: "poll",
|
||||
header: "轮询",
|
||||
header: "抓取间隔",
|
||||
hideBelow: "lg",
|
||||
cell: (f) => <span className="whitespace-nowrap">{Math.round(f.pollIntervalSec / 60)} 分钟</span>,
|
||||
},
|
||||
{
|
||||
key: "success",
|
||||
header: "最后成功",
|
||||
header: "上次成功",
|
||||
hideBelow: "sm",
|
||||
cell: (f) => (
|
||||
<div className="whitespace-nowrap">
|
||||
<p className="text-foreground">{relTime(f.lastSuccessAt)}</p>
|
||||
{f.consecutiveFailures ? (
|
||||
<p className="text-meta text-destructive">连续失败 {f.consecutiveFailures}</p>
|
||||
<p className="text-meta text-destructive">连续失败 {f.consecutiveFailures} 次</p>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
@@ -264,7 +264,7 @@ export function FeedTable({
|
||||
empty={{
|
||||
icon: Rss,
|
||||
title: filtered ? "没有符合筛选的 RSS" : "还没有 RSS",
|
||||
description: filtered ? undefined : "新建 RSS 后按轮询间隔抓取,新资源进入识别和收录评估。",
|
||||
description: filtered ? undefined : "新建 RSS 后按间隔抓取,抓到的新资源会自动识别,并按收录版本评估。",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -324,12 +324,12 @@ export function FeedTable({
|
||||
<DetailSection title="抓取状态">
|
||||
<KvList
|
||||
items={[
|
||||
{ label: "最后成功", value: formatDateTime(open.lastSuccessAt) },
|
||||
{ label: "最后抓取", value: formatDateTime(open.lastFetchAt) },
|
||||
{ label: "上次成功", value: formatDateTime(open.lastSuccessAt) },
|
||||
{ label: "上次抓取", value: formatDateTime(open.lastFetchAt) },
|
||||
{ label: "下次抓取", value: formatDateTime(open.nextFetchAt) },
|
||||
{ label: "退避至", value: formatDateTime(open.backoffUntil) },
|
||||
{ label: "连续失败", value: open.consecutiveFailures },
|
||||
{ label: "轮询间隔", value: `${Math.round(open.pollIntervalSec / 60)} 分钟` },
|
||||
{ label: "抓取间隔", value: `${Math.round(open.pollIntervalSec / 60)} 分钟` },
|
||||
{ label: "游标", value: cursorSummary(open) },
|
||||
{ label: "解析器", value: open.parserVersion, mono: true },
|
||||
{ label: "ETag", value: open.etag, mono: true },
|
||||
|
||||
@@ -21,19 +21,19 @@ import { InventoryOverlay, ReleaseBadges } from "./release-list.tsx";
|
||||
import { ExternalIdTags, externalLinks, ReleasePageSections } from "./release-page-section.tsx";
|
||||
|
||||
const DECISION_LABEL = {
|
||||
accept: { label: "符合,可收录", tone: "success" },
|
||||
accept: { label: "可下载", tone: "success" },
|
||||
upgrade: { label: "可升级", tone: "success" },
|
||||
reject: { label: "不收录", tone: "destructive" },
|
||||
unknown_field: { label: "缺字段,待复核", tone: "warning" },
|
||||
skipped: { label: "被跳过", tone: "neutral" },
|
||||
deferred: { label: "延后:已有计划在途", tone: "warning" },
|
||||
unknown_field: { label: "信息不全", tone: "warning" },
|
||||
skipped: { label: "已跳过", tone: "neutral" },
|
||||
deferred: { label: "延后:已有下载在进行", tone: "warning" },
|
||||
} as const;
|
||||
|
||||
/** 没关联作品时按识别状态说一句(原码不上屏) */
|
||||
const UNLINKED_TEXT: Record<string, string> = {
|
||||
unresolved: "还没识别作品。",
|
||||
unresolved: "未识别。",
|
||||
ambiguous: "有多个可能的作品,没有自动关联。",
|
||||
unidentifiable: "没识别出作品。",
|
||||
unidentifiable: "无法识别作品。",
|
||||
rejected: "识别结果已被排除。",
|
||||
};
|
||||
|
||||
@@ -43,9 +43,9 @@ function LocalInventory({ release, entry }: { release: WorkbenchRelease; entry:
|
||||
if (!release.entity) {
|
||||
return (
|
||||
<p className="text-caption text-muted-foreground">
|
||||
{UNLINKED_TEXT[release.resolutionStatus] ?? "未关联本地作品。"}
|
||||
{UNLINKED_TEXT[release.resolutionStatus] ?? "未识别。"}
|
||||
<Link to="/console/identity" className="ml-1 underline underline-offset-4">
|
||||
去指定作品
|
||||
指定作品
|
||||
</Link>
|
||||
</p>
|
||||
);
|
||||
@@ -87,7 +87,7 @@ function LocalInventory({ release, entry }: { release: WorkbenchRelease; entry:
|
||||
<li key={c.slotId} className="rounded-lg border border-border/70 px-3 py-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-caption">
|
||||
<StatusBadge tone={d.tone}>{d.label}</StatusBadge>
|
||||
<span className="text-foreground">{slot ? `目标 ${slot.name}` : "收录版本"}</span>
|
||||
<span className="text-foreground">{slot ? `收录版本 ${slot.name}` : "收录版本"}</span>
|
||||
<span className="text-muted-foreground">{relTime(c.evaluatedAt)}评估</span>
|
||||
</div>
|
||||
{c.reasons.length ? (
|
||||
@@ -158,7 +158,7 @@ export function ReleaseDetailSheet({
|
||||
{ siteId, rid: r.id },
|
||||
{
|
||||
onSuccess: (d) =>
|
||||
d.refreshError ? toast.warning(`复核失败,显示上次数据:${d.refreshError}`) : toast.success("已复核"),
|
||||
d.refreshError ? toast.warning(`刷新失败,显示的是上次的数据:${d.refreshError}`) : toast.success("已刷新"),
|
||||
onError: (e) => toast.error(errorMessage(e)),
|
||||
},
|
||||
);
|
||||
@@ -224,7 +224,7 @@ export function ReleaseDetailSheet({
|
||||
</span>
|
||||
<Button variant="outline" size="xs" className="ml-auto" disabled={refresh.isPending} onClick={doRefresh}>
|
||||
<RefreshCw className={refresh.isPending ? "animate-spin" : undefined} />
|
||||
复核详情
|
||||
刷新详情
|
||||
</Button>
|
||||
</div>
|
||||
{page && (externalLinks(page).length || page.tags.length || page.uploader) ? (
|
||||
@@ -240,7 +240,7 @@ export function ReleaseDetailSheet({
|
||||
) : null}
|
||||
{detail.data?.refreshError ? (
|
||||
<p className="rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
|
||||
最近一次复核失败,下面是之前的数据:{detail.data.refreshError}
|
||||
上次刷新失败,下面是之前的数据:{detail.data.refreshError}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -254,7 +254,7 @@ export function ReleaseDetailSheet({
|
||||
) : detail.data && !detail.data.detailSupported ? (
|
||||
<p className="text-caption text-muted-foreground">这个站点不支持抓详情页。</p>
|
||||
) : (
|
||||
<p className="text-caption text-muted-foreground">还没有详情页内容。点「复核详情」抓取。</p>
|
||||
<p className="text-caption text-muted-foreground">还没有详情页内容。点「刷新详情」抓取。</p>
|
||||
)}
|
||||
|
||||
{!page?.fileTree.length && r.fileList?.length ? (
|
||||
@@ -270,7 +270,7 @@ export function ReleaseDetailSheet({
|
||||
</DetailSection>
|
||||
) : null}
|
||||
|
||||
<DetailSection title="本地库存与候选理由">
|
||||
<DetailSection title="本地版本与评估">
|
||||
<LocalInventory release={r} entry={entry} />
|
||||
</DetailSection>
|
||||
|
||||
@@ -278,9 +278,9 @@ export function ReleaseDetailSheet({
|
||||
<DetailSection title="数据时效">
|
||||
<KvList
|
||||
items={[
|
||||
{ label: "促销观察于", value: formatDateTime(detail.data.volatile.promotionObservedAt) },
|
||||
{ label: "促销更新于", value: formatDateTime(detail.data.volatile.promotionObservedAt) },
|
||||
{ label: "促销截止", value: formatDateTime(detail.data.volatile.promotionExpiresAt) },
|
||||
{ label: "做种人数观察于", value: formatDateTime(detail.data.volatile.peersObservedAt) },
|
||||
{ label: "做种人数更新于", value: formatDateTime(detail.data.volatile.peersObservedAt) },
|
||||
{ label: "详情抓取于", value: formatDateTime(detail.data.volatile.detailFetchedAt) },
|
||||
]}
|
||||
/>
|
||||
@@ -294,8 +294,8 @@ export function ReleaseDetailSheet({
|
||||
{ label: "分类", value: categoryLabel(r) },
|
||||
{ label: "首次发现", value: formatDateTime(r.firstSeenAt) },
|
||||
{ label: "站点上传", value: formatDateTime(r.sourcePublishedAt) },
|
||||
{ label: "最近确认", value: `${formatDateTime(r.dataUpdatedAt)}(${ORIGIN_LABEL[r.origin]})` },
|
||||
{ label: "Feed 观察次数", value: r.observedByFeeds },
|
||||
{ label: "数据更新于", value: `${formatDateTime(r.dataUpdatedAt)}(${ORIGIN_LABEL[r.origin]})` },
|
||||
{ label: "RSS 出现次数", value: r.observedByFeeds },
|
||||
{ label: "站点 ID", value: r.siteReleaseId, mono: true },
|
||||
{ label: "infohash", value: r.infohashV1 ?? r.infohashV2, mono: true, wide: true },
|
||||
]}
|
||||
|
||||
@@ -169,7 +169,7 @@ export const ORIGIN_LABEL: Record<WorkbenchRelease["origin"], string> = {
|
||||
};
|
||||
|
||||
export const SITE_HEALTH: Record<SiteHealth, { label: string; tone: StatusTone }> = {
|
||||
unknown: { label: "健康未知", tone: "neutral" },
|
||||
unknown: { label: "状态未知", tone: "neutral" },
|
||||
healthy: { label: "健康", tone: "success" },
|
||||
degraded: { label: "降级", tone: "warning" },
|
||||
failing: { label: "故障", tone: "destructive" },
|
||||
@@ -196,6 +196,6 @@ export function accountSummary(accounts: SiteAccount[]): { label: string; tone:
|
||||
|
||||
export function circuitLabel(site: Pick<Site, "circuitState" | "circuitOpenUntil">): string | null {
|
||||
if (site.circuitState === "closed") return null;
|
||||
if (site.circuitState === "half_open") return "熔断半开(试探中)";
|
||||
return `熔断中${site.circuitOpenUntil ? `,${relTime(site.circuitOpenUntil)}恢复` : ""}`;
|
||||
if (site.circuitState === "half_open") return "熔断中,正在试探恢复";
|
||||
return `熔断中${site.circuitOpenUntil ? `,${relTime(site.circuitOpenUntil)}恢复` : ""}`;
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export function unresolvedLabel(release: Pick<WorkbenchRelease, "resolutionStatu
|
||||
? "无法识别"
|
||||
: release.resolutionStatus === "rejected"
|
||||
? "已排除"
|
||||
: "未识别到本地作品";
|
||||
: "未识别";
|
||||
}
|
||||
|
||||
/** 库存叠加:只来自本地身份/Slot/任务;未识别的资源明确写"未识别",不猜 */
|
||||
@@ -92,7 +92,7 @@ export function InventoryOverlay({
|
||||
{release.entity.title}
|
||||
{release.entity.year ? ` (${release.entity.year})` : ""}
|
||||
</span>
|
||||
{loading ? <span className="text-meta text-muted-foreground">库存加载中…</span> : null}
|
||||
{loading ? <span className="text-meta text-muted-foreground">本地版本加载中…</span> : null}
|
||||
{!loading && entry && flags.length === 0 ? <StatusBadge>本地无版本</StatusBadge> : null}
|
||||
{flags.map((f) =>
|
||||
f.title ? (
|
||||
@@ -135,7 +135,7 @@ export function ReleaseRow({
|
||||
const snatched = snatchedOf(r);
|
||||
const timeTitle = [
|
||||
r.sourcePublishedAt ? `上传 ${absTime(r.sourcePublishedAt)}` : "上传时间未知",
|
||||
`${ORIGIN_LABEL[r.origin]}数据 ${relTime(r.dataUpdatedAt)}确认`,
|
||||
`来源:${ORIGIN_LABEL[r.origin]} · ${relTime(r.dataUpdatedAt)}更新`,
|
||||
].join("\n");
|
||||
return (
|
||||
<li>
|
||||
|
||||
@@ -11,13 +11,13 @@ import { relTime, SITE_HEALTH } from "./release-fields.ts";
|
||||
|
||||
const KIND: Record<SiteSnapshot["kind"], string> = {
|
||||
periodic: "定期抓取",
|
||||
backfill: "缺口补漏",
|
||||
backfill: "补抓",
|
||||
live_search: "去站点搜",
|
||||
manual: "手动抓取",
|
||||
};
|
||||
const STATUS: Record<SiteSnapshot["status"], { label: string; tone: StatusTone }> = {
|
||||
running: { label: "进行中", tone: "neutral" },
|
||||
ok: { label: "完成", tone: "success" },
|
||||
ok: { label: "已完成", tone: "success" },
|
||||
partial: { label: "部分完成", tone: "warning" },
|
||||
failed: { label: "失败", tone: "destructive" },
|
||||
};
|
||||
@@ -44,7 +44,7 @@ export function SnapshotPanel({ site }: { site: Site }) {
|
||||
{!site.enabled ? <StatusBadge tone="warning">站点已停用</StatusBadge> : null}
|
||||
{site.snapshotEnabled ? (
|
||||
<StatusBadge tone={stale ? "warning" : "success"}>
|
||||
{stale ? "抓取已过期" : "已抓取"} · {relTime(site.lastSnapshotAt)}
|
||||
{stale ? "抓取已过期" : "上次抓取"} · {relTime(site.lastSnapshotAt)}
|
||||
</StatusBadge>
|
||||
) : (
|
||||
<StatusBadge>未开启定期抓取</StatusBadge>
|
||||
@@ -89,8 +89,8 @@ export function SnapshotPanel({ site }: { site: Site }) {
|
||||
<span className="text-foreground">{KIND[s.kind]}</span>
|
||||
{s.query ? <span className="min-w-0 truncate text-muted-foreground">“{s.query}”</span> : null}
|
||||
<span className="text-muted-foreground tabular-nums">
|
||||
{s.pages.length ? `第 ${s.pages.join(",")} 页 · ` : ""}
|
||||
{s.itemCount} 条(新 {s.newCount})
|
||||
{s.pages.length ? `第 ${s.pages.join("、")} 页 · ` : ""}
|
||||
{s.itemCount} 条,新增 {s.newCount}
|
||||
</span>
|
||||
<span className="ml-auto text-meta text-muted-foreground" title={formatDateTime(s.startedAt)}>
|
||||
{relTime(s.startedAt)}
|
||||
|
||||
Reference in new issue
Block a user