// 给模型看的人话格式(与控制台 apps/web/app/lib/utils.ts formatBytes、labels.ts formatRelative 同口径): // 环境层渲染和只读能力的输出(download.list 的 size / progress)共用,避免把原始字节数、ISO 时间交给模型转述。 /** 字节 → "79.4 GB"(1024 进制,与控制台一致);null → null */ export function humanBytes(bytes: number | null | undefined, decimals = 1): string | null { if (bytes === null || bytes === undefined || !Number.isFinite(bytes)) return null; if (bytes <= 0) return "0 B"; const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]; const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), sizes.length - 1); return `${Number.parseFloat((bytes / 1024 ** i).toFixed(i === 0 ? 0 : decimals))} ${sizes[i]}`; } /** 0..1 → "37%" */ export function humanPercent(ratio: number | null | undefined): string | null { if (ratio === null || ratio === undefined || !Number.isFinite(ratio)) return null; return `${Math.round(Math.max(0, Math.min(1, ratio)) * 100)}%`; } /** 本地日期时间 "09-29 16:12"(按给定时区) */ function clock(d: Date, timeZone?: string): { day: string; time: string } { const f = new Intl.DateTimeFormat("zh-CN", { ...(timeZone ? { timeZone } : {}), year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, }); const p = Object.fromEntries(f.formatToParts(d).map((x) => [x.type, x.value])); return { day: `${p.year}-${p.month}-${p.day}`, time: `${p.hour === "24" ? "00" : p.hour}:${p.minute}` }; } /** 当前时间 "2026-09-30 17:45" */ export function humanNow(now: Date, timeZone?: string): string { const c = clock(now, timeZone); return `${c.day} ${c.time}`; } /** * 相对时间:刚刚 / N 分钟前 / N 小时前(当天)/ 昨天 16:12 / N 天前;未来时间写"N 小时后"。 * 无效或空 → null。 */ export function humanRelative(iso: string | null | undefined, now: Date, timeZone?: string): string | null { if (!iso) return null; const t = new Date(iso); if (Number.isNaN(t.getTime())) return null; const diff = (now.getTime() - t.getTime()) / 1000; const abs = Math.abs(diff); const suffix = diff >= 0 ? "前" : "后"; if (abs < 45) return diff >= 0 ? "刚刚" : "马上"; if (abs < 3600) return `${Math.round(abs / 60)} 分钟${suffix}`; const a = clock(t, timeZone); const b = clock(now, timeZone); if (a.day === b.day) return `${Math.round(abs / 3600)} 小时${suffix}`; const yesterday = clock(new Date(now.getTime() - 86400_000), timeZone).day; if (diff > 0 && a.day === yesterday) return `昨天 ${a.time}`; if (abs < 86400 * 2) return `${Math.round(abs / 3600)} 小时${suffix}`; return `${Math.round(abs / 86400)} 天${suffix}`; } const KIND_NAMES: Record = { qbittorrent: "qBittorrent", transmission: "Transmission", deluge: "Deluge", aria2: "Aria2", jellyfin: "Jellyfin", emby: "Emby", plex: "Plex", pt: "PT 站", public_bt: "公开 BT 站", rss_feed: "自定义 RSS 源", torznab: "Torznab 索引器", }; /** 类型代码 → 界面名称(qbittorrent → qBittorrent,pt → PT 站);未知原样返回 */ export function kindName(kind: string): string { return KIND_NAMES[kind] ?? kind; } const HEALTH_NAMES: Record = { healthy: "正常", degraded: "异常", offline: "连不上", unknown: "还没检查过", failing: "连续失败", disabled: "已停用", }; /** 健康状态 → 人话 */ export function healthName(h: string | null | undefined): string { return HEALTH_NAMES[h ?? "unknown"] ?? h ?? "还没检查过"; } const DOWNLOAD_STATUS: Record = { queued: "排队中", submitting: "提交中", downloading: "下载中", paused: "已暂停", stalled: "无进展", completed: "已完成待整理", importing: "整理中", imported: "已入库", failed: "失败", removed: "已移除", missing: "文件缺失", }; const SUBSCRIPTION_STATUS: Record = { active: "追更中", paused: "已暂停", pending: "待开始", completed: "已完成", cancelled: "已取消", }; /** 下载任务状态 → 界面用词;未知原样返回 */ export function downloadStatusName(s: string): string { return DOWNLOAD_STATUS[s] ?? s; } /** 订阅状态 → 界面用词;未知原样返回 */ export function subscriptionStatusName(s: string): string { return SUBSCRIPTION_STATUS[s] ?? s; } const ACCOUNT_NAMES: Record = { active: "登录正常", invalid: "凭证无效", expired: "登录已失效", challenge: "需要人工验证", unknown: "登录状态未知", }; /** 站点账号状态 → 人话 */ export function accountName(s: string): string { return ACCOUNT_NAMES[s] ?? s; }