feat-chv-to10:往 10 分方向提升(类型/lint 清零、worker 适配抽取、审计落盘、lockfile 生成脚本、前端四态与统一文案、就绪清单 §13)
This commit is contained in:
1 parent
df8c64bd64
commit
2f3ff8b3ec
22 files changed
+1400
-282
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { chvAttachmentBlocked, chvAvailability, chvQueueHint, chvVmBadge, chvVmStatusText, countChvWorkers, isChvExecutor, isWorkerAlive } from "./chv";
|
||||
import { chvAttachmentBlocked, chvAvailability, chvAvailabilityText, chvQueueHint, chvVmBadge, chvVmDisplay, chvVmStatusText, countChvWorkers, isChvExecutor, isWorkerAlive } from "./chv";
|
||||
|
||||
// 纯逻辑测试:只测 chv.ts,不渲染组件、不发请求、不读 localStorage。
|
||||
// alive 规则与 WorkersSection 现有口径一致:alive 字段优先,
|
||||
@@ -103,3 +103,53 @@ describe("chvQueueHint", () => {
|
||||
expect(chvQueueHint("cloud-hypervisor", "queued", { state: "known" as const, online: 1, total: 1 })).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("chvVmDisplay(to10 四态)", () => {
|
||||
it("字段 present=true → dead", () => {
|
||||
const d = chvVmDisplay({ executor: "cloud-hypervisor", chv_vm_dead: true });
|
||||
expect(d.state).toBe("dead");
|
||||
expect(d.text).toContain("VM 中途死亡");
|
||||
});
|
||||
it("字段 absent → not-reported,不猜 live", () => {
|
||||
const d = chvVmDisplay({ executor: "cloud-hypervisor" });
|
||||
expect(d.state).toBe("not-reported");
|
||||
expect(d.text).toContain("不代表 VM 活着");
|
||||
});
|
||||
it("任务未加载 → unknown", () => {
|
||||
const d = chvVmDisplay(null);
|
||||
expect(d.state).toBe("unknown");
|
||||
expect(d.text).toContain("无法判断");
|
||||
});
|
||||
it("非 CHV → not-applicable,text 为空", () => {
|
||||
const d = chvVmDisplay({ executor: "docker", chv_vm_dead: true });
|
||||
expect(d.state).toBe("not-applicable");
|
||||
expect(d.text).toBe("");
|
||||
});
|
||||
it("旧入口仍是 display 的 text(unknown/not-applicable 回空)", () => {
|
||||
expect(chvVmStatusText({ executor: "cloud-hypervisor", chv_vm_dead: true })).toContain("VM 中途死亡");
|
||||
expect(chvVmStatusText(null)).toBe("");
|
||||
expect(chvVmStatusText({ executor: "docker" })).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("chvAvailabilityText(to10 四处统一文案)", () => {
|
||||
it("known 零在线:不拦提交、后端是最终权威", () => {
|
||||
const text = chvAvailabilityText({ state: "known", online: 0, total: 2 });
|
||||
expect(text).toContain("在线 0 / 共 2");
|
||||
expect(text).toContain("不拦提交");
|
||||
expect(text).toContain("最终权威");
|
||||
});
|
||||
it("known 非零:只有在线数,无排队警告", () => {
|
||||
const text = chvAvailabilityText({ state: "known", online: 1, total: 1 });
|
||||
expect(text).toContain("在线 1 / 共 1");
|
||||
expect(text).not.toContain("排队");
|
||||
});
|
||||
it("未知三原因各有不同文案,绝不写成 0", () => {
|
||||
expect(chvAvailabilityText({ state: "unknown", reason: "aggregate-only" })).toContain("仅全局管理员可见");
|
||||
expect(chvAvailabilityText({ state: "unknown", reason: "load-failed" })).toContain("加载失败");
|
||||
expect(chvAvailabilityText({ state: "unknown", reason: "not-loaded" })).toContain("尚未加载");
|
||||
for (const reason of ["aggregate-only", "load-failed", "not-loaded"] as const) {
|
||||
expect(chvAvailabilityText({ state: "unknown", reason })).not.toContain("在线 0");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -54,27 +54,85 @@ export function chvAttachmentBlocked(
|
||||
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: {
|
||||
// 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;
|
||||
}): string {
|
||||
} | 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 "VM 中途死亡,任务未完成、断点可续跑(后端只读字段 chv_vm_dead)";
|
||||
return {
|
||||
state: "dead",
|
||||
text: "VM 中途死亡,任务未完成、断点可续跑(后端只读字段 chv_vm_dead)",
|
||||
};
|
||||
}
|
||||
if (isChvExecutor(task.executor)) {
|
||||
return "未报告 VM 死亡(后端无 live 状态字段,不代表 VM 活着)";
|
||||
return {
|
||||
state: "not-reported",
|
||||
text: "未报告 VM 死亡(后端无 live 状态字段,不代表 VM 活着)",
|
||||
};
|
||||
}
|
||||
return "";
|
||||
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 区刷新。";
|
||||
}
|
||||
|
||||
// 列表行徽标:只有已报告死亡时才出现短徽标;未报告时
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRef, useState } from "react";
|
||||
import { useConsole } from "../context";
|
||||
import { Card, Bar } from "../components/ui";
|
||||
import { gatherAttachments } from "../api/client";
|
||||
import { chvAttachmentBlocked, chvAvailability, isChvExecutor } from "../api/chv";
|
||||
import { chvAttachmentBlocked, chvAvailability, chvAvailabilityText, isChvExecutor } from "../api/chv";
|
||||
import { EFFORTS } from "../api/types";
|
||||
|
||||
export default function DispatchSection() {
|
||||
@@ -97,14 +97,8 @@ export default function DispatchSection() {
|
||||
</Bar>
|
||||
{isChv ? <div className="muted">实验执行器,默认关闭:需后端已开启 CHV worker 且宿主有 KVM,否则任务无人认领、一直排队;带附件时建任务即被后端拒绝(错误文案原样显示在下方)。前端只传 executor 值,不绕过后端白名单;默认仍是 docker。</div> : null}
|
||||
{isChv ? (
|
||||
<div className="muted">
|
||||
{chvAvail.state === "known"
|
||||
? <>CHV worker 在线 {chvAvail.online} / 共 {chvAvail.total}(来自 /workers){chvAvail.online === 0 ? <b>:当前无 CHV worker 在线,任务将一直排队,请先确认宿主 worker 已起;本提示不拦提交,worker 随后上线可认领,后端是最终权威。</b> : null}</>
|
||||
: chvAvail.reason === "aggregate-only"
|
||||
? "CHV worker 在线数未知:完整 worker 清单仅全局管理员可见,非管理员只能看到聚合数,无法按执行器分项。"
|
||||
: chvAvail.reason === "load-failed"
|
||||
? "CHV worker 在线数未知:/workers 加载失败,请到 Workers 区刷新后重试。"
|
||||
: "CHV worker 在线数未知:/workers 尚未加载,请到 Workers 区刷新。"}
|
||||
<div className="muted" aria-live="polite">
|
||||
{chvAvailabilityText(chvAvail)}
|
||||
{" "}<button type="button" onClick={ctx.refreshWorkers}>刷新 CHV 可用性</button>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -141,14 +135,14 @@ export default function DispatchSection() {
|
||||
{goalInvincible ? <div className="muted">goal 模式无敌版:goal 模式 + 任务不限时(去掉 worker/docker 总超时;单轮 600 秒护栏还在,不限的是任务总时长与总轮次)。服务端默认的自动续跑与断点镜像同时生效。</div> : null}
|
||||
<Bar>
|
||||
<span className="muted">附件(放进任务工作区,图片与普通文件不区分):</span>
|
||||
<input ref={filesRef} type="file" multiple title="文件" disabled={isChv} onChange={refreshAttachments} />
|
||||
<input ref={dirRef} type="file" multiple title="文件夹" disabled={isChv} onChange={refreshAttachments} {...({ webkitdirectory: "" } as any)} />
|
||||
<input ref={imgRef} type="file" accept="image/*" multiple title="图片" disabled={isChv} onChange={refreshAttachments} />
|
||||
<input ref={filesRef} type="file" multiple title="文件" aria-label="附件文件" disabled={isChv} onChange={refreshAttachments} />
|
||||
<input ref={dirRef} type="file" multiple title="文件夹" aria-label="附件文件夹" disabled={isChv} onChange={refreshAttachments} {...({ webkitdirectory: "" } as any)} />
|
||||
<input ref={imgRef} type="file" accept="image/*" multiple title="图片" aria-label="附件图片" disabled={isChv} onChange={refreshAttachments} />
|
||||
</Bar>
|
||||
{isChv ? <div className="muted">cloud-hypervisor 任务不支持附件:附件输入已禁用(后端对带附件的 CHV 任务会 400,错误文案原样显示在下方;切换回其他执行器可恢复附件输入)。{hasAttachments ? <>当前仍有已选附件,请先清空再提交。 <button type="button" onClick={() => { if (filesRef.current) filesRef.current.value = ""; if (dirRef.current) dirRef.current.value = ""; if (imgRef.current) imgRef.current.value = ""; setHasAttachments(false); }}>清空附件</button></> : ""}</div> : null}
|
||||
<Bar>
|
||||
<button className="seal" onClick={submit}>提交任务</button>
|
||||
<span className="muted">{msg || ctx.dispatchMsg}</span>
|
||||
<span className="muted" role="status" aria-live="polite">{msg || ctx.dispatchMsg}</span>
|
||||
</Bar>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { useConsole } from "../context";
|
||||
import { Card, Bar, DataListRaw } from "../components/ui";
|
||||
import { shortId } from "../api/client";
|
||||
import { chvAvailability } from "../api/chv";
|
||||
import { chvAvailability, chvAvailabilityText } from "../api/chv";
|
||||
|
||||
export default function EnvSection() {
|
||||
const ctx = useConsole();
|
||||
@@ -37,7 +37,7 @@ export default function EnvSection() {
|
||||
</select>
|
||||
<button onClick={async () => { await ctx.createEnv(name, branch, executor); setName(""); setBranch(""); }}>新建环境</button>
|
||||
</Bar>
|
||||
{executor === "cloud-hypervisor" ? <div className="muted">实验执行器,默认关闭:环境创建白名单同样允许 cloud-hypervisor(后端环境接口 allowlist:local / docker / cloud-hypervisor,不含 cloud);仍需后端有 KVM 宿主上的 CHV worker 才会认领,否则任务排队。前端只传值,不绕过后端白名单。{chvAvail.state === "known" ? <> CHV worker 在线 {chvAvail.online} / 共 {chvAvail.total}(来自 /workers){chvAvail.online === 0 ? ":当前无 CHV worker 在线,任务将一直排队。" : ""}</> : " CHV worker 在线数未知:完整 worker 清单仅全局管理员可见。"}</div> : null}
|
||||
{executor === "cloud-hypervisor" ? <div className="muted" aria-live="polite">实验执行器,默认关闭:环境创建白名单同样允许 cloud-hypervisor(后端环境接口 allowlist:local / docker / cloud-hypervisor,不含 cloud);仍需后端有 KVM 宿主上的 CHV worker 才会认领,否则任务排队。前端只传值,不绕过后端白名单。 {chvAvailabilityText(chvAvail)}</div> : null}
|
||||
<span className="muted">{ctx.envMsg}</span>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Card } from "../components/ui";
|
||||
import { shortId, fmt } from "../api/client";
|
||||
import { STATUS_EXPLAIN } from "../api/types";
|
||||
import { firstChar, durationOf, tokenOf, fmtCostUsd } from "../api/taskMeta";
|
||||
import { chvAvailability, chvQueueHint, chvVmStatusText } from "../api/chv";
|
||||
import { chvAvailability, chvQueueHint, chvVmDisplay } from "../api/chv";
|
||||
import DetailActions from "./detail/DetailActions";
|
||||
import BrowserPanel from "./detail/BrowserPanel";
|
||||
import ReviewPanel from "./detail/ReviewPanel";
|
||||
@@ -131,7 +131,7 @@ export default function TaskDetailPanel() {
|
||||
<div><dt>turn</dt><dd>{shortId(t.turn_id)}</dd></div>
|
||||
<div><dt>root turn</dt><dd>{shortId(t.root_turn_id)}</dd></div>
|
||||
<div><dt>executor</dt><dd>{t.executor || ""}{t.executor === "cloud-hypervisor" ? "(实验执行器,默认关闭;仅展示,无 VM 控制按钮)" : ""}</dd></div>
|
||||
{t.executor === "cloud-hypervisor" ? <div><dt>VM 状态</dt><dd>{chvVmStatusText(t)}</dd></div> : null}
|
||||
{t.executor === "cloud-hypervisor" ? <div><dt>VM 状态</dt><dd role="status" aria-label="VM 状态">{chvVmDisplay(t).text}</dd></div> : null}
|
||||
<div><dt>主机</dt><dd>{t.worktree_host || ""}</dd></div>
|
||||
<div><dt>模型</dt><dd>{t.model || "默认"}</dd></div>
|
||||
<div><dt>强度</dt><dd>{t.effort || "默认"}</dd></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useConsole } from "../context";
|
||||
import { Card, Bar, DataListRaw } from "../components/ui";
|
||||
import { workerAgeText, shortId } from "../api/client";
|
||||
import { chvAvailability } from "../api/chv";
|
||||
import { chvAvailability, chvAvailabilityText } from "../api/chv";
|
||||
|
||||
export default function WorkersSection() {
|
||||
const ctx = useConsole();
|
||||
@@ -9,10 +9,8 @@ export default function WorkersSection() {
|
||||
return (
|
||||
<Card id="nav-workers" title="Workers">
|
||||
<Bar><button onClick={ctx.refreshWorkers}>刷新</button></Bar>
|
||||
<div className="muted">
|
||||
{chvAvail.state === "known"
|
||||
? <>cloud-hypervisor worker 在线 {chvAvail.online} / 共 {chvAvail.total}(实验,默认关闭){chvAvail.online === 0 ? ":当前无 CHV worker 在线,CHV 任务将一直排队。" : ""}</>
|
||||
: "cloud-hypervisor worker 在线数未知:完整 worker 清单仅全局管理员可见,非管理员只能看到聚合数,无法按执行器分项。"}
|
||||
<div className="muted" aria-live="polite">
|
||||
{chvAvailabilityText(chvAvail)}
|
||||
</div>
|
||||
{ctx.workersError ? <div className="muted">{ctx.workersError}</div>
|
||||
: ctx.workers === null ? <div className="muted">{ctx.workersSummary || "未加载"}</div>
|
||||
|
||||
Reference in new issue
Block a user