Add control monitor Start All for stopped fleet nodes.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -222,6 +222,45 @@ async def update_batch(
|
|||||||
return {"results": results}
|
return {"results": results}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start-batch")
|
||||||
|
async def start_batch(
|
||||||
|
body: dict,
|
||||||
|
_user: Annotated[str, Depends(require_control_user)],
|
||||||
|
) -> dict:
|
||||||
|
"""并行启动多台策略机(Fleet start)。"""
|
||||||
|
ids = body.get("ids") or []
|
||||||
|
if not isinstance(ids, list) or not ids:
|
||||||
|
raise HTTPException(status_code=400, detail="ids 不能为空")
|
||||||
|
db = get_control_db()
|
||||||
|
|
||||||
|
async def _one(nid: int) -> dict:
|
||||||
|
node = db.get_node(int(nid))
|
||||||
|
if not node:
|
||||||
|
return {"id": nid, "ok": False, "detail": "不存在", "name": str(nid)}
|
||||||
|
name = str(node.get("name") or nid)
|
||||||
|
if not node.get("token_sealed"):
|
||||||
|
return {"id": nid, "ok": False, "detail": "未生成 Token", "name": name}
|
||||||
|
try:
|
||||||
|
code, data = await call_node(node, "POST", "/api/fleet/start", timeout=20.0)
|
||||||
|
except Exception as ex:
|
||||||
|
return {"id": nid, "ok": False, "detail": str(ex), "name": name}
|
||||||
|
detail = ""
|
||||||
|
if code >= 400:
|
||||||
|
detail = _http_detail(data) if data else f"HTTP {code}"
|
||||||
|
return {
|
||||||
|
"id": nid,
|
||||||
|
"name": name,
|
||||||
|
"ok": code < 400,
|
||||||
|
"status": code,
|
||||||
|
"detail": detail,
|
||||||
|
"result": data,
|
||||||
|
}
|
||||||
|
|
||||||
|
items = list(await asyncio.gather(*[_one(int(x)) for x in ids]))
|
||||||
|
ok_n = sum(1 for x in items if x.get("ok"))
|
||||||
|
return {"ok": ok_n == len(items), "started": ok_n, "total": len(items), "results": items}
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{node_id}")
|
@router.patch("/{node_id}")
|
||||||
async def update_node(
|
async def update_node(
|
||||||
node_id: int,
|
node_id: int,
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ export default function MonitorPage() {
|
|||||||
const [nodes, setNodes] = useState<NodeCard[]>(() => cachedNodes);
|
const [nodes, setNodes] = useState<NodeCard[]>(() => cachedNodes);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
const [busy, setBusy] = useState<Record<number, string>>({});
|
const [busy, setBusy] = useState<Record<number, string>>({});
|
||||||
|
const [batchBusy, setBatchBusy] = useState(false);
|
||||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||||
const [sseSec, setSseSec] = useState(cachedSseSec);
|
const [sseSec, setSseSec] = useState(cachedSseSec);
|
||||||
const [liveState, setLiveState] = useState<"connecting" | "live" | "retry">(
|
const [liveState, setLiveState] = useState<"connecting" | "live" | "retry">(
|
||||||
@@ -494,6 +495,56 @@ export default function MonitorPage() {
|
|||||||
await batchUpdate(updatableIds);
|
await batchUpdate(updatableIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startableIds = nodes
|
||||||
|
.filter((n) => {
|
||||||
|
if (!n.token_configured) return false;
|
||||||
|
const s = pickStrategy(n);
|
||||||
|
const running = s.running === true || s.running === 1;
|
||||||
|
return !running;
|
||||||
|
})
|
||||||
|
.map((n) => n.id);
|
||||||
|
|
||||||
|
async function startAll() {
|
||||||
|
if (!startableIds.length) {
|
||||||
|
setErr("没有可启动的策略机(需已配对且当前未运行)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const names = nodes
|
||||||
|
.filter((n) => startableIds.includes(n.id))
|
||||||
|
.map((n) => n.name)
|
||||||
|
.join("、");
|
||||||
|
const ok = window.confirm(
|
||||||
|
`确认全部启动 ${startableIds.length} 台策略机?\n\n${names}\n\n将并行调用各机 Fleet 启动(已在运行的不会列入)。`,
|
||||||
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
setErr("");
|
||||||
|
setBatchBusy(true);
|
||||||
|
try {
|
||||||
|
const r = await apiFetch<{
|
||||||
|
started?: number;
|
||||||
|
total?: number;
|
||||||
|
results?: { id: number; name?: string; ok: boolean; detail?: string }[];
|
||||||
|
}>("/api/nodes/start-batch", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ ids: startableIds }),
|
||||||
|
});
|
||||||
|
await refresh();
|
||||||
|
const fails = (r.results || []).filter((x) => !x.ok);
|
||||||
|
if (fails.length) {
|
||||||
|
setErr(
|
||||||
|
`启动完成 ${r.started ?? 0}/${r.total ?? startableIds.length};失败:` +
|
||||||
|
fails
|
||||||
|
.map((x) => `${x.name || x.id}${x.detail ? `(${x.detail})` : ""}`)
|
||||||
|
.join(";"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
setErr(ex instanceof Error ? ex.message : String(ex));
|
||||||
|
} finally {
|
||||||
|
setBatchBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const detailNode = detailId != null ? nodes.find((x) => x.id === detailId) : null;
|
const detailNode = detailId != null ? nodes.find((x) => x.id === detailId) : null;
|
||||||
const detail = detailNode ? pickStrategy(detailNode) : null;
|
const detail = detailNode ? pickStrategy(detailNode) : null;
|
||||||
const detailRunning =
|
const detailRunning =
|
||||||
@@ -534,6 +585,15 @@ export default function MonitorPage() {
|
|||||||
<button type="button" className="btn ghost" onClick={() => void refresh()}>
|
<button type="button" className="btn ghost" onClick={() => void refresh()}>
|
||||||
刷新
|
刷新
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
disabled={batchBusy || !startableIds.length}
|
||||||
|
onClick={() => void startAll()}
|
||||||
|
title="并行启动全部已配对且未运行的策略机"
|
||||||
|
>
|
||||||
|
{batchBusy ? "启动中…" : `全部启动 (${startableIds.length})`}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn ghost"
|
className="btn ghost"
|
||||||
|
|||||||
Reference in New Issue
Block a user