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:
1 parent
29efc6c61f
commit
7c46ac8e5b
6 files changed
+892
-10
No files matched your search
+28
-1
@@ -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>"; }
|
||||
|
||||
Reference in new issue
Block a user