This commit is contained in:
+56
-56
@@ -1,15 +1,15 @@
|
||||
# Fonu 部署安装文档
|
||||
# Havline 部署安装文档
|
||||
|
||||
> 适用版本:v1.1.0(源码包 `fonu-1.1.0-src.zip`)。本文覆盖从拿到安装包到服务上线的完整流程:环境要求 → 打包 → 三种部署方式 → 首次配置 → FRP/DDNS/证书联动 → 升级与回滚 → 故障排查。
|
||||
> 适用版本:v1.1.0(源码包 `havline-1.1.0-src.zip`)。本文覆盖从拿到安装包到服务上线的完整流程:环境要求 → 打包 → 三种部署方式 → 首次配置 → FRP/DDNS/证书联动 → 升级与回滚 → 故障排查。
|
||||
> 功能细节与组合场景请配合项目内两份文档:`README.md`(功能总览)与 FRP 页面右上「使用说明」(内置十个组合场景详解)。
|
||||
|
||||
## 0. 交付物清单与安装路径速查
|
||||
|
||||
| 交付物 | 内容 | 适用 | 安装方式 |
|
||||
| --- | --- | --- | --- |
|
||||
| `fonu-1.1.0-linux-amd64.tar.gz`(14MB) | 编译好的 Linux 二进制(前端已内嵌)+ migrations + systemd + 一键脚本 | **裸机 Linux 服务器**,目标机只需系统 nginx | 解压 → `sudo bash install.sh`(见 3.4) |
|
||||
| `fonu-1.1.0-image.tar.gz`(约 100-150MB) | 构建好的 Docker 镜像 `fonu:local`(含 nginx + frpc) | 目标机有 Docker、要离线安装 | `docker load` → `docker run`(见 3.3 方式 C) |
|
||||
| `fonu-1.1.0-src.zip`(11MB) | 完整源码 + Dockerfile/compose + 部署文档 | 要自行构建 / 二次开发 / 交叉编译 | `docker compose build` 后按方式 A/B 部署(见第 2 节) |
|
||||
| `havline-1.1.0-linux-amd64.tar.gz`(14MB) | 编译好的 Linux 二进制(前端已内嵌)+ migrations + systemd + 一键脚本 | **裸机 Linux 服务器**,目标机只需系统 nginx | 解压 → `sudo bash install.sh`(见 3.4) |
|
||||
| `havline-1.1.0-image.tar.gz`(约 100-150MB) | 构建好的 Docker 镜像 `havline:local`(含 nginx + frpc) | 目标机有 Docker、要离线安装 | `docker load` → `docker run`(见 3.3 方式 C) |
|
||||
| `havline-1.1.0-src.zip`(11MB) | 完整源码 + Dockerfile/compose + 部署文档 | 要自行构建 / 二次开发 / 交叉编译 | `docker compose build` 后按方式 A/B 部署(见第 2 节) |
|
||||
|
||||
**怎么选**:飞牛 fnOS 首选镜像包导入(图形界面,见第 3 节末「飞牛 fnOS 部署详解」),要在 NAS 上构建则用源码包 + host 模式 compose;普通 Linux 服务器要省事用 Linux 运行包(免 Docker);目标机不方便联网构建用镜像包离线导入。三者的功能完全一致——容器里跑的也是同一份二进制 + 系统 nginx。
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
### 1.1 源码包内容
|
||||
|
||||
`fonu-1.1.0-src.zip`(约 11MB,304 项)为完整可构建源码:
|
||||
`havline-1.1.0-src.zip`(约 11MB,304 项)为完整可构建源码:
|
||||
|
||||
| 目录/文件 | 说明 |
|
||||
| --- | --- |
|
||||
@@ -50,7 +50,7 @@
|
||||
docker compose build
|
||||
```
|
||||
|
||||
一次构建同时产出:前端 `dist`、Go 后端二进制与运行镜像(frpc 不预装,运行期用「应用配置」按需下载),标签为 `fonu:local`。
|
||||
一次构建同时产出:前端 `dist`、Go 后端二进制与运行镜像(frpc 不预装,运行期用「应用配置」按需下载),标签为 `havline:local`。
|
||||
|
||||
**版本号**:读取根目录 `VERSION` 文件(当前 `1.1.0`)自动注入前后端显示;要改版本号只需编辑该文件后重新 build。
|
||||
|
||||
@@ -61,49 +61,49 @@ docker build \
|
||||
--build-arg NODE_IMAGE=docker.m.daocloud.io/library/node:22-alpine \
|
||||
--build-arg GO_IMAGE=docker.m.daocloud.io/library/golang:1.23-bookworm \
|
||||
--build-arg RUNTIME_IMAGE=docker.m.daocloud.io/library/debian:bookworm-slim \
|
||||
-t fonu:local .
|
||||
-t havline:local .
|
||||
```
|
||||
|
||||
## 3. 三种部署方式(按环境二选一)
|
||||
|
||||
### 方式 A:飞牛 fnOS / 已占用 80/443 的环境 —— host 模式(推荐)
|
||||
|
||||
飞牛系统自带 Nginx 占用 80/443,Fonu 使用 host 网络 + 高位反代端口避免冲突。
|
||||
飞牛系统自带 Nginx 占用 80/443,Havline 使用 host 网络 + 高位反代端口避免冲突。
|
||||
|
||||
```bash
|
||||
FONU_SESSION_SECRET=请改成一串随机字符 docker compose -f docker-compose.host.yml up -d --build
|
||||
HAVLINE_SESSION_SECRET=请改成一串随机字符 docker compose -f docker-compose.host.yml up -d --build
|
||||
```
|
||||
|
||||
- 无端口映射,容器直接使用宿主机网络栈;
|
||||
- 反代端口由环境变量 `FONU_NGINX_HTTP_PORT=18080`、`FONU_NGINX_HTTPS_PORT=9443` 决定(文件内已内置);
|
||||
- 反代端口由环境变量 `HAVLINE_NGINX_HTTP_PORT=18080`、`HAVLINE_NGINX_HTTPS_PORT=9443` 决定(文件内已内置);
|
||||
- 入口:管理后台 `http://<NAS-IP>:6893`,HTTP 反代 `http://<NAS-IP>:18080`,HTTPS 反代 `https://<NAS-IP>:9443`;
|
||||
- 数据持久化在命名卷 `fonu-data`(挂载到容器 `/data`)。
|
||||
- 数据持久化在命名卷 `havline-data`(挂载到容器 `/data`)。
|
||||
|
||||
### 方式 B:其他环境 / 空闲 80/443 —— Bridge 端口映射
|
||||
|
||||
```bash
|
||||
FONU_SESSION_SECRET=请改成一串随机字符 docker compose -f docker-compose.hub.yml up -d --build
|
||||
HAVLINE_SESSION_SECRET=请改成一串随机字符 docker compose -f docker-compose.hub.yml up -d --build
|
||||
```
|
||||
|
||||
- 端口映射 `6893:6893`、`18080:80`、`9443:443`(宿主机 18080/9443 → 容器 80/443);
|
||||
- 数据同样在命名卷 `fonu-data`。
|
||||
- 数据同样在命名卷 `havline-data`。
|
||||
|
||||
### 方式 C:docker run(不想用 compose)
|
||||
|
||||
```bash
|
||||
docker run -d --name fonu \
|
||||
docker run -d --name havline \
|
||||
--net=host \
|
||||
-v ./data:/data \
|
||||
-e FONU_SESSION_SECRET=请改成一串随机字符 \
|
||||
-e HAVLINE_SESSION_SECRET=请改成一串随机字符 \
|
||||
--restart unless-stopped \
|
||||
fonu:local
|
||||
havline:local
|
||||
```
|
||||
|
||||
host 模式下反代端口用环境变量控制:`-e FONU_NGINX_HTTP_PORT=18080 -e FONU_NGINX_HTTPS_PORT=9443`。Bridge 环境改为 `-p 6893:6893 -p 18080:80 -p 9443:443`(去掉 `--net=host`)。
|
||||
host 模式下反代端口用环境变量控制:`-e HAVLINE_NGINX_HTTP_PORT=18080 -e HAVLINE_NGINX_HTTPS_PORT=9443`。Bridge 环境改为 `-p 6893:6893 -p 18080:80 -p 9443:443`(去掉 `--net=host`)。
|
||||
|
||||
**生成随机 SESSION_SECRET**:`openssl rand -hex 32`(Linux)或 PowerShell:`-join ((48..57)+(97..122) | Get-Random -Count 32 | % {[char]$_})`。
|
||||
|
||||
> **`FONU_SESSION_SECRET` 是什么**:Fonu 的主密钥,两个用途——① 签名/加密登录会话 Cookie(决定登录态是否可被伪造);② 作为 `secret.Box` 加密存储中敏感凭据(DDNS API Token、FRP Token、SMTP 密码等)。默认值 `change-me-in-production` 是公开的:拿到它即可伪造管理员会话、解密数据库里的全部凭据,**生产环境必须替换**。⚠️ 换密钥的时机:首次启动前设置;若之后更换,旧密钥加密的凭据将无法解密(应用会提示重新保存),需到对应页面重录 Token。
|
||||
> **`HAVLINE_SESSION_SECRET` 是什么**:Havline 的主密钥,两个用途——① 签名/加密登录会话 Cookie(决定登录态是否可被伪造);② 作为 `secret.Box` 加密存储中敏感凭据(DDNS API Token、FRP Token、SMTP 密码等)。默认值 `change-me-in-production` 是公开的:拿到它即可伪造管理员会话、解密数据库里的全部凭据,**生产环境必须替换**。⚠️ 换密钥的时机:首次启动前设置;若之后更换,旧密钥加密的凭据将无法解密(应用会提示重新保存),需到对应页面重录 Token。
|
||||
|
||||
### Linux 服务器完整部署步骤(Ubuntu / Debian / CentOS)
|
||||
|
||||
@@ -113,13 +113,13 @@ curl -fsSL https://get.docker.com | sh
|
||||
sudo systemctl enable --now docker
|
||||
|
||||
# 1. 解压源码包并进入目录
|
||||
unzip fonu-1.1.0-src.zip -d fonu && cd fonu
|
||||
unzip havline-1.1.0-src.zip -d havline && cd havline
|
||||
|
||||
# 2. 构建镜像(约 3-10 分钟,取决于网络与机器性能)
|
||||
docker compose build
|
||||
|
||||
# 3. 生成密钥并启动(Bridge 模式,映射 6893/18080/9443)
|
||||
echo "FONU_SESSION_SECRET=$(openssl rand -hex 32)" > .env
|
||||
echo "HAVLINE_SESSION_SECRET=$(openssl rand -hex 32)" > .env
|
||||
docker compose -f docker-compose.hub.yml up -d
|
||||
|
||||
# 4. 防火墙放行(按需)
|
||||
@@ -128,48 +128,48 @@ sudo ufw allow 80/tcp 443/tcp # 反代(Bridge 模式容器内监听 80/443
|
||||
# firewalld 用: firewall-cmd --permanent --add-port=6893/tcp && firewall-cmd --reload
|
||||
|
||||
# 5. 验证
|
||||
docker logs fonu | grep -i password # 取 admin 初始密码
|
||||
docker logs havline | grep -i password # 取 admin 初始密码
|
||||
curl -I http://127.0.0.1:6893/login # 应返回 200
|
||||
```
|
||||
|
||||
- **开机自启**:compose 内已含 `restart: unless-stopped`,且第 0 步已 `enable docker`,重启服务器自动拉起;
|
||||
- **80/443 被系统 Nginx 占用**:改用 `docker-compose.host.yml`(host 网络 + 18080/9443 高位端口),或改 `docker-compose.hub.yml` 里的宿主端口映射;
|
||||
- **离线部署**(目标机不方便联网构建):在有网的机器上 `docker compose build` 后 `docker save fonu:local | gzip > fonu.tar.gz`,拷贝到目标机 `docker load < fonu.tar.gz`,再用方式 C 的 `docker run` 启动(无需再构建);
|
||||
- **数据位置**:命名卷 `fonu-data`(`docker volume inspect fonu-data` 查看宿主机实际路径),备份该卷即备份全部配置与证书。
|
||||
- **离线部署**(目标机不方便联网构建):在有网的机器上 `docker compose build` 后 `docker save havline:local | gzip > havline.tar.gz`,拷贝到目标机 `docker load < havline.tar.gz`,再用方式 C 的 `docker run` 启动(无需再构建);
|
||||
- **数据位置**:命名卷 `havline-data`(`docker volume inspect havline-data` 查看宿主机实际路径),备份该卷即备份全部配置与证书。
|
||||
|
||||
### Linux 运行包安装(免 Docker,裸机部署)
|
||||
|
||||
适用:不想装 Docker 的 Linux 服务器。前提:系统已装 nginx(Fonu 复用它做反代),root 权限。
|
||||
适用:不想装 Docker 的 Linux 服务器。前提:系统已装 nginx(Havline 复用它做反代),root 权限。
|
||||
|
||||
```bash
|
||||
# 1. 解压
|
||||
tar xzf fonu-1.1.0-linux-amd64.tar.gz -C fonu && cd fonu
|
||||
tar xzf havline-1.1.0-linux-amd64.tar.gz -C havline && cd havline
|
||||
|
||||
# 2. 一键安装(自动建 fonu 系统用户、装到 /opt/fonu、生成随机密钥、注册 systemd 开机自启)
|
||||
# 2. 一键安装(自动建 havline 系统用户、装到 /opt/havline、生成随机密钥、注册 systemd 开机自启)
|
||||
sudo bash install.sh
|
||||
|
||||
# 3. 验证
|
||||
systemctl status fonu
|
||||
systemctl status havline
|
||||
curl -I http://127.0.0.1:6893/login
|
||||
journalctl -u fonu | grep -i password # 取 admin 初始密码
|
||||
journalctl -u havline | grep -i password # 取 admin 初始密码
|
||||
```
|
||||
|
||||
包内容:`fonu` 二进制(前端已 `go:embed` 内嵌,无需单独部署 dist)、`migrations/`(从二进制同级目录自动查找)、`deploy/nginx/mime.types`、`fonu.service`(systemd 模板)、`install.sh`、`DEPLOYMENT.md`。
|
||||
包内容:`havline` 二进制(前端已 `go:embed` 内嵌,无需单独部署 dist)、`migrations/`(从二进制同级目录自动查找)、`deploy/nginx/mime.types`、`havline.service`(systemd 模板)、`install.sh`、`DEPLOYMENT.md`。
|
||||
|
||||
关键路径(install.sh 自动配置):
|
||||
|
||||
| 项 | 值 |
|
||||
| --- | --- |
|
||||
| 二进制 | `/opt/fonu/fonu` |
|
||||
| 数据目录 | `/var/lib/fonu`(数据库、证书、日志全在这) |
|
||||
| 会话密钥 | `/etc/default/fonu`(重复安装保留原值) |
|
||||
| 服务 | `systemctl restart fonu` 生效配置改动 |
|
||||
| 二进制 | `/opt/havline/havline` |
|
||||
| 数据目录 | `/var/lib/havline`(数据库、证书、日志全在这) |
|
||||
| 会话密钥 | `/etc/default/havline`(重复安装保留原值) |
|
||||
| 服务 | `systemctl restart havline` 生效配置改动 |
|
||||
|
||||
**备份/迁移**:停服务后打包 `/var/lib/fonu` 即可(`tar czf fonu-data.tar.gz /var/lib/fonu`),新机器解压到同路径 + 装同版本二进制即完成迁移。
|
||||
**备份/迁移**:停服务后打包 `/var/lib/havline` 即可(`tar czf havline-data.tar.gz /var/lib/havline`),新机器解压到同路径 + 装同版本二进制即完成迁移。
|
||||
|
||||
**手动安装**(不用脚本):按包内 `README.txt` 要点——建 `fonu` 系统用户、复制二进制与 migrations 到 `/opt/fonu`、`EnvironmentFile` 写入随机密钥、`systemctl enable --now fonu`。
|
||||
**手动安装**(不用脚本):按包内 `README.txt` 要点——建 `havline` 系统用户、复制二进制与 migrations 到 `/opt/havline`、`EnvironmentFile` 写入随机密钥、`systemctl enable --now havline`。
|
||||
|
||||
> 镜像包(`fonu-1.1.0-image.tar.gz`)来源:在任意装了 Docker 的机器上对源码包执行 `docker compose build` 后 `docker save fonu:local | gzip > fonu-1.1.0-image.tar.gz`;目标机 `docker load < fonu-1.1.0-image.tar.gz` 后按方式 C 启动。
|
||||
> 镜像包(`havline-1.1.0-image.tar.gz`)来源:在任意装了 Docker 的机器上对源码包执行 `docker compose build` 后 `docker save havline:local | gzip > havline-1.1.0-image.tar.gz`;目标机 `docker load < havline-1.1.0-image.tar.gz` 后按方式 C 启动。
|
||||
|
||||
### 飞牛 fnOS 部署详解(图形界面,不敲命令)
|
||||
|
||||
@@ -177,17 +177,17 @@ journalctl -u fonu | grep -i password # 取 admin 初始密码
|
||||
|
||||
**方式一:导入现成镜像(推荐,5 分钟)**
|
||||
|
||||
1. **上传文件**:把 `fonu-1.1.0-image.tar.gz` 通过文件管理 / SMB 上传到 NAS,如共享目录 `docker/fonu/`;
|
||||
2. **导入镜像**:打开 Docker 应用 → 镜像 → 导入(从文件加载)→ 选择该 tar.gz → 等待镜像列表出现 `fonu:local`(若界面只接受 .tar,先在电脑上解压出内层的 .tar 再导入);
|
||||
1. **上传文件**:把 `havline-1.1.0-image.tar.gz` 通过文件管理 / SMB 上传到 NAS,如共享目录 `docker/havline/`;
|
||||
2. **导入镜像**:打开 Docker 应用 → 镜像 → 导入(从文件加载)→ 选择该 tar.gz → 等待镜像列表出现 `havline:local`(若界面只接受 .tar,先在电脑上解压出内层的 .tar 再导入);
|
||||
3. **创建容器**:容器 → 创建:
|
||||
|
||||
| 配置项 | 填写 |
|
||||
| --- | --- |
|
||||
| 名称 | `fonu` |
|
||||
| 镜像 | `fonu:local` |
|
||||
| 名称 | `havline` |
|
||||
| 镜像 | `havline:local` |
|
||||
| 网络 | **host**(飞牛 80/443 被系统占用,host + 高位端口最省事;UI 不支持 host 时选 Bridge 并映射 6893/18080/9443) |
|
||||
| 存储挂载 | NAS 建目录(如 `/vol1/…/docker/fonu/data`)→ 映射到容器 `/data`(读写) |
|
||||
| 环境变量 | `FONU_SESSION_SECRET=随机串`、`FONU_DATA_DIR=/data`、`FONU_LISTEN=:6893`、`FONU_NGINX_HTTP_PORT=18080`、`FONU_NGINX_HTTPS_PORT=9443` |
|
||||
| 存储挂载 | NAS 建目录(如 `/vol1/…/docker/havline/data`)→ 映射到容器 `/data`(读写) |
|
||||
| 环境变量 | `HAVLINE_SESSION_SECRET=随机串`、`HAVLINE_DATA_DIR=/data`、`HAVLINE_LISTEN=:6893`、`HAVLINE_NGINX_HTTP_PORT=18080`、`HAVLINE_NGINX_HTTPS_PORT=9443` |
|
||||
| 重启策略 | 自动重启 / unless-stopped(开机自启) |
|
||||
|
||||
4. **启动并取初始密码**:容器详情 → 日志,搜 `password`;浏览器打开 `http://<NAS-IP>:6893` 登录。
|
||||
@@ -196,14 +196,14 @@ journalctl -u fonu | grep -i password # 取 admin 初始密码
|
||||
|
||||
1. 把源码包 zip 上传到 NAS 并解压(文件管理支持解压,或电脑解压后整目录上传);
|
||||
2. Docker 应用 → 项目(Compose)→ 新建:指定项目目录为解压后的源码目录、使用 `docker-compose.host.yml`(或直接把该文件内容粘贴到 Compose 编辑器);
|
||||
3. 在源码目录同层放一个 `.env` 文件,内容一行:`FONU_SESSION_SECRET=随机串`;
|
||||
3. 在源码目录同层放一个 `.env` 文件,内容一行:`HAVLINE_SESSION_SECRET=随机串`;
|
||||
4. 构建并启动。⚠️ NAS 上构建耗时较长(前端编译),低配机器建议改用方式一;
|
||||
5. 后续升级:上传新源码覆盖 → 项目重新构建即可,数据在卷里不受影响。
|
||||
|
||||
**Compose 构建报 401(docker.fnnas.com)**:fnOS 的 Docker 设置里默认配了自家加速地址,但它对 `library/` 官方镜像拒绝匿名拉取。三种解法任选:
|
||||
|
||||
1. **改用镜像包(最省事)**:跳过构建,按方式一导入 `fonu-1.1.0-image.tar.gz`;
|
||||
2. **覆盖基础镜像源**:在源码目录的 `.env` 里加三行(已有 FONU_SESSION_SECRET 就追加在下面):
|
||||
1. **改用镜像包(最省事)**:跳过构建,按方式一导入 `havline-1.1.0-image.tar.gz`;
|
||||
2. **覆盖基础镜像源**:在源码目录的 `.env` 里加三行(已有 HAVLINE_SESSION_SECRET 就追加在下面):
|
||||
```
|
||||
NODE_IMAGE=docker.m.daocloud.io/library/node:22-alpine
|
||||
GO_IMAGE=docker.m.daocloud.io/library/golang:1.23-bookworm
|
||||
@@ -224,10 +224,10 @@ journalctl -u fonu | grep -i password # 取 admin 初始密码
|
||||
|
||||
## 4. 首次登录与安全配置
|
||||
|
||||
1. 启动后查看初始密码:`docker logs fonu | grep -i password`(自动生成的 admin 初始密码会打印在日志里);
|
||||
1. 启动后查看初始密码:`docker logs havline | grep -i password`(自动生成的 admin 初始密码会打印在日志里);
|
||||
2. 浏览器打开 `http://<NAS-IP>:6893/login`,用 `admin` + 初始密码登录;
|
||||
3. **立即**在「设置 → 安全」修改管理员密码;
|
||||
4. 确认「设置 → 常规」中 `FONU_SESSION_SECRET` 已改为非默认值(compose 文件中已传入则跳过);
|
||||
4. 确认「设置 → 常规」中 `HAVLINE_SESSION_SECRET` 已改为非默认值(compose 文件中已传入则跳过);
|
||||
5. 建议在「设置 → 通知」配置一种告警方式(邮件 / Webhook / Telegram),DDNS 失败、证书续签失败、登录异常等事件会推送。
|
||||
|
||||
## 5. 反向代理与 HTTPS 证书
|
||||
@@ -254,7 +254,7 @@ DDNS 页 → 添加任务:选服务商 → 填 API 凭证(可「测试连接
|
||||
|
||||
前置:一台有公网 IP 的 VPS。
|
||||
|
||||
1. **Fonu 添加服务端**:FRP 穿透页 → 添加服务端 → 填 VPS 地址、bindPort(默认 7000)、认证方式与 Token(与 frps 一致);
|
||||
1. **Havline 添加服务端**:FRP 穿透页 → 添加服务端 → 填 VPS 地址、bindPort(默认 7000)、认证方式与 Token(与 frps 一致);
|
||||
2. **生成 frps 配置**:服务端「详情」→「frps.toml」→ 复制生成内容到 VPS 保存为 `frps.toml`,执行 `frps -c frps.toml`;按文件末尾的防火墙清单在 VPS 安全组放行端口;
|
||||
3. **添加穿透规则**:8 种类型按需选——tcp/udp 填远程端口;http/https/tcpmux 填自定义域名(走 frps vhost 端口);stcp/sudp/xtcp 凭 secretKey(无公网监听);
|
||||
4. **应用并验证**:「应用配置」→「校验并应用配置」;服务端「详情」看五项诊断(TCP 可达 / frpc 登录 / 代理可达 / DNS 解析 / 归属地)与独立日志。
|
||||
@@ -265,12 +265,12 @@ DDNS 页 → 添加任务:选服务商 → 填 API 凭证(可「测试连接
|
||||
|
||||
```bash
|
||||
# 1. 备份数据(最重要)
|
||||
docker run --rm -v fonu-data:/data -v ${PWD}:/backup alpine tar czf /backup/fonu-data-backup.tar.gz /data
|
||||
docker run --rm -v havline-data:/data -v ${PWD}:/backup alpine tar czf /backup/havline-data-backup.tar.gz /data
|
||||
# 2. 解压新版源码包覆盖项目目录(VERSION 与 migrations 会更新)
|
||||
# 3. 重建并启动(迁移 015–022 会在启动时自动应用)
|
||||
docker compose -f docker-compose.host.yml up -d --build
|
||||
# 4. 看启动日志确认迁移与版本
|
||||
docker logs -f fonu
|
||||
docker logs -f havline
|
||||
```
|
||||
|
||||
### 回滚
|
||||
@@ -288,18 +288,18 @@ docker compose -f docker-compose.host.yml up -d --build
|
||||
|
||||
| 现象 | 处理 |
|
||||
| --- | --- |
|
||||
| 容器起来但 6893 打不开 | `docker logs fonu` 看启动报错;`docker port fonu`(Bridge 模式)确认映射 |
|
||||
| 提示「会话加密密钥」警告 | 未设置 `FONU_SESSION_SECRET`,compose 传入即可 |
|
||||
| 容器起来但 6893 打不开 | `docker logs havline` 看启动报错;`docker port havline`(Bridge 模式)确认映射 |
|
||||
| 提示「会话加密密钥」警告 | 未设置 `HAVLINE_SESSION_SECRET`,compose 传入即可 |
|
||||
| 反代 502 | 目标内网应用未启动或地址填错;日志中心看 Nginx error 日志 |
|
||||
| 证书申请失败 | DDNS 凭证是否有效、域名 DNS 是否已生效、ACME 邮箱是否已设置 |
|
||||
| frpc 连不上 frps | 两端 Token/端口不一致;用服务端「详情 → frps.toml」重新生成对齐 |
|
||||
| 穿透流量显示 0 | frps 未配置 webServer 或 Fonu 服务端表单未填管理地址(非必须功能) |
|
||||
| 穿透流量显示 0 | frps 未配置 webServer 或 Havline 服务端表单未填管理地址(非必须功能) |
|
||||
| 页面改动未生效 | 确认访问的是容器端口而非开发服务器;`docker compose up -d --build` 重建 |
|
||||
|
||||
## 10. 卸载
|
||||
|
||||
```bash
|
||||
docker compose down # 停止并删除容器(数据卷保留)
|
||||
docker volume rm fonu-data # 彻底删除数据(不可逆,先备份)
|
||||
docker rmi fonu:local # 删除镜像
|
||||
docker volume rm havline-data # 彻底删除数据(不可逆,先备份)
|
||||
docker rmi havline:local # 删除镜像
|
||||
```
|
||||
|
||||
+24
-24
@@ -1,7 +1,7 @@
|
||||
# 内网应用经 FRP 暴露公网:NAS 侧完整操作手册
|
||||
|
||||
> 场景:把 NAS 内网应用(本例 `192.168.31.152:7023`)通过一台公网 VPS(`38.95.74.87`)以域名(`tv.36800.cc`)暴露出去,URL 不带端口号,且后续域名 / HTTPS / 访问控制全部在 NAS 的 Fonu 网页里管理,VPS 只做纯流量转发。
|
||||
> 适用版本:Fonu v1.1.0+(FRP 多服务端模块)。VPS 上 **不需要也不应该** 装 Nginx 反代。
|
||||
> 场景:把 NAS 内网应用(本例 `192.168.31.152:7023`)通过一台公网 VPS(`38.95.74.87`)以域名(`tv.36800.cc`)暴露出去,URL 不带端口号,且后续域名 / HTTPS / 访问控制全部在 NAS 的 Havline 网页里管理,VPS 只做纯流量转发。
|
||||
> 适用版本:Havline v1.1.0+(FRP 多服务端模块)。VPS 上 **不需要也不应该** 装 Nginx 反代。
|
||||
|
||||
## 0. 最终架构与访问地址
|
||||
|
||||
@@ -12,7 +12,7 @@ VPS frps(监听 80 —— 由 frpc 动态注册的 tcp 隧道,VPS 上无需
|
||||
↓ FRP 隧道(bindPort 7000,TLS)
|
||||
NAS frpc
|
||||
↓ 127.0.0.1:18080
|
||||
Fonu Nginx(按域名分流,后续可加 HTTPS/黑白名单)
|
||||
Havline Nginx(按域名分流,后续可加 HTTPS/黑白名单)
|
||||
↓ http://192.168.31.152:7023
|
||||
内网应用
|
||||
```
|
||||
@@ -23,17 +23,17 @@ Fonu Nginx(按域名分流,后续可加 HTTPS/黑白名单)
|
||||
| --- | --- | --- |
|
||||
| 公网 VPS | `38.95.74.87`(tv.36800.cc) | 只跑 frps,Nginx 停用 |
|
||||
| NAS 内网应用 | `192.168.31.152:7023` | 真正的 Web 服务 |
|
||||
| Fonu 管理后台 | `http://<NAS-IP>:6893` | 全部配置入口 |
|
||||
| Havline 管理后台 | `http://<NAS-IP>:6893` | 全部配置入口 |
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
- VPS:已部署 frps(bindPort 7000、token 认证;参考 Fonu 服务端详情「frps.toml」一键生成);
|
||||
- VPS:已部署 frps(bindPort 7000、token 认证;参考 Havline 服务端详情「frps.toml」一键生成);
|
||||
- VPS:frps 的 Nginx **未**占用 80(占用见第 5 节排障);
|
||||
- 云厂商安全组已放行:7000/tcp(frps 控制)、80/tcp(对外)、7500/tcp(frps 管理接口,可选,用于流量统计);
|
||||
- NAS:Fonu 已部署并能打开管理后台;
|
||||
- 域名:`tv.36800.cc` 的 DNS A 记录指向 VPS IP(可用 Fonu DDNS 页「自定义 IP」固定,或由 DNS 服务商直接管理)。
|
||||
- NAS:Havline 已部署并能打开管理后台;
|
||||
- 域名:`tv.36800.cc` 的 DNS A 记录指向 VPS IP(可用 Havline DDNS 页「自定义 IP」固定,或由 DNS 服务商直接管理)。
|
||||
|
||||
## 2. Fonu 网页配置(三处)
|
||||
## 2. Havline 网页配置(三处)
|
||||
|
||||
### 2.1 FRP 服务端
|
||||
|
||||
@@ -57,9 +57,9 @@ Fonu Nginx(按域名分流,后续可加 HTTPS/黑白名单)
|
||||
| --- | --- | --- |
|
||||
| 所属服务端 | 上一步的服务端 | |
|
||||
| 名称 | `tv`(frps 全局唯一,出现在日志里) | |
|
||||
| 类型 | **tcp** | ⚠️ 不选 http——80 直接由 frps 转发,域名分流交给 Fonu 反代 |
|
||||
| 类型 | **tcp** | ⚠️ 不选 http——80 直接由 frps 转发,域名分流交给 Havline 反代 |
|
||||
| 远程端口 | **80** | 选 tcp 类型后出现此字段;frpc 登录时向 frps 动态注册 |
|
||||
| 内网目标 | **127.0.0.1:18080** | 指向 Fonu 自己的 Nginx HTTP 监听(fnOS host 模式默认 18080) |
|
||||
| 内网目标 | **127.0.0.1:18080** | 指向 Havline 自己的 Nginx HTTP 监听(fnOS host 模式默认 18080) |
|
||||
|
||||
保存 → 右上 **「应用配置」→「校验并应用配置」**,或对该服务端「停止→启动」。
|
||||
|
||||
@@ -72,25 +72,25 @@ Fonu Nginx(按域名分流,后续可加 HTTPS/黑白名单)
|
||||
| 前端域名 | `tv.36800.cc` |
|
||||
| 监听端口 | **18080**(与 2.2 的内网目标端口一致;fnOS 下不要用 80,系统 Nginx 已占用) |
|
||||
| 目标地址 | `http://192.168.31.152:7023`(⚠️ 指向真正的内网应用,完整 URL 带协议与端口) |
|
||||
| HTTPS | 按需;fnOS 下 Fonu Nginx 的 HTTPS 监听默认 9443 |
|
||||
| HTTPS | 按需;fnOS 下 Havline Nginx 的 HTTPS 监听默认 9443 |
|
||||
|
||||
> ⚠️ **回环警告**:目标地址千万不要填 `http://tv.36800.cc:xxxx`——那会让请求从 NAS 出公网再回 NAS(NAS→VPS→NAS 绕圈)。反代目标是**内网应用本身**。
|
||||
> ⚠️ **「目标地址缺少主机名」报错**:目标地址格式必须完整 `http://IP:端口`(协议+主机+端口缺一不可,单斜杠 `http:/` 或中文冒号都会报 400)。
|
||||
|
||||
## 3. VPS 侧配置(一次性的两步)
|
||||
|
||||
### 3.1 frps.toml(Fonu 一键生成)
|
||||
### 3.1 frps.toml(Havline 一键生成)
|
||||
|
||||
Fonu 服务端「详情」→「frps.toml」→ 复制内容到 VPS 保存为 `frps.toml`:
|
||||
Havline 服务端「详情」→「frps.toml」→ 复制内容到 VPS 保存为 `frps.toml`:
|
||||
|
||||
```toml
|
||||
bindAddr = "0.0.0.0"
|
||||
bindPort = 7000
|
||||
|
||||
auth.method = "token"
|
||||
auth.token = "<与 Fonu 一致>"
|
||||
auth.token = "<与 Havline 一致>"
|
||||
|
||||
webServer.addr = "0.0.0.0" # 可选:Fonu 拉取穿透流量
|
||||
webServer.addr = "0.0.0.0" # 可选:Havline 拉取穿透流量
|
||||
webServer.port = 7500
|
||||
webServer.user = "admin"
|
||||
webServer.password = "<自定义>"
|
||||
@@ -130,8 +130,8 @@ frpc 约每 33 秒自动重试注册,释放后无需任何手动操作,注
|
||||
| 步骤 | 命令 / 操作 | 预期 |
|
||||
| --- | --- | --- |
|
||||
| VPS 80 已监听 | `ss -tlnp \| grep ':80 '` | 进程为 frps |
|
||||
| frpc 注册成功 | Fonu 服务端「日志」 | 无 `port unavailable`,有 proxy 启动记录 |
|
||||
| 隧道通 | Fonu 服务端「测速」/「详情」诊断 | TCP 可达、frpc 登录 ok、延迟正常 |
|
||||
| frpc 注册成功 | Havline 服务端「日志」 | 无 `port unavailable`,有 proxy 启动记录 |
|
||||
| 隧道通 | Havline 服务端「测速」/「详情」诊断 | TCP 可达、frpc 登录 ok、延迟正常 |
|
||||
| 分流通 | 服务端「详情」→ 诊断 `proxy_reachable` | ok |
|
||||
| 端到端 | 外网浏览器 `http://tv.36800.cc` | 直达内网应用页面 |
|
||||
|
||||
@@ -142,10 +142,10 @@ frpc 约每 33 秒自动重试注册,释放后无需任何手动操作,注
|
||||
| `Connection refused`(外部 curl 80) | **VPS 侧**:frps 没绑上 80 | ① `ss -tlnp \| grep ':80 '` 看 80 被谁占(多为 Nginx 没停)→ 停用;② `systemctl status frps` 确认在跑且用的新配置(nohup 手动跑的必须 `pkill frps` 重启);③ 重启后等 frpc 自动重试 |
|
||||
| frpc 日志 `start error: port unavailable` | 同上 | VPS 80 被占;见 3.2 |
|
||||
| 诊断「frpc 登录」失败 | Token/端口不一致 | 两端 token 对齐;端口=frps bindPort |
|
||||
| 登录成功但 `proxy_reachable` 失败 | Fonu 反代 | 反代规则监听端口与 FRP 内网目标端口一致(18080);反代已启用 |
|
||||
| 登录成功但 `proxy_reachable` 失败 | Havline 反代 | 反代规则监听端口与 FRP 内网目标端口一致(18080);反代已启用 |
|
||||
| 打开域名 404 | 域名分流失败 | 反代规则的前端域名与访问域名完全一致;frps 无需 vhost(tcp 方案) |
|
||||
| 域名解析不对 | DNS | `nslookup tv.36800.cc` 应返回 VPS IP;用 DDNS 自定义 IP 固定 |
|
||||
| 流量统计全 0 | frps webServer 未配 | 非必须;要在 Fonu 看流量就配 webServer 并在服务端表单填管理接口 |
|
||||
| 流量统计全 0 | frps webServer 未配 | 非必须;要在 Havline 看流量就配 webServer 并在服务端表单填管理接口 |
|
||||
|
||||
## 6. 两种方案对比(VPS 80 的归属)
|
||||
|
||||
@@ -154,9 +154,9 @@ frpc 约每 33 秒自动重试注册,释放后无需任何手动操作,注
|
||||
| FRP 规则类型 | tcp,远程端口 80 → 127.0.0.1:18080 | http 类型 + frps `vhostHTTPPort=8080` |
|
||||
| http 类型去端口 | —(tcp 方案直接占 80) | `vhostHTTPPort` 从 8080 改 **80**:URL 变 `http://域名`(无端口);前提同样是 VPS 停 Nginx 释放 80,改后 `pkill frps` 重启生效;NAS 侧规则无需动 |
|
||||
| VPS Nginx | 不需要(停用) | 必须:listen 80 → proxy_pass 127.0.0.1:8080(**必须带** `proxy_set_header Host $host`) |
|
||||
| 加新域名 | Fonu 反代加一条 + FRP 加一条 tcp 规则(远程端口仍 80,内网目标同 18080) | VPS 上加 server 块 |
|
||||
| 上 HTTPS | Fonu 证书页(DNS-01)+ 反代开 HTTPS + FRP 加 tcp 443→127.0.0.1:9443 | VPS 上装证书 |
|
||||
| 访问控制(IP/BasicAuth) | Fonu 反代页直接配 | VPS 上手写 |
|
||||
| 加新域名 | Havline 反代加一条 + FRP 加一条 tcp 规则(远程端口仍 80,内网目标同 18080) | VPS 上加 server 块 |
|
||||
| 上 HTTPS | Havline 证书页(DNS-01)+ 反代开 HTTPS + FRP 加 tcp 443→127.0.0.1:9443 | VPS 上装证书 |
|
||||
| 访问控制(IP/BasicAuth) | Havline 反代页直接配 | VPS 上手写 |
|
||||
| 限制 | VPS 80/443 不能再跑别的站点 | VPS 可共存多站点 |
|
||||
| URL | `http://tv.36800.cc`(无端口) | `http://tv.36800.cc`(80)或带 vhost 端口 |
|
||||
|
||||
@@ -164,7 +164,7 @@ frpc 约每 33 秒自动重试注册,释放后无需任何手动操作,注
|
||||
|
||||
## 7. HTTPS 升级路径(方案 A 延伸)
|
||||
|
||||
1. **Fonu 证书页**:申请 `tv.36800.cc` 或 `*.36800.cc` 证书(ACME DNS-01,复用 DDNS 页服务商凭证);
|
||||
1. **Havline 证书页**:申请 `tv.36800.cc` 或 `*.36800.cc` 证书(ACME DNS-01,复用 DDNS 页服务商凭证);
|
||||
2. **反代规则**:开 HTTPS、绑定证书、(可选)HTTP 强制跳转;
|
||||
3. **FRP 加一条 tcp 规则**:远程端口 443 → 内网目标 `127.0.0.1:9443`(Fonu Nginx HTTPS 监听);
|
||||
3. **FRP 加一条 tcp 规则**:远程端口 443 → 内网目标 `127.0.0.1:9443`(Havline Nginx HTTPS 监听);
|
||||
4. 安全组放行 443;之后 `https://tv.36800.cc` 可用,TLS 全程在 NAS 侧终止。
|
||||
|
||||
+8
-7
@@ -1,4 +1,4 @@
|
||||
# Fonu 文档索引
|
||||
# Havline 文档索引
|
||||
|
||||
> 维护约定:新增方案 / 审查文档时在下方登记;方案落地后在「状态」列标注实施结果,并给已被推翻的描述加时效说明。
|
||||
> 最近更新:2026-10-01(v1.9.0)
|
||||
@@ -11,14 +11,15 @@
|
||||
| `improvement-roadmap-2026-09-27.md` | P0-P3 改进路线实施方案:逐项改法、测试、验收、依赖、发布与回滚 | P0-1、P0-3、P0-4 已完成,其余待实施 |
|
||||
| `system-upgrade.md` | Docker Compose 一键升级:sidecar 架构、首次部署、安全边界、回滚、故障排查与验证页 | 已实施(v1.3.8) |
|
||||
| `agent-listen-strategy-plan.md` | Agent 默认监听策略:回环监听、SSH 隧道优先、HTTPS pin 可选与自动迁移方案 | 阶段 1-4 已实施(v1.4.0);2.0.0 兼容清理待后续 |
|
||||
| `cloudflared-integration-plan.md` | Cloudflare Tunnel(cloudflared)与 Fonu 的融合方案:托管模型、复用面、分阶段落地与 v2 优化 | 阶段 1-2 已实施(v1.6.0);后续按 `cloudflared-fonu-fit-plan.md` 推进 |
|
||||
| `cloudflared-feature-plan.md` | Cloudflare 隧道功能全集:官方最新文档、参考项目功能、Fonu 现状差距与 P0-P3 路线 | 查漏用;实施以 `cloudflared-fonu-fit-plan.md` 为准 |
|
||||
| `cloudflared-fonu-fit-plan.md` | Cloudflare 隧道 Fonu 适配版计划:出口抽象、规则同源、体系整合、明确不做项与阶段实施 | 阶段 1-3 主体已实施;剩余项见 `cloudflared-current-features.md` |
|
||||
| `cloudflared-integration-plan.md` | Cloudflare Tunnel(cloudflared)与 Havline 的融合方案:托管模型、复用面、分阶段落地与 v2 优化 | 阶段 1-2 已实施(v1.6.0);后续按 `cloudflared-havline-fit-plan.md` 推进 |
|
||||
| `cloudflared-feature-plan.md` | Cloudflare 隧道功能全集:官方最新文档、参考项目功能、Havline 现状差距与 P0-P3 路线 | 查漏用;实施以 `cloudflared-havline-fit-plan.md` 为准 |
|
||||
| `cloudflared-havline-fit-plan.md` | Cloudflare 隧道 Havline 适配版计划:出口抽象、规则同源、体系整合、明确不做项与阶段实施 | 阶段 1-3 主体已实施;剩余项见 `cloudflared-current-features.md` |
|
||||
| `cloudflared-current-features.md` | Cloudflare 隧道当前已实现功能说明:页面、出口、路由、DNS、监控、备份、诊断、API 与限制 | 当前版本功能基线 |
|
||||
| `cloudflared-github-ux-research.md` | GitHub 同类项目调研:Cloudflare 管理面板、反向代理、访问平台、监控与流量检查器的可借鉴体验 | 调研与优化路线 |
|
||||
| `fnos-app-packaging-plan.md` | 飞牛 fnOS 应用打包方案:官方文档依据、Docker 模板、向导、权限、数据迁移、升级与发布流程 | 方案待实施 |
|
||||
| `project-audit-2026-09-24.md` | 全项目审查:P0/P1/P2 分级,含精确行号与逐条修法 | **进行中**——P0 已随 v1.2.7 完成,P1 实施中 |
|
||||
| `frp-server-list-merge-plan.md` | FRP 服务端列表并入「公网服务端」页(方案 A,四步) | 步 1/2 已完成(v1.2.6);步 3 文案收尾、步 4 死样式清理未做 |
|
||||
| `agent-capability-plan.md` | fonu-agent 能力扩展(P0/P1/P2/P3) | 已实施(agent 0.11.0);P3 三项不做 |
|
||||
| `agent-capability-plan.md` | havline-agent 能力扩展(P0/P1/P2/P3) | 已实施(agent 0.11.0);P3 三项不做 |
|
||||
| `agent-view-design.md` | 公网 Agent 页设计方案(Pass 1/2 流程) | 部分过期:顶部下拉已被左列表 + 右详情取代 |
|
||||
| `agent-view-design-v2.md` | 公网 Agent 页设计方案 v2 | 部分过期:同上 |
|
||||
| `agent-view-refactor-plan.md` | 公网 Agent 页四段式(状态 → 控制 → 生命周期 → 输出)整改 | 已实施 |
|
||||
@@ -26,11 +27,11 @@
|
||||
| `agent-proxy-settings-plan.md` | 公网反代「部署设置」对齐本地反代的语义迁移 | 已实施 |
|
||||
| `agent-proxy-roadmap.md` | 公网反代功能增强规划(含部署踩坑史) | 部分已实施(漂移检测、全量重部署),其余待排期 |
|
||||
| `frps-features.md` | frps 服务端功能实现清单(对照官方 frps_full_example.toml) | 参考 |
|
||||
| `frps服务端管理方案.md` | FRPS 服务端管理方案(fonu-agent 部署在公网服务器) | 已实施(Agent 管理面板) |
|
||||
| `frps服务端管理方案.md` | FRPS 服务端管理方案(havline-agent 部署在公网服务器) | 已实施(Agent 管理面板) |
|
||||
| `frps管理方案.md` | FRPS 管理方案(早期版本) | 疑似已被 `frps服务端管理方案.md` 取代,保留供追溯(待确认) |
|
||||
| `upstream-sync-plan.md` | 上游 1.0.2 → 1.0.5 对照与可借鉴项 | 部分过期:对 `/frp` 现状的描述基于 2026-09-23 |
|
||||
| `FRP公网暴露操作手册.md` | FRP 公网暴露操作手册(面向使用) | 参考 |
|
||||
| `DEPLOYMENT.md` | 部署安装:裸机 / Docker / 飞牛 host 模式 | 参考;适用 `fonu-1.1.0` 安装包 |
|
||||
| `DEPLOYMENT.md` | 部署安装:裸机 / Docker / 飞牛 host 模式 | 参考;适用 `havline-1.1.0` 安装包 |
|
||||
|
||||
## 其它位置
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# fonu-agent 能力扩展方案
|
||||
# havline-agent 能力扩展方案
|
||||
|
||||
> **实施状态(2026-09-24)**:P0/P1/P2 已全部落地(agent 0.11.0);P3 三项按本文约定不做。剩余为真机验收项,见 `docs/project-audit-2026-09-24.md`。
|
||||
|
||||
> 本文盘点 VPS 侧 `fonu-agent` 现有的接口面与数据面,给出「能直接做」「需要新增 agent 端点」「不做」三类功能的详细设计。
|
||||
> 本文盘点 VPS 侧 `havline-agent` 现有的接口面与数据面,给出「能直接做」「需要新增 agent 端点」「不做」三类功能的详细设计。
|
||||
>
|
||||
> 依据:本仓库 `internal/agent/`、`internal/frp/agent_client.go`、`internal/frp/agent_installer.go`、`internal/frp/ssh_installer.go`、`internal/api/frpmulti_handler.go`、`web/src/views/AgentView.vue`、`web/src/views/ProxyRemoteView.vue`(均为读代码确认,非推测)。
|
||||
> 当前 `AgentVersion = "0.9.2"`(`internal/agent/server.go:34`)。
|
||||
@@ -20,7 +20,7 @@
|
||||
| --- | --- | --- |
|
||||
| 1 | `GET /api/v1/ping` | 存活探测(安装/升级后确认 token 与网络) |
|
||||
| 2 | `GET /api/v1/status` | 一次性返回 Nginx / frps / 端口 / 路由名的快照 |
|
||||
| 3 | `GET /api/v1/routes` | 每条 `fonu-*.conf` 的真实状态:`file_exists` / `syntax_ok` / `loaded` |
|
||||
| 3 | `GET /api/v1/routes` | 每条 `havline-*.conf` 的真实状态:`file_exists` / `syntax_ok` / `loaded` |
|
||||
| 4 | `PUT /api/v1/routes` | 写入(或覆盖)一条域名的 Nginx 片段 |
|
||||
| 5 | `GET /api/v1/routes/{domain}/conf` | 回读某域名实际生效的配置文件内容 |
|
||||
| 6 | `DELETE /api/v1/routes/{domain}` | 删除某域名的片段 |
|
||||
@@ -31,7 +31,7 @@
|
||||
| 11 | `POST /api/v1/frps/action` | frps 的 start / stop / restart |
|
||||
| 12 | `POST /api/v1/certs` | 推送证书(fullchain + privatekey) |
|
||||
|
||||
鉴权:全部走 `s.auth(...)` 包装的 token 校验;agent **只监听 VPS 本地**,由 Fonu 主动发起请求,**不做任何主动上报**。
|
||||
鉴权:全部走 `s.auth(...)` 包装的 token 校验;agent **只监听 VPS 本地**,由 Havline 主动发起请求,**不做任何主动上报**。
|
||||
|
||||
### 1.2 agent 的数据面(`status` 已返回、但应用侧只用了一部分)
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
| `nginx_binary` / `nginx_conf_dir` | 配置的二进制与 vhost 目录 | 已用 |
|
||||
| `ports` | 80 / 443 / 7000 / 8080 等端口是否监听 | **只用到了 8080 一项**(`AgentView.vue:359`) |
|
||||
| `frps_active` / `frps_enabled` | systemd 单元是否运行 / 是否开机自启 | 已用 |
|
||||
| `frps_binary` / `frps_config_path` | Fonu 侧配置的路径 | 已用 |
|
||||
| `frps_binary` / `frps_config_path` | Havline 侧配置的路径 | 已用 |
|
||||
| `frps_unit_exec` | systemd 实际执行的启动命令 | 已展示,**未做一致性核对** |
|
||||
| `frps_version` | 探测到的 frps 版本 | 已展示 |
|
||||
| `frps_log_tail` | frps 日志尾部 6 行 | 已展示 |
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
### 1.4 安装与升级链路(已闭环)
|
||||
|
||||
- agent 二进制**内嵌在 Fonu 里**(`internal/frp/agentbin`,按 arch 取),通过 **SSH** 上传到 VPS 并注册/重启 systemd 单元(`agent_installer.go` + `ssh_installer.go`)。SSH 连接信息就是迁移 023 加的 `ssh_host / ssh_port / ssh_user / ssh_auth / ssh_secret_enc`。
|
||||
- agent 二进制**内嵌在 Havline 里**(`internal/frp/agentbin`,按 arch 取),通过 **SSH** 上传到 VPS 并注册/重启 systemd 单元(`agent_installer.go` + `ssh_installer.go`)。SSH 连接信息就是迁移 023 加的 `ssh_host / ssh_port / ssh_user / ssh_auth / ssh_secret_enc`。
|
||||
- 因此「升级 Agent」不需要 agent 自己有下载能力——**自升级不是必须项**。
|
||||
- 版本约定:`AgentVersion` 上方按「每项行为变更一行」的注释累积,改动后手动递增(当前 0.9.2)。
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
1. **没有资源采集**:VPS 的 CPU / 内存 / 磁盘 / 负载完全不可见。
|
||||
2. **没有日志读取**:只有 frps 的 6 行 tail,Nginx 的 access / error 日志读不到(排障必须 SSH)。
|
||||
3. **frps 配置只写不读**:`PUT /api/v1/frps/config` 存在,没有对应的 GET;用户手改 VPS 上的 frps.toml 后 Fonu 无从知晓,下次下发直接覆盖。
|
||||
3. **frps 配置只写不读**:`PUT /api/v1/frps/config` 存在,没有对应的 GET;用户手改 VPS 上的 frps.toml 后 Havline 无从知晓,下次下发直接覆盖。
|
||||
4. **没有证书清单**:只能推送,无法列出 VPS 上现有的证书与到期时间(宝塔自己申请的证书看不到)。
|
||||
5. **部署是逐条的**:`SyncRouteSpec` 按单条规则调用,没有「全量重部署」入口——agent 重装/换机后只能一条条点。
|
||||
6. **写入结果不含 reload 细节**:片段写入成功但 Nginx reload 失败时,用户拿不到失败原因。
|
||||
@@ -89,7 +89,7 @@
|
||||
| **P2-2** | reload 失败原因回传 | agent + 应用 | 同上 | 1h |
|
||||
| **P3** | 不做:防火墙读写、任意命令执行、agent 自升级 | — | — | — |
|
||||
|
||||
**贯穿约束(所有新增 agent 端点都适用)**:老版本 agent 没有新端点,应用侧调用必须**按「端点不存在」优雅降级**(收到 404/405 时显示「VPS 上的 agent 版本过低,请升级 Agent」而不是报错),否则用户升级了 Fonu 但没升级 agent 时页面会直接报错。
|
||||
**贯穿约束(所有新增 agent 端点都适用)**:老版本 agent 没有新端点,应用侧调用必须**按「端点不存在」优雅降级**(收到 404/405 时显示「VPS 上的 agent 版本过低,请升级 Agent」而不是报错),否则用户升级了 Havline 但没升级 agent 时页面会直接报错。
|
||||
|
||||
---
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
| 类别 | 定义 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| 未部署 | `A - B` | 库里要部署但 VPS 上没有(agent 重装、换机、手工删过) |
|
||||
| 孤儿配置 | `B - A` | VPS 有但库里没有(手工加的 `fonu-*.conf`、已删规则的残留) |
|
||||
| 孤儿配置 | `B - A` | VPS 有但库里没有(手工加的 `havline-*.conf`、已删规则的残留) |
|
||||
| 未生效 | `A ∩ (B - C)` | 文件在但未被 Nginx 加载(`syntax_ok=false`,或 include 未覆盖) |
|
||||
| 内容不一致 | `RouteConf(domain)` 与「按当前部署设置生成的内容」规范化比对 | 手工改过上游 / 证书路径 / 安全项 |
|
||||
|
||||
@@ -117,18 +117,18 @@
|
||||
|
||||
**前端落点**:公网反代页顶部提示条(有差异时才出现)+「重新部署全部」按钮(二次确认,因为会覆盖手工改动)。
|
||||
|
||||
**验收**:手工删一个 `fonu-*.conf` → 清单出现「未部署」,重部署后消失;手工改一条 conf 的上游 → 出现「内容不一致」;语法错误时归入「未生效」并给出原因。
|
||||
**验收**:手工删一个 `havline-*.conf` → 清单出现「未部署」,重部署后消失;手工改一条 conf 的上游 → 出现「内容不一致」;语法错误时归入「未生效」并给出原因。
|
||||
|
||||
### 3.3 frps 运行一致性核对
|
||||
|
||||
- **比对三项**:`frps_unit_exec`(systemd 实际执行)vs Fonu 配置的 `frps_binary`;`frps_version` vs 期望版本;`frps_config_path` vs Fonu 生成路径。
|
||||
- **比对三项**:`frps_unit_exec`(systemd 实际执行)vs Havline 配置的 `frps_binary`;`frps_version` vs 期望版本;`frps_config_path` vs Havline 生成路径。
|
||||
- **呈现**:AgentView「frps 控制」段加一行「启动路径一致性」,不一致时并列显示两个路径。
|
||||
- **验收**:手工改过 systemd unit 的 VPS 上显示不一致;一致的机器显示绿色。
|
||||
|
||||
### 3.4 证书推送核对
|
||||
|
||||
- **数据**:已有 `RouteHealth` 的证书段(`cert_ok` / `cert_detail`)+ 推送结果。
|
||||
- **呈现**:公网反代页健康列已展示证书段;补一行「证书来源」(Fonu 推送 / 待推送 / VPS 本地已有)。
|
||||
- **呈现**:公网反代页健康列已展示证书段;补一行「证书来源」(Havline 推送 / 待推送 / VPS 本地已有)。
|
||||
- **验收**:证书过期时健康列给出证书段失败与到期天数。
|
||||
|
||||
---
|
||||
@@ -155,7 +155,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
- **实现要点**:**直接复用仓库里已有的 `internal/sysinfo`**(同一 module,`cmd/fonu-agent` 可直接引用)——`/proc/stat`、`/proc/meminfo`、`Statfs` 以及 Windows 的 kernel32 路径都已写好;agent 跑在 VPS 上,采到的天然是 VPS 自身数值。
|
||||
- **实现要点**:**直接复用仓库里已有的 `internal/sysinfo`**(同一 module,`cmd/havline-agent` 可直接引用)——`/proc/stat`、`/proc/meminfo`、`Statfs` 以及 Windows 的 kernel32 路径都已写好;agent 跑在 VPS 上,采到的天然是 VPS 自身数值。
|
||||
- **CPU 差值**:`sysinfo.Read(prev)` 需要上次采样;agent 进程内保存上次 `CPUSample`,并对间隔 < 1 秒的重复调用直接返回上次结果(避免连续刷新时数值抖动)。
|
||||
- **`load_avg` / `uptime_seconds`**:Linux 读 `/proc/loadavg`、`/proc/uptime`;非 Linux 省略这两个字段(前端按缺失处理)。
|
||||
- **应用侧链路**:`AgentClient.Metrics(ctx)` → `GET /api/frpmulti/servers/{id}/agent/metrics` → AgentView 新增「VPS 资源」卡(复用本地仪表盘「系统资源」的行样式:名称 / 用量明细 / 4px 进度条 / 百分比,≥75% 转橙、≥90% 转红)。
|
||||
@@ -181,7 +181,7 @@
|
||||
- **路径来源(白名单,不接受任意 path 参数)**:
|
||||
1. 优先从 `nginx -T` 的 dump 里解析 `access_log` / `error_log` 指令(agent 已有 `nginx -T` 解析与缓存经验,见 `resolveNginxConfDir`);
|
||||
2. 解析不到时回落默认路径 `/var/log/nginx/{access,error}.log`;
|
||||
3. 支持环境变量 `FONU_AGENT_NGINX_LOG_DIR` 覆盖(与现有 `FONU_AGENT_NGINX_CONF_DIR` 的回退风格一致)。
|
||||
3. 支持环境变量 `HAVLINE_AGENT_NGINX_LOG_DIR` 覆盖(与现有 `HAVLINE_AGENT_NGINX_CONF_DIR` 的回退风格一致)。
|
||||
- **只读尾部**:实现 `tailFile(path, n)`(与现有 `journalTail` 同思路),从文件末尾按块倒读,避免把整个日志读进内存;单行超长(如 4KB)截断。
|
||||
- **错误处理**:文件不存在 → 200 + `lines: []` + 原样返回 `path`(前端显示「未找到日志文件」);无权限 → 403 并给出明确原因。
|
||||
- **应用侧链路**:`AgentClient.NginxLogs(ctx, serverID, type, lines)` → `GET /api/frpmulti/servers/{id}/agent/logs/nginx?...`;日志中心新增「公网 Nginx」页签,复用现有 `LogToolbar` / `LogPagination` / `EmptyState` 与 `logTimeCell`,与「FRP 日志」页签同一套呈现。
|
||||
@@ -192,7 +192,7 @@
|
||||
响应:`{ "path": "/etc/frp/frps.toml", "exists": true, "content": "...", "mod_time": "2026-09-23T18:00:00Z", "checked_at": "..." }`
|
||||
|
||||
- **实现要点**:读 `s.cfg.FrpsConfigPath`,限制文件大小(如 256KB),超限返回 `truncated: true`;文件不存在返回 `exists: false` 而不是错误。
|
||||
- **应用侧**:在「公网服务端」的 frps 同源配置弹窗里加「与 VPS 实际文件比对」——把「Fonu 即将下发的内容」与回读内容做规范化 diff(去注释与空白),不一致时提示「VPS 上的 frps.toml 已被手工修改」,并给「重新下发」按钮(复用已有 `PushFRPSConfig`)。
|
||||
- **应用侧**:在「公网服务端」的 frps 同源配置弹窗里加「与 VPS 实际文件比对」——把「Havline 即将下发的内容」与回读内容做规范化 diff(去注释与空白),不一致时提示「VPS 上的 frps.toml 已被手工修改」,并给「重新下发」按钮(复用已有 `PushFRPSConfig`)。
|
||||
- **验收**:在 VPS 上手工改一行 frps.toml → 弹窗提示不一致并高亮差异行;重新下发后恢复一致。
|
||||
|
||||
---
|
||||
@@ -204,8 +204,8 @@
|
||||
响应:`{ "certs": [{ "domain": "7023.36800.cc", "cert_path": "...", "key_path": "...", "not_after": "2026-12-01T00:00:00Z", "days_left": 69 }], "warnings": [], "checked_at": "..." }`
|
||||
|
||||
- **实现要点**:遍历 `s.cfg.CertsDir` 下的域名目录,用标准库 `crypto/x509` 解析 `fullchain.pem` 的 `NotAfter`;解析失败的单条跳过并记入 `warnings`。
|
||||
- **应用侧**:与 Fonu 的证书列表交叉核对,输出「Fonu 有 / VPS 有 / 两边都有但到期时间不同」三类;宝塔自行申请的证书也能被看到。
|
||||
- **验收**:过期证书 `days_left < 0` 并标红;Fonu 未推送过的域名出现在「VPS 本地已有」分组。
|
||||
- **应用侧**:与 Havline 的证书列表交叉核对,输出「Havline 有 / VPS 有 / 两边都有但到期时间不同」三类;宝塔自行申请的证书也能被看到。
|
||||
- **验收**:过期证书 `days_left < 0` 并标红;Havline 未推送过的域名出现在「VPS 本地已有」分组。
|
||||
|
||||
### 5.2 reload 失败原因回传
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
|
||||
1. **VPS 防火墙规则读写**(ufw / firewalld / 宝塔面板差异大、需要 root,误操作会直接断网)——最多做只读展示。
|
||||
2. **任意命令执行端点**——agent 目前只有 token 鉴权,开放 shell 等于把 VPS 交出去。
|
||||
3. **agent 自升级**——现有链路(Fonu 内嵌二进制 + SSH 上传 + 重启 systemd)已经闭环,自升级只省掉存 SSH 凭据,收益有限,还要额外处理下载源与签名校验。
|
||||
3. **agent 自升级**——现有链路(Havline 内嵌二进制 + SSH 上传 + 重启 systemd)已经闭环,自升级只省掉存 SSH 凭据,收益有限,还要额外处理下载源与签名校验。
|
||||
|
||||
---
|
||||
|
||||
@@ -230,18 +230,18 @@
|
||||
### 7.1 agent 侧(每新增一个端点都要走完)
|
||||
|
||||
1. `internal/agent/server.go`:加 handler + `mux.HandleFunc(...)`,并在 `AgentVersion` 上方按现有约定补一行版本说明(例如 `// 0.10.0:新增 /api/v1/metrics 资源采集与 /api/v1/logs/nginx 日志读取。`),把常量递增到 **`0.10.0`**。
|
||||
2. 重编两个架构的内嵌二进制(Fonu 编译期嵌入,必须是真实二进制而不是占位文件):
|
||||
2. 重编两个架构的内嵌二进制(Havline 编译期嵌入,必须是真实二进制而不是占位文件):
|
||||
|
||||
```powershell
|
||||
$env:GOOS='linux'; $env:CGO_ENABLED='0'; $env:GOARCH='amd64'
|
||||
go build -trimpath -o internal/frp/agentbin/fonu-agent-linux-amd64.new ./cmd/fonu-agent
|
||||
go build -trimpath -o internal/frp/agentbin/havline-agent-linux-amd64.new ./cmd/havline-agent
|
||||
$env:GOARCH='arm64'
|
||||
go build -trimpath -o internal/frp/agentbin/fonu-agent-linux-arm64.new ./cmd/fonu-agent
|
||||
Move-Item -Force internal/frp/agentbin/fonu-agent-linux-amd64.new internal/frp/agentbin/fonu-agent-linux-amd64
|
||||
Move-Item -Force internal/frp/agentbin/fonu-agent-linux-arm64.new internal/frp/agentbin/fonu-agent-linux-arm64
|
||||
go build -trimpath -o internal/frp/agentbin/havline-agent-linux-arm64.new ./cmd/havline-agent
|
||||
Move-Item -Force internal/frp/agentbin/havline-agent-linux-amd64.new internal/frp/agentbin/havline-agent-linux-amd64
|
||||
Move-Item -Force internal/frp/agentbin/havline-agent-linux-arm64.new internal/frp/agentbin/havline-agent-linux-arm64
|
||||
```
|
||||
|
||||
3. 重启本机 Fonu(加载新内嵌二进制)→ VPS 上「升级 Agent」→ 用 `GET /api/v1/status` 的 `agent_version` 确认已是 `0.10.0`。
|
||||
3. 重启本机 Havline(加载新内嵌二进制)→ VPS 上「升级 Agent」→ 用 `GET /api/v1/status` 的 `agent_version` 确认已是 `0.10.0`。
|
||||
|
||||
### 7.2 应用侧(每个端点对应一条链路)
|
||||
|
||||
@@ -260,10 +260,10 @@ Move-Item -Force internal/frp/agentbin/fonu-agent-linux-arm64.new internal/frp/a
|
||||
|
||||
| 风险 | 说明 | 应对 |
|
||||
| --- | --- | --- |
|
||||
| **新老版本错配** | Fonu 已升级、VPS 上的 agent 仍是旧版 | 应用侧对新端点统一做 404/405 降级,前端显示「请升级 Agent」;不改动已有端点的行为 |
|
||||
| **新老版本错配** | Havline 已升级、VPS 上的 agent 仍是旧版 | 应用侧对新端点统一做 404/405 降级,前端显示「请升级 Agent」;不改动已有端点的行为 |
|
||||
| **只读端点的权限** | `/var/log/nginx` 与证书目录可能不允许 agent 运行用户读取 | 端点返回明确的 403 + 路径,前端提示「agent 无读取权限」;安装脚本里补充读权限说明 |
|
||||
| **`metrics` 采样抖动** | 高频调用时 CPU 差值失真 | 间隔 < 1 秒直接返回上次结果;前端按 15 秒轮询(与本地仪表盘一致) |
|
||||
| **全量重部署覆盖手工改动** | 会覆盖 VPS 上手工编辑过的 `fonu-*.conf` | 按钮带二次确认 + 差异预览;`RouteConf` 回读内容先展示再覆盖 |
|
||||
| **回滚** | agent 行为变更后需要退回旧版本 | agent 二进制随 Fonu 版本走:用旧版 Fonu 的「升级 Agent」即可把旧二进制重新上传;新增端点均为**纯增量**,不改变已有端点语义,应用侧回滚也无需数据迁移 |
|
||||
| **全量重部署覆盖手工改动** | 会覆盖 VPS 上手工编辑过的 `havline-*.conf` | 按钮带二次确认 + 差异预览;`RouteConf` 回读内容先展示再覆盖 |
|
||||
| **回滚** | agent 行为变更后需要退回旧版本 | agent 二进制随 Havline 版本走:用旧版 Havline 的「升级 Agent」即可把旧二进制重新上传;新增端点均为**纯增量**,不改变已有端点语义,应用侧回滚也无需数据迁移 |
|
||||
|
||||
**总原则**:新增端点一律只读或纯增量、不改变已有端点语义;所有 agent 交互都保持「Fonu 主动拉取、agent 被动应答」的现有模型,不引入 agent 主动上报。
|
||||
**总原则**:新增端点一律只读或纯增量、不改变已有端点语义;所有 agent 交互都保持「Havline 主动拉取、agent 被动应答」的现有模型,不引入 agent 主动上报。
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
推荐采用**自动为主、安全优先、旧模式显式兼容**的三层策略:
|
||||
|
||||
1. **默认模式:SSH 隧道 + 回环监听**
|
||||
- `fonu-agent` 默认只监听 `127.0.0.1:7700`。
|
||||
- 中央 Fonu 使用现有 SSH 凭据建立本地转发:
|
||||
- `havline-agent` 默认只监听 `127.0.0.1:7700`。
|
||||
- 中央 Havline 使用现有 SSH 凭据建立本地转发:
|
||||
|
||||
```text
|
||||
中央 Fonu 127.0.0.1:<随机本地端口>
|
||||
中央 Havline 127.0.0.1:<随机本地端口>
|
||||
→ SSH 加密连接
|
||||
→ VPS 127.0.0.1:7700
|
||||
→ fonu-agent
|
||||
→ havline-agent
|
||||
```
|
||||
|
||||
- 不开放公网 Agent 端口,不需要域名、证书、云安全组或防火墙修改。
|
||||
@@ -30,7 +30,7 @@
|
||||
- Agent 仍只监听回环地址。
|
||||
- HTTPS 不作为首次安装选项;Agent 安全安装完成后,用户可在「公网 Agent」页切换到该高级模式。
|
||||
- 切换时自动配置 Nginx HTTPS 反代,并使用自签名证书。
|
||||
- 中央 Fonu 首次安装时记录证书指纹,后续按指纹校验。
|
||||
- 中央 Havline 首次安装时记录证书指纹,后续按指纹校验。
|
||||
- 适用于希望免维护常驻 SSH 隧道、且愿意开放一个 HTTPS 端口的用户。
|
||||
|
||||
3. **兼容模式:显式公网明文 HTTP**
|
||||
@@ -38,7 +38,7 @@
|
||||
- UI 持续显示高危警告。
|
||||
- 不允许自动降级到该模式。
|
||||
|
||||
默认安装与升级过程不需要用户设置 `FONU_AGENT_LISTEN`、TLS 证书或防火墙规则。
|
||||
默认安装与升级过程不需要用户设置 `HAVLINE_AGENT_LISTEN`、TLS 证书或防火墙规则。
|
||||
|
||||
---
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
当前入口:
|
||||
|
||||
```go
|
||||
listenv := flag.String("listen", envOr("FONU_AGENT_LISTEN", ":7700"), ...)
|
||||
listenv := flag.String("listen", envOr("HAVLINE_AGENT_LISTEN", ":7700"), ...)
|
||||
```
|
||||
|
||||
`internal/agent.NewServer` 空配置时同样回落到:
|
||||
@@ -73,7 +73,7 @@ cfg.ListenAddr = ":7700"
|
||||
API: http://<VPS-IP>:7700
|
||||
```
|
||||
|
||||
### 2.3 中央 Fonu 连接
|
||||
### 2.3 中央 Havline 连接
|
||||
|
||||
安装完成后,`AgentInstall` 写入:
|
||||
|
||||
@@ -81,7 +81,7 @@ cfg.ListenAddr = ":7700"
|
||||
AgentURL = fmt.Sprintf("http://%s:%d", server.SSHHost, 7700)
|
||||
```
|
||||
|
||||
中央 Fonu 后续直接访问 VPS 公网 IP 的 7700 端口。
|
||||
中央 Havline 后续直接访问 VPS 公网 IP 的 7700 端口。
|
||||
|
||||
### 2.4 当前问题
|
||||
|
||||
@@ -99,13 +99,13 @@ AgentURL = fmt.Sprintf("http://%s:%d", server.SSHHost, 7700)
|
||||
### 3.1 必须实现
|
||||
|
||||
1. 新安装默认不监听公网网卡。
|
||||
2. 安装过程自动建立中央 Fonu 到 Agent 的安全连接。
|
||||
2. 安装过程自动建立中央 Havline 到 Agent 的安全连接。
|
||||
3. 不需要用户手工配置网络或证书。
|
||||
4. Agent 升级时可自动迁移旧监听配置。
|
||||
5. 旧部署仍可显式使用公网 HTTP。
|
||||
6. 传输失败时给出可执行的诊断信息。
|
||||
7. 中央 Fonu 重启后可自动恢复连接。
|
||||
8. Agent 与中央 Fonu 的版本兼容策略保持清晰。
|
||||
7. 中央 Havline 重启后可自动恢复连接。
|
||||
8. Agent 与中央 Havline 的版本兼容策略保持清晰。
|
||||
|
||||
### 3.2 非目标
|
||||
|
||||
@@ -155,7 +155,7 @@ AgentURL = fmt.Sprintf("http://%s:%d", server.SSHHost, 7700)
|
||||
| 防火墙 | 需要放行一个 HTTPS 端口 |
|
||||
| 证书 | 自动生成和固定,不需要公网 CA |
|
||||
| 复杂度 | 中 |
|
||||
| 主要优势 | 中央 Fonu 可直接连接,不依赖常驻 SSH |
|
||||
| 主要优势 | 中央 Havline 可直接连接,不依赖常驻 SSH |
|
||||
| 主要限制 | 云安全组仍需用户放行端口 |
|
||||
|
||||
结论:作为可选直连模式。
|
||||
@@ -181,26 +181,26 @@ AgentURL = fmt.Sprintf("http://%s:%d", server.SSHHost, 7700)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Web[Fonu Web] --> API[Fonu 后端]
|
||||
Web[Havline Web] --> API[Havline 后端]
|
||||
API --> Tunnel[SSH 隧道管理器]
|
||||
Tunnel -->|127.0.0.1:自动分配端口| SSH[SSH 加密连接]
|
||||
SSH --> VPSLoopback[VPS 127.0.0.1:7700]
|
||||
VPSLoopback --> Agent[fonu-agent]
|
||||
VPSLoopback --> Agent[havline-agent]
|
||||
```
|
||||
|
||||
### 5.2 角色职责
|
||||
|
||||
| 组件 | 职责 |
|
||||
| --- | --- |
|
||||
| Fonu 后端 | 决定 transport、启动隧道、调用 AgentClient |
|
||||
| Havline 后端 | 决定 transport、启动隧道、调用 AgentClient |
|
||||
| 隧道管理器 | 建立 SSH 本地转发、保持连接、重连、健康检查 |
|
||||
| fonu-agent | 默认只监听回环,不再负责公网暴露 |
|
||||
| 安装脚本 | 写入 `FONU_AGENT_LISTEN=127.0.0.1:7700`,禁止默认可公开 |
|
||||
| havline-agent | 默认只监听回环,不再负责公网暴露 |
|
||||
| 安装脚本 | 写入 `HAVLINE_AGENT_LISTEN=127.0.0.1:7700`,禁止默认可公开 |
|
||||
| UI | 显示安全模式、连接状态、可选迁移和显式旧模式警告 |
|
||||
|
||||
### 5.3 连接抽象
|
||||
|
||||
中央 Fonu 不再把“公网 IP + 7700”当作 Agent 的唯一定义。新增 transport 概念:
|
||||
中央 Havline 不再把“公网 IP + 7700”当作 Agent 的唯一定义。新增 transport 概念:
|
||||
|
||||
```text
|
||||
tunnel SSH 本地转发,默认
|
||||
@@ -310,7 +310,7 @@ const defaultListenAddr = "127.0.0.1:7700"
|
||||
|
||||
以下位置同步:
|
||||
|
||||
- `cmd/fonu-agent/main.go`
|
||||
- `cmd/havline-agent/main.go`
|
||||
- `internal/agent.NewServer`
|
||||
- `scripts/agent-install.sh`
|
||||
- `internal/frp/agent_installer.go` 内嵌安装脚本
|
||||
@@ -329,7 +329,7 @@ VPS公网IP:7700
|
||||
则必须显式设置:
|
||||
|
||||
```dotenv
|
||||
FONU_AGENT_ALLOW_REMOTE=1
|
||||
HAVLINE_AGENT_ALLOW_REMOTE=1
|
||||
```
|
||||
|
||||
否则 Agent 拒绝启动并输出:
|
||||
@@ -337,21 +337,21 @@ FONU_AGENT_ALLOW_REMOTE=1
|
||||
```text
|
||||
检测到 Agent 将监听非回环地址,但未显式允许远程访问。
|
||||
默认安全模式只监听 127.0.0.1:7700。
|
||||
如确认需要公网明文访问,请设置 FONU_AGENT_ALLOW_REMOTE=1。
|
||||
如确认需要公网明文访问,请设置 HAVLINE_AGENT_ALLOW_REMOTE=1。
|
||||
```
|
||||
|
||||
### 7.3 旧配置迁移
|
||||
|
||||
旧版本 `agent.env` 可能没有 `FONU_AGENT_LISTEN`,此时:
|
||||
旧版本 `agent.env` 可能没有 `HAVLINE_AGENT_LISTEN`,此时:
|
||||
|
||||
1. 新 Agent 默认监听回环;
|
||||
2. 安装脚本显式写入:
|
||||
|
||||
```dotenv
|
||||
FONU_AGENT_LISTEN=127.0.0.1:7700
|
||||
HAVLINE_AGENT_LISTEN=127.0.0.1:7700
|
||||
```
|
||||
|
||||
3. 中央 Fonu 在执行「升级 Agent」时同步建立隧道;
|
||||
3. 中央 Havline 在执行「升级 Agent」时同步建立隧道;
|
||||
4. 升级完成后切换 transport;
|
||||
5. 旧公网地址从 `agent_url` 中移除;
|
||||
6. 不停留手工维护窗口。
|
||||
@@ -376,12 +376,12 @@ Agent `/api/v1/status` 新增:
|
||||
### 8.1 新安装
|
||||
|
||||
1. 用户填写 VPS SSH 信息。
|
||||
2. 中央 Fonu 通过 SSH 探测系统与 Nginx。
|
||||
2. 中央 Havline 通过 SSH 探测系统与 Nginx。
|
||||
3. 分配 `agent_local_port`。
|
||||
4. 上传 Agent 二进制。
|
||||
5. 安装脚本写入:
|
||||
- Token;
|
||||
- `FONU_AGENT_LISTEN=127.0.0.1:7700`;
|
||||
- `HAVLINE_AGENT_LISTEN=127.0.0.1:7700`;
|
||||
- systemd 服务。
|
||||
6. 启动 Agent。
|
||||
7. 通过 SSH 本地转发建立隧道。
|
||||
@@ -413,7 +413,7 @@ Agent `/api/v1/status` 新增:
|
||||
|
||||
1. 读取启用管理且 transport=tunnel 的服务端。
|
||||
2. 逐个调用 `Tunnel.Ensure`。
|
||||
3. 启动失败不阻塞整个 Fonu 启动。
|
||||
3. 启动失败不阻塞整个 Havline 启动。
|
||||
4. UI 显示重连状态。
|
||||
5. 后台按指数退避继续重试。
|
||||
|
||||
@@ -426,7 +426,7 @@ Agent `/api/v1/status` 新增:
|
||||
- 不希望长期维护 SSH 隧道;
|
||||
- 可以接受开放一个 HTTPS 端口;
|
||||
- 没有域名或不想申请公网证书;
|
||||
- 希望 Agent API 可被中央 Fonu直接访问。
|
||||
- 希望 Agent API 可被中央 Havline直接访问。
|
||||
|
||||
### 9.2 安装后切换流程
|
||||
|
||||
@@ -436,8 +436,8 @@ Agent `/api/v1/status` 新增:
|
||||
2. 切换流程生成自签名证书:
|
||||
|
||||
```text
|
||||
/etc/fonu-agent/tls/agent.crt
|
||||
/etc/fonu-agent/tls/agent.key
|
||||
/etc/havline-agent/tls/agent.crt
|
||||
/etc/havline-agent/tls/agent.key
|
||||
```
|
||||
|
||||
3. 自动寻找可用端口:
|
||||
@@ -449,8 +449,8 @@ Agent `/api/v1/status` 新增:
|
||||
```nginx
|
||||
server {
|
||||
listen 7443 ssl;
|
||||
ssl_certificate /etc/fonu-agent/tls/agent.crt;
|
||||
ssl_certificate_key /etc/fonu-agent/tls/agent.key;
|
||||
ssl_certificate /etc/havline-agent/tls/agent.crt;
|
||||
ssl_certificate_key /etc/havline-agent/tls/agent.key;
|
||||
|
||||
location /api/v1/ {
|
||||
proxy_pass http://127.0.0.1:7700;
|
||||
@@ -459,7 +459,7 @@ Agent `/api/v1/status` 新增:
|
||||
```
|
||||
|
||||
5. 返回证书 SHA-256 指纹。
|
||||
6. 中央 Fonu 保存 pin,并使用自定义校验。
|
||||
6. 中央 Havline 保存 pin,并使用自定义校验。
|
||||
|
||||
### 9.3 TLS 固定
|
||||
|
||||
@@ -494,7 +494,7 @@ VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||
云厂商安全组始终不能自动修改。UI 必须提示:
|
||||
|
||||
```text
|
||||
请在云控制台放行 TCP 7443。主机防火墙需要你确认后由 Fonu 执行,Fonu 不会修改云安全组。
|
||||
请在云控制台放行 TCP 7443。主机防火墙需要你确认后由 Havline 执行,Havline 不会修改云安全组。
|
||||
```
|
||||
|
||||
---
|
||||
@@ -504,11 +504,11 @@ VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||
仅用于旧部署迁移或用户明确要求:
|
||||
|
||||
```dotenv
|
||||
FONU_AGENT_LISTEN=:7700
|
||||
FONU_AGENT_ALLOW_REMOTE=1
|
||||
HAVLINE_AGENT_LISTEN=:7700
|
||||
HAVLINE_AGENT_ALLOW_REMOTE=1
|
||||
```
|
||||
|
||||
中央 Fonu UI 必须:
|
||||
中央 Havline UI 必须:
|
||||
|
||||
- 显示“公网明文传输”红色状态;
|
||||
- 不在新安装中默认选择;
|
||||
@@ -638,7 +638,7 @@ Agent 监听:127.0.0.1:7700
|
||||
### 14.1 单元测试
|
||||
|
||||
- 默认监听地址是回环;
|
||||
- 非回环监听缺少 `FONU_AGENT_ALLOW_REMOTE` 时拒绝启动;
|
||||
- 非回环监听缺少 `HAVLINE_AGENT_ALLOW_REMOTE` 时拒绝启动;
|
||||
- 回环监听无需额外变量;
|
||||
- `agent_transport` 序列化与旧数据兼容;
|
||||
- 隧道本地端口分配、冲突与释放;
|
||||
@@ -661,7 +661,7 @@ Agent 监听:127.0.0.1:7700
|
||||
至少覆盖:
|
||||
|
||||
- Debian / Ubuntu / CentOS;
|
||||
- fnOS 中的 Fonu;
|
||||
- fnOS 中的 Havline;
|
||||
- 宝塔 Nginx;
|
||||
- 已存在 80 / 443 / 7443 占用的 VPS;
|
||||
- 云安全组只开放 SSH 的 VPS;
|
||||
@@ -673,7 +673,7 @@ Agent 监听:127.0.0.1:7700
|
||||
- 公网扫描不到 7700;
|
||||
- 无需新增云安全组规则;
|
||||
- UI 显示隧道已连接;
|
||||
- Fonu 重启后自动恢复;
|
||||
- Havline 重启后自动恢复;
|
||||
- 「升级 Agent」可以自动迁移旧配置;
|
||||
- 失败时不会丢失旧连接。
|
||||
|
||||
@@ -684,7 +684,7 @@ Agent 监听:127.0.0.1:7700
|
||||
### 阶段 1:默认回环与防误开
|
||||
|
||||
- Agent 默认监听改为 `127.0.0.1:7700`;
|
||||
- 非回环监听要求 `FONU_AGENT_ALLOW_REMOTE=1`;
|
||||
- 非回环监听要求 `HAVLINE_AGENT_ALLOW_REMOTE=1`;
|
||||
- 安装脚本显式写入监听地址;
|
||||
- UI 显示监听范围。
|
||||
|
||||
@@ -729,7 +729,7 @@ Agent 监听:127.0.0.1:7700
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | SSH 隧道作为唯一默认模式 | 首次安装不提供模式选择,统一安装为回环监听 + SSH 隧道;HTTPS 直连只作为安装后的高级切换项 | 现有安装流程已经依赖 SSH;默认路径必须最少输入、最少网络要求 |
|
||||
| 2 | 隧道本地端口使用固定范围 | 固定为 `17700-18700`,绑定 `127.0.0.1`;分配结果持久化;启动时先复用持久化端口,再在范围内顺序查找空闲端口;范围耗尽时明确报错,不使用临时端口 | 固定范围便于排查和审计;持久化避免每次重启变化;不回退临时端口避免地址漂移 |
|
||||
| 3 | 隧道跟随主应用生命周期 | 使用应用内 `agent_tunnel` 管理器,在 Fonu 启动后按服务端恢复;应用退出时统一关闭;断线后指数退避重连;首版不新增独立 systemd 服务 | 当前项目是单进程控制面,调度器、FRP、监控均在同一生命周期内,新增守护进程会扩大部署复杂度 |
|
||||
| 3 | 隧道跟随主应用生命周期 | 使用应用内 `agent_tunnel` 管理器,在 Havline 启动后按服务端恢复;应用退出时统一关闭;断线后指数退避重连;首版不新增独立 systemd 服务 | 当前项目是单进程控制面,调度器、FRP、监控均在同一生命周期内,新增守护进程会扩大部署复杂度 |
|
||||
| 4 | 公网 HTTP 保留整个 1.x 兼容期 | 自 `1.4.0` 起不再允许新安装选择 HTTP;存量连接继续可用但显示高危警告并提供迁移;计划在 `2.0.0` 移除 | 需要给旧 Agent 和环境留出迁移窗口,同时从版本边界上明确技术债的截止点 |
|
||||
| 5 | HTTPS 自签名证书使用短期自动轮换 | ECDSA P-256,有效期 365 天;剩余 30 天时自动轮换;轮换期间同时接受旧 pin 和新 pin,双 pin 窗口 7 天;优先通过现有 SSH 通道获取新指纹后再切换 | 避免长期证书失管;双 pin 防止轮换瞬间把所有中央连接锁死 |
|
||||
| 6 | 首版不自动改主机防火墙 | 首先只检测、展示和提示;SSH 隧道模式本身不需要放行新端口;HTTPS 高级模式后续再提供显式的“一键放行主机防火墙”操作,并始终不修改云厂商安全组 | 防火墙和云安全组属于高风险外部状态;首版先把安全默认值落地,避免自动变更导致 SSH 或现有服务失联 |
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
- 手动部署 / 移除单条规则的反代(部署时自动推证书、自动读最新 vhost 端口);
|
||||
- VPS 当前已加载路由展示(`agentStatus.routes`);
|
||||
- 反代文件按 `fonu-<域名>.conf` 写入 Nginx 实际加载目录(`-T` 试探-验证)。
|
||||
- 反代文件按 `havline-<域名>.conf` 写入 Nginx 实际加载目录(`-T` 试探-验证)。
|
||||
|
||||
---
|
||||
|
||||
@@ -103,7 +103,7 @@ curl -s -o /dev/null -w "%{http_code}" -H "Host: <域名>" http://127.0.0.1:<vho
|
||||
|
||||
### 8. 部署历史与回滚
|
||||
|
||||
每次部署在 agent 侧留 `fonu-<域名>.conf.bak-<时间戳>`(保留最近 N 份),
|
||||
每次部署在 agent 侧留 `havline-<域名>.conf.bak-<时间戳>`(保留最近 N 份),
|
||||
页面提供「查看历史 / 回滚到此版本」。
|
||||
|
||||
- 误操作(比如改坏了 upstream)30 秒恢复;
|
||||
@@ -112,7 +112,7 @@ curl -s -o /dev/null -w "%{http_code}" -H "Host: <域名>" http://127.0.0.1:<vho
|
||||
### 9. 域名健康检查面板
|
||||
|
||||
页面顶部一行:每个已部署域名的公网可达性(HTTPS 状态码 + 证书剩余天数),
|
||||
由 Fonu 侧定时探测(复用服务端诊断的 TTL 缓存模式)。
|
||||
由 Havline 侧定时探测(复用服务端诊断的 TTL 缓存模式)。
|
||||
|
||||
- 证书临期/隧道断链主动可见;
|
||||
- 成本:中。
|
||||
@@ -128,7 +128,7 @@ curl -s -o /dev/null -w "%{http_code}" -H "Host: <域名>" http://127.0.0.1:<vho
|
||||
|
||||
### 11. 流量统计接入
|
||||
|
||||
frps dashboard 已有连接数/流量数据(Fonu 正在拉取),按域名聚合展示到反代列表,
|
||||
frps dashboard 已有连接数/流量数据(Havline 正在拉取),按域名聚合展示到反代列表,
|
||||
可见每条反代的实际使用量。
|
||||
|
||||
- 成本:中。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 公网反代「部署设置」对齐本地反代 —— 方案设计
|
||||
|
||||
> 目标:把本地反向代理(`ProxyView.vue` 规则弹窗)中真正有价值的安全与运维设置,按公网反代的语义迁移到
|
||||
> 「公网反代」页(`/public-proxy`,`ProxyRemoteView.vue`)的「部署设置」中,由 fonu-agent 生成对应的 Nginx vhost 配置。
|
||||
> 「公网反代」页(`/public-proxy`,`ProxyRemoteView.vue`)的「部署设置」中,由 havline-agent 生成对应的 Nginx vhost 配置。
|
||||
>
|
||||
> 原则:**公网反代只管「公网入口」这一层**。凡是会破坏 frps vhost 路由、或本质属于「本机内网分流」的选项,
|
||||
> 一律不迁移(见「明确不做」一节)。
|
||||
@@ -22,7 +22,7 @@
|
||||
-
|
||||
**高级**:仅 TLS 1.3、安全响应头、限流、连接数限制
|
||||
|
||||
第一梯队 7 项都是 agent 模板内改动 + Fonu 参数透传,风险低、见效快;第二梯队 3 项需要 agent 多写一个 http 上下文文件。说一声我从第一梯队开始做,或者指定要哪几项。
|
||||
第一梯队 7 项都是 agent 模板内改动 + Havline 参数透传,风险低、见效快;第二梯队 3 项需要 agent 多写一个 http 上下文文件。说一声我从第一梯队开始做,或者指定要哪几项。
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
- `basic_auth_user`(密码由 agent 自动生成并落盘 htpasswd);
|
||||
- 默认头:Host / X-Real-IP / X-Forwarded-For。
|
||||
|
||||
Fonu 侧对应链路:`ProxyOptions`(`internal/frp/model.go`)→ `DeployAgentRoute` → `AgentClient.SyncRouteEx`
|
||||
Havline 侧对应链路:`ProxyOptions`(`internal/frp/model.go`)→ `DeployAgentRoute` → `AgentClient.SyncRouteEx`
|
||||
→ `PUT /api/v1/routes`。
|
||||
|
||||
---
|
||||
@@ -73,9 +73,9 @@ Fonu 侧对应链路:`ProxyOptions`(`internal/frp/model.go`)→ `DeployAge
|
||||
|
||||
| # | 功能 | 关键约束 |
|
||||
| --- | --- | --- |
|
||||
| 8 | **请求限流** | `limit_req_zone` 只能出现在 `http` 块。agent 需额外生成 `fonu-limits.conf`(宝塔将 vhost 目录 include 在 `http{}` 内,因此该文件顶层指令即 http 上下文),各域名 server 内 `limit_req zone=fonu_<域名>_req burst=N nodelay;` |
|
||||
| 9 | **连接数限制** | 同上,`limit_conn_zone` + `limit_conn fonu_<域名>_conn N;` |
|
||||
| 10 | **仅中国大陆 IP** | 需 `geo` 模块 + 中国 IP 段下发到 VPS(Fonu 已有 china cidr 模块,7456 条 v4),下发体积最大、成本最高,建议最后做 |
|
||||
| 8 | **请求限流** | `limit_req_zone` 只能出现在 `http` 块。agent 需额外生成 `havline-limits.conf`(宝塔将 vhost 目录 include 在 `http{}` 内,因此该文件顶层指令即 http 上下文),各域名 server 内 `limit_req zone=havline_<域名>_req burst=N nodelay;` |
|
||||
| 9 | **连接数限制** | 同上,`limit_conn_zone` + `limit_conn havline_<域名>_conn N;` |
|
||||
| 10 | **仅中国大陆 IP** | 需 `geo` 模块 + 中国 IP 段下发到 VPS(Havline 已有 china cidr 模块,7456 条 v4),下发体积最大、成本最高,建议最后做 |
|
||||
|
||||
### 明确不做(字段 / 语义不匹配)
|
||||
|
||||
@@ -109,13 +109,13 @@ Fonu 侧对应链路:`ProxyOptions`(`internal/frp/model.go`)→ `DeployAge
|
||||
|
||||
- 这些字段**只作用于公网 Nginx 模板,绝不写入 frpc.toml**(与现有 `websocket` / `redirect_https` 等一致,在 `ProxyOptions` 上以注释标明);
|
||||
- 「启用 HTTPS」采用**反向字段 `https_disabled`**:字段缺省时保持现有行为(证书推送成功即启用 TLS),
|
||||
保证新 Fonu + 旧 agent、旧数据不出现行为突变。
|
||||
保证新 Havline + 旧 agent、旧数据不出现行为突变。
|
||||
|
||||
---
|
||||
|
||||
## 四、实施步骤
|
||||
|
||||
### 阶段 1:第一梯队后端(agent + Fonu)
|
||||
### 阶段 1:第一梯队后端(agent + Havline)
|
||||
|
||||
1. `internal/agent/server.go`
|
||||
- `routeRequest` 增加第一梯队字段;
|
||||
@@ -149,8 +149,8 @@ Fonu 侧对应链路:`ProxyOptions`(`internal/frp/model.go`)→ `DeployAge
|
||||
|
||||
### 阶段 3:第二梯队(按需)
|
||||
|
||||
7. agent 增加 `fonu-limits.conf` 生成器(http 上下文 zone 定义)+ 每域名 server 引用;
|
||||
8. 中国 IP:Fonu 将 china cidr 列表推送到 agent(`/var/lib/fonu-agent/geo/china.conf`),agent 生成 `geo` 块。
|
||||
7. agent 增加 `havline-limits.conf` 生成器(http 上下文 zone 定义)+ 每域名 server 引用;
|
||||
8. 中国 IP:Havline 将 china cidr 列表推送到 agent(`/var/lib/havline-agent/geo/china.conf`),agent 生成 `geo` 块。
|
||||
|
||||
---
|
||||
|
||||
@@ -159,7 +159,7 @@ Fonu 侧对应链路:`ProxyOptions`(`internal/frp/model.go`)→ `DeployAge
|
||||
每次改动后全量跑(项目既有闭环):
|
||||
|
||||
```powershell
|
||||
cd f:\Fonu-main\Fonu-1.0.2
|
||||
cd f:\Havline-main\Havline-1.0.2
|
||||
gofmt -w internal/agent/server.go internal/frp/model.go internal/frp/agent_client.go internal/frp/service.go
|
||||
go build ./... ; go vet ./internal/frp ./internal/agent ./internal/api
|
||||
go test ./internal/frp ./internal/api
|
||||
@@ -170,7 +170,7 @@ agent 二进制需按既有流程交叉编译(amd64 + arm64)并升级:
|
||||
|
||||
```powershell
|
||||
$env:GOOS='linux'; $env:GOARCH='amd64'; $env:CGO_ENABLED='0'
|
||||
go build -trimpath -o internal/frp/agentbin/fonu-agent-linux-amd64.new ./cmd/fonu-agent
|
||||
go build -trimpath -o internal/frp/agentbin/havline-agent-linux-amd64.new ./cmd/havline-agent
|
||||
# arm64 同理;Move-Item 覆盖后,在「公网 Agent」页执行「升级 Agent」
|
||||
```
|
||||
|
||||
@@ -178,7 +178,7 @@ VPS 侧实测(部署 7023 规则后):
|
||||
|
||||
```bash
|
||||
nginx -t # 语法
|
||||
cat /www/server/panel/vhost/nginx/fonu-7023.36800.cc.conf # 配置内容(或页面「查看配置」)
|
||||
cat /www/server/panel/vhost/nginx/havline-7023.36800.cc.conf # 配置内容(或页面「查看配置」)
|
||||
curl -I http://7023.36800.cc # 强制跳转 → 301
|
||||
curl -sI -H 'Host: 7023.36800.cc' http://127.0.0.1:9080 # 回源自检
|
||||
curl -k -sI https://7023.36800.cc # 响应头(HSTS / X-Frame-Options)
|
||||
@@ -196,8 +196,8 @@ for i in $(seq 1 50); do curl -s -o /dev/null -w '%{http_code} ' http://7023.368
|
||||
|
||||
| 场景 | 期望行为 |
|
||||
| --- | --- |
|
||||
| 新 Fonu + 旧 agent | 旧 agent 忽略未知 JSON 字段(Go 默认行为),新选项静默不生效 → **部署前必须先「升级 Agent」**,页面提示 |
|
||||
| 旧 Fonu + 新 agent | 新字段缺省,行为与现状一致(`https_disabled` 缺省 = 启用 TLS) |
|
||||
| 新 Havline + 旧 agent | 旧 agent 忽略未知 JSON 字段(Go 默认行为),新选项静默不生效 → **部署前必须先「升级 Agent」**,页面提示 |
|
||||
| 旧 Havline + 新 agent | 新字段缺省,行为与现状一致(`https_disabled` 缺省 = 启用 TLS) |
|
||||
| frps v0.68.0 / v0.71.0 | 与本方案无关(作用于 Nginx vhost,不依赖 dashboard API) |
|
||||
| 回滚 | 关闭对应开关后重新部署即可恢复;agent 侧每域名配置为整文件覆盖写,无残留 |
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
| 框架 | Vue 3 + `<script setup lang="ts">` + Vite 6 |
|
||||
| 组件库 | **naive-ui**(`n-button` `n-select` `n-tabs` `n-modal` `n-popconfirm` `n-alert` `n-spin` `n-icon` `n-tag` `n-tooltip` `n-form`) |
|
||||
| 图标 | `@vicons/ionicons5` |
|
||||
| 令牌系统 | `web/src/styles/variables.css`:`:root` + `html.dark` 双主题,`--fonu-*` 前缀 |
|
||||
| 共享组件 | `PageHeader` `FonuCard` `StatCard` `StatusBadge` `EmptyState` `LoadError` |
|
||||
| 令牌系统 | `web/src/styles/variables.css`:`:root` + `html.dark` 双主题,`--havline-*` 前缀 |
|
||||
| 共享组件 | `PageHeader` `HavlineCard` `StatCard` `StatusBadge` `EmptyState` `LoadError` |
|
||||
| 工具 | `utils/format.ts`(`formatBytes/formatRate/formatDate/formatRelativeTime/…`)、`utils/status.ts`(`StatusKind`) |
|
||||
|
||||
**结论**:沿用既有体系(naive-ui + `--fonu-*` + 共享组件),**不引入**第二套框架/令牌。
|
||||
**结论**:沿用既有体系(naive-ui + `--havline-*` + 共享组件),**不引入**第二套框架/令牌。
|
||||
|
||||
---
|
||||
|
||||
@@ -84,20 +84,20 @@
|
||||
|
||||
| 语义 | 令牌 | 用于 | 判定 |
|
||||
| --- | --- | --- | --- |
|
||||
| 正常 | `--fonu-brand`(`-soft`/`-text`) | 在线、运行中、监听中、成功输出 | 无需干预 |
|
||||
| 待处理 | `--fonu-warning` | frps 未运行、端口未监听、frps 未安装、开机自启未启用 | 可一键恢复 |
|
||||
| 需人工干预 | `--fonu-error` | Agent 未连接、Nginx 缺失、操作失败、卸载确认 | 需排查/不可逆 |
|
||||
| 中性 | `--fonu-text-secondary` / `--fonu-text-muted` | 字段名、说明、时间、空态 | — |
|
||||
| 引导 | `--fonu-info`(`-soft`) | 页面级引导(反代入口等) | 非故障 |
|
||||
| 正常 | `--havline-brand`(`-soft`/`-text`) | 在线、运行中、监听中、成功输出 | 无需干预 |
|
||||
| 待处理 | `--havline-warning` | frps 未运行、端口未监听、frps 未安装、开机自启未启用 | 可一键恢复 |
|
||||
| 需人工干预 | `--havline-error` | Agent 未连接、Nginx 缺失、操作失败、卸载确认 | 需排查/不可逆 |
|
||||
| 中性 | `--havline-text-secondary` / `--havline-text-muted` | 字段名、说明、时间、空态 | — |
|
||||
| 引导 | `--havline-info`(`-soft`) | 页面级引导(反代入口等) | 非故障 |
|
||||
|
||||
规则:一屏 error ≤ 1 处;warning 只用于"有目标状态但未达成",不用于"没数据"。亮/暗由 `variables.css` 覆盖,**页面零硬编码色值**。
|
||||
|
||||
**Type(沿用项目字体,显式声明)**
|
||||
|
||||
- 正文 `--fonu-font`,等宽 `--fonu-mono`。skill 禁用 system-ui 系默认栈,但本项目已有设计系统,按 **"MATCH it, never add a second"** 继续使用;换字体属全局决策(改 `variables.css` + 全站回归),不在本页范围。
|
||||
- 页内 4 档字号 / 3 档字重:数值词条 18px/700(`.stat-card__value--sm`)、数值数字 30px/700/`-0.03em`、字段名与卡标签 12–13px/600、说明与时间 12–12.5px/400;路径/命令/版本/日志走 `--fonu-mono` 12.5–13px。
|
||||
- 正文 `--havline-font`,等宽 `--havline-mono`。skill 禁用 system-ui 系默认栈,但本项目已有设计系统,按 **"MATCH it, never add a second"** 继续使用;换字体属全局决策(改 `variables.css` + 全站回归),不在本页范围。
|
||||
- 页内 4 档字号 / 3 档字重:数值词条 18px/700(`.stat-card__value--sm`)、数值数字 30px/700/`-0.03em`、字段名与卡标签 12–13px/600、说明与时间 12–12.5px/400;路径/命令/版本/日志走 `--havline-mono` 12.5–13px。
|
||||
|
||||
**Spacing & Radius**:只用 `--fonu-space-1…6` 与 `--fonu-radius(-sm)`、`--fonu-shadow`;禁止魔数;内联尺寸一律改类(`.agent-select` / `.frps-version-select`)。
|
||||
**Spacing & Radius**:只用 `--havline-space-1…6` 与 `--havline-radius(-sm)`、`--havline-shadow`;禁止魔数;内联尺寸一律改类(`.agent-select` / `.frps-version-select`)。
|
||||
|
||||
---
|
||||
|
||||
@@ -111,8 +111,8 @@
|
||||
│ ● Agent 连接 ● frps 服务 ● 反代路由 ● 端口监听
|
||||
│ 在线/未连接 运行中/未运行 3 条 80✓ 443✓
|
||||
│ <agent 地址> <frps 版本> 健康 2/3 已生效 7000✓ 8080✗
|
||||
├ ② 环境与依赖(Fonu Agent | Nginx | frps 运行时,三列并排)
|
||||
│ Fonu Agent Nginx(本页新增) frps 运行时
|
||||
├ ② 环境与依赖(Havline Agent | Nginx | frps 运行时,三列并排)
|
||||
│ Havline Agent Nginx(本页新增) frps 运行时
|
||||
│ 版本 / 地址 可用性徽标 / 版本 二进制 / 版本
|
||||
│ 凭据:已配置 ✓ 可执行路径 / 配置目录 配置路径
|
||||
│ [探测环境] 错误原文(红) 启动命令 / 开机自启
|
||||
@@ -160,7 +160,7 @@
|
||||
|
||||
**动效**:首屏一次性 `fade-up`(`translateY(4px)→0`,160ms),仅 `onMounted` 触发一次;`prefers-reduced-motion: reduce` 全禁用;除此之外不做散点微动效(不加卡片 hover 位移/脉冲)。
|
||||
|
||||
**无障碍**:状态区 `<section aria-label="运行状态">`;装饰图标 `aria-hidden`;按钮均有可读文本;输出区 `aria-live="polite"`;Tab 顺序 = 选择器 → 刷新 → 状态区(跳过只读)→ 各分区按钮带 → tabs;对比度实测 AA(`--fonu-brand/warning/error/text-muted` 在 `--fonu-surface` 上,亮+暗各一遍);≤760px 按钮 `min-height: 44px`;响应式 `1200+ / 900–1199 / ≤900 / ≤760`(运行状态 4→2→1 列;环境与依赖三列 →≤1199 降 2 列 →≤900 单列,且三列时卡片内短字段(版本/徽标)双列并排、长路径与命令整行,控制卡片高度),**320px 无横向溢出**。
|
||||
**无障碍**:状态区 `<section aria-label="运行状态">`;装饰图标 `aria-hidden`;按钮均有可读文本;输出区 `aria-live="polite"`;Tab 顺序 = 选择器 → 刷新 → 状态区(跳过只读)→ 各分区按钮带 → tabs;对比度实测 AA(`--havline-brand/warning/error/text-muted` 在 `--havline-surface` 上,亮+暗各一遍);≤760px 按钮 `min-height: 44px`;响应式 `1200+ / 900–1199 / ≤900 / ≤760`(运行状态 4→2→1 列;环境与依赖三列 →≤1199 降 2 列 →≤900 单列,且三列时卡片内短字段(版本/徽标)双列并排、长路径与命令整行,控制卡片高度),**320px 无横向溢出**。
|
||||
|
||||
---
|
||||
|
||||
@@ -217,7 +217,7 @@ function pushOutput(source: string, text: string, ok = true) {
|
||||
|
||||
## 九、验收清单
|
||||
|
||||
- [ ] 复用项目真实栈与令牌(naive-ui + `--fonu-*` + 共享组件),未新增第二套
|
||||
- [ ] 复用项目真实栈与令牌(naive-ui + `--havline-*` + 共享组件),未新增第二套
|
||||
- [ ] 单一设计方向(机架面板)落地,颜色/间距/圆角全走令牌,无 banned 字体引入
|
||||
- [ ] 交互状态齐全:hover / pressed / **focused(键盘实测)** / disabled / loading / empty / error
|
||||
- [ ] 无障碍:对比度 AA(亮+暗)、可见焦点环、键盘可完成全部操作、`aria-live` 播报、图标 `aria-hidden`
|
||||
@@ -233,7 +233,7 @@ function pushOutput(source: string, text: string, ok = true) {
|
||||
| --- | --- |
|
||||
| `docs/agent-view-design.md`(v1) | 被本文取代(v1 只是结构搬家,未解决"功能不可见");确认后可删除 |
|
||||
| `docs/agent-view-refactor-plan.md` | 被本文取代 |
|
||||
| 工作区 skill `fonu-page-ui` | 页面 UI 通用标准;本文是它在「公网 Agent」页的具体化 |
|
||||
| 工作区 skill `havline-page-ui` | 页面 UI 通用标准;本文是它在「公网 Agent」页的具体化 |
|
||||
|
||||
工作量:**单文件改动(`web/src/views/AgentView.vue`)**,约 5–6 小时(含 5 处能力补齐、状态机重构、无障碍与响应式)。
|
||||
验证:`npx vue-tsc --noEmit` + `npx vite build`,再同步 `web/dist → cmd/fonu/web/dist` 后 `go build ./...`。
|
||||
验证:`npx vue-tsc --noEmit` + `npx vite build`,再同步 `web/dist → cmd/havline/web/dist` 后 `go build ./...`。
|
||||
|
||||
+22
-22
@@ -3,7 +3,7 @@
|
||||
> **时效说明(2026-09-24)**:文中「顶部下拉选服务端」的结构已被 v1.2.6 的**左列表 + 右详情**取代(页面标题也改为「公网服务端」),现状见 `docs/frp-server-list-merge-plan.md`。
|
||||
|
||||
> 本文按 `breezell-design` 两遍流程产出:**Pass 1a 检测栈 → Pass 1b 确定设计方向与令牌 → Pass 2 实施规格**,
|
||||
> 并结合本项目(Fonu)既有页面与设计系统。落地时以本文为准,`docs/agent-view-refactor-plan.md`(结构整改)被本文吸收。
|
||||
> 并结合本项目(Havline)既有页面与设计系统。落地时以本文为准,`docs/agent-view-refactor-plan.md`(结构整改)被本文吸收。
|
||||
>
|
||||
> 更新日期:2026-09-23
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
| 框架 | Vue 3 + `<script setup lang="ts">` + Vite 6(`web/package.json`) |
|
||||
| 组件库 | **naive-ui**(`n-button` `n-select` `n-tabs` `n-modal` `n-popconfirm` `n-alert` `n-spin` `n-icon` `n-tag` `n-form`) |
|
||||
| 图标 | `@vicons/ionicons5` |
|
||||
| 令牌系统 | `web/src/styles/variables.css`:`:root` + `html.dark` / `[data-theme='dark']` 双主题,`--fonu-*` 前缀 |
|
||||
| 共享组件 | `PageHeader` `FonuCard` `StatCard` `StatusBadge` `EmptyState` `LoadError`(另有 `MiniBarChart` 等图表件) |
|
||||
| 令牌系统 | `web/src/styles/variables.css`:`:root` + `html.dark` / `[data-theme='dark']` 双主题,`--havline-*` 前缀 |
|
||||
| 共享组件 | `PageHeader` `HavlineCard` `StatCard` `StatusBadge` `EmptyState` `LoadError`(另有 `MiniBarChart` 等图表件) |
|
||||
| 工具 | `utils/format.ts`(`formatBytes/formatRate/formatDate/formatRelativeTime/…`)、`utils/status.ts`(`StatusKind` 语义) |
|
||||
| 同族页面 | `DashboardView`(统计卡 + 插槽)、`DdnsView`(提示 + 状态卡)、`CertificatesView`(统计卡规格基准)、`ProxyView`(分组/弹窗)、`SettingsView`(分区) |
|
||||
|
||||
**结论(skill 的 "Already has a design system → MATCH it" 规则)**:本页**沿用** naive-ui + `--fonu-*` 令牌 + 既有共享组件,
|
||||
**结论(skill 的 "Already has a design system → MATCH it" 规则)**:本页**沿用** naive-ui + `--havline-*` 令牌 + 既有共享组件,
|
||||
**不引入** Tailwind / shadcn-vue 等第二套体系,**不新增**第二套令牌或全局样式。
|
||||
|
||||
---
|
||||
@@ -39,17 +39,17 @@
|
||||
|
||||
| 语义 | 令牌 | 用在哪 | 判定规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| 正常 / 运行 | `--fonu-brand`(`--fonu-brand-soft`、`--fonu-brand-text`) | 在线、运行中、监听中、成功输出 | 无需人工干预 |
|
||||
| 待处理 | `--fonu-warning` | frps 未运行、端口未监听、frps 未安装、开机自启未启用 | 可一键恢复(引导操作) |
|
||||
| 需人工干预 | `--fonu-error` | Agent 未连接、操作失败输出、卸载确认 | 需用户排查或不可逆 |
|
||||
| 中性信息 | `--fonu-text-secondary` / `--fonu-text-muted` | 字段名、说明、时间戳、空态 | — |
|
||||
| 引导 | `--fonu-info`(`--fonu-info-soft`) | 页面级引导提示(反代入口等) | 非故障类提示 |
|
||||
| 正常 / 运行 | `--havline-brand`(`--havline-brand-soft`、`--havline-brand-text`) | 在线、运行中、监听中、成功输出 | 无需人工干预 |
|
||||
| 待处理 | `--havline-warning` | frps 未运行、端口未监听、frps 未安装、开机自启未启用 | 可一键恢复(引导操作) |
|
||||
| 需人工干预 | `--havline-error` | Agent 未连接、操作失败输出、卸载确认 | 需用户排查或不可逆 |
|
||||
| 中性信息 | `--havline-text-secondary` / `--havline-text-muted` | 字段名、说明、时间戳、空态 | — |
|
||||
| 引导 | `--havline-info`(`--havline-info-soft`) | 页面级引导提示(反代入口等) | 非故障类提示 |
|
||||
|
||||
规则:**一屏内 error 不超过一处**;warning 不用于"没数据",只用于"有目标状态但未达成"。亮/暗两套值由 `variables.css` 的 `html.dark` 覆盖,**页面内零硬编码色值**。
|
||||
|
||||
### Type(沿用项目字体,显式声明)
|
||||
|
||||
- 正文字体 `--fonu-font`(`-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei'`),等宽 `--fonu-mono`。
|
||||
- 正文字体 `--havline-font`(`-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei'`),等宽 `--havline-mono`。
|
||||
- **关于 skill 的"禁用 system-ui 系默认栈"**:本项目已存在设计系统,按 "MATCH it, never add a second" 规则,**本页继续使用项目字体**;替换字体属**全局决策**(改 `variables.css`,影响全站并需全站回归),不在本页范围。
|
||||
- 页内只用 4 档字号 + 3 档字重(与 `CertificatesView` 统计卡规格一致):
|
||||
|
||||
@@ -58,13 +58,13 @@
|
||||
| 统计卡数值(词条类) | 18px / 700 | `.stat-card__value--sm` |
|
||||
| 统计卡数值(数字类) | 30px / 700,`letter-spacing:-0.03em` | `.stat-card__value` |
|
||||
| 字段名 / 卡片标签 | 12–13px / 600 | `.stat-card__label`、`.field-grid label` |
|
||||
| 说明 / 时间戳 / 空态 | 12–12.5px / 400 | `--fonu-text-muted`、`.form-hint` |
|
||||
| 路径 / 命令 / 版本 / 日志 | `--fonu-mono` 12.5–13px | `.mono` / `.mono-wrap` / `.code-block` |
|
||||
| 说明 / 时间戳 / 空态 | 12–12.5px / 400 | `--havline-text-muted`、`.form-hint` |
|
||||
| 路径 / 命令 / 版本 / 日志 | `--havline-mono` 12.5–13px | `.mono` / `.mono-wrap` / `.code-block` |
|
||||
|
||||
### Spacing & Radius
|
||||
|
||||
- 间距只用 `--fonu-space-1…6`(4/8/12/16/24/32),**禁止魔数**;卡片内边距统一 `var(--fonu-space-4) var(--fonu-space-5)`。
|
||||
- 圆角只用 `--fonu-radius`(12px) / `--fonu-radius-sm`(8px);阴影只用 `--fonu-shadow`。
|
||||
- 间距只用 `--havline-space-1…6`(4/8/12/16/24/32),**禁止魔数**;卡片内边距统一 `var(--havline-space-4) var(--havline-space-5)`。
|
||||
- 圆角只用 `--havline-radius`(12px) / `--havline-radius-sm`(8px);阴影只用 `--havline-shadow`。
|
||||
- 页面里已有的 `style="width: 220px"` 这类内联尺寸,一律改为类(`.agent-select` / `.frps-version-select`)。
|
||||
|
||||
---
|
||||
@@ -80,13 +80,13 @@
|
||||
│ 在线/未连接 运行中/未运行 监听中/未监听 监听中/未监听
|
||||
│ <agent 地址> <frps 版本> HTTP 入口 HTTPS 入口
|
||||
├ 两列网格
|
||||
│ FonuCard 服务状态详情 FonuCard frps 服务控制
|
||||
│ HavlineCard 服务状态详情 HavlineCard frps 服务控制
|
||||
│ field-grid 2 列: 按钮带:启动|停止 · 重启 ·
|
||||
│ Agent 版本 / frps 版本 frps 管理面板 · 下发配置
|
||||
│ 启动命令 / 配置文件 [版本选择 ▾] 安装/升级
|
||||
│ 安装位置 / 开机自启
|
||||
│ 7000 端口
|
||||
│ FonuCard Agent 生命周期(SSH) FonuCard 输出与日志
|
||||
│ HavlineCard Agent 生命周期(SSH) HavlineCard 输出与日志
|
||||
│ 探测环境 · 安装/升级 Agent n-tabs:操作输出 | frps 日志
|
||||
│ SSH 诊断 · 卸载(保留) 操作输出 = 时间线(来源+时间+ok)
|
||||
│ 卸载(含数据) ← 二次确认 空态 EmptyState · [清空]
|
||||
@@ -125,7 +125,7 @@
|
||||
|
||||
**动效(high-impact, restrained)**
|
||||
|
||||
- 首屏**一次性**入场:状态区与卡片 `fade-up`(`translateY(4px)→0`,160ms,`--fonu-shadow` 不变),仅 `onMounted` 后触发一次;
|
||||
- 首屏**一次性**入场:状态区与卡片 `fade-up`(`translateY(4px)→0`,160ms,`--havline-shadow` 不变),仅 `onMounted` 后触发一次;
|
||||
- `@media (prefers-reduced-motion: reduce)` 下全部禁用;
|
||||
- 除入场与按钮库内置反馈外,**不做**散点微动效(不加卡片 hover 位移、不加脉冲动画)。
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
- 语义:状态区用 `<section aria-label="服务状态">`;装饰性图标 `aria-hidden="true"`;按钮均有可读文本(不出现只有图标的裸按钮);
|
||||
- 动态内容:操作输出容器 `aria-live="polite"`(异步追加需播报);`n-popconfirm` 已带键盘与焦点管理;
|
||||
- 键盘:Tab 顺序 = 选择器 → 刷新 → 状态区(跳过只读)→ 各卡片按钮带 → tabs;`n-tabs` 支持方向键;
|
||||
- 对比度:实测下列组合达 WCAG AA(正文 4.5:1、大字号 3:1)——`--fonu-brand`/`--fonu-warning`/`--fonu-error`/`--fonu-text-muted` 在 `--fonu-surface` 上,**亮色与暗色各测一遍**;
|
||||
- 对比度:实测下列组合达 WCAG AA(正文 4.5:1、大字号 3:1)——`--havline-brand`/`--havline-warning`/`--havline-error`/`--havline-text-muted` 在 `--havline-surface` 上,**亮色与暗色各测一遍**;
|
||||
- 触控:≤760px 时按钮 `min-height: 44px`(或升为 `size="medium"`),满足最小触控目标;
|
||||
- 响应式:`1200px+` 4 列状态 / `900–1199px` 2 列 / `≤900px` 单列 + 两列网格并一列 / `≤760px` 按钮铺满 / **`320px` 无横向溢出**。
|
||||
|
||||
@@ -184,7 +184,7 @@ function pushOutput(source: string, text: string, ok = true) {
|
||||
|
||||
## 七、本页不做(Never)
|
||||
|
||||
- 不引入第二套 UI 框架 / 令牌 / 全局样式(naive-ui + `--fonu-*` 已存在);
|
||||
- 不引入第二套 UI 框架 / 令牌 / 全局样式(naive-ui + `--havline-*` 已存在);
|
||||
- 不改全局字体与令牌(属全站决策);
|
||||
- 不用硬编码色值与魔数间距(一律令牌);
|
||||
- 不做渐变、居中大标题、统一灰卡片这类 slop 模式;
|
||||
@@ -196,7 +196,7 @@ function pushOutput(source: string, text: string, ok = true) {
|
||||
|
||||
## 八、验收清单(对照 skill self-check)
|
||||
|
||||
- [ ] 复用了项目真实栈与令牌(naive-ui + `--fonu-*` + 共享组件),未新增第二套
|
||||
- [ ] 复用了项目真实栈与令牌(naive-ui + `--havline-*` + 共享组件),未新增第二套
|
||||
- [ ] 单一设计方向(机架面板)落地:状态条 + 输出时间线为 signature,无 banned 字体引入,颜色/间距/圆角全部走令牌
|
||||
- [ ] 交互状态齐全:hover / pressed / **focused(键盘实测)** / disabled / loading / empty / error
|
||||
- [ ] 无障碍:对比度 AA(亮+暗)、可见焦点环、键盘可完成全部操作、`aria-live` 播报输出、图标 `aria-hidden`
|
||||
@@ -210,7 +210,7 @@ function pushOutput(source: string, text: string, ok = true) {
|
||||
| 文档 | 关系 |
|
||||
| --- | --- |
|
||||
| `docs/agent-view-refactor-plan.md` | 结构整改清单(12 项问题 + 分步改造),**本文吸收其全部内容**并补上设计方向、状态矩阵、无障碍与动效规格;实施时以本文为准 |
|
||||
| `web/.breezell/skills/fonu-page-ui`(工作区 skill) | 页面 UI 通用标准;本文是它在「公网 Agent」页的具体化 |
|
||||
| `web/.breezell/skills/havline-page-ui`(工作区 skill) | 页面 UI 通用标准;本文是它在「公网 Agent」页的具体化 |
|
||||
|
||||
工作量:**单文件改动(`web/src/views/AgentView.vue`)**,约 4–5 小时(比原结构整改多出的部分:设计方向落地、状态矩阵、无障碍与动效、320px 适配)。
|
||||
验证:`npx vue-tsc --noEmit` + `npx vite build`,再同步 `web/dist → cmd/fonu/web/dist` 后 `go build ./...` 使本地生效。
|
||||
验证:`npx vue-tsc --noEmit` + `npx vite build`,再同步 `web/dist → cmd/havline/web/dist` 后 `go build ./...` 使本地生效。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 公网 Agent 页(`AgentView.vue`)整合整改方案
|
||||
|
||||
> 目标:把「公网 Agent」页从"功能堆叠"整理成**状态 → 控制 → 生命周期 → 输出**四段式结构,
|
||||
> 信息层级清晰、操作归属明确、危险操作可控,并完全符合 `fonu-page-ui` 页面 UI 标准。
|
||||
> 信息层级清晰、操作归属明确、危险操作可控,并完全符合 `havline-page-ui` 页面 UI 标准。
|
||||
>
|
||||
> 参照页:`DdnsView.vue`(状态卡 + 提示)、`CertificatesView.vue`(统计卡规格)、`ProxyView.vue`(分组与按钮区)、`SettingsView.vue`(分区)。
|
||||
> 更新日期:2026-09-23
|
||||
@@ -17,7 +17,7 @@
|
||||
| 3 | **loading 粒度太粗** | 所有按钮都用 `:loading="agentActionId === server.id"`——任何操作进行中,页面**全部**按钮一起转圈 | 无法分辨在跑哪个操作,也无法阻止并发操作 |
|
||||
| 4 | **输出全部塞进一个 `agentOutput`** | 探测 / 安装 / 卸载 / SSH 诊断 / frps 启停 / 下发配置 / 安装 frps 七类输出共用一个 ref,显示在「Agent 生命周期」卡底部 | 输出归属不明;执行「重启 frps」后输出出现在 SSH 卡里,语义错位 |
|
||||
| 5 | **日志与输出分居两卡** | 「frps 最近日志」独立成卡,`agentOutput` 在另一张卡 | 排障时要在两处来回看 |
|
||||
| 6 | **危险操作无二次确认** | 「卸载(含数据)」按钮点击即执行,`rm -rf /var/lib/fonu-agent` 不可逆 | 误点即丢路由与证书 |
|
||||
| 6 | **危险操作无二次确认** | 「卸载(含数据)」按钮点击即执行,`rm -rf /var/lib/havline-agent` 不可逆 | 误点即丢路由与证书 |
|
||||
| 7 | **页面级提示缺失** | agent 未连接、frps 未安装等状态只在卡片里体现,没有 `n-alert` 引导(技能要求:页面级提示放 PageHeader 之后) | 新用户不知道下一步该去哪一页 |
|
||||
| 8 | **跳转卡占位浪费** | 「公网 Nginx 反代」整卡只放一句说明 + 一个跳转按钮 | 占掉半行网格,信息密度低 |
|
||||
| 9 | **轮询与用户操作耦合** | 10 秒 `refreshAll()` 里设置 `agentActionId`,与用户点击共用同一状态 | 轮询时按钮闪烁 loading |
|
||||
@@ -38,13 +38,13 @@ stats-row(统一 4 列稳定网格)
|
||||
├─ frps 服务(运行中/未运行 + 版本)
|
||||
├─ 80 端口
|
||||
└─ 443 端口
|
||||
FonuCard「服务状态详情」
|
||||
HavlineCard「服务状态详情」
|
||||
└─ field-grid:Agent 版本 / 已安装 frps 版本 / frps 启动命令 / 配置文件 / 安装位置 / 开机自启 / 7000 端口(StatusBadge)
|
||||
FonuCard「frps 服务控制」
|
||||
HavlineCard「frps 服务控制」
|
||||
└─ 按钮区:启动|停止 · 重启 · frps 管理面板 · 下发配置 · [版本选择] 安装/升级
|
||||
FonuCard「Agent 生命周期(SSH)」
|
||||
HavlineCard「Agent 生命周期(SSH)」
|
||||
└─ 按钮区:探测环境 · 安装/升级 Agent · SSH 诊断 · 卸载(保留数据)· 卸载(含数据,二次确认)
|
||||
FonuCard「输出与日志」(n-tabs)
|
||||
HavlineCard「输出与日志」(n-tabs)
|
||||
├─ Tab 操作输出:带来源标签 + 时间;空态 EmptyState;「清空」按钮
|
||||
└─ Tab frps 日志:frps_log_tail;空态 EmptyState
|
||||
```
|
||||
@@ -64,8 +64,8 @@ FonuCard「输出与日志」(n-tabs)
|
||||
### 2. 统一网格(解决 #2、#12)
|
||||
|
||||
```css
|
||||
.stats-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--fonu-space-4); margin-bottom: var(--fonu-space-4); }
|
||||
.agent-page-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--fonu-space-4); margin-bottom: var(--fonu-space-4); }
|
||||
.stats-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--havline-space-4); margin-bottom: var(--havline-space-4); }
|
||||
.agent-page-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--havline-space-4); margin-bottom: var(--havline-space-4); }
|
||||
@media (max-width: 900px) { .stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } .agent-page-grid { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 760px) { .stats-row { grid-template-columns: 1fr; } .agent-actions { width: 100%; } .agent-actions .n-button { flex: 1 1 auto; } }
|
||||
```
|
||||
@@ -111,7 +111,7 @@ function pushOutput(source: string, text: string, ok = true) {
|
||||
```vue
|
||||
<n-popconfirm @positive-click="uninstallAgent(server, false)">
|
||||
<template #trigger><n-button size="small" type="warning">卸载(含数据)</n-button></template>
|
||||
将删除 VPS 上的 agent、frps 单元与 /var/lib/fonu-agent(路由与证书一并丢失),确认继续?
|
||||
将删除 VPS 上的 agent、frps 单元与 /var/lib/havline-agent(路由与证书一并丢失),确认继续?
|
||||
</n-popconfirm>
|
||||
```
|
||||
|
||||
@@ -123,7 +123,7 @@ function pushOutput(source: string, text: string, ok = true) {
|
||||
|
||||
| 条件 | 类型 | 文案要点 | 动作 |
|
||||
| --- | --- | --- | --- |
|
||||
| `!agentStatusOk` | `error` | Agent 未连接:检查 VPS 上 fonu-agent 是否运行、Agent 地址与 Token 是否正确 | 「探测环境」 |
|
||||
| `!agentStatusOk` | `error` | Agent 未连接:检查 VPS 上 havline-agent 是否运行、Agent 地址与 Token 是否正确 | 「探测环境」 |
|
||||
| `!agentStatus?.frps_active` | `warning` | frps 未运行:可点「启动 frps」或在下方安装 | 「启动 frps」 |
|
||||
| 反代入口引导(常驻) | `info` | 反代的逐条部署/配置查看/访问控制在「公网反代」页 | 跳转 `/public-proxy` |
|
||||
|
||||
@@ -152,7 +152,7 @@ watch(releases, (list) => { if (!frpsInstallVersion.value && list.length) frpsIn
|
||||
| --- | --- |
|
||||
| `web/src/views/AgentView.vue` | 模板结构重排、脚本状态重构、样式网格与断点补齐(单文件改动) |
|
||||
| `web/src/api/client.ts` / `types.ts` | 预期**无需**改动(现有 `getFrpAgentStatus` / `probeFrpAgent` / `installFrpAgent` / `uninstallFrpAgent` / `sshDiagnoseFrpAgent` / `frpsServerActionRaw` / `pushFrpConfigToAgent` / `installFrpServer` / `listFrpReleases` 已够用) |
|
||||
| `cmd/fonu/web/dist` | 构建产物同步(gitignore,不入库) |
|
||||
| `cmd/havline/web/dist` | 构建产物同步(gitignore,不入库) |
|
||||
|
||||
验证闭环(技能第 8 节):
|
||||
|
||||
@@ -160,7 +160,7 @@ watch(releases, (list) => { if (!frpsInstallVersion.value && list.length) frpsIn
|
||||
cd web
|
||||
npx vue-tsc --noEmit # 0 错误
|
||||
npx vite build # 成功
|
||||
# 本地生效:同步 dist → cmd/fonu/web/dist 后 go build ./...
|
||||
# 本地生效:同步 dist → cmd/havline/web/dist 后 go build ./...
|
||||
```
|
||||
|
||||
回归检查点(真机,VPS 38.95.74.87):
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
> 相关文档:
|
||||
> - `cloudflared-integration-plan.md`:早期融合方案
|
||||
> - `cloudflared-feature-plan.md`:官方与参考项目功能全集,用于查漏
|
||||
> - `cloudflared-fonu-fit-plan.md`:Fonu 适配版阶段计划
|
||||
> - `cloudflared-havline-fit-plan.md`:Havline 适配版阶段计划
|
||||
|
||||
---
|
||||
|
||||
## 一、模块定位
|
||||
|
||||
Cloudflare 隧道在 Fonu 中不是独立的 cloudflared 管理面板,而是公网访问的第二条出口:
|
||||
Cloudflare 隧道在 Havline 中不是独立的 cloudflared 管理面板,而是公网访问的第二条出口:
|
||||
|
||||
- 本机 Nginx:适合有公网 IP 或已有端口映射的场景。
|
||||
- FRP:适合需要 TCP/UDP、自控 VPS 或大流量的场景。
|
||||
- Cloudflare 隧道:适合没有公网 IP、主要发布 HTTP/HTTPS Web 服务的场景。
|
||||
|
||||
当前模块已经融入 Fonu 的规则、监控、通知、状态页、日志、备份、诊断和升级体系。
|
||||
当前模块已经融入 Havline 的规则、监控、通知、状态页、日志、备份、诊断和升级体系。
|
||||
|
||||
---
|
||||
|
||||
@@ -183,7 +183,7 @@ Cloudflare 隧道在 Fonu 中不是独立的 cloudflared 管理面板,而是
|
||||
- 崩溃后读取日志尾部便于诊断。
|
||||
- 开机启动:
|
||||
- 全局托管开启时,应用启动会恢复全部隧道。
|
||||
- “随 Fonu 自动启动”开关保留为隧道级选项。
|
||||
- “随 Havline 自动启动”开关保留为隧道级选项。
|
||||
- 全局总开关:
|
||||
- 开启时启动全部隧道。
|
||||
- 关闭时停止全部隧道。
|
||||
@@ -300,10 +300,10 @@ Cloudflare 隧道在 Fonu 中不是独立的 cloudflared 管理面板,而是
|
||||
<hostname> -> <tunnel-id>.cfargotunnel.com
|
||||
```
|
||||
- 开启 Cloudflare 代理(橙云)。
|
||||
- 记录 Fonu 创建或接管的 DNS 记录:
|
||||
- 记录 Havline 创建或接管的 DNS 记录:
|
||||
- `cf_dns_records`
|
||||
- 删除域名或移除出口时:
|
||||
- 只删除 Fonu 自己接管的记录。
|
||||
- 只删除 Havline 自己接管的记录。
|
||||
- 不覆盖指向其他目标的用户 DNS 记录。
|
||||
- 域名从一条隧道迁移到另一条隧道时支持接管迁移。
|
||||
|
||||
@@ -395,15 +395,15 @@ Cloudflare 隧道在 Fonu 中不是独立的 cloudflared 管理面板,而是
|
||||
已实现:
|
||||
|
||||
- 备份内容包含:
|
||||
- `fonu.db`
|
||||
- `havline.db`
|
||||
- `certs`
|
||||
- `nginx`
|
||||
- `cloudflared/tunnels`
|
||||
- `cloudflared/tunnels` 包含:
|
||||
- `config.yml`
|
||||
- `creds.json`
|
||||
- 数据库中的 `cf_tunnels`、`cf_settings`、`cf_dns_records` 随 `fonu.db` 恢复。
|
||||
- 恢复后仍建议重启 Fonu,由启动流程恢复全部隧道。
|
||||
- 数据库中的 `cf_tunnels`、`cf_settings`、`cf_dns_records` 随 `havline.db` 恢复。
|
||||
- 恢复后仍建议重启 Havline,由启动流程恢复全部隧道。
|
||||
|
||||
---
|
||||
|
||||
@@ -436,7 +436,7 @@ Cloudflare 隧道在 Fonu 中不是独立的 cloudflared 管理面板,而是
|
||||
- 本地 `config.yml`。
|
||||
- 隧道进程状态。
|
||||
- Cloudflare API 与云端配置可访问性。
|
||||
- Fonu 接管的 DNS 记录。
|
||||
- Havline 接管的 DNS 记录。
|
||||
- Cloudflare edge region1。
|
||||
- Cloudflare edge region2。
|
||||
|
||||
@@ -572,7 +572,7 @@ GET /api/cloudflare/access-status
|
||||
|
||||
### `cf_dns_records`
|
||||
|
||||
保存 Fonu 接管的 DNS 记录:
|
||||
保存 Havline 接管的 DNS 记录:
|
||||
|
||||
- 本地隧道 ID。
|
||||
- Zone ID。
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Cloudflare 隧道模块全功能规划
|
||||
|
||||
> 调研日期:2026-09-30
|
||||
> 调研对象:Cloudflare 官方文档(developers.cloudflare.com,2026-08/09 更新)+ `F:\WWW\cf\Tunnel-Cloudflared-linux`(Tunnel-Cloudflared-Manager 1.0.1)+ Fonu 1.8.0 现有实现
|
||||
> 目标:把 Fonu 的「Cloudflare 隧道」从「可用的创建与托管」推进到「覆盖官方主要能力、可长期运维」的完整模块。
|
||||
> 调研对象:Cloudflare 官方文档(developers.cloudflare.com,2026-08/09 更新)+ `F:\WWW\cf\Tunnel-Cloudflared-linux`(Tunnel-Cloudflared-Manager 1.0.1)+ Havline 1.8.0 现有实现
|
||||
> 目标:把 Havline 的「Cloudflare 隧道」从「可用的创建与托管」推进到「覆盖官方主要能力、可长期运维」的完整模块。
|
||||
|
||||
---
|
||||
|
||||
## 一、官方文档调研结论
|
||||
|
||||
Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条,规划时需要同时兼顾两条:
|
||||
Cloudflare 把 Tunnel 分成两条产品线,Havline 目前主要落在第一条,规划时需要同时兼顾两条:
|
||||
|
||||
| 产品线 | 文档入口 | 典型用途 |
|
||||
| --- | --- | --- |
|
||||
@@ -76,14 +76,14 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
### 1.3 官方版本要点(2026-09)
|
||||
|
||||
- 运行参数文档最近更新 2026-09;`post-quantum`、`token-file`、`region`、`tag`、`dns-resolver-addrs`、`edge-bind-address` 等参数已进入官方参考。
|
||||
- `edge-ip-version` 官方默认值曾由 `4` 调整为 `auto`;Fonu 已显式下发 `4`,与国内链路建议一致。
|
||||
- 官方把「公开发布应用」与「Zero Trust 私有网络」拆成两条文档线,Fonu 需要明确自己的默认定位是公开发布应用。
|
||||
- `edge-ip-version` 官方默认值曾由 `4` 调整为 `auto`;Havline 已显式下发 `4`,与国内链路建议一致。
|
||||
- 官方把「公开发布应用」与「Zero Trust 私有网络」拆成两条文档线,Havline 需要明确自己的默认定位是公开发布应用。
|
||||
|
||||
---
|
||||
|
||||
## 二、参考项目功能清单(Tunnel-Cloudflared-Manager 1.0.1)
|
||||
|
||||
参考项目是一个独立面板,功能面比 Fonu 当前实现更宽,但缺少与 Fonu 的规则、健康、通知、状态页体系融合。
|
||||
参考项目是一个独立面板,功能面比 Havline 当前实现更宽,但缺少与 Havline 的规则、健康、通知、状态页体系融合。
|
||||
|
||||
### 2.1 隧道管理
|
||||
|
||||
@@ -143,7 +143,7 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
|
||||
---
|
||||
|
||||
## 三、Fonu 当前实现(v1.8.0)
|
||||
## 三、Havline 当前实现(v1.8.0)
|
||||
|
||||
| 能力 | 状态 |
|
||||
| --- | --- |
|
||||
@@ -167,7 +167,7 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
| 账号模式(cert.pem) | ❌ 未实现 |
|
||||
| 实时日志流(WebSocket/SSE) | ❌ 未实现 |
|
||||
| 诊断日志、网络预检、隧道 info | ❌ 未实现 |
|
||||
| 接入 Fonu monitor / 通知 / 状态页 | ❌ 未实现 |
|
||||
| 接入 Havline monitor / 通知 / 状态页 | ❌ 未实现 |
|
||||
| cloudflared 自动更新 | ❌ 未实现 |
|
||||
| 自定义镜像管理、下载进度 | ❌ 未实现 |
|
||||
| Origin 高级参数 UI | ❌ 未实现 |
|
||||
@@ -181,10 +181,10 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
|
||||
### 4.1 必须补齐(否则模块不完整)
|
||||
|
||||
1. **云端隧道列表与导入**:目前 Fonu 只能管理自己创建的隧道,已有隧道无法纳管。
|
||||
1. **云端隧道列表与导入**:目前 Havline 只能管理自己创建的隧道,已有隧道无法纳管。
|
||||
2. **端点级 CRUD**:现在是整体覆盖 ingress,无法单独增删改一条 hostname/path。
|
||||
3. **云端配置拉取与漂移检测**:云端被手动修改后,Fonu 无法发现。
|
||||
4. **健康接入 Fonu 体系**:隧道状态没有进入 monitor、通知、状态页、可用率。
|
||||
3. **云端配置拉取与漂移检测**:云端被手动修改后,Havline 无法发现。
|
||||
4. **健康接入 Havline 体系**:隧道状态没有进入 monitor、通知、状态页、可用率。
|
||||
5. **实时日志**:轮询文件在故障排查时体验差。
|
||||
6. **诊断能力**:缺 `tunnel info`、`tunnel diag`、网络预检、端口要求检查。
|
||||
|
||||
@@ -215,7 +215,7 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
|
||||
**P0-1 云端隧道同步与导入**
|
||||
|
||||
- 目标:列出 Cloudflare 账号下的全部隧道,把未纳管的隧道导入 Fonu。
|
||||
- 目标:列出 Cloudflare 账号下的全部隧道,把未纳管的隧道导入 Havline。
|
||||
- 实现:`GET /accounts/{id}/cfd_tunnel` 列表;导入时读取 token、写 `creds.json`/`config.yml`、落库。
|
||||
- 验收:账号下已有隧道能在页面看到并导入;重复导入不产生重复记录。
|
||||
|
||||
@@ -227,11 +227,11 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
|
||||
**P0-3 云端配置拉取与漂移检测**
|
||||
|
||||
- 目标:云端被手动修改后,Fonu 能发现差异并选择拉取或覆盖。
|
||||
- 目标:云端被手动修改后,Havline 能发现差异并选择拉取或覆盖。
|
||||
- 实现:`GET /accounts/{id}/cfd_tunnel/{tunnel}/configurations`;与本地 config 做 diff;提供「拉取到本地」「推送到云端」。
|
||||
- 验收:diff 可见;两个方向都能执行并留痕。
|
||||
|
||||
**P0-4 健康接入 Fonu 体系**
|
||||
**P0-4 健康接入 Havline 体系**
|
||||
|
||||
- 目标:隧道断连、恢复进入现有 monitor、通知、状态页与可用率。
|
||||
- 实现:新增 cloudflared 的 `monitor.Source`;`/ready` + `ha_connections` 判活;断连/恢复事件复用 notify;状态页增加隧道条目。
|
||||
@@ -240,7 +240,7 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
**P0-5 实时日志(SSE)**
|
||||
|
||||
- 目标:日志页实时滚动,替代轮询。
|
||||
- 实现:Fonu 现有日志 SSE 模式复用;cloudflared 日志文件 tail + 增量推送;保留轮询回退。
|
||||
- 实现:Havline 现有日志 SSE 模式复用;cloudflared 日志文件 tail + 增量推送;保留轮询回退。
|
||||
- 验收:页面实时看到新日志;切换页面/重连不丢关键行。
|
||||
|
||||
**P0-6 诊断能力**
|
||||
@@ -254,7 +254,7 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
**P1-1 快速隧道**
|
||||
|
||||
- 目标:临时把本地端口发布到 `trycloudflare.com`,用于演示与开发。
|
||||
- 实现:`--url` 启动、日志解析公共 URL、进程随 Fonu 生命周期、默认关闭。
|
||||
- 实现:`--url` 启动、日志解析公共 URL、进程随 Havline 生命周期、默认关闭。
|
||||
- 边界:明确标注不保证 SLA、URL 每次变化。
|
||||
|
||||
**P1-2 多副本与零停机更新**
|
||||
@@ -284,7 +284,7 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
**P1-6 审计日志**
|
||||
|
||||
- 目标:记录谁在何时修改了隧道、端点、DNS、令牌与全局开关。
|
||||
- 实现:复用 Fonu 日志/审计基础设施;操作前记录旧值、操作后记录新值。
|
||||
- 实现:复用 Havline 日志/审计基础设施;操作前记录旧值、操作后记录新值。
|
||||
- 验收:关键变更可查询、可导出。
|
||||
|
||||
### P2:体验与增强
|
||||
@@ -425,18 +425,18 @@ Cloudflare 把 Tunnel 分成两条产品线,Fonu 目前主要落在第一条
|
||||
- **网络边界**:只做本机到 CF 边缘的调优;访客侧不可控。
|
||||
- **合规边界**:大文件/视频分发不在推荐用法内。
|
||||
- **安全边界**:不代配 Access 高风险策略;不写 hosts;不改系统 ulimit/防火墙。
|
||||
- **运行边界**:Fonu 只托管自己启动的 cloudflared 进程;不接管外部 systemd 服务。
|
||||
- **运行边界**:Havline 只托管自己启动的 cloudflared 进程;不接管外部 systemd 服务。
|
||||
- **数据边界**:凭据加密存储;预览脱敏;审计日志不记录明文 token。
|
||||
|
||||
---
|
||||
|
||||
## 十、结论
|
||||
|
||||
Fonu 的 Cloudflare 隧道模块已经具备「创建、运行、托管、DNS」的闭环。下一阶段的目标是把它从「能用」推进到「可长期运维」:
|
||||
Havline 的 Cloudflare 隧道模块已经具备「创建、运行、托管、DNS」的闭环。下一阶段的目标是把它从「能用」推进到「可长期运维」:
|
||||
|
||||
1. 先补齐云端同步、端点 CRUD、漂移检测、健康接入、实时日志、诊断(阶段 1)。
|
||||
2. 再补快速隧道、多副本、自动更新、令牌轮换、Origin 高级参数、审计(阶段 2)。
|
||||
3. 最后做镜像、图表、版本回滚、日志治理、系统检查与网络调优扩展(阶段 3)。
|
||||
4. 私有网络、Access、负载均衡、边缘优选等平台化能力按需推进(阶段 4)。
|
||||
|
||||
这样 Fonu 才能在官方能力、参考项目功能与自身产品体系之间取得平衡,而不是停留在「进程开关 + 配置预览」。
|
||||
这样 Havline 才能在官方能力、参考项目功能与自身产品体系之间取得平衡,而不是停留在「进程开关 + 配置预览」。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Cloudflare 隧道同类项目调研与 Fonu 体验优化建议
|
||||
# Cloudflare 隧道同类项目调研与 Havline 体验优化建议
|
||||
|
||||
> 日期:2026-09-30
|
||||
> 调研范围:GitHub 上 Cloudflare Tunnel / cloudflared 管理面板、反向代理管理器、自托管访问平台、状态监测与流量检查器。
|
||||
> 目标:不是照搬功能,而是提取适合 Fonu 的自动化方式、页面结构和长期运维体验。
|
||||
> 目标:不是照搬功能,而是提取适合 Havline 的自动化方式、页面结构和长期运维体验。
|
||||
|
||||
---
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
- 代表:Uptime Kuma、Tunnel Inspector
|
||||
- 重点:可用率、状态页、事故记录、请求级观测、重放和抓包体验。
|
||||
|
||||
对 Fonu 最重要的启发不是“再加多少按钮”,而是:
|
||||
对 Havline 最重要的启发不是“再加多少按钮”,而是:
|
||||
|
||||
- 用期望状态和自动调和代替手工点同步。
|
||||
- 让规则、隧道、DNS、证书、监控在同一套模型中管理。
|
||||
@@ -69,16 +69,16 @@
|
||||
- 支持环境变量配置协议、边缘 IP、metrics、重试等。
|
||||
- 数据持久化到 `/config`。
|
||||
|
||||
对 Fonu 的启示:
|
||||
对 Havline 的启示:
|
||||
|
||||
- 初次配置应该更短:Account ID、API Token、Zone、Tunnel 四步内完成。
|
||||
- 所有高级网络参数应该折叠到“高级选项”,默认值直接可用。
|
||||
- 当前 Fonu 已覆盖它的主要能力,并且多了 API 托管、DNS、规则同步和监控。
|
||||
- 当前 Havline 已覆盖它的主要能力,并且多了 API 托管、DNS、规则同步和监控。
|
||||
|
||||
不建议照搬:
|
||||
|
||||
- 只有 Token 和启停,模型太薄。
|
||||
- 不适合 Fonu 的“服务发布规则”产品定位。
|
||||
- 不适合 Havline 的“服务发布规则”产品定位。
|
||||
|
||||
### 3.2 DockFlare
|
||||
|
||||
@@ -97,9 +97,9 @@
|
||||
- 加密配置、运行时状态和数据的备份恢复。
|
||||
- 多语言 UI。
|
||||
|
||||
对 Fonu 的启示:
|
||||
对 Havline 的启示:
|
||||
|
||||
- 当前 Fonu 已经有“反向代理规则派生 Cloudflare ingress”,方向正确。
|
||||
- 当前 Havline 已经有“反向代理规则派生 Cloudflare ingress”,方向正确。
|
||||
- 下一步应补“期望状态”和“调和结果”:
|
||||
- 本地期望配置。
|
||||
- Cloudflare 实际配置。
|
||||
@@ -108,7 +108,7 @@
|
||||
- 一键修复。
|
||||
- Docker 标签自动发布非常适合 NAS。
|
||||
- 多主机 Agent 适合进阶用户,但不应优先于单机体验。
|
||||
- Access 自动化风险较高,Fonu 暂时保持只读更稳。
|
||||
- Access 自动化风险较高,Havline 暂时保持只读更稳。
|
||||
|
||||
### 3.3 Tunnel Ingress Controller
|
||||
|
||||
@@ -122,9 +122,9 @@
|
||||
- Controller 持续把声明同步到 Cloudflare。
|
||||
- 不要求用户手工编辑 cloudflared ingress。
|
||||
|
||||
对 Fonu 的启示:
|
||||
对 Havline 的启示:
|
||||
|
||||
- Fonu 的“反向代理规则就是服务入口声明”是同一思路的非 K8s 版本。
|
||||
- Havline 的“反向代理规则就是服务入口声明”是同一思路的非 K8s 版本。
|
||||
- 无需引入 K8s,但应借鉴:
|
||||
- 声明与运行状态分离。
|
||||
- 调和结果可见。
|
||||
@@ -146,13 +146,13 @@
|
||||
- 实时日志。
|
||||
- 自动下载 cloudflared。
|
||||
|
||||
对 Fonu 的启示:
|
||||
对 Havline 的启示:
|
||||
|
||||
- Quick Tunnel 适合“临时演示”,不建议作为正式发布方式。
|
||||
- 双向同步是 Fonu 当前明显缺口:
|
||||
- 双向同步是 Havline 当前明显缺口:
|
||||
- 当前支持检测漂移并“本地覆盖云端”。
|
||||
- 缺少“云端覆盖本地”。
|
||||
- 多账号模式对 Fonu 属于进阶能力,可放到远期。
|
||||
- 多账号模式对 Havline 属于进阶能力,可放到远期。
|
||||
- 实时日志和指标已具备,继续加强即可。
|
||||
|
||||
### 3.5 cftunnel
|
||||
@@ -172,7 +172,7 @@
|
||||
- 一键分享二维码、地址、Telegram。
|
||||
- 自动更新。
|
||||
|
||||
对 Fonu 的启示:
|
||||
对 Havline 的启示:
|
||||
|
||||
- “快速隧道”可以作为临时验证入口,但必须标明不保证 SLA。
|
||||
- 场景模板非常值得借鉴:
|
||||
@@ -182,7 +182,7 @@
|
||||
- 相册
|
||||
- Jellyfin 管理页
|
||||
- “复制访问地址 / 二维码分享”适合手机用户。
|
||||
- Cloud + FRP 双引擎的概念与 Fonu 的出口抽象一致。
|
||||
- Cloud + FRP 双引擎的概念与 Havline 的出口抽象一致。
|
||||
|
||||
### 3.6 Nginx Proxy Manager
|
||||
|
||||
@@ -197,12 +197,12 @@
|
||||
- 证书申请和续签直接在规则旁完成。
|
||||
- 管理界面简单,新手可按表单完成部署。
|
||||
|
||||
对 Fonu 的启示:
|
||||
对 Havline 的启示:
|
||||
|
||||
- 反向代理规则的“编辑页”应成为服务发布中心。
|
||||
- Cloudflare 出口应该是一个选项,而不是另一套孤立页面。
|
||||
- SSL、Access、健康检查、日志应围绕同一条规则组织。
|
||||
- Fonu 当前已有这一方向,但页面还可以更像“资源详情页”。
|
||||
- Havline 当前已有这一方向,但页面还可以更像“资源详情页”。
|
||||
|
||||
### 3.7 Pangolin
|
||||
|
||||
@@ -218,14 +218,14 @@
|
||||
- 对每个资源做审计和会话记录。
|
||||
- 多 connector 和站点冗余。
|
||||
|
||||
对 Fonu 的启示:
|
||||
对 Havline 的启示:
|
||||
|
||||
- 普通 NAS 用户更需要“访问我的服务”,不是“管理网络隧道”。
|
||||
- 可以借鉴资源入口页:
|
||||
- 按应用、域名、出口分组。
|
||||
- 一键打开服务。
|
||||
- 显示维护状态和最近访问。
|
||||
- 不建议 Fonu 当前做企业 RBAC、SSO、PAM;成本过高。
|
||||
- 不建议 Havline 当前做企业 RBAC、SSO、PAM;成本过高。
|
||||
|
||||
### 3.8 Uptime Kuma
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
- 通知渠道丰富。
|
||||
- 2FA。
|
||||
|
||||
对 Fonu 的启示:
|
||||
对 Havline 的启示:
|
||||
|
||||
- 当前公开状态页已有域名和可用率。
|
||||
- 下一步最值得补:
|
||||
@@ -268,7 +268,7 @@
|
||||
- 请求重放和 Copy as cURL。
|
||||
- SSE 实时推送。
|
||||
|
||||
对 Fonu 的启示:
|
||||
对 Havline 的启示:
|
||||
|
||||
- 适合做成“Cloudflare 出口调试模式”,不要默认常驻。
|
||||
- 可提供:
|
||||
@@ -291,7 +291,7 @@
|
||||
|
||||
优秀项目不会让用户直接面对复杂配置。
|
||||
|
||||
建议 Fonu 增加:
|
||||
建议 Havline 增加:
|
||||
|
||||
1. 检查 cloudflared 是否安装。
|
||||
2. 检查 Account ID。
|
||||
@@ -346,7 +346,7 @@ nas.example.com
|
||||
|
||||
### 4.4 路由表体验
|
||||
|
||||
当前 Fonu 已有路由表。建议继续补:
|
||||
当前 Havline 已有路由表。建议继续补:
|
||||
|
||||
- 拖拽排序。
|
||||
- 启用/停用。
|
||||
@@ -410,7 +410,7 @@ nas.example.com
|
||||
|
||||
桌面管理器和 DockFlare 都把多账号作为进阶能力。
|
||||
|
||||
建议 Fonu:
|
||||
建议 Havline:
|
||||
|
||||
- 当前继续以 Account ID + API Token 为默认。
|
||||
- 远期可支持多账号。
|
||||
@@ -431,12 +431,12 @@ nas.example.com
|
||||
|
||||
---
|
||||
|
||||
## 五、Fonu 不该照搬的功能
|
||||
## 五、Havline 不该照搬的功能
|
||||
|
||||
| 功能 | 结论 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| Kubernetes Controller | 不做 | 不是 NAS 用户场景 |
|
||||
| 企业 SSO / PAM / RBAC 平台 | 暂不做 | 成本高,偏离 Fonu 定位 |
|
||||
| 企业 SSO / PAM / RBAC 平台 | 暂不做 | 成本高,偏离 Havline 定位 |
|
||||
| Cloudflare 邮件套件 | 不做 | 与访问管理无关 |
|
||||
| 多语言 13 种起步 | 暂不做 | 先保持中文产品体验 |
|
||||
| 多主机 Agent 优先 | 后置 | 先做好单机 NAS |
|
||||
@@ -446,7 +446,7 @@ nas.example.com
|
||||
|
||||
---
|
||||
|
||||
## 六、Fonu 建议路线
|
||||
## 六、Havline 建议路线
|
||||
|
||||
> 实施状态:截至 2026-10-01,本节的 P0 已完成第一轮落地,详见 `cloudflared-current-features.md`。
|
||||
|
||||
@@ -468,7 +468,7 @@ nas.example.com
|
||||
6. **拓扑图**
|
||||
- 域名 → 出口 → 隧道 → 服务。
|
||||
7. **Docker 标签发布**
|
||||
- 容器标签自动生成 Fonu 规则和 Cloudflare 出口。
|
||||
- 容器标签自动生成 Havline 规则和 Cloudflare 出口。
|
||||
8. **场景模板**
|
||||
- Home Assistant、Immich、Jellyfin、qBittorrent、AdGuard。
|
||||
9. **访问二维码与分享**
|
||||
@@ -494,7 +494,7 @@ nas.example.com
|
||||
|
||||
---
|
||||
|
||||
## 七、针对当前 Fonu 页面的具体改版建议
|
||||
## 七、针对当前 Havline 页面的具体改版建议
|
||||
|
||||
### 7.1 Cloudflare 总览页
|
||||
|
||||
@@ -562,7 +562,7 @@ DNS:已创建
|
||||
|
||||
## 八、最终建议
|
||||
|
||||
Fonu 不应继续横向堆叠 cloudflared 参数。更值得投入的是:
|
||||
Havline 不应继续横向堆叠 cloudflared 参数。更值得投入的是:
|
||||
|
||||
1. 把 Cloudflare 接入做成向导。
|
||||
2. 把“规则 → 出口 → 隧道 → DNS → 监控”做成同一条链路。
|
||||
@@ -571,6 +571,6 @@ Fonu 不应继续横向堆叠 cloudflared 参数。更值得投入的是:
|
||||
5. 把 Docker / NAS 服务发布做到自动发现。
|
||||
6. 把高级能力放到高级页,不干扰默认路径。
|
||||
|
||||
这会让 Fonu 与普通 cloudflared 面板产生明确差异:
|
||||
这会让 Havline 与普通 cloudflared 面板产生明确差异:
|
||||
|
||||
> 普通面板管理 cloudflared;Fonu 管理“我的服务如何安全、稳定地访问”。
|
||||
> 普通面板管理 cloudflared;Havline 管理“我的服务如何安全、稳定地访问”。
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Cloudflare 隧道:Fonu 适配版功能计划
|
||||
# Cloudflare 隧道:Havline 适配版功能计划
|
||||
|
||||
> 日期:2026-09-30
|
||||
> 定位:`cloudflared-feature-plan.md` 是「官方 + 参考项目的功能全集」;本文是**按 Fonu 产品定位筛选后的适配版计划**,实施以本文为准。
|
||||
> 核心原则:Fonu 不是 cloudflared 管理面板,而是「NAS / 家庭服务器的公网访问管理面板」。Cloudflare 隧道只是它的第二条出口。
|
||||
> 定位:`cloudflared-feature-plan.md` 是「官方 + 参考项目的功能全集」;本文是**按 Havline 产品定位筛选后的适配版计划**,实施以本文为准。
|
||||
> 核心原则:Havline 不是 cloudflared 管理面板,而是「NAS / 家庭服务器的公网访问管理面板」。Cloudflare 隧道只是它的第二条出口。
|
||||
|
||||
---
|
||||
|
||||
## 一、先明确 Fonu 是什么
|
||||
## 一、先明确 Havline 是什么
|
||||
|
||||
### 1.1 产品定位
|
||||
|
||||
Fonu 面对的是家庭 NAS、小主机、家用服务器的用户,核心价值是:
|
||||
Havline 面对的是家庭 NAS、小主机、家用服务器的用户,核心价值是:
|
||||
|
||||
- 用域名访问内网服务,而不是记 IP 和端口;
|
||||
- 自动处理证书、反代、穿透、DDNS;
|
||||
- 尽量少配置、少暴露、少维护。
|
||||
|
||||
Fonu 已经有三条主线:
|
||||
Havline 已经有三条主线:
|
||||
|
||||
1. **反向代理**:域名 → 本机 Nginx → 内网服务;
|
||||
2. **FRP 穿透**:本机 frpc → VPS frps → 域名;
|
||||
@@ -30,7 +30,7 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
|
||||
1. **CF 隧道是第二条出口,不是替代 FRP**:TCP/UDP、自主可控、大流量仍走 FRP。
|
||||
2. **规则同源**:用户维护的是「服务发布规则」,不是「隧道 ingress」。CF 只是一个出口选项。
|
||||
3. **融入 Fonu 体系**:监控、通知、状态页、日志、备份、升级、审计都复用现有模块,不另造一套面板。
|
||||
3. **融入 Havline 体系**:监控、通知、状态页、日志、备份、升级、审计都复用现有模块,不另造一套面板。
|
||||
|
||||
---
|
||||
|
||||
@@ -54,45 +54,45 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
|
||||
## 三、借鉴与不借鉴
|
||||
|
||||
参考项目 `Tunnel-Cloudflared-Manager 1.0.1` 是一个独立的 cloudflared 面板。以下判断按「是否符合 Fonu 产品形态」给出:
|
||||
参考项目 `Tunnel-Cloudflared-Manager 1.0.1` 是一个独立的 cloudflared 面板。以下判断按「是否符合 Havline 产品形态」给出:
|
||||
|
||||
### 3.1 值得借鉴
|
||||
|
||||
| 参考项目做法 | Fonu 该怎么用 |
|
||||
| 参考项目做法 | Havline 该怎么用 |
|
||||
| --- | --- |
|
||||
| token 解码成本地凭据 | 已用 Account ID + API Token 替代;保留解码能力用于导入 |
|
||||
| metrics 端口从日志发现 | 已实现,继续沿用 |
|
||||
| ingress 三条不变式 | 已实现,继续沿用 |
|
||||
| 云端 / 本地配置互相同步 | 值得做,但要简化为「diff → 拉取或覆盖」 |
|
||||
| 端点级增删改 | 值得做,但必须挂在 Fonu 的「服务发布规则」下 |
|
||||
| 端点级增删改 | 值得做,但必须挂在 Havline 的「服务发布规则」下 |
|
||||
| 快速隧道 | 只做「临时演示」,不做正式功能 |
|
||||
| 网络诊断 | 值得做,做成 Fonu 的一键诊断 |
|
||||
| 镜像与下载进度 | 值得做,但复用 Fonu 现有镜像表,不做用户自定义镜像管理 |
|
||||
| 网络诊断 | 值得做,做成 Havline 的一键诊断 |
|
||||
| 镜像与下载进度 | 值得做,但复用 Havline 现有镜像表,不做用户自定义镜像管理 |
|
||||
| Windows 隐藏控制台 | 已实现,继续沿用 |
|
||||
|
||||
### 3.2 不借鉴
|
||||
|
||||
| 参考项目做法 | 不借鉴的理由 |
|
||||
| --- | --- |
|
||||
| 自带 JWT + 账号体系 | Fonu 已有会话 + API Token,重复建设 |
|
||||
| 独立主题、多语言 | 与 Fonu 全局设置重复 |
|
||||
| 多账号 cert.pem 登录 | 与 Fonu 的 Account ID + API Token 模型冲突;远期若有需求再评估 |
|
||||
| 自带 JWT + 账号体系 | Havline 已有会话 + API Token,重复建设 |
|
||||
| 独立主题、多语言 | 与 Havline 全局设置重复 |
|
||||
| 多账号 cert.pem 登录 | 与 Havline 的 Account ID + API Token 模型冲突;远期若有需求再评估 |
|
||||
| 私有网络 / WARP routing / virtual network | 面向企业 Zero Trust,NAS 用户不需要 |
|
||||
| Kubernetes / Ansible / Terraform / 云厂商部署指南 | 不是 Fonu 的交付形态 |
|
||||
| Kubernetes / Ansible / Terraform / 云厂商部署指南 | 不是 Havline 的交付形态 |
|
||||
| 负载均衡 | 家用单机场景不需要 |
|
||||
| Workers VPC / Hyperdrive / Spectrum / Keyless SSL | 超出 Fonu 定位 |
|
||||
| WebSocket 实时日志 | Fonu 已有 SSE 日志模式,不为 CF 单独引入 WSS |
|
||||
| 自定义镜像管理(增删改测) | Fonu 只需内置镜像表 + 自定义 URL,不需要用户维护镜像源 |
|
||||
| 独立 systemd / Windows service 安装 | Fonu 自己托管进程,不接管外部服务 |
|
||||
| Workers VPC / Hyperdrive / Spectrum / Keyless SSL | 超出 Havline 定位 |
|
||||
| WebSocket 实时日志 | Havline 已有 SSE 日志模式,不为 CF 单独引入 WSS |
|
||||
| 自定义镜像管理(增删改测) | Havline 只需内置镜像表 + 自定义 URL,不需要用户维护镜像源 |
|
||||
| 独立 systemd / Windows service 安装 | Havline 自己托管进程,不接管外部服务 |
|
||||
| 快速隧道作为正式功能 | 官方明确不保证 SLA,只做演示入口 |
|
||||
|
||||
---
|
||||
|
||||
## 四、符合 Fonu 的功能全景
|
||||
## 四、符合 Havline 的功能全景
|
||||
|
||||
按「用户价值」而不是「参考项目有什么」重新分组。
|
||||
|
||||
### 4.1 出口抽象(Fonu 的核心差异化)
|
||||
### 4.1 出口抽象(Havline 的核心差异化)
|
||||
|
||||
**F-1 服务发布规则增加「出口」字段**
|
||||
|
||||
@@ -111,7 +111,7 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
- 出口支持多选(FRP + Cloudflare),用于灰度或双保险。
|
||||
- 验收:同一域名可同时存在 FRP 与 CF 入口,互不影响。
|
||||
|
||||
### 4.2 自动化(延续 Fonu「自动为主」)
|
||||
### 4.2 自动化(延续 Havline「自动为主」)
|
||||
|
||||
**F-4 一次配置,后续全自动**(大部分已完成)
|
||||
|
||||
@@ -134,14 +134,14 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
|
||||
- 进程退出自动重启;
|
||||
- `/ready` 与 `ha_connections` 判活;
|
||||
- 连续失败进入 Fonu 告警;恢复后发通知。
|
||||
- 连续失败进入 Havline 告警;恢复后发通知。
|
||||
|
||||
### 4.3 融入 Fonu 现有体系
|
||||
### 4.3 融入 Havline 现有体系
|
||||
|
||||
**F-8 监控与可用率**
|
||||
|
||||
- 新增 cloudflared 的 `monitor.Source`;
|
||||
- 隧道健康进入 Fonu 现有可用率与历史指标。
|
||||
- 隧道健康进入 Havline 现有可用率与历史指标。
|
||||
|
||||
**F-9 通知**
|
||||
|
||||
@@ -155,7 +155,7 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
|
||||
**F-11 日志中心**
|
||||
|
||||
- CF 隧道日志进入 Fonu 日志页;
|
||||
- CF 隧道日志进入 Havline 日志页;
|
||||
- 复用 SSE 增量推送,不单独做 WebSocket。
|
||||
|
||||
**F-12 仪表盘**
|
||||
@@ -164,19 +164,19 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
|
||||
**F-13 备份与迁移**
|
||||
|
||||
- `cf_tunnels` 与凭据纳入 Fonu 备份/恢复流程;
|
||||
- `cf_tunnels` 与凭据纳入 Havline 备份/恢复流程;
|
||||
- 恢复后自动重启隧道。
|
||||
|
||||
**F-14 一键升级与版本策略**
|
||||
|
||||
- cloudflared 版本跟随 Fonu 升级流程;
|
||||
- cloudflared 版本跟随 Havline 升级流程;
|
||||
- 提供「检查更新 / 更新 cloudflared / 回滚」;
|
||||
- 不要求用户手工替换二进制。
|
||||
|
||||
**F-15 审计**
|
||||
|
||||
- 记录谁在何时修改了服务发布规则、出口、CF 凭据、全局开关;
|
||||
- 复用 Fonu 的日志/审计基础设施。
|
||||
- 复用 Havline 的日志/审计基础设施。
|
||||
|
||||
### 4.4 NAS 场景体验
|
||||
|
||||
@@ -198,13 +198,13 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
**F-19 证书提示**
|
||||
|
||||
- 使用 CF 出口时,CF 边缘自动提供 HTTPS;
|
||||
- 页面提示「该域名无需在 Fonu 申请证书」,避免用户重复操作。
|
||||
- 页面提示「该域名无需在 Havline 申请证书」,避免用户重复操作。
|
||||
|
||||
### 4.5 运维与安全
|
||||
|
||||
**F-20 端点级编辑**
|
||||
|
||||
- 在 Fonu 规则详情里编辑 hostname、path、service、协议;
|
||||
- 在 Havline 规则详情里编辑 hostname、path、service、协议;
|
||||
- 提供 `ingress validate` 与 `ingress rule <url>` 校验。
|
||||
|
||||
**F-21 云端漂移检测**
|
||||
@@ -215,7 +215,7 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
**F-22 一键诊断**
|
||||
|
||||
- `tunnel info`、`tunnel diag`、网络预检(region1/region2、7844、443)、系统要求检查;
|
||||
- 结果转成 Fonu 风格的可读结论与建议。
|
||||
- 结果转成 Havline 风格的可读结论与建议。
|
||||
|
||||
**F-23 令牌轮换**
|
||||
|
||||
@@ -266,24 +266,24 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
|
||||
| 功能 | 结论 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| 独立登录、JWT、多用户体系 | 不做 | Fonu 已有认证与 API Token |
|
||||
| 独立主题、多语言 | 不做 | 属于 Fonu 全局设置 |
|
||||
| 独立登录、JWT、多用户体系 | 不做 | Havline 已有认证与 API Token |
|
||||
| 独立主题、多语言 | 不做 | 属于 Havline 全局设置 |
|
||||
| 私有网络 / WARP routing | 不做(远期评估) | 面向 Zero Trust 企业,NAS 用户不需要 |
|
||||
| 负载均衡 | 不做 | 家用单机场景不需要 |
|
||||
| Kubernetes / Ansible / Terraform 指南 | 不做 | 不是 Fonu 交付形态 |
|
||||
| Kubernetes / Ansible / Terraform 指南 | 不做 | 不是 Havline 交付形态 |
|
||||
| Workers VPC / Hyperdrive / Spectrum | 不做 | 超范围 |
|
||||
| WebSocket 实时日志 | 不做 | 复用 Fonu SSE |
|
||||
| WebSocket 实时日志 | 不做 | 复用 Havline SSE |
|
||||
| 自定义镜像管理 | 不做 | 内置镜像表 + 自定义 URL 足够 |
|
||||
| 接管外部 systemd / Windows service | 不做 | Fonu 只托管自己启动的进程 |
|
||||
| 接管外部 systemd / Windows service | 不做 | Havline 只托管自己启动的进程 |
|
||||
| 快速隧道作为正式功能 | 不做 | 官方不保证 SLA,只做演示 |
|
||||
| 代配 Access 策略 | 不做 | 高风险,只做只读检测 |
|
||||
| 自动扩权 / 自动改系统防火墙 | 不做 | 保持 Fonu 现有安全边界 |
|
||||
| 自动扩权 / 自动改系统防火墙 | 不做 | 保持 Havline 现有安全边界 |
|
||||
|
||||
---
|
||||
|
||||
## 六、与现有模块的整合点
|
||||
|
||||
| Fonu 模块 | 整合方式 |
|
||||
| Havline 模块 | 整合方式 |
|
||||
| --- | --- |
|
||||
| 反向代理规则 | 增加「出口」字段;CF 隧道从规则派生 |
|
||||
| FRP 多服务端 | 与 CF 出口并列,互不影响 |
|
||||
@@ -335,7 +335,7 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
| 阶段 | 内容 | 目标 |
|
||||
| --- | --- | --- |
|
||||
| 阶段 1 | F-1 ~ F-3 出口抽象;F-5 规则同步;F-6 DNS 管理完善 | 让 CF 成为反代规则的一个出口,而不是独立模块 |
|
||||
| 阶段 2 | F-8 ~ F-15 体系整合;F-20 ~ F-22 编辑与诊断 | 让 CF 隧道进入 Fonu 监控、通知、状态页、日志、备份、审计 |
|
||||
| 阶段 2 | F-8 ~ F-15 体系整合;F-20 ~ F-22 编辑与诊断 | 让 CF 隧道进入 Havline 监控、通知、状态页、日志、备份、审计 |
|
||||
| 阶段 3 | F-16 ~ F-19 NAS 体验;F-23 ~ F-26 运维安全 | 一键发布、域名选择、令牌轮换、条款提示、Access 检测 |
|
||||
| 阶段 4 | F-27 ~ F-30 可选增强 | 快速隧道、多副本、边缘优选、账号模式按需 |
|
||||
|
||||
@@ -347,7 +347,7 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
|
||||
- 用户只维护「域名 → 服务」的规则,出口切换不需要改域名或端口;
|
||||
- 反代规则变化后,CF 隧道与 DNS 自动跟随;
|
||||
- 隧道断连能进 Fonu 告警与状态页;
|
||||
- 隧道断连能进 Havline 告警与状态页;
|
||||
- 一条规则可以同时走 FRP 与 CF,互不影响;
|
||||
- CF 出口域名不需要本地证书;
|
||||
- 备份恢复后隧道能自动恢复;
|
||||
@@ -358,13 +358,13 @@ Cloudflare 隧道的加入,是为了解决一类现有链路解决不了的人
|
||||
|
||||
## 十、结论
|
||||
|
||||
Fonu 需要的不是「另一个 cloudflared 面板」,而是:
|
||||
Havline 需要的不是「另一个 cloudflared 面板」,而是:
|
||||
|
||||
> 把 Cloudflare 隧道做成反向代理规则的一个出口选项,并让监控、通知、状态页、日志、备份、升级、审计自动覆盖它。
|
||||
|
||||
按本文阶段 1-3 实施,Fonu 的 CF 模块会形成与参考项目不同的产品形态:参考项目面向「管理 cloudflared」,Fonu 面向「管理我的服务怎么上公网」。
|
||||
按本文阶段 1-3 实施,Havline 的 CF 模块会形成与参考项目不同的产品形态:参考项目面向「管理 cloudflared」,Havline 面向「管理我的服务怎么上公网」。
|
||||
|
||||
本文与 `cloudflared-feature-plan.md` 的关系:
|
||||
|
||||
- `cloudflared-feature-plan.md`:官方 + 参考项目的功能全集,用于查漏;
|
||||
- 本文:按 Fonu 定位筛选后的实施计划,**实施以本文为准**。
|
||||
- 本文:按 Havline 定位筛选后的实施计划,**实施以本文为准**。
|
||||
@@ -1,18 +1,18 @@
|
||||
# Cloudflare Tunnel(cloudflared)与 Fonu 的融合方案
|
||||
# Cloudflare Tunnel(cloudflared)与 Havline 的融合方案
|
||||
|
||||
> 日期:2026-09-25,v2 修订 2026-09-28 对象:Fonu 1.5.4(VERSION 1.5.4)
|
||||
> 日期:2026-09-25,v2 修订 2026-09-28 对象:Havline 1.5.4(VERSION 1.5.4)
|
||||
> 方法:先查 Cloudflare 官方文档与同类开源项目,再逐个核对本仓库现有可复用面(下文带 `文件:行` 的都是读过代码得出的);v2 逐文件核对了 `F:\WWW\cf\Tunnel-Cloudflared-linux`(Tunnel-Cloudflared-Manager 1.0.1)源码。
|
||||
> 结论速览:**值得做,但建议以「第二条出口」的形态并存,先做 locally-managed + 进程托管,不接 Cloudflare API**。v2 修订了部署位置、模式生效侧、metrics / 健康与二进制校验四处关键细节,详见 §五、§六、§十二;实施以 §十二 为准。
|
||||
|
||||
---
|
||||
|
||||
## 一、为什么值得做(对 Fonu 的战略意义)
|
||||
## 一、为什么值得做(对 Havline 的战略意义)
|
||||
|
||||
Fonu 现在的穿透链路**必须有公网服务端**:本机 `frpc` → VPS 上的 `frps` → 域名。用户得有一台带公网 IP 的云主机,还得自己放行端口、签证书、配 DNS。
|
||||
Havline 现在的穿透链路**必须有公网服务端**:本机 `frpc` → VPS 上的 `frps` → 域名。用户得有一台带公网 IP 的云主机,还得自己放行端口、签证书、配 DNS。
|
||||
|
||||
Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `cloudflared` **主动向外**建立到边缘的长连接(出站 only),公网侧不需要任何监听端口,也不需要公网 IP——顺带白拿 CF 的 WAF / DDoS 防护与自动 HTTPS。
|
||||
|
||||
对 Fonu 的用户画像(家庭/个人 NAS)来说,这正好补上一类人:**没有实体公网 IP、或不想为穿透长期租 VPS**的人。
|
||||
对 Havline 的用户画像(家庭/个人 NAS)来说,这正好补上一类人:**没有实体公网 IP、或不想为穿透长期租 VPS**的人。
|
||||
|
||||
但要说清楚它**替代不了什么**(这决定了它是「并存」而不是「替换」):
|
||||
|
||||
@@ -30,16 +30,16 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
|
||||
## 二、同类项目(检索结果)
|
||||
|
||||
| 项目 | 做什么 | 对 Fonu 的借鉴 |
|
||||
| 项目 | 做什么 | 对 Havline 的借鉴 |
|
||||
| --- | --- | --- |
|
||||
| [WisdomSky/Cloudflared-web](https://github.com/WisdomSky/Cloudflared-web)(≈451★,Vue) | Docker 镜像里打包 `cloudflared` + 简单 Web UI,用来 start/stop **remotely-managed** 隧道 | **最接近的同位产品**:证明「面板管 cloudflared 进程」这个形态成立;但它的能力止步于开关,规则仍在 CF 面板里——这正是 Fonu 可以做得更好的地方(规则同源生成) |
|
||||
| [adyanth/cloudflare-operator](https://github.com/adyanth/cloudflare-operator)(≈691★) | Kubernetes Operator,用 **CF API** 创建/管理 Tunnel 与 DNS | 演示了「API 驱动」的完整闭环;但需要 API Token + Account/Zone ID,是第二个凭据面(Fonu 第一轮不建议走,见 §六) |
|
||||
| [WisdomSky/Cloudflared-web](https://github.com/WisdomSky/Cloudflared-web)(≈451★,Vue) | Docker 镜像里打包 `cloudflared` + 简单 Web UI,用来 start/stop **remotely-managed** 隧道 | **最接近的同位产品**:证明「面板管 cloudflared 进程」这个形态成立;但它的能力止步于开关,规则仍在 CF 面板里——这正是 Havline 可以做得更好的地方(规则同源生成) |
|
||||
| [adyanth/cloudflare-operator](https://github.com/adyanth/cloudflare-operator)(≈691★) | Kubernetes Operator,用 **CF API** 创建/管理 Tunnel 与 DNS | 演示了「API 驱动」的完整闭环;但需要 API Token + Account/Zone ID,是第二个凭据面(Havline 第一轮不建议走,见 §六) |
|
||||
| [qingchencloud/cftunnel](https://github.com/qingchencloud/cftunnel)(≈338★) | 轻量隧道控制 | 佐证需求存在 |
|
||||
| [brenner-tobias/addon-cloudflared](https://github.com/brenner-tobias/addon-cloudflared) | Home Assistant 插件,locally-managed 与 remotely-managed 两种模式都支持 | HA 社区的做法可参考:**两种模式并存**,文档把差异讲透 |
|
||||
| [cloudflare/cloudflared](https://github.com/cloudflare/cloudflared) | 官方客户端(Apache-2.0) | 我们的被托管对象;GitHub Release 提供多平台二进制 |
|
||||
| 对照项:[Pangolin](https://github.com/fosrl/pangolin) / Tailscale Funnel / ngrok | 同类「免公网 IP」方案 | Tailscale Funnel 只开 HTTPS 且端口数量有限、ngrok 免费档收紧;CF Tunnel 免费档最宽松,这也是它值得优先支持的原因 |
|
||||
|
||||
`docs/feature-benchmark-2026-09-24.md` 里已经把 Pangolin 列为架构最近的同类项目;Cloudflare Tunnel 的差别在于:**它把「服务端」外包给了 Cloudflare,Fonu 只需管好本机客户端**。
|
||||
`docs/feature-benchmark-2026-09-24.md` 里已经把 Pangolin 列为架构最近的同类项目;Cloudflare Tunnel 的差别在于:**它把「服务端」外包给了 Cloudflare,Havline 只需管好本机客户端**。
|
||||
|
||||
---
|
||||
|
||||
@@ -51,23 +51,23 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
| 规则来源 | 本机 `config.yml` 的 `ingress:` | **CF 面板 / API**(本地 `config.yml` 会被**忽略**) |
|
||||
| 建隧道 | `cloudflared tunnel create <name>` | CF 面板,或 `POST /accounts/{id}/cfd_tunnel` |
|
||||
| DNS | `cloudflared tunnel route dns <tunnel> <hostname>` | CF 面板自动建 CNAME |
|
||||
| 与 Fonu 的价值取向 | ✅ **契合**:反代规则由 Fonu 同源生成(和现在生成 `frps.toml` / nginx vhost 一个路子) | ⚠️ 冲突:规则跑到 CF 面板去了,Fonu 沦为「进程开关」 |
|
||||
| 与 Havline 的价值取向 | ✅ **契合**:反代规则由 Havline 同源生成(和现在生成 `frps.toml` / nginx vhost 一个路子) | ⚠️ 冲突:规则跑到 CF 面板去了,Havline 沦为「进程开关」 |
|
||||
|
||||
官方文档确认了这个分界([配置参考](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/configuration-file)),社区也踩过坑:给 remotely-managed 隧道传 `--config` 时配置会被云端覆盖,日志里出现 `No ingress rules were defined ... cloudflared will return 503`([cloudflared#1029](https://github.com/cloudflare/cloudflared/issues/1029))。
|
||||
|
||||
**结论:第一轮只支持 locally-managed。** 若用户只想粘贴 token,提供「仅托管进程」的降级模式并**在界面上明确写清「规则由 Cloudflare 面板管理」**,不要让用户以为 Fonu 生成的规则生效了。
|
||||
**结论:第一轮只支持 locally-managed。** 若用户只想粘贴 token,提供「仅托管进程」的降级模式并**在界面上明确写清「规则由 Cloudflare 面板管理」**,不要让用户以为 Havline 生成的规则生效了。
|
||||
|
||||
> 补充(参考 §九 后修正):这个二分法不够细。参考实现证明**token 可以解码**(base64 的 JSON,含 `AccountTag` / `TunnelID` / `TunnelSecret`),用它自己生成 `creds.json` 就能按 locally-managed 跑本地 `config.yml`;再配合「云端 ↔ 本地 ingress 互相同步」,两种模式实际上是可切换的。所以 Fonu 的正确做法不是「二选一」,而是**显式区分并同步**:当前生效的是本地 `config.yml` 还是云端 ingress,以及「推送/拉取」两个动作。**v2 已把模式拆成三种并明确生效侧,见 §12.3。**
|
||||
> 补充(参考 §九 后修正):这个二分法不够细。参考实现证明**token 可以解码**(base64 的 JSON,含 `AccountTag` / `TunnelID` / `TunnelSecret`),用它自己生成 `creds.json` 就能按 locally-managed 跑本地 `config.yml`;再配合「云端 ↔ 本地 ingress 互相同步」,两种模式实际上是可切换的。所以 Havline 的正确做法不是「二选一」,而是**显式区分并同步**:当前生效的是本地 `config.yml` 还是云端 ingress,以及「推送/拉取」两个动作。**v2 已把模式拆成三种并明确生效侧,见 §12.3。**
|
||||
|
||||
### 健康探测(这点比 frp 更省事)
|
||||
|
||||
`cloudflared tunnel --metrics 127.0.0.1:<port> run --config ...` 会暴露 HTTP 端点,其中 **`/ready` 在有到边缘的活跃连接时返回 200**(CF 官方在 k8s livenessProbe 里就是这么用的)。Fonu 现在为了判断「隧道通不通」要绕 frps dashboard,还专门加过 `tunnel_known` 来区分「判不出来」和「不通」;CF Tunnel 则有明确的就绪端点,可以直接喂给现有健康链路。
|
||||
`cloudflared tunnel --metrics 127.0.0.1:<port> run --config ...` 会暴露 HTTP 端点,其中 **`/ready` 在有到边缘的活跃连接时返回 200**(CF 官方在 k8s livenessProbe 里就是这么用的)。Havline 现在为了判断「隧道通不通」要绕 frps dashboard,还专门加过 `tunnel_known` 来区分「判不出来」和「不通」;CF Tunnel 则有明确的就绪端点,可以直接喂给现有健康链路。
|
||||
|
||||
---
|
||||
|
||||
## 四、本仓库可复用的面(已核对代码)
|
||||
|
||||
| 能力 | Fonu 现有实现 | 复用方式 |
|
||||
| 能力 | Havline 现有实现 | 复用方式 |
|
||||
| --- | --- | --- |
|
||||
| 二进制下载 / 启用 | `Service.ListReleases`(`internal/frp/service.go:112`)、`ListBinaries`(`:120`)、`ActivateBinary`(`:258`);`internal/frp/runtime.go` 的 `fetchOfficialReleases` / `listBinaries` / `readActiveBinary` / `writeActiveBinary` / `versionFromBinaryPath` | cloudflared 同样发 GitHub Release,**同一套框架泛化成「可指定仓库」即可**(现在 URL 拼的是 frp 的 release 页) |
|
||||
| 客户端进程托管 | `internal/frp/manager.go`:`os.OpenFile(logPath, O_CREATE\|O_APPEND\|O_WRONLY, 0600)` + `cmd.Stdout/Stderr` 重定向 + 状态/退出原因 + `tailFile` 读日志尾 | 与 `cloudflared tunnel run` 完全同构,可直接复用(日志追加、不截断的语义也一致) |
|
||||
@@ -87,7 +87,7 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
|
||||
- 管理一份 cloudflared 二进制(复用 §四 的下载/启用框架)。
|
||||
- 管理 N 个隧道实例,每个实例 = 一份 `config.yml` + credentials + 日志文件 + 子进程。
|
||||
- `ingress` 规则**由 Fonu 生成**:从本机已有的反代规则里挑「要暴露的域名」,映射到 `http://127.0.0.1:<本地端口>`,模板与 nginx vhost 同源风格;末尾固定给 catch-all `service: http_status:404`(CF 要求最后一条兜底)。
|
||||
- `ingress` 规则**由 Havline 生成**:从本机已有的反代规则里挑「要暴露的域名」,映射到 `http://127.0.0.1:<本地端口>`,模板与 nginx vhost 同源风格;末尾固定给 catch-all `service: http_status:404`(CF 要求最后一条兜底)。
|
||||
- 健康:`--metrics` + `/ready`,写入现有健康与可用率链路;通知复用(隧道断开/恢复)。
|
||||
- 前端:与「公网服务端」同构的列表 + 详情(状态 / 日志 / 配置预览 / 下发)。
|
||||
|
||||
@@ -106,7 +106,7 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
|
||||
只生成 `config.yml` 与 DNS 指引,让用户自己跑 `cloudflared`。
|
||||
|
||||
**不建议**:不接管进程就没有状态、日志、健康、可用率,等于放弃 Fonu 的全部价值。
|
||||
**不建议**:不接管进程就没有状态、日志、健康、可用率,等于放弃 Havline 的全部价值。
|
||||
|
||||
---
|
||||
|
||||
@@ -119,7 +119,7 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
1. 新增「Cloudflare 隧道」入口:粘贴 **token**(remotely-managed)或上传 **credentials JSON + tunnel UUID**(locally-managed),凭据加密存储。
|
||||
2. 生成 `config.yml` 预览(**脱敏**,沿用 v1.2.12 的预览/下发两分支约定)并把文件写到 `<DataDir>/cloudflared/<name>/config.yml`。
|
||||
3. 界面给出 CF 侧操作指引(建隧道、CNAME、`cloudflared tunnel route dns`),**不接管进程**。
|
||||
4. 明确提示模式差异:locally-managed 才由 Fonu 生成规则。
|
||||
4. 明确提示模式差异:locally-managed 才由 Havline 生成规则。
|
||||
|
||||
### 阶段 2:托管进程与健康(核心价值)
|
||||
|
||||
@@ -146,7 +146,7 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
- TryCloudflare(快速隧道:`cloudflared tunnel --url http://localhost:PORT`)官方明确「不保证 SLA 与可用性,仅供测试与开发」,域名随机、每次重启变化——**不作为正式功能**,最多做成「临时演示」按钮。
|
||||
|
||||
**网络可用性**
|
||||
- 中国大陆访问 CF 边缘的延迟与稳定性不稳定,这对 Fonu 的用户群是**现实问题**:建议在文档与界面上对比说明「CF 适合海外访问优先的场景,国内链路以 frp + 自持 VPS 更可控」。
|
||||
- 中国大陆访问 CF 边缘的延迟与稳定性不稳定,这对 Havline 的用户群是**现实问题**:建议在文档与界面上对比说明「CF 适合海外访问优先的场景,国内链路以 frp + 自持 VPS 更可控」。
|
||||
- 出站 only 意味着 VPS 上不需要任何放行端口,但也就**读不到**「CF 侧有哪些域名指向了这条隧道」——相关校验只能靠 `cloudflared` 自身命令,不能靠 agent。
|
||||
|
||||
**实现边界**
|
||||
@@ -182,29 +182,29 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
### 9.2 值得直接借鉴的六个做法
|
||||
|
||||
**① token 可以解码成凭据,于是 token 模式也能本地管规则**
|
||||
`initialize_token_tunnel` 把 CF 隧道 token 解成 `{AccountTag, TunnelID, TunnelSecret}` 并写出 `creds.json` + `config.yml`——即「用户只给一个 token,但规则仍由面板本地生成」。这直接提升了 Fonu 方案的上限(§三 的补充已按此修正)。
|
||||
`initialize_token_tunnel` 把 CF 隧道 token 解成 `{AccountTag, TunnelID, TunnelSecret}` 并写出 `creds.json` + `config.yml`——即「用户只给一个 token,但规则仍由面板本地生成」。这直接提升了 Havline 方案的上限(§三 的补充已按此修正)。
|
||||
|
||||
**② metrics 端口不去猜,从日志里读出来**
|
||||
命名隧道启动时**故意不传 `--metrics`**,让 cloudflared 用默认端口段(20241-20245),然后从 stderr 解析 `INF starting metrics server on 127.0.0.1:65432/metrics` 拿到真实端口——天然避开端口冲突(Fonu 现在给 frps dashboard 固定端口,多实例时会撞)。
|
||||
命名隧道启动时**故意不传 `--metrics`**,让 cloudflared 用默认端口段(20241-20245),然后从 stderr 解析 `INF starting metrics server on 127.0.0.1:65432/metrics` 拿到真实端口——天然避开端口冲突(Havline 现在给 frps dashboard 固定端口,多实例时会撞)。
|
||||
|
||||
**③ `localhost` 必须归一化成 `127.0.0.1`**
|
||||
`add_tunnel_endpoint` 里把 `localhost` 换成 `127.0.0.1`,注释写明「避免 Cloudflare 将其转换为本地网络 IP」。这是 CF 侧的真实坑,Fonu 生成 `ingress` 时要照做。
|
||||
`add_tunnel_endpoint` 里把 `localhost` 换成 `127.0.0.1`,注释写明「避免 Cloudflare 将其转换为本地网络 IP」。这是 CF 侧的真实坑,Havline 生成 `ingress` 时要照做。
|
||||
|
||||
**④ 始终保留最后一条 catch-all**
|
||||
新增端点插在「数组倒数第一位之前」(`insert_pos = len - 1`),末条永远是 `service: http_status:404`。Fonu 改 `ingress` 时必须沿用这个不变式,否则配置校验不过。
|
||||
新增端点插在「数组倒数第一位之前」(`insert_pos = len - 1`),末条永远是 `service: http_status:404`。Havline 改 `ingress` 时必须沿用这个不变式,否则配置校验不过。
|
||||
|
||||
**⑤ 带宽指标:官方 metrics 没有 HTTP/2 字节数,它做了系统级兜底**
|
||||
- QUIC 下直接读 `quic_client_receive_bytes` / `quic_client_sent_bytes` / `quic_client_smoothed_rtt`。
|
||||
- HTTP/2 下官方无字节数,改用**按进程采 TCP 连接字节**:Linux 走 netlink `SOCK_DIAG_BY_FAMILY` + `/proc/<pid>/fd` 取 socket inode;Windows 走 `GetExtendedTcpTable` + `GetPerTcpConnectionEStats`(需要管理员,失败时给 `windows_estats_requires_admin` 降级原因)。
|
||||
- 并且**只统计公网远端 IP**(过滤私有/回环/链路本地/`100.64/10` CGNAT),延迟兜底用**主动 HEAD 探测** ingress 里的公网 hostname(带 `?__cf_probe=<nonce>` + no-cache)。
|
||||
> 对 Fonu 的意义:这一块可以**先不做**(我们已有规则级流量与可用率),但「降级要给出明确原因而不是空白」这个原则要学。
|
||||
> 对 Havline 的意义:这一块可以**先不做**(我们已有规则级流量与可用率),但「降级要给出明确原因而不是空白」这个原则要学。
|
||||
|
||||
**⑥ Windows 下起子进程要隐藏控制台窗口**
|
||||
`creation_flags(CREATE_NO_WINDOW)`(`0x08000000`)。Fonu 本机在 Windows 上跑,托管 `cloudflared.exe` 时必须带上,否则会闪黑窗。
|
||||
`creation_flags(CREATE_NO_WINDOW)`(`0x08000000`)。Havline 本机在 Windows 上跑,托管 `cloudflared.exe` 时必须带上,否则会闪黑窗。
|
||||
|
||||
### 9.3 与 Fonu 的差异(决定「借鉴什么、不借鉴什么」)
|
||||
### 9.3 与 Havline 的差异(决定「借鉴什么、不借鉴什么」)
|
||||
|
||||
| 维度 | cf-tunnel-manager | Fonu 该怎么做 |
|
||||
| 维度 | cf-tunnel-manager | Havline 该怎么做 |
|
||||
| --- | --- | --- |
|
||||
| 职责 | **专做** Cloudflare Tunnel 的一个独立面板 | 把 CF 作为**第二条出口**并入现有规则/健康/通知/状态页体系 |
|
||||
| 存储 | JSON 文件(`data/*.json`)+ `data/tunnels/<名>/` | 继续用 SQLite + 迁移;凭据进 `secret.Box` 加密字段 |
|
||||
@@ -212,11 +212,11 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
| 健康 | metrics 端点 scraping + 主动探测(仅前端展示) | 接入 `internal/monitor` 的 `Source`/`HealthRecorder`,落库可用率 + 复用告警与通知 |
|
||||
| 实时日志 | WebSocket 广播(专为「看着它滚」设计) | 沿用现有「读日志文件 + 日志页」模型(与 frpc 一致,无需引入 WS) |
|
||||
| 多实例 | 多隧道并行(每隧道一个子进程) | 同模;但用 SQLite 建 `tunnels` 表而不是内存 HashMap |
|
||||
| 认证 | 自带 JWT + 账号体系 | 复用 Fonu 现有会话 + API Token |
|
||||
| 认证 | 自带 JWT + 账号体系 | 复用 Havline 现有会话 + API Token |
|
||||
|
||||
### 9.4 对方案本体的修订建议
|
||||
|
||||
1. **方案 A 的第一阶段就加「凭据导入」两条路**:`token`(解码出 creds.json)与 `cert.pem + credentials JSON`(账号模式),二者最终都落到「本地 `config.yml` 由 Fonu 生成」。
|
||||
1. **方案 A 的第一阶段就加「凭据导入」两条路**:`token`(解码出 creds.json)与 `cert.pem + credentials JSON`(账号模式),二者最终都落到「本地 `config.yml` 由 Havline 生成」。
|
||||
2. **阶段 2 把「metrics 端口从日志发现」写成实现约定**,不要固定端口。
|
||||
3. **阶段 3 增加「云端 ↔ 本地同步」两个动作**(拉取 / 推送),并在界面上标明当前生效侧——这是参考项目比原方案更完整的地方。
|
||||
4. **`ingress` 生成器锁定三条不变式**:末条 catch-all、`localhost` → `127.0.0.1`、插入位置在 catch-all 之前;并补一个「生成后自检」的纯函数测试(参照本仓 `maskFRPSSecrets` 的测试写法)。
|
||||
@@ -234,7 +234,7 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
| 手段 | 具体做法 | 适用与代价 |
|
||||
| --- | --- | --- |
|
||||
| **选协议** | `--protocol quic` / `http2` | 国内 QUIC(UDP 7844)常被 QoS 掉,`http2`(TCP 7844)反而稳;这是**成本最低、收益最直接**的一项 |
|
||||
| **选地址族** | `--edge-ip-version 4` / `6` / `auto`,或环境变量 `TUNNEL_EDGE_IP_VERSION` | ⚠️ **2026.4.0 起默认值由 `4` 改成 `auto`**(官方 CHANGES 里的 Breaking Change)——`auto` 会跟着系统解析器走,国内环境若 IPv6 路由差就可能反而变慢。**建议 Fonu 显式下发 `--edge-ip-version 4`**(除非用户自己有 IPv6 好线路) |
|
||||
| **选地址族** | `--edge-ip-version 4` / `6` / `auto`,或环境变量 `TUNNEL_EDGE_IP_VERSION` | ⚠️ **2026.4.0 起默认值由 `4` 改成 `auto`**(官方 CHANGES 里的 Breaking Change)——`auto` 会跟着系统解析器走,国内环境若 IPv6 路由差就可能反而变慢。**建议 Havline 显式下发 `--edge-ip-version 4`**(除非用户自己有 IPv6 好线路) |
|
||||
| **优选边缘 IP** | 对 `region1.v2.argotunnel.com` / `region2.v2.argotunnel.com` 做延迟测速,选中最快的官方边缘 IP,写 hosts 覆盖解析 | 官方文档给出固定端点与端口:这类域名指向 CF 的 IPv4 段(`198.41.192.7`~`198.41.192.127`、`162.159.234.1`~`162.159.234.10`)与 **端口 7844(TCP/UDP)**。这是社区「Tunnel 优选 IP」的通行做法 |
|
||||
|
||||
**优选时两条不能错的约束**:
|
||||
@@ -250,19 +250,19 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
|
||||
也就是说:换域名解析 / 让访客改 hosts 都不现实,**访问侧这一段在 Tunnel 架构下就是交给 CF 的**。
|
||||
|
||||
### 对 Fonu 方案的落点
|
||||
### 对 Havline 方案的落点
|
||||
|
||||
| 阶段 | 做什么 | 依据 |
|
||||
| --- | --- | --- |
|
||||
| 阶段 2(建议直接做) | 「网络调优」三项做成设置项:`protocol`(默认 http2)、`edge-ip-version`(**默认 4**)、`ha-connections`;以命令行参数下发 | 参考实现已验证可行,且改一项就能治国内 QUIC 被打压的场景 |
|
||||
| 阶段 3(可选) | 「边缘优选」工具:并发探测官方 IPv4 段的 **TCP 7844 握手延迟** → 生成「两个 region 各 1~2 个优选 IP」的 hosts 建议;**页面展示而不是代写 hosts**(Windows 改 hosts 需管理员,Fonu 不应越权),并提供「复制这段内容」+ 生效检测 | 沿用本仓「不越权、降级要说明原因」的口径 |
|
||||
| 阶段 3(可选) | 「边缘优选」工具:并发探测官方 IPv4 段的 **TCP 7844 握手延迟** → 生成「两个 region 各 1~2 个优选 IP」的 hosts 建议;**页面展示而不是代写 hosts**(Windows 改 hosts 需管理员,Havline 不应越权),并提供「复制这段内容」+ 生效检测 | 沿用本仓「不越权、降级要说明原因」的口径 |
|
||||
| 阶段 3(可选) | 优选前后对比:用**已有的** `metrics_samples` 延迟/可用率序列展示效果,而不是另造一套测速图 | 复用 §四 的健康与指标链路 |
|
||||
|
||||
> metrics 端口发现、健康判定与网络调优默认值见 §12.5 / §12.7;本节只讨论「本机 → CF 边缘」的优选。
|
||||
|
||||
### 必须写进功能说明的三句实话
|
||||
|
||||
1. 优选只影响「本机 → CF 边缘」这一段;访客到自己那侧 CF 边缘的那段,Fonu 与用户都控制不了。
|
||||
1. 优选只影响「本机 → CF 边缘」这一段;访客到自己那侧 CF 边缘的那段,Havline 与用户都控制不了。
|
||||
2. 所以它**不是**「翻墙提速」类工具:国内访问 CF 的稳定性受链路与运营商影响,优选只能把「本机出口到边缘」这一段挑好。
|
||||
3. 官方端点会变(见上),因此把所有 IP/域名做成**可配置 + 有默认值 + 可一键恢复默认**。
|
||||
|
||||
@@ -270,13 +270,13 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
|
||||
## 十一、给用户的一句话结论(可直接对外)
|
||||
|
||||
> Fonu 现在支持两类公网出口:**frp**(自持 VPS,任意 TCP/UDP,完全自主)与 **Cloudflare 隧道**(无需公网 IP 与 VPS,HTTP 类最省事,自带 CF 防护)。
|
||||
> Havline 现在支持两类公网出口:**frp**(自持 VPS,任意 TCP/UDP,完全自主)与 **Cloudflare 隧道**(无需公网 IP 与 VPS,HTTP 类最省事,自带 CF 防护)。
|
||||
> 要裸 TCP 端口、要完全自主,用 frp;要让内网 Web 服务零端口暴露地上公网,用 Cloudflare 隧道。
|
||||
> Cloudflare 隧道还提供网络调优(协议 / 地址族 / 连接数)与**边缘 IP 优选**(仅作用于本机到 CF 边缘这一段,访客侧无法自选)。
|
||||
|
||||
---
|
||||
|
||||
## 十二、方案优化 v2(结合 Tunnel-Cloudflared-Manager 1.0.1 与 Fonu 1.5.4)
|
||||
## 十二、方案优化 v2(结合 Tunnel-Cloudflared-Manager 1.0.1 与 Havline 1.5.4)
|
||||
|
||||
> 本节是 2026-09-28 的优化版落地口径。§一~§十一 保留为背景与论证;出现冲突时以本节为准。
|
||||
|
||||
@@ -290,43 +290,43 @@ Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `
|
||||
| --- | --- | --- |
|
||||
| token 解码后的 JSON 字段是 `a` / `t` / `s`,兼容 `AccountTag` / `TunnelID` / `TunnelSecret`;使用标准 base64 并自动补 `=` | `src/services/cloudflare_api.rs` 的 `decode_tunnel_token` / `base64_decode` | token 模式也能本地生成 `creds.json`,从而按 locally-managed 跑本地规则 |
|
||||
| `cert.pem` 内含 `-----BEGIN ARGO TUNNEL TOKEN-----` 块,解出 `accountID` + `apiToken` / `serviceKey` | 同文件 `parse_cert_pem` | 只有账号模式持有云端 API 凭据,才能 push / fetch 云端 ingress |
|
||||
| `start_tunnel` 在 token 非空时优先用 `--token` 启动,即使已经生成了本地 `config.yml` | `src/services/tunnel.rs` 的 `start_tunnel` | **本地规则会被云端覆盖**;Fonu 必须显式选择「本地生效」还是「云端生效」 |
|
||||
| `start_tunnel` 在 token 非空时优先用 `--token` 启动,即使已经生成了本地 `config.yml` | `src/services/tunnel.rs` 的 `start_tunnel` | **本地规则会被云端覆盖**;Havline 必须显式选择「本地生效」还是「云端生效」 |
|
||||
| 命名隧道**不传** `--metrics`,从 stderr 解析 `starting metrics server on 127.0.0.1:<port>/metrics`;快速隧道传 `--metrics 127.0.0.1:0` | `start_tunnel` / `start_quick_tunnel` | metrics 端口必须从日志发现,不能固定,否则多实例会撞端口 |
|
||||
| 健康用 `cloudflared_tunnel_ha_connections > 0` 判定已连接;QUIC 有字节指标,HTTP/2 没有 | `start_metrics_scraper` | `/ready` 只适合做就绪判断,连接数与带宽要走 metrics;HTTP/2 带宽要明确降级 |
|
||||
| 二进制下载只校验「文件 >= 10MB」,没有 SHA-256;`get_available_mirrors` 返回的镜像 ID 与实际支持的镜像不一致 | `src/services/binary.rs` | Fonu 必须做哈希 / 版本校验,并统一镜像表 |
|
||||
| 二进制下载只校验「文件 >= 10MB」,没有 SHA-256;`get_available_mirrors` 返回的镜像 ID 与实际支持的镜像不一致 | `src/services/binary.rs` | Havline 必须做哈希 / 版本校验,并统一镜像表 |
|
||||
| ingress 三条不变式:末条固定 `http_status:404`、`localhost` 归一化为 `127.0.0.1`、新规则插在 catch-all 之前 | `add_tunnel_endpoint` / `add_tunnel_endpoint_local_only` | 生成器必须锁定这三条并加纯函数测试 |
|
||||
| Windows 子进程设置 `CREATE_NO_WINDOW` | `start_tunnel` | Fonu 在 Windows 托管 `cloudflared.exe` 时必须带,否则闪黑窗 |
|
||||
| Windows 子进程设置 `CREATE_NO_WINDOW` | `start_tunnel` | Havline 在 Windows 托管 `cloudflared.exe` 时必须带,否则闪黑窗 |
|
||||
| 快速隧道从 stderr 解析 `*.trycloudflare.com` URL 并缓存 | `start_quick_tunnel` | 只能做「临时演示」;URL 每次重启都会变 |
|
||||
| 存储是 JSON 文件 + 内存 `HashMap<String, Child>` | `src/storage/config.rs`、`TunnelProcessManager` | Fonu 用 SQLite + 迁移,多实例与并发状态更稳 |
|
||||
| 存储是 JSON 文件 + 内存 `HashMap<String, Child>` | `src/storage/config.rs`、`TunnelProcessManager` | Havline 用 SQLite + 迁移,多实例与并发状态更稳 |
|
||||
|
||||
### 12.2 部署位置:原方案最大的空白
|
||||
|
||||
原方案默认「本机跑 cloudflared」,但没有回答一个关键问题:**Fonu 是容器化部署,容器里的 `127.0.0.1` 不是宿主机。**
|
||||
原方案默认「本机跑 cloudflared」,但没有回答一个关键问题:**Havline 是容器化部署,容器里的 `127.0.0.1` 不是宿主机。**
|
||||
|
||||
Fonu 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx,`docker-compose.yml` 只映射 `6893` / `80` / `443`。因此 v2 的默认拓扑是:
|
||||
Havline 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx,`docker-compose.yml` 只映射 `6893` / `80` / `443`。因此 v2 的默认拓扑是:
|
||||
|
||||
> **cloudflared 跑在 Fonu 容器内,与 nginx 同命名空间;CF ingress 统一指向 `http://127.0.0.1:<nginx_http_port>`(默认 80),由 nginx 再转发到上游。**
|
||||
> **cloudflared 跑在 Havline 容器内,与 nginx 同命名空间;CF ingress 统一指向 `http://127.0.0.1:<nginx_http_port>`(默认 80),由 nginx 再转发到上游。**
|
||||
|
||||
这样做有四个直接收益:
|
||||
|
||||
1. 绕开容器网络命名空间的坑:`127.0.0.1` 在容器内确定指向 Fonu nginx,而不是宿主机或上游。
|
||||
1. 绕开容器网络命名空间的坑:`127.0.0.1` 在容器内确定指向 Havline nginx,而不是宿主机或上游。
|
||||
2. 复用现有 nginx 的安全、限流、geo、TLS、错误页与 Basic Auth,不在 CF 侧重复一套策略。
|
||||
3. 规则同源:CF 只负责「域名 → Fonu nginx」,nginx 负责「域名 → 上游」;本机反代规则仍是唯一事实来源。
|
||||
3. 规则同源:CF 只负责「域名 → Havline nginx」,nginx 负责「域名 → 上游」;本机反代规则仍是唯一事实来源。
|
||||
4. 只有 HTTP / HTTPS 规则能进 CF;TCP / UDP 规则明确排除,避免用户误解。
|
||||
|
||||
三种拓扑的取舍:
|
||||
|
||||
| 拓扑 | cloudflared 位置 | ingress 目标 | 适用 |
|
||||
| --- | --- | --- | --- |
|
||||
| **容器内(默认)** | Fonu 容器 | `http://127.0.0.1:80`(容器内 nginx) | 绝大多数部署;规则先接入本机 nginx |
|
||||
| host 网络 | Fonu 容器 `network_mode: host` | `http://127.0.0.1:80`(宿主机 nginx) | 需要直接访问宿主机端口、或已有宿主机 nginx |
|
||||
| VPS Agent(可选) | VPS 上的 `fonu-agent` 托管 | `http://127.0.0.1:<frps vhost 端口>` | 已有 VPS、只想让部分域名走 CF 的高级用户 |
|
||||
| **容器内(默认)** | Havline 容器 | `http://127.0.0.1:80`(容器内 nginx) | 绝大多数部署;规则先接入本机 nginx |
|
||||
| host 网络 | Havline 容器 `network_mode: host` | `http://127.0.0.1:80`(宿主机 nginx) | 需要直接访问宿主机端口、或已有宿主机 nginx |
|
||||
| VPS Agent(可选) | VPS 上的 `havline-agent` 托管 | `http://127.0.0.1:<frps vhost 端口>` | 已有 VPS、只想让部分域名走 CF 的高级用户 |
|
||||
|
||||
部署注意事项:
|
||||
|
||||
- cloudflared 是**出站 only**,不要为它额外映射任何入站端口。
|
||||
- 二进制、`creds.json`、`cert.pem`、`config.yml`、日志都必须落在 `/data` 持久卷下,否则容器重建即丢失。
|
||||
- 如果要暴露宿主机或内网服务,推荐先把该服务接入 Fonu 的本机 nginx 反代,再让 CF 指向 nginx;不要在 CF ingress 里直接写宿主机 IP。
|
||||
- 如果要暴露宿主机或内网服务,推荐先把该服务接入 Havline 的本机 nginx 反代,再让 CF 指向 nginx;不要在 CF ingress 里直接写宿主机 IP。
|
||||
- host 网络模式只作为显式选项,不默认开启;开启后 `127.0.0.1` 的语义会变化,UI 必须提示。
|
||||
|
||||
### 12.3 凭据与模式:显式区分「谁在生效」
|
||||
@@ -335,21 +335,21 @@ Fonu 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx
|
||||
|
||||
| 模式 | 凭据 | 启动方式 | 规则来源 | 云端 fetch / push |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **A. token + 本地规则** | token | 解码 token → 写 `creds.json` → `tunnel --config config.yml run` | Fonu 生成的 `config.yml` | ❌(token 不含 API 凭据) |
|
||||
| **A. token + 本地规则** | token | 解码 token → 写 `creds.json` → `tunnel --config config.yml run` | Havline 生成的 `config.yml` | ❌(token 不含 API 凭据) |
|
||||
| **B. token + 云端规则** | token | `tunnel run --token <token>` | Cloudflare Dashboard / 云端 | ❌ |
|
||||
| **C. 账号 + 本地规则 + 云端同步** | `cert.pem` + credentials JSON | `tunnel --config config.yml run` | 本地,可与云端互相同步 | ✅ |
|
||||
|
||||
实施口径:
|
||||
|
||||
- 模式 A 绝不传 `--token`,只用 `--config`,否则本地规则不生效。
|
||||
- 模式 B 必须在 UI 明确写「规则由 Cloudflare 面板管理,Fonu 的本地预览不生效」,不要让用户以为 Fonu 生成的规则已经下发。
|
||||
- 模式 B 必须在 UI 明确写「规则由 Cloudflare 面板管理,Havline 的本地预览不生效」,不要让用户以为 Havline 生成的规则已经下发。
|
||||
- 模式 C 才提供「从云端拉取」「推送到云端」两个动作;token 模式不提供,只给 Dashboard 指引。
|
||||
- 所有凭据走 `secret.Box` 加密字段;预览一律掩码,下发时用原文。`creds.json` 权限 `0600`,`config.yml` 权限 `0640`,目录 `0750`。
|
||||
- 状态接口返回 `effective_side: local | cloud`,前端常驻显示,切换模式时要求二次确认。
|
||||
|
||||
### 12.4 数据模型与目录布局(Fonu 版)
|
||||
### 12.4 数据模型与目录布局(Havline 版)
|
||||
|
||||
参考实现用 JSON 文件 + 内存 HashMap,适合单进程小工具;Fonu 已有 SQLite 与迁移体系,v2 建议落成表:
|
||||
参考实现用 JSON 文件 + 内存 HashMap,适合单进程小工具;Havline 已有 SQLite 与迁移体系,v2 建议落成表:
|
||||
|
||||
`cf_tunnels`:
|
||||
|
||||
@@ -366,7 +366,7 @@ Fonu 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx
|
||||
| `network_json` | protocol / edge-ip-version / ha-connections / proxy_mode / originRequest |
|
||||
| `status` / `last_error` | `stopped` / `starting` / `connected` / `reconnecting` / `failed` |
|
||||
| `metrics_port` | 从日志发现的端口,仅运行时缓存 |
|
||||
| `auto_start` | Fonu 启动时是否自动拉起 |
|
||||
| `auto_start` | Havline 启动时是否自动拉起 |
|
||||
|
||||
目录布局:
|
||||
|
||||
@@ -391,7 +391,7 @@ Fonu 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx
|
||||
- 统一镜像表:`official` / `gh-proxy-org` / `gh-proxy-hk` / `gh-proxy-cdn` / `gh-proxy-edgeone` + 自定义 URL 模板;修正参考项目「镜像列表与实现不一致」的问题。
|
||||
- 下载校验:优先官方 checksums(若提供);否则至少校验文件大小、可执行性、`cloudflared --version`;镜像源与官方源使用同一份哈希。
|
||||
- 版本目录 + 原子替换 + 激活指针,复用 `internal/frp/runtime.go` 的 `readActiveBinary` / `writeActiveBinary` 思路。
|
||||
- 支持 linux amd64 / arm64、windows amd64、darwin amd64 / arm64;Fonu 侧用 `runtime.GOOS/GOARCH` 选择。
|
||||
- 支持 linux amd64 / arm64、windows amd64、darwin amd64 / arm64;Havline 侧用 `runtime.GOOS/GOARCH` 选择。
|
||||
|
||||
**进程**
|
||||
|
||||
@@ -414,7 +414,7 @@ Fonu 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx
|
||||
- `/ready` 返回 200:cloudflared 已就绪。
|
||||
- `cloudflared_tunnel_ha_connections > 0`:已连上边缘。
|
||||
- 延迟:QUIC 用 `quic_client_smoothed_rtt`;HTTP/2 用 `cloudflared_rpc_client_latency_secs` 或主动 HEAD 探测。
|
||||
- 带宽:QUIC 直接用 `quic_client_receive_bytes` / `quic_client_sent_bytes` 差值;HTTP/2 官方没有字节指标,Fonu 第一轮明确降级为「不展示带宽」并给出原因,不做参考项目那套进程 socket 统计。
|
||||
- 带宽:QUIC 直接用 `quic_client_receive_bytes` / `quic_client_sent_bytes` 差值;HTTP/2 官方没有字节指标,Havline 第一轮明确降级为「不展示带宽」并给出原因,不做参考项目那套进程 socket 统计。
|
||||
|
||||
---
|
||||
|
||||
@@ -423,7 +423,7 @@ Fonu 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx
|
||||
输入:
|
||||
|
||||
- 本机反代规则中启用且属于 HTTP / HTTPS 的域名;
|
||||
- Fonu nginx 的容器内入口端口(默认 `80`,可用配置覆盖);
|
||||
- Havline nginx 的容器内入口端口(默认 `80`,可用配置覆盖);
|
||||
- 用户勾选要暴露的域名集合。
|
||||
|
||||
输出:`config.yml` 的 `ingress` 数组。三条不变式必须写进生成器并用纯函数测试锁定:
|
||||
@@ -435,7 +435,7 @@ Fonu 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx
|
||||
与 nginx 的关系:
|
||||
|
||||
```text
|
||||
访客 → Cloudflare 边缘 → cloudflared(Fonu 容器)→ 127.0.0.1:80(Fonu nginx)→ 上游
|
||||
访客 → Cloudflare 边缘 → cloudflared(Havline 容器)→ 127.0.0.1:80(Havline nginx)→ 上游
|
||||
```
|
||||
|
||||
- CF 侧不重复配置限流、geo、TLS、Basic Auth;这些仍由本机 nginx 负责。
|
||||
@@ -444,7 +444,7 @@ Fonu 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx
|
||||
|
||||
### 12.7 网络调优(默认值与下发方式)
|
||||
|
||||
| 设置 | Fonu 默认 | 下发方式 | 说明 |
|
||||
| 设置 | Havline 默认 | 下发方式 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `transport_protocol` | `http2` | 环境变量 `TUNNEL_TRANSPORT_PROTOCOL` + `config.yml` 的 `protocol` | 国内 QUIC(UDP 7844)常被 QoS,TCP 更稳;提供 `auto` / `quic` |
|
||||
| `edge_ip_version` | `4` | 环境变量 `TUNNEL_EDGE_IP_VERSION` + `config.yml` 的 `edge-ip-version` | 2026.4.0 起官方默认改为 `auto`,国内 IPv6 路由差时反而变慢;提供 `auto` / `6` |
|
||||
@@ -477,9 +477,9 @@ Fonu 的运行时(`Dockerfile`)在同一个容器里装了主程序与 nginx
|
||||
|
||||
token 模式不提供 push / fetch,只给 Dashboard 指引;这是凭据能力的硬边界,不要用「同步失败」这类模糊提示掩盖。
|
||||
|
||||
### 12.9 与 Fonu 现有模块的集成点
|
||||
### 12.9 与 Havline 现有模块的集成点
|
||||
|
||||
| Fonu 模块 | 复用还是新增 | 具体落点 |
|
||||
| Havline 模块 | 复用还是新增 | 具体落点 |
|
||||
| --- | --- | --- |
|
||||
| 二进制下载 / 启用 | 新增 `internal/cloudflared`,参考 `internal/frp/runtime.go` | 镜像表、哈希校验、版本目录、激活指针 |
|
||||
| 进程托管 | 新增 `internal/cloudflared`,参考 `internal/frp/manager.go` | 启停、日志追加、退出原因、`tailFile` |
|
||||
@@ -532,7 +532,7 @@ token 模式不提供 push / fetch,只给 Dashboard 指引;这是凭据能
|
||||
- **容器网络**:默认拓扑是容器内 cloudflared → 容器内 nginx;不要假设 `127.0.0.1` 是宿主机。
|
||||
- **token 覆盖**:模式 B 的本地 `config.yml` 不生效;UI 必须明确,模式 A 才能本地管规则。
|
||||
- **HTTP/2 带宽**:官方 metrics 没有字节指标;第一轮明确降级并说明原因,不做进程 socket 统计。
|
||||
- **Windows**:`CREATE_NO_WINDOW` 必带;若做进程级字节统计还需要管理员权限,Fonu 第一轮不做。
|
||||
- **Windows**:`CREATE_NO_WINDOW` 必带;若做进程级字节统计还需要管理员权限,Havline 第一轮不做。
|
||||
- **CF 条款**:视频 / 大文件分发不在推荐用法内;TryCloudflare 不保证 SLA。
|
||||
- **国内链路**:CF 边缘延迟受运营商影响;优选只影响本机到边缘一段。
|
||||
- **API 凭据**:账号模式的 `cert.pem` 与可选 API Token 都是高权限凭据,必须最小化权限并加密存储。
|
||||
@@ -542,9 +542,9 @@ token 模式不提供 push / fetch,只给 Dashboard 指引;这是凭据能
|
||||
|
||||
### 12.13 结论
|
||||
|
||||
Cloudflare Tunnel 对 Fonu 是**第二条出口**,不是 frp 的替代:它补上「没有公网 IP、也不想长期租 VPS」的 HTTP 类场景。v2 的核心修订是四件事:
|
||||
Cloudflare Tunnel 对 Havline 是**第二条出口**,不是 frp 的替代:它补上「没有公网 IP、也不想长期租 VPS」的 HTTP 类场景。v2 的核心修订是四件事:
|
||||
|
||||
1. cloudflared 跑在 Fonu 容器内,ingress 统一指向容器内 nginx,绕开容器网络坑;
|
||||
1. cloudflared 跑在 Havline 容器内,ingress 统一指向容器内 nginx,绕开容器网络坑;
|
||||
2. 显式区分「本地规则生效」与「云端规则生效」,不让 token 模式悄悄覆盖本地配置;
|
||||
3. metrics 端口从日志发现,健康用 `/ready` + `ha_connections`,HTTP/2 带宽明确降级;
|
||||
4. 二进制下载补哈希 / 版本校验,ingress 生成器锁定三条不变式。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Fonu 容器部署与数据指南
|
||||
# Havline 容器部署与数据指南
|
||||
|
||||
> 适用版本:v1.2.19 及以后(Docker / 飞牛 fnOS 等 NAS 环境)
|
||||
> 相关文件:`docker-compose.yml`、`docker-compose.hub.yml`、`docker-compose.host.yml`、`Dockerfile`、`scripts/upgrade.sh`
|
||||
@@ -13,79 +13,79 @@
|
||||
| `docker-compose.yml` | 用**本地源码**构建(`context: .`) | 已经有源码目录(改代码后直接重建最方便) |
|
||||
| `docker-compose.host.yml` | 本地源码构建 + `network_mode: host` | 80/443 已被别的服务占用(如飞牛、群晖自带反代) |
|
||||
|
||||
三份都写了 `name: fonu`:**不要删掉这行**。不写它,改了目录名/项目名后 compose 会把它当成新项目,`fonu-data` 变成新的空卷——现象就是「数据全没了」。
|
||||
三份都写了 `name: havline`:**不要删掉这行**。不写它,改了目录名/项目名后 compose 会把它当成新项目,`havline-data` 变成新的空卷——现象就是「数据全没了」。
|
||||
|
||||
---
|
||||
|
||||
## 二、干净机器部署(推荐路径)
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/fonu && cd /opt/fonu
|
||||
mkdir -p /opt/havline && cd /opt/havline
|
||||
# 从仓库取这一份编排文件(用你仓库页面上的「原始文件」链接,或 git clone 后复制)
|
||||
# curl -fsSLo docker-compose.hub.yml <仓库里该文件的原始地址>
|
||||
docker compose up -d --build
|
||||
docker compose logs -f fonu # 首次启动会初始化数据库、生成会话密钥
|
||||
docker compose logs -f havline # 首次启动会初始化数据库、生成会话密钥
|
||||
```
|
||||
|
||||
首次访问 `http://<机器IP>:6893`,用日志里的初始口令登录。初始口令只在**首次启动**生成一次:
|
||||
|
||||
```bash
|
||||
docker compose logs fonu 2>&1 | grep -i password | tail -n 3
|
||||
docker compose logs havline 2>&1 | grep -i password | tail -n 3
|
||||
```
|
||||
|
||||
80/443 被占用的环境改用 host 那份:
|
||||
|
||||
```bash
|
||||
COMPOSE_FILE=docker-compose.host.yml docker compose up -d --build
|
||||
# 反代端口默认 18080/9443(可用 FONU_NGINX_HTTP_PORT / FONU_NGINX_HTTPS_PORT 覆盖)
|
||||
# 反代端口默认 18080/9443(可用 HAVLINE_NGINX_HTTP_PORT / HAVLINE_NGINX_HTTPS_PORT 覆盖)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、数据存在哪
|
||||
|
||||
数据**不在镜像里**,全部在数据目录中(容器内固定为 `/data`,由 `FONU_DATA_DIR` 指定):
|
||||
数据**不在镜像里**,全部在数据目录中(容器内固定为 `/data`,由 `HAVLINE_DATA_DIR` 指定):
|
||||
|
||||
| 路径 | 内容 |
|
||||
| --- | --- |
|
||||
| `fonu.db` | SQLite 主库:反代规则、穿透规则与服务端、DDNS、证书记录、设置、API Token、健康事件、指标历史 |
|
||||
| `havline.db` | SQLite 主库:反代规则、穿透规则与服务端、DDNS、证书记录、设置、API Token、健康事件、指标历史 |
|
||||
| `session_secret` | 会话与**凭据加密密钥**(v1.2.17 起未配置时自动生成,0600) |
|
||||
| `logs/` | `app.log`(应用日志)、`access.log` / `error.log`(本机 Nginx) |
|
||||
| `certs/` | 证书与私钥 |
|
||||
| `nginx/` | Fonu 生成的 Nginx 配置与 pid |
|
||||
| `nginx/` | Havline 生成的 Nginx 配置与 pid |
|
||||
| `frp/` | frpc 二进制、各服务端的 `frpc-server-N.toml` 与日志 |
|
||||
| `ssh_known_hosts` | SSH 主机密钥 TOFU 记录 |
|
||||
| `backups/` | 应用内「数据备份」导出的归档 |
|
||||
|
||||
### 3.1 默认:命名卷
|
||||
|
||||
默认挂在命名卷 `fonu-data`(compose 项目名是 `fonu`,所以完整卷名是 `fonu_fonu-data`),由 Docker 管理,位置随 Docker 数据根。
|
||||
默认挂在命名卷 `havline-data`(compose 项目名是 `havline`,所以完整卷名是 `havline_havline-data`),由 Docker 管理,位置随 Docker 数据根。
|
||||
|
||||
```bash
|
||||
docker volume ls | grep fonu
|
||||
docker volume inspect fonu_fonu-data --format '{{.Mountpoint}}'
|
||||
docker compose exec fonu ls -l /data # 容器内视角,最直观
|
||||
docker volume ls | grep havline
|
||||
docker volume inspect havline_havline-data --format '{{.Mountpoint}}'
|
||||
docker compose exec havline ls -l /data # 容器内视角,最直观
|
||||
```
|
||||
|
||||
飞牛这类 NAS 的 Docker 数据根常在 `/vol1/docker`,于是实际落在
|
||||
`/vol1/docker/volumes/fonu_fonu-data/_data`。**以 `docker volume inspect` 为准**,各家不同。
|
||||
`/vol1/docker/volumes/havline_havline-data/_data`。**以 `docker volume inspect` 为准**,各家不同。
|
||||
|
||||
### 3.2 指定宿主机目录(bind mount)
|
||||
|
||||
在同层 `.env` 里写一行即可,以 `/` 或 `.` 开头的路径会被 compose 当成 bind mount:
|
||||
|
||||
```bash
|
||||
echo 'FONU_DATA_PATH=/vol1/docker/fonu-data' >> .env
|
||||
echo 'HAVLINE_DATA_PATH=/vol1/docker/havline-data' >> .env
|
||||
```
|
||||
|
||||
从**已有命名卷**迁过去(迁移前先停服务):
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
sudo mkdir -p /vol1/docker/fonu-data
|
||||
docker run --rm -v fonu_fonu-data:/from -v /vol1/docker/fonu-data:/to alpine sh -c 'cp -av /from/. /to/'
|
||||
sudo mkdir -p /vol1/docker/havline-data
|
||||
docker run --rm -v havline_havline-data:/from -v /vol1/docker/havline-data:/to alpine sh -c 'cp -av /from/. /to/'
|
||||
docker compose up -d
|
||||
docker compose exec fonu ls -l /data # 确认数据还在
|
||||
docker compose exec havline ls -l /data # 确认数据还在
|
||||
```
|
||||
|
||||
反过来(bind → 卷)同理,把两边路径对调即可。**不要**只做一边的拷贝就改配置,否则会看到「空实例」。
|
||||
@@ -97,14 +97,14 @@ docker compose exec fonu ls -l /data # 确认数据还在
|
||||
**整目录备份**(推荐,含密钥与证书):
|
||||
|
||||
```bash
|
||||
docker run --rm -v fonu_fonu-data:/data -v "$PWD":/backup alpine \
|
||||
tar czf /backup/fonu-data-$(date +%F).tgz -C /data .
|
||||
docker run --rm -v havline_havline-data:/data -v "$PWD":/backup alpine \
|
||||
tar czf /backup/havline-data-$(date +%F).tgz -C /data .
|
||||
```
|
||||
|
||||
三个必须知道的点:
|
||||
|
||||
1. 千万别只拷 `fonu.db`:`session_secret` 是凭据的加密密钥,缺了它库里存的 frp token、通知密码都解不开;
|
||||
2. 不要在服务运行时手改 `fonu.db`(SQLite 单连接,容易被写坏)——要改先 `docker compose stop fonu`;
|
||||
1. 千万别只拷 `havline.db`:`session_secret` 是凭据的加密密钥,缺了它库里存的 frp token、通知密码都解不开;
|
||||
2. 不要在服务运行时手改 `havline.db`(SQLite 单连接,容易被写坏)——要改先 `docker compose stop havline`;
|
||||
3. 应用内「设置 → 数据」也有备份/恢复按钮,归档落在 `backups/`,恢复后需要重启容器。
|
||||
|
||||
---
|
||||
@@ -127,11 +127,11 @@ COMPOSE_FILE=docker-compose.host.yml sudo bash scripts/upgrade.sh # host 网
|
||||
### 5.2 手动
|
||||
|
||||
```bash
|
||||
cd /opt/fonu && docker compose pull 2>/dev/null; docker compose up -d --build
|
||||
docker images fonu # 看历史上留下哪些 tag,回滚时用
|
||||
cd /opt/havline && docker compose pull 2>/dev/null; docker compose up -d --build
|
||||
docker images havline # 看历史上留下哪些 tag,回滚时用
|
||||
```
|
||||
|
||||
镜像 tag 由 `FONU_VERSION` 决定(`scripts/upgrade.sh` 会从仓库 `VERSION` 自动取),不设时是 `fonu:local`。
|
||||
镜像 tag 由 `HAVLINE_VERSION` 决定(`scripts/upgrade.sh` 会从仓库 `VERSION` 自动取),不设时是 `havline:local`。
|
||||
|
||||
镜像是本地构建的,所以升级不需要 `docker pull`;上面那条 `up` 会带上 `--build` 重新构建。
|
||||
|
||||
@@ -140,16 +140,16 @@ docker images fonu # 看历史上留下哪些 tag,回滚时
|
||||
构建上下文是 Git 地址,所以**每次 `--build` 都会重新解析仓库分支**(日志里那句 `load git source ... refs/heads/main` 就是它),不需要 `git pull`:
|
||||
|
||||
```bash
|
||||
cd /opt/fonu
|
||||
cd /opt/havline
|
||||
docker compose up -d --build # 重新克隆 main → 构建 → 按新镜像重建容器
|
||||
```
|
||||
|
||||
建议**带上版本号**,给镜像留一个固定 tag(否则一直是 `fonu:local`,回滚时找不到旧版):
|
||||
建议**带上版本号**,给镜像留一个固定 tag(否则一直是 `havline:local`,回滚时找不到旧版):
|
||||
|
||||
```bash
|
||||
FONU_VERSION=1.2.19 docker compose up -d --build
|
||||
HAVLINE_VERSION=1.2.19 docker compose up -d --build
|
||||
# 回滚:镜像还在本地,不用重新构建
|
||||
FONU_VERSION=1.2.18 docker compose up -d
|
||||
HAVLINE_VERSION=1.2.18 docker compose up -d
|
||||
```
|
||||
|
||||
版本号从仓库的 `CHANGELOG.md` 或 `VERSION` 文件取;面板左侧栏底部也会显示当前运行的版本。
|
||||
@@ -159,7 +159,7 @@ FONU_VERSION=1.2.18 docker compose up -d
|
||||
### 5.4 升级前后的两件小事
|
||||
|
||||
- **升级前备份**(整目录,含密钥):见 §四那条 `tar` 命令;遇到异常可整个目录回滚。
|
||||
- **升级后确认**:`docker compose logs -f fonu` 看启动是否正常(会打印「会话密钥已就绪」),再刷新面板看版本与功能。
|
||||
- **升级后确认**:`docker compose logs -f havline` 看启动是否正常(会打印「会话密钥已就绪」),再刷新面板看版本与功能。
|
||||
|
||||
---
|
||||
|
||||
@@ -167,12 +167,12 @@ FONU_VERSION=1.2.18 docker compose up -d
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `FONU_DATA_DIR` | `/data` | 容器内数据目录,一般不用改;要换位置改**挂载** |
|
||||
| `FONU_DATA_PATH` | `fonu-data` | **宿主机侧**数据位置:命名卷名,或绝对路径(bind mount) |
|
||||
| `FONU_LISTEN` | `:6893` | 面板监听地址 |
|
||||
| `FONU_SESSION_SECRET` | 空 | 留空即自动生成并持久化;**它是凭据加密密钥,有数据后不要更换** |
|
||||
| `FONU_NGINX_HTTP_PORT` / `FONU_NGINX_HTTPS_PORT` | `80` / `443` | 反代监听端口(host 网络那份默认 18080/9443) |
|
||||
| `FONU_UPDATER_SOCKET` | `/run/fonu-updater/updater.sock` | 一键升级 sidecar 的 Unix Socket |
|
||||
| `HAVLINE_DATA_DIR` | `/data` | 容器内数据目录,一般不用改;要换位置改**挂载** |
|
||||
| `HAVLINE_DATA_PATH` | `havline-data` | **宿主机侧**数据位置:命名卷名,或绝对路径(bind mount) |
|
||||
| `HAVLINE_LISTEN` | `:6893` | 面板监听地址 |
|
||||
| `HAVLINE_SESSION_SECRET` | 空 | 留空即自动生成并持久化;**它是凭据加密密钥,有数据后不要更换** |
|
||||
| `HAVLINE_NGINX_HTTP_PORT` / `HAVLINE_NGINX_HTTPS_PORT` | `80` / `443` | 反代监听端口(host 网络那份默认 18080/9443) |
|
||||
| `HAVLINE_UPDATER_SOCKET` | `/run/havline-updater/updater.sock` | 一键升级 sidecar 的 Unix Socket |
|
||||
| `DOCKER_SOCK` | `/var/run/docker.sock` | 宿主机 Docker Socket;rootless Docker 或自定义路径时覆盖 |
|
||||
| `TZ` | `Asia/Shanghai` | 通知与日志显示用的时区 |
|
||||
| `NODE_IMAGE` / `GO_IMAGE` / `RUNTIME_IMAGE` / `UPDATER_IMAGE` | 国内镜像源 | 构建用的基础镜像,被加速器拦截时用来覆盖 |
|
||||
@@ -184,20 +184,20 @@ FONU_VERSION=1.2.18 docker compose up -d
|
||||
**构建报 `401 Unauthorized`(`resolve image config for docker-image://docker/dockerfile:1`)**
|
||||
国内加速器(如飞牛的 `docker.fnnas.com`)会拦下 BuildKit 的 frontend 镜像。Dockerfile 已不再显式指定 `# syntax=`,正常不会再遇到;若仍报错,改用加速器能拉的地址:`# syntax=docker.m.daocloud.io/docker/dockerfile:1`,或去掉四处 `--mount=type=cache`。
|
||||
|
||||
**`error while interpolating ... FONU_SESSION_SECRET is missing a value`**
|
||||
这是旧版 compose(把该变量写成必填)留下的行为。新版本是 `${FONU_SESSION_SECRET:-}`:留空即由应用自动生成。重新取一份编排文件即可。
|
||||
**`error while interpolating ... HAVLINE_SESSION_SECRET is missing a value`**
|
||||
这是旧版 compose(把该变量写成必填)留下的行为。新版本是 `${HAVLINE_SESSION_SECRET:-}`:留空即由应用自动生成。重新取一份编排文件即可。
|
||||
|
||||
**构建时 `fonu Pulling` 之后超时告警**
|
||||
**构建时 `havline Pulling` 之后超时告警**
|
||||
有 `build:` 段时 compose 仍会先尝试拉同名镜像;`.hub.yml` 已加 `pull_policy: build` 跳过。其余两份可忽略这条警告。
|
||||
|
||||
**换目录后「数据没了」**
|
||||
几乎没有真的丢:要么新目录是空的(迁移没做),要么项目名变了导致卷名变了。先用 `docker volume ls` 找到旧卷,按 §3.2 拷过去。
|
||||
|
||||
**面板能开但「今日请求/实时流量/性能」没数据**
|
||||
这三张卡读的是**本机 Nginx 访问日志**(`<数据目录>/logs/access.log`),只有在 Fonu 里配了本机反代规则并产生访问后才有数据;VPS 侧的流量在「公网反代」页与「日志中心 → 公网 Nginx」。
|
||||
这三张卡读的是**本机 Nginx 访问日志**(`<数据目录>/logs/access.log`),只有在 Havline 里配了本机反代规则并产生访问后才有数据;VPS 侧的流量在「公网反代」页与「日志中心 → 公网 Nginx」。
|
||||
|
||||
---
|
||||
|
||||
## 八、与原生安装的分工
|
||||
|
||||
不想用容器时可以用 `scripts/build-linux.ps1` 打出 `fonu-<版本>-linux-<arch>.tar.gz`,在目标机用 `sudo bash install.sh` 安装(装到 `/opt/fonu`,数据在 `/var/lib/fonu`,systemd 单元 `fonu.service`)。两者的数据目录结构完全一致,因此**可以互相迁移**:把一方的数据目录内容整个拷到另一方即可,注意带上 `session_secret`。
|
||||
不想用容器时可以用 `scripts/build-linux.ps1` 打出 `havline-<版本>-linux-<arch>.tar.gz`,在目标机用 `sudo bash install.sh` 安装(装到 `/opt/havline`,数据在 `/var/lib/havline`,systemd 单元 `havline.service`)。两者的数据目录结构完全一致,因此**可以互相迁移**:把一方的数据目录内容整个拷到另一方即可,注意带上 `session_secret`。
|
||||
|
||||
@@ -0,0 +1,832 @@
|
||||
# Havline 飞牛 fnOS 应用打包方案
|
||||
|
||||
> 日期:2026-10-01
|
||||
> 依据:飞牛应用开放平台文档:<https://developer.fnnas.com/docs/guide/>
|
||||
> 目标:将 Havline 从 Docker Compose / 裸机部署,改造为可安装、可升级、可卸载的飞牛 fnOS `.fpk` 应用。
|
||||
|
||||
---
|
||||
|
||||
## 一、官方文档结论
|
||||
|
||||
飞牛 fnOS 应用包的核心组成:
|
||||
|
||||
```text
|
||||
manifest
|
||||
config/privilege
|
||||
config/resource
|
||||
app/
|
||||
cmd/
|
||||
wizard/
|
||||
ICON.PNG
|
||||
ICON_256.PNG
|
||||
```
|
||||
|
||||
官方提供三种常见形态:
|
||||
|
||||
1. **CGI 应用**
|
||||
- 适合静态页面和轻量入口。
|
||||
- 不支持 WebSocket,不适合 Havline 这种常驻服务。
|
||||
2. **统一网关应用**
|
||||
- 复用 fnOS 访问域名和登录态。
|
||||
- 服务监听 Unix Socket,由 fnOS 网关转发。
|
||||
- 支持 WebSocket,但 Havline 需要新增 Unix Socket 监听能力。
|
||||
3. **Docker 应用**
|
||||
- 使用 `app/docker/docker-compose.yaml`。
|
||||
- 在 `config/resource` 中声明 `docker-project`。
|
||||
- 最适合 Havline 当前形态。
|
||||
|
||||
**首选方案:飞牛 Docker 应用模板。**
|
||||
|
||||
原因:
|
||||
|
||||
- Havline 已经是完整 Docker 应用。
|
||||
- 镜像内已包含 Nginx、Go 主程序和数据库运行环境。
|
||||
- Cloudflare Tunnel、FRP、Agent、证书管理都依赖常驻进程。
|
||||
- fnOS Docker 项目资源可以托管启动、停止和运行状态。
|
||||
- 避免把 Havline 的 Go、Nginx、SQLite 和 shell 依赖重新拆进原生应用生命周期。
|
||||
|
||||
---
|
||||
|
||||
## 二、当前 Havline 的部署形态
|
||||
|
||||
### 2.1 当前主要组成
|
||||
|
||||
```text
|
||||
havline
|
||||
├── Go 主程序
|
||||
├── 内嵌 Web 前端
|
||||
├── 内置 Nginx
|
||||
├── SQLite
|
||||
├── FRP 客户端 / Agent 管理
|
||||
├── Cloudflare Tunnel / cloudflared
|
||||
├── ACME / DDNS / 通知
|
||||
└── 可选 havline-updater sidecar
|
||||
```
|
||||
|
||||
### 2.2 当前关键目录
|
||||
|
||||
```text
|
||||
/data
|
||||
├── havline.db
|
||||
├── session_secret
|
||||
├── nginx/
|
||||
├── certs/
|
||||
├── logs/
|
||||
├── frp/
|
||||
└── cloudflared/
|
||||
```
|
||||
|
||||
### 2.3 当前端口
|
||||
|
||||
| 端口 | 用途 |
|
||||
| --- | --- |
|
||||
| `6893` | Havline 管理界面/API |
|
||||
| `80` | 本机 Nginx HTTP |
|
||||
| `443` | 本机 Nginx HTTPS |
|
||||
|
||||
飞牛 fnOS 通常已占用 `80/443`,因此 fnOS 包应默认使用:
|
||||
|
||||
```text
|
||||
管理端口:6893
|
||||
Nginx HTTP:18080
|
||||
Nginx HTTPS:9443
|
||||
```
|
||||
|
||||
端口应允许安装向导修改。
|
||||
|
||||
### 2.4 当前 updater 的限制
|
||||
|
||||
Havline 当前的一键升级依赖:
|
||||
|
||||
- Docker Socket
|
||||
- updater sidecar
|
||||
- 宿主机 Compose 项目目录
|
||||
- 对宿主 Docker 执行 `compose build/up` 的权限
|
||||
|
||||
在 fnOS 应用包内继续保留这条链路会带来:
|
||||
|
||||
- 容器内 path mapping 复杂;
|
||||
- fnOS 应用目录路径和宿主路径不一致;
|
||||
- 应用中心升级与 Havline 内部升级冲突;
|
||||
- 需要给应用额外 Docker Socket 权限。
|
||||
|
||||
**方案:fnOS 首发版本默认关闭 Havline 内置 updater,统一由飞牛应用中心升级。**
|
||||
|
||||
后续如果需要,可做“高级模式”单独启用。
|
||||
|
||||
---
|
||||
|
||||
## 三、目标包结构
|
||||
|
||||
建议在仓库新增:
|
||||
|
||||
```text
|
||||
fnos/
|
||||
├── manifest
|
||||
├── ICON.PNG
|
||||
├── ICON_256.PNG
|
||||
├── config/
|
||||
│ ├── privilege
|
||||
│ └── resource
|
||||
├── app/
|
||||
│ ├── docker/
|
||||
│ │ └── docker-compose.yaml
|
||||
│ └── ui/
|
||||
│ ├── config
|
||||
│ └── images/
|
||||
│ ├── icon_64.png
|
||||
│ └── icon_256.png
|
||||
├── cmd/
|
||||
│ ├── main
|
||||
│ ├── install_init
|
||||
│ ├── install_callback
|
||||
│ ├── upgrade_init
|
||||
│ ├── upgrade_callback
|
||||
│ ├── uninstall_init
|
||||
│ ├── uninstall_callback
|
||||
│ ├── config_init
|
||||
│ └── config_callback
|
||||
└── wizard/
|
||||
├── install
|
||||
├── upgrade
|
||||
├── config
|
||||
└── uninstall
|
||||
```
|
||||
|
||||
构建脚本负责:
|
||||
|
||||
1. 复制 `fnos/` 模板到临时目录。
|
||||
2. 用 `VERSION` 替换 `manifest.version`。
|
||||
3. 生成对应的 `docker-compose.yaml`。
|
||||
4. 调用 `fnpack build`。
|
||||
5. 输出 `havline-<version>.fpk`。
|
||||
|
||||
---
|
||||
|
||||
## 四、Manifest 草案
|
||||
|
||||
```ini
|
||||
appname=havline
|
||||
version=1.9.0
|
||||
display_name=Havline
|
||||
desc=NAS 公网访问管理:反向代理、FRP、Cloudflare Tunnel、DDNS 与证书
|
||||
source=thirdparty
|
||||
platform=all
|
||||
maintainer=Havline
|
||||
maintainer_url=https://example.com
|
||||
os_min_version=1.0.0
|
||||
desktop_uidir=ui
|
||||
desktop_applaunchname=havline.main
|
||||
service_port=6893
|
||||
checkport=true
|
||||
ctl_stop=true
|
||||
changelog=本次更新
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
- `appname=havline`:应用唯一标识。
|
||||
- `platform=all`:包内不含架构二进制,实际架构由 multi-arch 镜像决定。
|
||||
- `service_port=6893`:桌面入口默认访问端口。
|
||||
- `ctl_stop=true`:应用中心显示启动、停止和运行状态。
|
||||
- `desktop_applaunchname=havline.main`:应用卡片默认入口。
|
||||
|
||||
### 多架构要求
|
||||
|
||||
发布时必须提供:
|
||||
|
||||
```text
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
```
|
||||
|
||||
否则 `platform=all` 只在镜像本身支持目标架构时成立。
|
||||
|
||||
---
|
||||
|
||||
## 五、权限与资源
|
||||
|
||||
### 5.1 `config/privilege`
|
||||
|
||||
```json
|
||||
{
|
||||
"defaults": {
|
||||
"run-as": "package"
|
||||
},
|
||||
"username": "havline",
|
||||
"groupname": "havline"
|
||||
}
|
||||
```
|
||||
|
||||
原则:
|
||||
|
||||
- 长期运行的主程序使用包用户。
|
||||
- 不默认给 Root。
|
||||
- 不默认挂载 Docker Socket。
|
||||
- 不默认加入 `docker` 用户组。
|
||||
|
||||
如果未来需要启用内部 updater,应单独评估 Docker Socket 的权限边界。
|
||||
|
||||
### 5.2 `config/resource`
|
||||
|
||||
```json
|
||||
{
|
||||
"docker-project": {
|
||||
"projects": [
|
||||
{
|
||||
"name": "havline",
|
||||
"path": "docker"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可选共享目录:
|
||||
|
||||
```json
|
||||
{
|
||||
"data-share": {
|
||||
"shares": [
|
||||
{
|
||||
"name": "havline/backups"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
建议首发版本只暴露备份目录。
|
||||
|
||||
不要暴露:
|
||||
|
||||
- `havline.db`
|
||||
- `session_secret`
|
||||
- `certs/`
|
||||
- `nginx/`
|
||||
- `cloudflared/`
|
||||
- `frp/`
|
||||
|
||||
这些内容包含访问控制、证书和加密凭据。
|
||||
|
||||
---
|
||||
|
||||
## 六、Docker Compose 草案
|
||||
|
||||
建议新建:
|
||||
|
||||
```text
|
||||
fnos/app/docker/docker-compose.yaml
|
||||
```
|
||||
|
||||
内容:
|
||||
|
||||
```yaml
|
||||
name: havline
|
||||
|
||||
services:
|
||||
havline:
|
||||
image: ghcr.io/example/havline:${TRIM_APPVER}
|
||||
container_name: havline
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${TRIM_SERVICE_PORT}:6893"
|
||||
- "${wizard_nginx_http_port}:18080"
|
||||
- "${wizard_nginx_https_port}:9443"
|
||||
volumes:
|
||||
- "${TRIM_PKGVAR}/data:/data"
|
||||
environment:
|
||||
HAVLINE_DATA_DIR: /data
|
||||
HAVLINE_LISTEN: ":6893"
|
||||
HAVLINE_NGINX_HTTP_PORT: "${wizard_nginx_http_port}"
|
||||
HAVLINE_NGINX_HTTPS_PORT: "${wizard_nginx_https_port}"
|
||||
HAVLINE_SESSION_SECRET: ""
|
||||
TZ: "${wizard_timezone}"
|
||||
HAVLINE_UPDATER_SOCKET: ""
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `TRIM_SERVICE_PORT` 默认映射管理界面。
|
||||
- 使用 bridge 网络,避免与 fnOS 现有 host 网络服务直接冲突。
|
||||
- Nginx 使用 18080/9443,避免占用 fnOS 的 80/443。
|
||||
- 数据全部落在 `TRIM_PKGVAR/data`。
|
||||
- 不挂载 Docker Socket。
|
||||
- 不启动 updater sidecar。
|
||||
|
||||
### 为什么首发不直接用 host 网络
|
||||
|
||||
host 网络确实更贴近 Havline 裸机模式,但:
|
||||
|
||||
- fnOS 本身和其他应用可能占用大量端口;
|
||||
- 应用中心无法明确管理端口冲突;
|
||||
- bridge + 端口映射更容易解释和回滚。
|
||||
|
||||
首发建议 bridge。
|
||||
|
||||
后续可提供“高级安装模式:host 网络”。
|
||||
|
||||
---
|
||||
|
||||
## 七、桌面入口
|
||||
|
||||
`app/ui/config`:
|
||||
|
||||
```json
|
||||
{
|
||||
".url": {
|
||||
"havline.main": {
|
||||
"title": "Havline",
|
||||
"icon": "images/icon_{0}.png",
|
||||
"type": "iframe",
|
||||
"protocol": "http",
|
||||
"port": "${TRIM_SERVICE_PORT}",
|
||||
"url": "/",
|
||||
"allUsers": false,
|
||||
"control": {
|
||||
"accessPerm": "readonly"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
考虑:
|
||||
|
||||
- Havline 当前是单管理员产品,默认只让管理员可见。
|
||||
- 使用端口服务,不依赖 fnOS 网关。
|
||||
- 后续若新增统一网关,可把入口改成 `gatewayPrefix/gatewaySocket`,无需端口。
|
||||
|
||||
---
|
||||
|
||||
## 八、生命周期脚本
|
||||
|
||||
### `cmd/main`
|
||||
|
||||
Docker 项目的启停由 fnOS Docker 资源处理,`main` 主要提供状态检查。
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
CONTAINER_NAME="havline"
|
||||
|
||||
is_running() {
|
||||
docker inspect "$CONTAINER_NAME" 2>/dev/null | grep -q '"Status": "running"'
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start|stop)
|
||||
exit 0
|
||||
;;
|
||||
status)
|
||||
if is_running; then
|
||||
exit 0
|
||||
fi
|
||||
exit 3
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $1" > "$TRIM_TEMP_LOGFILE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
### `cmd/install_callback`
|
||||
|
||||
职责:
|
||||
|
||||
- 创建 `TRIM_PKGVAR/data`
|
||||
- 修正目录权限
|
||||
- 如果用户选择了迁移,执行数据复制
|
||||
- 输出安装摘要
|
||||
|
||||
### `cmd/upgrade_callback`
|
||||
|
||||
职责:
|
||||
|
||||
- 检查数据库可读写
|
||||
- 保留已有的 `session_secret`
|
||||
- 记录升级前后版本
|
||||
- 可选:导入旧版本迁移文件
|
||||
|
||||
### `cmd/config_callback`
|
||||
|
||||
职责:
|
||||
|
||||
- 配置端口或时区变化时,验证新端口
|
||||
- 如果只是环境变量变化,尽量交给 Docker Compose 重建
|
||||
- 输出清晰的失败原因到 `TRIM_TEMP_LOGFILE`
|
||||
|
||||
### `cmd/uninstall_callback`
|
||||
|
||||
默认行为:
|
||||
|
||||
- 删除容器和程序
|
||||
- 保留应用数据
|
||||
|
||||
只有用户在卸载向导中明确选择“删除全部数据”时,才删除:
|
||||
|
||||
```text
|
||||
${TRIM_PKGVAR}/data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、安装向导
|
||||
|
||||
### `wizard/install`
|
||||
|
||||
建议字段:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"stepTitle": "Havline 访问端口",
|
||||
"items": [
|
||||
{
|
||||
"type": "text",
|
||||
"field": "wizard_nginx_http_port",
|
||||
"label": "Nginx HTTP 端口",
|
||||
"initValue": "18080",
|
||||
"rules": [
|
||||
{
|
||||
"required": true,
|
||||
"message": "请输入端口"
|
||||
},
|
||||
{
|
||||
"pattern": "^[0-9]+$",
|
||||
"message": "端口必须是数字"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"field": "wizard_nginx_https_port",
|
||||
"label": "Nginx HTTPS 端口",
|
||||
"initValue": "9443",
|
||||
"rules": [
|
||||
{
|
||||
"required": true,
|
||||
"message": "请输入端口"
|
||||
},
|
||||
{
|
||||
"pattern": "^[0-9]+$",
|
||||
"message": "端口必须是数字"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"stepTitle": "基础设置",
|
||||
"items": [
|
||||
{
|
||||
"type": "select",
|
||||
"field": "wizard_timezone",
|
||||
"label": "时区",
|
||||
"initValue": "Asia/Shanghai",
|
||||
"options": [
|
||||
{
|
||||
"label": "Asia/Shanghai",
|
||||
"value": "Asia/Shanghai"
|
||||
},
|
||||
{
|
||||
"label": "UTC",
|
||||
"value": "UTC"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 管理员初始化
|
||||
|
||||
当前 Havline 首次启动会自动生成随机管理员密码并写入日志。
|
||||
|
||||
对于 fnOS 应用,建议改造为:
|
||||
|
||||
1. 安装向导收集初始管理员密码;或
|
||||
2. 首次打开 Havline 时进入创建管理员页面;或
|
||||
3. 读取 `HAVLINE_INITIAL_ADMIN_PASSWORD` 进行一次性初始化。
|
||||
|
||||
**不建议继续把管理员密码写入日志。**
|
||||
|
||||
---
|
||||
|
||||
## 十、环境变量映射
|
||||
|
||||
| fnOS 变量 | 映射到 Havline | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `TRIM_APPVER` | 镜像 tag | 应用版本 |
|
||||
| `TRIM_SERVICE_PORT` | 6893 映射 | 桌面入口 |
|
||||
| `TRIM_PKGVAR` | `/data` | 持久化数据 |
|
||||
| `TRIM_APPDEST` | 应用 target | 只读程序目录 |
|
||||
| `wizard_nginx_http_port` | `HAVLINE_NGINX_HTTP_PORT` | Nginx HTTP |
|
||||
| `wizard_nginx_https_port` | `HAVLINE_NGINX_HTTPS_PORT` | Nginx HTTPS |
|
||||
| `wizard_timezone` | `TZ` | 时区 |
|
||||
|
||||
Havline 数据目录内应继续保留:
|
||||
|
||||
```text
|
||||
${TRIM_PKGVAR}/data/session_secret
|
||||
```
|
||||
|
||||
不要每次安装或升级重新生成。
|
||||
|
||||
---
|
||||
|
||||
## 十一、更新策略
|
||||
|
||||
### 首发方案
|
||||
|
||||
统一使用飞牛应用中心升级:
|
||||
|
||||
- 更新 `.fpk`
|
||||
- 拉取新 multi-arch 镜像
|
||||
- 保留 `${TRIM_PKGVAR}/data`
|
||||
- 重启 Docker Compose 项目
|
||||
|
||||
### 禁用 Havline 自带 updater
|
||||
|
||||
fnOS 包内:
|
||||
|
||||
```text
|
||||
HAVLINE_UPDATER_SOCKET=""
|
||||
```
|
||||
|
||||
并且 compose 不启动:
|
||||
|
||||
```text
|
||||
havline-updater
|
||||
```
|
||||
|
||||
原因:
|
||||
|
||||
- App Center 已经是唯一更新入口。
|
||||
- 避免 Docker Socket 权限。
|
||||
- 避免应用目录映射错误。
|
||||
- 避免升级 Recursive / 自更新冲突。
|
||||
|
||||
### 后续可选
|
||||
|
||||
如果确实需要应用内更新,再增加“高级更新模式”:
|
||||
|
||||
- 显式让用户开启;
|
||||
- 显式挂载 Docker Socket;
|
||||
- 显式配置宿主项目路径;
|
||||
- 失败时回滚到上一版镜像。
|
||||
|
||||
---
|
||||
|
||||
## 十二、数据迁移方案
|
||||
|
||||
### 从 Docker Compose 迁移
|
||||
|
||||
旧环境可能有:
|
||||
|
||||
```text
|
||||
/opt/havline/data
|
||||
```
|
||||
|
||||
或 Docker 命名卷:
|
||||
|
||||
```text
|
||||
havline-data
|
||||
```
|
||||
|
||||
迁移步骤:
|
||||
|
||||
1. 停止旧 Havline。
|
||||
2. 备份旧数据目录。
|
||||
3. 将旧目录复制到:
|
||||
|
||||
```text
|
||||
${TRIM_PKGVAR}/data
|
||||
```
|
||||
|
||||
4. 确认 `session_secret` 和 `havline.db` 都存在。
|
||||
5. 启动 fnOS 应用。
|
||||
6. 登录并验证:
|
||||
- 用户与设置
|
||||
- 反向代理规则
|
||||
- FRP 服务端
|
||||
- Cloudflare 隧道
|
||||
- 证书
|
||||
- 备份列表
|
||||
|
||||
### 从裸机迁移
|
||||
|
||||
裸机默认数据目录通常是:
|
||||
|
||||
```text
|
||||
./.data
|
||||
```
|
||||
|
||||
迁移内容一致,重点是保持 `session_secret` 不变。
|
||||
|
||||
否则:
|
||||
|
||||
- 已加密的 API Token
|
||||
- DDNS 凭据
|
||||
- 通知凭据
|
||||
- Cloudflare 凭据
|
||||
|
||||
可能无法解密。
|
||||
|
||||
---
|
||||
|
||||
## 十三、安全设计
|
||||
|
||||
### 默认安全基线
|
||||
|
||||
- `run-as=package`
|
||||
- 不启用 Root
|
||||
- 不挂载 Docker Socket
|
||||
- 不暴露证书、数据库和加密密钥目录
|
||||
- 数据目录仅应用用户可读写
|
||||
- 仅暴露管理端口、Nginx 高位的 HTTP/HTTPS 端口
|
||||
- 管理员入口默认只对 fnOS 管理员可见
|
||||
|
||||
### 首次管理员密码
|
||||
|
||||
必须改造当前“随机密码写日志”行为。
|
||||
|
||||
推荐优先级:
|
||||
|
||||
1. 首次进入创建管理员页面
|
||||
2. 安装向导收集密码
|
||||
3. 环境变量一次性初始化
|
||||
|
||||
### Cloudflare Token
|
||||
|
||||
仍由 Havline 独立加密保存:
|
||||
|
||||
```text
|
||||
${TRIM_PKGVAR}/data/session_secret
|
||||
```
|
||||
|
||||
不放入 fnOS 共享目录。
|
||||
|
||||
---
|
||||
|
||||
## 十四、构建与发布流程
|
||||
|
||||
### 1. 构建 multi-arch 镜像
|
||||
|
||||
```bash
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--build-arg VERSION_FILE=VERSION \
|
||||
-t registry.example.com/havline:${VERSION} \
|
||||
--push .
|
||||
```
|
||||
|
||||
### 2. 生成 fnOS 项目
|
||||
|
||||
从以下模板生成:
|
||||
|
||||
```text
|
||||
fnos/
|
||||
```
|
||||
|
||||
替换:
|
||||
|
||||
- `manifest.version`
|
||||
- Compose 镜像 tag
|
||||
- 应用说明
|
||||
- `changelog`
|
||||
|
||||
### 3. 打包
|
||||
|
||||
```bash
|
||||
fnpack build --directory dist/fnos/havline
|
||||
```
|
||||
|
||||
### 4. 测试设备安装
|
||||
|
||||
```bash
|
||||
appcenter-cli install-fpk havline-${VERSION}.fpk --env config.env
|
||||
```
|
||||
|
||||
### 5. 发布前验证
|
||||
|
||||
在至少一台 x86 和一台 ARM 测试设备验证:
|
||||
|
||||
- 安装
|
||||
- 启动
|
||||
- 桌面入口
|
||||
- 端口映射
|
||||
- 数据持久化
|
||||
- 应用中心升级
|
||||
- 卸载保留数据
|
||||
- 卸载删除数据
|
||||
|
||||
---
|
||||
|
||||
## 十五、分阶段实施
|
||||
|
||||
### 阶段 1:可用包
|
||||
|
||||
- 新增 `fnos/` 目录
|
||||
- 编写 manifest
|
||||
- Docker Compose 模板
|
||||
- 安装/卸载/状态生命周期
|
||||
- 管理入口
|
||||
- 端口向导
|
||||
- 使用固定镜像 tag
|
||||
|
||||
目标:可以在飞牛测试设备安装并打开 Havline。
|
||||
|
||||
### 阶段 2:数据与升级
|
||||
|
||||
- `TRIM_PKGVAR` 持久化
|
||||
- 旧 Docker Compose 数据迁移脚本
|
||||
- 应用中心升级验证
|
||||
- 保留/删除数据选项
|
||||
- `session_secret` 兼容检查
|
||||
|
||||
目标:数据不丢,可稳定升级。
|
||||
|
||||
### 阶段 3:fnOS 体验
|
||||
|
||||
- 首次管理员初始化页面
|
||||
- 端口冲突检测
|
||||
- 应用入口图标完善
|
||||
- 备份目录共享
|
||||
- 时区和系统信息适配
|
||||
|
||||
目标:符合飞牛应用中心体验。
|
||||
|
||||
### 阶段 4:高级集成
|
||||
|
||||
- 可选统一网关
|
||||
- 可选 Docker Socket updater
|
||||
- fnOS 开放 API 集成
|
||||
- 应用中心审核材料
|
||||
- ARM 实机完整验证
|
||||
|
||||
---
|
||||
|
||||
## 十六、风险与处理
|
||||
|
||||
| 风险 | 影响 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| `80/443` 被 fnOS 占用 | Havline Nginx 无法启动 | 默认改用 18080/9443,向导可改 |
|
||||
| 镜像只有 amd64 | ARM 设备无法运行 | CI 构建 amd64 + arm64 |
|
||||
| fnOS 应用升级与内置 updater 冲突 | 版本回退或容器失控 | 首发禁用内置 updater |
|
||||
| `session_secret` 丢失 | 加密凭据无法解密 | 持久化到 `TRIM_PKGVAR`,迁移时必须保留 |
|
||||
| Docker Socket 权限过大 | 安全风险 | 默认不挂载 |
|
||||
| 数据目录权限错误 | 数据库无法写入 | `install_callback` 修正权限 |
|
||||
| 管理员密码写日志 | 凭据泄露 | 安装向导或首次设置页 |
|
||||
| 端口冲突 | 应用无法启动 | 向导配置,安装时检测 |
|
||||
| ARM 镜像无 cloudflared | Cloudflare 隧道不可用 | 镜像内校验架构,或运行时下载对应 binary |
|
||||
|
||||
---
|
||||
|
||||
## 十七、官方文档参考
|
||||
|
||||
- 欢迎加入:<https://developer.fnnas.com/docs/guide/>
|
||||
- 快速开始:
|
||||
- <https://developer.fnnas.com/docs/quick-started/prerequisites/>
|
||||
- <https://developer.fnnas.com/docs/quick-started/create-application/>
|
||||
- <https://developer.fnnas.com/docs/quick-started/test-application/>
|
||||
- <https://developer.fnnas.com/docs/quick-started/publish-application/>
|
||||
- 核心概念:
|
||||
- <https://developer.fnnas.com/docs/core-concepts/manifest/>
|
||||
- <https://developer.fnnas.com/docs/core-concepts/privilege/>
|
||||
- <https://developer.fnnas.com/docs/core-concepts/resource/>
|
||||
- <https://developer.fnnas.com/docs/core-concepts/runtime/>
|
||||
- <https://developer.fnnas.com/docs/core-concepts/environment-variables/>
|
||||
- <https://developer.fnnas.com/docs/core-concepts/app-entry/>
|
||||
- <https://developer.fnnas.com/docs/core-concepts/gateway-registration/>
|
||||
- <https://developer.fnnas.com/docs/core-concepts/wizard/>
|
||||
- 工具:
|
||||
- <https://developer.fnnas.com/docs/cli/fnpack/>
|
||||
- <https://developer.fnnas.com/docs/cli/appcentercli/>
|
||||
- Docker 案例:
|
||||
- <https://developer.fnnas.com/docs/examples/docker/>
|
||||
|
||||
---
|
||||
|
||||
## 十八、结论
|
||||
|
||||
Havline 适合做飞牛 Docker 应用,不适合首发做成 CGI 或原生应用。
|
||||
|
||||
首发目标:
|
||||
|
||||
```text
|
||||
飞牛应用中心安装
|
||||
→ Docker Compose 启动 Havline
|
||||
→ 桌面入口打开管理界面
|
||||
→ 数据持久化到 TRIM_PKGVAR
|
||||
→ 应用中心升级
|
||||
```
|
||||
|
||||
最需要先解决的代码问题:
|
||||
|
||||
1. 管理员初始化不再写日志。
|
||||
2. 默认 updater 改为可关闭,fnOS 包中关闭。
|
||||
3. 增加 multi-arch 镜像发布。
|
||||
4. 增加 `fnos/` 打包模板和构建脚本。
|
||||
5. 验证 `TRIM_PKGVAR/data` 数据迁移。
|
||||
@@ -83,7 +83,7 @@ PageHeader「公网服务端」(原「公网 Agent」)
|
||||
│ 操作列:启动·停止 · 查看 · 日志 · 测速 · 校验 · 编辑 · 删除
|
||||
│ (去掉「Agent 管理」——它就在本页;行点击即选中)
|
||||
├─ 选中服务端的详情(现有 AgentView 分区原样搬过来)
|
||||
│ 环境与依赖(Fonu Agent / Nginx / frps 运行时 / VPS 资源 / VPS 证书)
|
||||
│ 环境与依赖(Havline Agent / Nginx / frps 运行时 / VPS 资源 / VPS 证书)
|
||||
│ frps 控制 + Agent 生命周期(SSH)
|
||||
│ 输出与日志
|
||||
└─ 弹窗:服务端表单(新增/编辑)、服务端日志、服务端详情、frps 同源配置、导出、FRP 应用配置
|
||||
@@ -147,7 +147,7 @@ PageHeader「公网服务端」(原「公网 Agent」)
|
||||
- 服务端表单里的「打开管理面板」改为「查看服务端详情」(关弹窗 + 页内选中);详情弹窗「客户端」一行所需的 `runtime` 由新增的 `loadRuntimeVersion()` 拉取。
|
||||
- 详情弹窗「代理」表依赖的 `proxyPublicEndpoint` 在 `AgentView` 本地补了一份(原属规则侧);表单里的「下发 frps 配置」复用 `AgentView` 既有的 `run('push-config', …)` 版本,输出进「输出与日志」。
|
||||
- **未迁移**:`/frp` 的「查看脱敏配置」弹窗(`showExport`)留在原页——触发按钮在「穿透规则」卡片头部,导出物是脱敏 frpc.toml,属客户端 / 规则视角。这是按 §4.3 自己的判据(弹窗依赖服务端还是规则)做的取舍,与 §4.3 表格里「导出脱敏配置 → 迁走」一行不同。
|
||||
- 验证:`vue-tsc --noEmit` 0 错误、`vite build` 通过、`FrpView.vue` 内旧标识符 grep 为 0、全仓无 0 字节源文件、`bin\fonu.exe` 已重建。
|
||||
- 验证:`vue-tsc --noEmit` 0 错误、`vite build` 通过、`FrpView.vue` 内旧标识符 grep 为 0、全仓无 0 字节源文件、`bin\havline.exe` 已重建。
|
||||
- **踩坑记录(scoped 样式不随模板搬迁)**:`.frp-proxy-modal` 系列规则在 `FrpView.vue` 里分散在多处——1250–1260 的弹窗外壳、1263–1282 的说明栏 / 高级折叠 / 服务端选项区。只搬前一段时,说明栏失去 `width: 300px; flex-shrink: 0`,在 `display: flex` 的弹窗里按内容宽度膨胀,把 `flex: 1` 的表单列挤成一条,表现为标题重叠、页签一字一行。**步 4 清理时注意**:`.proxy-modal*` / `.frp-proxy-modal*` 在 `/frp` 仍被规则表单弹窗使用,不能删。
|
||||
- **布局调整(2026-09-24,用户要求对齐 DDNS 页)**:`/frp-agent` 改为 DDNS 那样的**左列表 + 右详情**——`.agent-layout { grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) }`,≤1100px 收成一列;侧栏结构照抄 DDNS 的 `.ddns-layout` / `.task-sidebar`(类名换成本页语义的 `.server-sidebar*` / `.server-item*`)。
|
||||
- 原 8 列 `n-data-table` 换成紧凑列表项:名称 + 启动状态标签 + `主机:端口` + 延迟 / 位置 + TLS / Token / Agent / SSH 标签;行点击即选中(`--active` 高亮)。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> 基于 frp v0.71.0 官方 `frps_full_example.toml` 全量对照。
|
||||
> 状态图例:✅ 已实现 🔶 部分实现(有缺口或需手动步骤) ⬜ 未实现 🚫 有意不实现(有说明理由)
|
||||
>
|
||||
> 「Fonu 入口」= 用户在 Fonu 界面操作的位置;「生成器」= `internal/frp/frps_template.go` 的 frps.toml 同源生成器。
|
||||
> 「Havline 入口」= 用户在 Havline 界面操作的位置;「生成器」= `internal/frp/frps_template.go` 的 frps.toml 同源生成器。
|
||||
> 更新日期:2026-09-21
|
||||
|
||||
---
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
| 配置项 | 官方默认 | 状态 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `webServer.addr / port` | `127.0.0.1:0` | ✅ | 表单「frps 管理接口」地址/端口;Fonu 借此拉流量;「frps 管理」按钮直达面板 |
|
||||
| `webServer.addr / port` | `127.0.0.1:0` | ✅ | 表单「frps 管理接口」地址/端口;Havline 借此拉流量;「frps 管理」按钮直达面板 |
|
||||
| `webServer.user / password` | — | ✅ | 表单同源;密码加密存储(ConfiguredSecretField 掩码闭环) |
|
||||
| `webServer.tls.certFile / keyFile` | — | ⬜ | dashboard 自身 HTTPS 未提供 |
|
||||
|
||||
@@ -87,10 +87,10 @@
|
||||
|
||||
| 配置项 | 官方默认 | 状态 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 多用户 `user` 前缀隔离 | — | ⬜ | 未提供(frps 端 `user` 是 frpc 登录名前缀隔离,与 Fonu 的服务端 user 概念不同) |
|
||||
| 多用户 `user` 前缀隔离 | — | ⬜ | 未提供(frps 端 `user` 是 frpc 登录名前缀隔离,与 Havline 的服务端 user 概念不同) |
|
||||
| `udpPacketSize` | 1500 | ⬜ | 未提供(需与 frpc 同源,frpc 侧也无此表单项) |
|
||||
|
||||
## 十、Fonu Agent 侧的 frps 运维能力(与配置生成配套)
|
||||
## 十、Havline Agent 侧的 frps 运维能力(与配置生成配套)
|
||||
|
||||
| 功能 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
@@ -116,6 +116,6 @@
|
||||
|
||||
## 附:架构口径
|
||||
|
||||
- **frps.toml 是生成器产物,不是用户手写文件**:所有已实现项均以 Fonu 服务端表单为唯一事实源,VPS 侧禁止手改(改了会被下次「下发 frps 配置」覆盖);
|
||||
- **frps.toml 是生成器产物,不是用户手写文件**:所有已实现项均以 Havline 服务端表单为唯一事实源,VPS 侧禁止手改(改了会被下次「下发 frps 配置」覆盖);
|
||||
- **端口同源链**:表单 `vhost_http_port/vhost_https_port` → frps.toml `vhostHTTPPort/vhostHTTPSPort` → Nginx `proxy_pass 127.0.0.1:<port>`,三方由 `agentUpstreamFor`/生成器共用同一值;
|
||||
- **agent 与 frps 解耦**:agent 只管 Nginx 片段/证书/frps 生命周期,frps 进程由 systemd 托管;二者通过 Fonu 下发配置保持一致。
|
||||
- **agent 与 frps 解耦**:agent 只管 Nginx 片段/证书/frps 生命周期,frps 进程由 systemd 托管;二者通过 Havline 下发配置保持一致。
|
||||
|
||||
+52
-52
@@ -1,19 +1,19 @@
|
||||
# FRPS 服务端管理方案(fonu-agent,部署于公网服务器)
|
||||
# FRPS 服务端管理方案(havline-agent,部署于公网服务器)
|
||||
|
||||
> 需求:frpc 端(Fonu/NAS)提交穿透规则后,**公网服务器端自动完成反向代理设置**,实现域名无端口访问(`http://tv.36800.cc`),并自动管理服务器侧证书与 frps。
|
||||
> 实现形态:在公网 VPS 上部署一个常驻管理组件 **fonu-agent**(单个 Go 二进制 + systemd),对外提供管理 API;Fonu 在规则变化时调用该 API,agent 在 VPS 本地完成 Nginx 反代配置、frps 管理与证书部署。
|
||||
> **安装与管理通道**:agent 的**首次安装、升级、卸载通过 SSH 由 Fonu 远程完成**(用户在 Fonu 界面填 SSH 信息点按钮即可,无需手工登 VPS);装好后 agent 以 **API** 接受日常管理。管理面板全部在本地 Fonu,VPS 上没有任何面板。
|
||||
> 需求:frpc 端(Havline/NAS)提交穿透规则后,**公网服务器端自动完成反向代理设置**,实现域名无端口访问(`http://tv.36800.cc`),并自动管理服务器侧证书与 frps。
|
||||
> 实现形态:在公网 VPS 上部署一个常驻管理组件 **havline-agent**(单个 Go 二进制 + systemd),对外提供管理 API;Havline 在规则变化时调用该 API,agent 在 VPS 本地完成 Nginx 反代配置、frps 管理与证书部署。
|
||||
> **安装与管理通道**:agent 的**首次安装、升级、卸载通过 SSH 由 Havline 远程完成**(用户在 Havline 界面填 SSH 信息点按钮即可,无需手工登 VPS);装好后 agent 以 **API** 接受日常管理。管理面板全部在本地 Havline,VPS 上没有任何面板。
|
||||
> 修订记录:v2 混合形态(SSH 装/卸 + API 管理);v1 为纯 SSH 推送方案,v2 初稿为「agent 纯 API」方案,两版历史见文末对照。
|
||||
|
||||
## 1. 目标与效果
|
||||
|
||||
| 诉求 | 达成方式 |
|
||||
| --- | --- |
|
||||
| frpc 提交规则后 frps 端自动反代 | Fonu 保存规则 → 调 agent API 注册域名路由 → agent 写 Nginx vhost 并 reload |
|
||||
| frpc 提交规则后 frps 端自动反代 | Havline 保存规则 → 调 agent API 注册域名路由 → agent 写 Nginx vhost 并 reload |
|
||||
| 无端口访问 | agent 的 Nginx 监听 80(443),`proxy_pass` 到 frps vhost 端口,URL 不带端口号 |
|
||||
| 管理证书 | Fonu 证书页(DNS-01)签发后一键推送到 agent,自动挂 443 并续期重推 |
|
||||
| 管理证书 | Havline 证书页(DNS-01)签发后一键推送到 agent,自动挂 443 并续期重推 |
|
||||
| frps 管理 | 配置下发(vhost 端口等)、进程重启/状态/版本、端口占用诊断,全部 API 化 |
|
||||
| VPS 上已有 Nginx/站点 | 共存:agent 只写自己的 vhost 文件(`conf.d/fonu-*.conf`),不动其他配置 |
|
||||
| VPS 上已有 Nginx/站点 | 共存:agent 只写自己的 vhost 文件(`conf.d/havline-*.conf`),不动其他配置 |
|
||||
|
||||
最终链路(http 规则为例):
|
||||
|
||||
@@ -26,46 +26,46 @@
|
||||
## 2. 总体架构
|
||||
|
||||
```text
|
||||
Fonu (NAS,管理面板所在地) 公网服务器 (VPS)
|
||||
Havline (NAS,管理面板所在地) 公网服务器 (VPS)
|
||||
┌──────────────────────────────┐ ┌────────────────────────────────┐
|
||||
│ FRP 模块(现有) │ │ frps :7000 控制 / 8080 vhost │
|
||||
│ 规则 CRUD / frpc / frps.toml │ │ nginx :80/443 │
|
||||
│ 证书页(lego DNS-01,已有) │ │ └─ fonu-*.conf(agent 产物) │
|
||||
│ ┌────────────────────┐ │ │ fonu-agent :7700(常驻) │
|
||||
│ 证书页(lego DNS-01,已有) │ │ └─ havline-*.conf(agent 产物) │
|
||||
│ ┌────────────────────┐ │ │ havline-agent :7700(常驻) │
|
||||
│ │ SSH 安装器(新增) │──SSH──┼→ 首次安装/升级/卸载 agent │
|
||||
│ │ agent 客户端(新增) │──API──┼→ 日常管理:路由/配置/证书/状态 │
|
||||
│ └────────────────────┘ │ │ ├─ 路由管理(nginx vhost 编排) │
|
||||
│ 管理面板 = Fonu 网页 │ │ ├─ frps 管理(配置/进程/诊断) │
|
||||
│ 管理面板 = Havline 网页 │ │ ├─ frps 管理(配置/进程/诊断) │
|
||||
│ (规则页/证书页/管理弹窗) │ │ └─ 证书接收与挂载 │
|
||||
└──────────────────────────────┘ └────────────────────────────────┘
|
||||
```
|
||||
|
||||
两条通道各司其职:
|
||||
|
||||
- **SSH(低频)**:仅用于 agent 生命周期——安装、升级、卸载、agent 失联自救。用户在 Fonu 填一次 SSH 信息(可选保存备用);
|
||||
- **API(高频)**:日常全部管理——规则同步、frps 配置下发、证书推送、状态查询。面板 100% 在 Fonu 本地,VPS 上没有任何管理界面。
|
||||
- **SSH(低频)**:仅用于 agent 生命周期——安装、升级、卸载、agent 失联自救。用户在 Havline 填一次 SSH 信息(可选保存备用);
|
||||
- **API(高频)**:日常全部管理——规则同步、frps 配置下发、证书推送、状态查询。面板 100% 在 Havline 本地,VPS 上没有任何管理界面。
|
||||
|
||||
职责划分:Fonu 是**控制面**(用户在哪配置就存哪),agent 是**执行面**(只接收指令并落实),frps/Nginx 是**数据面**。agent 无状态(配置即文件),可随时重装。
|
||||
职责划分:Havline 是**控制面**(用户在哪配置就存哪),agent 是**执行面**(只接收指令并落实),frps/Nginx 是**数据面**。agent 无状态(配置即文件),可随时重装。
|
||||
|
||||
## 3. fonu-agent 设计(VPS 侧)
|
||||
## 3. havline-agent 设计(VPS 侧)
|
||||
|
||||
单个 Go 二进制(复用主项目代码编译,`cmd/fonu-agent`),systemd 常驻,管理 API 监听 `:7700`(可配置)。
|
||||
单个 Go 二进制(复用主项目代码编译,`cmd/havline-agent`),systemd 常驻,管理 API 监听 `:7700`(可配置)。
|
||||
|
||||
### 3.1 管理 API(Bearer Token 鉴权)
|
||||
|
||||
| 方法/路径 | 作用 |
|
||||
| --- | --- |
|
||||
| `GET /api/v1/ping` | 连通性/鉴权校验(Fonu「测试连接」按钮) |
|
||||
| `GET /api/v1/ping` | 连通性/鉴权校验(Havline「测试连接」按钮) |
|
||||
| `GET /api/v1/status` | frps 版本/运行方式/配置路径、80/443/7000/8080 端口占用者、已注册路由、证书有效期 |
|
||||
| `PUT /api/v1/routes` | 注册/更新域名路由:`{domain, upstream(=frps vhost 端口), tls, cert_id}` → 写 Nginx vhost → `nginx -t` → reload(失败自动回滚文件) |
|
||||
| `DELETE /api/v1/routes/{domain}` | 注销路由(规则删除/停用时调用) |
|
||||
| `GET /api/v1/routes` | 当前路由清单(对账用) |
|
||||
| `PUT /api/v1/frps/config` | 下发完整 frps.toml(Fonu 同源生成)→ 备份旧文件 → 按运行方式重启/重载 → 回读验证 |
|
||||
| `POST /api/v1/certs` | 部署证书:`{domain, fullchain_pem, key_pem}` → 写 `/var/lib/fonu-agent/certs/<domain>/`(0600)→ 更新 443 vhost → reload |
|
||||
| `PUT /api/v1/frps/config` | 下发完整 frps.toml(Havline 同源生成)→ 备份旧文件 → 按运行方式重启/重载 → 回读验证 |
|
||||
| `POST /api/v1/certs` | 部署证书:`{domain, fullchain_pem, key_pem}` → 写 `/var/lib/havline-agent/certs/<domain>/`(0600)→ 更新 443 vhost → reload |
|
||||
|
||||
### 3.2 Nginx 编排(反代核心)
|
||||
|
||||
- 每个域名一个文件:`/etc/nginx/conf.d/fonu-tv.36800.cc.conf`,模板:
|
||||
- 每个域名一个文件:`/etc/nginx/conf.d/havline-tv.36800.cc.conf`,模板:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
@@ -82,34 +82,34 @@ server {
|
||||
# 启用证书后追加 443 server 块(ssl_certificate 指向 agent 证书目录)+ 80 跳转
|
||||
```
|
||||
|
||||
- 写入流程:写 `.tmp` → `nginx -t` 校验 → 通过则替换并 `nginx -s reload`,失败保留原文件并把 `nginx -t` 输出回传给 Fonu 显示;
|
||||
- **只增删自己的 `fonu-*.conf`**,绝不触碰其他配置 → 与宝塔/手工站点共存;
|
||||
- 写入流程:写 `.tmp` → `nginx -t` 校验 → 通过则替换并 `nginx -s reload`,失败保留原文件并把 `nginx -t` 输出回传给 Havline 显示;
|
||||
- **只增删自己的 `havline-*.conf`**,绝不触碰其他配置 → 与宝塔/手工站点共存;
|
||||
- 首次运行自检:nginx 是否安装、`conf.d`/`sites-enabled` 目录探测、80/443 监听者识别(nginx? frps? 其他?)。
|
||||
|
||||
### 3.3 frps 管理
|
||||
|
||||
- **配置下发**:Fonu「frps.toml 同源生成」已有(含 vhost 端口、auth、webServer),改为推送给 agent 落盘:备份旧文件 → 原子替换 → 按运行方式生效(systemd 单元 `systemctl restart frps`;nohup 场景 `pkill frps` 后按原命令拉起,agent 从状态接口学习原启动命令);
|
||||
- **配置下发**:Havline「frps.toml 同源生成」已有(含 vhost 端口、auth、webServer),改为推送给 agent 落盘:备份旧文件 → 原子替换 → 按运行方式生效(systemd 单元 `systemctl restart frps`;nohup 场景 `pkill frps` 后按原命令拉起,agent 从状态接口学习原启动命令);
|
||||
- **诊断**:`status` 聚合 frps 版本/存活/配置路径 + `ss -tlnp` 端口占用判定——把之前手工排查(`port unavailable`、80 被占)变成 API 一次调用;
|
||||
- **边界**:不安装/升级 frps 二进制(检测报告提示版本即可)。
|
||||
|
||||
### 3.4 证书
|
||||
|
||||
- 签发仍在 **Fonu 证书页**(lego DNS-01,复用 DDNS 凭证,NAS 侧完成,VPS 无需参与);
|
||||
- 签发仍在 **Havline 证书页**(lego DNS-01,复用 DDNS 凭证,NAS 侧完成,VPS 无需参与);
|
||||
- 部署:证书页「部署到服务器」→ `POST /certs` 推送 PEM → agent 落盘并生成 443 vhost → reload;
|
||||
- 续期:Fonu 定时任务续期成功后自动重推,VPS 侧无感;
|
||||
- 续期:Havline 定时任务续期成功后自动重推,VPS 侧无感;
|
||||
- (可选演进)agent 本地 ACME HTTP-01:agent 已在 80 上,可自助签发,作为无 DNS 凭证场景的备选,本期不做。
|
||||
|
||||
## 4. 通信协议与安全
|
||||
|
||||
| 项 | 设计 |
|
||||
| --- | --- |
|
||||
| 鉴权 | Bearer Token(Fonu 服务端表单生成/粘贴,agent 配置文件持有;两端一致即可用),`7700` 建议用防火墙限制仅 NAS IP 访问 |
|
||||
| 鉴权 | Bearer Token(Havline 服务端表单生成/粘贴,agent 配置文件持有;两端一致即可用),`7700` 建议用防火墙限制仅 NAS IP 访问 |
|
||||
| 传输 | 明文 HTTP 起步可接受(Token + 内网限制);增强路径:agent 用 frps 证书或自签证书提供 HTTPS 管理口 |
|
||||
| 命令安全 | agent 只执行固定动作(写文件/nginx -t/reload/重启 frps),**不执行 Fonu 下发的任意命令**;域名/路径参数做白名单校验(域名格式、路径锁定 `conf.d/fonu-*` 与 `/var/lib/fonu-agent/`) |
|
||||
| 幂等 | 路由/配置/证书接口全部幂等,重复调用无副作用;`routes` 对账接口支持 Fonu 定时校准 |
|
||||
| 审计 | agent 每个写操作记本地日志(who/when/what),Fonu 侧同步状态入系统日志 |
|
||||
| 命令安全 | agent 只执行固定动作(写文件/nginx -t/reload/重启 frps),**不执行 Havline 下发的任意命令**;域名/路径参数做白名单校验(域名格式、路径锁定 `conf.d/havline-*` 与 `/var/lib/havline-agent/`) |
|
||||
| 幂等 | 路由/配置/证书接口全部幂等,重复调用无副作用;`routes` 对账接口支持 Havline 定时校准 |
|
||||
| 审计 | agent 每个写操作记本地日志(who/when/what),Havline 侧同步状态入系统日志 |
|
||||
|
||||
## 5. Fonu 侧改动(frpc 端)
|
||||
## 5. Havline 侧改动(frpc 端)
|
||||
|
||||
| 模块 | 内容 |
|
||||
| --- | --- |
|
||||
@@ -120,15 +120,15 @@ server {
|
||||
| 管理弹窗 | 服务端「管理」按钮:status 报告(frps 状态/端口占用者/路由清单/证书有效期)+ 一键操作(下发配置/全量对账) |
|
||||
| API 客户端 | `internal/frp/agent_client.go`:Go HTTP 客户端封装上述 7 个接口(超时/重试/错误透传) |
|
||||
|
||||
## 6. agent 部署与生命周期管理(SSH,全程在 Fonu 界面完成)
|
||||
## 6. agent 部署与生命周期管理(SSH,全程在 Havline 界面完成)
|
||||
|
||||
### 6.1 首次安装(用户不登 VPS)
|
||||
|
||||
1. Fonu 服务端表单「远程管理」分组填 SSH 信息(地址/端口/用户/密码或私钥,可选保存)+ 点「安装 agent」;
|
||||
2. Fonu 经单次 SSH 会话自动完成:
|
||||
1. Havline 服务端表单「远程管理」分组填 SSH 信息(地址/端口/用户/密码或私钥,可选保存)+ 点「安装 agent」;
|
||||
2. Havline 经单次 SSH 会话自动完成:
|
||||
- 探测环境:`uname -m`(选二进制)、nginx 是否安装、`systemd` 可用性、端口占用快照;
|
||||
- SFTP 上传 `fonu-agent` 二进制与 `install.sh`(从 Fonu 自带资源解出对应架构);
|
||||
- 远程执行 `install.sh`:落位 `/usr/local/bin/fonu-agent`、写 `/etc/fonu-agent/agent.toml`(**生成随机 Token 并回传给 Fonu**,无需用户抄写)、注册 `systemd` 单元并启动;
|
||||
- SFTP 上传 `havline-agent` 二进制与 `install.sh`(从 Havline 自带资源解出对应架构);
|
||||
- 远程执行 `install.sh`:落位 `/usr/local/bin/havline-agent`、写 `/etc/havline-agent/agent.toml`(**生成随机 Token 并回传给 Havline**,无需用户抄写)、注册 `systemd` 单元并启动;
|
||||
- 安装后立刻走 API `ping` 验证 + 防火墙提示(如 7700 未放行给出命令);
|
||||
3. UI 展示每步输出,失败可重试(幂等,重复安装=覆盖升级)。
|
||||
|
||||
@@ -136,21 +136,21 @@ server {
|
||||
|
||||
| 动作 | 方式 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 升级 agent | SSH:上传新二进制 + `systemctl restart fonu-agent` | 路由/证书/Token 均在磁盘,不丢 |
|
||||
| 升级 agent | SSH:上传新二进制 + `systemctl restart havline-agent` | 路由/证书/Token 均在磁盘,不丢 |
|
||||
| 卸载 agent | SSH:`install.sh --uninstall`(停服务、可选保留数据) | nginx vhost 文件一并清理(可选项) |
|
||||
| agent 失联 | API 不可达时,Fonu 管理弹窗提示「走 SSH 通道诊断」:进程状态/端口/日志尾部 | SSH 是 API 故障时的备用通道 |
|
||||
| agent 失联 | API 不可达时,Havline 管理弹窗提示「走 SSH 通道诊断」:进程状态/端口/日志尾部 | SSH 是 API 故障时的备用通道 |
|
||||
|
||||
### 6.3 日常管理(API,面板在 Fonu)
|
||||
### 6.3 日常管理(API,面板在 Havline)
|
||||
|
||||
安装完成后的一切操作不再用 SSH:规则联动同步、frps.toml 下发、证书部署、status 查询、全量对账——全部是 Fonu 界面按钮/自动触发,走 agent API。VPS 上没有任何管理面板,用户日常甚至不需要记住 VPS 密码。
|
||||
安装完成后的一切操作不再用 SSH:规则联动同步、frps.toml 下发、证书部署、status 查询、全量对账——全部是 Havline 界面按钮/自动触发,走 agent API。VPS 上没有任何管理面板,用户日常甚至不需要记住 VPS 密码。
|
||||
|
||||
## 6.5 Fonu 侧新增「SSH 安装器」(与 agent 客户端并列)
|
||||
## 6.5 Havline 侧新增「SSH 安装器」(与 agent 客户端并列)
|
||||
|
||||
| 模块 | 内容 |
|
||||
| --- | --- |
|
||||
| `internal/frp/ssh_installer.go`(新) | SSH 连接(x/crypto/ssh 已在 go.mod)、SFTP 上传二进制/install.sh、远程幂等安装流程、Token 回传 |
|
||||
| agent 二进制内嵌 | 构建期把 `fonu-agent` 各架构二进制 embed 进 Fonu(构建矩阵:linux-amd64/arm64),安装时按 `uname -m` 选择,无需用户单独下载 |
|
||||
| 数据模型 | 复用 023 迁移:`ssh_host/port/user/auth/secret_enc` 可选保存(仅启用「安装/升级/卸载」功能时需要),agent API Token 由安装器自动生成并写入 Fonu |
|
||||
| agent 二进制内嵌 | 构建期把 `havline-agent` 各架构二进制 embed 进 Havline(构建矩阵:linux-amd64/arm64),安装时按 `uname -m` 选择,无需用户单独下载 |
|
||||
| 数据模型 | 复用 023 迁移:`ssh_host/port/user/auth/secret_enc` 可选保存(仅启用「安装/升级/卸载」功能时需要),agent API Token 由安装器自动生成并写入 Havline |
|
||||
| UI | 服务端表单「远程管理」:SSH 信息(可选)+「安装 agent / 升级 / 卸载 / 走 SSH 诊断」按钮,安装过程实时输出展示 |
|
||||
|
||||
> SSH 信息**可选保存**:仅安装/升级时需要;日常管理走 API Token。安全上 SSH 凭据同 token 机制(secret.Box 加密、不回显),远程命令固定白名单模板(上传/安装/卸载三种),无注入面。
|
||||
@@ -159,22 +159,22 @@ server {
|
||||
|
||||
| 模块 | 位置 | 内容 | 量级 |
|
||||
| --- | --- | --- | --- |
|
||||
| agent 主体 | `cmd/fonu-agent/` + `internal/agent/`(新) | HTTP 服务、鉴权、路由/frps/证书三个执行器、nginx 模板与 reload、状态聚合 | 大(约 1200 行) |
|
||||
| Fonu SSH 安装器 | `internal/frp/ssh_installer.go`(新) | SSH/SFTP 上传、远程幂等安装/升级/卸载、agent 二进制 embed、Token 自动回传 | 中 |
|
||||
| Fonu agent 客户端 | `internal/frp/agent_client.go`(新) | 7 个接口封装 | 中 |
|
||||
| agent 主体 | `cmd/havline-agent/` + `internal/agent/`(新) | HTTP 服务、鉴权、路由/frps/证书三个执行器、nginx 模板与 reload、状态聚合 | 大(约 1200 行) |
|
||||
| Havline SSH 安装器 | `internal/frp/ssh_installer.go`(新) | SSH/SFTP 上传、远程幂等安装/升级/卸载、agent 二进制 embed、Token 自动回传 | 中 |
|
||||
| Havline agent 客户端 | `internal/frp/agent_client.go`(新) | 7 个接口封装 | 中 |
|
||||
| 数据模型/迁移 | `023_frp_server_agent.sql` | SSH + agent 字段 + 规则同步状态 | 小 |
|
||||
| 规则联动 | `internal/frp/service.go` | 保存/删除/启停后同步调用 + 状态落库 | 中 |
|
||||
| API/UI | `frpmulti_handler.go`、`router.go`、`FrpView.vue`、`client.ts` | 5 个端点 + 表单分组 + 管理弹窗 + 同步状态列 | 中 |
|
||||
| 测试 | `agent_test.go` 等 | nginx 模板渲染/回滚、接口幂等、鉴权 | 中 |
|
||||
|
||||
总计约 2500–3000 行(含测试)。`mgmt_enabled=0` 时 Fonu 行为与 v1.1.0 完全一致;agent 未部署的服务端不受任何影响。
|
||||
总计约 2500–3000 行(含测试)。`mgmt_enabled=0` 时 Havline 行为与 v1.1.0 完全一致;agent 未部署的服务端不受任何影响。
|
||||
|
||||
## 8. 里程碑
|
||||
|
||||
| 阶段 | 内容 | 验收 |
|
||||
| --- | --- | --- |
|
||||
| M1 | agent 主体(ping/status/routes)+ Fonu SSH 安装器(一键安装) | Fonu 填 SSH 信息点「安装」后自动装好 agent 并回连成功,status 能报端口占用者 |
|
||||
| M2 | Fonu 规则联动 + 路由自动注册 + frps 配置下发 | 保存 http 规则后 `http://域名` 无端口直达;frps.toml 一键下发生效 |
|
||||
| M1 | agent 主体(ping/status/routes)+ Havline SSH 安装器(一键安装) | Havline 填 SSH 信息点「安装」后自动装好 agent 并回连成功,status 能报端口占用者 |
|
||||
| M2 | Havline 规则联动 + 路由自动注册 + frps 配置下发 | 保存 http 规则后 `http://域名` 无端口直达;frps.toml 一键下发生效 |
|
||||
| M3 | 证书推送 + 续期联动 + 443 路由 | `https://域名` 可用,续期自动重推 |
|
||||
| M4 | 管理弹窗 UI + 全量对账 + 升级/卸载/SSH 诊断 + 手册更新 | 手册第 6 节新增方案 C(agent 模式:SSH 装、API 管) |
|
||||
|
||||
@@ -189,11 +189,11 @@ server {
|
||||
|
||||
| | v2 最终(agent + SSH 安装器) | v1(纯 SSH 推送) |
|
||||
| --- | --- | --- |
|
||||
| VPS 侧 | 常驻 agent,但**安装/升级由 Fonu SSH 一键完成** | 零安装 |
|
||||
| 日常管理 | API(面板全在 Fonu,日常无需 SSH) | 每次操作都走 SSH 命令 |
|
||||
| VPS 侧 | 常驻 agent,但**安装/升级由 Havline SSH 一键完成** | 零安装 |
|
||||
| 日常管理 | API(面板全在 Havline,日常无需 SSH) | 每次操作都走 SSH 命令 |
|
||||
| 能力上限 | 高:本地编排 nginx/frps/证书,可自愈可扩展 | 受限于 SSH 命令白名单 |
|
||||
| 与已有站点共存 | 只写 `fonu-*.conf`,共存 | 可实现但每步都要远程拼配置 |
|
||||
| 与已有站点共存 | 只写 `havline-*.conf`,共存 | 可实现但每步都要远程拼配置 |
|
||||
| 故障定位 | status API 一次拿全;agent 失联时 SSH 急救 | 每次探测拼 SSH 命令 |
|
||||
| 适用 | VPS 长期做穿透入口、多域名/HTTPS 全自动 | 不想在 VPS 装常驻组件 |
|
||||
|
||||
结论:**SSH 与 API 不是二选一**——SSH 只承担 agent 生命周期(装/升/卸/急救),API 承担日常管理;管理面板 100% 在 Fonu 本地,VPS 上没有任何管理界面。
|
||||
结论:**SSH 与 API 不是二选一**——SSH 只承担 agent 生命周期(装/升/卸/急救),API 承担日常管理;管理面板 100% 在 Havline 本地,VPS 上没有任何管理界面。
|
||||
|
||||
+17
-17
@@ -1,6 +1,6 @@
|
||||
# FRPS 服务端自动管理方案(frps-manager)
|
||||
|
||||
> 目标:frpc 端在 Fonu 提交穿透规则后,frps 端**自动完成反向代理配置与生效**,实现域名无端口访问(`http://tv.36800.cc`),并自动管理 VPS 侧证书。本文是基于现有项目(Fonu v1.1.0 FRP 多服务端模块)的实施方案,不引入新服务、不改 frp 二进制。
|
||||
> 目标:frpc 端在 Havline 提交穿透规则后,frps 端**自动完成反向代理配置与生效**,实现域名无端口访问(`http://tv.36800.cc`),并自动管理 VPS 侧证书。本文是基于现有项目(Havline v1.1.0 FRP 多服务端模块)的实施方案,不引入新服务、不改 frp 二进制。
|
||||
|
||||
## 1. 现状与问题
|
||||
|
||||
@@ -10,28 +10,28 @@
|
||||
| --- | --- | --- |
|
||||
| tcp 规则注册(远程端口) | ✅ 自动(frpc 登录时向 frps 动态注册) | 无 |
|
||||
| http 规则域名路由 | ⚠️ 需 frps.toml 预先配置 `vhostHTTPPort` | 首次使用 http 类型时 VPS 要手工改配置+重启 |
|
||||
| frps.toml 生成 | ✅ Fonu 一键生成,用户手动复制到 VPS | 每次变更都要 SSH 手工同步 |
|
||||
| frps.toml 生成 | ✅ Havline 一键生成,用户手动复制到 VPS | 每次变更都要 SSH 手工同步 |
|
||||
| VPS 端口释放(80/443 被占) | ❌ 手动 SSH 停 Nginx | `port unavailable` 最常见根因 |
|
||||
| 改配置后生效 | ❌ 手动 `pkill frps` 重启(frp 无热加载) | 容易忘、容易误 |
|
||||
| VPS 侧证书 | ❌ 无 | HTTPS 要在 VPS 手工装证书 |
|
||||
| 配置漂移检测 | ❌ 无 | VPS 上的文件与 Fonu 数据库可能不一致 |
|
||||
| 配置漂移检测 | ❌ 无 | VPS 上的文件与 Havline 数据库可能不一致 |
|
||||
|
||||
## 2. 总体设计
|
||||
|
||||
在 Fonu(frpc 所在机器)新增 **frps-manager** 子系统:Fonu 通过 **SSH** 连接 VPS,执行「检测 → 生成 → 推送 → 生效 → 验证」闭环。VPS 侧**零安装**(只要求 frps + SSH)。
|
||||
在 Havline(frpc 所在机器)新增 **frps-manager** 子系统:Havline 通过 **SSH** 连接 VPS,执行「检测 → 生成 → 推送 → 生效 → 验证」闭环。VPS 侧**零安装**(只要求 frps + SSH)。
|
||||
|
||||
```text
|
||||
Fonu (NAS) VPS (38.95.74.87)
|
||||
Havline (NAS) VPS (38.95.74.87)
|
||||
┌─────────────────────────────┐ ┌──────────────────────────┐
|
||||
│ FRP 模块(现有) │ │ frps (7000/80/443/7500) │
|
||||
│ 规则/服务端 CRUD │ │ ↑ frpc 动态注册端口 │
|
||||
│ frps-manager(新增) │ SSH │ frps.toml (推送产物) │
|
||||
│ 检测→生成→推送→reload→验证 │ ────────→ │ /etc/fonu/frps/ │
|
||||
│ 证书申请→推送→挂载 │ │ /etc/fonu/frps/certs/ │
|
||||
│ 检测→生成→推送→reload→验证 │ ────────→ │ /etc/havline/frps/ │
|
||||
│ 证书申请→推送→挂载 │ │ /etc/havline/frps/certs/ │
|
||||
└─────────────────────────────┘ └──────────────────────────┘
|
||||
```
|
||||
|
||||
核心思路:**frps 不认识"反代",它只认 frps.toml**。因此"自动反代"= ① 规则变化时重算出应然的 frps.toml(含 vhost 端口、subDomainHost、TLS 配置)→ ② 与 VPS 现状 diff → ③ 无变化则跳过,有变化则原子推送 → ④ 通过 frps **admin API 热重载**(无需重启进程)→ ⑤ 回读验证。证书同理:Fonu 证书页现有 ACME 能力签好后把证书文件推到 VPS 并写入 frps.toml 的 `transport.tls`/`webServer.tls` 段。
|
||||
核心思路:**frps 不认识"反代",它只认 frps.toml**。因此"自动反代"= ① 规则变化时重算出应然的 frps.toml(含 vhost 端口、subDomainHost、TLS 配置)→ ② 与 VPS 现状 diff → ③ 无变化则跳过,有变化则原子推送 → ④ 通过 frps **admin API 热重载**(无需重启进程)→ ⑤ 回读验证。证书同理:Havline 证书页现有 ACME 能力签好后把证书文件推到 VPS 并写入 frps.toml 的 `transport.tls`/`webServer.tls` 段。
|
||||
|
||||
## 3. 服务端模型扩展(frp_servers 表)
|
||||
|
||||
@@ -44,7 +44,7 @@ Fonu (NAS) VPS (38.95.74.87)
|
||||
| `ssh_user` | TEXT | 建议 root 或具备 sudo 的用户 |
|
||||
| `ssh_auth` | TEXT | `password` / `key` |
|
||||
| `ssh_secret_enc` | TEXT | 密码或私钥(加密) |
|
||||
| `frps_dir` | TEXT | VPS 上 frps 目录,默认 `/etc/fonu/frps` |
|
||||
| `frps_dir` | TEXT | VPS 上 frps 目录,默认 `/etc/havline/frps` |
|
||||
| `frps_bin` | TEXT | frps 二进制路径,默认 `/usr/local/bin/frps` |
|
||||
| `mgmt_enabled` | INTEGER | 是否启用自动管理(0/1,默认 0=纯手动,向后兼容) |
|
||||
|
||||
@@ -64,7 +64,7 @@ cat /proc/$(pidof frps)/cmdline # 实际使用的配置路径
|
||||
cat <配置路径> # 现有 frps.toml 内容(做 diff)
|
||||
```
|
||||
|
||||
返回结构化报告:frps 版本/运行状态/配置路径、80/443 占用者判定(frps/nginx/其他/空闲)、现有 frps.toml 与 Fonu 期望配置的 **diff 摘要**、待办建议(如「80 被 nginx 占用,需释放」)。这就是用户之前反复手工排查的自动化。
|
||||
返回结构化报告:frps 版本/运行状态/配置路径、80/443 占用者判定(frps/nginx/其他/空闲)、现有 frps.toml 与 Havline 期望配置的 **diff 摘要**、待办建议(如「80 被 nginx 占用,需释放」)。这就是用户之前反复手工排查的自动化。
|
||||
|
||||
### 4.2 下发(Apply)——「自动反代」的主流程
|
||||
|
||||
@@ -74,7 +74,7 @@ cat <配置路径> # 现有 frps.toml 内容(做 dif
|
||||
- 存在 http/https/tcpmux 规则 → 输出 `vhostHTTPPort = 80`、`vhostHTTPSPort = 443`(去端口访问的关键);
|
||||
- 存在 subdomain 规则 → 输出 `subDomainHost = <用户在服务端表单新填的主域>`;
|
||||
- 证书启用时输出 `transport.tls.force` / `webServer.tls` 段;
|
||||
2. **SSH 推送(原子)**:新内容写 `/etc/fonu/frps/frps.toml.new` → `mv` 原子替换 → 旧文件留 `frps.toml.bak`(回滚点);
|
||||
2. **SSH 推送(原子)**:新内容写 `/etc/havline/frps/frps.toml.new` → `mv` 原子替换 → 旧文件留 `frps.toml.bak`(回滚点);
|
||||
3. **热重载**:调 frps admin API `PUT /api/reload`(frps webServer 7500 已有 Basic Auth 凭据)——**不杀进程**,连接不断;API 不可用时降级 `systemctl restart frps`(nohup 场景提示用户);
|
||||
4. **验证**:重读 VPS 上的 frps.toml 确认与期望一致 + `ss` 确认 80/443 已由 frps 监听 + 通过公网侧发起一次探测请求;
|
||||
5. 全程动作与输出写入系统日志(复用 logstore),UI 弹窗展示每步结果。
|
||||
@@ -88,14 +88,14 @@ cat <配置路径> # 现有 frps.toml 内容(做 dif
|
||||
|
||||
### 4.4 证书管理(VPS 侧 HTTPS)
|
||||
|
||||
复用 Fonu 证书页现有 ACME(lego)能力,证书签发在 NAS 侧完成(DNS-01,无需 VPS 参与),然后:
|
||||
复用 Havline 证书页现有 ACME(lego)能力,证书签发在 NAS 侧完成(DNS-01,无需 VPS 参与),然后:
|
||||
|
||||
1. 证书页选「部署到 VPS」→ 选目标服务端;
|
||||
2. SSH 推送 `fullchain.pem` / `privkey.pem` 到 `/etc/fonu/frps/certs/<域名>/`(0600);
|
||||
2. SSH 推送 `fullchain.pem` / `privkey.pem` 到 `/etc/havline/frps/certs/<域名>/`(0600);
|
||||
3. frps.toml 写入 `webServer.tls.certFile/keyFile`(管理面板 HTTPS);对外流量 TLS 若需要也在 frps 侧终止时,走 `transport.tls` 相关段(v0.71 支持 `transport.tls.force` + 证书);
|
||||
4. 续期:Fonu 证书定时任务续期成功后自动重推 + reload(证书 60 天滚动,VPS 侧无感)。
|
||||
4. 续期:Havline 证书定时任务续期成功后自动重推 + reload(证书 60 天滚动,VPS 侧无感)。
|
||||
|
||||
> 设计取舍:对外业务的 HTTPS 终止仍推荐在 **Fonu Nginx(NAS 侧)**(本手册方案 A 架构),frps 侧证书用于 webServer 管理面板与 `tls.force` 场景。若用户想让 VPS 终止 TLS,同一套推送机制也覆盖。
|
||||
> 设计取舍:对外业务的 HTTPS 终止仍推荐在 **Havline Nginx(NAS 侧)**(本手册方案 A 架构),frps 侧证书用于 webServer 管理面板与 `tls.force` 场景。若用户想让 VPS 终止 TLS,同一套推送机制也覆盖。
|
||||
|
||||
## 5. API 与 UI
|
||||
|
||||
@@ -133,8 +133,8 @@ cat <配置路径> # 现有 frps.toml 内容(做 dif
|
||||
## 7. 安全设计
|
||||
|
||||
1. SSH 凭据 secret.Box 加密落库,API 永不回显(同 token 掩码机制);
|
||||
2. 推荐文档引导用户用**密钥登录**(Fonu 可一键生成 ed25519 密钥对、展示公钥让用户贴到 VPS `authorized_keys`);
|
||||
3. 所有远程命令为**固定白名单模板**(版本/端口/状态/替换文件/reload),不拼接用户输入,杜绝命令注入;SFTP 只写 `/etc/fonu/frps/` 前缀内路径;
|
||||
2. 推荐文档引导用户用**密钥登录**(Havline 可一键生成 ed25519 密钥对、展示公钥让用户贴到 VPS `authorized_keys`);
|
||||
3. 所有远程命令为**固定白名单模板**(版本/端口/状态/替换文件/reload),不拼接用户输入,杜绝命令注入;SFTP 只写 `/etc/havline/frps/` 前缀内路径;
|
||||
4. 危险动作(释放端口、重启 frps)二次确认 + 动作日志;
|
||||
5. Apply 幂等:重复执行无副作用(diff 无变化即跳过);
|
||||
6. frps admin API 调用走本机 127.0.0.1:7500(SSH 远端执行 curl),不暴露公网。
|
||||
|
||||
@@ -0,0 +1,962 @@
|
||||
# Fonu → Havline 品牌改名实施与影响评估方案
|
||||
|
||||
> 日期:2026-10-01
|
||||
> 当前版本:`1.9.0`
|
||||
> 当前 Agent 版本:`0.14.1`
|
||||
> 扫描范围:仓库源码、部署文件、脚本、前端、文档、GitHub Actions、迁移与生成配置
|
||||
> 扫描结果:**227 个包含 `Fonu / fonu / FONU` 的文件,共 4032 处匹配**
|
||||
> 扫描排除:`.git`、`web/node_modules`、`web/dist`、`cmd/fonu/web/dist`、`output`、`.playwright-cli`、Havline Logo 预览页
|
||||
> 说明:本方案文档本身不计入上述扫描统计。
|
||||
> 执行记录:2026-10-01 已按“无兼容迁移”模式执行全量切换。本文中的兼容迁移设计作为历史设计记录保留,不再代表当前实施状态。
|
||||
|
||||
---
|
||||
|
||||
## 0. 结论速览
|
||||
|
||||
品牌改名可以做,但**不能做一次无脑全局替换**。
|
||||
|
||||
真正危险的不是页面文字,而是下面这些会直接影响老用户升级的契约:
|
||||
|
||||
| 风险项 | 当前值 | 改名后的风险 |
|
||||
| --- | --- | --- |
|
||||
| 数据文件名 | `fonu.db` | 裸改名后应用会新建空库,用户会认为数据丢失 |
|
||||
| 环境变量前缀 | `FONU_*` | 裸改名后老 `.env`、compose、systemd 配置全部失效 |
|
||||
| Docker Volume | `fonu-data` | 裸改名后会创建新空卷 |
|
||||
| Compose 项目名 | `name: fonu` | 项目名变化会让容器、网络、卷全部重新创建 |
|
||||
| Agent 路径 | `/etc/fonu-agent`、`/var/lib/fonu-agent` | 服务端与 VPS Agent 版本错配时探测和升级失败 |
|
||||
| Agent systemd | `fonu-agent.service` | 新老 Agent 可能同时存在或互相抢端口 |
|
||||
| 会话 Cookie | `fonu_session` | 裸改名会让所有已登录用户掉线 |
|
||||
| 一键升级 | `fonu`、`fonu-updater` 服务名与镜像 | 改名后 updater 找不到目标服务或镜像 |
|
||||
| 前端版本检查 | `O96u/Fonu` | 仓库改名或常量未同步后,在线版本检查失效 |
|
||||
|
||||
推荐采用:
|
||||
|
||||
**分层渐进改名 + 一个稳定版本周期的兼容层。**
|
||||
|
||||
推荐目标命名:
|
||||
|
||||
| 形态 | 目标值 |
|
||||
| --- | --- |
|
||||
| 显示名 | `Havline` |
|
||||
| 小写标识 | `havline` |
|
||||
| 环境变量前缀 | `HAVLINE_` |
|
||||
| Go module | `github.com/havline/havline` |
|
||||
| 主程序 | `havline` |
|
||||
| Agent | `havline-agent` |
|
||||
| Updater | `havline-updater` |
|
||||
| Docker 镜像 | `havline:<version>` |
|
||||
| Updater 镜像 | `havline-updater:<version>` |
|
||||
| Compose 项目 | `name: havline` |
|
||||
| Compose 服务 | `havline`、`updater` |
|
||||
| 数据卷 | `havline-data` |
|
||||
| 数据库 | `havline.db` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前工作区前提
|
||||
|
||||
当前工作区不是干净状态,执行正式改名之前必须先处理基线问题。
|
||||
|
||||
已存在的未提交或未跟踪内容:
|
||||
|
||||
| 类型 | 路径 | 与本次改名关系 |
|
||||
| --- | --- | --- |
|
||||
| 已修改 | `web/src/layouts/AppLayout.vue` | 侧栏已切到 `havline-logo.svg`,属于品牌展示层改动 |
|
||||
| 已修改 | `web/src/assets/brand/logo.png` | 旧品牌资源,改名时必须统一处理 |
|
||||
| 已修改 | `web/public/favicon.png` | 旧 favicon,改名时必须统一处理 |
|
||||
| 已新增 | `web/src/assets/brand/havline-logo.svg` | 当前侧栏使用的第一版 SVG |
|
||||
| 已新增 | `web/src/assets/brand/havline-logo-v1.svg` | 备选或历史 SVG |
|
||||
| 已新增 | `web/src/assets/brand/havline-logo-v2.svg` | 当前推荐的第二版 SVG,尚未接入侧栏 |
|
||||
| 已新增 | `web/assets/image/havline-logo.svg` | 镜像内资源目录的 SVG |
|
||||
| 已新增 | `docs/havline-logo-preview.html` | 第一版 Logo 预览 |
|
||||
| 已新增 | `docs/havline-logo-v2-preview.html` | 第二版 Logo 预览 |
|
||||
| 未跟踪 | `docs/fnos-app-packaging-plan.md` | 飞牛应用打包方案,与品牌名有关 |
|
||||
| 已删除 | `internal/frp-1.0.2/**` | 非本次改名逻辑的一部分,执行前需确认是否保留该删除 |
|
||||
| 已删除 | `web/src/views/FrpView-1.0.2.vue` | 非本次改名逻辑的一部分,执行前需确认是否保留该删除 |
|
||||
|
||||
建议:
|
||||
|
||||
1. 先把 Logo V2 和侧栏资源作为独立提交合入。
|
||||
2. 再确认 `internal/frp-1.0.2` 与 `FrpView-1.0.2.vue` 的删除是否属于当前基线。
|
||||
3. 然后从干净、可回滚的基线开始兼容层和全仓改名。
|
||||
|
||||
---
|
||||
|
||||
## 2. 全仓扫描结果
|
||||
|
||||
### 2.1 数量统计
|
||||
|
||||
| 分类 | 文件数 |
|
||||
| --- | ---: |
|
||||
| Go、module、迁移 | 144 |
|
||||
| 前端 Web | 40 |
|
||||
| 部署、Docker、脚本 | 9 |
|
||||
| 文档、README、CHANGELOG | 31 |
|
||||
| GitHub Actions、`.gitignore` | 3 |
|
||||
| 合计 | 227 |
|
||||
|
||||
匹配总量:**4032 处**。
|
||||
|
||||
### 2.2 重复率最高的文件
|
||||
|
||||
| 文件 | 匹配数 | 说明 |
|
||||
| --- | ---: | --- |
|
||||
| `web/src/views/AgentView.vue` | 214 | Agent 管理页,包含大量文案、变量和 `FonuCard` 引用 |
|
||||
| `web/src/views/FrpView.vue` | 159 | 内网穿透页,包含品牌文案、变量和组件 |
|
||||
| `web/src/views/ProxyView.vue` | 133 | 反向代理页,主要是 CSS 变量与组件名 |
|
||||
| `web/src/views/ProxyRemoteView.vue` | 97 | 公网反代页 |
|
||||
| `web/src/views/DashboardView.vue` | 97 | 仪表盘 |
|
||||
| `web/src/views/SettingsView.vue` | 87 | 设置页 |
|
||||
| `web/src/views/CertificatesView.vue` | 80 | 证书页 |
|
||||
| `web/src/views/DdnsView.vue` | 77 | DDNS 页 |
|
||||
| `docs/fnos-app-packaging-plan.md` | 65 | 飞牛应用方案 |
|
||||
| `docs/improvement-roadmap-2026-09-27.md` | 65 | 改进路线图 |
|
||||
| `docs/cloudflared-integration-plan.md` | 62 | Cloudflare 集成方案 |
|
||||
| `CHANGELOG.md` | 57 | 历史版本记录 |
|
||||
| `docs/brand-rename-plan.md` | 57 | 上一版改名评估文档 |
|
||||
| `docs/DEPLOYMENT.md` | 56 | 部署文档 |
|
||||
| `web/src/styles/variables.css` | 51 | CSS 品牌变量定义 |
|
||||
| `internal/agent/server.go` | 48 | Agent 行为与远端路径 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 命名映射表
|
||||
|
||||
### 3.1 用户可见名称
|
||||
|
||||
| 当前 | 目标 | 处理方式 |
|
||||
| --- | --- | --- |
|
||||
| `Fonu` | `Havline` | 页面标题、侧栏、登录页、通知、状态页 |
|
||||
| `github.com/O96u/Fonu` | 待确认的新 GitHub 地址 | 版本检查、Release 链接 |
|
||||
| Gitea `mrlee/-Fonu.git` | 待确认的新仓库地址 | Compose Git 构建、Updater |
|
||||
| `Fonu Agent` | `Havline Agent` | UI、日志、systemd Description |
|
||||
| `Fonu 自托管网关管理面板` | `Havline 自托管网关管理面板` | systemd unit 描述 |
|
||||
|
||||
### 3.2 代码与构建标识
|
||||
|
||||
| 类型 | 当前 | 目标 | 风险 |
|
||||
| --- | --- | --- | --- |
|
||||
| Go module | `github.com/fonu/fonu` | `github.com/havline/havline` | 中 |
|
||||
| 主程序目录 | `cmd/fonu` | `cmd/havline` | 中 |
|
||||
| Agent 目录 | `cmd/fonu-agent` | `cmd/havline-agent` | 高 |
|
||||
| Updater 目录 | `cmd/fonu-updater` | `cmd/havline-updater` | 高 |
|
||||
| 前端包名 | `fonu-web` | `havline-web` | 低 |
|
||||
| 嵌入目录 | `cmd/fonu/web/dist` | `cmd/havline/web/dist` | 中 |
|
||||
| Agent 嵌入文件 | `fonu-agent-linux-amd64` | `havline-agent-linux-amd64` | 高 |
|
||||
| 构建版本注入 | `github.com/fonu/fonu/internal/version.Version` | `github.com/havline/havline/internal/version.Version` | 高 |
|
||||
| Docker 镜像 | `fonu:<version>` | `havline:<version>` | 高 |
|
||||
| Updater 镜像 | `fonu-updater:<version>` | `havline-updater:<version>` | 高 |
|
||||
|
||||
### 3.3 运行期契约
|
||||
|
||||
| 类型 | 当前 | 目标 | 兼容要求 |
|
||||
| --- | --- | --- | --- |
|
||||
| 环境变量 | `FONU_*` | `HAVLINE_*` | 新前缀优先,旧前缀兜底 |
|
||||
| 数据库 | `fonu.db` | `havline.db` | 自动探测并接管旧库 |
|
||||
| Cookie | `fonu_session` | `havline_session` | 新 Cookie 优先,旧 Cookie 兜底 |
|
||||
| 本地主题 | `fonu-theme` | `havline-theme` | 读旧值,写新值 |
|
||||
| Compose 项目 | `name: fonu` | `name: havline` | 需要卷迁移说明 |
|
||||
| Compose 服务 | `fonu` | `havline` | Updater 变量必须同步 |
|
||||
| Updater Socket | `/run/fonu-updater/updater.sock` | `/run/havline-updater/updater.sock` | Compose 两侧同步 |
|
||||
| Updater Socket 卷 | `fonu-updater-sock` | `havline-updater-sock` | 需要卷迁移 |
|
||||
| 数据卷 | `fonu-data` | `havline-data` | 不能直接改名,需要复制或保留旧卷名 |
|
||||
| Agent 响应字段 | `"service": "fonu-agent"` | `"service": "havline-agent"` | 新服务端同时识别新旧值 |
|
||||
|
||||
### 3.4 Agent 远端契约
|
||||
|
||||
| 类型 | 当前 | 目标 | 风险 |
|
||||
| --- | --- | --- | --- |
|
||||
| 二进制 | `/usr/local/bin/fonu-agent` | `/usr/local/bin/havline-agent` | 高 |
|
||||
| 配置目录 | `/etc/fonu-agent` | `/etc/havline-agent` | 高 |
|
||||
| 数据目录 | `/var/lib/fonu-agent` | `/var/lib/havline-agent` | 高 |
|
||||
| systemd unit | `fonu-agent.service` | `havline-agent.service` | 高 |
|
||||
| 环境变量 | `FONU_AGENT_*` | `HAVLINE_AGENT_*` | 高 |
|
||||
| Nginx 标记 | `# Managed by fonu-agent.` | `# Managed by havline-agent.` | 高 |
|
||||
| Nginx 探针 | `fonu-probe.conf` | `havline-probe.conf` | 高 |
|
||||
| Nginx 残留 | `fonu-*.conf` | `havline-*.conf` | 高 |
|
||||
| 证书目录 | `/etc/fonu-agent/tls` | `/etc/havline-agent/tls` | 高 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 分层影响分析
|
||||
|
||||
### 4.1 展示层
|
||||
|
||||
主要范围:
|
||||
|
||||
| 区域 | 文件 |
|
||||
| --- | --- |
|
||||
| 浏览器标题 | `web/index.html` |
|
||||
| 侧栏品牌 | `web/src/layouts/AppLayout.vue` |
|
||||
| 登录页 | `web/src/views/LoginView.vue` |
|
||||
| 公开状态页 | `internal/api/status_page_handler.go`、`internal/api/status_handler.go` |
|
||||
| 通知 | `internal/notify/format.go`、`internal/notify/service.go`、`internal/notify/push.go` |
|
||||
| Agent 页面 | `web/src/views/AgentView.vue` |
|
||||
| FRP 页面 | `web/src/views/FrpView.vue` |
|
||||
| 设置页 | `web/src/views/SettingsView.vue` |
|
||||
| 提示文案 | `web/src/constants/notifyEvents.ts`、`web/src/router/index.ts` |
|
||||
|
||||
高风险文案:
|
||||
|
||||
| 路径 | 当前内容 |
|
||||
| --- | --- |
|
||||
| `web/src/layouts/AppLayout.vue` | `Fonu`、`让 NAS 访问更简单` |
|
||||
| `web/src/views/LoginView.vue` | `Fonu` 标题、旧 Logo、`alt="Fonu"` |
|
||||
| `web/index.html` | `<title>Fonu</title>` |
|
||||
| `web/src/views/AgentView.vue` | `Fonu Agent`、`fonu-agent`、删除确认文案 |
|
||||
| `web/src/constants/notifyEvents.ts` | `VPS 上的 fonu-agent 连续无响应` |
|
||||
| `internal/notify/*` | 邮件、推送中的品牌前缀 |
|
||||
| `internal/api/status_*` | 公开状态页标题、页脚 |
|
||||
|
||||
### 4.2 前端资源层
|
||||
|
||||
当前品牌资源:
|
||||
|
||||
| 路径 | 用途 | 建议 |
|
||||
| --- | --- | --- |
|
||||
| `web/src/assets/brand/logo.png` | 侧栏/登录页旧 Logo | 保留至替换完成,随后删除 |
|
||||
| `web/src/assets/brand/logo00.png` | 历史/备用 Logo | 确认无引用后处理 |
|
||||
| `web/src/assets/brand/havline-logo.svg` | 当前第一版 SVG | 如决定 V2,替换引用 |
|
||||
| `web/src/assets/brand/havline-logo-v1.svg` | 第一版 SVG 备份 | 归档或删除 |
|
||||
| `web/src/assets/brand/havline-logo-v2.svg` | 推荐第二版 SVG | 作为正式 Logo 候选 |
|
||||
| `web/assets/image/logo.png` | 镜像内旧 Logo | 替换为 SVG 或导出 PNG |
|
||||
| `web/assets/image/havline-logo.svg` | 镜像内 SVG | 确认与 `src/assets` 保持一致 |
|
||||
| `web/assets/image/banner.png` | 品牌 Banner | 需要重绘 |
|
||||
| `web/src/assets/brand/banner.png` | 前端 Banner | 需要重绘 |
|
||||
| `web/public/favicon.png` | 浏览器 favicon | 换成 Havline SVG/PNG |
|
||||
| `web/public/111favicon.png` | 本地备用文件 | 确认是否应该入库 |
|
||||
|
||||
### 4.3 CSS 与组件层
|
||||
|
||||
CSS 变量数量:`web/src/styles/variables.css` 中 51 处 `--fonu-*`,且被大量 `.vue` 文件引用。
|
||||
|
||||
建议:
|
||||
|
||||
| 当前 | 目标 |
|
||||
| --- | --- |
|
||||
| `--fonu-brand` | `--havline-brand` |
|
||||
| `--fonu-surface` | `--havline-surface` |
|
||||
| `--fonu-text` | `--havline-text` |
|
||||
| `--fonu-space-*` | `--havline-space-*` |
|
||||
| `--fonu-radius*` | `--havline-radius*` |
|
||||
|
||||
组件重命名:
|
||||
|
||||
| 当前 | 目标 |
|
||||
| --- | --- |
|
||||
| `web/src/components/FonuCard.vue` | `web/src/components/HavlineCard.vue` |
|
||||
| `.fonu-card` | `.havline-card` |
|
||||
| `FonuCard` import | `HavlineCard` import |
|
||||
|
||||
影响文件:
|
||||
|
||||
`web/src/views/AgentView.vue`、`CertificatesView.vue`、`CloudflareTunnelConfigView.vue`、`CloudflareView.vue`、`DashboardView.vue`、`DdnsView.vue`、`ExampleView.vue`、`FrpView.vue`、`LogsView.vue`、`ProxyRemoteView.vue`、`ProxyView.vue`、`SettingsView.vue`。
|
||||
|
||||
### 4.4 构建与发布层
|
||||
|
||||
| 文件 | 影响 |
|
||||
| --- | --- |
|
||||
| `Dockerfile` | Agent 交叉编译、嵌入目录、主程序路径、Updater 二进制、环境变量 |
|
||||
| `docker-compose.yml` | 项目名、服务名、镜像、容器名、卷、环境变量 |
|
||||
| `docker-compose.hub.yml` | Git 构建地址、镜像、Updater、Socket、卷、服务名 |
|
||||
| `docker-compose.host.yml` | 项目名、镜像、容器名、卷、host 网络环境变量 |
|
||||
| `scripts/build-linux.ps1` | 发布物命名、`cmd/fonu`、agent 输出路径、ldflags |
|
||||
| `scripts/fonu-install.sh` | `/opt/fonu`、`/var/lib/fonu`、`/etc/fonu`、`fonu.service` |
|
||||
| `scripts/agent-install.sh` | Agent 二进制、目录、systemd unit、环境变量 |
|
||||
| `scripts/upgrade.sh` | 容器名、镜像名、`FONU_VERSION` |
|
||||
| `scripts/upgrade-hub.sh` | Gitea 地址、容器名、镜像名、回滚命令 |
|
||||
| `.github/workflows/ci.yml` | `cmd/fonu/web/dist` 同步路径 |
|
||||
| `.github/workflows/docker-release.yml` | Docker Hub 镜像 `DOCKERHUB_USERNAME}/fonu` |
|
||||
| `.github/workflows/github-release.yml` | 虽无直接旧名,但 Release 流程需要回归 |
|
||||
|
||||
### 4.5 Go 代码标识层
|
||||
|
||||
`go.mod`:
|
||||
|
||||
```text
|
||||
github.com/fonu/fonu
|
||||
```
|
||||
|
||||
一旦修改 module path,所有 Go 文件中的 import 都需要更新。
|
||||
|
||||
当前受影响 Go 文件包括:
|
||||
|
||||
| 目录 | 文件数量 |
|
||||
| --- | ---: |
|
||||
| `cmd` | 3 |
|
||||
| `internal/acme` | 7 |
|
||||
| `internal/agent` | 5 |
|
||||
| `internal/api` | 27 |
|
||||
| `internal/app` | 3 |
|
||||
| `internal/auth` | 3 |
|
||||
| `internal/backup` | 4 |
|
||||
| `internal/cloudflared` | 10 |
|
||||
| `internal/config` | 2 |
|
||||
| `internal/frp` | 13 |
|
||||
| `internal/nginx` | 23 |
|
||||
| `internal/notify` | 7 |
|
||||
| `internal/updater` | 6 |
|
||||
| 其他 internal | 17 |
|
||||
| `migrations` | 2 |
|
||||
|
||||
### 4.6 运行契约层
|
||||
|
||||
| 文件 | 当前契约 | 改名风险 |
|
||||
| --- | --- | --- |
|
||||
| `internal/config/config.go` | `FONU_*`、`fonu.db` | 环境变量和数据库路径 |
|
||||
| `internal/api/middleware.go` | `fonu_session` | 登录态 |
|
||||
| `internal/api/auth_handler.go` | 设置/清除 `fonu_session` | 登录态 |
|
||||
| `web/src/composables/useTheme.ts` | `fonu-theme` | 主题偏好 |
|
||||
| `internal/backup/service.go` | 备份名、`fonu.db` 检查 | 备份兼容 |
|
||||
| `internal/backup/archive.go` | `fonu-backup-*`、`fonu.db` | 导入旧备份 |
|
||||
| `internal/updater/runner.go` | `FONU_VERSION`、`fonu:<version>` | 一键升级 |
|
||||
| `cmd/fonu-updater/main.go` | Updater 环境变量与默认服务名 | 一键升级 |
|
||||
| `internal/frp/agent_installer.go` | `/etc/fonu-agent`、`/usr/local/bin/fonu-agent` | Agent 安装与探测 |
|
||||
| `internal/frp/ssh_installer.go` | 上传 `/tmp/fonu-agent` | Agent 安装 |
|
||||
| `internal/agent/server.go` | Agent 版本、远端路径、Nginx 标记 | Agent 兼容 |
|
||||
| `internal/agent/transport.go` | `/etc/fonu-agent/tls` | HTTPS 直连 |
|
||||
| `internal/monitor/monitor.go` | `fonu-agent` 状态文案与判定 | 监控与通知 |
|
||||
|
||||
### 4.7 数据与备份层
|
||||
|
||||
| 数据 | 当前值 | 处理策略 |
|
||||
| --- | --- | --- |
|
||||
| SQLite 主库 | `<DataDir>/fonu.db` | 检测旧库,优先原子重命名,失败则回退旧路径 |
|
||||
| Session Secret | `<DataDir>/session_secret` | 文件名不含品牌,不需要改名 |
|
||||
| 备份文件名 | `fonu-backup-YYYYMMDD-HHMMSS-N.tar.gz` | 新名生成,旧备份仍允许导入 |
|
||||
| 备份识别 | 必须存在 `fonu.db` | 同时兼容 `havline.db` 与 `fonu.db` |
|
||||
| Docker 卷 | `fonu-data` | 不自动删旧卷,提供复制/挂载迁移 |
|
||||
| Updater 卷 | `fonu-updater-sock` | 与 Compose 同步迁移 |
|
||||
| Agent 数据 | `/var/lib/fonu-agent` | 安装脚本迁移到新目录,旧目录保留一个版本 |
|
||||
| Agent 配置 | `/etc/fonu-agent/agent.env` | 新目录优先,旧目录兼容读取 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 兼容迁移设计
|
||||
|
||||
### 5.1 环境变量
|
||||
|
||||
当前读取逻辑:
|
||||
|
||||
```go
|
||||
envOr("FONU_DATA_DIR", defaultDataDir())
|
||||
```
|
||||
|
||||
目标逻辑:
|
||||
|
||||
```go
|
||||
envOrHavline("HAVLINE_DATA_DIR", "FONU_DATA_DIR", defaultDataDir())
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
1. `HAVLINE_*` 优先。
|
||||
2. `FONU_*` 作为 fallback。
|
||||
3. 命中旧变量时启动日志给出一条迁移提示。
|
||||
4. 至少保留一个 stable 版本后再删除旧前缀。
|
||||
|
||||
必须兼容的环境变量:
|
||||
|
||||
```text
|
||||
FONU_DATA_DIR
|
||||
FONU_LISTEN
|
||||
FONU_SESSION_SECRET
|
||||
FONU_NGINX_BIN
|
||||
FONU_NGINX_PID
|
||||
FONU_NGINX_MIME_TYPES
|
||||
FONU_NGINX_HTTP_PORT
|
||||
FONU_NGINX_HTTPS_PORT
|
||||
FONU_FRPC_BIN
|
||||
FONU_FRP_PID
|
||||
FONU_UPDATER_SOCKET
|
||||
FONU_AGENT_LISTEN
|
||||
FONU_AGENT_ALLOW_REMOTE
|
||||
FONU_AGENT_TOKEN
|
||||
FONU_AGENT_NGINX_CONF_DIR
|
||||
FONU_AGENT_NGINX_LOG_DIR
|
||||
FONU_AGENT_NGINX_BIN
|
||||
FONU_AGENT_CERTS_DIR
|
||||
FONU_AGENT_DATA_DIR
|
||||
FONU_AGENT_FRPS_CONFIG
|
||||
FONU_AGENT_FRPS_BIN
|
||||
FONU_UPDATE_PROJECT_DIR
|
||||
FONU_UPDATE_COMPOSE_FILE
|
||||
FONU_UPDATE_SERVICE
|
||||
FONU_UPDATE_MODE
|
||||
FONU_UPDATE_REPO
|
||||
FONU_UPDATE_BRANCH
|
||||
FONU_UPDATE_APP_URL
|
||||
FONU_UPDATE_HOST_DIR
|
||||
FONU_UPDATE_PROJECT_NAME
|
||||
FONU_UPDATE_SELF_IMAGE
|
||||
FONU_UPDATE_DOCKER_SOCK
|
||||
FONU_VERSION
|
||||
FONU_DATA_PATH
|
||||
```
|
||||
|
||||
### 5.2 数据库接管
|
||||
|
||||
判断顺序:
|
||||
|
||||
1. `havline.db` 存在:直接使用。
|
||||
2. `havline.db` 不存在且 `fonu.db` 存在:尝试原子重命名。
|
||||
3. 重命名失败:继续使用 `fonu.db`,日志提示迁移。
|
||||
4. 两者都不存在:创建 `havline.db`。
|
||||
|
||||
禁止行为:
|
||||
|
||||
1. 不能在旧库存在时直接创建新空库。
|
||||
2. 不能删除旧库。
|
||||
3. 不能修改 `session_secret`,否则已有加密凭据无法解密。
|
||||
|
||||
### 5.3 Cookie 与本地偏好
|
||||
|
||||
| 项目 | 读策略 | 写策略 |
|
||||
| --- | --- | --- |
|
||||
| Session Cookie | `havline_session` 优先,回退 `fonu_session` | 只写 `havline_session` |
|
||||
| 主题偏好 | `havline-theme` 优先,回退 `fonu-theme` | 读到旧值后回写新键 |
|
||||
|
||||
### 5.4 Docker Compose 与 Volume
|
||||
|
||||
不能直接把 `fonu-data` 改成 `havline-data` 就结束。
|
||||
|
||||
推荐策略:
|
||||
|
||||
1. 新 compose 使用 `name: havline`。
|
||||
2. 新卷名为 `havline-data`。
|
||||
3. 文档提供旧卷到新卷的复制命令。
|
||||
4. 升级前先备份旧卷。
|
||||
5. 首版兼容可以直接把 `havline-data` 显式指向旧卷进行阶段性过渡。
|
||||
|
||||
示例:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
havline-data:
|
||||
name: fonu-data
|
||||
```
|
||||
|
||||
上述做法只用于过渡版本。完全切换完成后再恢复为 `name: havline-data`。
|
||||
|
||||
### 5.5 Updater 迁移
|
||||
|
||||
一键升级涉及四组变量:
|
||||
|
||||
```text
|
||||
FONU_UPDATE_SERVICE=fonu
|
||||
FONU_UPDATE_APP_URL=http://fonu:6893
|
||||
FONU_UPDATE_SELF_IMAGE=fonu-updater:<version>
|
||||
FONU_UPDATER_SOCKET=/run/fonu-updater/updater.sock
|
||||
```
|
||||
|
||||
改名时必须一起处理:
|
||||
|
||||
| 当前 | 目标 |
|
||||
| --- | --- |
|
||||
| Service `fonu` | Service `havline` |
|
||||
| App URL `http://fonu:6893` | `http://havline:6893` |
|
||||
| Updater 镜像 `fonu-updater` | `havline-updater` |
|
||||
| Updater socket 目录 | `/run/havline-updater/updater.sock` |
|
||||
| Socket volume | `havline-updater-sock` |
|
||||
|
||||
### 5.6 Agent 迁移
|
||||
|
||||
推荐 Agent 安装脚本流程:
|
||||
|
||||
1. 检测旧 `/etc/fonu-agent/agent.env`。
|
||||
2. 停止并禁用 `fonu-agent.service`。
|
||||
3. 安装新 `havline-agent` 二进制。
|
||||
4. 创建 `/etc/havline-agent`、`/var/lib/havline-agent`。
|
||||
5. 迁移或复制旧 token、证书和 frps 配置。
|
||||
6. 安装 `havline-agent.service`。
|
||||
7. 保留旧目录一个版本,便于回滚。
|
||||
8. 服务端探测同时检查新旧二进制、目录和 systemd unit。
|
||||
|
||||
Agent 版本必须递增。当前 `AgentVersion = 0.14.1`,品牌与路径变更建议递增到 `0.15.0`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 实施阶段
|
||||
|
||||
### 阶段 0:冻结名称与外部身份
|
||||
|
||||
需要确认:
|
||||
|
||||
1. GitHub 仓库新地址。
|
||||
2. Gitea 仓库新地址。
|
||||
3. Docker Hub 命名空间与仓库名。
|
||||
4. 是否保留旧仓库重定向。
|
||||
5. 是否发布旧镜像名兼容标签。
|
||||
6. 域名与商标状态。
|
||||
|
||||
### 阶段 1:兼容层先行
|
||||
|
||||
先不替换全仓品牌,只增加兼容能力:
|
||||
|
||||
1. `internal/config/config.go` 支持 `HAVLINE_*` 与 `FONU_*` 双读。
|
||||
2. 数据库支持 `havline.db` 与 `fonu.db`。
|
||||
3. Session Cookie 支持新旧双读。
|
||||
4. `useTheme.ts` 支持新旧 key。
|
||||
5. Agent 探测支持新旧路径。
|
||||
6. Backup 导入支持新旧数据库名。
|
||||
7. Updater 支持新旧服务名与镜像名过渡。
|
||||
|
||||
### 阶段 2:展示层改名
|
||||
|
||||
1. `web/index.html` 标题。
|
||||
2. `AppLayout.vue` 品牌名、标语、Logo。
|
||||
3. `LoginView.vue` 标题、Logo、说明文案。
|
||||
4. 公开状态页和通知品牌名。
|
||||
5. `web/src/constants/notifyEvents.ts` 和路由 meta。
|
||||
6. 前端资源与 favicon。
|
||||
7. `FonuCard.vue` -> `HavlineCard.vue`。
|
||||
8. `--fonu-*` -> `--havline-*`。
|
||||
|
||||
### 阶段 3:代码标识改名
|
||||
|
||||
1. `go.mod` module path。
|
||||
2. 全仓 Go import。
|
||||
3. `cmd/fonu` -> `cmd/havline`。
|
||||
4. `cmd/fonu-agent` -> `cmd/havline-agent`。
|
||||
5. `cmd/fonu-updater` -> `cmd/havline-updater`。
|
||||
6. Dockerfile ldflags。
|
||||
7. Agent 嵌入文件。
|
||||
8. CI `cmd/fonu/web/dist` 路径。
|
||||
|
||||
### 阶段 4:交付与升级链改名
|
||||
|
||||
1. Docker 镜像名。
|
||||
2. Updater 镜像名。
|
||||
3. Compose service、container、volume。
|
||||
4. `scripts/upgrade.sh`、`scripts/upgrade-hub.sh`。
|
||||
5. 一键升级配置。
|
||||
6. Docker Release workflow。
|
||||
|
||||
### 阶段 5:Agent 全量切换
|
||||
|
||||
1. 新 Agent 二进制。
|
||||
2. 新 systemd unit。
|
||||
3. 新目录和环境变量。
|
||||
4. Nginx 标记兼容。
|
||||
5. 旧 Agent 探测、升级、回滚。
|
||||
6. Agent 版本递增与双架构重编。
|
||||
|
||||
### 阶段 6:文档与历史清理
|
||||
|
||||
1. README、DEPLOYMENT、容器文档、飞牛打包文档。
|
||||
2. CHANGELOG 只追加,不改历史发布记录。
|
||||
3. 删除旧兼容代码。
|
||||
4. 移除旧 Logo 和旧资源。
|
||||
5. 全仓 grep 验收。
|
||||
|
||||
---
|
||||
|
||||
## 7. 验证矩阵
|
||||
|
||||
### 7.1 构建验证
|
||||
|
||||
```powershell
|
||||
go build ./...
|
||||
go vet ./...
|
||||
go test ./... -count=1
|
||||
cd web
|
||||
npx vue-tsc --noEmit
|
||||
npx vite build
|
||||
npm test
|
||||
```
|
||||
|
||||
### 7.2 Docker 验证
|
||||
|
||||
1. `docker-compose.yml` 新建部署。
|
||||
2. `docker-compose.hub.yml` 单文件部署。
|
||||
3. `docker-compose.host.yml` host 网络部署。
|
||||
4. 旧 `fonu-data` 卷启动新版本。
|
||||
5. 旧 `.env` 中 `FONU_*` 继续生效。
|
||||
6. 新 `.env` 中 `HAVLINE_*` 优先。
|
||||
7. 一键升级成功。
|
||||
8. 回滚到旧镜像成功。
|
||||
|
||||
### 7.3 数据验证
|
||||
|
||||
1. 旧 `fonu.db` 自动接管。
|
||||
2. 旧备份导入。
|
||||
3. 新备份导出与恢复。
|
||||
4. `session_secret` 不变,凭据仍能解密。
|
||||
5. 旧 Cookie 能继续登录一次并迁移到新 Cookie。
|
||||
6. 旧主题偏好迁移。
|
||||
|
||||
### 7.4 Agent 验证
|
||||
|
||||
1. 旧 Agent -> 新服务端。
|
||||
2. 新 Agent -> 旧服务端。
|
||||
3. 新 Agent -> 新服务端。
|
||||
4. 旧 `/etc/fonu-agent` 升级到新目录。
|
||||
5. 旧 `fonu-agent.service` 被正确停止和替换。
|
||||
6. 旧 `fonu-probe.conf` 和 `fonu-*.conf` 能被识别和清理。
|
||||
|
||||
---
|
||||
|
||||
## 8. 回滚策略
|
||||
|
||||
| 阶段 | 回滚方式 |
|
||||
| --- | --- |
|
||||
| 展示层 | Git revert 文案与图片 |
|
||||
| CSS/组件 | Git revert 组件重命名 |
|
||||
| 环境变量双读 | 保留旧逻辑,无需回滚 |
|
||||
| 数据库 | 优先使用旧库,反向重命名需单独文档 |
|
||||
| Docker Volume | 保留旧卷,不自动删除 |
|
||||
| Updater | 恢复旧 compose 与旧镜像 |
|
||||
| Agent | 保留旧目录与旧 unit,重新启用旧服务 |
|
||||
| Module path | 整体 revert,避免混合 import |
|
||||
|
||||
---
|
||||
|
||||
## 9. 建议提交拆分
|
||||
|
||||
| 提交 | 内容 |
|
||||
| --- | --- |
|
||||
| 1 | 增加 Havline Logo 资产与预览 |
|
||||
| 2 | 品牌改名方案文档 |
|
||||
| 3 | 环境变量、DB、Cookie、主题兼容层 |
|
||||
| 4 | 前端展示层与资源替换 |
|
||||
| 5 | CSS 变量与组件重命名 |
|
||||
| 6 | Go module、cmd 目录、Agent 嵌入路径 |
|
||||
| 7 | Docker、Compose、Updater、脚本 |
|
||||
| 8 | Agent 远端安装与迁移 |
|
||||
| 9 | 文档全面更新 |
|
||||
| 10 | 兼容层清理 |
|
||||
|
||||
每个提交都必须能独立构建和回滚,不要把 10 个阶段压成一个提交。
|
||||
|
||||
---
|
||||
|
||||
## 10. 完整文件清单
|
||||
|
||||
以下文件为当前扫描到包含 `Fonu / fonu / FONU` 的全部文件。生成目录、`node_modules`、`.git` 不在此列表中。
|
||||
|
||||
### 10.1 GitHub Actions 与忽略规则
|
||||
|
||||
```text
|
||||
.github/workflows/ci.yml
|
||||
.github/workflows/docker-release.yml
|
||||
.gitignore
|
||||
```
|
||||
|
||||
### 10.2 Go、module 与迁移
|
||||
|
||||
```text
|
||||
go.mod
|
||||
cmd/fonu/main.go
|
||||
cmd/fonu-agent/main.go
|
||||
cmd/fonu-updater/main.go
|
||||
internal/acme/dns_provider.go
|
||||
internal/acme/dns_provider_test.go
|
||||
internal/acme/domains.go
|
||||
internal/acme/download.go
|
||||
internal/acme/job.go
|
||||
internal/acme/service.go
|
||||
internal/acme/volcengine_dns.go
|
||||
internal/agent/insights.go
|
||||
internal/agent/route_test.go
|
||||
internal/agent/server.go
|
||||
internal/agent/transport.go
|
||||
internal/agent/versions.go
|
||||
internal/agent/versions_test.go
|
||||
internal/api/auth_handler.go
|
||||
internal/api/auth_handler_test.go
|
||||
internal/api/backup_handler.go
|
||||
internal/api/cert_handler.go
|
||||
internal/api/chinacidr_handler.go
|
||||
internal/api/cloudflare_handler.go
|
||||
internal/api/ddns_handler.go
|
||||
internal/api/deps.go
|
||||
internal/api/discovery_handler.go
|
||||
internal/api/frpmulti_handler.go
|
||||
internal/api/logs_handler.go
|
||||
internal/api/logs_handler_test.go
|
||||
internal/api/metrics_handler.go
|
||||
internal/api/middleware.go
|
||||
internal/api/middleware_test.go
|
||||
internal/api/nginx_handler.go
|
||||
internal/api/notify_handler.go
|
||||
internal/api/proxy_handler.go
|
||||
internal/api/proxy_security.go
|
||||
internal/api/router.go
|
||||
internal/api/router_auth_test.go
|
||||
internal/api/settings_handler.go
|
||||
internal/api/status_handler.go
|
||||
internal/api/status_page_handler.go
|
||||
internal/api/token_handler.go
|
||||
internal/api/update_handler.go
|
||||
internal/api/version_handler.go
|
||||
internal/api/version_handler_test.go
|
||||
internal/app/app.go
|
||||
internal/app/monitor_source.go
|
||||
internal/app/monitor_source_test.go
|
||||
internal/auth/auth.go
|
||||
internal/auth/auth_test.go
|
||||
internal/auth/token_test.go
|
||||
internal/backup/archive.go
|
||||
internal/backup/auto.go
|
||||
internal/backup/auto_test.go
|
||||
internal/backup/service.go
|
||||
internal/chinacidr/service.go
|
||||
internal/cloudflared/cloudflare_api.go
|
||||
internal/cloudflared/config.go
|
||||
internal/cloudflared/config_test.go
|
||||
internal/cloudflared/health_test.go
|
||||
internal/cloudflared/manager.go
|
||||
internal/cloudflared/model.go
|
||||
internal/cloudflared/operations.go
|
||||
internal/cloudflared/routes.go
|
||||
internal/cloudflared/service.go
|
||||
internal/cloudflared/settings_test.go
|
||||
internal/cloudflared/store.go
|
||||
internal/config/config.go
|
||||
internal/config/config_test.go
|
||||
internal/db/db_test.go
|
||||
internal/ddns/service.go
|
||||
internal/frp/agent_client.go
|
||||
internal/frp/agent_insights.go
|
||||
internal/frp/agent_installer.go
|
||||
internal/frp/agentbin/embed.go
|
||||
internal/frp/frps_template.go
|
||||
internal/frp/manager.go
|
||||
internal/frp/model.go
|
||||
internal/frp/route_drift.go
|
||||
internal/frp/runtime.go
|
||||
internal/frp/service.go
|
||||
internal/frp/ssh_installer.go
|
||||
internal/frp/ssh_installer_test.go
|
||||
internal/fsutil/fsutil_test.go
|
||||
internal/logstore/dashboard_access_test.go
|
||||
internal/logstore/reader.go
|
||||
internal/monitor/monitor.go
|
||||
internal/nginx/certs.go
|
||||
internal/nginx/comments.go
|
||||
internal/nginx/custom.go
|
||||
internal/nginx/custom_test.go
|
||||
internal/nginx/endpoints.go
|
||||
internal/nginx/endpoints_test.go
|
||||
internal/nginx/errors.go
|
||||
internal/nginx/errors_test.go
|
||||
internal/nginx/generate.go
|
||||
internal/nginx/generate_china_bypass_test.go
|
||||
internal/nginx/generate_china_cidr_path_test.go
|
||||
internal/nginx/generate_comments_test.go
|
||||
internal/nginx/generate_https_no_cert_test.go
|
||||
internal/nginx/generate_https_with_cert_test.go
|
||||
internal/nginx/generate_multi_test.go
|
||||
internal/nginx/generate_order_test.go
|
||||
internal/nginx/generate_security_test.go
|
||||
internal/nginx/generate_seed_mix_test.go
|
||||
internal/nginx/generate_test.go
|
||||
internal/nginx/global_framework_test.go
|
||||
internal/nginx/htpasswd.go
|
||||
internal/nginx/htpasswd_test.go
|
||||
internal/nginx/nginx.go
|
||||
internal/nginx/nginx_integration_test.go
|
||||
internal/nginx/nginx_unavailable_test.go
|
||||
internal/nginx/pid_test.go
|
||||
internal/nginx/realip.go
|
||||
internal/nginx/security.go
|
||||
internal/nginx/versions.go
|
||||
internal/nginx/versions_test.go
|
||||
internal/nginxtmpl/nginxtmpl_test.go
|
||||
internal/notify/format.go
|
||||
internal/notify/format_test.go
|
||||
internal/notify/push.go
|
||||
internal/notify/push_test.go
|
||||
internal/notify/service.go
|
||||
internal/notify/smtp_test.go
|
||||
internal/notify/store.go
|
||||
internal/notify/store_test.go
|
||||
internal/proxy/custom_endpoints_test.go
|
||||
internal/proxy/proxy.go
|
||||
internal/proxy/security.go
|
||||
internal/proxy/security_test.go
|
||||
internal/service/nginx_config.go
|
||||
internal/service/proxy.go
|
||||
internal/service/proxy_opts.go
|
||||
internal/sysinfo/sysinfo_windows.go
|
||||
internal/traffic/collector.go
|
||||
internal/traffic/collector_test.go
|
||||
internal/updater/client.go
|
||||
internal/updater/model.go
|
||||
internal/updater/remote_version_test.go
|
||||
internal/updater/runner.go
|
||||
internal/updater/server.go
|
||||
internal/updater/server_test.go
|
||||
migrations/023_frp_server_agent.sql
|
||||
migrations/035_proxy_exits.sql
|
||||
```
|
||||
|
||||
### 10.3 前端 Web
|
||||
|
||||
```text
|
||||
web/index.html
|
||||
web/package.json
|
||||
web/package-lock.json
|
||||
web/src/App.vue
|
||||
web/src/main.ts
|
||||
web/src/api/client.ts
|
||||
web/src/api/types.ts
|
||||
web/src/components/ConfigDiffView.vue
|
||||
web/src/components/EmptyState.vue
|
||||
web/src/components/FonuCard.vue
|
||||
web/src/components/LoadError.vue
|
||||
web/src/components/MetricsTrendPanel.vue
|
||||
web/src/components/NginxCodeEditor.vue
|
||||
web/src/components/PageHeader.vue
|
||||
web/src/components/ProxyAccessLogBox.vue
|
||||
web/src/components/StatCard.vue
|
||||
web/src/components/StatusBadge.vue
|
||||
web/src/components/TaskProviderForm.vue
|
||||
web/src/components/UpdateModal.vue
|
||||
web/src/composables/useTheme.ts
|
||||
web/src/constants/app.ts
|
||||
web/src/constants/notifyEvents.ts
|
||||
web/src/layouts/AppLayout.vue
|
||||
web/src/router/index.ts
|
||||
web/src/styles/variables.css
|
||||
web/src/utils/clipboard.ts
|
||||
web/src/views/AgentView.vue
|
||||
web/src/views/CertificatesView.vue
|
||||
web/src/views/CloudflareTunnelConfigView.vue
|
||||
web/src/views/CloudflareView.vue
|
||||
web/src/views/DashboardView.vue
|
||||
web/src/views/DdnsView.vue
|
||||
web/src/views/ExampleView.vue
|
||||
web/src/views/FrpView.vue
|
||||
web/src/views/LoginView.vue
|
||||
web/src/views/LogsView.vue
|
||||
web/src/views/ProxyRemoteView.vue
|
||||
web/src/views/ProxyView.vue
|
||||
web/src/views/SettingsView.vue
|
||||
web/src/views/StatusPageView.vue
|
||||
```
|
||||
|
||||
### 10.4 部署、Docker 与脚本
|
||||
|
||||
```text
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
docker-compose.hub.yml
|
||||
docker-compose.host.yml
|
||||
scripts/agent-install.sh
|
||||
scripts/build-linux.ps1
|
||||
scripts/fonu-install.sh
|
||||
scripts/upgrade.sh
|
||||
scripts/upgrade-hub.sh
|
||||
```
|
||||
|
||||
### 10.5 文档与历史
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
README.md
|
||||
docs/DEPLOYMENT.md
|
||||
docs/FRP公网暴露操作手册.md
|
||||
docs/README.md
|
||||
docs/agent-capability-plan.md
|
||||
docs/agent-listen-strategy-plan.md
|
||||
docs/agent-proxy-roadmap.md
|
||||
docs/agent-proxy-settings-plan.md
|
||||
docs/agent-view-design-v2.md
|
||||
docs/agent-view-design.md
|
||||
docs/agent-view-refactor-plan.md
|
||||
docs/brand-rename-plan.md
|
||||
docs/cloudflared-current-features.md
|
||||
docs/cloudflared-feature-plan.md
|
||||
docs/cloudflared-fonu-fit-plan.md
|
||||
docs/cloudflared-github-ux-research.md
|
||||
docs/cloudflared-integration-plan.md
|
||||
docs/container-deploy-and-data.md
|
||||
docs/feature-benchmark-2026-09-24.md
|
||||
docs/fnos-app-packaging-plan.md
|
||||
docs/frp-server-list-merge-plan.md
|
||||
docs/frps-features.md
|
||||
docs/frps服务端管理方案.md
|
||||
docs/frps管理方案.md
|
||||
docs/improvement-roadmap-2026-09-27.md
|
||||
docs/project-analysis-2026-09-27.md
|
||||
docs/project-audit-2026-09-24.md
|
||||
docs/proxy-remote-design.md
|
||||
docs/system-upgrade.md
|
||||
docs/upstream-sync-plan.md
|
||||
```
|
||||
|
||||
### 10.6 不在 grep 结果中但需要同步处理
|
||||
|
||||
| 路径 | 原因 |
|
||||
| --- | --- |
|
||||
| `web/src/assets/brand/havline-logo-v2.svg` | 新 Logo 资源,需要决定最终接入口径 |
|
||||
| `web/src/assets/brand/havline-logo-v1.svg` | Logo 历史版本 |
|
||||
| `web/src/assets/brand/havline-logo.svg` | 当前已接入侧栏 |
|
||||
| `web/src/assets/brand/logo00.png` | 历史/备用 Logo |
|
||||
| `web/src/assets/brand/banner.png` | 旧品牌 Banner |
|
||||
| `web/src/assets/brand/banner-art.png` | 旧品牌 Banner 素材 |
|
||||
| `web/assets/image/havline-logo.svg` | 镜像资源目录中的 SVG |
|
||||
| `web/assets/image/logo.png` | 旧品牌图片,需要替换 |
|
||||
| `web/assets/image/banner.png` | 旧品牌 Banner,需要重绘 |
|
||||
| `web/public/favicon.png` | 旧 favicon,需要替换 |
|
||||
| `web/public/111favicon.png` | 未确认用途 |
|
||||
| `docs/havline-brand-rename-plan.md` | 本方案文档本身 |
|
||||
| `docs/havline-logo-preview.html` | 第一版 Logo 预览 |
|
||||
| `docs/havline-logo-v2-preview.html` | 第二版 Logo 预览 |
|
||||
| `cmd/fonu/web/dist/**` | 生成目录,改名后由构建重新生成 |
|
||||
| `internal/frp/agentbin/fonu-agent-linux-amd64` | 生成/嵌入二进制,改名后重新编译 |
|
||||
| `internal/frp/agentbin/fonu-agent-linux-arm64` | 生成/嵌入二进制,改名后重新编译 |
|
||||
| `fonu-agent.exe` | 本地构建产物 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 最终执行顺序
|
||||
|
||||
推荐顺序:
|
||||
|
||||
1. 确认最终 Logo、GitHub、Gitea、Docker Hub 身份。
|
||||
2. 增加兼容层。
|
||||
3. 替换展示层。
|
||||
4. 替换 CSS 与组件。
|
||||
5. 替换 Go module 与 cmd 目录。
|
||||
6. 替换 Docker、Compose、Updater。
|
||||
7. 替换 Agent 远端安装与探测逻辑。
|
||||
8. 全量迁移文档。
|
||||
9. 跑完整回归。
|
||||
10. 保留兼容层一个版本。
|
||||
11. 删除旧兼容代码与旧资源。
|
||||
|
||||
---
|
||||
|
||||
## 12. 验收 grep
|
||||
|
||||
改名过程中可重复执行:
|
||||
|
||||
```powershell
|
||||
rg --hidden -in "fonu" `
|
||||
--glob '!web/node_modules/**' `
|
||||
--glob '!web/dist/**' `
|
||||
--glob '!cmd/havline/web/dist/**' `
|
||||
--glob '!.git/**' `
|
||||
--glob '!output/**' `
|
||||
--glob '!.playwright-cli/**'
|
||||
```
|
||||
|
||||
分类验收:
|
||||
|
||||
```powershell
|
||||
rg -n "FONU_"
|
||||
rg -n "fonu\.db|fonu_session|fonu-theme"
|
||||
rg -n "fonu-agent|fonu-probe|/etc/fonu|/var/lib/fonu"
|
||||
rg -n "github\.com/fonu/fonu|O96u/Fonu|/fonu:|fonu-data"
|
||||
rg -n -- "--fonu-"
|
||||
```
|
||||
|
||||
最终目标:
|
||||
|
||||
1. 源码中不再出现旧品牌展示文案。
|
||||
2. 运行时代码不再依赖旧环境变量、旧 Cookie、旧数据库文件名。
|
||||
3. 兼容层只保留明确记录的旧值 fallback。
|
||||
4. 文档历史记录不改写,只新增迁移说明。
|
||||
5. 新安装、旧数据升级、旧 Agent 升级、旧 Compose 回滚四条路径全部通过。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,905 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3C/svg%3E">
|
||||
<title>Havline Logo V2 概念预览</title>
|
||||
<style>
|
||||
:root {
|
||||
--page: #06121e;
|
||||
--panel: #0b1c2b;
|
||||
--panel-soft: #10283a;
|
||||
--text: #f2fbfa;
|
||||
--muted: #8daab6;
|
||||
--line: rgba(235, 255, 252, 0.13);
|
||||
--line-strong: rgba(235, 255, 252, 0.24);
|
||||
--blue: #2fa9ff;
|
||||
--mint: #63f0d0;
|
||||
--gold: #ffb15a;
|
||||
--radius: 8px;
|
||||
--shadow: 0 22px 70px rgba(0, 0, 0, 0.28);
|
||||
--font: "Segoe UI", "Microsoft YaHei", "PingFang SC", system-ui, sans-serif;
|
||||
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
--page: #eef5f6;
|
||||
--panel: #ffffff;
|
||||
--panel-soft: #e4f0f1;
|
||||
--text: #0a2535;
|
||||
--muted: #58717b;
|
||||
--line: rgba(10, 37, 53, 0.12);
|
||||
--line-strong: rgba(10, 37, 53, 0.21);
|
||||
--shadow: 0 22px 70px rgba(20, 55, 70, 0.13);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--page);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.sprite {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
min-height: 70px;
|
||||
padding: 14px clamp(18px, 4vw, 56px);
|
||||
background: color-mix(in srgb, var(--page) 92%, transparent);
|
||||
border-bottom: 1px solid var(--line);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.topbar__brand,
|
||||
.topbar__actions,
|
||||
.inline-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.topbar__mark {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.topbar__title {
|
||||
display: grid;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.topbar__title strong {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.topbar__title span {
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.control {
|
||||
min-height: 38px;
|
||||
padding: 0 14px;
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.control:hover,
|
||||
.control[aria-pressed="true"] {
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.control--primary {
|
||||
color: #04131d;
|
||||
background: linear-gradient(135deg, var(--blue), var(--mint));
|
||||
border-color: transparent;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
width: min(1160px, calc(100% - 36px));
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.92fr) minmax(380px, 1.08fr);
|
||||
gap: clamp(28px, 5vw, 72px);
|
||||
align-items: center;
|
||||
min-height: calc(100vh - 70px);
|
||||
padding: 70px 0;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 14px;
|
||||
color: var(--mint);
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 12px;
|
||||
font-size: clamp(52px, 8vw, 92px);
|
||||
line-height: 0.94;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin-bottom: 18px;
|
||||
font-size: clamp(19px, 2.3vw, 29px);
|
||||
font-weight: 600;
|
||||
line-height: 1.28;
|
||||
}
|
||||
|
||||
.copy {
|
||||
max-width: 620px;
|
||||
margin-bottom: 22px;
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 10px;
|
||||
color: var(--mint);
|
||||
background: color-mix(in srgb, var(--mint) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--mint) 28%, transparent);
|
||||
border-radius: 999px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 560px;
|
||||
padding: 48px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(to right, color-mix(in srgb, var(--line) 62%, transparent) 1px, transparent 1px) 0 0 / 36px 36px,
|
||||
linear-gradient(to bottom, color-mix(in srgb, var(--line) 62%, transparent) 1px, transparent 1px) 0 0 / 36px 36px,
|
||||
color-mix(in srgb, var(--panel) 95%, #071a29);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.stage::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 18px;
|
||||
border: 1px solid color-mix(in srgb, var(--mint) 17%, transparent);
|
||||
border-radius: var(--radius);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-lockup {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(100%, 570px);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 82px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.section__head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.7fr) minmax(0, 1.3fr);
|
||||
gap: 28px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section__head h2 {
|
||||
margin-bottom: 0;
|
||||
font-size: clamp(27px, 4vw, 44px);
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.section__head p {
|
||||
max-width: 680px;
|
||||
margin-bottom: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.fix-grid,
|
||||
.concept-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.fix-card,
|
||||
.concept-card,
|
||||
.preview-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.fix-card {
|
||||
min-height: 200px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.fix-card__index {
|
||||
display: block;
|
||||
margin-bottom: 42px;
|
||||
color: var(--mint);
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.fix-card h3 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.fix-card p {
|
||||
margin-bottom: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.compare {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.compare-card {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 360px;
|
||||
padding: 34px;
|
||||
background: #f4fbfb;
|
||||
border: 1px solid rgba(7, 34, 50, 0.12);
|
||||
border-radius: var(--radius);
|
||||
color: #092638;
|
||||
}
|
||||
|
||||
.compare-card--dark {
|
||||
color: var(--text);
|
||||
background: #071a29;
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.compare-card__label {
|
||||
align-self: end;
|
||||
margin-top: 22px;
|
||||
color: inherit;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.1em;
|
||||
opacity: 0.62;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mark-large {
|
||||
width: min(100%, 190px);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.concept-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.concept-card {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 340px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.concept-card__mark {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.concept-card__copy {
|
||||
align-self: end;
|
||||
margin-top: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.concept-card__copy h3 {
|
||||
margin-bottom: 5px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.concept-card__copy p {
|
||||
margin-bottom: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.concept-card--recommended {
|
||||
border-color: color-mix(in srgb, var(--mint) 42%, transparent);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--mint) 12%, transparent);
|
||||
}
|
||||
|
||||
.sidebar-preview {
|
||||
display: grid;
|
||||
grid-template-columns: 300px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sidebar-card {
|
||||
width: 300px;
|
||||
padding: 18px 0;
|
||||
color: #0b2735;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(12, 43, 58, 0.13);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 18px 18px;
|
||||
border-bottom: 1px solid rgba(12, 43, 58, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-brand__mark {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sidebar-brand__name {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sidebar-brand__tagline {
|
||||
color: #66808b;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 10px 12px 0;
|
||||
padding: 10px 14px;
|
||||
color: #2a4a57;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sidebar-item--active {
|
||||
color: #168f75;
|
||||
background: #e5f6f1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-dot {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border: 2px solid currentColor;
|
||||
border-radius: 5px;
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.sidebar-note {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
padding: 34px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.sidebar-note h3 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.sidebar-note p {
|
||||
margin-bottom: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.size-row {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: 28px;
|
||||
flex-wrap: wrap;
|
||||
padding: 34px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.size-sample {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
justify-items: center;
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.size-sample svg {
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.source {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
padding: 18px;
|
||||
background: #04101a;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
pre {
|
||||
max-height: 430px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
color: #b9dbe7;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.footnote {
|
||||
padding: 34px 0 56px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.hero,
|
||||
.section__head,
|
||||
.sidebar-preview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.fix-grid,
|
||||
.concept-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.topbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topbar__actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.control {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.stage {
|
||||
min-height: 390px;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.fix-grid,
|
||||
.concept-grid,
|
||||
.compare {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.source {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg class="sprite" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<linearGradient id="v2-bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#0f506d"></stop>
|
||||
<stop offset="1" stop-color="#061827"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="v2-ink" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#f6fffd"></stop>
|
||||
<stop offset="1" stop-color="#c9f5ed"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="v2-line" x1="0" y1="1" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#299cff"></stop>
|
||||
<stop offset="1" stop-color="#68f3d2"></stop>
|
||||
</linearGradient>
|
||||
<symbol id="mark-v2" viewBox="0 0 96 96">
|
||||
<rect x="4" y="4" width="88" height="88" rx="22" fill="url(#v2-bg)"></rect>
|
||||
<rect x="4.8" y="4.8" width="86.4" height="86.4" rx="21.2" fill="none" stroke="rgba(242,255,252,.14)" stroke-width="1.6"></rect>
|
||||
<path d="M17 62C29 52 37 68 47 58C57 48 66 52 79 43" stroke="url(#v2-line)" stroke-width="6.5" fill="none" stroke-linecap="round"></path>
|
||||
<rect x="28" y="24" width="11" height="48" rx="5.5" fill="url(#v2-ink)"></rect>
|
||||
<rect x="57" y="24" width="11" height="48" rx="5.5" fill="url(#v2-ink)"></rect>
|
||||
</symbol>
|
||||
<symbol id="mark-gate" viewBox="0 0 96 96">
|
||||
<rect x="4" y="4" width="88" height="88" rx="22" fill="url(#v2-bg)"></rect>
|
||||
<rect x="4.8" y="4.8" width="86.4" height="86.4" rx="21.2" fill="none" stroke="rgba(242,255,252,.14)" stroke-width="1.6"></rect>
|
||||
<path d="M40 24C32 24 28 31 28 48C28 65 32 72 40 72" stroke="url(#v2-ink)" stroke-width="10" fill="none" stroke-linecap="round"></path>
|
||||
<path d="M56 24C64 24 68 31 68 48C68 65 64 72 56 72" stroke="url(#v2-ink)" stroke-width="10" fill="none" stroke-linecap="round"></path>
|
||||
<path d="M34 59C43 51 54 45 63 37" stroke="url(#v2-line)" stroke-width="7" fill="none" stroke-linecap="round"></path>
|
||||
</symbol>
|
||||
<symbol id="mark-lower" viewBox="0 0 96 96">
|
||||
<rect x="4" y="4" width="88" height="88" rx="22" fill="url(#v2-bg)"></rect>
|
||||
<rect x="4.8" y="4.8" width="86.4" height="86.4" rx="21.2" fill="none" stroke="rgba(242,255,252,.14)" stroke-width="1.6"></rect>
|
||||
<path d="M31 23V73" stroke="url(#v2-ink)" stroke-width="11" stroke-linecap="round"></path>
|
||||
<path d="M31 51C31 36 40 29 50 29C60 29 66 37 66 51V73" stroke="url(#v2-ink)" stroke-width="11" fill="none" stroke-linecap="round"></path>
|
||||
<circle cx="49" cy="51" r="5" fill="#68f3d2"></circle>
|
||||
<path d="M39 62C44 55 54 55 58 62" stroke="url(#v2-line)" stroke-width="5" fill="none" stroke-linecap="round"></path>
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<header class="topbar">
|
||||
<div class="topbar__brand">
|
||||
<svg class="topbar__mark" viewBox="0 0 96 96" aria-label="Havline V2 标志">
|
||||
<use href="#mark-v2"></use>
|
||||
</svg>
|
||||
<div class="topbar__title">
|
||||
<strong>Havline V2</strong>
|
||||
<span>Refined mark / harbor gate</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topbar__actions">
|
||||
<button class="control" type="button" data-theme-set="dark" aria-pressed="true">深海</button>
|
||||
<button class="control" type="button" data-theme-set="light" aria-pressed="false">白昼</button>
|
||||
<button class="control control--primary" type="button" id="download-v2">下载 SVG</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="wrap hero">
|
||||
<div>
|
||||
<p class="eyebrow">Logo evolution / 02</p>
|
||||
<h1>Havline</h1>
|
||||
<p class="lead">从“一个 H”变成“一个港湾入口”。</p>
|
||||
<p class="copy">V2 保留 H 的识别骨架,但不再用两个端点圆点做连接。线路改为从 H 后方穿过的连续波浪,视觉上像一条服务路径穿过港湾入口,也更适合侧栏的小尺寸使用。</p>
|
||||
<div class="inline-row">
|
||||
<span class="pill">NO EYES / CONTINUOUS ROUTE</span>
|
||||
<span class="pill">40PX SIDEBAR SAFE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stage">
|
||||
<div class="hero-lockup">
|
||||
<svg viewBox="0 0 560 140" role="img" aria-labelledby="hero-v2-title hero-v2-desc">
|
||||
<title id="hero-v2-title">Havline V2 横向标志</title>
|
||||
<desc id="hero-v2-desc">左侧为 Havline V2 标志,右侧为 havline 字体组合。</desc>
|
||||
<use href="#mark-v2" x="0" y="10" width="120" height="120"></use>
|
||||
<text x="146" y="67" fill="currentColor" font-family="Segoe UI, Microsoft YaHei, sans-serif" font-size="49" font-weight="700" letter-spacing="0.4">havline</text>
|
||||
<text x="148" y="94" fill="currentColor" font-family="SFMono-Regular, Consolas, monospace" font-size="12" letter-spacing="1.5" opacity="0.62">SAFE LINE FOR EVERY SERVICE</text>
|
||||
<line x1="148" y1="110" x2="525" y2="110" stroke="currentColor" stroke-width="1" opacity="0.16"></line>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section__head">
|
||||
<h2>这版改了什么</h2>
|
||||
<p>问题不在 H,而在连接方式。旧版的端点圆点在 40px 侧栏里会形成类似“眼睛”的视觉焦点,抢掉了 H 和线路的主次关系。V2 让线路后退,保留一个干净的入口。</p>
|
||||
</div>
|
||||
<div class="fix-grid">
|
||||
<article class="fix-card">
|
||||
<span class="fix-card__index">01 / REMOVE</span>
|
||||
<h3>去掉端点圆点</h3>
|
||||
<p>不再在左右立柱上放节点,避免小尺寸下像双眼或天线,让 H 的主体轮廓更干净。</p>
|
||||
</article>
|
||||
<article class="fix-card">
|
||||
<span class="fix-card__index">02 / LAYER</span>
|
||||
<h3>线路退到 H 后方</h3>
|
||||
<p>波浪线路从双柱背后穿过,只在中间形成连接,保留“服务穿过港湾入口”的叙事。</p>
|
||||
</article>
|
||||
<article class="fix-card">
|
||||
<span class="fix-card__index">03 / FLOW</span>
|
||||
<h3>改用连续波浪</h3>
|
||||
<p>斜线改成更平滑的水波路径,既有链路感,也接近港湾与水面意象。</p>
|
||||
</article>
|
||||
<article class="fix-card">
|
||||
<span class="fix-card__index">04 / SIDEBAR</span>
|
||||
<h3>为 40px 优化</h3>
|
||||
<p>主结构只保留两根立柱和一条线路,缩放后仍能读出 H,不依赖额外细节。</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section__head">
|
||||
<h2>前后对比</h2>
|
||||
<p>左侧是上一版,右侧是 V2。对比重点不是“更复杂”,而是让标志在侧栏尺寸下更稳定、更耐看。</p>
|
||||
</div>
|
||||
<div class="compare">
|
||||
<article class="compare-card">
|
||||
<svg class="mark-large" viewBox="0 0 96 96" aria-label="旧版 Havline 标志">
|
||||
<rect x="4" y="4" width="88" height="88" rx="22" fill="url(#v2-bg)"></rect>
|
||||
<rect x="4.8" y="4.8" width="86.4" height="86.4" rx="21.2" fill="none" stroke="rgba(242,255,252,.13)" stroke-width="1.6"></rect>
|
||||
<path d="M31 25.5V70.5" stroke="#f2fffc" stroke-width="11" stroke-linecap="round"></path>
|
||||
<path d="M65 25.5V70.5" stroke="#f2fffc" stroke-width="11" stroke-linecap="round"></path>
|
||||
<path d="M31 59C39 59 42.5 49 49.5 49C56.5 49 57.5 37 65 37" stroke="url(#v2-line)" stroke-width="7" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<circle cx="31" cy="59" r="5" fill="#2fa9ff" stroke="#061827" stroke-width="2.2"></circle>
|
||||
<circle cx="65" cy="37" r="5" fill="#63f0d0" stroke="#061827" stroke-width="2.2"></circle>
|
||||
</svg>
|
||||
<span class="compare-card__label">V1 / endpoint nodes</span>
|
||||
</article>
|
||||
<article class="compare-card compare-card--dark">
|
||||
<svg class="mark-large" viewBox="0 0 96 96" aria-label="新版 Havline V2 标志">
|
||||
<use href="#mark-v2"></use>
|
||||
</svg>
|
||||
<span class="compare-card__label">V2 / continuous route</span>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section__head">
|
||||
<h2>三个方向</h2>
|
||||
<p>推荐主案是左侧的“连续路线 H”。另外两个方向可以用于周边、活动或后续品牌迭代,不适合直接替代主标志。</p>
|
||||
</div>
|
||||
<div class="concept-grid">
|
||||
<article class="concept-card concept-card--recommended">
|
||||
<svg class="concept-card__mark" viewBox="0 0 96 96" aria-label="连续路线 H">
|
||||
<use href="#mark-v2"></use>
|
||||
</svg>
|
||||
<div class="concept-card__copy">
|
||||
<h3>连续路线 H</h3>
|
||||
<p>推荐主案</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="concept-card">
|
||||
<svg class="concept-card__mark" viewBox="0 0 96 96" aria-label="港湾拱门">
|
||||
<use href="#mark-gate"></use>
|
||||
</svg>
|
||||
<div class="concept-card__copy">
|
||||
<h3>港湾拱门</h3>
|
||||
<p>更抽象、画面感更强</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="concept-card">
|
||||
<svg class="concept-card__mark" viewBox="0 0 96 96" aria-label="小写 h 路线">
|
||||
<use href="#mark-lower"></use>
|
||||
</svg>
|
||||
<div class="concept-card__copy">
|
||||
<h3>小写 h 路线</h3>
|
||||
<p>更亲和、更偏产品化</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section__head">
|
||||
<h2>侧栏模拟</h2>
|
||||
<p>在当前的 40px 品牌区里,V2 不再出现两个高亮圆点,视觉重心重新回到 H 和品牌名称上。</p>
|
||||
</div>
|
||||
<div class="sidebar-preview">
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-brand">
|
||||
<svg class="sidebar-brand__mark" viewBox="0 0 96 96" aria-label="Havline V2 侧栏标志">
|
||||
<use href="#mark-v2"></use>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="sidebar-brand__name">Havline</div>
|
||||
<div class="sidebar-brand__tagline">让 NAS 访问更简单</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-item sidebar-item--active">
|
||||
<span class="sidebar-dot"></span>
|
||||
仪表盘
|
||||
</div>
|
||||
<div class="sidebar-item">
|
||||
<span class="sidebar-dot"></span>
|
||||
DDNS
|
||||
</div>
|
||||
<div class="sidebar-item">
|
||||
<span class="sidebar-dot"></span>
|
||||
证书管理
|
||||
</div>
|
||||
<div class="sidebar-item">
|
||||
<span class="sidebar-dot"></span>
|
||||
反向代理
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-note">
|
||||
<h3>更像“连接”,不像“表情”</h3>
|
||||
<p>旧版的两个端点圆点在侧栏压缩后过于醒目。V2 取消了端点节点,把线路收进 H 的结构里,因此远看是一个稳定的入口符号,近看才读到服务路径。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section__head">
|
||||
<h2>尺寸检查</h2>
|
||||
<p>主标志建议最小使用 16px。侧栏品牌区 40px,应用图标 64px,登录页主标志 120px 以上。</p>
|
||||
</div>
|
||||
<div class="size-row">
|
||||
<span class="size-sample" style="--size:16px">
|
||||
<svg viewBox="0 0 96 96"><use href="#mark-v2"></use></svg>
|
||||
16
|
||||
</span>
|
||||
<span class="size-sample" style="--size:24px">
|
||||
<svg viewBox="0 0 96 96"><use href="#mark-v2"></use></svg>
|
||||
24
|
||||
</span>
|
||||
<span class="size-sample" style="--size:40px">
|
||||
<svg viewBox="0 0 96 96"><use href="#mark-v2"></use></svg>
|
||||
40
|
||||
</span>
|
||||
<span class="size-sample" style="--size:64px">
|
||||
<svg viewBox="0 0 96 96"><use href="#mark-v2"></use></svg>
|
||||
64
|
||||
</span>
|
||||
<span class="size-sample" style="--size:96px">
|
||||
<svg viewBox="0 0 96 96"><use href="#mark-v2"></use></svg>
|
||||
96
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section__head">
|
||||
<h2>SVG 源文件</h2>
|
||||
<p>这是 V2 主案的可下载 SVG,后续可以直接替换侧栏中的现有标志,不需要再调整图片尺寸。</p>
|
||||
</div>
|
||||
<div class="source">
|
||||
<pre id="v2-code"></pre>
|
||||
<div class="inline-row">
|
||||
<button class="control control--primary" type="button" id="copy-v2">复制 SVG</button>
|
||||
<button class="control" type="button" id="download-v2-2">下载 SVG</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="wrap footnote">
|
||||
Havline V2 · 主案:连续路线 H · 设计目标:更干净、更有港湾入口感、在 40px 侧栏里不产生“表情”错觉。
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const root = document.documentElement;
|
||||
const themeButtons = document.querySelectorAll("[data-theme-set]");
|
||||
|
||||
function setTheme(theme) {
|
||||
root.dataset.theme = theme;
|
||||
themeButtons.forEach((button) => {
|
||||
button.setAttribute("aria-pressed", String(button.dataset.themeSet === theme));
|
||||
});
|
||||
}
|
||||
|
||||
themeButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => setTheme(button.dataset.themeSet));
|
||||
});
|
||||
|
||||
const exportSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-label="Havline logo v2">
|
||||
<defs>
|
||||
<linearGradient id="havline-v2-bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#0f506d"/>
|
||||
<stop offset="1" stop-color="#061827"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="havline-v2-ink" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#f6fffd"/>
|
||||
<stop offset="1" stop-color="#c9f5ed"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="havline-v2-line" x1="0" y1="1" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#299cff"/>
|
||||
<stop offset="1" stop-color="#68f3d2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="88" height="88" rx="22" fill="url(#havline-v2-bg)"/>
|
||||
<rect x="4.8" y="4.8" width="86.4" height="86.4" rx="21.2" fill="none" stroke="rgba(242,255,252,.14)" stroke-width="1.6"/>
|
||||
<path d="M17 62C29 52 37 68 47 58C57 48 66 52 79 43" stroke="url(#havline-v2-line)" stroke-width="6.5" fill="none" stroke-linecap="round"/>
|
||||
<rect x="28" y="24" width="11" height="48" rx="5.5" fill="url(#havline-v2-ink)"/>
|
||||
<rect x="57" y="24" width="11" height="48" rx="5.5" fill="url(#havline-v2-ink)"/>
|
||||
</svg>`;
|
||||
|
||||
document.getElementById("v2-code").textContent = exportSvg;
|
||||
|
||||
function downloadV2() {
|
||||
const blob = new Blob([exportSvg], { type: "image/svg+xml;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "havline-logo-v2.svg";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
document.getElementById("download-v2").addEventListener("click", downloadV2);
|
||||
document.getElementById("download-v2-2").addEventListener("click", downloadV2);
|
||||
|
||||
document.getElementById("copy-v2").addEventListener("click", async (event) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(exportSvg);
|
||||
event.currentTarget.textContent = "已复制";
|
||||
setTimeout(() => {
|
||||
event.currentTarget.textContent = "复制 SVG";
|
||||
}, 1400);
|
||||
} catch {
|
||||
event.currentTarget.textContent = "复制失败";
|
||||
setTimeout(() => {
|
||||
event.currentTarget.textContent = "复制 SVG";
|
||||
}, 1400);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+10
-10
@@ -16,12 +16,12 @@
|
||||
| 框架 | Vue 3 + `<script setup lang="ts">` + Vite 6 |
|
||||
| 组件库 | **naive-ui**(`n-select` `n-button` `n-modal` `n-form` `n-switch` `n-input` `n-input-number` `n-tag`) |
|
||||
| 图标 | `@vicons/ionicons5`(本页目前未用图标) |
|
||||
| 令牌系统 | `web/src/styles/variables.css`:`:root` + `html.dark` 双主题,`--fonu-*` 前缀 |
|
||||
| 共享组件 | `PageHeader` `FonuCard` `LoadError` `EmptyState` `StatusBadge`(本页**未用** `StatusBadge`) |
|
||||
| 令牌系统 | `web/src/styles/variables.css`:`:root` + `html.dark` 双主题,`--havline-*` 前缀 |
|
||||
| 共享组件 | `PageHeader` `HavlineCard` `LoadError` `EmptyState` `StatusBadge`(本页**未用** `StatusBadge`) |
|
||||
| 工具 | `utils/format.ts`(已用 `formatBytes` / `formatRate`) |
|
||||
| 同族页面 | `ProxyView`(本地反代,交互参照)、`AgentView`(已按 v2 重构,同为公网侧) |
|
||||
|
||||
**结论**:沿用既有体系(naive-ui + `--fonu-*` + 共享组件),**不引入**第二套框架/令牌。
|
||||
**结论**:沿用既有体系(naive-ui + `--havline-*` + 共享组件),**不引入**第二套框架/令牌。
|
||||
|
||||
---
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
| 2 | **没有统计区** | 全页无 `.stats-row` / `.stat-card`,而数据齐全(规则数、已部署数、健康通过数、流量、连接数) | 与所有标准页(Dashboard/Ddns/Certificates/Proxy)不一致,一眼看不到全局 |
|
||||
| 3 | **硬编码颜色 6 处** | `#f0c36d`、`#e79b9b`、`#dc2626`、`#16a34a`、`rgba(240,195,109,.12)`、`rgba(34,197,94,.12)` | 违反令牌规则;暗色主题下对比度未验证 |
|
||||
| 4 | **自造状态组件** | `nginxState()` 返回自造 `n-tag`;`healthBadges()` 自造 `.health-badge/.health-ok/.health-bad` | 与 `StatusBadge` + `utils/status.ts` 语义分叉,同类状态在不同页面长得不一样 |
|
||||
| 5 | **「使用说明」整卡占位** | 一整张 `FonuCard` 只有一段 `form-hint` | 占掉整行网格,信息密度低(`AgentView` 已把这类内容改成 `n-alert` + 入口行) |
|
||||
| 5 | **「使用说明」整卡占位** | 一整张 `HavlineCard` 只有一段 `form-hint` | 占掉整行网格,信息密度低(`AgentView` 已把这类内容改成 `n-alert` + 入口行) |
|
||||
| 6 | **空态与折叠区未令牌化** | 自造 `.empty-route` div;`<details class="raw-routes">` 原生折叠 | 空态应为 `EmptyState`;原生折叠样式不随主题 |
|
||||
| 7 | **loading 粒度粗** | 单个 `refreshing` 同时驱动「刷新」「重新检测」与轮询 | 轮询时按钮闪 loading;无法区分"在部署"与"在刷新" |
|
||||
| 8 | **切换服务端不重置** | `watch(selectedId)` 不存在 | 上一台的健康/流量/agent 状态残留在新服务端视图里(串台) |
|
||||
@@ -95,11 +95,11 @@
|
||||
- **Layout**:状态提示 → 统计区(4 列)→ 环境与 Nginx(2 列)→ 规则台账(整宽)→ 入口行;左对齐,无居中、无渐变。
|
||||
- **Signature**:**规则行左侧的「四段健康徽标串」**(DNS · 证书 · 隧道 · 服务),一眼扫完所有规则的健康。
|
||||
|
||||
**Color**:沿用 5 个语义令牌(同 `AgentView` v2):`--fonu-brand` 正常 / `--fonu-warning` 待处理 / `--fonu-error` 需人工干预 / 中性 `--fonu-text-*` / `--fonu-info` 引导。**页面零硬编码色值**(现状 6 处全部替换:`#16a34a`→`--fonu-brand`、`#dc2626`→`--fonu-error`、`#f0c36d`→`--fonu-warning`、`#e79b9b`→`--fonu-error`、两处 `rgba(...)`→`--fonu-brand-soft` / `--fonu-warning` 的 soft 变体或 `--fonu-bg-muted`)。
|
||||
**Color**:沿用 5 个语义令牌(同 `AgentView` v2):`--havline-brand` 正常 / `--havline-warning` 待处理 / `--havline-error` 需人工干预 / 中性 `--havline-text-*` / `--havline-info` 引导。**页面零硬编码色值**(现状 6 处全部替换:`#16a34a`→`--havline-brand`、`#dc2626`→`--havline-error`、`#f0c36d`→`--havline-warning`、`#e79b9b`→`--havline-error`、两处 `rgba(...)`→`--havline-brand-soft` / `--havline-warning` 的 soft 变体或 `--havline-bg-muted`)。
|
||||
|
||||
**Type**:沿用 `--fonu-font` / `--fonu-mono`;页内 4 档字号(12 / 12.5 / 13 / 18-30)+ 3 档字重,与 `CertificatesView` 统计卡规格一致。
|
||||
**Type**:沿用 `--havline-font` / `--havline-mono`;页内 4 档字号(12 / 12.5 / 13 / 18-30)+ 3 档字重,与 `CertificatesView` 统计卡规格一致。
|
||||
|
||||
**Spacing & Radius**:只用 `--fonu-space-1…6` 与 `--fonu-radius(-sm)`、`--fonu-shadow`;`style="width: 220px"` → `.proxy-remote-select`。
|
||||
**Spacing & Radius**:只用 `--havline-space-1…6` 与 `--havline-radius(-sm)`、`--havline-shadow`;`style="width: 220px"` → `.proxy-remote-select`。
|
||||
|
||||
---
|
||||
|
||||
@@ -187,10 +187,10 @@ timer = window.setInterval(() => {
|
||||
|
||||
## 十、验收清单
|
||||
|
||||
- [ ] 复用项目真实栈与令牌(naive-ui + `--fonu-*` + `PageHeader/FonuCard/LoadError/EmptyState/StatusBadge`),未新增第二套
|
||||
- [ ] 复用项目真实栈与令牌(naive-ui + `--havline-*` + `PageHeader/HavlineCard/LoadError/EmptyState/StatusBadge`),未新增第二套
|
||||
- [ ] 单一设计方向(部署台账)落地;零硬编码色值;`style="width: …"` 清零
|
||||
- [ ] 交互状态齐全:hover / pressed / focused(键盘实测)/ disabled / loading / empty / error
|
||||
- [ ] 无障碍:对比度 AA(亮+暗,重点验 `--fonu-brand` / `--fonu-warning` / `--fonu-error` 在 `--fonu-surface` 上)、键盘可达全部动作、装饰图标 `aria-hidden`、健康徽标有可读文本
|
||||
- [ ] 无障碍:对比度 AA(亮+暗,重点验 `--havline-brand` / `--havline-warning` / `--havline-error` 在 `--havline-surface` 上)、键盘可达全部动作、装饰图标 `aria-hidden`、健康徽标有可读文本
|
||||
- [ ] 最窄 320px 无横向溢出;≤760px 规则行纵向堆叠、动作按钮铺满且 ≥44px
|
||||
- [ ] **功能可见性:对照第三节 A–Q,每项有明确位置;#1 错误可见、#8 切换不串台、#7 loading 分粒度 三项缺陷已修**
|
||||
- [ ] 回归:部署/移除/保存部署设置三条路径真机验证;`vue-tsc` 0 错误 + `vite build` 成功
|
||||
@@ -210,4 +210,4 @@ timer = window.setInterval(() => {
|
||||
合计约 **5.5–6 小时**,单文件改动(`ProxyRemoteView.vue`),**不涉及后端与 agent**。
|
||||
能力缺口 R(批量部署)与 S(配置回滚)不在本轮:R 可后续单独做(纯前端循环 + 进度),S 需 agent 侧新增历史接口。
|
||||
|
||||
验证:`npx vue-tsc --noEmit` + `npx vite build`,再同步 `web/dist → cmd/fonu/web/dist` 后 `go build ./...`。
|
||||
验证:`npx vue-tsc --noEmit` + `npx vite build`,再同步 `web/dist → cmd/havline/web/dist` 后 `go build ./...`。
|
||||
|
||||
+38
-38
@@ -1,4 +1,4 @@
|
||||
# Fonu Web 一键升级方案
|
||||
# Havline Web 一键升级方案
|
||||
|
||||
> 实施版本:v1.3.0
|
||||
> 适用范围:`docker-compose.yml`、`docker-compose.hub.yml`
|
||||
@@ -9,8 +9,8 @@
|
||||
在管理后台检测到新版本后,用户点击「一键升级」即可完成:
|
||||
|
||||
1. 读取远端最新版本;
|
||||
2. 构建新的 `fonu` 镜像;
|
||||
3. 只重建 `fonu` 容器;
|
||||
2. 构建新的 `havline` 镜像;
|
||||
3. 只重建 `havline` 容器;
|
||||
4. 等待 `/api/version` 返回目标版本;
|
||||
5. 失败时尝试回滚到升级前的镜像 tag;
|
||||
6. 把升级过程和结果返回给前端。
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
主程序容器**不挂载** `/var/run/docker.sock`。
|
||||
|
||||
只有 `fonu-updater` sidecar 持有:
|
||||
只有 `havline-updater` sidecar 持有:
|
||||
|
||||
- Docker Socket;
|
||||
- Docker CLI 与 Compose;
|
||||
@@ -30,16 +30,16 @@
|
||||
|
||||
```text
|
||||
浏览器
|
||||
→ fonu 后端 /api/system/update/*
|
||||
→ /run/fonu-updater/updater.sock
|
||||
→ fonu-updater
|
||||
→ havline 后端 /api/system/update/*
|
||||
→ /run/havline-updater/updater.sock
|
||||
→ havline-updater
|
||||
→ docker compose build/up
|
||||
```
|
||||
|
||||
这表示:
|
||||
|
||||
- 管理后台被攻破时,攻击者仍需通过 sidecar 的固定接口才能触发升级;
|
||||
- sidecar 只接受固定的 Compose 文件名和固定的 `fonu` 服务名;
|
||||
- sidecar 只接受固定的 Compose 文件名和固定的 `havline` 服务名;
|
||||
- 不提供任意命令执行接口;
|
||||
- 不支持自定义 shell 命令。
|
||||
|
||||
@@ -53,13 +53,13 @@
|
||||
两种模式都会:
|
||||
|
||||
- 读取目标版本;
|
||||
- 构建 `fonu` 镜像;
|
||||
- 执行 `docker compose up -d --no-deps --force-recreate fonu`;
|
||||
- 构建 `havline` 镜像;
|
||||
- 执行 `docker compose up -d --no-deps --force-recreate havline`;
|
||||
- 轮询 `/api/version` 验证升级结果。
|
||||
|
||||
## 4. 首次部署
|
||||
|
||||
一键升级依赖 `fonu-updater` 已经运行。
|
||||
一键升级依赖 `havline-updater` 已经运行。
|
||||
|
||||
旧版本升级到 v1.3.0 时,需要先手动执行一次:
|
||||
|
||||
@@ -79,22 +79,22 @@ docker compose -f docker-compose.hub.yml up -d --build
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `FONU_UPDATER_SOCKET` | `/run/fonu-updater/updater.sock` | 与 sidecar 通信的 Unix Socket |
|
||||
| `HAVLINE_UPDATER_SOCKET` | `/run/havline-updater/updater.sock` | 与 sidecar 通信的 Unix Socket |
|
||||
|
||||
### updater sidecar
|
||||
|
||||
| 变量 | 说明 |
|
||||
| --- | --- |
|
||||
| `FONU_UPDATER_SOCKET` | sidecar 监听的 Unix Socket |
|
||||
| `FONU_UPDATE_PROJECT_DIR` | Compose 项目目录,容器内为 `/workspace` |
|
||||
| `FONU_UPDATE_COMPOSE_FILE` | 只允许 `docker-compose.yml`、`docker-compose.hub.yml`、`docker-compose.host.yml` |
|
||||
| `FONU_UPDATE_SERVICE` | 要重建的服务,默认 `fonu` |
|
||||
| `FONU_UPDATE_MODE` | `local` 或 `git` |
|
||||
| `FONU_UPDATE_REPO` | `git` 模式下的仓库地址 |
|
||||
| `FONU_UPDATE_BRANCH` | `git` 模式下的分支,默认 `main` |
|
||||
| `FONU_UPDATE_APP_URL` | 健康检查地址,默认 `http://fonu:6893` |
|
||||
| `HAVLINE_UPDATER_SOCKET` | sidecar 监听的 Unix Socket |
|
||||
| `HAVLINE_UPDATE_PROJECT_DIR` | Compose 项目目录,容器内为 `/workspace` |
|
||||
| `HAVLINE_UPDATE_COMPOSE_FILE` | 只允许 `docker-compose.yml`、`docker-compose.hub.yml`、`docker-compose.host.yml` |
|
||||
| `HAVLINE_UPDATE_SERVICE` | 要重建的服务,默认 `havline` |
|
||||
| `HAVLINE_UPDATE_MODE` | `local` 或 `git` |
|
||||
| `HAVLINE_UPDATE_REPO` | `git` 模式下的仓库地址 |
|
||||
| `HAVLINE_UPDATE_BRANCH` | `git` 模式下的分支,默认 `main` |
|
||||
| `HAVLINE_UPDATE_APP_URL` | 健康检查地址,默认 `http://havline:6893` |
|
||||
| `DOCKER_SOCK` | 宿主机 Docker Socket,默认 `/var/run/docker.sock` |
|
||||
| `FONU_UPDATE_HOST_DIR` | 宿主机上 Compose 文件所在目录;留空时自动从 Compose 容器标签发现,设置后作为显式覆盖 |
|
||||
| `HAVLINE_UPDATE_HOST_DIR` | 宿主机上 Compose 文件所在目录;留空时自动从 Compose 容器标签发现,设置后作为显式覆盖 |
|
||||
|
||||
## 6. 前端行为
|
||||
|
||||
@@ -119,7 +119,7 @@ docker compose -f docker-compose.hub.yml up -d --build
|
||||
升级前会记录当前容器镜像。升级后健康检查超时,sidecar 会尝试:
|
||||
|
||||
```bash
|
||||
FONU_VERSION=<上一个版本> docker compose up -d --no-deps --force-recreate fonu
|
||||
HAVLINE_VERSION=<上一个版本> docker compose up -d --no-deps --force-recreate havline
|
||||
```
|
||||
|
||||
回滚是否成功会写入升级日志。
|
||||
@@ -131,9 +131,9 @@ FONU_VERSION=<上一个版本> docker compose up -d --no-deps --force-recreate f
|
||||
检查:
|
||||
|
||||
1. 当前是否使用 `docker-compose.yml` 或 `docker-compose.hub.yml`;
|
||||
2. `fonu-updater` 容器是否在运行;
|
||||
3. `FONU_UPDATER_SOCKET` 是否在两侧指向同一路径;
|
||||
4. 共享卷 `fonu-updater-sock` 是否存在。
|
||||
2. `havline-updater` 容器是否在运行;
|
||||
3. `HAVLINE_UPDATER_SOCKET` 是否在两侧指向同一路径;
|
||||
4. 共享卷 `havline-updater-sock` 是否存在。
|
||||
|
||||
如果状态接口返回:
|
||||
|
||||
@@ -160,18 +160,18 @@ docker compose up -d --build
|
||||
|
||||
### 升级时报 `/workspace/docker-compose.hub.yml: no such file or directory`
|
||||
|
||||
v1.3.6 起 updater 会自动从 `fonu` 容器的 Compose 标签读取宿主机目录,并通过 helper 容器执行 Compose。如果自动发现仍失败,可以显式设置目录:
|
||||
v1.3.6 起 updater 会自动从 `havline` 容器的 Compose 标签读取宿主机目录,并通过 helper 容器执行 Compose。如果自动发现仍失败,可以显式设置目录:
|
||||
|
||||
先查询运行中 `fonu` 容器的 Compose 工作目录:
|
||||
先查询运行中 `havline` 容器的 Compose 工作目录:
|
||||
|
||||
```bash
|
||||
docker inspect fonu --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}'
|
||||
docker inspect havline --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}'
|
||||
```
|
||||
|
||||
把输出目录写入 Compose 同层 `.env` 作为覆盖:
|
||||
|
||||
```dotenv
|
||||
FONU_UPDATE_HOST_DIR=/实际/的/Compose/目录
|
||||
HAVLINE_UPDATE_HOST_DIR=/实际/的/Compose/目录
|
||||
```
|
||||
|
||||
然后只重建 updater:
|
||||
@@ -182,11 +182,11 @@ docker compose -f /实际/的/Compose/目录/docker-compose.hub.yml up -d --buil
|
||||
|
||||
状态接口会明确提示 Compose 文件缺失,修复挂载或设置覆盖后按钮才会重新显示。
|
||||
|
||||
### 报 `container name "/fonu" is already in use`
|
||||
### 报 `container name "/havline" is already in use`
|
||||
|
||||
这表示 Compose 没有识别到当前容器所属的原项目,可能把升级当成新项目并尝试新建网络与数据卷。v1.3.7 的更新版 updater 会读取现有容器的 `com.docker.compose.project` 标签,并显式复用原项目名。
|
||||
|
||||
如果现有 `fonu` 容器不是由 Compose 管理,updater 会拒绝升级,避免新建 `fonu_fonu-data` 等空数据卷。应先按 Compose 方式重新部署,再使用一键升级。
|
||||
如果现有 `havline` 容器不是由 Compose 管理,updater 会拒绝升级,避免新建 `havline_havline-data` 等空数据卷。应先按 Compose 方式重新部署,再使用一键升级。
|
||||
|
||||
### helper 命令里出现 `-v /workspace:/workspace`
|
||||
|
||||
@@ -197,7 +197,7 @@ docker compose -f /实际/的/Compose/目录/docker-compose.hub.yml up -d --buil
|
||||
检查 sidecar 容器内是否可以访问 Git 仓库:
|
||||
|
||||
```bash
|
||||
docker exec -it fonu-updater sh -lc 'git ls-remote http://45.207.223.190:10082/mrlee/-Fonu.git main'
|
||||
docker exec -it havline-updater sh -lc 'git ls-remote http://45.207.223.190:10082/mrlee/Havline.git main'
|
||||
```
|
||||
|
||||
### 升级卡在 `running`
|
||||
@@ -205,14 +205,14 @@ docker exec -it fonu-updater sh -lc 'git ls-remote http://45.207.223.190:10082/m
|
||||
查看 sidecar 日志:
|
||||
|
||||
```bash
|
||||
docker logs -f fonu-updater
|
||||
docker logs -f havline-updater
|
||||
```
|
||||
|
||||
以及 Compose 输出:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose logs --tail 100 fonu
|
||||
docker compose logs --tail 100 havline
|
||||
```
|
||||
|
||||
### 回滚失败
|
||||
@@ -220,13 +220,13 @@ docker compose logs --tail 100 fonu
|
||||
确认升级前的镜像 tag 仍然存在:
|
||||
|
||||
```bash
|
||||
docker images fonu
|
||||
docker images havline
|
||||
```
|
||||
|
||||
然后用明确的版本 tag 手动切回:
|
||||
|
||||
```bash
|
||||
FONU_VERSION=<旧版本> docker compose up -d --no-deps --force-recreate fonu
|
||||
HAVLINE_VERSION=<旧版本> docker compose up -d --no-deps --force-recreate havline
|
||||
```
|
||||
|
||||
## 9. 暂不支持
|
||||
@@ -234,13 +234,13 @@ FONU_VERSION=<旧版本> docker compose up -d --no-deps --force-recreate fonu
|
||||
- `docker-compose.host.yml`:需要确认 host 网络与端口占用后再设计;
|
||||
- 原生 systemd 安装:需要单独的受控升级 helper;
|
||||
- 回滚到远端不存在的镜像:需要额外发布流程;
|
||||
- 升级 `fonu-updater` 自身:当前只重建 `fonu` 服务,sidecar 更新仍需一次手动 Compose 操作。
|
||||
- 升级 `havline-updater` 自身:当前只重建 `havline` 服务,sidecar 更新仍需一次手动 Compose 操作。
|
||||
|
||||
## 10. 在线升级验证
|
||||
|
||||
示例页面用于验证升级结果。v1.9.0 的验证流程:
|
||||
|
||||
1. 确认当前线上实例为 v1.3.0,且 `fonu-updater` sidecar 已运行。
|
||||
1. 确认当前线上实例为 v1.3.0,且 `havline-updater` sidecar 已运行。
|
||||
2. 确认远端 `main` 分支的 `VERSION` 为 `1.9.0`。
|
||||
3. 在侧栏看到「一键升级」后点击并确认。
|
||||
4. 等待升级弹窗显示成功,页面自动刷新。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 上游同步方案:Fonu 1.0.2 → 1.0.5 对照与可借鉴项
|
||||
# 上游同步方案:Havline 1.0.2 → 1.0.5 对照与可借鉴项
|
||||
|
||||
> **时效说明(2026-09-24)**:文中对 `/frp` 页现状的描述基于 2026-09-23,已被 v1.2.6 的重构推翻(现为**左服务端列表 + 右规则表格**,服务端增删改已并入 `/frp-agent`)。
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
> 本文逐版本列出上游 1.0.3 / 1.0.4 / 1.0.5 的全部变更,并逐条核对**本项目代码现状**,
|
||||
> 给出「同步 / 已具备 / 不适用 / 待核对」的判定与落地建议。
|
||||
>
|
||||
> 上游来源:`https://github.com/O96u/Fonu`(CHANGELOG.md + 提交记录)
|
||||
> 核对方式:对本仓库 `f:\Fonu-main\Fonu-1.0.2` 做 grep / 文件树核对(证据写在每条的「本项目现状」里)
|
||||
> 上游来源:`https://github.com/O96u/Havline`(CHANGELOG.md + 提交记录)
|
||||
> 核对方式:对本仓库 `f:\Havline-main\Havline-1.0.2` 做 grep / 文件树核对(证据写在每条的「本项目现状」里)
|
||||
> 更新日期:2026-09-23
|
||||
|
||||
---
|
||||
@@ -91,7 +91,7 @@
|
||||
`AgentView.vue`(1 处,「配置查看」弹窗的复制)、
|
||||
`DdnsView.vue`(1 处)、`ProxyView.vue`(1 处)、
|
||||
归档文件 `FrpView-1.0.2.vue`(1 处,已弃用代码,可不动)。
|
||||
- 影响:Fonu 常用 `http://内网IP:6893` 访问,属**非安全上下文**,此时 `navigator.clipboard` 为 `undefined` → 复制功能直接失败(本项目现在只弹「复制失败」提示,用户无从下手)。
|
||||
- 影响:Havline 常用 `http://内网IP:6893` 访问,属**非安全上下文**,此时 `navigator.clipboard` 为 `undefined` → 复制功能直接失败(本项目现在只弹「复制失败」提示,用户无从下手)。
|
||||
- 建议:**P0 同步**。做法:新增 `web/src/utils/clipboard.ts`:
|
||||
```ts
|
||||
export async function copyText(text: string): Promise<boolean> {
|
||||
@@ -309,7 +309,7 @@ Nginx 的冲突规则是「**同端口 + 同 `server_name`**」:此时只在 r
|
||||
|
||||
1. **不按「同端口」判冲突**:同端口不同域名是合法虚拟主机,按端口报错会让「一个端口挂多个站点」
|
||||
这种最常见的用法无法保存。
|
||||
2. **不探测系统层面的端口占用**(`net.Listen` 试占、`ss -ltn`):Fonu 自己就是占用方(Nginx 监听 80/443),
|
||||
2. **不探测系统层面的端口占用**(`net.Listen` 试占、`ss -ltn`):Havline 自己就是占用方(Nginx 监听 80/443),
|
||||
且容器与宿主机视角不同,误报率极高。上游所指也是 Nginx 配置内的冲突,而非系统端口。
|
||||
|
||||
### 8.6 验收标准
|
||||
|
||||
Reference in New Issue
Block a user