Files
Havline/docs/install-progress-log-plan.md
T
mrlee 2a0740a60f
CI / verify (push) Canceled after 0s
Docker Release / build-and-push (amd64, linux/amd64) (push) Canceled after 0s
Docker Release / build-and-push (arm64, linux/arm64) (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
Docker Release / create-manifest (push) Canceled after 0s
chore(release): 发布 v1.9.2
- 「公网服务端」新增 Nginx 启停控制(Agent 0.15.2,停止带二次确认)
- frps 启动失败不再误报成功,并增强启动诊断
- frps vhost 端口禁止 80/443,避免与 VPS 上 Nginx 冲突
- 旧服务端配置的 tcp_mux / login_fail_exit 默认值补齐
- 新增安全审查、页面布局方案与场景操作文档
2026-10-07 19:26:02 +08:00

21 KiB
Raw Blame History

Agent、Nginx、frps 安装日志与进度方案

1. 背景

当前在界面执行以下操作时,按钮只显示加载圈,用户无法判断任务处于哪个阶段:

  • 安装或升级 Agent
  • 安装 Nginx
  • 安装或升级 frps

这些操作耗时较长,尤其是:

  • SSH 上传 Agent 二进制
  • apt-get update 与 apt-get install nginx
  • 下载 frps 安装包
  • 校验 SHA-256
  • 解压 tar.gz
  • 写入 systemd unit
  • 启动或重启服务

用户目前只能在操作结束后看到最终输出,失败时也无法判断是网络、SSH、包管理器、下载还是 systemd 阶段的问题。

2. 当前实现分析

2.1 Agent 安装

调用链:

AgentView.vue
  -> POST /api/frpmulti/servers/{id}/agent/install
  -> internal/api/frpmulti_handler.go
  -> Service.agentInstall
  -> AgentInstaller.Install
  -> SSHInstaller.Install
  -> uploadBytes + runSSHSession

特点:

  • 安装确实通过 SSH 执行,不依赖 Agent 的 HTTPS 或隧道。
  • uploadBytes 当前一次性写入二进制,没有上传进度。
  • runSSHSession 使用 session.Stdout 和 session.Stderr 收集输出,但只在结束后返回。
  • 前端 run() 只负责按钮 loading 和最终输出,没有实时事件通道。
  • 安装输出里包含 Token: ...,如果直接展示日志,必须先脱敏。

2.2 Nginx 安装

调用链:

ProxyRemoteView.vue
  -> POST /api/frpmulti/servers/{id}/agent/nginx/install
  -> Service.InstallAgentNginx
  -> AgentClient.InstallNginx
  -> Agent POST /api/v1/nginx/install
  -> Server.installNginx

特点:

  • 真正的安装在 VPS Agent 进程内执行。
  • 当前使用 exec.CommandContext(...).CombinedOutput(),命令结束后才能拿到输出。
  • apt-get update、dnf install、yum install、apk add 都可能持续数分钟。
  • Agent 的 HTTP 响应是一次性的,中间过程没有 job、SSE 或状态查询接口。

2.3 frps 安装

调用链:

AgentView.vue
  -> POST /api/frpmulti/servers/{id}/agent/frps-install
  -> Service.InstallFRPSOnAgent
  -> AgentClient.InstallFRPS
  -> Agent POST /api/v1/frps/install
  -> Server.installFRPS

特点:

  • downloadToFile 当前没有下载进度回调。
  • SHA-256 校验、解压、写 unit、systemctl 重启都只在最终响应里返回 notes。
  • 安装完成后,中央端还会继续下发 frps.toml,这一步也应该纳入任务日志。

2.4 现有可复用能力

项目已有两类进度模式:

  1. 证书申请任务

    • internal/acme/job.go
    • POST /api/certificates/apply 返回 job_id
    • GET /api/certificates/jobs/{id}/stream 使用 SSE
    • 前端 CertificatesView.vue 使用 EventSource
  2. FRP 客户端下载任务

    • internal/frp/service.go 的 DownloadTask
    • GET /api/frpmulti/download/status
    • 前端 FrpView.vue 每 500ms 轮询
    • 支持 downloaded、total、message、status

建议以 ACME 的 SSE 任务模型为主,以 FRP 下载的进度字段为补充,不新造第三套机制。

3. 方案目标

3.1 用户目标

  • 点击安装后立即看到任务已经开始。
  • 能看到当前阶段,例如“连接 SSH”“上传 Agent”“下载 frps”“校验安装包”“重启服务”。
  • 能看到实时日志,而不是只看圈圈。
  • 能区分“确定进度”和“不确定进度”。
  • 失败时能定位到具体阶段和最后几行日志。
  • 刷新页面或切换页面后,任务不丢,回来还能继续看。

3.2 工程目标

  • 统一 Agent 安装、Nginx 安装、frps 安装的任务模型。
  • 复用现有 SSE、认证、前端 EventSource 经验。
  • 保持旧 Agent 兼容,不要求所有 VPS 同时升级。
  • 日志必须脱敏,不能把 Agent Token 或 SSH 凭据输出到浏览器。
  • 多服务器之间任务完全隔离。
  • 同一服务器同一安装类型不允许并发执行。

4. 总体架构

4.1 两类执行域

需要明确区分:

中央 Havline 服务
  -> 通过 SSH 执行 Agent 安装

VPS Agent
  -> 在 VPS 本机执行 Nginx 安装
  -> 在 VPS 本机执行 frps 安装

因此不能只做一套任务管理器:

  • Agent 安装任务由中央服务管理。
  • Nginx 和 frps 安装任务由 Agent 管理,中央服务负责桥接和转发。

4.2 数据流

sequenceDiagram
    participant Browser as 浏览器
    participant Central as Havline 中央服务
    participant SSH as VPS SSH
    participant Agent as VPS Agent

    Browser->>Central: POST 创建安装任务
    Central-->>Browser: 202 { job_id }
    Browser->>Central: GET /jobs/{id}/stream

    alt Agent 安装
        Central->>SSH: SSH 上传并安装
        SSH-->>Central: 实时 stdout / stderr
        Central-->>Browser: SSE log / progress / done
    else Nginx 或 frps 安装
        Central->>Agent: POST 创建 Agent 任务
        Agent-->>Central: { job_id }
        Central->>Agent: GET /jobs/{id}/stream
        Agent-->>Central: 实时日志和进度
        Central-->>Browser: 转发为中央 SSE
    end

4.3 为什么选择 SSE

推荐使用 SSE,而不是 WebSocket:

  • 项目已有 SSE 实现和前端 EventSource 使用经验。
  • 安装日志是单向流,不需要双向通信。
  • 更容易通过 Nginx、隧道和现有代理。
  • 断线后可以重新连接并回放快照。

轮询作为兼容兜底:

  • 某些代理环境可能缓冲 SSE。
  • 旧 Agent 不支持 SSE 时,中央服务可以轮询 Agent 任务状态。
  • 前端 SSE 失败时,可以退化为轮询中央任务状态。

5. 统一任务模型

5.1 任务字段

建议新增共享任务结构:

type OperationJob struct {
    ID         string
    ServerID   int64
    Kind       string
    Status     string
    Phase      string
    Message    string
    Current    int64
    Total      int64
    Percent    float64
    Error      string
    StartedAt  time.Time
    UpdatedAt  time.Time
    FinishedAt *time.Time
}

Kind 建议值:

agent-install
nginx-install
frps-install

Status 建议值:

queued
running
completed
failed
cancelled

Phase 建议值:

Agent 安装:

prepare
ssh-connect
upload-agent
upload-script
install-agent
verify-agent
configure-transport
done

Nginx 安装:

detect-package-manager
update-package-index
install-package
enable-service
probe-nginx
done

frps 安装:

fetch-checksum
download-package
verify-checksum
extract-binary
write-systemd-unit
restart-service
push-config
probe-frps
done

5.2 SSE 事件

统一事件格式:

{
  "type": "log",
  "job_id": "20261006-001",
  "kind": "nginx-install",
  "status": "running",
  "phase": "install-package",
  "level": "info",
  "message": "正在执行 apt-get install -y nginx",
  "current": 0,
  "total": 0,
  "percent": 0,
  "timestamp": "2026-10-06T10:00:00Z"
}

事件类型:

snapshot   连接后先发送当前完整状态
phase      阶段变化
log        stdout / stderr 日志
progress   确定进度,例如下载字节数
done       任务完成
error      任务失败
heartbeat  心跳,防止代理断开

进度事件示例:

{
  "type": "progress",
  "phase": "download-package",
  "message": "正在下载 frps 安装包",
  "current": 18350080,
  "total": 45613056,
  "percent": 40.2
}

不确定进度示例:

{
  "type": "phase",
  "phase": "install-package",
  "message": "正在安装 Nginx,包管理器未提供可解析进度"
}

6. 中央服务设计

6.1 新增共享任务包

建议新增:

internal/opjob/job.go
internal/opjob/manager.go
internal/opjob/sse.go

职责:

  • 创建任务
  • 保存事件快照
  • 管理订阅者
  • 发送 SSE
  • 限制日志缓冲区
  • 任务完成和过期清理

internal/acme/job.go 后续可以迁移到该包,避免重复维护。

6.2 中央任务管理器

任务管理器需要支持:

  • Create(serverID, kind)
  • Get(jobID)
  • Publish(jobID, event)
  • Finish(jobID, result)
  • Stream(ctx, jobID, writer)
  • Latest(serverID, kind)
  • CleanupExpired

内存策略:

  • 每个任务最多保留 500 到 1000 条日志事件。
  • 超出后丢弃最旧日志,并插入“日志已截断”提示。
  • 已完成任务保留 30 分钟。
  • 服务重启后任务丢失,前端提示“任务记录已丢失,请重新执行”。

6.3 中央 API

推荐统一操作入口:

POST /api/frpmulti/servers/{id}/operations
Content-Type: application/json

{
  "kind": "agent-install",
  "params": {
    "trust_new_host_key": false
  }
}

返回:

{
  "job_id": "20261006-abc123",
  "status": "queued"
}

其他接口:

GET /api/frpmulti/jobs/{jobID}
GET /api/frpmulti/jobs/{jobID}/stream
GET /api/frpmulti/servers/{id}/jobs/latest?kind=nginx-install

兼容策略:

  • 保留现有同步安装接口一个版本周期。
  • 新前端优先使用 /operations 和 SSE。
  • 旧 Agent 不支持任务流时,中央任务使用兼容模式。

6.4 中央任务隔离

必须按 server_id 隔离:

  • 任务查询校验 server_id 与当前登录会话权限。
  • 任务流校验任务属于目标服务器。
  • 同一服务器同一 kind 只允许一个运行中任务。
  • 不同服务器可以并行安装,互不阻塞。
  • 前端切换服务器时关闭旧 EventSource,重新连接新服务器任务。

7. Agent 侧任务设计

7.1 Agent 任务管理器

建议新增:

internal/agent/jobs.go
internal/agent/job_handlers.go

Agent 任务接口:

POST /api/v1/jobs/nginx/install
POST /api/v1/jobs/frps/install
GET  /api/v1/jobs/{id}
GET  /api/v1/jobs/{id}/stream

鉴权:

  • 继续使用 Agent Token。
  • 浏览器不直接连接 Agent SSE。
  • 中央服务使用 Authorization: Bearer <token> 读取 Agent SSE。

7.2 Nginx 安装改造

当前:

out, err := install.CombinedOutput()

改造为:

stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
startStreamScanner(stdout, job.Log)
startStreamScanner(stderr, job.Log)
err := cmd.Run()

阶段:

  1. detect-package-manager
  2. update-package-index
  3. install-package
  4. enable-service
  5. probe-nginx

进度策略:

  • apt、dnf、yum、apk 的安装进度无法统一解析。
  • 使用阶段进度,例如 0、15、45、75、90、100。
  • UI 使用 indeterminate 进度条,同时显示实时日志。

7.3 frps 安装改造

当前:

downloadToFile(downloadURL, tarball)

改造为:

downloadToFileWithProgress(downloadURL, tarball, func(current, total int64) {
    job.Progress("download-package", current, total)
})

阶段和进度:

阶段 进度 说明
fetch-checksum 5% 获取官方校验文件
download-package 5% 到 60% 按字节数计算真实进度
verify-checksum 65% 校验 SHA-256
extract-binary 75% 解压 frps
write-systemd-unit 85% 写入或接管 unit
restart-service 95% systemctl enable/restart
push-config 98% 中央下发 frps.toml
probe-frps 100% 探测最终状态

7.4 Agent 并发控制

同一 Agent 上:

  • Nginx 安装不能并发执行。
  • frps 安装不能并发执行。
  • 包管理器安装建议全局串行,避免 apt/dpkg 锁冲突。
  • 下载任务可以独立,但最终写 unit 和重启服务要串行。

7.5 Agent 任务取消

建议 P1 实现:

POST /api/v1/jobs/{id}/cancel

取消行为:

  • 取消 context。
  • 对 apt/dnf/yum/apk 使用进程组 kill。
  • 保留已产生日志。
  • 标记任务为 cancelled。

8. 中央桥接 Agent 任务

8.1 AgentClient 新方法

在 internal/frp/agent_client.go 增加:

StartAgentJob(ctx, kind, payload) (string, error)
AgentJobSnapshot(ctx, jobID) (OperationJob, error)
StreamAgentJob(ctx, jobID, onEvent func(Event) error) error

注意:

  • 现有 AgentClient.http 有 60 秒总超时,不适合 SSE。
  • 新增无总超时的 streamHTTP client。
  • SSE 读取使用 bufio.Scanner,设置最大行长度。
  • 连接中断时自动重试快照或重新订阅。

8.2 中央任务执行器

创建中央 job
  -> 启动 Agent job
  -> 订阅 Agent SSE
  -> 将 Agent 事件发布到中央 job
  -> Agent 完成
  -> 中央执行后续步骤
  -> 中央 job 完成

frps 的后续步骤:

agent frps install done
  -> 生成 frps.toml
  -> 下发 frps.toml
  -> 探测 frps 状态
  -> 完成

8.3 旧 Agent 兼容

新增最低版本常量,例如:

const agentMinVersionJobs = "0.16.0"

旧 Agent 处理:

  • 中央检测到 Agent 不支持 /api/v1/jobs/*。
  • 中央仍创建任务,但只发布阶段事件。
  • 调用旧同步接口。
  • 完成后发布最终日志和结果。
  • UI 提示“当前 Agent 版本不支持实时日志,已使用兼容模式”。

9. 前端设计

9.1 新 composable

建议新增:

web/src/composables/useOperationJob.ts

职责:

  • 创建任务
  • 建立 EventSource
  • 维护任务状态
  • 维护日志数组
  • 自动重连
  • 页面隐藏时暂停 UI 更新
  • 组件卸载时关闭 EventSource

状态示例:

interface OperationJobState {
  jobId: string
  kind: string
  status: 'idle' | 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
  phase: string
  message: string
  percent: number
  current: number
  total: number
  logs: OperationLogLine[]
  error: string
  result?: Record<string, unknown>
}

9.2 新组件

建议新增:

web/src/components/OperationProgressDrawer.vue

布局:

标题:安装 Agent / 安装 Nginx / 安装 frps
状态:运行中 / 已完成 / 失败
阶段:准备 -> SSH -> 上传 -> 安装 -> 校验 -> 完成
进度条:确定进度或 indeterminate
日志框:实时 stdout / stderr
底部:复制日志、下载日志、取消、关闭

交互:

  • 安装开始后自动打开抽屉。
  • 日志自动滚动,用户手动上滚后暂停自动滚动。
  • 失败时显示错误摘要和“查看完整日志”。
  • 完成后显示最终结果,例如 Agent 版本、Nginx 路径、frps 版本。
  • 关闭抽屉不取消任务。

9.3 页面接入

Agent 页面:

  • “安装 Agent / 升级 Agent”改为创建 agent-install 任务。
  • “安装 / 升级 frps”改为创建 frps-install 任务。

公网反代页面:

  • “一键安装 Nginx”改为创建 nginx-install 任务。

现有 opOutput:

  • 保留为历史摘要。
  • 实时日志迁移到新抽屉。
  • 完成后仍然写入历史摘要,方便回看。

9.4 页面刷新和切换

建议在浏览器保存最近一次任务:

localStorage["havline:last-job:{serverID}:{kind}"] = jobID

页面加载时:

  1. 读取最近 jobID。
  2. 请求 GET /api/frpmulti/jobs/{jobID}。
  3. 如果任务仍在运行,恢复日志面板。
  4. 如果任务已完成或过期,清理记录。

服务端同时提供:

GET /api/frpmulti/servers/{id}/jobs/latest?kind=agent-install

用于跨浏览器恢复。

10. 日志脱敏

这是本方案的安全硬要求。

10.1 必须脱敏的内容

  • Token: ...
  • HAVLINE_AGENT_TOKEN=...
  • Authorization: Bearer ...
  • SSH 密码
  • 私钥内容
  • 证书私钥
  • 可能包含密码的命令行

10.2 脱敏位置

必须在以下位置之前脱敏:

  • Agent 任务事件发送前
  • 中央任务事件发送前
  • SSE 写入前
  • 历史日志保存前

不能只在前端隐藏,因为浏览器和日志接口仍可能看到明文。

10.3 示例

原始:

Token: 8f3c2e...

输出:

Token: ********

11. 性能与稳定性

11.1 日志节流

apt、dnf、systemctl 输出可能非常密集。

策略:

  • 每 100ms 批量发送一次日志。
  • 单条日志超过 4KB 时截断。
  • 单任务最多保留 1000 条事件。
  • 超出后丢弃旧日志并发送“日志已截断”。

11.2 SSE 心跳

  • 每 15 秒发送一次 heartbeat。
  • 避免 Nginx、隧道、浏览器代理因空闲断开。

11.3 背压

  • 订阅通道使用有缓冲 channel。
  • 慢订阅者不能阻塞任务执行。
  • 状态、phase、done、error 事件必须可靠送达。
  • 普通日志在缓冲满时可以丢弃并标记。

11.4 超时

建议:

操作 超时
Agent 安装 10 分钟
Nginx 安装 10 分钟
frps 安装 15 分钟
SSE 空闲 不超时,使用 heartbeat
Agent SSE 重连 指数退避,最多 3 次

12. 错误处理

12.1 SSH 断开

  • 任务标记失败。
  • 保留已经收到的日志。
  • 提示“SSH 连接中断,Agent 是否安装成功未知,请先点击探测环境”。

12.2 Agent 重启

  • 中央连接 Agent SSE 时返回 404。
  • 任务标记失败。
  • 提示“Agent 已重启,任务状态丢失,请重新执行或探测当前状态”。

12.3 浏览器断开

  • 任务继续执行。
  • 浏览器重新连接后从 snapshot 开始回放。

12.4 中央服务重启

  • 内存任务丢失。
  • 前端收到任务不存在。
  • 提示“任务记录已丢失,请重新执行或查看实际状态”。

13. 分阶段实施

阶段 1:统一任务基础设施

目标:

  • 新增 internal/opjob。
  • 实现 Job、Manager、SSE。
  • 支持 snapshot、log、phase、progress、done、error、heartbeat。
  • 增加单元测试。

不改用户可见行为。

阶段 2:Agent 安装实时日志

目标:

  • 新增中央 agent-install 任务。
  • SSH 上传、安装脚本输出实时推送。
  • 前端新增 OperationProgressDrawer。
  • Agent 页面接入。

这是收益最高、依赖最少的一步。

阶段 3:Nginx 安装实时日志

目标:

  • Agent 增加 job 接口。
  • installNginx 改造为流式执行。
  • 中央桥接 Agent SSE。
  • 公网反代页面接入。
  • 旧 Agent 使用兼容模式。

阶段 4:frps 安装进度

目标:

  • Agent frps 下载增加字节进度。
  • 校验、解压、systemd、配置下发纳入任务阶段。
  • Agent 页面接入。

阶段 5:任务恢复和取消

目标:

  • 浏览器刷新恢复任务。
  • 任务历史查询。
  • 取消任务。
  • 任务中心或统一历史面板。

阶段 6:持久化任务历史

可选:

  • 将已完成任务摘要写入 SQLite。
  • 支持按服务器、类型、时间筛选。
  • 支持导出诊断包。

14. 文件影响清单

14.1 新增

internal/opjob/job.go
internal/opjob/manager.go
internal/opjob/sse.go
internal/opjob/job_test.go
internal/opjob/sse_test.go
internal/agent/jobs.go
internal/agent/job_handlers.go
web/src/composables/useOperationJob.ts
web/src/composables/useOperationJob.spec.ts
web/src/components/OperationProgressDrawer.vue

14.2 修改

internal/frp/ssh_installer.go
internal/frp/agent_installer.go
internal/frp/agent_client.go
internal/frp/service.go
internal/api/frpmulti_handler.go
internal/api/router.go
internal/agent/server.go
internal/agent/transport.go
web/src/api/client.ts
web/src/api/types.ts
web/src/views/AgentView.vue
web/src/views/ProxyRemoteView.vue
web/src/views/FrpView.vue
web/vite.config.ts

15. 测试计划

15.1 后端单元测试

  • Job 快照回放。
  • 多订阅者同时接收。
  • done 后订阅自动结束。
  • 日志缓冲区上限。
  • SSE 心跳。
  • 脱敏规则。
  • 同一服务器同一任务并发拒绝。
  • 不同服务器任务隔离。
  • 旧 Agent 兼容回退。

15.2 后端集成测试

  • 假 SSH 服务器返回多行输出,验证 Agent 安装事件顺序。
  • 假 Agent 返回 SSE,验证中央桥接。
  • frps 下载模拟慢速 reader,验证进度百分比。
  • SSE 中途断开,验证重连和快照恢复。

15.3 前端测试

  • useOperationJob 创建任务并连接 EventSource。
  • snapshot、log、progress、done、error 状态更新。
  • EventSource 重连。
  • 组件卸载时关闭连接。
  • 日志自动滚动与暂停。
  • 安装按钮在任务运行中禁用。

15.4 手工验收

  • 慢速 VPS 安装 Agent,能看到上传和安装日志。
  • Ubuntu 安装 Nginx,能看到 apt 输出。
  • CentOS 安装 Nginx,能看到 yum/dnf 输出。
  • frps 下载慢时能看到百分比。
  • 安装中途刷新页面,回来后仍能看到任务。
  • 切换到另一台服务器,不会串台。
  • 日志中没有 Token 和密码。

16. 验收标准

满足以下条件才算完成:

  1. Agent 安装不再只显示圈圈,至少显示阶段和实时日志。
  2. Nginx 安装能显示包管理器实时输出。
  3. frps 安装能显示下载百分比、校验、解压和 systemctl 结果。
  4. 安装失败时能指出失败阶段和最后错误。
  5. 浏览器刷新或切换页面后,任务不丢。
  6. 多服务器任务完全隔离。
  7. 同一服务器同一安装类型不能重复并发。
  8. Agent Token、SSH 凭据、私钥不会出现在日志中。
  9. 旧 Agent 仍然可以安装 Nginx 和 frps,只是没有实时日志。
  10. 后端、前端测试全部通过。

17. 推荐实施顺序

优先级从高到低:

  1. 中央 opjob 任务基础设施。
  2. Agent 安装实时日志。
  3. 前端通用进度抽屉。
  4. Agent 侧 Nginx job。
  5. 中央桥接 Agent SSE。
  6. frps 下载进度。
  7. 任务恢复。
  8. 任务取消。
  9. 任务历史持久化。

先做第 1 到第 3 步,可以最快解决“只有圈圈”的核心体验问题,而且不需要先升级所有 VPS Agent。