首页:顶部一行状态(自动化开关 + 只在异常时出现的服务 / 下载盘 / 站点账号 / 下载器问题), 下面只有「在忙什么」「要我处理」「最近入库」三块;去掉运行状态、计划、缓存、来源健康、 满足率与 0 值铺陈;0 订阅时写「还没有订阅。去搜索添加」。只读对账移出首页,设置里放进 自动化 →「高级」(与存储预算、流水保留、缓存有效期一起默认收起)。 计划页:「智能体巡检」移到最上面——巡检数量少、由用户自己建、每次会花钱,是这页最需要 看的;自动任务任务多、多数不用管,放在下面。自动任务按分类分组、每组可折叠:有失败或 运行中的组默认展开,其余收起为「N 个任务 · 下次 xx」一行,折叠状态记在 localStorage; 维护类整组叫「数据清理」,未接入调度的备份不列。巡检卡只留名称、开关、间隔、上次结果与 费用,目标 / 能力 / 预算 / 模型 / 最近会话收进「详情」。去掉顶部 6 个计数卡。定时任务说明 去掉表名、字段名与实现备注。 任务页:默认隐藏系统任务(无业务对象的引擎步骤、定时任务的成功运行),可一键显示; 标题不再用 ID;耗时移入技术信息;< 1 秒写「< 1 秒」;费用两位小数,不足一分写「< $0.01」。 下载盘相关文案改用「暂停线 / 恢复线」。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
159 lines
6.7 KiB
TypeScript
159 lines
6.7 KiB
TypeScript
import type { StatusTone } from "~/components/console/status-badge.tsx";
|
|
|
|
// 状态值 → 中文文案 + 语气。状态徽章必须带文字,这里集中维护常用的几组。
|
|
|
|
export type Labeled = { label: string; tone: StatusTone };
|
|
|
|
const pick = (map: Record<string, Labeled>, v: string | null | undefined): Labeled =>
|
|
(v && map[v]) || { label: v || "未知", tone: "neutral" };
|
|
|
|
const HEALTH: Record<string, Labeled> = {
|
|
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<string, Labeled> = {
|
|
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<string, string> = {
|
|
identity_ambiguous: "没识别出作品",
|
|
identity_unresolved: "未识别",
|
|
download_stalled: "下载无进展",
|
|
download_failed: "下载失败",
|
|
path_conflict: "整理冲突",
|
|
space_insufficient: "空间不足",
|
|
approval_required: "待审批",
|
|
metadata_conflict: "元数据冲突",
|
|
source_unreachable: "来源不可达",
|
|
other: "其它",
|
|
};
|
|
export const decisionKindLabel = (v: string) => DECISION_KIND[v] ?? v;
|
|
|
|
const ORG: Record<string, Labeled> = {
|
|
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<string, string> = {
|
|
copy: "复制",
|
|
move: "移动",
|
|
rename: "重命名",
|
|
relocate: "目录调整",
|
|
hardlink: "硬链接",
|
|
trash: "移入回收站",
|
|
delete: "永久删除",
|
|
restore: "还原",
|
|
};
|
|
export const organizationActionLabel = (v: string) => ORG_ACTION[v] ?? v;
|
|
|
|
const AGENT_RUN: Record<string, Labeled> = {
|
|
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<string, Labeled> = {
|
|
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:<id>` / `api_key:<id>` / `policy:<key>` / `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<string, string> = {
|
|
added: "已添加到下载器",
|
|
"organize:succeeded": "整理成功",
|
|
"organize:partial": "部分整理成功",
|
|
"organize:failed": "整理失败",
|
|
};
|
|
export const planReasonText = (v: string | null | undefined) => (v ? (PLAN_REASON[v] ?? v) : null);
|