Files
UHD_Media_Tool/apps/web/app/components/sites/site-fields.ts
T
rootandClaude Opus 5.5 57ce40168d refactor(web): 技术信息折叠铺到媒体库 / 审计 / 运行状态,清理正常态徽章与解释型页描述(DEAI-2)
- 媒体库详情:作品 UUID 进技术信息;「实际路径与库根」改「文件位置」,正常态(已核验、
  可访问、主位置)不加徽章;外部身份显示「TMDB 90228 · 你确认的 / 自动匹配」,修订号进悬停;
  元数据字段来源默认收起(「显示来源」,跟随界面设置);别名默认只列中英文与手动加的;
  「内容单元」→「季与集」,「临时单元」→「编号待确认」;侧栏与页标题统一「媒体库」
- 审计:动作列不再重复英文码,对象列显示中文类型 / 能力名,ID 只在详情;补 capability.denied
- 运行状态:进程、已下线实例、趋势图与最大表收进折叠区;图例同名序列加序号,不再拼接
- 站点卡:账号正常与登录方式不加徽章,健康不显示毫秒,限流移出卡片
- 文件管理:目录「正常」徽章与重复的剩余空间去掉;已登记 → 已入库,临时文件 → 未完成
- 规则:「修订」列改「最后修改」;不支持的关键字合成一行;页签改「默认规则 / 自动收新作品 / 已忽略」;
  默认规则说明去掉「带层」等算法描述
- 站点 Passkey 与登录用通行密钥区分命名;命令面板提示去重
- 删解释型页描述(审计、搜索、下载、下载计划、RSS、规则、媒体库、运行状态、站点、任务、计划、整理)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 20:58:38 +08:00

209 lines
8.1 KiB
TypeScript

import type {
SiteAccount,
SiteAccountOverall,
SiteAdapter,
SiteAuthKind,
SiteDefinitionView,
SiteLastHealth,
SiteListItem,
SiteLoginFailureReason,
SiteRateLimit,
} from "@umt/contracts";
import type { StatusTone } from "~/components/console/status-badge.tsx";
import { relTime } from "~/components/workbench/release-fields.ts";
import { formatBytes } from "~/lib/utils.ts";
// 站点页的展示口径(docs/design/site-center.md §4.2)。缺失字段显示"—",不当成 0。
export const ADAPTER_LABEL: Record<SiteAdapter, string> = {
nexusphp: "NexusPHP",
mteam_api: "M-Team API",
api: "站点 API",
generic_rss: "通用 RSS",
};
export const ACCOUNT_OVERALL: Record<SiteAccountOverall, { label: string; tone: StatusTone }> = {
active: { label: "正常", tone: "success" },
expired: { label: "已失效", tone: "destructive" },
invalid: { label: "已失效", tone: "destructive" },
challenge: { label: "需验证", tone: "warning" },
unknown: { label: "未验证", tone: "neutral" },
unconfigured: { label: "未配置", tone: "neutral" },
};
export const AUTH_KIND_LABEL: Record<SiteAuthKind, string> = {
cookie: "Cookie",
api_key: "API Key",
password: "账号密码",
passkey: "Passkey",
};
/** 主凭证三选一(passkey 只用于 RSS / 下载,不参与登录) */
export type LoginKind = "password" | "cookie" | "api_key";
/** 定义支持的登录方式;定义没写 authSupported 时按适配器推断 */
export function supportedKinds(def: Pick<SiteDefinitionView, "adapter" | "authSupported"> | undefined): LoginKind[] {
if (!def) return ["cookie", "password", "api_key"];
const map: Record<string, LoginKind> = {
cookie: "cookie",
credential: "password",
password: "password",
apikey: "api_key",
api_key: "api_key",
};
const listed = [...new Set(def.authSupported.map((a) => map[a]).filter((x): x is LoginKind => !!x))];
if (listed.length) return listed;
if (def.adapter === "mteam_api") return ["api_key"];
if (def.adapter === "nexusphp") return ["cookie", "password"];
if (def.adapter === "api") return ["api_key", "cookie"];
return [];
}
/** 站点主账号:启用的里取最差状态那个;没有启用账号取第一个 */
const WORST: SiteAccount["status"][] = ["invalid", "expired", "challenge", "unknown", "active"];
export function primaryAccount(accounts: SiteAccount[] | undefined): SiteAccount | null {
const list = accounts ?? [];
const enabled = list.filter((a) => a.enabled);
if (!enabled.length) return list[0] ?? null;
return [...enabled].sort((a, b) => WORST.indexOf(a.status) - WORST.indexOf(b.status))[0] ?? null;
}
/** 有用户数据的账号(优先正常的) */
export function dataAccount(accounts: SiteAccount[] | undefined): SiteAccount | null {
const list = (accounts ?? []).filter((a) => a.enabled && a.userData);
return list.find((a) => a.status === "active") ?? list[0] ?? null;
}
export function overallStatus(site: SiteListItem): SiteAccountOverall {
if (site.accountStatus) return site.accountStatus;
const acc = primaryAccount(site.accounts);
return acc?.enabled ? acc.status : "unconfigured";
}
/** 需处理:账号非正常(已配置)或健康故障 */
export function needsAttention(site: SiteListItem): boolean {
const st = overallStatus(site);
return (
site.enabled && (st === "expired" || st === "invalid" || st === "challenge" || site.healthStatus === "failing")
);
}
export const bytes = (n: number | null | undefined) => (n === null || n === undefined ? "—" : formatBytes(n, 2));
/** 分享率:download=0 显示 ∞;null 显示 — */
export function ratioText(d: { ratio: number | null; download: number } | null | undefined): string {
if (!d) return "—";
if (d.download === 0 && d.ratio === null) return "∞";
if (d.ratio === null) return "—";
return d.ratio >= 1000 ? d.ratio.toFixed(0) : d.ratio.toFixed(2);
}
export function hostOf(url: string): string {
try {
return new URL(url).host;
} catch {
return url;
}
}
const HEALTH_STATUS_TEXT: Record<string, string> = {
ok: "正常",
auth_missing: "未登录",
auth_expired: "登录已失效",
auth_invalid: "凭证无效",
rate_limited: "被站点限流",
unreachable: "连不上站点",
site_maintenance: "站点维护中",
parse_error: "页面解析失败",
};
/** 健康检测一行:"正常 · 320 ms · 5 分钟前" / 失败原因一句 */
export function healthLine(h: SiteLastHealth | null, at: string | null): { text: string; tone: StatusTone } {
if (!h) return { text: "未检测", tone: "neutral" };
const parts = [HEALTH_STATUS_TEXT[h.status] ?? h.status];
// 延迟毫秒数不上屏(DEAI-2),需要时看站点详情
parts.push(relTime(at));
const tone: StatusTone = h.ok ? "success" : h.status.startsWith("auth_") ? "destructive" : "warning";
return { text: parts.join(" · "), tone };
}
export function healthMessage(h: SiteLastHealth | null): string | null {
if (!h || h.ok) return null;
return h.message;
}
/** 限流:"20 次/分";空对象 = 默认 */
export function rateLimitText(r: SiteRateLimit | null | undefined): string {
if (!r || (!r.requestsPerMinute && !r.minIntervalMs && !r.concurrency)) return "默认";
const parts: string[] = [];
if (r.requestsPerMinute) parts.push(`${r.requestsPerMinute} 次/分`);
if (r.minIntervalMs) parts.push(`间隔 ${(r.minIntervalMs / 1000).toFixed(1).replace(/\.0$/, "")} 秒`);
if (r.concurrency) parts.push(`并发 ${r.concurrency}`);
return parts.join(" · ");
}
/** 登录失败分类文案(先说发生了什么,再说怎么办) */
export function loginFailureText(
reason: SiteLoginFailureReason | null,
message: string | null,
authKind?: SiteAuthKind,
): { text: string; suggestCookie: boolean } {
switch (reason) {
case "captcha":
return { text: "登录失败:站点要求验证码。改用 Cookie,或稍后重试", suggestCookie: true };
case "two_factor":
return { text: "登录失败:站点要求二次验证。填写 TOTP 密钥后重试,或改用 Cookie", suggestCookie: true };
case "challenge":
return { text: "站点返回 Cloudflare 挑战页,无法自动登录。改用 Cookie", suggestCookie: true };
case "bad_credentials":
return { text: "登录失败:账号或密码错误", suggestCookie: false };
case "account_blocked":
return { text: `登录失败:账号被封禁或未确认${message ? `(${message})` : ""}`, suggestCookie: false };
case "auth_expired":
return {
text:
authKind === "api_key"
? "API Key 无效。更新 API Key 后重新刷新登录"
: "Cookie 已失效。更新 Cookie 后重新刷新登录",
suggestCookie: false,
};
case "network":
return { text: `连不上站点${message ? `:${message}` : ""}`, suggestCookie: false };
case "unsupported":
return { text: message ?? "这个站点不支持该凭证类型的自动登录", suggestCookie: false };
default:
return { text: message ?? "登录失败,原因未知", suggestCookie: false };
}
}
/** 列表排序键 */
export type SortKey = "name" | "ratio" | "upload" | "lastLogin";
export function sortSites(items: SiteListItem[], key: SortKey): SiteListItem[] {
const val = (s: SiteListItem): number => {
const acc = dataAccount(s.accounts);
if (key === "ratio")
return acc?.userData ? (acc.userData.download === 0 ? Infinity : (acc.userData.ratio ?? -1)) : -2;
if (key === "upload") return acc?.userData?.upload ?? -1;
if (key === "lastLogin") {
const t = primaryAccount(s.accounts)?.lastLoginAt;
return t ? new Date(t).getTime() : -1;
}
return 0;
};
if (key === "name") return [...items].sort((a, b) => a.name.localeCompare(b.name, "zh-CN"));
return [...items].sort((a, b) => val(b) - val(a));
}
/** 失败原因短标签(登录历史徽标) */
export const REASON_SHORT: Record<SiteLoginFailureReason, string> = {
captcha: "需验证码",
two_factor: "需二次验证",
bad_credentials: "密码错误",
account_blocked: "账号封禁",
challenge: "Cloudflare 挑战",
auth_expired: "凭证失效",
network: "网络失败",
unsupported: "不支持",
unknown: "失败",
};