feat: 任务附件——server/worker/console 链路打通(+ popen goal 新语义补漏)
server:POST /uploads(multipart,413 预检、路径/数量/大小校验、 24h 过期包、used 防一包多用)+ 建任务绑定(同事务写附件行 + 包置 used,cloud 执行器 400)+ 列表/详情带 attachments + 取消删暂存 + 分支带走附件清单;db.py PG 建表 + init_db 补表。 worker:_task_seed_dir(无附件/目录已删静默 None)+ dispatch/ goal 三路(同步本地、popen、Docker 只读挂载 /tmp/codex-seed) + cloud 执行器兜底失败 + 终局删暂存。 console:文件/文件夹/图片三路输入,先上传拿 upload_id 再建任务, 列表 [附件×n]、详情列附件路径。 附带:popen 的 goal 命令此前强制要求 verify_command,goal 新语义 任务到本地 worker(走 popen)会直接报错,改成与同步 goal_task 一致(无验收不传 --verify,轮次 0 原样传)。 测试:新增 test_task_attachments.py 15 个。
This commit is contained in:
1 parent
9537e6f0d2
commit
7c444a688b
8 files changed
+869
-32
No files matched your search
+54
-1
@@ -173,6 +173,12 @@
|
||||
</select>
|
||||
</div>
|
||||
<div id="goalHint" style="display:none" class="muted">goal 模式:输入框内容即目标,不需要专用 prompt 与验收命令;不限轮次,模型确认达成后在回复末尾输出“目标已达成”即结束,未达成会一直运行,可用取消停止。</div>
|
||||
<div class="bar">
|
||||
<span class="muted">附件(放进任务工作区,图片与普通文件不区分):</span>
|
||||
<input type="file" id="attFiles" multiple title="文件">
|
||||
<input type="file" id="attDir" webkitdirectory title="文件夹">
|
||||
<input type="file" id="attImg" accept="image/*" multiple title="图片">
|
||||
</div>
|
||||
<div class="bar">
|
||||
<button id="createTask" class="primary">提交任务</button>
|
||||
<span id="taskMsg" class="muted"></span>
|
||||
@@ -378,7 +384,7 @@ async function loadTasks() {
|
||||
var pr = t.pr_url ? "<a href='" + esc(t.pr_url) + "' target='_blank' rel='noopener'>#" + esc(t.pr_number) + "</a>" : "";
|
||||
return "<tr data-id='" + esc(t.id) + "' class='" + (currentTask && currentTask.id === t.id ? "selected" : "") + "'>" +
|
||||
"<td>" + esc(shortId(t.id)) + "</td>" +
|
||||
"<td>" + (t.plan ? "[计划] " : "") + (t.forked_from ? "[分支] " : "") + ((t.goal_mode || t.verify_command) ? "[目标] " : "") + (t.environment_id ? "[环境 " + esc(shortId(t.environment_id)) + "] " : "") + esc(String(t.prompt || "").slice(0, 60)) + "</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 class='st-" + esc(t.status) + "'>" + esc(t.status) + "</td>" +
|
||||
"<td>" + esc(t.executor || "") + "</td>" +
|
||||
"<td>" + esc(t.worktree_host || "") + "</td>" +
|
||||
@@ -505,6 +511,7 @@ 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.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 || "");
|
||||
var body = "";
|
||||
@@ -614,6 +621,41 @@ $("#createEnvironment").onclick = async function () {
|
||||
loadEnvironments();
|
||||
} catch (e) { $("#envMsg").innerHTML = "<span class='err'>新建失败:" + esc(e.message) + "</span>"; }
|
||||
};
|
||||
function gatherAttachments() {
|
||||
// 三路输入合并成 [{path, file}]:文件路用文件名,文件夹路用
|
||||
// webkitRelativePath(含子目录),图片路用文件名;同路径只留
|
||||
// 第一个(跨路重复以先选为准,服务端还会再校验一次)。
|
||||
var out = [], seen = {};
|
||||
function add(list, pathOf) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var p = pathOf(list[i]);
|
||||
if (!p || seen[p]) continue;
|
||||
seen[p] = true;
|
||||
out.push({ path: p, file: list[i] });
|
||||
}
|
||||
}
|
||||
add($("#attFiles").files, function (f) { return f.name; });
|
||||
add($("#attDir").files, function (f) { return f.webkitRelativePath || f.name; });
|
||||
add($("#attImg").files, function (f) { return f.name; });
|
||||
return out;
|
||||
}
|
||||
async function uploadAttachments(items) {
|
||||
// 先 POST /uploads 拿 upload_id,建任务时绑定;上传失败直接
|
||||
// 抛错,任务不建(避免无附件的任务先跑起来)。注意不能复用
|
||||
// api()(它会固定写 Content-Type: application/json,multipart
|
||||
// 的 boundary 必须由浏览器自己生成)。
|
||||
var fd = new FormData();
|
||||
fd.append("paths", JSON.stringify(items.map(function (x) { return x.path; })));
|
||||
items.forEach(function (x, i) { fd.append("file" + i, x.file); });
|
||||
var r = await fetch("/uploads", {
|
||||
method: "POST",
|
||||
headers: { "X-API-Key": getKey() },
|
||||
body: fd
|
||||
});
|
||||
var d = await r.json().catch(function () { return {}; });
|
||||
if (!r.ok) throw new Error((d && d.error) || ("HTTP " + r.status));
|
||||
return d.upload_id;
|
||||
}
|
||||
$("#createTask").onclick = async function () {
|
||||
var pid = $("#projectSelect").value;
|
||||
var prompt = $("#prompt").value.trim();
|
||||
@@ -643,10 +685,21 @@ $("#createTask").onclick = async function () {
|
||||
// 验收命令与轮次(服务端 goal_mode=1 时一律不限轮次)。
|
||||
body.goal_mode = 1;
|
||||
}
|
||||
var atts = gatherAttachments();
|
||||
if (atts.length) {
|
||||
try {
|
||||
$("#taskMsg").textContent = "上传附件中…";
|
||||
body.upload_id = await uploadAttachments(atts);
|
||||
} catch (e) {
|
||||
$("#taskMsg").innerHTML = "<span class='err'>附件上传失败:" + esc(e.message) + "</span>";
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
var t = await api("/tasks", "POST", body);
|
||||
$("#taskMsg").innerHTML = "<span class='ok'>任务已建:" + esc(shortId(t.id)) + "</span>";
|
||||
$("#prompt").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