feat: project directory tree + first-round snapshot
- cloud/tree.py: structured worktree tree (dirs first, ignore
.git/node_modules, symlinks not followed, entry/depth
truncation flags, text render for injection)
- GET /tasks/{id}/tree with /files auth + worktree resolution,
max_depth 1..5, max_entries 1..500, escape 400, missing 404,
cloud/cross-host 409
- MCP get_task_tree translating to the new endpoint
- worker first-round injection of a repo-checkout top-level
snapshot (2 layers, 120 entries) after repo resolution,
labelled as checkout snapshot not worktree; skipped for
coordinator/resume/fork, silent when no host source
- docs/project-tree.md; tests for tree, endpoint, injection,
MCP translation
This commit is contained in:
1 parent
85f280e5e7
commit
ce7fb3afd6
9 files changed
+865
No files matched your search
@@ -74,6 +74,9 @@ from cloud.peer_messages import ensure_followups_table as _peer_ensure_followups
|
||||
from cloud.peer_messages import peer_message_prefix as _peer_prefix
|
||||
from cloud.peer_messages import row_get as _row_get
|
||||
from cloud.marker_tools import parse_spawn_defaults as _parse_spawn_defaults
|
||||
from cloud.tree import DEFAULT_MAX_DEPTH as _TREE_DEFAULT_DEPTH
|
||||
from cloud.tree import DEFAULT_MAX_ENTRIES as _TREE_DEFAULT_ENTRIES
|
||||
from cloud.tree import build_tree as _build_tree
|
||||
from cloud.automations import cancel_automation as _auto_cancel
|
||||
from cloud.automations import create_automation as _auto_create
|
||||
from cloud.automations import get_automation as _auto_get
|
||||
@@ -2316,6 +2319,8 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
self._task_timeline(user_id, rest[: -len("/timeline")])
|
||||
elif rest.endswith("/files") and method == "GET":
|
||||
self._task_files(user_id, rest[: -len("/files")], query)
|
||||
elif rest.endswith("/tree") and method == "GET":
|
||||
self._task_tree(user_id, rest[: -len("/tree")], query)
|
||||
elif rest.endswith("/browser/state") and method == "GET":
|
||||
self._browser_state(user_id,
|
||||
rest[: -len("/browser/state")])
|
||||
@@ -5214,6 +5219,61 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
self._send_json(200, {"task_id": task_id, "path": rel,
|
||||
"size": size, "content": content})
|
||||
|
||||
def _task_tree(self, user_id, task_id, query):
|
||||
"""GET /tasks/{id}/tree:结构化目录树(不读文件内容)。
|
||||
|
||||
与 /files 同款权限与 worktree 解析:viewer+ 可读,
|
||||
cloud 任务与跨机 worktree 一律 409,解析不到时 404。
|
||||
?path=<相对路径>:不传列 worktree 根;传目录列该目录。
|
||||
?max_depth:1..5,缺省 3;?max_entries:1..500,缺省
|
||||
200。非法整数一律 400,不静默回落缺省。路径逃逸
|
||||
400,不存在或非目录 404。条数到顶时 truncated=True,
|
||||
被 max_depth 截断的目录 children=[] 且
|
||||
children_truncated=True,空目录则是 children=[] 且
|
||||
children_truncated=False,两者可区分。
|
||||
"""
|
||||
task, err = self._thread_task(user_id, task_id, "read")
|
||||
if err is not None:
|
||||
self._send_json(err[0], {"error": err[1]})
|
||||
return
|
||||
worktree, werr = _task_worktree_for_files(self.server, task)
|
||||
if werr is not None:
|
||||
self._send_json(werr[0], {"error": werr[1]})
|
||||
return
|
||||
try:
|
||||
max_depth = int((query.get("max_depth")
|
||||
or [str(_TREE_DEFAULT_DEPTH)])[0])
|
||||
max_entries = int((query.get("max_entries")
|
||||
or [str(_TREE_DEFAULT_ENTRIES)])[0])
|
||||
except (TypeError, ValueError):
|
||||
self._send_json(400, {"error":
|
||||
"max_depth/max_entries must be integers"})
|
||||
return
|
||||
if not 1 <= max_depth <= 5 or not 1 <= max_entries <= 500:
|
||||
self._send_json(400, {"error":
|
||||
"max_depth must be 1..5 and "
|
||||
"max_entries must be 1..500"})
|
||||
return
|
||||
rel = (query.get("path") or [""])[0]
|
||||
try:
|
||||
tree = _build_tree(worktree, path=rel,
|
||||
max_depth=max_depth,
|
||||
max_entries=max_entries)
|
||||
except ValueError:
|
||||
self._send_json(400, {"error": "path escapes task worktree"})
|
||||
return
|
||||
except FileNotFoundError:
|
||||
self._send_json(404, {"error": "no such path"})
|
||||
return
|
||||
except NotADirectoryError:
|
||||
self._send_json(404, {"error": "path is not a directory"})
|
||||
return
|
||||
tree = dict(tree)
|
||||
tree["task_id"] = task_id
|
||||
tree["max_depth"] = max_depth
|
||||
tree["max_entries"] = max_entries
|
||||
self._send_json(200, tree)
|
||||
|
||||
def _browser_worktree_for(self, user_id, task_id):
|
||||
"""Auth + worktree resolution shared by browser endpoints.
|
||||
|
||||
|
||||
Reference in new issue
Block a user