fix(agent): 卸载时指出该去哪删节点,而不是一律说 agents.json

agent 不持有运维令牌——节点不是运维,一个能删掉邻居的被控节点比它解决的问题更糟——所以
它不能自己从中继上摘掉自己。此前卸载的结束语对所有节点都写"the relay's agents.json still
lists this node",而自注册的节点根本不在那里:它在中继的状态文件里,要从手机上删。运维照着
那句话去翻一个没有这条记录的文件。

现在节点在登记时把来历写进配置(`selfEnrolled`,带 `--code` 为真),卸载结束语据此给出
正确的那一句:

- 自注册的 → "在中继的登记状态里,去 App 的「中继节点」移除"
- 手工登记的 → "在 agents.json 里,删掉那条并重启中继"
- 配置读不出来 → 两句都提,因为读不到不是让卸载失败的理由

测试两项:卸载打印的是它被告知的那一句(且不再对自注册节点说 agents.json)、登记时正确
记录来历(带码为真、不带码不写这个字段)。

Go 全包、Node 141 项、Swift 121 项通过。
This commit is contained in:
chick
2026-09-17 19:12:32 +08:00
parent 934760f8e2
commit 7d174a0261
6 changed files with 96 additions and 1 deletions
+25
View File
@@ -51,6 +51,9 @@ func runEnroll(ctx context.Context, opts EnrollOptions) error {
}
result, err := enroll.Generate(enroll.Options{
// The flag is what records the node's own provenance: with a code the relay holds a
// self-enrolled entry for it, without one an operator wrote it into agents.json.
SelfEnrolled: opts.Code != "",
RelayURL: opts.RelayURL,
LocalPort: opts.LocalPort,
AuditPath: opts.AuditPath,
@@ -132,6 +135,9 @@ func runUninstall(ctx context.Context, opts install.Options, configPath string)
if opts.NotifyRelay == nil && !opts.DryRun {
opts.NotifyRelay = func() error { return retireAtRelay(ctx, configPath) }
}
if opts.RemovalHint == "" {
opts.RemovalHint = removalHint(configPath)
}
return install.Uninstall(opts)
}
@@ -195,3 +201,22 @@ func retireAtRelay(ctx context.Context, configPath string) error {
}
return nil
}
// removalHint says what is left at the relay, which depends on how this node got there.
//
// The distinction matters because the two records live in different places and are removed by different
// people. A self-enrolled node is in the relay's own state file and comes off from the app; a node an
// operator wrote into agents.json is in a file the relay only reads, and has to be edited and the relay
// restarted. Telling the operator the wrong one sends them to the wrong place.
//
// An unreadable config is not a reason to fail an uninstall, so it falls back to naming both.
func removalHint(configPath string) string {
cfg, err := config.LoadForPairing(configPath)
if err != nil {
return "the relay may still list this node; check its agents.json and, if it accepts self-enrollment, the app"
}
if cfg.SelfEnrolled {
return "the relay still lists this node in its enrollment state; remove it in the app (中继节点 → 移除)"
}
return "the relay's agents.json still lists this node; remove that entry and restart the relay"
}