cmd

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 47 Imported by: 0

Documentation

Overview

本文件枚举本机可广告的单播地址,供 init 配对片段的 addr 使用。

职责:

  • 从网卡地址里筛出可给对端抄的 IPv4(排除 loopback / link-local)
  • 把 listen 上的通配地址换成排序后的第一条,拼进配对 addr

边界:

  • **不写 listen、不改配置**:探到的 IP 只出现在配对片段。绑到某一张 网卡会让 127.0.0.1 连不上,DHCP / Tailscale 一变 agentd 也起不来。
  • 本期只要 IPv4;IPv6 留给后续(本仓库远程场景是 Tailscale CGNAT)

本文件实现 handoff agentd 子命令:加载配置、初始化统一日志、打开 SQLite 存储、 构建 HTTP/WS 服务并监听。agentd 是本机/配对主机上的长驻服务,是任务的执行入口。

职责:

  • 按序完成 bootstrap:config.Load → logx.Setup + slog.SetDefault → pathenv.Apply(PATH 补全,先于一切 fork 子进程)→ store.Open → agentd.NewServer
  • 对外服务前做启动恢复(RecoverOnStartup):探活未终结任务的执行器,重建订阅或转 failed
  • 启动任务卡住看门狗 goroutine(RunWatchdog),长时间无事件产出触发 stalled 唤醒协调者
  • 监听配置中的 Listen 地址,进程生命周期与 HTTP server 一致
  • 经 agentd.Shutdown 提供优雅关停:SIGINT/SIGTERM 停收新连接 → 等在途请求 → 停看门狗 → 关库 → 放锁;正常关停 exit 0,供进程管理器据此拉起新版

边界:

  • 不创建任务/工单:任务生命周期由 manager 驱动(executor 按 --executor 挂载)
  • 不决定何时停机:信号与进程内触发都汇到 agentd.Shutdown,本文件只接线

本文件实现 handoff attach 子命令:在终端跟随任务实况。

职责:

  • 从 agentd 的 render 流式接口取任务实况,原样打印到 stdout,Ctrl+C 退出

边界:

  • 不解析实况内容:render.log 是模型回合文本原样增量,这里只做搬运
  • 不连 executor、不碰任务目录:一切经 agentd 的 HTTP 接口

为什么不再 exec 外部命令:旧实现用 syscall.Exec 换进程进 tmux(本机), 或 ssh -t <host> tmux attach(远程)。tmux 拆除后实况改由 agentd 落盘 + 流式吐出,attach 退化成一个普通 HTTP 客户端——顺带拿到三个收益: 远程不再需要 ssh(复用 agentd 连接与鉴权,配置里的 user 字段对 attach 不再必要)、 Windows 协调者可用(syscall.Exec 在 Windows 上直接返回 EWINDOWS)、 断线可凭已收字节数续传。

本文件实现 handoff continue 子命令:向任务续发修改指令。

职责:

  • 把协调者的修改指令经 client.Continue 原样透传给 executor(同一会话续接, 上下文完整保留;任务必须处于 waiting_review)
  • 成功时单行输出 {"ok":true}(供上层脚本解析)

边界:

  • 不解释指令语义,原文透传;任务状态校验由 agentd 判定并返回错误

本文件实现 handoff diff 子命令:取任务分支相对基准分支的审阅素材(git diff + 提交列表)。

职责:

  • 调 client.Diff 拉取 diff 文本并原文输出到 stdout(协调者阅读/管道分析用)

边界:

  • 不做 diff 语义判断;基准分支可经 --base 指定,缺省由 agentd 按仓库默认分支推导

本文件实现 handoff dispatch 子命令:把本地 plan 文件(或 --prompt 直接指令) 派发到 agentd 执行。

职责:

  • 读取本地 plan 文件并 base64 编码,连同项目身份/计划名/target/执行者/模型/ 分支/worktree 等参数一并 POST 给 agentd(body {project_id, plan_b64, prompt, ...})
  • 派发的项目由 cwd 识别:读当前目录 git 仓库的 origin 离线算出 project_id, cwd 不是目标项目时用 --project <名字> 显式指定
  • 远程派发时采集本地 HEAD 作基线随请求上送,并校验本地工作区完整性 (已跟踪改动拒发、未跟踪警告;--no-sync-check 关掉整块,--allow-dirty 只关拒发)
  • 派发成功后在 stderr 打一行基线摘要(起点短号 + 任务仓库领先的提交数)
  • 派发成功后在 stderr 提示执行机仓库的未提交改动(managed 工作树不含它们)
  • 成功时单行输出任务 JSON(state=running,供上层脚本解析任务 id)

边界:

  • 只做文件读取与上传,不校验计划内容语义(解析与执行由 executor 负责)
  • --no-terminal 在本文件只注册 flag 并参与「是否弹终端」的判定骨架; 弹终端默认**不弹**(cfg.Terminal.Auto 默认 false),配置 auto: true 时 才在 darwin 弹窗,--no-terminal 用于逐次关闭

本文件实现 dispatch 的本地工作区完整性校验(backlog B29)。

职责:

  • 把 git status --porcelain 的输出分成「已跟踪改动」与「未跟踪文件」两类
  • 已跟踪改动拒发(--allow-dirty 可放行),未跟踪只警告
  • 全部提示走调用方给的 stderr writer

边界:

  • 只看当前工作目录(cwd)这一棵树;agentd 侧任务仓库的脏检查是另一回事, 由 internal/agentd 的 ensureCleanWorktree 负责,两者互不替代
  • 不发起任何网络请求:拒发必须发生在 HTTP 请求之前
  • 不解释 git 的退出码:status 本身失败时降级放行,不把派发挡死

本文件实现 handoff done 子命令:归档任务。

职责:

  • 审核通过后调用 client.Done 把任务置为 completed 并回收 executor(任务必须 处于 waiting_review)
  • 成功时单行输出 {"ok":true}(供上层脚本解析)
  • 携带可选完成说明(--note)并在 stderr 提示保存结果

边界:

  • 不做 push 等归档后动作(按任务配置决定是否 push 不在 MVP 范围)
  • 不做说明内容的校验与加工(只校验长度)

本文件定义 CLI 的退出码语义:把「失败的类别」编码进进程退出码。

职责:

  • 提供 exitCodeError 包装,让特定失败带上专属退出码
  • 提供 ExitCode,供 main 把 Execute 返回的错误换算成退出码

边界:

  • 不打印任何东西(错误文本由 cobra 打到 stderr)
  • 不决定「什么算失败」,只决定「这次失败对外表达成几号」

本文件实现 handoff fetch 子命令:读取任务仓库内文件内容(审核取上下文用)。

职责:

  • 调 client.Fetch 拉取仓库内相对路径文件并原文输出到 stdout

边界:

  • 不修改文件;路径由协调者指定,逃逸路径由 agentd 拒绝

footprint.go —— `handoff footprint` 命令:体检全部任务的进程足迹。

职责:

  • 拉取对端全部任务(含已归档)的进程占用与判定结论并渲染

边界:

  • **只数不杀**:本命令不回收任何进程。清扫由 agentd 在 executor 判死时 自动完成(见 spec §3.4),本命令只负责让人看见
  • 不改任何任务状态、不发事件

本文件实现 handoff init 子命令:一台新机器的问答式配置。

职责:

  • 探测四家 executor 的状态并成表打印
  • 按角色分支问配置问题,把答案写进 config.yaml
  • 末尾打印本机 token 与现成的配对 yaml 片段

边界:

  • **不发起任何真实模型调用**:探测一律用轻量本地判据(见 internal/toolchain)
  • **不主动装服务,但会问**:角色含执行机且 stdin 是终端时,init 会追问一句 是否托管,答 y 则调 installService(与 handoff service install 同一条路径)。 托管是「重启后 agentd 还回得来」的唯一保障,只留一行提示的触达率不够(B71)。 Linux 上非 root 时一律不代跑,只打印 sudo 命令
  • **不阻断任何选择**:探测结果只影响默认值与标注;没装任何 executor 也能配完 (纯协调者机的正常情况),选了「未登录」的执行者只警告不拦
  • stdin 非 tty 时一问不问:init 会被 install.sh 经管道调起,问了没人答, 卡住比不问糟得多

本文件是 init 在真终端上的 huh 问答实现。

职责:

  • 用 huh 的 Select / Input / Confirm 实现 prompter
  • 把用户取消(Ctrl-C、huh.ErrUserAborted、context 取消)译成 errPromptCanceled

边界:

  • **只服务 TTY**:测试不得走这里。CI 没有真终端,huh 会挂死; 测试经 newInteractivePrompter 缝换成脚本化实现
  • **取消 / 失败绝不写配置**:本文件只返回错误。写盘是 RunE 的事, 见到错就不 Save,避免留下一份只配了一半的 config.yaml
  • 不负责问题集合;问什么仍由 init.go 的 askAll 决定

本文件实现 handoff permission-mcp 隐藏子命令:Claude Code 的权限裁决 MCP server。

职责:

  • 以 stdio JSON-RPC 提供一个 ask 工具,claude 经 --permission-prompt-tool 调用它
  • 把每次授权请求经 unix socket 转给 agentd 侧的 adapter,阻塞等待人工/审批者裁决
  • 把裁决还原成 claude 认识的 {"behavior":"allow"|"deny"} 返回

边界:

  • 不读 handoff 配置、不连 agentd HTTP:唯一对外面就是 --sock 指定的路径, 被监管的 executor 因此拿不到 agentd token
  • 不做任何审批判断:连不上就一直重试等待,绝不自作主张放行(fail-closed)

为什么是隐藏子命令而不是独立二进制:claude 侧只需要一个可执行文件路径, 复用 handoff 自身避免了额外分发与版本漂移。

日志例外(本文件唯一允许不用 slog 的地方):stdout 是 JSON-RPC 通道,任何 非协议内容混入都会让 claude 侧解析失败,且本进程是被 claude 拉起的短命子进程, 不接 agentd 的 logger——诊断只能走 stderr 的 fmt.Fprintf。

本文件实现 handoff project 子命令族:把一个项目登记到本机与(可选的)一台 远程开发机上,并维护「项目 × 机器」的位置表。

职责:

  • project add:把 cwd 登记为本机位置;--target 时一并登记到那台机器
  • project ls:列出位置,并显示每条的实际状态(登记与磁盘漂移时看得见)
  • project rm:注销位置

边界:

  • 不自己 ssh、不自己 clone:clone 由目标机上的 agentd 执行,用它自己的 git 凭据
  • 不删磁盘上的仓库:rm 只删登记
  • 不决定「项目在那台机器的哪个目录」:远程落点由那台机器的 repo_root 决定, 本机一个远程细节都不需要知道(spec §6.2)

本文件是 init 问答的通道:接口 + 按行读答案的脚本化实现。

职责:

  • 定义 prompter(Select / Input / Confirm)
  • 提供 scriptedPrompter:从 Reader 按行读,空行 / EOF 取默认 (测试与 CI 用;真终端走 init_huh.go)

边界:

  • **不写配置**:只返回用户(或脚本)的答案,不碰 config.yaml
  • **不探测工具链**:选项列表由调用方传入,这里不调 toolchain.Detect
  • 不负责问题集合;问什么仍由 init.go 的 askAll 决定

本文件实现 handoff pull 子命令:把远程执行机上的任务分支同步到本地仓库。

职责:

  • 查任务拿到 target/仓库路径/分支,换算出 ssh 形式的远程地址并 fetch 到本地同名分支

边界:

  • 只 fetch,不 checkout、不合并(合并是协调者的决定)
  • 本机任务(无 target)无需同步:代码本来就在同一台机器上

reclaim.go —— handoff reclaim 子命令:回收终态任务残留的 managed worktree。

职责:

  • 无参:列出仍占着 managed worktree 的终态任务(净/脏/元数据残留/判不出)
  • 带任务 id:回收那一个;脏树默认拒绝并报出改动清单,--force 才强删

边界:

  • 不删任务分支(协调者的工作成果),每次成功输出都明说这一点
  • 不删任务目录(失败任务的排查素材还在里面)
  • 不改任务状态:回收前后 handoff show 看到的状态一致

本文件实现 handoff reply 子命令:回答一个待办工单(权限门/提问)。

职责:

  • 把协调者的裁决转成应答原文:--approve → "allow"、--deny [--reason] → "deny[:原因]"、 --answer 原样透传,经 client.Reply 交给 agentd
  • 成功时单行输出 {"ok":true}(供上层脚本解析)

边界:

  • 不解释应答语义:answer 原样落库,含义由 manager 侧解释(allow → once / 其余 → reject)
  • 不校验任务状态(工单是否存在、已回答等由 agentd 判定并返回错误)

本文件实现 handoff resume 子命令:解开卡死的任务。

职责:

  • 调用 client.Resume 让 agentd 重投「已落库但未送达 executor」的应答, 并对断连窗口内丢失的回合终态做会话对账(B38)
  • 原样输出恢复报告 JSON(重投条数 / 对账结果 / executor 是否已不在 / 收尾状态 / 结论)

边界:

  • 不自己判断任务是否卡死,也不改任何状态:判定与收尾全在 agentd 侧 (Manager.RecoverStuck),CLI 只负责发起与呈现
  • 与 continue/done 的分工:那两条要求任务已在待审核;本条专治两类中间态 ——「reply 拿到 502 之后 reply/continue/done 三条路全封死」,以及 「agentd 与 executor 断连期间回合已完结、终态事件丢失、任务冻死在 running」

Package cmd 提供 handoff 的 cobra 命令行入口。

职责:

  • 定义根命令与全局 flag(--agentd / --target / --config)
  • 提供 TargetEndpoint 辅助函数,供各子命令换算实际 agentd 端点

边界:

  • 不包含具体业务逻辑(dispatch/gate 等子命令由后续任务补充)
  • 不在此处初始化日志,由各子命令按需调用 logx.Setup

本文件实现 handoff run 子命令:在任务仓库远程执行审阅命令(跑测试/lint)。

职责:

  • 把命令原文透传给 agentd 执行(sh -c,10min 超时),合并输出原文打印; 非零退出码以错误返回(cobra 打印到 stderr),输出已先行打印

边界:

  • 只透传命令,不解释输出语义;命令执行于任务仓库,由 agentd 限时回收

本文件实现 handoff service 子命令:把本机 agentd 交给进程管理器托管。

职责:

  • install:解析当前二进制与配置路径,生成并安装服务单元,复核起来了
  • uninstall:停止并移除单元
  • status:报告托管状态

边界:

  • 不启动/停止 agentd 进程本身:那是管理器的事,本命令只管单元
  • 不改 handoff 的配置文件:托管与配置是两件事,配置走 handoff init
  • 托管之后 agentd 的形态会变:手动 Ctrl-C 会被管理器拉回,停服务要用 systemctl stop / launchctl bootout。install 成功时会把这句打给用户

本文件实现隐藏子命令 handoff _shim:执行者进程的承载壳。

职责:

  • 解析 --spec,把控制权交给 prochost.RunShim(阻塞到执行者退出)

边界:

  • 不做任何业务判断:全部逻辑在 prochost.RunShim 里,本文件只是 cobra 包装
  • 不面向用户:Hidden=true,不出现在 help 里。它由 agentd 自己拉起, 人手动跑没有意义(缺 spec.json 就什么都做不了)

本文件实现 handoff show 子命令:输出任务的完整现场快照。

职责:

  • 调用 client.Attach 拉取任务 + 待办工单 + 最近事件,单行输出完整 AttachInfo JSON—— pending_tickets 是协调者恢复现场(「我还没答哪些」)的关键数据源

边界:

  • 只读快照,不修改任何状态
  • 二期起快照命令从一期 attach 更名而来:attach 改为终端实况(见 attach.go), 本命令是协调者会话恢复的关键数据源,供 wait/tasks/show 之外的脚本解析

本文件实现 handoff skill:报告与同步内嵌 skill 的安装状态。

职责:

  • handoff skill:逐落点报告是否与当前二进制一致
  • handoff skill install:把内嵌内容装到本机各家 agent

边界:

  • 不含安装逻辑本身(在 internal/skill):本层只做参数、打印与退出码
  • 不装到远端:skill 服务于协调者,协调者在本机

本文件实现 handoff status 子命令:一条命令回答「这个 agentd 能不能用、是什么」。

职责:

  • 调 client.Status 取服务端聚合结果,渲染人读文本(默认)或 JSON(--json)
  • 把老 agentd 的 404 直译成一条**成功的**诊断结论
  • 退出码只回答「能不能用」:0=可达且鉴权通过,1=够不着

边界:

  • 不做探活:判据在各 adapter 里,服务端已经做完,本层只渲染
  • 不因两边版本不一致而阻断:handoff 没有兼容矩阵,revision 不同不等于 不兼容,并列报出交给人判

本文件实现 handoff stop 子命令:主动中止一个还在跑的任务。

职责:

  • 调用 agentd 的 stop 路由,停 executor、作废挂起工单、任务落 failed
  • 依据响应体 worktree_removed 打印与实际行为一致的提示:managed worktree (agentd 建的)已删则如实告知,用户自带 worktree / 原地模式则说明保留

边界:

  • 不删任务分支(那是协调者的工作成果,审阅/回滚仍可切回分支)
  • 不做「停完再重派」:重派是独立决定,由协调者显式 dispatch

本文件实现 handoff tasks 子命令:列出全部任务。

职责:

  • 调用 client.ListTasks 拉取任务列表,每行输出一个任务 JSON(供上层脚本逐行解析)

边界:

  • 只做列表展示,不做任何状态判断与筛选

本文件实现 handoff upgrade:一条命令巡检并升级本机与全部 target。

职责:

  • 不带参数(或 --check):巡检表——列出所有机器的版本与结论
  • --now:升级所有落后的机器(远端全部处理完,本机最后);--target 只升那一台
  • --force:越过闸一(活跃任务)。**永不越过闸二(非托管)**
  • --rollback:本机回滚(不接 --target,回滚是单机应急动作)

数据流(spec §4.2):本机下载各机平台的资产并校验 → POST /api/update 把 tar.gz 原文推给远端(执行机无需出网)→ agentd 复检两道闸、再校验、解包、 自检、原子换版 → 触发优雅关停由进程管理器拉起新版 → 本 CLI 轮询 status 确认新版本上线。

边界:

  • 会通过接口触发 agentd 重启(本机最后:它会重启操作者正用着的 agentd)
  • 部分失败不中断其余:机器之间没有事务关系,逐行报告,任一台失败退出码非零
  • 处置建议必须对症:非托管不给 --force(它不越过闸二),够不着只报原文不编处置

本文件是 handoff upgrade 的**唯一判据来源**(B64)。

职责:

  • 把一台机器的探测结果收敛成单一结论(verdict)
  • 结论之间的优先级在此定义一次,供只读巡检与 --now 两个消费方共用

边界:

  • 纯函数:不做 I/O、不打日志、不产出面向操作者的文案
  • 不判 busy:活跃任务是「要不要现在换」的闸,不是「这台机器是什么状态」的 结论;它只在 verdictNeedsUpgrade 之后由 process 施加(spec §4.3)

为什么必须只有一处:B64 的病根是 renderCheckRow 与 process 各维护一套分支表, 两套的分支集合与优先级不一致,于是同一台机器有两套说法。

本文件实现 handoff version 子命令:打印本二进制的版本标识。

职责:

  • 首行输出纯版本字符串,供机器精确比对
  • 其后输出 revision / Go 版本 / 平台三行,供人排障

边界:

  • 不联网、不读配置文件:这条命令只回答「我是谁」。它必须能在一台刚装完、 还没有 ~/.handoff/config.yaml 的机器上跑通
  • **首行格式是对外契约**:B54.3 的自更新自检会拉起新下载的二进制跑本命令, 把首行与期望 tag 精确比对(见 spec §4.6 步骤 ⑤)。改这一行的格式等于改 协议,必须同步改自检侧

本文件实现 handoff wait 子命令:阻塞等待任务的下一个可动作事件并输出单行 JSON。

职责:

  • 调用 client.WaitEvent(progress 不唤醒、断线自动退避重连、cursor 续拉), 事件到达时把完整事件 JSON 单行输出到 stdout(供上层脚本解析)
  • --notify:事件到达时发 macOS 系统通知(spec §7 风险#4 的兜底:协调者会话 不在时提醒其重新拉起),失败仅 Warn 不影响主流程
  • 收到 SIGINT(Ctrl+C)时由进程默认行为终止,WaitEvent 随 ctx 取消退出
  • 任务结束事件到达时自动同步远程任务分支到本地(输出走 stderr,不污染 stdout 的事件 JSON 契约)
  • --follow:持续订阅同一任务的事件流,每条事件单行输出,直到任务终结 (failed 事件或被 done 归档)。此模式下 --timeout 的语义是**空闲**上限 ——距上一次收到任何帧(含被过滤掉的 progress)的时长,且跨重连累计
  • --follow 每次建连前先对账:本机 cursor 之后有积压时吐**一行** backlog_summary (带 missed/stale/actionable),把 cursor 推到当前水位,积压事件不再逐条重放 ——stdout 每行是一次会话唤醒,逐条重放会把一次重连变成 N 次唤醒

边界:

  • 不做事件语义判断与审批(审批在协调者脑中),事件原样输出
  • 不覆盖「协调者会话被关闭」:Monitor 是会话级的,会话没了订阅就没了, 本命令给不出任何补救(spec §7.2 明确接受的边界)

Index

Constants

View Source
const (
	ExitFailure = 1
	ExitTimeout = 124
)

退出码约定。

为什么不全用 1:wait 的无人值守场景(cron/脚本挂在后台等唤醒)拿不到 stderr, 只能看退出码。全是 1 的话,「等满了时限」与「token 没同步导致鉴权失败」这两件 处置完全不同的事,脚本无从区分——前者该继续等,后者该立刻报警。 124 沿用 timeout(1) 的惯例,也与 handoff run 里被杀命令的退出码一致。

Variables

This section is empty.

Functions

func Execute

func Execute() error

Execute 运行根命令,错误返回给 main。

func ExecuteContext

func ExecuteContext(ctx context.Context) error

ExecuteContext 是带 ctx 的 Execute(同样先清理单次执行的残留状态)。

参数:

  • ctx: 传给命令 RunE 的上下文(取消即中断长驻命令,如 wait)

func ExitCode

func ExitCode(err error) int

ExitCode 把 Execute 返回的错误换算成进程退出码。

参数:

  • err: Execute 的返回值(nil 表示成功)

返回:

  • 0(成功)、错误自带的专属退出码,或通用失败码 ExitFailure

func LocalEndpoint

func LocalEndpoint() (addr, token string, err error)

LocalEndpoint 返回**本机** agentd 的地址与令牌,忽略 --target。

返回:

  • addr: 本机 agentd 完整地址(含 http:// 前缀)
  • token: 本机令牌
  • err: 配置加载失败或本机 token 为空时返回

为什么需要它而不是复用 TargetEndpoint:登记是**两跳**(本机 + 目标机, spec §6.1),而 TargetEndpoint 读的是包级 targetName,指定了 --target 时 拿不到本机端点。两跳都要发,就必须有一个不受 --target 影响的取端点入口。

func SetSkillContent

func SetSkillContent(s string)

SetSkillContent 由 main 在启动时注入内嵌的 skill 全文。

func TargetEndpoint

func TargetEndpoint() (addr, token string, err error)

TargetEndpoint 根据 --target / --agentd / --config 换算实际请求的 agentd 端点与令牌。

参数(读取全局 flag):

  • --target 为空(本机模式):token 一律取本地配置 cfg.Token(服务端无条件要求 Bearer,无 token 的本机调用必然 401);地址由 localDialAddr 决议(loopback 照拨,通配/单网卡改拨 127.0.0.1,B85);显式 --agentd 优先;cfg.Token 为空时 返回错误
  • --target 非空:从配置 Targets 中查出 addr/token(远程配对)

返回:

  • addr: agentd 完整地址(含 http:// 前缀)
  • token: 访问令牌
  • err: 配置加载失败、target 未定义或本机 token 为空时返回

Types

type Endpoint

type Endpoint struct {
	Name  string
	Addr  string
	Token string
	Local bool
}

Endpoint 是一台可被升级的机器。

Local 为 true 时 Name 恒为「本机」:它的二进制由 CLI 直接换(文件就在本地), 与远端走的是两条不同的路径(spec §4.2)。

func Endpoints

func Endpoints(only string) ([]Endpoint, error)

Endpoints 返回要处理的机器清单。

参数:

  • only: 为空时返回 [本机, 全部 target(按名字排序)];非空时只返回该 target

返回:

  • 机器清单;only 指定的 target 不存在时返回错误

为什么本机也在清单里:版本一致本身就是要解决的问题,把本机排除在外, 「本机新远端旧」就会成为常态。而操作者不必记住配置里有哪些 target—— 这正是「一条命令看清所有机器」的前提(spec D2)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL