feat: P1 补齐(自动评审队列、自动化+分诊、并发 caps、运行时审批队列)
This commit is contained in:
1 parent
29efc6c61f
commit
e0cecd70aa
18 files changed
+3474
-27
No files matched your search
+186
-1
@@ -250,6 +250,24 @@
|
||||
<pre id="detailBody" style="display:none"></pre>
|
||||
<h3>子代理链路</h3>
|
||||
<div id="agentBox" class="muted">点任务行查看详情后,这里显示子代理链路</div>
|
||||
<h3>评审队列</h3>
|
||||
<div id="reviewForm">
|
||||
<select id="reviewTargetType">
|
||||
<option value="uncommittedChanges">未提交改动(工作区)</option>
|
||||
<option value="baseBranch">与分支的差异</option>
|
||||
<option value="commit">指定提交</option>
|
||||
<option value="custom">自定义评审指令</option>
|
||||
</select>
|
||||
<input id="reviewBranch" placeholder="与分支的差异用:分支名(如 main)" style="width:100%">
|
||||
<input id="reviewSha" placeholder="指定提交用:提交 sha" style="width:100%">
|
||||
<textarea id="reviewInstructions" placeholder="自定义评审指令用:评审指令正文"></textarea>
|
||||
<div class="bar">
|
||||
<button id="btnCreateReview" class="primary" onclick="postReview()">发起评审</button>
|
||||
<button onclick="if (currentTask) loadReviews(currentTask.id)">刷新队列</button>
|
||||
<span id="reviewMsg" class="muted"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="reviewList" class="muted">点任务行查看详情后,点“刷新队列”查看评审记录</div>
|
||||
<h3>实时链路</h3>
|
||||
<div id="traceBox" class="muted">点任务行查看详情后,这里显示过程链路</div>
|
||||
<div id="costBox"></div>
|
||||
@@ -263,6 +281,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="followupList" class="muted"></div>
|
||||
<h3>运行时审批队列</h3>
|
||||
<div class="bar"><button id="btnRefreshApprovals" onclick="if(currentTask)loadApprovals(currentTask.id)">刷新审批队列</button><span id="approvalMsg" class="muted"></span></div>
|
||||
<div class="muted">待审批项需人工点允许或拒绝;超时默认拒绝。本队列的决定会留回执;实时请求的事前拦截待 host 接入后生效,在此之前实时行为不变。</div>
|
||||
<div id="approvalList" class="muted"></div>
|
||||
<h3>给其他任务发消息</h3>
|
||||
<div id="peerMessageForm">
|
||||
<input id="peerTarget" placeholder="目标任务 id(与本任务同项目;对方运行中排队执行、已终态则续跑复活,绝不打断当前轮)" style="width:100%">
|
||||
@@ -274,6 +296,22 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="peerMessageList" class="muted"></div>
|
||||
<h3>自动化</h3>
|
||||
<div id="automationForm">
|
||||
<input id="autoName" placeholder="自动化名称(同项目内启用中的名称唯一)" style="width:100%">
|
||||
<textarea id="autoPrompt" placeholder="定时任务内容(创建只登记定义与到期状态;执行由后续执行器经 claim 抢单后建任务,不另起调度器)"></textarea>
|
||||
<div class="bar">
|
||||
<input id="autoInterval" placeholder="间隔秒数(默认 86400,最小 60)" style="width:40%">
|
||||
<input id="autoRepo" placeholder="仓库(可选)" style="width:40%">
|
||||
<button id="btnCreateAutomation" class="primary">创建自动化</button>
|
||||
<button id="btnLoadAutomations">刷新自动化</button>
|
||||
<span id="autoMsg" class="muted"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="automationList" class="muted"></div>
|
||||
<h3>分诊收件箱</h3>
|
||||
<div class="bar"><button id="btnLoadTriage">刷新分诊</button><span id="triageMsg" class="muted"></span></div>
|
||||
<div id="triageBox" class="muted"></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
@@ -399,14 +437,46 @@ async function loadProjects() {
|
||||
} catch (e) { $("#projectMsg").textContent = "加载失败:" + e.message; }
|
||||
}
|
||||
|
||||
async function loadApprovals(taskId) {
|
||||
var box = $("#approvalList");
|
||||
try {
|
||||
var d = await api("/tasks/" + encodeURIComponent(taskId) + "/approvals", "GET");
|
||||
var list = d.approvals || [];
|
||||
if (!list.length) { box.innerHTML = "(暂无审批项)"; return; }
|
||||
box.innerHTML = list.map(function (a) {
|
||||
var receipt = "";
|
||||
if (a.status === "approved" || a.status === "denied" || a.status === "expired") {
|
||||
receipt = "|回执:" + esc(a.decision || a.status) + " by " + esc(String(a.decided_by || "").slice(0, 8)) + " " + fmt(a.decided_at);
|
||||
}
|
||||
var btns = "";
|
||||
if (a.status === "pending") {
|
||||
btns = " <button onclick=\"decideApproval('" + esc(a.id) + "', 'approved')\">允许</button>" +
|
||||
" <button onclick=\"decideApproval('" + esc(a.id) + "', 'denied')\">拒绝</button>";
|
||||
}
|
||||
return "[" + esc(a.kind) + "|" + esc(a.status) + "] " + esc(a.summary) + receipt + btns;
|
||||
}).join("<br>");
|
||||
} catch (e) { box.innerHTML = "<span class='err'>审批队列加载失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
async function decideApproval(approvalId, decision) {
|
||||
if (!currentTask) return;
|
||||
var verb = decision === "approved" ? "approve" : "deny";
|
||||
try {
|
||||
await api("/tasks/" + encodeURIComponent(currentTask.id) + "/approvals/" + encodeURIComponent(approvalId) + "/" + verb, "POST", {});
|
||||
$("#approvalMsg").innerHTML = "<span class='ok'>已" + (decision === "approved" ? "允许" : "拒绝") + "</span>";
|
||||
loadApprovals(currentTask.id);
|
||||
} catch (e) { $("#approvalMsg").innerHTML = "<span class='err'>决定失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
async function loadWorkers() {
|
||||
try {
|
||||
var d = await api("/workers", "GET");
|
||||
var list = d.workers || d || [];
|
||||
if (!list.length) { $("#workers").textContent = "无 worker 注册"; return; }
|
||||
$("#workers").innerHTML = list.map(function (w) {
|
||||
var run = (w.running === undefined || w.running === null) ? "?" : w.running;
|
||||
var cap = (w.capacity === undefined || w.capacity === null) ? "?" : w.capacity;
|
||||
var live = (w.alive === false) ? " / 离线(占用为上次上报值)" : "";
|
||||
return esc(w.worker_id || w.id || "?") + " / " + esc(w.executor || "") +
|
||||
" / capacity " + esc(w.capacity || "") +
|
||||
" / 占用 " + esc(run) + "/" + esc(cap) + live +
|
||||
" / last_seen " + esc(w.last_seen || w.last_seen_seconds_ago || "");
|
||||
}).join("<br>");
|
||||
} catch (e) { $("#workers").textContent = "加载失败:" + e.message; }
|
||||
@@ -639,6 +709,52 @@ async function startTrace(taskId, status) {
|
||||
if (done) { loadTasks(); showDetail(taskId); }
|
||||
}
|
||||
|
||||
async function loadReviews(taskId) {
|
||||
var box = $("#reviewList");
|
||||
try {
|
||||
var d = await api("/tasks/" + encodeURIComponent(taskId) + "/reviews", "GET");
|
||||
var list = d.reviews || [];
|
||||
if (!list.length) { box.innerHTML = "(该任务暂无评审记录)"; return; }
|
||||
box.innerHTML = list.map(function (r) {
|
||||
var target = esc(r.target_type);
|
||||
if (r.branch) target += ":" + esc(r.branch);
|
||||
if (r.sha) target += ":" + esc(String(r.sha).slice(0, 12));
|
||||
if (r.instructions) target += ":" + esc(String(r.instructions).slice(0, 80));
|
||||
var html = "<div style='margin:6px 0;padding:8px 10px;border:1px solid var(--border);border-radius:var(--radius);background:#fff'>";
|
||||
html += "<b>评审</b> " + esc(String(r.id).slice(0, 8)) + " | " + target +
|
||||
" | <span class='st-" + esc(r.status) + "'>" + esc(r.status) + "</span>" +
|
||||
(r.created_at ? " | " + fmt(r.created_at) : "") + "<br>";
|
||||
html += r.findings ? esc(String(r.findings).slice(0, 1000)) : "<span class='muted'>(暂无 findings:评审尚未执行或尚未写回)</span>";
|
||||
html += "<br><button onclick=\"submitReviewFindings('" + esc(r.id) + "')\">写 findings</button>";
|
||||
html += "</div>";
|
||||
return html;
|
||||
}).join("");
|
||||
} catch (e) { box.innerHTML = "<span class='err'>评审队列加载失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
async function postReview() {
|
||||
if (!currentTask) return;
|
||||
var body = { target_type: $("#reviewTargetType").value };
|
||||
var branch = $("#reviewBranch").value.trim();
|
||||
var sha = $("#reviewSha").value.trim();
|
||||
var instructions = $("#reviewInstructions").value.trim();
|
||||
if (branch) body.branch = branch;
|
||||
if (sha) body.sha = sha;
|
||||
if (instructions) body.instructions = instructions;
|
||||
try {
|
||||
await api("/tasks/" + encodeURIComponent(currentTask.id) + "/reviews", "POST", body);
|
||||
$("#reviewMsg").innerHTML = "<span class='ok'>评审已登记(requested)</span>";
|
||||
loadReviews(currentTask.id);
|
||||
} catch (e) { $("#reviewMsg").innerHTML = "<span class='err'>发起评审失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
async function submitReviewFindings(reviewId) {
|
||||
if (!currentTask) return;
|
||||
var text = prompt("输入该条评审的 findings(将写回记录并标为 completed):");
|
||||
if (text === null) return;
|
||||
try {
|
||||
await api("/tasks/" + encodeURIComponent(currentTask.id) + "/reviews/" + encodeURIComponent(reviewId) + "/findings", "POST", { findings: text });
|
||||
loadReviews(currentTask.id);
|
||||
} catch (e) { $("#reviewMsg").innerHTML = "<span class='err'>写 findings 失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
async function showDetail(taskId) {
|
||||
stopTrace();
|
||||
try {
|
||||
@@ -696,6 +812,75 @@ async function doAction(kind) {
|
||||
} catch (e) { $("#actionMsg").innerHTML = "<span class='err'>" + kind + " 失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
|
||||
async function loadAutomations() {
|
||||
var pid = $("#projectSelect").value;
|
||||
var box = $("#automationList");
|
||||
if (!pid) { box.innerHTML = "(先选项目)"; return; }
|
||||
try {
|
||||
var d = await api("/projects/" + encodeURIComponent(pid) + "/automations", "GET");
|
||||
var list = d.automations || [];
|
||||
if (!list.length) { box.innerHTML = "(暂无自动化)"; return; }
|
||||
box.innerHTML = list.map(function (a) {
|
||||
var html = "[" + esc(a.status) + (a.due ? "|到期" : "") + "] " + esc(a.name) +
|
||||
"|每 " + esc(a.interval_seconds) + " 秒|下次 " + esc(fmt(a.next_run_at));
|
||||
if (a.status === "active") html += " <button data-auto-cancel='" + esc(a.id) + "'>取消</button>";
|
||||
return html;
|
||||
}).join("<br>");
|
||||
box.querySelectorAll("[data-auto-cancel]").forEach(function (btn) {
|
||||
btn.onclick = function () { cancelAutomation(btn.getAttribute("data-auto-cancel")); };
|
||||
});
|
||||
} catch (e) { box.innerHTML = "<span class='err'>自动化加载失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
async function createAutomation() {
|
||||
var pid = $("#projectSelect").value;
|
||||
if (!pid) { $("#autoMsg").textContent = "先选项目"; return; }
|
||||
var name = $("#autoName").value.trim();
|
||||
var prompt = $("#autoPrompt").value.trim();
|
||||
if (!name) { $("#autoMsg").textContent = "自动化名称必填"; return; }
|
||||
if (!prompt) { $("#autoMsg").textContent = "定时任务内容必填"; return; }
|
||||
var body = { name: name, prompt: prompt };
|
||||
var iv = $("#autoInterval").value.trim();
|
||||
if (iv) body.interval_seconds = parseInt(iv, 10);
|
||||
var repo = $("#autoRepo").value.trim();
|
||||
if (repo) body.repo_url = repo;
|
||||
try {
|
||||
await api("/projects/" + encodeURIComponent(pid) + "/automations", "POST", body);
|
||||
$("#autoMsg").innerHTML = "<span class='ok'>自动化已创建(登记定义,到期后由执行器认领)</span>";
|
||||
$("#autoName").value = ""; $("#autoPrompt").value = ""; $("#autoInterval").value = ""; $("#autoRepo").value = "";
|
||||
loadAutomations();
|
||||
} catch (e) { $("#autoMsg").innerHTML = "<span class='err'>创建失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
async function cancelAutomation(autoId) {
|
||||
var pid = $("#projectSelect").value;
|
||||
if (!pid) return;
|
||||
try {
|
||||
await api("/projects/" + encodeURIComponent(pid) + "/automations/" + encodeURIComponent(autoId) + "/cancel", "POST", {});
|
||||
$("#autoMsg").innerHTML = "<span class='ok'>自动化已取消</span>";
|
||||
loadAutomations();
|
||||
} catch (e) { $("#autoMsg").innerHTML = "<span class='err'>取消失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
async function loadTriage() {
|
||||
var pid = $("#projectSelect").value;
|
||||
var box = $("#triageBox");
|
||||
if (!pid) { box.innerHTML = "(先选项目)"; return; }
|
||||
try {
|
||||
var d = await api("/projects/" + encodeURIComponent(pid) + "/triage", "GET");
|
||||
var html = "";
|
||||
html += "<b>到期自动化(" + (d.counts.automations_due || 0) + ")</b><br>";
|
||||
html += (d.automations_due || []).length ? (d.automations_due || []).map(function (a) { return esc(a.name) + "|每 " + esc(a.interval_seconds) + " 秒"; }).join("<br>") : "(无)";
|
||||
html += "<br><b>失败/中断任务(" + (d.counts.failed_tasks || 0) + ")</b><br>";
|
||||
html += (d.failed_tasks || []).length ? (d.failed_tasks || []).map(function (t) { return esc(String(t.id).slice(0, 8)) + "|" + esc(t.status) + "|" + esc(String(t.prompt || "").slice(0, 80)); }).join("<br>") : "(无)";
|
||||
html += "<br><b>待处理后续消息(" + (d.counts.pending_followups || 0) + ")</b><br>";
|
||||
html += (d.pending_followups || []).length ? (d.pending_followups || []).map(function (f) { return "任务 " + esc(String(f.task_id).slice(0, 8)) + "|" + esc(String(f.body || "").slice(0, 80)); }).join("<br>") : "(无)";
|
||||
html += "<br><b>最近自动化产出(" + (d.counts.recent_runs || 0) + ")</b><br>";
|
||||
html += (d.recent_runs || []).length ? (d.recent_runs || []).map(function (r) { return esc(String(r.automation_id).slice(0, 8)) + "|" + esc(r.status) + (r.task_id ? "|任务 " + esc(String(r.task_id).slice(0, 8)) : "") + (r.task_status ? "|" + esc(r.task_status) : ""); }).join("<br>") : "(无)";
|
||||
box.innerHTML = html;
|
||||
} catch (e) { box.innerHTML = "<span class='err'>分诊加载失败:" + esc(e.message) + "</span>"; }
|
||||
}
|
||||
$("#btnCreateAutomation").onclick = createAutomation;
|
||||
$("#btnLoadAutomations").onclick = loadAutomations;
|
||||
$("#btnLoadTriage").onclick = loadTriage;
|
||||
$("#projectSelect").addEventListener("change", function () { loadAutomations(); loadTriage(); });
|
||||
async function loadFollowups(taskId) {
|
||||
var box = $("#followupList");
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user