feat(agent): add read-only download.list capability
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0023872b9b
commit
0f20c19b7c
6 files changed
+294
-7
No files matched your search
@@ -0,0 +1,138 @@
|
||||
// 给模型看的人话格式(与控制台 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<string, string> = {
|
||||
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<string, string> = {
|
||||
healthy: "正常",
|
||||
degraded: "异常",
|
||||
offline: "连不上",
|
||||
unknown: "还没检查过",
|
||||
failing: "连续失败",
|
||||
disabled: "已停用",
|
||||
};
|
||||
|
||||
/** 健康状态 → 人话 */
|
||||
export function healthName(h: string | null | undefined): string {
|
||||
return HEALTH_NAMES[h ?? "unknown"] ?? h ?? "还没检查过";
|
||||
}
|
||||
|
||||
const DOWNLOAD_STATUS: Record<string, string> = {
|
||||
queued: "排队中",
|
||||
submitting: "提交中",
|
||||
downloading: "下载中",
|
||||
paused: "已暂停",
|
||||
stalled: "无进展",
|
||||
completed: "已完成待整理",
|
||||
importing: "整理中",
|
||||
imported: "已入库",
|
||||
failed: "失败",
|
||||
removed: "已移除",
|
||||
missing: "文件缺失",
|
||||
};
|
||||
|
||||
const SUBSCRIPTION_STATUS: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
active: "登录正常",
|
||||
invalid: "凭证无效",
|
||||
expired: "登录已失效",
|
||||
challenge: "需要人工验证",
|
||||
unknown: "登录状态未知",
|
||||
};
|
||||
|
||||
/** 站点账号状态 → 人话 */
|
||||
export function accountName(s: string): string {
|
||||
return ACCOUNT_NAMES[s] ?? s;
|
||||
}
|
||||
Reference in new issue
Block a user