成本配额与文件只读标记:task-cost/quota-status/task-files
This commit is contained in:
1 parent
2fd73c1121
commit
9d6430a4d0
5 files changed
+711
-14
No files matched your search
+248
-12
@@ -154,8 +154,10 @@ from cloud.marker_tools import check_control_gate as _marker_gate
|
||||
from cloud.marker_tools import count_direct_spawn_children as _marker_count_direct
|
||||
from cloud.marker_tools import fetch_task_snapshot as _marker_snapshot
|
||||
from cloud.marker_tools import format_snapshot as _marker_format
|
||||
from cloud.marker_tools import format_cost as _format_cost
|
||||
from cloud.marker_tools import format_task as _format_task
|
||||
from cloud.marker_tools import format_todo as _format_todo
|
||||
from cloud.marker_tools import safe_worktree_path as _safe_worktree_path
|
||||
from cloud.marker_tools import marker_report_header as _marker_header
|
||||
from cloud.todos import create_todo as _todo_create_db
|
||||
from cloud.todos import get_todo as _todo_get_db
|
||||
@@ -1804,7 +1806,11 @@ class Worker:
|
||||
"```task-list",
|
||||
'{"status": "failed"}',
|
||||
"```",
|
||||
"(不传 status 时查全部状态并附各状态计数。)"]
|
||||
"(不传 status 时查全部状态并附各状态计数。)",
|
||||
"成本、配额、子任务文件同款标记块:task-cost"
|
||||
" {\"task_id\": \"...\"}、quota-status {}、"
|
||||
"task-files {\"task_id\": \"...\", \"path\": \"\"}"
|
||||
"(path 空串列顶层;只读派生链内或同创建人的任务)。"]
|
||||
prefix = "\n".join(lines)
|
||||
new_task = dict(task)
|
||||
new_task["prompt"] = prefix + "\n\n" + (task.get("prompt") or "")
|
||||
@@ -3366,19 +3372,57 @@ class Worker:
|
||||
return None, "权限检查失败:%s" % exc
|
||||
return bool(ok), ""
|
||||
|
||||
def _task_files_worktree(self, target):
|
||||
"""task-files 用的目标 worktree 解析,返回 (path, None)
|
||||
或 (None, reason)。
|
||||
|
||||
候选顺序与服务端 _task_worktree_for_files 同款:租户
|
||||
Docker 任务走 tenants_root/<安全项目名>/work/
|
||||
host-worktrees/<安全任务名>,本地任务走 host.py 同级
|
||||
work/host-worktrees/<安全任务名>;只接受已存在的目录,
|
||||
不存在时按“没有本地 worktree”报告,绝不把共享挂载点
|
||||
本身或默认仓库路径当成目标任务的目录(那才是错指的
|
||||
来源)。cloud 任务与 worktree 在别机的任务直接拒绝。"""
|
||||
if target.get("executor") == "cloud":
|
||||
return None, "cloud 任务的 worktree 不在本机"
|
||||
worktree_host = target.get("worktree_host")
|
||||
if worktree_host and worktree_host != socket.gethostname():
|
||||
return None, ("任务 worktree 在主机 %s,不在本机"
|
||||
% worktree_host)
|
||||
safe_task = re.sub(r"[^A-Za-z0-9_.-]", "-",
|
||||
str(target["id"]))
|
||||
try:
|
||||
safe_project = self._tenant_safe_name(
|
||||
target.get("project_id"))
|
||||
except Exception:
|
||||
safe_project = None
|
||||
if safe_project:
|
||||
tenant_path = os.path.join(
|
||||
self.tenants_root, safe_project, "work",
|
||||
"host-worktrees", safe_task)
|
||||
if os.path.isdir(tenant_path):
|
||||
return tenant_path, None
|
||||
host_dir = os.path.dirname(
|
||||
os.path.abspath(self.host_py_path))
|
||||
local_path = os.path.join(host_dir, "work",
|
||||
"host-worktrees", safe_task)
|
||||
if os.path.isdir(local_path):
|
||||
return local_path, None
|
||||
return None, "目标没有本地 worktree(未启动或目录已清理)"
|
||||
|
||||
def _deliver_marker_tools(self, task, result):
|
||||
"""执行本轮 agent 输出里的标记工具块(见 marker_tools)。
|
||||
|
||||
只执行 completed 轮次的块;goal 任务与协调员任务不解析
|
||||
(只收不发,与 peer 同款);父任务被取消时不执行。
|
||||
执行顺序:创建 → 取消 → 重试 → 待办创建 → 待办更新 →
|
||||
待办列表 → 任务列表 → 轮询 → 等待——创建在前让同一轮
|
||||
刚创建的任务立刻能被轮询,控制操作在快照之前让轮询/
|
||||
等待读到取消或重试后的新状态;待办列表放在待办写操作
|
||||
之后,让同一轮刚建/刚改的待办立刻能在列表里看到。
|
||||
全部报告拼成一条系统后续消息排给父任务自己,由排空
|
||||
循环在轮次边界跑下一轮;拒绝只记报告与审计,不炸
|
||||
run_one。"""
|
||||
待办列表 → 任务列表 → 任务成本 → 配额 → 任务文件 →
|
||||
轮询 → 等待——创建在前让同一轮刚创建的任务立刻能被
|
||||
轮询,控制操作在快照之前让轮询/等待读到取消或重试后
|
||||
的新状态;待办列表放在待办写操作之后,让同一轮刚建/
|
||||
刚改的待办立刻能在列表里看到。全部报告拼成一条系统
|
||||
后续消息排给父任务自己,由排空循环在轮次边界跑下
|
||||
一轮;拒绝只记报告与审计,不炸 run_one。"""
|
||||
if not result or result.get("status") != "completed":
|
||||
return
|
||||
if self._was_cancelled(task["id"]):
|
||||
@@ -3396,9 +3440,13 @@ class Worker:
|
||||
todo_creates = result.get("todo_creates") or []
|
||||
todo_updates = result.get("todo_updates") or []
|
||||
task_lists = result.get("task_lists") or []
|
||||
task_costs = result.get("task_costs") or []
|
||||
quota_status = result.get("quota_status") or []
|
||||
task_files = result.get("task_files") or []
|
||||
if not (creates or polls or waits or cancels or retries
|
||||
or todo_lists or todo_creates or todo_updates
|
||||
or task_lists):
|
||||
or task_lists or task_costs or quota_status
|
||||
or task_files):
|
||||
return
|
||||
reports = []
|
||||
created = 0
|
||||
@@ -3581,7 +3629,8 @@ class Worker:
|
||||
# 一律拒绝,不静默放宽。跨项目待办按“不存在”拒绝;
|
||||
# 关联任务 id 跨项目时同样拒绝,避免把别的项目的任务
|
||||
# 挂到本项目待办上。
|
||||
if todo_lists or todo_creates or todo_updates or task_lists:
|
||||
if (todo_lists or todo_creates or todo_updates or task_lists
|
||||
or task_costs or quota_status or task_files):
|
||||
read_ok, _read_reason = self._marker_auth(task, "read")
|
||||
write_ok, _write_reason = self._marker_auth(
|
||||
task, "create_task")
|
||||
@@ -3747,6 +3796,190 @@ class Worker:
|
||||
lines.append("(还有 %d 条未列出。)"
|
||||
% (len(tasks) - block["limit"]))
|
||||
reports.append("\n".join(lines))
|
||||
for block in task_costs:
|
||||
if read_ok is not True:
|
||||
reports.append("查任务 %s 成本:拒绝(发送方"
|
||||
"创建人无 read 权限)。"
|
||||
% block["task_id"])
|
||||
rejected += 1
|
||||
continue
|
||||
target = self.get_task(block["task_id"])
|
||||
if (target is None
|
||||
or target.get("project_id")
|
||||
!= task["project_id"]):
|
||||
reports.append("查任务 %s 成本:不存在(或"
|
||||
"属于别的项目),已跳过。"
|
||||
% block["task_id"])
|
||||
rejected += 1
|
||||
continue
|
||||
try:
|
||||
conn = _db_connect(self.db_path)
|
||||
try:
|
||||
relation = _marker_ancestry(
|
||||
conn, task, target)
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
relation = "other"
|
||||
if relation not in ("descendant", "same_creator"):
|
||||
reports.append("查任务 %s 成本:拒绝(目标不"
|
||||
"在派生链内且创建人不同)。"
|
||||
% block["task_id"])
|
||||
rejected += 1
|
||||
continue
|
||||
try:
|
||||
store = CostStore(self.db_path)
|
||||
try:
|
||||
report = store.report_by_task(
|
||||
block["task_id"])
|
||||
finally:
|
||||
try:
|
||||
store._conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
reports.append(_format_cost(block["task_id"],
|
||||
report))
|
||||
except Exception as exc:
|
||||
reports.append("查任务 %s 成本失败:%s"
|
||||
% (block["task_id"], exc))
|
||||
rejected += 1
|
||||
for _block in quota_status:
|
||||
if read_ok is not True:
|
||||
reports.append("查配额:拒绝(发送方创建人"
|
||||
"无 read 权限)。")
|
||||
rejected += 1
|
||||
continue
|
||||
owner = task.get("created_by")
|
||||
if not owner and task.get("parent_task_id"):
|
||||
parent = self.get_task(task["parent_task_id"])
|
||||
if parent is not None:
|
||||
owner = parent.get("created_by")
|
||||
if not owner:
|
||||
reports.append("查配额:拒绝(发送方无创建"
|
||||
"人,无法确定配额属主)。")
|
||||
rejected += 1
|
||||
continue
|
||||
try:
|
||||
quota, used, month, unpriced = (
|
||||
self._quota_limit_and_used(owner))
|
||||
except Exception as exc:
|
||||
reports.append("查配额失败:%s" % exc)
|
||||
rejected += 1
|
||||
continue
|
||||
if quota is None:
|
||||
reports.append("配额(%s):不限额,已用"
|
||||
" $%.4f,无价 actual token %d。"
|
||||
% (month, used / 1000000.0,
|
||||
unpriced))
|
||||
else:
|
||||
reports.append("配额(%s):总额 $%.4f,已用"
|
||||
" $%.4f,剩余 $%.4f,无价"
|
||||
" actual token %d。"
|
||||
% (month, quota / 1000000.0,
|
||||
used / 1000000.0,
|
||||
(quota - used) / 1000000.0,
|
||||
unpriced))
|
||||
for block in task_files:
|
||||
if read_ok is not True:
|
||||
reports.append("读任务 %s 文件:拒绝(发送方"
|
||||
"创建人无 read 权限)。"
|
||||
% block["task_id"])
|
||||
rejected += 1
|
||||
continue
|
||||
target = self.get_task(block["task_id"])
|
||||
if (target is None
|
||||
or target.get("project_id")
|
||||
!= task["project_id"]):
|
||||
reports.append("读任务 %s 文件:不存在(或"
|
||||
"属于别的项目),已跳过。"
|
||||
% block["task_id"])
|
||||
rejected += 1
|
||||
continue
|
||||
try:
|
||||
conn = _db_connect(self.db_path)
|
||||
try:
|
||||
relation = _marker_ancestry(
|
||||
conn, task, target)
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
relation = "other"
|
||||
if relation not in ("descendant", "same_creator"):
|
||||
reports.append("读任务 %s 文件:拒绝(目标不"
|
||||
"在派生链内且创建人不同)。"
|
||||
% block["task_id"])
|
||||
rejected += 1
|
||||
continue
|
||||
worktree, reason = self._task_files_worktree(target)
|
||||
if worktree is None:
|
||||
reports.append("读任务 %s 文件:拒绝(%s)。"
|
||||
% (block["task_id"], reason))
|
||||
rejected += 1
|
||||
continue
|
||||
safe = _safe_worktree_path(worktree, block["path"])
|
||||
if safe is None:
|
||||
reports.append("读任务 %s 文件 %s:拒绝"
|
||||
"(路径逃逸出 worktree)。"
|
||||
% (block["task_id"],
|
||||
block["path"] or "(顶层)"))
|
||||
rejected += 1
|
||||
continue
|
||||
if os.path.isdir(safe):
|
||||
try:
|
||||
names = sorted(os.listdir(safe))[:50]
|
||||
except Exception as exc:
|
||||
reports.append("列任务 %s 目录失败:%s"
|
||||
% (block["task_id"], exc))
|
||||
rejected += 1
|
||||
continue
|
||||
lines = ["任务 %s 目录 %s(共 %d 项,"
|
||||
"只列前 50 项):"
|
||||
% (block["task_id"],
|
||||
block["path"] or "(顶层)",
|
||||
len(names))]
|
||||
for name in names:
|
||||
full = os.path.join(safe, name)
|
||||
lines.append(name + ("/" if os.path.isdir(
|
||||
full) else ""))
|
||||
reports.append("\n".join(lines))
|
||||
elif os.path.isfile(safe):
|
||||
try:
|
||||
with open(safe, "rb") as fh:
|
||||
raw = fh.read(20001)
|
||||
except Exception as exc:
|
||||
reports.append("读任务 %s 文件 %s 失败:"
|
||||
"%s" % (block["task_id"],
|
||||
block["path"], exc))
|
||||
rejected += 1
|
||||
continue
|
||||
try:
|
||||
text = raw.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
text = None
|
||||
if text is None or "\x00" in text:
|
||||
# NUL 字节是二进制文件的可靠信号:纯控制
|
||||
# 字符也能通过 utf-8 解码,只靠解码失败
|
||||
# 拦不住,必须另查 NUL。
|
||||
reports.append("读任务 %s 文件 %s:拒绝"
|
||||
"(二进制文件,不读)。"
|
||||
% (block["task_id"],
|
||||
block["path"]))
|
||||
rejected += 1
|
||||
continue
|
||||
truncated = len(raw) > 20000
|
||||
if truncated:
|
||||
text = text[:20000]
|
||||
reports.append("任务 %s 文件 %s%s:\n%s"
|
||||
% (block["task_id"],
|
||||
block["path"],
|
||||
"(已截断前 20000 字)"
|
||||
if truncated else "",
|
||||
text))
|
||||
else:
|
||||
reports.append("读任务 %s 文件 %s:不存在。"
|
||||
% (block["task_id"],
|
||||
block["path"]))
|
||||
rejected += 1
|
||||
for poll in polls:
|
||||
try:
|
||||
reports.append(self._poll_task_snapshot(task, poll))
|
||||
@@ -3763,11 +3996,14 @@ class Worker:
|
||||
self._queue_self_followup(task, body)
|
||||
detail = ("%s: spawned=%d rejected=%d polls=%d waits=%d "
|
||||
"cancels=%d retries=%d todo_lists=%d "
|
||||
"todo_creates=%d todo_updates=%d task_lists=%d"
|
||||
"todo_creates=%d todo_updates=%d task_lists=%d "
|
||||
"task_costs=%d quota=%d task_files=%d"
|
||||
% (task["id"], created, rejected, len(polls),
|
||||
len(waits), len(cancels), len(retries),
|
||||
len(todo_lists), len(todo_creates),
|
||||
len(todo_updates), len(task_lists)))
|
||||
len(todo_updates), len(task_lists),
|
||||
len(task_costs), len(quota_status),
|
||||
len(task_files)))
|
||||
self._audit(task["project_id"], "worker_marker_tools", detail)
|
||||
|
||||
def _deliver_peer_messages(self, task, result):
|
||||
|
||||
Reference in new issue
Block a user