chore(release): 发布 v1.9.2
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

- 「公网服务端」新增 Nginx 启停控制(Agent 0.15.2,停止带二次确认)
- frps 启动失败不再误报成功,并增强启动诊断
- frps vhost 端口禁止 80/443,避免与 VPS 上 Nginx 冲突
- 旧服务端配置的 tcp_mux / login_fail_exit 默认值补齐
- 新增安全审查、页面布局方案与场景操作文档
This commit is contained in:
2026-10-07 19:26:02 +08:00
parent a7d71752e3
commit 2a0740a60f
55 changed files with 5065 additions and 467 deletions
+951
View File
@@ -0,0 +1,951 @@
# 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 安装
调用链:
```text
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 安装
调用链:
```text
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 安装
调用链:
```text
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 两类执行域
需要明确区分:
```text
中央 Havline 服务
-> 通过 SSH 执行 Agent 安装
VPS Agent
-> 在 VPS 本机执行 Nginx 安装
-> 在 VPS 本机执行 frps 安装
```
因此不能只做一套任务管理器:
- Agent 安装任务由中央服务管理。
- Nginx 和 frps 安装任务由 Agent 管理,中央服务负责桥接和转发。
### 4.2 数据流
```mermaid
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 任务字段
建议新增共享任务结构:
```go
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` 建议值:
```text
agent-install
nginx-install
frps-install
```
`Status` 建议值:
```text
queued
running
completed
failed
cancelled
```
`Phase` 建议值:
Agent 安装:
```text
prepare
ssh-connect
upload-agent
upload-script
install-agent
verify-agent
configure-transport
done
```
Nginx 安装:
```text
detect-package-manager
update-package-index
install-package
enable-service
probe-nginx
done
```
frps 安装:
```text
fetch-checksum
download-package
verify-checksum
extract-binary
write-systemd-unit
restart-service
push-config
probe-frps
done
```
### 5.2 SSE 事件
统一事件格式:
```json
{
"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"
}
```
事件类型:
```text
snapshot 连接后先发送当前完整状态
phase 阶段变化
log stdout / stderr 日志
progress 确定进度,例如下载字节数
done 任务完成
error 任务失败
heartbeat 心跳,防止代理断开
```
进度事件示例:
```json
{
"type": "progress",
"phase": "download-package",
"message": "正在下载 frps 安装包",
"current": 18350080,
"total": 45613056,
"percent": 40.2
}
```
不确定进度示例:
```json
{
"type": "phase",
"phase": "install-package",
"message": "正在安装 Nginx,包管理器未提供可解析进度"
}
```
## 6. 中央服务设计
### 6.1 新增共享任务包
建议新增:
```text
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
推荐统一操作入口:
```http
POST /api/frpmulti/servers/{id}/operations
Content-Type: application/json
{
"kind": "agent-install",
"params": {
"trust_new_host_key": false
}
}
```
返回:
```json
{
"job_id": "20261006-abc123",
"status": "queued"
}
```
其他接口:
```http
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 任务管理器
建议新增:
```text
internal/agent/jobs.go
internal/agent/job_handlers.go
```
Agent 任务接口:
```http
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 安装改造
当前:
```go
out, err := install.CombinedOutput()
```
改造为:
```go
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 安装改造
当前:
```go
downloadToFile(downloadURL, tarball)
```
改造为:
```go
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 实现:
```http
POST /api/v1/jobs/{id}/cancel
```
取消行为:
- 取消 context。
- 对 apt/dnf/yum/apk 使用进程组 kill。
- 保留已产生日志。
- 标记任务为 `cancelled`。
## 8. 中央桥接 Agent 任务
### 8.1 AgentClient 新方法
在 `internal/frp/agent_client.go` 增加:
```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 中央任务执行器
```text
创建中央 job
-> 启动 Agent job
-> 订阅 Agent SSE
-> 将 Agent 事件发布到中央 job
-> Agent 完成
-> 中央执行后续步骤
-> 中央 job 完成
```
frps 的后续步骤:
```text
agent frps install done
-> 生成 frps.toml
-> 下发 frps.toml
-> 探测 frps 状态
-> 完成
```
### 8.3 旧 Agent 兼容
新增最低版本常量,例如:
```go
const agentMinVersionJobs = "0.16.0"
```
旧 Agent 处理:
- 中央检测到 Agent 不支持 `/api/v1/jobs/*`。
- 中央仍创建任务,但只发布阶段事件。
- 调用旧同步接口。
- 完成后发布最终日志和结果。
- UI 提示“当前 Agent 版本不支持实时日志,已使用兼容模式”。
## 9. 前端设计
### 9.1 新 composable
建议新增:
```text
web/src/composables/useOperationJob.ts
```
职责:
- 创建任务
- 建立 EventSource
- 维护任务状态
- 维护日志数组
- 自动重连
- 页面隐藏时暂停 UI 更新
- 组件卸载时关闭 EventSource
状态示例:
```ts
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 新组件
建议新增:
```text
web/src/components/OperationProgressDrawer.vue
```
布局:
```text
标题:安装 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 页面刷新和切换
建议在浏览器保存最近一次任务:
```text
localStorage["havline:last-job:{serverID}:{kind}"] = jobID
```
页面加载时:
1. 读取最近 jobID。
2. 请求 `GET /api/frpmulti/jobs/{jobID}`。
3. 如果任务仍在运行,恢复日志面板。
4. 如果任务已完成或过期,清理记录。
服务端同时提供:
```http
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 示例
原始:
```text
Token: 8f3c2e...
```
输出:
```text
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 新增
```text
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 修改
```text
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。