import { looksLikeCode } from "@umt/contracts"; import type { StatusTone } from "~/components/console/status-badge.tsx"; import { reasonLine } from "~/lib/reasons.ts"; // 状态值 → 中文文案 + 语气。状态徽章必须带文字,这里集中维护常用的几组。 export type Labeled = { label: string; tone: StatusTone }; const pick = (map: Record, v: string | null | undefined): Labeled => (v && map[v]) || { label: v || "未知", tone: "neutral" }; const HEALTH: Record = { healthy: { label: "正常", tone: "success" }, ok: { label: "正常", tone: "success" }, degraded: { label: "降级", tone: "warning" }, failing: { label: "连续失败", tone: "destructive" }, offline: { label: "离线", tone: "destructive" }, circuit_open: { label: "已熔断", tone: "destructive" }, disabled: { label: "已停用", tone: "neutral" }, unknown: { label: "未检测", tone: "neutral" }, unverified: { label: "未校验", tone: "neutral" }, failed: { label: "失败", tone: "destructive" }, }; export const healthLabel = (v: string | null | undefined) => pick(HEALTH, v); const DOWNLOAD: Record = { queued: { label: "排队", tone: "neutral" }, submitting: { label: "提交中", tone: "neutral" }, downloading: { label: "下载中", tone: "neutral" }, paused: { label: "已暂停", tone: "warning" }, stalled: { label: "无进展", tone: "warning" }, completed: { label: "下载完成", tone: "success" }, importing: { label: "入库中", tone: "neutral" }, imported: { label: "已入库", tone: "success" }, failed: { label: "失败", tone: "destructive" }, removed: { label: "已移除", tone: "neutral" }, missing: { label: "丢失", tone: "destructive" }, }; export const downloadLabel = (v: string | null | undefined) => pick(DOWNLOAD, v); const DECISION_KIND: Record = { identity_ambiguous: "没识别出作品", identity_unresolved: "未识别", episode_mapping: "集数待确认", download_stalled: "下载无进展", download_failed: "下载失败", path_conflict: "整理冲突", space_insufficient: "空间不足", approval_required: "待审批", metadata_conflict: "元数据冲突", source_unreachable: "来源不可达", downloader_offline: "下载器连不上", cache_volume_unknown: "下载目录读不到容量", other: "其它", }; export const decisionKindLabel = (v: string) => DECISION_KIND[v] ?? v; const ORG: Record = { queued: { label: "排队", tone: "neutral" }, running: { label: "进行中", tone: "neutral" }, succeeded: { label: "成功", tone: "success" }, partial: { label: "部分成功", tone: "warning" }, failed: { label: "失败", tone: "destructive" }, cancelled: { label: "已取消", tone: "neutral" }, }; export const organizationLabel = (v: string | null | undefined) => pick(ORG, v); const ORG_ACTION: Record = { copy: "复制", move: "移动", rename: "重命名", relocate: "目录调整", hardlink: "硬链接", trash: "移入回收站", delete: "永久删除", restore: "还原", }; export const organizationActionLabel = (v: string) => ORG_ACTION[v] ?? v; const AGENT_RUN: Record = { queued: { label: "排队中", tone: "neutral" }, running: { label: "运行中", tone: "neutral" }, waiting: { label: "等待中", tone: "warning" }, succeeded: { label: "已完成", tone: "success" }, failed: { label: "失败", tone: "destructive" }, cancelled: { label: "已停止", tone: "neutral" }, budget_exceeded: { label: "超出预算", tone: "warning" }, }; export const agentRunLabel = (v: string | null | undefined) => pick(AGENT_RUN, v); const ENGINE_RUN: Record = { pending: { label: "待执行", tone: "neutral" }, running: { label: "运行中", tone: "neutral" }, sleeping: { label: "休眠(旧)", tone: "neutral" }, succeeded: { label: "完成(旧)", tone: "success" }, completed: { label: "完成", tone: "success" }, failed: { label: "失败", tone: "destructive" }, canceled: { label: "已取消", tone: "warning" }, }; export const engineRunLabel = (v: string | null | undefined) => pick(ENGINE_RUN, v); /** 相对时长:秒 → "3 分钟" */ export function formatDuration(sec: number | null | undefined): string { if (sec === null || sec === undefined || Number.isNaN(sec)) return "—"; if (sec < 1) return "< 1 秒"; if (sec < 60) return `${Math.round(sec)} 秒`; if (sec < 3600) return `${Math.floor(sec / 60)} 分 ${Math.round(sec % 60)} 秒`; if (sec < 86400) return `${Math.floor(sec / 3600)} 小时 ${Math.floor((sec % 3600) / 60)} 分`; return `${Math.floor(sec / 86400)} 天 ${Math.floor((sec % 86400) / 3600)} 小时`; } /** 相对现在:"3 分钟前" */ export function formatRelative(iso: string | null | undefined): string { if (!iso) return "—"; const t = new Date(iso).getTime(); if (Number.isNaN(t)) return iso; const diff = (Date.now() - t) / 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}`; if (abs < 86400) return `${Math.round(abs / 3600)} 小时${suffix}`; return `${Math.round(abs / 86400)} 天${suffix}`; } /** startedAt → finishedAt(未结束取现在)的秒数;未开始为 null */ export function spanSec(r: { startedAt: string | null; finishedAt: string | null }): number | null { if (!r.startedAt) return null; const end = r.finishedAt ? new Date(r.finishedAt).getTime() : Date.now(); return (end - new Date(r.startedAt).getTime()) / 1000; } /** 服务端记录的操作者串(`admin_session:` / `api_key:` / `policy:` / `system`)→ 界面文字,不露内部 ID */ export function actorText(v: string | null | undefined): string { if (!v) return "—"; const i = v.indexOf(":"); const kind = i < 0 ? v : v.slice(0, i); const rest = i < 0 ? "" : v.slice(i + 1); if (kind === "admin_session" || kind === "dev_admin" || kind === "user") return "管理员"; if (kind === "agent" || kind === "agent_run") return "智能体"; if (kind === "schedule" || kind === "scheduler") return "定时任务"; if (kind === "api_key") return "API Key"; if (kind === "policy") return `收录策略 ${rest}`; if (v === "system") return "系统"; return v; } export const ACTOR_TYPES = [ { value: "user", label: "管理员" }, { value: "api_key", label: "API Key" }, { value: "agent", label: "智能体" }, { value: "system", label: "系统" }, ]; export const actorTypeLabel = (t: string) => ACTOR_TYPES.find((a) => a.value === t)?.label ?? t; /** 计划 statusReason(`added`、`organize:succeeded` 等)→ 中文;不认识的原样显示 */ const PLAN_REASON: Record = { added: "已添加到下载器", "organize:succeeded": "整理成功", "organize:partial": "部分整理成功", "organize:failed": "整理失败", authorized: "已批准", "reserved:agent": "智能体已预留空间", }; /** 不认识的内部码走原因码表,仍不认识就不显示(原码在技术信息里) */ export const planReasonText = (v: string | null | undefined) => v ? (PLAN_REASON[v] ?? (looksLikeCode(v) ? reasonLine(v, "") || null : v)) : null;