架构:WarmPool 从 cloud/chv/pool.py 移到 experimental_pool.py, 生产残留清理彻底分文件到 cloud/chv/orphans.py(facade 直接读 registry 文件清理,不构造池、不碰 prewarm/acquire/release); cloud/chv 与 cloud/sandbox 不再导出 WarmPool,旧路径一律 ImportError(CHV 未合并、无生产用户,兼容保留已无保留理由); worker 改走 orphans facade,不再持有 _warm_pool;#215 对齐从 接口形状推进到 REPLACEMENT_POINTS 替换清单(R1/R2 可机械替换、 R3 永不替换,#215 模块当前不存在由测试钉住)。 可维护性:build_chv_config 拆成 base/ssh/network/quota/disk/ timeout 六片段(组装只做 dict 合并,键冲突即报错;38 字段集 与取值不变,单项非法报错文案逐字一致);ADR-12 模块职责图 与禁 import 清单(AST 钉住);cloud/chv/ 公开 API 全量类型 标注、无 Any 逃逸,并修正三处旧标注错误;死代码扫一遍 (删实验池已无调用的 _quarantine 委托与未用 _LOGGER)。 前端(只读侧,不做控制按钮):chv.ts 新增纯逻辑谓词与文案 (执行器精确匹配、附件守卫、VM 状态文案、列表徽标);列表 行新增 VM 已报告死亡徽标;详情对全部 CHV 任务显示 VM 行 (已报告死亡/未报告死亡,不猜 live);WorkersSection 与成本 卡沿用已有数据,不新开池管理/成本页;服务端未新增第二个 生命周期字段(持久化事实只有三项,chv_vm_dead 已是完整派生), 只把派生抽成 _chv_vm_dead_from_row 并补 goal 回落形态测试。
95 lines
5.9 KiB
TypeScript
95 lines
5.9 KiB
TypeScript
import { useConsole } from "../context";
|
||
import { shortId } from "../api/client";
|
||
import { firstChar, durationOf, tokenOf } from "../api/taskMeta";
|
||
import { chvAvailability, chvQueueHint, chvVmBadge } from "../api/chv";
|
||
|
||
// 任务列表行 meta:首字 / 总耗时 / token,计算见 api/taskMeta.ts,
|
||
// 与任务详情共用同一口径。
|
||
|
||
// GitHub issues-style task list: search on top, the project selector
|
||
// (the auto-project fix — do not remove it), a 主要聊天 header with a
|
||
// + button, then hairline rows with a Primer status dot at the left,
|
||
// a 14px title and a 12px muted meta line. The search input IS the
|
||
// existing task search (id/prompt); status and executor filters stay
|
||
// as a compact row below it. The selected row gets the light
|
||
// blue-grey background (#ddf4ff), not a bubble.
|
||
export default function TaskListSection({ onNew }: { onNew: () => void }) {
|
||
const ctx = useConsole();
|
||
const chvAvail = chvAvailability(ctx.workers, ctx.workersSummary, ctx.workersError);
|
||
return (
|
||
<div>
|
||
<div className="search-box">
|
||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
|
||
<circle cx="11" cy="11" r="7" /><path d="M21 21l-4.3-4.3" />
|
||
</svg>
|
||
<input type="text" placeholder="搜索" aria-label="搜索任务" value={ctx.searchText} onChange={(e) => ctx.setSearchText(e.target.value)} />
|
||
</div>
|
||
<div className="bar">
|
||
<select aria-label="项目" value={ctx.projectId} onChange={(e) => ctx.selectProject(e.target.value)}>
|
||
{ctx.projects.length === 0 ? <option value="">(先加载项目)</option> : null}
|
||
{ctx.projects.map((p) => <option key={p.id} value={p.id}>{p.name} ({shortId(p.id)})</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="list-section-head">
|
||
<span>主要聊天</span>
|
||
<button type="button" aria-label="派发新任务" title="派发新任务" onClick={onNew}>+</button>
|
||
</div>
|
||
<div className="bar">
|
||
<select aria-label="状态筛选" value={ctx.statusFilter} onChange={(e) => ctx.setStatusFilter(e.target.value)}>
|
||
<option value="">状态:全部</option>
|
||
{["queued", "running", "completed", "failed", "interrupted", "merged", "merge-failed"].map((s) => <option key={s} value={s}>{s}</option>)}
|
||
</select>
|
||
<select aria-label="执行器筛选" value={ctx.executorFilter} onChange={(e) => ctx.setExecutorFilter(e.target.value)}>
|
||
<option value="">执行器:全部</option><option value="local">local</option><option value="docker">docker</option><option value="cloud">cloud</option><option value="cloud-hypervisor">cloud-hypervisor(实验)</option>
|
||
</select>
|
||
</div>
|
||
<div className="bar">
|
||
<button onClick={ctx.refreshTasks}>刷新</button>
|
||
<span className="muted">{ctx.taskCountText}</span>
|
||
</div>
|
||
<div className="bar">
|
||
<button disabled={ctx.taskPage <= 0} onClick={() => ctx.goToTaskPage(ctx.taskPage - 1)}>上一页</button>
|
||
<span className="muted">第 {ctx.taskPage + 1} / {ctx.taskTotalPages} 页(每页 20 条,最新在前)</span>
|
||
<button disabled={ctx.taskPage + 1 >= ctx.taskTotalPages} onClick={() => ctx.goToTaskPage(ctx.taskPage + 1)}>下一页</button>
|
||
</div>
|
||
{ctx.taskError ? <div className="err">{ctx.taskError}</div>
|
||
: !ctx.projectId ? <div className="muted">先选项目</div>
|
||
: ctx.filteredTasks.length === 0 ? <div className="muted">{ctx.taskTotal ? "没有符合筛选条件的任务" : "该项目暂无任务"}</div>
|
||
: (
|
||
<ul className="issue-list">
|
||
{ctx.filteredTasks.map((t) => {
|
||
const [dur, durTitle] = durationOf(t);
|
||
const [tok, tokTitle] = tokenOf(t);
|
||
const fc = firstChar(t);
|
||
return (
|
||
<li key={t.id} className={"issue-item" + (ctx.currentTask?.id === t.id ? " selected" : "")} onClick={() => ctx.showDetail(t.id)}>
|
||
<span className={"issue-state st-" + t.status} aria-hidden="true" />
|
||
<span className="issue-first" title={"任务首字:" + fc} aria-hidden="true">{fc}</span>
|
||
<span style={{ flex: 1, minWidth: 0 }}>
|
||
<div className="issue-title">
|
||
{t.plan ? "[计划] " : ""}{t.forked_from ? "[分支] " : ""}{t.coordinator_parts ? "[协调] " : ""}{t.parent_task_id ? "[协调子任务] " : ""}{String(t.prompt || shortId(t.id)).slice(0, 60)}
|
||
</div>
|
||
<div className="issue-meta">
|
||
{shortId(t.id)} · <span className={"st-" + t.status}>{t.status}</span> · {t.executor || ""}{t.updated_at ? " · " + new Date(t.updated_at * 1000).toLocaleString() : ""}
|
||
{" · "}<span title={durTitle}>耗时 {dur}</span>
|
||
{" · "}<span title={tokTitle}>{tok}</span>
|
||
</div>
|
||
{(t.status === "failed" || t.status === "interrupted" || t.status === "merge-failed") && t.error_text ? (
|
||
<div className="issue-meta" title={t.error_text}>{/503|Service Unavailable/.test(t.error_text) ? "上游 503:模型服务暂时不可用" : t.error_text.replace(/\s+/g, " ").slice(0, 60)}</div>
|
||
) : null}
|
||
{chvQueueHint(t.executor, t.status, chvAvail) ? (
|
||
<div className="issue-meta" title={chvQueueHint(t.executor, t.status, chvAvail)}>{chvQueueHint(t.executor, t.status, chvAvail)}</div>
|
||
) : null}
|
||
{chvVmBadge(t) ? (
|
||
<div className="issue-meta" title="后端只读字段 chv_vm_dead(worker 终局写回的 VM 死亡原因派生);无徽标时不代表 VM 活着">{chvVmBadge(t)}</div>
|
||
) : null}
|
||
</span>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|