agent 在回合输出里写 goal-convert 块,worker 在轮次边界 排成 /goal 自后续消息、走已有 followup 转换路径把本会话 转成 goal;写 automation-create 块,worker 在轮次边界 创建自动化定义(权限 create_task、配额整轮门、创建人跟 父任务走、name 缺失自动生成、首次时刻已过去单块拒绝、 bind heartbeat 可绑回本会话)。 automations 新增 first_run_at / run_once:首次时刻只管 首次、之后按间隔算;只跑一次跑过后 is_due / due / claim 三处都不再到期(SQL 到期条件与 Python 口径共用同一 _DUE_SQL);API 创建与更新同步接受这两个字段。 刻意不向 LLM 自动注入工具指南:避免 prompt 臃肿,格式 只写进 docs/marker-tools.md 与 README,需要时由用户在 会话里手动告诉 agent;服务端不按关键词再猜意图。 单测 cloud/tests/test_session_goal_automation.py 12 项, 相关回归 218 项全过。
420 lines
20 KiB
Markdown
420 lines
20 KiB
Markdown
# 任务内标记工具(marker tools)
|
||
|
||
适用基线:main d129f7b 上的 feat-marker-tools 分支(本地提交,未推、
|
||
未建 PR、未部署)。本批只做四类标记工具,不要把本文档读成
|
||
“所有 Codex app 线程工具都已实现”:handoff、worktree 创建状态、
|
||
PR 检查诊断、clock-sleep 都不在本批;MCP 真工具路
|
||
(同轮函数返回)也不在本批。
|
||
|
||
新增(feat-mainline-goal 分支,本地提交,未推、未部署):
|
||
goal-convert 与 automation-create 两个标记工具已实现,见下面
|
||
新增的两节。注意:本批刻意**不**向 LLM 自动注入工具指南——
|
||
不靠服务端再猜关键词、也不让每个任务的 prompt 都变长;
|
||
agent 知道这两个工具的唯一途径,是用户在会话里手动告诉它
|
||
(例如把下面两节的格式贴给它),或由调用方在任务 prompt 里
|
||
自己写明。LLM 是否用它们、判成 goal 还是自动化,全由 LLM
|
||
按用户话里的意图决定,服务端不按“收 / 每天 / 明天”这类词
|
||
再做一次关键词判断。
|
||
|
||
## 一句话原理
|
||
|
||
agent 不用联网、不用凭证。agent 在回合输出里写标记块,本地与
|
||
Docker 执行器都经 sandbox 的结果解析提取标记块(与 task-message
|
||
同款两处解析),可信的 worker 在 completed 轮次后执行。
|
||
|
||
异步语义(必须先讲清,否则会被当成函数调用):
|
||
|
||
- 写块 → 回合结束 → worker 执行 → 报告拼成一条后续消息、
|
||
下一轮才回到父任务。不是同轮函数返回。
|
||
- 轮询与等待在 worker 执行期间循环查库、占 worker 槽位(与
|
||
协调员等齐同款代价);超时或父任务被取消时中止。
|
||
- 只有 completed 轮次的块被执行;失败 / 超时 / 取消 / 配额中断
|
||
的轮次不执行;父任务执行期间被取消时也不执行。
|
||
- 拒绝只记报告与审计、不炸任务、不改目标状态。
|
||
|
||
## 边界:谁能用、谁不能用
|
||
|
||
- 能用:普通任务。普通任务当项目经理、自己派生员工、轮询员工、
|
||
等一批员工、取消或重试员工,走的就是这批工具。
|
||
- 不能用:goal 任务与协调员任务不解析这些标记块(只收不发)。
|
||
协调员任务本身不跑模型回合,等齐循环也不会在中途排空后续
|
||
消息做判断,所以协调员 PM 不能因此获得任务内工具。
|
||
- 容易混淆的一点:协调员的子任务本身是普通任务(不是协调员
|
||
任务),它写标记块会被解析;它的派生代数从 0 计(见“派生护栏”)。
|
||
|
||
## 新增的两个工具:goal-convert 与 automation-create
|
||
|
||
### goal-convert:把本会话转成 goal
|
||
|
||
```text
|
||
```goal-convert
|
||
{"goal": "收 xxx 到 yyy 为止"}
|
||
```
|
||
```
|
||
|
||
- goal 必填、1-20000 字;每轮最多 1 块。转成的是新语义 goal
|
||
(goal_mode=1、无限轮直到模型自己输出“目标已达成”),
|
||
与服务端 /goal 转换同款,不接受 verify_command /
|
||
goal_max_rounds。
|
||
- 执行方式是排队,不是立即转:worker 把它排成一条
|
||
“/goal <目标>”的自后续消息,放在本轮其它工具的报告消息
|
||
之后——排空先跑报告那轮,再跑 /goal 那条走已有的 followup
|
||
/goal 转换路径(plan 冲突、CAS、终局 queued 都在那条路里)。
|
||
所以同轮其它标记块的报告不会丢在 goal 循环外。
|
||
- 只在普通任务里有效:goal 任务与协调员任务不解析标记块。
|
||
|
||
### automation-create:新建自动化定义
|
||
|
||
```text
|
||
```automation-create
|
||
{"name": "看 xxx 每日", "prompt": "查看 xxx 最新并汇总",
|
||
"interval_seconds": 86400,
|
||
"first_run_at": "2026-10-30T09:00:00+08:00",
|
||
"once": false, "bind": null, "repo_url": null}
|
||
```
|
||
```
|
||
|
||
- prompt 必填、1-20000 字;name 可选、1-100 字,缺失时
|
||
worker 自动生成(`auto-<任务短 id>-<时间>-<序号>`),不会因
|
||
重名撞上同项目已有的 active 自动化。
|
||
- interval_seconds 可选、缺省 86400,必须是整数且在 60 到
|
||
30*86400 之间;非法时整块跳过,不静默钳制。
|
||
- first_run_at 可选:epoch 秒(毫秒级自动除 1000)、ISO8601
|
||
字符串(带偏移量按偏移量算,无时区按 Asia/Shanghai 算)。
|
||
不给表示建完即到期(沿用自动化原有语义);给了表示首次
|
||
在该时刻跑——“每天 9 点”要写成 first_run_at=下一个 9 点 +
|
||
interval_seconds=86400,而不是只写间隔(只写间隔会从创建
|
||
时间起算,每天跑在创建时间那个钟点)。首次时刻已过去的块
|
||
在 worker 层拒绝并写进报告,agent 下一轮可纠正。
|
||
- once 可选、缺省 false;true 表示只跑一次,跑过后 is_due /
|
||
due / claim 三处都不再到期(automations 模块的
|
||
first_run_at / run_once 字段与 SQL 到期条件已同步改过,
|
||
API 的创建与更新也接受这两个字段)。
|
||
- bind 可选、缺省 null(到期独立跑新任务);"heartbeat"
|
||
表示绑到本会话,到期唤醒本线程;"cron" 与 null 同款。
|
||
- 每轮最多 5 块;创建人跟父任务走,须有项目 create_task
|
||
权限,配额超限时整轮跳过。创建只是登记定义,到期执行
|
||
仍走 automations 的 claim 门与外部触发器,不是建完就跑。
|
||
- 只在普通任务里有效:goal 任务与协调员任务不解析标记块。
|
||
|
||
## 四个工具的标记格式
|
||
|
||
### task-create:新建平级主线任务
|
||
|
||
```text
|
||
```task-create
|
||
{"name": "test-xxx", "prompt": "...", "model": null,
|
||
"effort": null, "executor": "docker"}
|
||
```
|
||
```
|
||
|
||
- 新任务是平级主线,不是子代理、不是 fork;不继承父任务历史;
|
||
环境基线继承父任务的 environment_id,但 worker 给它独立的
|
||
tenant/worktree,不动父任务的 cwd;父任务未提交的改动不会
|
||
自动带过去。
|
||
- prompt 必填、1-20000 字;name 可选、1-100 字(仅用于派生报告,
|
||
tasks 表没有 name 列,不落库、不改 prompt);model 不做白名单;
|
||
effort 只能是 low / medium / high / xhigh / max;executor 只能是
|
||
local / docker / cloud,未指定时继承父任务。
|
||
- 只建普通任务:不通过标记建协调员任务与 goal 任务。
|
||
- 每轮最多 5 块,多余的忽略;非法 JSON、prompt 缺失/空串/超长、
|
||
name 超长、effort/executor 非法时整块跳过;正文里的行首围栏
|
||
会把块截断成非法 JSON 而被跳过(与 task-message 同款非贪婪匹配)。
|
||
|
||
### task-poll / task-read:按任务 id 轮询阅读
|
||
|
||
```text
|
||
```task-poll
|
||
{"task_id": "<目标任务 id>", "interval_seconds": 5,
|
||
"timeout_seconds": 300, "chars": 2000}
|
||
```
|
||
|
||
```task-read
|
||
{"task_id": "<目标任务 id>", "wait_seconds": 30}
|
||
```
|
||
```
|
||
|
||
- task-read 是 task-poll 的别名;read-thread / read_thread 也是
|
||
同款别名。不传 wait_seconds 时立即读一次快照;传了则等这么久
|
||
再读一次(等价于 interval = timeout = wait_seconds)。
|
||
- interval 钳制 1-60 秒,timeout 钳制 0-3600 秒(0 = 立即快照),
|
||
chars 钳制 200-8000 字;各类块合计每轮最多 5 块。
|
||
- 轮询目标不存在、跨项目、轮询自己时,报告里写清拒绝原因,
|
||
不改任何状态。轮询目标是只读的,goal 任务也可以被轮询。
|
||
|
||
### wait-threads:一次等多个目标任务
|
||
|
||
```text
|
||
```wait-threads
|
||
{"task_ids": ["B", "C"], "interval_seconds": 5,
|
||
"timeout_seconds": 300, "chars": 2000}
|
||
```
|
||
```
|
||
|
||
- wait_threads 是同款写法;task_ids 也接受 thread_ids / threads
|
||
两个别名;去重保序。
|
||
- 每块最多 8 个目标,每轮最多 2 块。8 这个数是刻意与服务端
|
||
POST /tasks/{id}/wait 的“一次最多 8 个线程”对齐的;spawn
|
||
分支原先的每块 10 个在本批收紧到 8 个,不要再用 10。
|
||
- 并行查库:每轮查一次全部目标,不是逐个串行等超时相加;
|
||
全部终态或超时后把每目标快照一起排回父任务。
|
||
- 缺失、跨项目、等待自己的目标当场记为已处理(写清原因),
|
||
不让它们把整批拖到超时;其余目标继续等到终态或超时。
|
||
- 超时口径与服务端 /wait 不同,且是故意的:服务端 /wait 是给
|
||
外部调用方持 HTTP 连接等的,上限 120 秒;标记 wait 是 worker
|
||
在回合后直连库循环查,不持 HTTP 连接,超时钳制与 task-poll
|
||
同款(0-3600 秒),占的是 worker 槽位而不是 HTTP 连接。
|
||
两者不要混用、不要互抄上限。
|
||
|
||
### task-cancel / task-retry:取消 / 重试已存在的任务
|
||
|
||
```text
|
||
```task-cancel
|
||
{"task_id": "<目标任务 id>"}
|
||
```
|
||
|
||
```task-retry
|
||
{"task_id": "<目标任务 id>"}
|
||
```
|
||
```
|
||
|
||
- task_cancel / task_retry 是同款写法;task_id 也接受 to /
|
||
to_task_id / thread_id 别名;每种每轮最多 5 块。
|
||
- 取消复用 worker 现有的取消路径语义:取消后状态为 interrupted,
|
||
正在跑的进程组 / 容器按现有取消路径真杀,不只改库状态。
|
||
- 重试与服务端 POST /tasks/{id}/retry 对齐:仅 failed /
|
||
interrupted / merge-failed 可重试,重试时清空上一轮
|
||
result_text / error_text / actual_model、状态回 queued、
|
||
prompt 与模型等原样保留、记审计。重试本身不花模型钱,
|
||
重试后的新一轮照常计费。
|
||
- 执行顺序:创建 → 取消 → 重试 → 轮询 → 等待。创建在前让同一轮
|
||
刚创建的任务立刻能被轮询;控制操作在快照之前让轮询/等待读到
|
||
取消或重试后的新状态。
|
||
|
||
## 门禁(task-cancel / task-retry,缺一不可)
|
||
|
||
纯函数层(cloud/marker_tools.py check_control_gate)与 worker
|
||
权限层(worker.py _deliver_marker_tools)两道,拒绝时只记报告
|
||
与审计、不改目标状态:
|
||
|
||
1. 目标存在;同项目;不是自己。
|
||
2. 非 goal 任务(verify_command 非空或 goal_mode 为真都算 goal)。
|
||
3. 取消要求目标非终态;重试要求目标仅为 failed / interrupted /
|
||
merge-failed。已合并(merged)两者都拒绝;merge-failed 本身
|
||
是终态,所以取消拒绝、重试接受。
|
||
4. 关系二选一:目标在发送方派生链上(沿 parent_task_id 链最多
|
||
32 步能追溯到发送方),或目标 created_by 与发送方 created_by
|
||
相同。“同项目猜到 id 就能取消任何人的任务”在此被挡住。
|
||
5. 权限层:取消要求发送方创建人有项目 create_task 权限(与
|
||
服务端取消口径同款),重试要求有 approve_merge 权限(与
|
||
服务端重试口径同款)。权限检查本身抛异常时收紧到仅派生链内
|
||
可用,同创建人-only 的目标一律拒绝,并把原因写进报告。
|
||
|
||
## 派生护栏与费用
|
||
|
||
- 派生深度上限 3 代(父任务本身是派生任务时逐代计;协调员子任务
|
||
的代数从 0 计,见“边界”);单个父任务直接派生上限 20 个
|
||
(不含协调员子任务);超限的块跳过。
|
||
- 派生任务是独立主线:父任务完成或取消都不自动杀派生任务;
|
||
想停派生任务,用 task-cancel 或控制台 / API 显式取消它。
|
||
协调员孤儿收口只扫 coordinator_part 非空的协调员子任务,
|
||
不扫派生任务。
|
||
- 费用与配额:派生任务 created_by 跟父任务走,认领时照常过
|
||
key 门与配额门;创建时 worker 先查一次父任务配额,超限时
|
||
整轮创建跳过(排队本身不花钱,但不让超限方再批量排队)。
|
||
|
||
## 诚实边界(与服务端口径不一致处,逐条列出)
|
||
|
||
- 重试的 goal 拒绝比服务端严:服务端 POST /tasks/{id}/retry
|
||
没有单独的 goal 检查,标记重试为了不破坏 goal 循环语义,
|
||
明确拒绝 goal 目标。这是标记口径比服务端口径严,不是遗漏。
|
||
- 取消的 goal 拒绝同理:服务端取消也没有单独的 goal 检查,
|
||
标记取消同样拒绝 goal 目标。
|
||
- 服务端重试是双人门(触发人与原任务创建人任一超限即拦,
|
||
且触发人须有 approve_merge 权限);标记重试的触发方是发送
|
||
方任务本身,其 created_by 与派生任务的 created_by 天然相同
|
||
(派生时跟父任务走),所以 worker 侧只查发送方创建人这一道
|
||
approve_merge,效果与双人门在派生链内等价;同创建人-only
|
||
的目标同样只查这一道,权限查不到时则收紧到仅派生链内。
|
||
- wait 的超时口径与服务端 /wait 不同(见上文),数量口径已
|
||
对齐到 8 个;不要把标记 wait 的 3600 秒上限套到 HTTP /wait 上。
|
||
- 本批没有 MCP 真工具:标记工具永远是异步的(下一轮才拿到
|
||
报告),做不到同轮函数返回;想做同轮返回要另搭任务级鉴权的
|
||
MCP 服务器,不在本批。
|
||
- 签名门残留:worker 的 _code_signature 只签 worker / sandbox /
|
||
coordinator / cost / server / host 共 6 个文件,新模块
|
||
marker_tools.py 不在签名候选里。本批部署时 worker.py 与
|
||
sandbox.py 本身都变了,签名门照常生效;但将来只改
|
||
marker_tools.py 的补丁不会单独触发签名门,届时要么把
|
||
marker_tools.py 加进签名候选并同步改单测的 6 个断言,要么
|
||
接受这一处缺口——本批不顺手改签名,避免动无关安全测试的
|
||
预期。
|
||
- spawn 分支的未提交半成品没有原样合并进来:本批只移植了
|
||
已提交的 task-create / task-poll / task-read 实现与未提交的
|
||
wait 解析(收紧到 8 个后接通执行),handoff / worktree /
|
||
PR 检查 / automation / clock-sleep 的解析与 send-message-to-thread
|
||
别名均未移植;spawn 工作区本身未动。
|
||
|
||
## 关键实现位置
|
||
|
||
- cloud/marker_tools.py:纯逻辑(解析、校验、快照格式化、
|
||
门禁、派生代数与直接上限计数),单测不需要 worker/server。
|
||
- cloud/sandbox.py:本地 _result_from_runner 与 Docker
|
||
_parse_host_result 两处提取标记块,worker 侧按结果
|
||
dict 里的 task_creates / task_polls / wait_threads /
|
||
task_cancels / task_retries / todo_lists / todo_creates /
|
||
todo_updates 执行。
|
||
- cloud/worker.py:_deliver_marker_tools(5 处投递点,与
|
||
_deliver_peer_messages 同款位置)、_poll_task_snapshot、
|
||
_wait_task_targets、_queue_self_followup、_marker_auth。
|
||
- cloud/tests/test_marker_tools.py:解析上限与钳制、非法块跳过、
|
||
跨项目拒绝、自操作拒绝、已终态拒绝、goal / 已合并拒绝、
|
||
重试仅三态且清空旧结果、wait 多目标终态与超时快照、派生
|
||
深度与直接上限、权限检查失败收紧。
|
||
|
||
## 待办标记工具(todo-list / todo-create / todo-update)
|
||
|
||
与上面四类同款异步语义:写块 → 回合结束 → worker 执行 →
|
||
报告拼成后续消息、下一轮才回到父任务。goal 任务与协调员
|
||
任务不解析(只收不发),所以首轮待办注入也跳过 goal 任务,
|
||
避免模型写出永远不会执行的 todo-update 块。
|
||
|
||
### todo-list:查本项目待办
|
||
|
||
```text
|
||
```todo-list
|
||
{"status": "pending", "q": "文档"}
|
||
```
|
||
```
|
||
|
||
- 不传 status 时只返回未完成项(pending / in_progress /
|
||
blocked),查已完成项须显式传 status;q 是标题/描述子串。
|
||
- 每轮最多 5 块;status 非法、q 非字符串或超长时整块跳过。
|
||
- 读权限:发送方创建人须有项目 read 权限,否则报告里写清
|
||
拒绝;报告最多列 20 条,超出写明还有几条。
|
||
|
||
### todo-create:新建待办
|
||
|
||
```text
|
||
```todo-create
|
||
{"title": "写接口文档", "priority": "high"}
|
||
```
|
||
```
|
||
|
||
- title 必填 1-500 字;description 可选 <=20000 字;
|
||
status / priority 只能是枚举值;due_at 是 epoch 秒,
|
||
>=1e11 按毫秒除 1000;task_id 关联的任务必须在本项目内,
|
||
否则整块拒绝;sort_order 整数可选。
|
||
- 每轮最多 5 块;写权限:发送方创建人须有 create_task
|
||
权限,否则拒绝。
|
||
|
||
### todo-update:改待办(最常用是改状态)
|
||
|
||
```text
|
||
```todo-update
|
||
{"id": "<待办 id>", "status": "completed"}
|
||
```
|
||
```
|
||
|
||
- id 必填,至少改一个字段;各字段校验与 todo-create 同款,
|
||
未知字段忽略;行不存在或属于别的项目时按“不存在”拒绝,
|
||
不改任何状态;completed 记 completed_at,离开 completed
|
||
清掉(复用 todos 模块状态机)。
|
||
- 每轮最多 5 块;删除不走标记块(硬删除只在控制台 /
|
||
REST DELETE 里做,避免模型误删整条)。
|
||
|
||
### 首轮注入(worker _apply_todo_injection)
|
||
|
||
- 普通任务被认领的首轮,worker 在内存副本的 prompt 前贴
|
||
一份本项目未完成待办(最多 20 条、标题截断 100 字),附
|
||
三行 todo-update 示例;原 prompt 不落库,续跑与断点续跑
|
||
不重复贴,协调员与 goal 任务跳过,无未完成待办时不贴空
|
||
章节,注入查询失败时静默跳过、不炸认领。
|
||
- 注入贴的是数据,不是完整工具指南;完整格式只在本文件里。
|
||
|
||
## 任务列表标记(task-list)与项目注入
|
||
|
||
### task-list:查本项目任务
|
||
|
||
```text
|
||
```task-list
|
||
{"status": "failed", "q": "登录", "limit": 20}
|
||
```
|
||
```
|
||
|
||
- 不传 status 时查全部状态(与 todo-list 默认只查未完成
|
||
不同——分诊首先要看到失败与运行中的任务),报告附各状态
|
||
计数;q 是 prompt 子串;limit 钳制 1-50(默认 20),
|
||
超出写明还有几条;列表按新建倒序。
|
||
- 每轮最多 5 块;status 非法、q 非字符串或超长、limit 非
|
||
整数时整块跳过;只读,发送方创建人须有项目 read 权限,
|
||
否则报告里写清拒绝;跨项目任务不出现。
|
||
- 只读不改状态:捡到失败任务后改状态走 task-retry /
|
||
task-cancel,合并与审批仍由人操作。
|
||
|
||
### 首轮项目注入(worker _apply_project_injection)
|
||
|
||
- 普通任务与 goal 任务被认领的首轮,worker 在内存副本前
|
||
贴三行项目上下文:项目名、仓库(未绑定时明写未绑定,
|
||
不编地址)、环境(有 environment_id 时写环境名/分支/
|
||
状态,否则写无);再加一行 task-list 格式示例。
|
||
- goal 任务同时改内存里的 goal(执行器取 goal 优先于
|
||
prompt,只改 prompt 的话 goal 任务看不到);原 prompt /
|
||
goal 不落库,协调员、续跑与断点续跑跳过,项目行查不到
|
||
或查询失败时原样返回、不贴半截上下文、不炸认领。
|
||
- 贴的是数据加一行格式示例,不是完整工具指南;完整格式
|
||
只在本文件里。注入顺序:项目上下文在最前,其次待办、
|
||
记忆,最后是原 prompt。
|
||
|
||
## 成本、配额与文件标记(task-cost / quota-status / task-files)
|
||
|
||
### task-cost:查单个任务成本(只读)
|
||
|
||
```text
|
||
```task-cost
|
||
{"task_id": "<目标任务 id>"}
|
||
```
|
||
```
|
||
|
||
- 报告:美元数(只计有价行)、总 token(actual / estimated
|
||
分列)、轮次;无价 actual token 单独列,不会被静默算成
|
||
0 美元(与 CostStore._report 同款口径)。
|
||
- 门禁比服务端只要求 read 更严:目标须在本项目内,且在
|
||
派生链内或与发送方同创建人(自己也算同创建人),防止在
|
||
项目内猜 id 看别人的花费;每轮最多 5 块。
|
||
|
||
### quota-status:查发送方创建人当月配额(只读)
|
||
|
||
```text
|
||
```quota-status
|
||
{}
|
||
```
|
||
```
|
||
|
||
- 块体必须是 JSON 对象,不接受任何参数:配额永远按发送方
|
||
任务的创建人算(创建人缺失时回查父任务,仍查不到时拒绝),
|
||
块里写别人 id 也无效。报告:总额、已用、剩余、无价
|
||
actual token;显式不限额时明写不限额,不编一个数字。
|
||
- 每轮最多 5 块;发送方创建人须有项目 read 权限,否则拒绝。
|
||
|
||
### task-files:读派生链内或同创建人任务的文件(只读)
|
||
|
||
```text
|
||
```task-files
|
||
{"task_id": "<目标任务 id>", "path": "out.txt"}
|
||
```
|
||
```
|
||
|
||
- 不传 path(或空串)时列顶层(只列前 50 项,目录名带 /);
|
||
传相对路径读文件,上限 20000 字,超出写明截断。
|
||
- 门禁:目标须在本项目内且在派生链内或同创建人;cloud
|
||
任务与 worktree 在别机的任务拒绝(文件不在本机);目标
|
||
没有本地 worktree 时按“没有本地 worktree”报告,不编内容。
|
||
- 路径安全:绝对路径、.. 逃逸、符号链接逃逸一律拒绝并在
|
||
报告里写清;二进制文件(解码失败或含 NUL 字节)拒绝;
|
||
worktree 解析只接受已存在的任务级目录(租户路径或
|
||
host.py 同级 work/host-worktrees),绝不把共享挂载点本身
|
||
当成目标任务的目录。
|
||
- 每轮最多 5 块;删除、写文件不走标记块(模型改文件走它
|
||
自己的执行器工作区,不是读别人 worktree 的路)。
|