feat: 协调员任务(并行子任务、等齐、合流汇总)

建任务时传 coordinator_parts(分工列表,1-20 项,分工名唯一)即为协调员任务:本身不跑模型回合,worker 认领后一次建齐子任务(普通任务,并行被各 worker 认领),等全部终态后合流成汇总报告写回。子任务带 parent_task_id 与 coordinator_part,控制台可用 parent_task_id 查到;协调员重跑按分工名复用已完成子任务、失败/中断的回 queued 重跑;取消与超时尽力取消未终态子任务。子任务的配额/key/执行器门照常生效,不构成绕过。协调员任务拒绝与 goal/plan/验收命令同用。控制台:任务列表 [协调]/[协调子任务] 标记、详情分工与子任务区、新建表单子任务 JSON 输入。新单测 26 个。
This commit is contained in:
host committed 2026-10-27 15:45:17 +08:00
1 parent 29efc6c61f
commit 7c46ac8e5b
6 files changed
+892 -10

No files matched your search

+28 -1
View File
@@ -160,6 +160,8 @@
<label><input id="planMode" type="checkbox"> 计划模式(只读)</label>
</div>
<textarea id="prompt" placeholder="任务 prompt(必填)"></textarea>
<textarea id="coordParts" placeholder="协调员子任务 JSON(可选),留空为普通任务"></textarea>
<div class="muted">协调员格式:[{"name":"后端","prompt":"做后端"},{"name":"前端","prompt":"做前端"}];提交后 worker 并行建子任务、等齐、合流汇总。不能与 goal/计划模式同用。</div>
<div class="bar">
<select id="modelSelect">
<option value="">模型:默认(跟随配置)</option>
@@ -248,6 +250,8 @@
<div class="bar"><button id="btnContinueConfirm" class="primary">继续会话</button><span id="continueMsgSpan" class="muted"></span></div>
</div>
<pre id="detailBody" style="display:none"></pre>
<h3>协调子任务</h3>
<div id="coordBox" class="muted">点任务行查看详情后,这里显示协调员的子任务</div>
<h3>子代理链路</h3>
<div id="agentBox" class="muted">点任务行查看详情后,这里显示子代理链路</div>
<h3>实时链路</h3>
@@ -485,7 +489,7 @@ async function loadTasks() {
"<td>" + esc(shortId(t.id)) + "</td>" +
"<td title='" + esc(t.turn_id || "") + "'>" + esc(shortId(t.turn_id)) + "</td>" +
"<td title='" + esc(t.root_turn_id || "") + "'>" + esc(shortId(t.root_turn_id)) + "</td>" +
"<td>" + (t.plan ? "[计划] " : "") + (t.forked_from ? "[分支] " : "") + ((t.goal_mode || t.verify_command) ? "[目标] " : "") + (t.environment_id ? "[环境 " + esc(shortId(t.environment_id)) + "] " : "") + ((t.attachments && t.attachments.length) ? "[附件×" + t.attachments.length + "] " : "") + esc(String(t.prompt || "").slice(0, 60)) + "</td>" +
"<td>" + (t.plan ? "[计划] " : "") + (t.forked_from ? "[分支] " : "") + (t.coordinator_parts ? "[协调] " : "") + (t.parent_task_id ? "[协调子任务] " : "") + ((t.goal_mode || t.verify_command) ? "[目标] " : "") + (t.environment_id ? "[环境 " + esc(shortId(t.environment_id)) + "] " : "") + ((t.attachments && t.attachments.length) ? "[附件×" + t.attachments.length + "] " : "") + esc(String(t.prompt || "").slice(0, 60)) + "</td>" +
"<td class='st-" + esc(t.status) + "'>" + esc(t.status) + "</td>" +
"<td>" + esc(t.executor || "") + "</td>" +
"<td>" + esc(t.worktree_host || "") + "</td>" +
@@ -568,6 +572,18 @@ function scheduleAgents(taskId) {
if (agentTimer) clearTimeout(agentTimer);
agentTimer = setTimeout(function () { loadAgents(taskId); }, 1000);
}
async function loadCoordChildren(taskId) {
var box = $("#coordBox");
if (!currentTask || !currentTask.coordinator_parts) { box.innerHTML = "<span class='muted'>(非协调员任务)</span>"; return; }
try {
var d = await api("/tasks?project_id=" + encodeURIComponent(currentTask.project_id) + "&parent_task_id=" + encodeURIComponent(taskId), "GET");
var kids = d.tasks || [];
if (!kids.length) { box.innerHTML = "<span class='muted'>(子任务尚未创建)</span>"; return; }
box.innerHTML = kids.map(function (k) {
return "<div>分工 <b>" + esc(k.coordinator_part || "") + "</b> | 状态 <span class='st-" + esc(k.status) + "'>" + esc(k.status) + "</span> | " + esc(shortId(k.id)) + " | " + esc(String(k.prompt || "").slice(0, 60)) + "</div>";
}).join("");
} catch (e) { box.innerHTML = "<span class='err'>协调子任务加载失败:" + esc(e.message) + "</span>"; }
}
async function startTrace(taskId, status) {
stopTrace();
var box = $("#traceBox");
@@ -655,6 +671,8 @@ async function showDetail(taskId) {
(t.plan ? " | <b>计划模式(只读)</b>" : "") +
(t.forked_from ? " | 分支自 " + esc(shortId(t.forked_from)) : "") +
(t.goal_mode ? " | <b>goal 模式(不限轮次,模型宣告完成)</b>" : (t.verify_command ? " | goal 模式(旧语义,验收命令)" : "")) +
(t.coordinator_parts ? " | <b>协调员:" + t.coordinator_parts.length + " 个子任务(" + esc(t.coordinator_parts.map(function (p) { return p.name; }).join("、")) + ")</b>" : "") +
(t.parent_task_id ? " | 协调子任务,父任务 " + esc(shortId(t.parent_task_id)) + (t.coordinator_part ? "(" + esc(t.coordinator_part) + ")" : "") : "") +
((t.attachments && t.attachments.length) ? " | 附件:" + t.attachments.map(esc).join("、") : "") +
(t.pr_url ? " | <a href='" + esc(t.pr_url) + "' target='_blank' rel='noopener'>PR #" + esc(t.pr_number) + "</a>" : "") +
"<br>prompt:<br>" + esc(t.prompt || "");
@@ -678,6 +696,8 @@ async function showDetail(taskId) {
startTrace(taskId, t.status);
$("#agentBox").innerHTML = "子代理链路加载中…";
loadAgents(taskId);
$("#coordBox").innerHTML = "协调子任务加载中…";
loadCoordChildren(taskId);
loadFollowups(taskId);
loadPeerMessages(taskId);
$("#peerMsg").textContent = "";
@@ -853,6 +873,12 @@ $("#createTask").onclick = async function () {
// 验收命令与轮次(服务端 goal_mode=1 时一律不限轮次)。
body.goal_mode = 1;
}
var coordRaw = $("#coordParts").value.trim();
if (coordRaw) {
try { body.coordinator_parts = JSON.parse(coordRaw); }
catch (e) { $("#taskMsg").textContent = "协调员子任务 JSON 解析失败"; return; }
if (!Array.isArray(body.coordinator_parts)) { $("#taskMsg").textContent = "协调员子任务必须是数组"; return; }
}
var atts = gatherAttachments();
if (atts.length) {
try {
@@ -867,6 +893,7 @@ $("#createTask").onclick = async function () {
var t = await api("/tasks", "POST", body);
$("#taskMsg").innerHTML = "<span class='ok'>任务已建:" + esc(shortId(t.id)) + "</span>";
$("#prompt").value = "";
$("#coordParts").value = "";
$("#attFiles").value = ""; $("#attDir").value = ""; $("#attImg").value = "";
loadTasks();
} catch (e) { $("#taskMsg").innerHTML = "<span class='err'>提交失败:" + esc(e.message) + "</span>"; }