164 lines
7.2 KiB
TypeScript
164 lines
7.2 KiB
TypeScript
import type { Worker } from "./types";
|
||
import { workerAgeText } from "./client";
|
||
|
||
// CHV 可用性纯逻辑:不发请求、不拼 URL,只从 ctx 已有的
|
||
// /workers 数据推断。非管理员的 /workers 只返回聚合数
|
||
// (workers_total/online/offline),没有按执行器的分项,
|
||
// 此时 CHV 在线数是未知,绝不能把未知写成 0,也绝不能
|
||
// 把聚合在线数当成 CHV 在线数。
|
||
export function isWorkerAlive(w: Worker): boolean {
|
||
if (w.alive !== undefined && w.alive !== null) return !!w.alive;
|
||
const info = workerAgeText(w);
|
||
return info.age !== null && info.age <= 120;
|
||
}
|
||
|
||
export function countChvWorkers(workers: Worker[]): { online: number; total: number } {
|
||
const chv = workers.filter((w) => w.executor === "cloud-hypervisor");
|
||
return { online: chv.filter(isWorkerAlive).length, total: chv.length };
|
||
}
|
||
|
||
export type ChvAvailability =
|
||
| { state: "known"; online: number; total: number }
|
||
| { state: "unknown"; reason: "not-loaded" | "aggregate-only" | "load-failed" };
|
||
|
||
export function chvAvailability(
|
||
workers: Worker[] | null,
|
||
workersSummary: string,
|
||
workersError: string,
|
||
): ChvAvailability {
|
||
if (workers) return { state: "known", ...countChvWorkers(workers) };
|
||
if (workersError) return { state: "unknown", reason: "load-failed" };
|
||
if (workersSummary.includes("无 worker 注册")) return { state: "known", online: 0, total: 0 };
|
||
if (workersSummary.includes("完整 worker 清单仅全局管理员可见")) {
|
||
return { state: "unknown", reason: "aggregate-only" };
|
||
}
|
||
return { state: "unknown", reason: "not-loaded" };
|
||
}
|
||
|
||
// 执行器选择谓词:派发下拉与列表筛选都以字符串相等为准,
|
||
// 不做大小写/前缀宽松匹配(后端白名单是精确值,见 server
|
||
// POST /tasks 的 executor 白名单;宽松匹配会把非法值放进
|
||
// “CHV 任务”分支,造成前端提示与后端拒绝不一致)。
|
||
export function isChvExecutor(executor: string | undefined | null): boolean {
|
||
return executor === "cloud-hypervisor";
|
||
}
|
||
|
||
// 附件守卫谓词:CHV + 带附件 → 后端建任务即拒绝(server
|
||
// POST /tasks 的附件绑定分支,错误文案“附件不支持
|
||
// cloud-hypervisor 执行器”)。本函数只作前端提示口径,
|
||
// 绝不代替后端白名单:前端照常提交,后端拒绝为准。
|
||
export function chvAttachmentBlocked(
|
||
executor: string | undefined | null,
|
||
hasAttachments: boolean,
|
||
): boolean {
|
||
return isChvExecutor(executor) && hasAttachments;
|
||
}
|
||
|
||
// VM 只读状态:四种情形各有明确文案,绝不编造第五种,
|
||
// 绝不猜 live(to10 精确化,见 ADR-15):
|
||
// - dead:CHV 任务且 chv_vm_dead 为 true(字段已 present;
|
||
// 来源:worker 终局写回的 interrupted_reason,见 server
|
||
// _chv_vm_dead_from_row);
|
||
// - not-reported:CHV 任务但字段 absent(后端只在死亡时
|
||
// 才返回该字段;absent 只表示“未报告”,且明写“后端无
|
||
// live 状态字段,不代表 VM 活着”,server 没有 VM 登记表);
|
||
// - unknown:任务详情本身未加载(task 为 null),无法判断;
|
||
// - not-applicable:非 CHV 任务,不显示 VM 行(text 为空)。
|
||
export type ChvVmDisplay =
|
||
| { state: "dead"; text: string }
|
||
| { state: "not-reported"; text: string }
|
||
| { state: "unknown"; text: string }
|
||
| { state: "not-applicable"; text: string };
|
||
|
||
export function chvVmDisplay(task: {
|
||
executor?: string | null;
|
||
chv_vm_dead?: boolean;
|
||
} | null | undefined): ChvVmDisplay {
|
||
if (task === null || task === undefined) {
|
||
return {
|
||
state: "unknown",
|
||
text: "任务详情未加载,无法判断 VM 状态(后端无 live 状态字段,详情加载后才显示)",
|
||
};
|
||
}
|
||
// 死亡文案同样先过执行器门:后端只对 CHV 任务派生该字段,
|
||
// 万一某天非 CHV 行误带该字段,前端也不把 docker 任务
|
||
// 写成“VM 死亡”。
|
||
if (task.chv_vm_dead && isChvExecutor(task.executor)) {
|
||
return {
|
||
state: "dead",
|
||
text: "VM 中途死亡,任务未完成、断点可续跑(后端只读字段 chv_vm_dead)",
|
||
};
|
||
}
|
||
if (isChvExecutor(task.executor)) {
|
||
return {
|
||
state: "not-reported",
|
||
text: "未报告 VM 死亡(后端无 live 状态字段,不代表 VM 活着)",
|
||
};
|
||
}
|
||
return { state: "not-applicable", text: "" };
|
||
}
|
||
|
||
// 旧入口(保留给已测调用点):即 chvVmDisplay 的 text,
|
||
// not-applicable 与 unknown 均回空串(调用点只在已加载的
|
||
// CHV 行上用它,unknown 分支由详情页的新入口处理)。
|
||
export function chvVmStatusText(task: {
|
||
executor?: string | null;
|
||
chv_vm_dead?: boolean;
|
||
} | null | undefined): string {
|
||
const display = chvVmDisplay(task);
|
||
return display.state === "not-applicable" ||
|
||
display.state === "unknown"
|
||
? ""
|
||
: display.text;
|
||
}
|
||
|
||
// CHV 可用性统一文案(to10):新建任务、Workers、环境
|
||
// 三处此前各写各的,Workers 的未知分支甚至恒写“仅全局
|
||
// 管理员可见”(加载失败/未加载时也是这句,是文案 bug)。
|
||
// 四处统一走本函数:known 零在线时不拦提交、后端是最终
|
||
// 权威;未知时按 reason 分三句,绝不把未知写成 0。
|
||
export function chvAvailabilityText(
|
||
availability: ChvAvailability,
|
||
): string {
|
||
if (availability.state === "known") {
|
||
const base = `CHV worker 在线 ${availability.online} / 共 ${availability.total}(来自 /workers)`;
|
||
return availability.online === 0
|
||
? `${base}:当前无 CHV worker 在线,任务将一直排队,请先确认宿主 worker 已起;本提示不拦提交,worker 随后上线可认领,后端是最终权威。`
|
||
: base;
|
||
}
|
||
if (availability.reason === "aggregate-only") {
|
||
return "CHV worker 在线数未知:完整 worker 清单仅全局管理员可见,非管理员只能看到聚合数,无法按执行器分项。";
|
||
}
|
||
if (availability.reason === "load-failed") {
|
||
return "CHV worker 在线数未知:/workers 加载失败,请到 Workers 区刷新后重试。";
|
||
}
|
||
return "CHV worker 在线数未知:/workers 尚未加载,请到 Workers 区刷新。";
|
||
}
|
||
|
||
// 列表行徽标:只有已报告死亡时才出现短徽标;未报告时
|
||
// 列表行不写 VM 状态(详情里才写“未报告”长文案,避免
|
||
// 列表行被未知状态刷屏)。
|
||
export function chvVmBadge(task: {
|
||
executor?: string | null;
|
||
chv_vm_dead?: boolean;
|
||
}): string {
|
||
return task.chv_vm_dead && isChvExecutor(task.executor)
|
||
? "VM 已报告死亡"
|
||
: "";
|
||
}
|
||
|
||
// 排队原因提示:只在“已知零 CHV worker 在线 + CHV 任务排队”时
|
||
// 给出,文案明确写“可能原因”,且不编造后端未返回的
|
||
// VM 状态/IP/快照。未知时返回空串(不提示、不猜测)。
|
||
export function chvQueueHint(
|
||
executor: string | undefined | null,
|
||
status: string | undefined | null,
|
||
availability: ChvAvailability,
|
||
): string {
|
||
if (executor !== "cloud-hypervisor" || status !== "queued") return "";
|
||
if (availability.state === "known" && availability.online === 0) {
|
||
return "可能原因:当前无 cloud-hypervisor worker 在线,任务在排队(基于 /workers 推断;VM 状态/IP/快照等后端未返回字段不展示)。请先确认 KVM 宿主上的 CHV worker 已起,worker 上线后可认领。";
|
||
}
|
||
return "";
|
||
}
|