Daemon CLI 工具
后台常驻 CLI 工具开发指南,涵盖进程管理、心跳机制、HTTP API 架构和守护进程生命周期。
Daemon CLI 工具在后台持续运行,通过 HTTP API 供 CLI 命令交互。适用于需要长连接、高初始化成本、持续任务执行或共享状态的场景。
阅读本页前,请先了解适用于所有工具类型的通用规范。
为什么需要 Daemon
普通 CLI 是「调用即退出」的模式,但有些场景下这种模式存在根本性的局限:
1. 外部连接需要保活
以微信消息代理(wechat_agent)为例:微信网页协议需要维持一个长连接来接收消息。如果每次发消息都重新登录、建立连接,不仅速度慢(需要扫码),而且频繁登录会触发风控。Daemon 模式下,只需登录一次,连接就一直保持,后续操作直接复用。
2. 初始化成本高昂
某些工具的启动过程涉及耗时操作:加载大量配置、建立数据库连接池、初始化 SDK 等。如果每次调用都重复这些初始化,性能完全无法接受。Daemon 将这些成本一次性支付,后续调用直接使用已初始化的资源。
3. 需要持续运行的任务
定时调度(agent_cron)需要在后台按计划执行任务,即使用户关闭了终端也不能中断。这本质上需要一个常驻进程来承载调度器。
4. 共享状态与资源
多个 CLI 调用可能需要共享同一份状态(如微信的登录会话、已加载的联系人列表)。Daemon 作为中心进程持有这些状态,所有 CLI 调用通过 API 访问,避免了状态同步的问题。
总结: 当你的工具需要满足以下任一条件时,就应该考虑 Daemon 模式:
- 需要维持长连接或外部会话
- 初始化成本高,需要复用资源
- 需要在后台持续执行任务
- 多次调用之间需要共享状态
架构概览
Daemon CLI 采用 CLI + 后台进程 + HTTP API 的三层架构:
┌──────────────────┐ HTTP API ┌───────────────────┐
│ CLI 命令 │ ─────────────────────► │ Daemon 进程 │
│ │ │ │
│ tool start │ 启动 daemon │ 常驻后台运行 │
│ tool status │ 查询状态 │ 提供 HTTP API │
│ tool stop │ 停止 daemon │ 管理会话/心跳 │
│ tool send │ 业务操作 │ │
└──────────────────┘ ◄──────────────── └───────────────────┘
JSON 响应
用户和 AI Agent 仍然通过 CLI 命令交互,感知上与普通 CLI 无异。区别在于:CLI 命令不再直接执行业务逻辑,而是将请求转发给后台 Daemon 进程,由 Daemon 执行后返回结果。
项目结构
wechat_agent/
├── main.go
├── go.mod
├── cmd/
│ ├── root.go # 命令路由、--help / --skill
│ ├── skill.go # skillSpec 常量
│ ├── server.go # daemon 启动与进程管理
│ ├── client.go # CLI → daemon 的 HTTP 请求封装
│ ├── send.go # 发送消息(通过 API 转发)
│ ├── contacts.go # 查询联系人(通过 API 转发)
│ ├── status.go # 查询 daemon 状态
│ ├── stop.go # 停止 daemon
│ ├── daemon_unix.go # Unix 进程分离
│ └── daemon_windows.go # Windows 进程分离
└── internal/
└── output/
└── output.go
Daemon 生命周期
启动
采用「自启动」模式:CLI 命令启动自身的一个子进程并立即分离:
func startDaemon(dataDir string, port int) {
exePath, _ := os.Executable()
childArgs := []string{"start", "--foreground", "--port", strconv.Itoa(port), "--data-dir", dataDir}
cmd := exec.Command(exePath, childArgs...)
cmd.Stdout = logFile
cmd.Stderr = logFile
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
cmd.Start()
cmd.Process.Release()
}
关键点:
- 默认
tool start以后台方式启动,用户终端不会被阻塞 - 子进程使用
--foreground参数来真正运行服务(用于调试或进程管理器场景) Setsid: true让进程脱离终端会话,终端关闭不影响 Daemon- 日志输出到文件而非终端
状态文件
Daemon 通过文件系统与 CLI 共享连接信息:
~/.my_tool/
├── daemon.pid # 进程 PID —— 用于检测是否存活
├── daemon.port # HTTP 服务端口 —— CLI 通过此端口连接
├── session.json # 业务会话状态(如微信登录态)
└── daemon.log # 运行日志
func WriteDaemonPort(port int) error {
return os.WriteFile(portFilePath(), []byte(strconv.Itoa(port)), 0644)
}
func ReadDaemonPort() (int, error) {
data, err := os.ReadFile(portFilePath())
if err != nil {
return 0, err
}
return strconv.Atoi(strings.TrimSpace(string(data)))
}
func IsDaemonRunning() bool {
pid, err := ReadPID()
if err != nil {
return false
}
return isProcessAlive(pid)
}
停止
var daemonStopCmd = &cobra.Command{
Use: "stop",
Short: "Stop the running daemon",
RunE: func(cmd *cobra.Command, args []string) error {
if !session.IsDaemonRunning() {
output.Print(GetJSONOutput(), map[string]interface{}{
"message": "daemon not running",
}, nil)
return nil
}
resp, err := daemon.Stop()
if err != nil {
session.CleanDaemonFiles()
output.Success(GetJSONOutput(), "daemon stopped")
return nil
}
// ...
},
}
Daemon HTTP API
Daemon 进程在本地随机端口启动 HTTP 服务,仅监听 127.0.0.1:
mux := http.NewServeMux()
mux.HandleFunc("/api/status", s.handleStatus)
mux.HandleFunc("/api/stop", s.handleStop)
mux.HandleFunc("/api/jobs/list", s.handleJobsList)
s.listener, _ = net.Listen("tcp", "127.0.0.1:0")
s.httpPort = s.listener.Addr().(*net.TCPAddr).Port
session.WriteDaemonPort(s.httpPort)
CLI 命令通过读取端口文件找到 Daemon 并发起请求:
func getServerPort(dataDir string) (int, error) {
data, err := os.ReadFile(portFilePath(dataDir))
if err != nil {
return 0, fmt.Errorf("daemon not running (no port file)")
}
return strconv.Atoi(strings.TrimSpace(string(data)))
}
func apiGet(dataDir, path string) (*http.Response, error) {
port, err := getServerPort(dataDir)
if err != nil {
return nil, err
}
url := fmt.Sprintf("http://127.0.0.1:%d%s", port, path)
return http.Get(url)
}
心跳机制
对于需要维持外部连接的 Daemon(如微信长连接),必须实现心跳来保持连接活性并处理异常:
bot.MessageErrorHandler = func(err error) error {
log.Printf("[心跳] 同步异常: %v(将自动重试)", err)
return nil
}
心跳设计要点:
- 错误不应终止进程,而是记录日志后自动重试
- 连接断开时尝试重新建立
- 提供状态查询接口,让 CLI 和 AI Agent 能感知连接健康度
Daemon 子命令设计
推荐使用 daemon 子命令组(或直接作为顶级命令)来管理守护进程生命周期:
daemonCmd = &cobra.Command{
Use: "daemon <start|status|stop>",
Short: "Manage the daemon process",
}
daemonStartCmd = &cobra.Command{
Use: "start",
Short: "Start the daemon",
}
daemonStatusCmd = &cobra.Command{
Use: "status",
Short: "Show daemon status",
}
daemonStopCmd = &cobra.Command{
Use: "stop",
Short: "Stop the running daemon",
}
func init() {
rootCmd.AddCommand(daemonCmd)
daemonCmd.AddCommand(daemonStartCmd)
daemonCmd.AddCommand(daemonStatusCmd)
daemonCmd.AddCommand(daemonStopCmd)
daemonStartCmd.Flags().BoolVar(&foreground, "foreground", false,
"Run daemon in foreground (blocks terminal)")
}
对于 wechat_agent 这类工具,start / status / stop 直接作为顶级命令,因为 Daemon 是该工具的唯一运行方式:
wechat_agent start # 启动 daemon(默认后台)
wechat_agent status # 查询状态
wechat_agent stop # 停止 daemon
wechat_agent send ... # 业务命令(自动转发到 daemon)
--help 中的 Daemon 说明
Daemon CLI 的 --help 需要额外说明架构模式,让用户理解工具的运行方式:
fmt.Fprint(w, `wechat_agent - 微信消息助手
重要: 使用前请务必通过如下命令获取完整使用指南:
wechat_agent --skill
架构:
采用守护进程模式。start 命令默认以后台 daemon 运行(终端关闭不影响),
然后使用 contacts/send 等命令操作,会话在进程存活期间一直有效。
用法:
wechat_agent start [flags] 启动守护进程
wechat_agent contacts [flags] 获取联系人列表
wechat_agent send [flags] "消息文本" 发送消息
wechat_agent status [flags] 查看状态
wechat_agent stop [flags] 停止守护进程
`)
实战案例:agent_cron
agent_cron 是一个基于 Daemon 模式的定时任务调度器,完整展示了 Daemon CLI 的设计模式。它支持两种周期性任务:
- 脚本任务 — 按计划执行本地脚本/可执行文件
- 外部任务 — 按计划向 WinClaw 本地开放接口 提交 prompt
Daemon 负责任务调度、执行和结果持久化,CLI 提供熟悉的命令行界面进行任务管理:
agent_cron daemon start # 启动调度器守护进程
agent_cron job add --type external_task \ # 添加定时 AI 任务
--name "morning-report" \
--cron "0 0 9 * * 1-5" \
--prompt "请总结昨天的工作进展"
agent_cron job ls # 列出所有任务
agent_cron job run <id> # 立即触发一个任务
agent_cron job logs <id> # 查看执行历史
外部任务类型使用 WinClaw 本地开放接口 将 prompt 发送给 WinClaw 执行,实现定时自动运行 AI 任务。
开发检查清单
- 通用规范中的所有基础要求
-
start/status/stop生命周期管理命令 -
--foreground标志(用于调试和进程管理器) - PID 文件和端口文件管理
- 进程分离(Unix:
Setsid,Windows:CREATE_NEW_PROCESS_GROUP) - 日志输出到文件
- HTTP API 仅监听
127.0.0.1 - 心跳机制(如需维持外部连接)
- 优雅停止(清理资源、删除状态文件)
-
--help中说明 Daemon 架构