feat: 任务间互发消息(API + agent 标记块,不中断任务)

同项目任务互发消息、对方做完可回消息:API POST /tasks/<目标>/messages
(from_task_id,目标项目 create_task + 发送项目 read 鉴权)与 agent
回合输出标记块(worker 回合后解析投递,不用联网、不用凭证,本地
与 Docker 都通)。投递不中断任务:对方运行中/排队中排成后续消息
(轮次边界执行,direct 优先);对方终态则续跑复活(resume_message +
queued),A→B→A 往返不受回合时序影响。拒绝项:自己、跨项目、goal
目标、已合并、复活位被占;每轮最多 5 块,仅 completed 轮次投递;
目标执行时配额门照常生效。控制台后续消息列显示发送方,新增给
其他任务发消息框(落已代理的 /tasks 下,不用改 nginx)。

测试:新增 test_peer_messages(15)、test_peer_messages_server(8)、
test_worker_peer(4);全量 cloud 654、host 64、deploy 19 全过(cloud 4 跳过)。
This commit is contained in:
host committed 2026-10-27 14:12:28 +08:00
1 parent 4901a1090b
commit ef30558f7c
11 files changed
+1171 -23

No files matched your search

+42 -1
View File
@@ -263,6 +263,17 @@
</div>
</div>
<div id="followupList" class="muted"></div>
<h3>给其他任务发消息</h3>
<div id="peerMessageForm">
<input id="peerTarget" placeholder="目标任务 id(与本任务同项目;对方运行中排队执行、已终态则续跑复活,绝不打断当前轮)" style="width:100%">
<textarea id="peerBody" placeholder="消息正文(以本任务名义发出;对方 agent 会看到发送方任务 id,可照此回消息)"></textarea>
<div class="bar">
<button id="btnQueuePeerMessage">排队发给对方</button>
<button id="btnDirectPeerMessage" class="primary">直接发送</button>
<span id="peerMsg" class="muted"></span>
</div>
</div>
<div id="peerMessageList" class="muted"></div>
</section>
<script>
@@ -666,6 +677,8 @@ async function showDetail(taskId) {
$("#agentBox").innerHTML = "子代理链路加载中…";
loadAgents(taskId);
loadFollowups(taskId);
loadPeerMessages(taskId);
$("#peerMsg").textContent = "";
} catch (e) { $("#detail").innerHTML = "<span class='err'>加载失败:" + esc(e.message) + "</span>"; }
}
@@ -688,7 +701,8 @@ async function loadFollowups(taskId) {
var list = d.followups || [];
if (!list.length) { box.innerHTML = "(暂无后续消息)"; return; }
box.innerHTML = list.map(function (f) {
return "[" + esc(f.mode) + "|" + esc(f.status) + "] " + esc(String(f.body || "").slice(0, 120));
var from = f.sender_task_id ? "来自任务 " + esc(String(f.sender_task_id).slice(0, 8)) + "|" : "";
return "[" + esc(f.mode) + "|" + esc(f.status) + "] " + from + esc(String(f.body || "").slice(0, 120));
}).join("<br>");
} catch (e) { box.innerHTML = "<span class='err'>后续消息加载失败:" + esc(e.message) + "</span>"; }
}
@@ -899,8 +913,35 @@ $("#btnContinueConfirm").onclick = async function () {
loadTasks();
} catch (e) { $("#continueMsgSpan").innerHTML = "<span class='err'>继续失败:" + esc(e.message) + "</span>"; }
};
async function loadPeerMessages(taskId) {
var box = $("#peerMessageList");
try {
var d = await api("/tasks/" + encodeURIComponent(taskId) + "/messages", "GET");
var list = d.messages || [];
if (!list.length) { box.innerHTML = "(本任务尚未发出任务间消息)"; return; }
box.innerHTML = list.map(function (f) {
return "[发给 " + esc(String(f.task_id).slice(0, 8)) + "|" + esc(f.mode) + "|" + esc(f.status) + "] " + esc(String(f.body || "").slice(0, 120));
}).join("<br>");
} catch (e) { box.innerHTML = "<span class='err'>发出消息加载失败:" + esc(e.message) + "</span>"; }
}
async function postPeerMessage(mode) {
if (!currentTask) return;
var target = $("#peerTarget").value.trim();
var text = $("#peerBody").value.trim();
if (!target) { $("#peerMsg").textContent = "目标任务 id 不能为空"; return; }
if (!text) { $("#peerMsg").textContent = "消息正文不能为空"; return; }
try {
var d = await api("/tasks/" + encodeURIComponent(target) + "/messages", "POST", { from_task_id: currentTask.id, body: text, mode: mode });
$("#peerMsg").innerHTML = "<span class='ok'>已发送(" + esc(d.delivery) + ")</span>";
$("#peerBody").value = "";
loadPeerMessages(currentTask.id);
} catch (e) { $("#peerMsg").innerHTML = "<span class='err'>发送失败:" + esc(e.message) + "</span>"; }
}
$("#btnQueueFollowup").onclick = function () { postFollowup("queued"); };
$("#btnDirectFollowup").onclick = function () { postFollowup("direct"); };
$("#btnQueuePeerMessage").onclick = function () { postPeerMessage("queued"); };
$("#btnDirectPeerMessage").onclick = function () { postPeerMessage("direct"); };
$("#btnCost").onclick = async function () {
if (!currentTask) return;
try {