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 只读状态文案:输入只有后端已返回的两个字段,输出 // 三种情形之一,绝不编造第四种: // - chv_vm_dead 为 true → 已报告死亡(来源:worker 终局 // 写回的 interrupted_reason,见 server _chv_vm_dead_from_row); // - CHV 任务但无该字段 → 未报告死亡,且明写“后端无 live // 状态,不代表 VM 活着”(server 没有 VM 登记表); // - 非 CHV 任务 → 空串(不显示 VM 行)。 export function chvVmStatusText(task: { executor?: string | null; chv_vm_dead?: boolean; }): string { // 死亡文案同样先过执行器门:后端只对 CHV 任务派生该字段, // 万一某天非 CHV 行误带该字段,前端也不把 docker 任务 // 写成“VM 死亡”。 if (task.chv_vm_dead && isChvExecutor(task.executor)) { return "VM 中途死亡,任务未完成、断点可续跑(后端只读字段 chv_vm_dead)"; } if (isChvExecutor(task.executor)) { return "未报告 VM 死亡(后端无 live 状态字段,不代表 VM 活着)"; } return ""; } // 列表行徽标:只有已报告死亡时才出现短徽标;未报告时 // 列表行不写 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 ""; }