Documentation
¶
Overview ¶
本文件是「首次配置问什么」的字段描述表:每个字段的控件类型、标题、选项、 适用角色、显隐条件与随答案变化的默认值,全部表达成数据。
职责:
- 描述首次配置要问哪些字段、按什么顺序、在什么条件下显示
- 提供字段表的求值:Visible(显隐)、DefaultOf(随答案变的默认值)、 Apply(校验答案并写回 *config.Config)
边界:
- **不描述怎么问**:不碰终端、窗口或任何 UI 形态,也不调用 Prompter
- **不落盘**:Apply 只改内存里的 cfg,Save 由调用方决定
- 不改 AskAll:AskAll 仍由 initflow.go 持有,本文件只是把它的字段与分支 规则抽成数据,供 CLI 与桌面壳共用同一份真相
Package initflow 持有 handoff 首次配置的问答逻辑:问什么、按什么顺序问、 默认值怎么算、角色如何分支。
职责:
- 提供 AskAll:按角色分支问完全部问题,就地改写 *config.Config
- 提供默认值与选项的纯函数(DefaultRole / ListenPreset / ExecutorOptions / RoleOptions)
边界:
- **不决定 UI 形态**。问答经 Prompter 接口发生:CLI 侧是 huh(cmd/init_huh.go), 桌面壳侧是事件驱动实现(desktop/internal/shell/wizard.go)
- **不写盘**。AskAll 只改内存里的 cfg,Save 由调用方决定——半截答案不得落盘
- **不探测工具链**。探测结果由调用方传入
- 不得 import huh / bubbletea / cobra / isatty,见 boundary_test.go
本包由 cmd 下沉而来(spec §4.4):那批逻辑本就与 TUI 解耦,但封在 package cmd 里且未导出,桌面壳够不着。下沉是为了让 CLI 与 GUI 共用同一份事实来源, 避免两套 role 默认值、两套 listen 预设各自漂移。
本文件是 init 问答的通道:接口 + 按行读答案的脚本化实现。
职责:
- 定义 Prompter(Select / Input / Confirm)
- 提供 ScriptedPrompter:从 Reader 按行读,空行 / EOF 取默认 (测试与 CI 用;真终端走 cmd/init_huh.go)
边界:
- **不写配置**:只返回用户(或脚本)的答案,不碰 config.yaml
- **不探测工具链**:选项列表由调用方传入,这里不调 toolchain.Detect
- 不负责问题集合;问什么仍由本包的 AskAll 决定
Index ¶
- Constants
- Variables
- func Apply(cfg *config.Config, fields []Field, answers map[string]string) error
- func AskAll(w io.Writer, p Prompter, cfg *config.Config, rs []toolchain.Result, ...) (bool, string, error)
- func DefaultOf(f Field, answers map[string]string) string
- func DefaultRole(cfg *config.Config, cfgExisted bool, rs []toolchain.Result, goos string) string
- func ListenPreset(listen string, cfgExisted, isExec bool) string
- func MaybeInstallService(w io.Writer, p Prompter, isExec bool, cfgPath string)
- func Visible(f Field, answers map[string]string) bool
- type Cond
- type DefaultRule
- type Field
- type Kind
- type Option
- type Prompter
- type ScriptedPrompter
Constants ¶
const ( RoleExecutor = "executor" // 执行机:跑 agentd 与 executor RoleCoordinator = "coordinator" // 协调者机:派发与审阅 RoleBoth = "both" )
角色取值写入 Select 的 Value,也是配置语义上的角色名。
Variables ¶
HostGOOS / HostGeteuid 是 MaybeInstallService 里那道平台门的测试缝。
为什么必须是缝:托管路径在 macOS(launchd,用户级)与 Linux(systemd,要 root) 上行为**相反**——前者当场装,后者只打一行 sudo 提示就返回。直接读 runtime.GOOS 的话,一套用例只能覆盖跑测试的那个平台,另一条分支在该平台上 恒不成立:开发机是 macOS,于是「答 y 必须真的调 Install」在 Linux CI 上 必然失败(2026-08-13 实测三条),而 Linux 那条真行为反倒从来没人验过。 下沉到 initflow 后 cmd 仍要钉平台(runInitWith 钉成 darwin),故导出。
var ErrCanceled = errors.New("已取消")
ErrCanceled 表示用户中途取消(Ctrl-C / huh.ErrUserAborted)。 RunE 见到它必须立刻返回、不得 Save:半截答案写出一份只配了一半的配置, 比取消本身更糟。
var InstallService func(w io.Writer, cfgPath string) error
InstallService 是 MaybeInstallService 在「答 y」后代跑托管的安装入口。
CLI 侧由 cmd/service.go 注入 installService——与 handoff service install 走同一条代码路径(B71 要求 init 不复制一份);桌面壳侧注入它自己的安装实现。 nil 时只打印提示、不 panic:托管是附属动作,配置此时已写盘,装不上也不该 让 init 退非零。
Functions ¶
func Apply ¶
Apply 校验答案并写回 cfg。
**不可见字段的答案被忽略而不是报错**:前端可能在用户切角色后残留旧值, 报错会让一个本来可以正常走完的向导卡死。Select 答案必须落在 Options 内、 Confirm 只接受 "true"/"false"——这两类校验失败是承重,防止非法值落盘。
func AskAll ¶
func AskAll(w io.Writer, p Prompter, cfg *config.Config, rs []toolchain.Result, cfgExisted bool) (bool, string, error)
AskAll 按字段表逐项提问并把答案写回 cfg。
参数:
- w: 产品输出(前言与字段的 Notice);桌面壳不走这条路径
- p: 问答实现(生产 TTY 走 huh,测试走脚本化实现)
- cfg: 就地写回;出错时不保证未被部分修改,调用方**绝不可**在出错后落盘
- rs: 工具链探测结果,决定执行者选项与默认值
- cfgExisted: 配置文件是否已存在,影响监听预设的默认档
返回:
- isExec: 本机是否承担执行机角色(调用方据此决定后续是否装 service)
- role: 角色答案原文
- err: 用户取消或校验失败
注意:提问顺序即 Form 返回的切片顺序。想改问什么、问的顺序、默认值, 改 form.go,**不要改本函数**——本函数只负责把表渲染成一问一答。
func DefaultRole ¶
DefaultRole 挑角色预选项。
配置不记角色,只能从已有字段反推:有 targets 说明做过协调者; listen 不是 loopback 说明跑过执行机。推不出时:探到就绪执行者 → 执行机, 否则协调者。
func ListenPreset ¶
ListenPreset 决定监听 Select 的光标停在哪一档。
为什么看「文件事先是否存在」而不是看当前 listen 字符串: config.Load 会把缺文件写成 127.0.0.1:7777,和用户选过「仅本机」是同一个值。 首次执行机要预选所有网卡(否则协调者连不上);重跑时同一字符串必须保住 loopback。
func MaybeInstallService ¶
MaybeInstallService 在执行机上追问是否现在把 agentd 交给进程管理器托管, 答 y 则就地代跑。
参数:
- w: 面向用户的输出
- p: 问答通道(与 AskAll 共用同一实例)
- isExec: 本机角色是否包含执行机
- cfgPath: 配置路径(传给服务单元)
注意:
- 无返回值:托管失败**绝不**让 init 失败。配置此时已经写盘,为一个附属动作 把整条 init 退非零,用户会以为配置没保存(与 install.sh 对 skill install 的处置同一个道理)
- Linux 上非 root 时不代跑:systemd 单元要写 /etc/systemd/system,需要 root, 而 init 不 sudo。此时只打印命令
- why 要追问而不是只提示:托管是「机器重启后 agentd 还回得来」的唯一保障, 它此前只是最后一行提示——B71 现场那台就是这么变成手工拉起的,重启后 PATH 全靠运气
Types ¶
type Cond ¶
type Cond struct {
Key string `json:"key"`
Equal string `json:"equal,omitempty"`
In []string `json:"in,omitempty"`
NonEmpty bool `json:"non_empty,omitempty"`
}
Cond 描述一条显隐或默认值的条件:命中则成立。
字段全部可选;规则是数据,因此 CLI 与桌面前端求值方式相同—— 不会出现两边显隐不一致。
type DefaultRule ¶
DefaultRule 描述一条「满足条件时默认值改用 Value」的规则。
存在的唯一理由是监听预设依赖角色答案,而字段表必须在答题前就交出去。 规则是数据,因此 CLI 与桌面前端求值方式相同——不会出现两边预选不一致。
type Field ¶
type Field struct {
Key string `json:"key"`
Kind Kind `json:"kind"`
Title string `json:"title"`
Notice string `json:"notice"`
Default string `json:"default"`
Options []Option `json:"options,omitempty"`
Roles []string `json:"roles,omitempty"`
Advanced bool `json:"advanced"`
ShowWhen *Cond `json:"show_when,omitempty"`
DefaultWhen []DefaultRule `json:"default_when,omitempty"`
}
Field 描述「问什么」:Key、控件类型、标题、选项、适用角色、显隐条件与随答案变的默认值。
它是一张数据表:CLI 的 AskAll 与桌面壳的前端都按这张表渲染,字段与 分支规则只有这一份真相。**Key 一旦发布不得更名**——桌面前端按它取值。
type Option ¶
Option 是 Select 的一项。Value 写入配置,Label 给人看。
json tag 是**对外契约**:桌面壳把字段表整体序列化给前端,前端按这些 小写键名取值。改名等于改协议,必须同步改 desktop/frontend/src/wizard.ts。
func ExecutorOptions ¶
ExecutorOptions 把四家探测结果编成 Select 选项。没装的也留在列表里—— 探测只影响旁注和 warnIfNotReady,不阻断选择。
func RoleOptions ¶
RoleOptions 返回本平台可选的角色列表。
参数:goos 取 runtime.GOOS;抽成参数是为了让平台分支在任意 CI 上测得到 (判据写死则 Windows 分支在 linux 的 CI 上永远测不到)。
返回:角色选项列表。
注意:B37 之前 Windows 只给协调者,因为 agentd 的进程承载层在该平台全是 not implemented。进程承载层落地后三个角色一律可选,本函数不再分平台。
type Prompter ¶
type Prompter interface {
Select(title string, options []Option, def string) (string, error)
Input(title, def string) (string, error)
Confirm(title string, def bool) (bool, error)
}
Prompter 是 init 问答的唯一入口。生产 TTY 走 huh(cmd/init_huh.go); 测试经 runInitWith 把该缝换成脚本化实现,读 cmd.In。
type ScriptedPrompter ¶
type ScriptedPrompter struct {
// contains filtered or unexported fields
}
ScriptedPrompter 从 Reader 按行读答案,行为对齐旧 ask / askString / askBool。
func NewScriptedPrompter ¶
func NewScriptedPrompter(in io.Reader, out io.Writer) *ScriptedPrompter
NewScriptedPrompter 构造按行读的 Prompter。
参数:
- in: 答案来源(测试喂 strings.Reader)
- out: 提示打到哪;nil 当 io.Discard
注意:同一份输入只能包一层。AskAll 与 MaybeInstallService 必须共用实例, 否则各自 bufio 会把后续答案提前吃掉。
func (*ScriptedPrompter) Confirm ¶
func (p *ScriptedPrompter) Confirm(title string, def bool) (bool, error)
Confirm 读 y/n。空行 / EOF 取 def;认 y/yes 为真、n/no 为假;其余当假。