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
+49
-14
@@ -270,7 +270,7 @@ class LocalExecutor(Executor):
|
||||
raw = str(task.get("id"))
|
||||
return re.sub(r"[^A-Za-z0-9_.-]", "-", raw)
|
||||
|
||||
def dispatch_task(self, task) -> dict:
|
||||
def dispatch_task(self, task, seed_dir=None) -> dict:
|
||||
"""调 host.py dispatch:建 host 任务并跑一轮 turn。
|
||||
|
||||
命令:host.py [--config CFG] [--model M] dispatch --name <host 名>
|
||||
@@ -281,6 +281,8 @@ class LocalExecutor(Executor):
|
||||
model 字段,没有则取构造时传的 model,都没有则不传 --model,
|
||||
host.py 用配置文件里的 model(fallback 时 worker 会把任务的
|
||||
model 换成 fallback 模型,第二次 dispatch/run 就跑在新模型上)。
|
||||
seed_dir 非空时追加 --seed-dir:host.py 在建 worktree 后、
|
||||
跑 turn 前把暂存附件拷进去(run 续跑不走这条路,不重复拷)。
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
@@ -296,6 +298,8 @@ class LocalExecutor(Executor):
|
||||
repo = task.get("repo") or self.repo
|
||||
if repo:
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
return self._result_from_runner(
|
||||
task["id"], self.runner(cmd))
|
||||
|
||||
@@ -342,7 +346,7 @@ class LocalExecutor(Executor):
|
||||
return self._result_from_runner(
|
||||
task["id"], self.runner(cmd))
|
||||
|
||||
def goal_task(self, task) -> dict:
|
||||
def goal_task(self, task, seed_dir=None) -> dict:
|
||||
"""调 host.py goal:目标循环(turn→验测→喂回重试)。
|
||||
|
||||
命令:host.py [--config CFG] [--model M] goal --name <host 名>
|
||||
@@ -353,6 +357,7 @@ class LocalExecutor(Executor):
|
||||
0 是合法值不能用 or 兜底成 3;旧形态仍按原样传验收命令
|
||||
与轮次。轮次缺省 3(任务行没写时),与 host.py 默认一致;
|
||||
repo 取任务的 repo 字段,没有则取构造时传的 repo。
|
||||
seed_dir 非空时追加 --seed-dir(同 dispatch 语义)。
|
||||
结论解析见 _goal_result_from_runner:host.py goal 两种结局
|
||||
都 exit 0,不能沿用 _result_from_runner。
|
||||
"""
|
||||
@@ -376,6 +381,8 @@ class LocalExecutor(Executor):
|
||||
repo = task.get("repo") or self.repo
|
||||
if repo:
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
return self._goal_result_from_runner(
|
||||
task["id"], self.runner(cmd))
|
||||
|
||||
@@ -424,7 +431,7 @@ class LocalExecutor(Executor):
|
||||
"usage": _parse_goal_usage(stdout_full),
|
||||
}
|
||||
|
||||
def popen_task(self, task, kind="dispatch"):
|
||||
def popen_task(self, task, kind="dispatch", seed_dir=None):
|
||||
"""进程组启动 host.py 子进程,返回 Popen(供真取消用)。
|
||||
|
||||
start_new_session=True 让子进程自成进程组,worker 取消时
|
||||
@@ -432,6 +439,10 @@ class LocalExecutor(Executor):
|
||||
subprocess)时才用本方法;单测注入的假 runner 走同步路径,
|
||||
不走这里。kind=goal 时建 goal 命令(结论解析由 worker 的
|
||||
_execute_via_process 按 kind 分发到 _goal_result_from_runner)。
|
||||
goal 语义与同步 goal_task 一致:新形态(goal_mode)无验收
|
||||
命令时 --verify 不传、--max-rounds 传 0(0 不能用 or 兜底
|
||||
成 3);旧形态仍传验收命令与轮次。seed_dir 只对 dispatch/
|
||||
goal 追加(run 续跑不传,文件已在 worktree 里)。
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
@@ -446,25 +457,31 @@ class LocalExecutor(Executor):
|
||||
cmd += ["run", "--task", self._host_name(task),
|
||||
"--prompt", task["prompt"]]
|
||||
elif kind == "goal":
|
||||
verify = task.get("verify_command")
|
||||
if not verify:
|
||||
raise ValueError("goal 任务缺少 verify_command")
|
||||
goal_text = task.get("goal") or task.get("prompt")
|
||||
if not goal_text:
|
||||
raise ValueError("任务缺少 prompt")
|
||||
rounds = task.get("goal_max_rounds") or 3
|
||||
verify = task.get("verify_command")
|
||||
rounds = task.get("goal_max_rounds")
|
||||
if rounds is None:
|
||||
rounds = 3
|
||||
cmd += ["goal", "--name", self._host_name(task),
|
||||
"--task", goal_text, "--verify", verify,
|
||||
"--max-rounds", str(rounds)]
|
||||
"--task", goal_text]
|
||||
if verify:
|
||||
cmd += ["--verify", verify]
|
||||
cmd += ["--max-rounds", str(rounds)]
|
||||
repo = task.get("repo") or self.repo
|
||||
if repo:
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
else:
|
||||
cmd += ["dispatch", "--name", self._host_name(task),
|
||||
"--task", task["prompt"]]
|
||||
repo = task.get("repo") or self.repo
|
||||
if repo:
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
return subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True,
|
||||
start_new_session=True)
|
||||
@@ -859,8 +876,14 @@ class DockerExecutor(Executor):
|
||||
return parsed
|
||||
|
||||
def dispatch_task(self, task, timeout=None, extra_mounts=None,
|
||||
network=None, extra_env=None) -> dict:
|
||||
"""容器内调 host.py dispatch:建 host 任务并跑一轮 turn。"""
|
||||
network=None, extra_env=None,
|
||||
seed_host_dir=None) -> dict:
|
||||
"""容器内调 host.py dispatch:建 host 任务并跑一轮 turn。
|
||||
|
||||
seed_host_dir 非空时把暂存目录只读挂载进容器
|
||||
(/tmp/codex-seed),内层 host.py 用 --seed-dir
|
||||
/tmp/codex-seed 拷进 worktree;挂载整个目录(不是单个
|
||||
文件)是为了一包多文件与子目录。"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
if not task.get("prompt"):
|
||||
@@ -873,8 +896,12 @@ class DockerExecutor(Executor):
|
||||
repo = task.get("repo")
|
||||
if repo:
|
||||
inner += ["--repo", repo]
|
||||
mounts = list(extra_mounts or [])
|
||||
if seed_host_dir:
|
||||
inner += ["--seed-dir", "/tmp/codex-seed"]
|
||||
mounts.append("%s:/tmp/codex-seed:ro" % seed_host_dir)
|
||||
return self._run_in_container(task, inner, timeout=timeout,
|
||||
extra_mounts=extra_mounts,
|
||||
extra_mounts=mounts or None,
|
||||
network=network,
|
||||
extra_env=extra_env)
|
||||
|
||||
@@ -913,9 +940,13 @@ class DockerExecutor(Executor):
|
||||
extra_env=extra_env)
|
||||
|
||||
def goal_task(self, task, timeout=None, extra_mounts=None,
|
||||
network=None, extra_env=None) -> dict:
|
||||
network=None, extra_env=None,
|
||||
seed_host_dir=None) -> dict:
|
||||
"""容器内调 host.py goal:目标循环(turn→验测→喂回重试)。
|
||||
|
||||
seed_host_dir 语义同 dispatch_task(只读挂载 + 内层
|
||||
--seed-dir /tmp/codex-seed)。
|
||||
|
||||
不走 _run_in_container:它经 _parse_host_result 截头
|
||||
stdout,而 goal 结论在末尾,截头会把结论截掉;这里直接
|
||||
调 build_run_command + _call_runner,结论用完整 stdout
|
||||
@@ -944,8 +975,12 @@ class DockerExecutor(Executor):
|
||||
repo = task.get("repo")
|
||||
if repo:
|
||||
inner += ["--repo", repo]
|
||||
mounts = list(extra_mounts or [])
|
||||
if seed_host_dir:
|
||||
inner += ["--seed-dir", "/tmp/codex-seed"]
|
||||
mounts.append("%s:/tmp/codex-seed:ro" % seed_host_dir)
|
||||
cmd = self.build_run_command(task, inner,
|
||||
extra_mounts=extra_mounts,
|
||||
extra_mounts=mounts or None,
|
||||
network=network,
|
||||
extra_env=extra_env)
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user