Files
codex-app-server/cloud/console-react/src/api/chv.ts
T

164 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 "";
}