diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aff01d4..74a7c88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # 顺序不能颠倒:cmd/fonu/main.go 用 //go:embed all:web/dist 嵌入前端, + # 顺序不能颠倒:cmd/havline/main.go 用 //go:embed all:web/dist 嵌入前端, # 干净检出时该目录不存在,go vet / go build 会直接失败。 - name: Setup Node uses: actions/setup-node@v4 @@ -39,8 +39,8 @@ jobs: - name: Sync web dist into embed directory run: | - rm -rf cmd/fonu/web/dist - cp -r web/dist cmd/fonu/web/dist + rm -rf cmd/havline/web/dist + cp -r web/dist cmd/havline/web/dist - name: Setup Go uses: actions/setup-go@v5 diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 553ee8e..c62c5e9 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -60,7 +60,7 @@ jobs: push: true build-args: | VERSION=${{ steps.version.outputs.version }} - tags: ${{ secrets.DOCKERHUB_USERNAME }}/fonu:${{ steps.version.outputs.version }}-${{ matrix.arch }} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/havline:${{ steps.version.outputs.version }}-${{ matrix.arch }} labels: | org.opencontainers.image.source=${{ github.repositoryUrl }} org.opencontainers.image.version=${{ steps.version.outputs.version }} @@ -91,7 +91,7 @@ jobs: - name: Create multi-arch manifest env: - IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/fonu + IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/havline VERSION: ${{ steps.version.outputs.version }} run: | if [ -z "${IMAGE}" ]; then diff --git a/.gitignore b/.gitignore index 2e7a2c9..1e225d7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ bin/ .data/ web/node_modules/ web/dist/ -cmd/fonu/web/dist/ +cmd/havline/web/dist/ dist/ *.db /data/ @@ -11,11 +11,15 @@ dist/ .env.local .DS_Store .cursor/ +.playwright-cli/ +output/ +web/public/111favicon.png +web/src/assets/brand/logo00.png # scripts/ 下的本地辅助脚本不入库,但随仓库分发的 agent 安装脚本需要跟踪 scripts/* !scripts/agent-install.sh !scripts/build-linux.ps1 -!scripts/fonu-install.sh +!scripts/havline-install.sh !scripts/upgrade.sh !scripts/upgrade-hub.sh # 本地压缩包与导出产物不入库(如 web/src.rar) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8768a58..deb873a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### 变更 + +- **品牌全量切换为 Havline**:项目显示名、Go module、命令目录、二进制、环境变量、数据库、Cookie、Docker/Compose、Agent、Updater、脚本和当前文档统一切换到 `Havline / havline / HAVLINE_` +- **AgentVersion 提升到 0.15.0**:Agent 安装路径、配置目录、数据目录、systemd unit、Nginx 托管标记和 API 服务标识全部切换到 `havline-agent` +- **历史文档保留原品牌名**:`CHANGELOG.md` 历史版本、旧品牌评估文档和带日期的历史分析报告保留原始名称,作为项目沿革和 GPL 来源记录 + ## [v1.9.0] - 2026-10-01 ### 新增 diff --git a/Dockerfile b/Dockerfile index 3c27dd2..2e130e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,13 +37,13 @@ COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod go mod download COPY . . COPY --from=version /VERSION ./VERSION -# 先交叉编译 fonu-agent 到 embed 目录(替换仓库中的占位文件),主程序构建时自动内嵌 -RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o internal/frp/agentbin/fonu-agent-linux-amd64 ./cmd/fonu-agent \ - && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -o internal/frp/agentbin/fonu-agent-linux-arm64 ./cmd/fonu-agent -COPY --from=web-builder /src/web/dist ./cmd/fonu/web/dist +# 先交叉编译 havline-agent 到 embed 目录(替换仓库中的占位文件),主程序构建时自动内嵌 +RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o internal/frp/agentbin/havline-agent-linux-amd64 ./cmd/havline-agent \ + && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -o internal/frp/agentbin/havline-agent-linux-arm64 ./cmd/havline-agent +COPY --from=web-builder /src/web/dist ./cmd/havline/web/dist RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \ - -ldflags "-X github.com/fonu/fonu/internal/version.Version=$(cat /src/VERSION | tr -d ' \t\n\r')" \ - -o /fonu ./cmd/fonu + -ldflags "-X github.com/havline/havline/internal/version.Version=$(cat /src/VERSION | tr -d ' \t\n\r')" \ + -o /havline ./cmd/havline # 一键升级 sidecar:独立容器持有 Docker Socket,主程序只通过共享 Unix Socket 调用它 FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS updater-builder @@ -55,13 +55,13 @@ COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod go mod download COPY cmd ./cmd COPY internal ./internal -RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -o /fonu-updater ./cmd/fonu-updater +RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -o /havline-updater ./cmd/havline-updater # updater 运行时:Docker CLI / Compose 与 Git 只在 sidecar 内可用 FROM ${UPDATER_IMAGE} AS updater RUN apk add --no-cache git docker-cli-compose docker-cli-buildx ca-certificates -COPY --from=updater-builder /fonu-updater /usr/local/bin/fonu-updater -ENTRYPOINT ["/usr/local/bin/fonu-updater"] +COPY --from=updater-builder /havline-updater /usr/local/bin/havline-updater +ENTRYPOINT ["/usr/local/bin/havline-updater"] # 运行时 stage FROM ${RUNTIME_IMAGE} @@ -72,18 +72,18 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* WORKDIR /app -COPY --from=go-builder /fonu /app/fonu +COPY --from=go-builder /havline /app/havline COPY --from=go-builder /src/web/assets/image /app/web/assets/image COPY migrations /app/migrations # 时区:Go 的 time.Local 取决于 TZ 环境变量,且需要镜像内存在 zoneinfo(故装上 tzdata) ENV TZ=Asia/Shanghai \ - FONU_DATA_DIR=/data \ - FONU_LISTEN=:6893 \ - FONU_NGINX_MIME_TYPES=/etc/nginx/mime.types + HAVLINE_DATA_DIR=/data \ + HAVLINE_LISTEN=:6893 \ + HAVLINE_NGINX_MIME_TYPES=/etc/nginx/mime.types EXPOSE 6893 80 443 VOLUME ["/data"] -CMD ["/app/fonu"] +CMD ["/app/havline"] diff --git a/README.md b/README.md index 42e87f2..559123a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

- Fonu Logo + Havline Logo

-

Fonu

+

Havline

让 NAS 访问更简单

@@ -24,7 +24,7 @@ ## 简介 -**Fonu** 是一款主要为 **飞牛 fnOS** 编写的反代与管理工具,帮助你在 NAS 上快速搭建稳定的公网访问能力:通过 Web 界面管理 Nginx 反向代理、自动同步 DDNS、申请与续期 Let's Encrypt 证书,并在一个仪表盘里查看服务状态、访问日志与流量统计。 +**Havline** 是一款主要为 **飞牛 fnOS** 编写的反代与管理工具,帮助你在 NAS 上快速搭建稳定的公网访问能力:通过 Web 界面管理 Nginx 反向代理、自动同步 DDNS、申请与续期 Let's Encrypt 证书,并在一个仪表盘里查看服务状态、访问日志与流量统计。 无需手写 Nginx 配置,也无需在多个工具之间切换。当前开发与测试环境以飞牛 fnOS 为主,**群晖、威联通、自建 Linux 等其他环境尚未充分测试**,部署前请自行验证。 @@ -56,10 +56,10 @@ ## 部署 -本地项目:三个 compose 文件均从源码构建镜像(`build: .`),不拉取任何仓库镜像,构建产物标签为 `fonu:local`。 +本地项目:三个 compose 文件均从源码构建镜像(`build: .`),不拉取任何仓库镜像,构建产物标签为 `havline:local`。 > **详细部署安装文档**:[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) —— 环境要求、镜像构建、三种部署方式、首次配置、FRP/DDNS/证书联动、升级回滚与故障排查。 -> **FRPS 服务端管理方案**:[`docs/frps服务端管理方案.md`](frps服务端管理方案.md) —— 公网 VPS 部署 `fonu-agent`,SSH 从本地 Fonu 安装/升级/卸载,日常通过本地管理面板调用 API 自动编排 Nginx、frps 与证书。 +> **FRPS 服务端管理方案**:[`docs/frps服务端管理方案.md`](frps服务端管理方案.md) —— 公网 VPS 部署 `havline-agent`,SSH 从本地 Havline 安装/升级/卸载,日常通过本地管理面板调用 API 自动编排 Nginx、frps 与证书。 ### 快速部署(推荐 · 飞牛 fnOS · Host 模式) @@ -67,11 +67,11 @@ ```bash docker run -d \ - --name fonu \ + --name havline \ --net=host \ -v ./data:/data \ --restart unless-stopped \ - fonu:local + havline:local ``` | 入口 | 地址 | @@ -89,7 +89,7 @@ docker run -d \ Docker Compose ```bash -FONU_SESSION_SECRET=your-secret docker compose -f docker-compose.host.yml up -d +HAVLINE_SESSION_SECRET=your-secret docker compose -f docker-compose.host.yml up -d ``` @@ -98,7 +98,7 @@ FONU_SESSION_SECRET=your-secret docker compose -f docker-compose.host.yml up -d Bridge 网络(端口映射,非飞牛环境可自行尝试) ```bash -FONU_SESSION_SECRET=your-secret docker compose -f docker-compose.hub.yml up -d --build +HAVLINE_SESSION_SECRET=your-secret docker compose -f docker-compose.hub.yml up -d --build ``` @@ -114,7 +114,7 @@ docker compose up -d --build ### Web 一键升级 -`docker-compose.yml` 与 `docker-compose.hub.yml` 会同时启动受限的 `fonu-updater` sidecar。检测到新版本后,侧栏会出现「一键升级」入口,可在管理页面完成构建、重建与健康检查。 +`docker-compose.yml` 与 `docker-compose.hub.yml` 会同时启动受限的 `havline-updater` sidecar。检测到新版本后,侧栏会出现「一键升级」入口,可在管理页面完成构建、重建与健康检查。 > 从旧版本升级时,需要先手动更新 Compose 文件并执行一次 `docker compose up -d --build` 启动 sidecar。详见 [`docs/system-upgrade.md`](docs/system-upgrade.md)。 @@ -126,15 +126,15 @@ docker compose up -d --build ### FRP 内网穿透(无公网 IP) -适用于无法端口映射、无公网 IPv4 的场景。Fonu 内置 **frpc**,支持**多台公网服务端**与全部 8 种穿透类型(tcp / udp / http / https / tcpmux / stcp / sudp / xtcp)及插件(static_file / socks5 / https2http 等)。 +适用于无法端口映射、无公网 IPv4 的场景。Havline 内置 **frpc**,支持**多台公网服务端**与全部 8 种穿透类型(tcp / udp / http / https / tcpmux / stcp / sudp / xtcp)及插件(static_file / socks5 / https2http 等)。 -**典型流量路径**(tcp 规则 + 本地反代):用户 → DNS(解析到 VPS)→ frps → frpc → Fonu Nginx → 内网服务 +**典型流量路径**(tcp 规则 + 本地反代):用户 → DNS(解析到 VPS)→ frps → frpc → Havline Nginx → 内网服务 #### 1. VPS 部署 frps 在服务端「详情」弹窗点 **「frps.toml」** 一键生成与 frpc 侧同源的服务端配置(bindPort / 认证 / webServer / 传输参数 / vhost 端口与防火墙清单全部对齐),复制到 VPS 保存后 `frps -c frps.toml` 启动,按文件末尾清单放行防火墙端口。 -#### 2. Fonu 配置 frpc +#### 2. Havline 配置 frpc 在侧栏 **FRP 穿透** 页面: @@ -153,25 +153,25 @@ docker compose up -d --build | 变量 | 默认值 | 说明 | | ----------------------- | ------------------------- | ------------------------------------------ | -| `FONU_LISTEN` | `:6893` | 管理后台监听地址 | -| `FONU_DATA_DIR` | `/data` | 数据目录(数据库、Nginx 配置、证书、日志) | -| `FONU_SESSION_SECRET` | 空(自动生成) | 会话与凭据加密密钥;有数据后不要更换 | -| `FONU_NGINX_HTTP_PORT` | `80` | 新建 HTTP 反代规则的默认监听端口 | -| `FONU_NGINX_HTTPS_PORT` | `443` | 新建 HTTPS 反代规则的默认监听端口 | -| `FONU_NGINX_BIN` | `nginx` | Nginx 可执行文件路径(本地开发可指定) | -| `FONU_FRPC_BIN` | `frpc` | frpc 可执行文件路径(镜像不预装,用「应用配置」按需下载或自行放置) | -| `FONU_FRP_PID` | `{DATA_DIR}/frp/frpc.pid` | frpc 进程 PID 文件路径 | -| `FONU_UPDATER_SOCKET` | `/run/fonu-updater/updater.sock` | 一键升级 sidecar 的 Unix Socket;未挂载时自动禁用 | +| `HAVLINE_LISTEN` | `:6893` | 管理后台监听地址 | +| `HAVLINE_DATA_DIR` | `/data` | 数据目录(数据库、Nginx 配置、证书、日志) | +| `HAVLINE_SESSION_SECRET` | 空(自动生成) | 会话与凭据加密密钥;有数据后不要更换 | +| `HAVLINE_NGINX_HTTP_PORT` | `80` | 新建 HTTP 反代规则的默认监听端口 | +| `HAVLINE_NGINX_HTTPS_PORT` | `443` | 新建 HTTPS 反代规则的默认监听端口 | +| `HAVLINE_NGINX_BIN` | `nginx` | Nginx 可执行文件路径(本地开发可指定) | +| `HAVLINE_FRPC_BIN` | `frpc` | frpc 可执行文件路径(镜像不预装,用「应用配置」按需下载或自行放置) | +| `HAVLINE_FRP_PID` | `{DATA_DIR}/frp/frpc.pid` | frpc 进程 PID 文件路径 | +| `HAVLINE_UPDATER_SOCKET` | `/run/havline-updater/updater.sock` | 一键升级 sidecar 的 Unix Socket;未挂载时自动禁用 | ## 注意事项 -1. **生产环境务必设置** `FONU_SESSION_SECRET`,不要使用默认值。 -2. **端口冲突**:宿主机已有 Nginx(如飞牛系统)占用 80/443 时,不要让 Fonu 反代规则监听 80/443;使用 `18080/9443` 或 host 模式 + 环境变量。 -3. **已有反代规则**:修改 `FONU_NGINX_HTTP_PORT` / `FONU_NGINX_HTTPS_PORT` 后,**不会自动更新**数据库中已有规则的端口,需在反代页手动修改并保存。 -4. **访问日志**:仅统计经 Fonu Nginx 反代的域名流量,不包含管理后台 `:6893` 的请求。 +1. **生产环境务必设置** `HAVLINE_SESSION_SECRET`,不要使用默认值。 +2. **端口冲突**:宿主机已有 Nginx(如飞牛系统)占用 80/443 时,不要让 Havline 反代规则监听 80/443;使用 `18080/9443` 或 host 模式 + 环境变量。 +3. **已有反代规则**:修改 `HAVLINE_NGINX_HTTP_PORT` / `HAVLINE_NGINX_HTTPS_PORT` 后,**不会自动更新**数据库中已有规则的端口,需在反代页手动修改并保存。 +4. **访问日志**:仅统计经 Havline Nginx 反代的域名流量,不包含管理后台 `:6893` 的请求。 5. **证书与 DNS**:HTTPS 申请前请确保域名 DNS 已解析到本机,且 DDNS 凭证能管理对应根域。 -6. **数据持久化**:Docker 部署请挂载 volume 到 `FONU_DATA_DIR`,避免容器重建后配置丢失。 -7. **备份**:可通过设置页导出配置;重要证书与数据库建议定期备份 `FONU_DATA_DIR` 目录。 +6. **数据持久化**:Docker 部署请挂载 volume 到 `HAVLINE_DATA_DIR`,避免容器重建后配置丢失。 +7. **备份**:可通过设置页导出配置;重要证书与数据库建议定期备份 `HAVLINE_DATA_DIR` 目录。 ## 本地开发 @@ -179,9 +179,9 @@ docker compose up -d --build # 前端热更新 cd web && npm ci && npm run dev -# 后端(需先把 web/dist 复制到 cmd/fonu/web/dist) +# 后端(需先把 web/dist 复制到 cmd/havline/web/dist) cd web && npm run build -go run ./cmd/fonu +go run ./cmd/havline # 测试 go test ./... diff --git a/cmd/fonu-agent/main.go b/cmd/fonu-agent/main.go deleted file mode 100644 index bb17fa7..0000000 --- a/cmd/fonu-agent/main.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "log" - "os" - "os/signal" - "strings" - "syscall" - - "github.com/fonu/fonu/internal/agent" -) - -func main() { - listen := flag.String("listen", envOr("FONU_AGENT_LISTEN", "127.0.0.1:7700"), "agent listen address") - allowRemote := flag.Bool("allow-remote", envBool("FONU_AGENT_ALLOW_REMOTE"), "allow non-loopback listen address") - token := flag.String("token", os.Getenv("FONU_AGENT_TOKEN"), "agent bearer token") - nginxDir := flag.String("nginx-conf-dir", envOr("FONU_AGENT_NGINX_CONF_DIR", ""), "managed nginx config directory(留空自动探测 Nginx 实际加载的 vhost 目录)") - certsDir := flag.String("certs-dir", envOr("FONU_AGENT_CERTS_DIR", "/var/lib/fonu-agent/certs"), "certificate directory") - frpsConfig := flag.String("frps-config", envOr("FONU_AGENT_FRPS_CONFIG", "/etc/frp/frps.toml"), "frps config path") - frpsBin := flag.String("frps-bin", envOr("FONU_AGENT_FRPS_BIN", "frps"), "frps binary") - dataDir := flag.String("data-dir", envOr("FONU_AGENT_DATA_DIR", "/var/lib/fonu-agent"), "agent data directory") - showVersion := flag.Bool("version", false, "print agent version and exit") - flag.Parse() - if *showVersion { - fmt.Println("fonu-agent", agent.AgentVersion) - return - } - if *token == "" { - log.Fatal("FONU_AGENT_TOKEN or -token is required") - } - server, err := agent.NewServer(agent.Config{ - ListenAddr: *listen, AllowRemote: *allowRemote, Token: *token, NginxConfDir: *nginxDir, - CertsDir: *certsDir, FrpsConfigPath: *frpsConfig, FrpsBinary: *frpsBin, DataDir: *dataDir, - }) - if err != nil { - log.Fatal(err) - } - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() - if err := server.ListenAndServe(ctx); err != nil && ctx.Err() == nil { - log.Fatal(err) - } -} - -func envOr(key, fallback string) string { - if value := os.Getenv(key); value != "" { - return value - } - return fallback -} - -func envBool(key string) bool { - switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) { - case "1", "true", "yes", "on": - return true - default: - return false - } -} diff --git a/cmd/fonu-updater/main.go b/cmd/fonu-updater/main.go deleted file mode 100644 index af84490..0000000 --- a/cmd/fonu-updater/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "context" - "log" - "os" - "os/signal" - "syscall" - - "github.com/fonu/fonu/internal/updater" -) - -func main() { - server, err := updater.NewServer(updater.Config{ - SocketPath: envOr("FONU_UPDATER_SOCKET", "/run/fonu-updater/updater.sock"), - ProjectDir: envOr("FONU_UPDATE_PROJECT_DIR", "/workspace"), - ComposeFile: envOr("FONU_UPDATE_COMPOSE_FILE", "docker-compose.yml"), - Service: envOr("FONU_UPDATE_SERVICE", "fonu"), - Mode: envOr("FONU_UPDATE_MODE", "local"), - RepoURL: envOr("FONU_UPDATE_REPO", ""), - Branch: envOr("FONU_UPDATE_BRANCH", "main"), - AppURL: envOr("FONU_UPDATE_APP_URL", "http://fonu:6893"), - HostProjectDir: envOr("FONU_UPDATE_HOST_DIR", ""), - ProjectName: envOr("FONU_UPDATE_PROJECT_NAME", ""), - SelfImage: envOr("FONU_UPDATE_SELF_IMAGE", ""), - DockerSocket: envOr("FONU_UPDATE_DOCKER_SOCK", "/var/run/docker.sock"), - }, nil) - if err != nil { - log.Fatal(err) - } - - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() - if err := server.Serve(ctx); err != nil && ctx.Err() == nil { - log.Fatal(err) - } -} - -func envOr(key, fallback string) string { - if value := os.Getenv(key); value != "" { - return value - } - return fallback -} diff --git a/cmd/havline-agent/main.go b/cmd/havline-agent/main.go new file mode 100644 index 0000000..7f4322e --- /dev/null +++ b/cmd/havline-agent/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/havline/havline/internal/agent" +) + +func main() { + listen := flag.String("listen", envOr("HAVLINE_AGENT_LISTEN", "127.0.0.1:7700"), "agent listen address") + allowRemote := flag.Bool("allow-remote", envBool("HAVLINE_AGENT_ALLOW_REMOTE"), "allow non-loopback listen address") + token := flag.String("token", os.Getenv("HAVLINE_AGENT_TOKEN"), "agent bearer token") + nginxDir := flag.String("nginx-conf-dir", envOr("HAVLINE_AGENT_NGINX_CONF_DIR", ""), "managed nginx config directory(留空自动探测 Nginx 实际加载的 vhost 目录)") + certsDir := flag.String("certs-dir", envOr("HAVLINE_AGENT_CERTS_DIR", "/var/lib/havline-agent/certs"), "certificate directory") + frpsConfig := flag.String("frps-config", envOr("HAVLINE_AGENT_FRPS_CONFIG", "/etc/frp/frps.toml"), "frps config path") + frpsBin := flag.String("frps-bin", envOr("HAVLINE_AGENT_FRPS_BIN", "frps"), "frps binary") + dataDir := flag.String("data-dir", envOr("HAVLINE_AGENT_DATA_DIR", "/var/lib/havline-agent"), "agent data directory") + showVersion := flag.Bool("version", false, "print agent version and exit") + flag.Parse() + if *showVersion { + fmt.Println("havline-agent", agent.AgentVersion) + return + } + if *token == "" { + log.Fatal("HAVLINE_AGENT_TOKEN or -token is required") + } + server, err := agent.NewServer(agent.Config{ + ListenAddr: *listen, AllowRemote: *allowRemote, Token: *token, NginxConfDir: *nginxDir, + CertsDir: *certsDir, FrpsConfigPath: *frpsConfig, FrpsBinary: *frpsBin, DataDir: *dataDir, + }) + if err != nil { + log.Fatal(err) + } + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + if err := server.ListenAndServe(ctx); err != nil && ctx.Err() == nil { + log.Fatal(err) + } +} + +func envOr(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} + +func envBool(key string) bool { + switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} diff --git a/cmd/havline-updater/main.go b/cmd/havline-updater/main.go new file mode 100644 index 0000000..c462b69 --- /dev/null +++ b/cmd/havline-updater/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "log" + "os" + "os/signal" + "syscall" + + "github.com/havline/havline/internal/updater" +) + +func main() { + server, err := updater.NewServer(updater.Config{ + SocketPath: envOr("HAVLINE_UPDATER_SOCKET", "/run/havline-updater/updater.sock"), + ProjectDir: envOr("HAVLINE_UPDATE_PROJECT_DIR", "/workspace"), + ComposeFile: envOr("HAVLINE_UPDATE_COMPOSE_FILE", "docker-compose.yml"), + Service: envOr("HAVLINE_UPDATE_SERVICE", "havline"), + Mode: envOr("HAVLINE_UPDATE_MODE", "local"), + RepoURL: envOr("HAVLINE_UPDATE_REPO", ""), + Branch: envOr("HAVLINE_UPDATE_BRANCH", "main"), + AppURL: envOr("HAVLINE_UPDATE_APP_URL", "http://havline:6893"), + HostProjectDir: envOr("HAVLINE_UPDATE_HOST_DIR", ""), + ProjectName: envOr("HAVLINE_UPDATE_PROJECT_NAME", ""), + SelfImage: envOr("HAVLINE_UPDATE_SELF_IMAGE", ""), + DockerSocket: envOr("HAVLINE_UPDATE_DOCKER_SOCK", "/var/run/docker.sock"), + }, nil) + if err != nil { + log.Fatal(err) + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + if err := server.Serve(ctx); err != nil && ctx.Err() == nil { + log.Fatal(err) + } +} + +func envOr(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} diff --git a/cmd/fonu/main.go b/cmd/havline/main.go similarity index 91% rename from cmd/fonu/main.go rename to cmd/havline/main.go index ccd5c38..cf74858 100644 --- a/cmd/fonu/main.go +++ b/cmd/havline/main.go @@ -12,8 +12,8 @@ import ( "syscall" "time" - "github.com/fonu/fonu/internal/app" - "github.com/fonu/fonu/internal/config" + "github.com/havline/havline/internal/app" + "github.com/havline/havline/internal/config" ) //go:embed all:web/dist diff --git a/docker-compose.host.yml b/docker-compose.host.yml index be24451..310e5d5 100644 --- a/docker-compose.host.yml +++ b/docker-compose.host.yml @@ -1,8 +1,8 @@ # 飞牛 NAS 等已占用 80/443 的环境:使用 host 网络 + 高位默认反代端口 # 本地项目:直接用源码构建镜像,不从 Docker Hub 拉取 -name: fonu +name: havline services: - fonu: + havline: build: context: . args: @@ -10,25 +10,25 @@ services: GO_IMAGE: ${GO_IMAGE:-docker.m.daocloud.io/library/golang:1.23-bookworm} RUNTIME_IMAGE: ${RUNTIME_IMAGE:-docker.m.daocloud.io/library/debian:bookworm-slim} GOPROXY: ${GOPROXY:-https://goproxy.cn,direct} - image: fonu:${FONU_VERSION:-local} - container_name: fonu + image: havline:${HAVLINE_VERSION:-local} + container_name: havline restart: unless-stopped network_mode: host volumes: - # 数据目录:默认用命名卷 fonu-data(由 Docker 管理,位置随 Docker 数据根)。 - # 想落到指定目录就在同层 .env 里设 FONU_DATA_PATH=/vol1/docker/fonu-data —— + # 数据目录:默认用命名卷 havline-data(由 Docker 管理,位置随 Docker 数据根)。 + # 想落到指定目录就在同层 .env 里设 HAVLINE_DATA_PATH=/vol1/docker/havline-data —— # 以 / 或 . 开头的写法会被 compose 当成 bind mount,其余字符串仍是命名卷。 - - ${FONU_DATA_PATH:-fonu-data}:/data + - ${HAVLINE_DATA_PATH:-havline-data}:/data environment: - FONU_DATA_DIR: /data - FONU_LISTEN: :6893 + HAVLINE_DATA_DIR: /data + HAVLINE_LISTEN: :6893 # 留空即自动:应用会在数据目录里生成并持久化 /session_secret(0600) # 想自己指定就设这个变量;注意它同时是凭据加密密钥,已有数据后不要随意更换 - FONU_SESSION_SECRET: ${FONU_SESSION_SECRET:-} + HAVLINE_SESSION_SECRET: ${HAVLINE_SESSION_SECRET:-} # 时区:通知消息时间按此显示(默认东八区) TZ: ${TZ:-Asia/Shanghai} - FONU_NGINX_HTTP_PORT: ${FONU_NGINX_HTTP_PORT:-18080} - FONU_NGINX_HTTPS_PORT: ${FONU_NGINX_HTTPS_PORT:-9443} + HAVLINE_NGINX_HTTP_PORT: ${HAVLINE_NGINX_HTTP_PORT:-18080} + HAVLINE_NGINX_HTTPS_PORT: ${HAVLINE_NGINX_HTTPS_PORT:-9443} volumes: - fonu-data: + havline-data: diff --git a/docker-compose.hub.yml b/docker-compose.hub.yml index 712b8c5..955c503 100644 --- a/docker-compose.hub.yml +++ b/docker-compose.hub.yml @@ -1,13 +1,13 @@ # 从仓库拉源码构建并启动(适合干净机器:只要这一个文件) # # 用法: -# 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 # 会话密钥由应用首次启动自动生成并持久化在数据卷里 # # 想自己指定会话密钥(可选项):在 compose 文件同层建 .env 写入 -# FONU_SESSION_SECRET=<一串随机字符,openssl rand -hex 32 可生成> +# HAVLINE_SESSION_SECRET=<一串随机字符,openssl rand -hex 32 可生成> # 注意:它同时是凭据加密密钥,数据卷里已经有数据后不要随意更换,否则已存的凭据无法解密。 # # 构建上下文用的是 Git 地址:BuildKit 会自己把仓库克隆下来,不需要先 git clone 到本地; @@ -16,22 +16,22 @@ # # 首次构建要拉前端与 Go 依赖,比较慢;之后靠 Dockerfile 里的 cache mount 复用缓存。 # 80/443 已被占用(如飞牛 NAS)请改用 docker-compose.host.yml。 -name: fonu +name: havline services: - fonu: + havline: build: - context: http://45.207.223.190:10082/mrlee/-Fonu.git#main + context: http://45.207.223.190:10082/mrlee/Havline.git#main args: NODE_IMAGE: ${NODE_IMAGE:-docker.m.daocloud.io/library/node:22-alpine} GO_IMAGE: ${GO_IMAGE:-docker.m.daocloud.io/library/golang:1.23-bookworm} RUNTIME_IMAGE: ${RUNTIME_IMAGE:-docker.m.daocloud.io/library/debian:bookworm-slim} UPDATER_IMAGE: ${UPDATER_IMAGE:-docker.m.daocloud.io/library/docker:27-cli} GOPROXY: ${GOPROXY:-https://goproxy.cn,direct} - image: fonu:${FONU_VERSION:-local} - # 有 build 段时 compose 默认会先尝试拉同名镜像(日志里那条 "fonu Pulling" 就是), + image: havline:${HAVLINE_VERSION:-local} + # 有 build 段时 compose 默认会先尝试拉同名镜像(日志里那条 "havline Pulling" 就是), # 国内直连 Docker Hub 会超时报警;强制走构建,跳过这一步。 pull_policy: build - container_name: fonu + container_name: havline restart: unless-stopped depends_on: - updater @@ -40,53 +40,53 @@ services: - "18080:80" - "9443:443" volumes: - # 数据目录:默认用命名卷 fonu-data(由 Docker 管理,位置随 Docker 数据根)。 - # 想落到指定目录就在同层 .env 里设 FONU_DATA_PATH=/vol1/docker/fonu-data —— + # 数据目录:默认用命名卷 havline-data(由 Docker 管理,位置随 Docker 数据根)。 + # 想落到指定目录就在同层 .env 里设 HAVLINE_DATA_PATH=/vol1/docker/havline-data —— # 以 / 或 . 开头的写法会被 compose 当成 bind mount,其余字符串仍是命名卷。 - - ${FONU_DATA_PATH:-fonu-data}:/data - - fonu-updater-sock:/run/fonu-updater + - ${HAVLINE_DATA_PATH:-havline-data}:/data + - havline-updater-sock:/run/havline-updater environment: - FONU_DATA_DIR: /data - FONU_LISTEN: :6893 - FONU_UPDATER_SOCKET: /run/fonu-updater/updater.sock + HAVLINE_DATA_DIR: /data + HAVLINE_LISTEN: :6893 + HAVLINE_UPDATER_SOCKET: /run/havline-updater/updater.sock # 留空即自动:应用会在数据目录里生成并持久化 /session_secret(0600) # 想自己指定就在同层 .env 里设这个变量;注意它同时是凭据加密密钥,已有数据后不要更换 - FONU_SESSION_SECRET: ${FONU_SESSION_SECRET:-} + HAVLINE_SESSION_SECRET: ${HAVLINE_SESSION_SECRET:-} # 时区:通知消息时间按此显示(默认东八区) TZ: ${TZ:-Asia/Shanghai} # 受限升级 sidecar:只有它持有 Docker Socket,主程序通过共享 Unix Socket 触发 updater: build: - context: http://45.207.223.190:10082/mrlee/-Fonu.git#main + context: http://45.207.223.190:10082/mrlee/Havline.git#main target: updater args: GO_IMAGE: ${GO_IMAGE:-docker.m.daocloud.io/library/golang:1.23-bookworm} UPDATER_IMAGE: ${UPDATER_IMAGE:-docker.m.daocloud.io/library/docker:27-cli} GOPROXY: ${GOPROXY:-https://goproxy.cn,direct} - image: fonu-updater:${FONU_VERSION:-local} + image: havline-updater:${HAVLINE_VERSION:-local} pull_policy: build - container_name: fonu-updater + container_name: havline-updater restart: unless-stopped volumes: - ${DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock - - ${FONU_UPDATE_HOST_DIR:-.}:/workspace - - fonu-updater-sock:/run/fonu-updater + - ${HAVLINE_UPDATE_HOST_DIR:-.}:/workspace + - havline-updater-sock:/run/havline-updater environment: - FONU_UPDATER_SOCKET: /run/fonu-updater/updater.sock - FONU_UPDATE_PROJECT_DIR: /workspace - FONU_UPDATE_COMPOSE_FILE: docker-compose.hub.yml - FONU_UPDATE_SERVICE: fonu - FONU_UPDATE_MODE: git - FONU_UPDATE_REPO: http://45.207.223.190:10082/mrlee/-Fonu.git - FONU_UPDATE_BRANCH: main - FONU_UPDATE_APP_URL: http://fonu:6893 - FONU_UPDATE_HOST_DIR: ${FONU_UPDATE_HOST_DIR:-} - FONU_UPDATE_PROJECT_NAME: ${FONU_UPDATE_PROJECT_NAME:-fonu} - FONU_UPDATE_SELF_IMAGE: fonu-updater:${FONU_VERSION:-local} - FONU_UPDATE_DOCKER_SOCK: ${DOCKER_SOCK:-/var/run/docker.sock} + HAVLINE_UPDATER_SOCKET: /run/havline-updater/updater.sock + HAVLINE_UPDATE_PROJECT_DIR: /workspace + HAVLINE_UPDATE_COMPOSE_FILE: docker-compose.hub.yml + HAVLINE_UPDATE_SERVICE: havline + HAVLINE_UPDATE_MODE: git + HAVLINE_UPDATE_REPO: http://45.207.223.190:10082/mrlee/Havline.git + HAVLINE_UPDATE_BRANCH: main + HAVLINE_UPDATE_APP_URL: http://havline:6893 + HAVLINE_UPDATE_HOST_DIR: ${HAVLINE_UPDATE_HOST_DIR:-} + HAVLINE_UPDATE_PROJECT_NAME: ${HAVLINE_UPDATE_PROJECT_NAME:-havline} + HAVLINE_UPDATE_SELF_IMAGE: havline-updater:${HAVLINE_VERSION:-local} + HAVLINE_UPDATE_DOCKER_SOCK: ${DOCKER_SOCK:-/var/run/docker.sock} TZ: ${TZ:-Asia/Shanghai} volumes: - fonu-data: - fonu-updater-sock: + havline-data: + havline-updater-sock: diff --git a/docker-compose.yml b/docker-compose.yml index 5d2ffd0..676d99b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,12 @@ # 固定项目名:不改的话,改了目录名/项目名后 compose 会把它当成新项目, -# fonu-data 会变成新空卷(看起来像「数据丢了」)。 -name: fonu +# havline-data 会变成新空卷(看起来像「数据丢了」)。 +name: havline # 本地项目:用源码构建镜像(镜像名随 VERSION 变,便于回滚;也便于导出 / 备份) # 升级:docker compose up -d --build(或在部署机跑 scripts/upgrade.sh) -# 回滚:FONU_VERSION=上一个版本号 docker compose up -d(镜像 tag 还在本地) +# 回滚:HAVLINE_VERSION=上一个版本号 docker compose up -d(镜像 tag 还在本地) services: - fonu: + havline: build: context: . args: @@ -15,8 +15,8 @@ services: RUNTIME_IMAGE: ${RUNTIME_IMAGE:-docker.m.daocloud.io/library/debian:bookworm-slim} UPDATER_IMAGE: ${UPDATER_IMAGE:-docker.m.daocloud.io/library/docker:27-cli} GOPROXY: ${GOPROXY:-https://goproxy.cn,direct} - image: fonu:${FONU_VERSION:-local} - container_name: fonu + image: havline:${HAVLINE_VERSION:-local} + container_name: havline restart: unless-stopped depends_on: - updater @@ -25,18 +25,18 @@ services: - "18080:80" - "9443:443" volumes: - # 数据目录:默认用命名卷 fonu-data(由 Docker 管理,位置随 Docker 数据根)。 - # 想落到指定目录就在同层 .env 里设 FONU_DATA_PATH=/vol1/docker/fonu-data —— + # 数据目录:默认用命名卷 havline-data(由 Docker 管理,位置随 Docker 数据根)。 + # 想落到指定目录就在同层 .env 里设 HAVLINE_DATA_PATH=/vol1/docker/havline-data —— # 以 / 或 . 开头的写法会被 compose 当成 bind mount,其余字符串仍是命名卷。 - - ${FONU_DATA_PATH:-fonu-data}:/data - - fonu-updater-sock:/run/fonu-updater + - ${HAVLINE_DATA_PATH:-havline-data}:/data + - havline-updater-sock:/run/havline-updater environment: - FONU_DATA_DIR: /data - FONU_LISTEN: :6893 - FONU_UPDATER_SOCKET: /run/fonu-updater/updater.sock + HAVLINE_DATA_DIR: /data + HAVLINE_LISTEN: :6893 + HAVLINE_UPDATER_SOCKET: /run/havline-updater/updater.sock # 留空即自动:应用会在数据目录里生成并持久化 /session_secret(0600) # 想自己指定就设这个变量;注意它同时是凭据加密密钥,已有数据后不要随意更换 - FONU_SESSION_SECRET: ${FONU_SESSION_SECRET:-} + HAVLINE_SESSION_SECRET: ${HAVLINE_SESSION_SECRET:-} # 时区:通知消息时间按此显示(默认东八区) TZ: ${TZ:-Asia/Shanghai} @@ -49,26 +49,26 @@ services: GO_IMAGE: ${GO_IMAGE:-docker.m.daocloud.io/library/golang:1.23-bookworm} UPDATER_IMAGE: ${UPDATER_IMAGE:-docker.m.daocloud.io/library/docker:27-cli} GOPROXY: ${GOPROXY:-https://goproxy.cn,direct} - image: fonu-updater:${FONU_VERSION:-local} - container_name: fonu-updater + image: havline-updater:${HAVLINE_VERSION:-local} + container_name: havline-updater restart: unless-stopped volumes: - ${DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock - - ${FONU_UPDATE_HOST_DIR:-.}:/workspace - - fonu-updater-sock:/run/fonu-updater + - ${HAVLINE_UPDATE_HOST_DIR:-.}:/workspace + - havline-updater-sock:/run/havline-updater environment: - FONU_UPDATER_SOCKET: /run/fonu-updater/updater.sock - FONU_UPDATE_PROJECT_DIR: /workspace - FONU_UPDATE_COMPOSE_FILE: docker-compose.yml - FONU_UPDATE_SERVICE: fonu - FONU_UPDATE_MODE: local - FONU_UPDATE_APP_URL: http://fonu:6893 - FONU_UPDATE_HOST_DIR: ${FONU_UPDATE_HOST_DIR:-} - FONU_UPDATE_PROJECT_NAME: ${FONU_UPDATE_PROJECT_NAME:-fonu} - FONU_UPDATE_SELF_IMAGE: fonu-updater:${FONU_VERSION:-local} - FONU_UPDATE_DOCKER_SOCK: ${DOCKER_SOCK:-/var/run/docker.sock} + HAVLINE_UPDATER_SOCKET: /run/havline-updater/updater.sock + HAVLINE_UPDATE_PROJECT_DIR: /workspace + HAVLINE_UPDATE_COMPOSE_FILE: docker-compose.yml + HAVLINE_UPDATE_SERVICE: havline + HAVLINE_UPDATE_MODE: local + HAVLINE_UPDATE_APP_URL: http://havline:6893 + HAVLINE_UPDATE_HOST_DIR: ${HAVLINE_UPDATE_HOST_DIR:-} + HAVLINE_UPDATE_PROJECT_NAME: ${HAVLINE_UPDATE_PROJECT_NAME:-havline} + HAVLINE_UPDATE_SELF_IMAGE: havline-updater:${HAVLINE_VERSION:-local} + HAVLINE_UPDATE_DOCKER_SOCK: ${DOCKER_SOCK:-/var/run/docker.sock} TZ: ${TZ:-Asia/Shanghai} volumes: - fonu-data: - fonu-updater-sock: + havline-data: + havline-updater-sock: diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 77631d0..35eac8d 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -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://:6893`,HTTP 反代 `http://:18080`,HTTPS 反代 `https://: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://: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://: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 # 删除镜像 ``` diff --git a/docs/FRP公网暴露操作手册.md b/docs/FRP公网暴露操作手册.md index 56a93c1..1ea87fe 100644 --- a/docs/FRP公网暴露操作手册.md +++ b/docs/FRP公网暴露操作手册.md @@ -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://:6893` | 全部配置入口 | +| Havline 管理后台 | `http://: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 侧终止。 diff --git a/docs/README.md b/docs/README.md index 6cb5e32..4b6a1a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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` 安装包 | ## 其它位置 diff --git a/docs/agent-capability-plan.md b/docs/agent-capability-plan.md index 8bbc989..0a8454d 100644 --- a/docs/agent-capability-plan.md +++ b/docs/agent-capability-plan.md @@ -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 主动上报。 diff --git a/docs/agent-listen-strategy-plan.md b/docs/agent-listen-strategy-plan.md index 2b45856..470cc05 100644 --- a/docs/agent-listen-strategy-plan.md +++ b/docs/agent-listen-strategy-plan.md @@ -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://: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 或现有服务失联 | diff --git a/docs/agent-proxy-roadmap.md b/docs/agent-proxy-roadmap.md index 6d749e3..c88838b 100644 --- a/docs/agent-proxy-roadmap.md +++ b/docs/agent-proxy-roadmap.md @@ -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:.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:" http://127.0.0.1: 目标:把本地反向代理(`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 侧每域名配置为整文件覆盖写,无残留 | diff --git a/docs/agent-view-design-v2.md b/docs/agent-view-design-v2.md index c17c6b0..2d16831 100644 --- a/docs/agent-view-design-v2.md +++ b/docs/agent-view-design-v2.md @@ -17,11 +17,11 @@ | 框架 | Vue 3 + ` + + diff --git a/docs/havline-logo-v2-preview.html b/docs/havline-logo-v2-preview.html new file mode 100644 index 0000000..ac2e0f3 --- /dev/null +++ b/docs/havline-logo-v2-preview.html @@ -0,0 +1,905 @@ + + + + + + + + Havline Logo V2 概念预览 + + + + + +
+
+ + + +
+ Havline V2 + Refined mark / harbor gate +
+
+
+ + + +
+
+ +
+
+
+

Logo evolution / 02

+

Havline

+

从“一个 H”变成“一个港湾入口”。

+

V2 保留 H 的识别骨架,但不再用两个端点圆点做连接。线路改为从 H 后方穿过的连续波浪,视觉上像一条服务路径穿过港湾入口,也更适合侧栏的小尺寸使用。

+
+ NO EYES / CONTINUOUS ROUTE + 40PX SIDEBAR SAFE +
+
+ +
+
+ + Havline V2 横向标志 + 左侧为 Havline V2 标志,右侧为 havline 字体组合。 + + havline + SAFE LINE FOR EVERY SERVICE + + +
+
+
+ +
+
+
+

这版改了什么

+

问题不在 H,而在连接方式。旧版的端点圆点在 40px 侧栏里会形成类似“眼睛”的视觉焦点,抢掉了 H 和线路的主次关系。V2 让线路后退,保留一个干净的入口。

+
+
+
+ 01 / REMOVE +

去掉端点圆点

+

不再在左右立柱上放节点,避免小尺寸下像双眼或天线,让 H 的主体轮廓更干净。

+
+
+ 02 / LAYER +

线路退到 H 后方

+

波浪线路从双柱背后穿过,只在中间形成连接,保留“服务穿过港湾入口”的叙事。

+
+
+ 03 / FLOW +

改用连续波浪

+

斜线改成更平滑的水波路径,既有链路感,也接近港湾与水面意象。

+
+
+ 04 / SIDEBAR +

为 40px 优化

+

主结构只保留两根立柱和一条线路,缩放后仍能读出 H,不依赖额外细节。

+
+
+
+
+ +
+
+
+

前后对比

+

左侧是上一版,右侧是 V2。对比重点不是“更复杂”,而是让标志在侧栏尺寸下更稳定、更耐看。

+
+
+
+ + + + + + + + + + V1 / endpoint nodes +
+
+ + + + V2 / continuous route +
+
+
+
+ +
+
+
+

三个方向

+

推荐主案是左侧的“连续路线 H”。另外两个方向可以用于周边、活动或后续品牌迭代,不适合直接替代主标志。

+
+
+ +
+ + + +
+

港湾拱门

+

更抽象、画面感更强

+
+
+
+ + + +
+

小写 h 路线

+

更亲和、更偏产品化

+
+
+
+
+
+ +
+
+
+

侧栏模拟

+

在当前的 40px 品牌区里,V2 不再出现两个高亮圆点,视觉重心重新回到 H 和品牌名称上。

+
+ +
+
+ +
+
+
+

尺寸检查

+

主标志建议最小使用 16px。侧栏品牌区 40px,应用图标 64px,登录页主标志 120px 以上。

+
+
+ + + 16 + + + + 24 + + + + 40 + + + + 64 + + + + 96 + +
+
+
+ +
+
+
+

SVG 源文件

+

这是 V2 主案的可下载 SVG,后续可以直接替换侧栏中的现有标志,不需要再调整图片尺寸。

+
+
+

+          
+ + +
+
+
+
+
+ +
+ Havline V2 · 主案:连续路线 H · 设计目标:更干净、更有港湾入口感、在 40px 侧栏里不产生“表情”错觉。 +
+ + + + diff --git a/docs/proxy-remote-design.md b/docs/proxy-remote-design.md index 78d2b4d..0ad3cd5 100644 --- a/docs/proxy-remote-design.md +++ b/docs/proxy-remote-design.md @@ -16,12 +16,12 @@ | 框架 | Vue 3 + ` diff --git a/web/src/components/EmptyState.vue b/web/src/components/EmptyState.vue index a4f2f10..b2a67bd 100644 --- a/web/src/components/EmptyState.vue +++ b/web/src/components/EmptyState.vue @@ -22,17 +22,17 @@ withDefaults( diff --git a/web/src/components/FonuCard.vue b/web/src/components/FonuCard.vue deleted file mode 100644 index a01635e..0000000 --- a/web/src/components/FonuCard.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - - diff --git a/web/src/components/HavlineCard.vue b/web/src/components/HavlineCard.vue new file mode 100644 index 0000000..36e865e --- /dev/null +++ b/web/src/components/HavlineCard.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/web/src/components/LoadError.vue b/web/src/components/LoadError.vue index 133d924..96fa60b 100644 --- a/web/src/components/LoadError.vue +++ b/web/src/components/LoadError.vue @@ -15,7 +15,7 @@ withDefaults( defineProps<{ message?: string }>(), - { message: '请检查 Fonu 服务是否正常运行' }, + { message: '请检查 Havline 服务是否正常运行' }, ) defineEmits<{ retry: [] }>() @@ -23,6 +23,6 @@ defineEmits<{ retry: [] }>() diff --git a/web/src/components/MetricsTrendPanel.vue b/web/src/components/MetricsTrendPanel.vue index 2b71e49..248fa0c 100644 --- a/web/src/components/MetricsTrendPanel.vue +++ b/web/src/components/MetricsTrendPanel.vue @@ -131,11 +131,11 @@ watch(() => props.hours, load) diff --git a/web/src/components/NginxCodeEditor.vue b/web/src/components/NginxCodeEditor.vue index 24d8897..b6554b8 100644 --- a/web/src/components/NginxCodeEditor.vue +++ b/web/src/components/NginxCodeEditor.vue @@ -56,32 +56,32 @@ const nginxLanguage = StreamLanguage.define(nginx) const lightTheme = EditorView.theme( { '&': { - backgroundColor: 'var(--fonu-surface)', - color: 'var(--fonu-text)', + backgroundColor: 'var(--havline-surface)', + color: 'var(--havline-text)', }, '.cm-content': { - caretColor: 'var(--fonu-brand)', - fontFamily: 'var(--fonu-mono)', + caretColor: 'var(--havline-brand)', + fontFamily: 'var(--havline-mono)', fontSize: '12px', lineHeight: '1.7', }, '.cm-gutters': { - backgroundColor: 'var(--fonu-bg-muted)', - color: 'var(--fonu-text-muted)', - borderRight: '1px solid var(--fonu-border)', + backgroundColor: 'var(--havline-bg-muted)', + color: 'var(--havline-text-muted)', + borderRight: '1px solid var(--havline-border)', }, '.cm-activeLineGutter': { - backgroundColor: 'var(--fonu-bg)', - color: 'var(--fonu-text-secondary)', + backgroundColor: 'var(--havline-bg)', + color: 'var(--havline-text-secondary)', }, '.cm-activeLine': { - backgroundColor: 'var(--fonu-brand-soft)', + backgroundColor: 'var(--havline-brand-soft)', }, '.cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection': { - backgroundColor: 'color-mix(in srgb, var(--fonu-brand) 22%, transparent) !important', + backgroundColor: 'color-mix(in srgb, var(--havline-brand) 22%, transparent) !important', }, '.cm-cursor, .cm-dropCursor': { - borderLeftColor: 'var(--fonu-brand)', + borderLeftColor: 'var(--havline-brand)', }, '&.cm-focused': { outline: 'none', @@ -128,10 +128,10 @@ function onUpdate(value: string) { diff --git a/web/src/components/ProxyAccessLogBox.vue b/web/src/components/ProxyAccessLogBox.vue index c66bf5e..329fa49 100644 --- a/web/src/components/ProxyAccessLogBox.vue +++ b/web/src/components/ProxyAccessLogBox.vue @@ -150,11 +150,11 @@ defineExpose({ scrollToBottom }) color: #e2e8f0; overflow-y: auto; overflow-x: auto; - padding: var(--fonu-space-3); - font-family: var(--fonu-mono); + padding: var(--havline-space-3); + font-family: var(--havline-mono); font-size: 12px; line-height: 1.7; - border-radius: var(--fonu-radius-sm); + border-radius: var(--havline-radius-sm); scrollbar-width: thin; scrollbar-color: rgba(148, 163, 184, 0.2) transparent; } @@ -195,7 +195,7 @@ defineExpose({ scrollToBottom }) .proxy-log-box--fullscreen { flex: 1; min-height: 0; - border-radius: var(--fonu-radius); + border-radius: var(--havline-radius); } .proxy-log-entry { @@ -297,11 +297,11 @@ defineExpose({ scrollToBottom }) } .proxy-log-empty { - padding: var(--fonu-space-5); + padding: var(--havline-space-5); text-align: center; color: #94a3b8; white-space: normal; } -.mono { font-family: var(--fonu-mono); } +.mono { font-family: var(--havline-mono); } diff --git a/web/src/components/StatCard.vue b/web/src/components/StatCard.vue index d9f3985..32a8f5a 100644 --- a/web/src/components/StatCard.vue +++ b/web/src/components/StatCard.vue @@ -35,11 +35,11 @@ withDefaults( diff --git a/web/src/components/StatusBadge.vue b/web/src/components/StatusBadge.vue index c585841..10a4e04 100644 --- a/web/src/components/StatusBadge.vue +++ b/web/src/components/StatusBadge.vue @@ -36,28 +36,28 @@ const label = computed(() => props.text ?? statusLabel(props.value)) .status-badge.pill { padding: 4px 10px; border-radius: 999px; - background: var(--fonu-bg); + background: var(--havline-bg); } .status-badge.pill.success { background: rgba(16, 185, 129, 0.12); - color: var(--fonu-success); + color: var(--havline-success); } .status-badge.pill.warning { background: rgba(245, 158, 11, 0.12); - color: var(--fonu-warning); + color: var(--havline-warning); } .status-badge.pill.error { background: rgba(239, 68, 68, 0.12); - color: var(--fonu-error); + color: var(--havline-error); } .status-badge.pill.disabled, .status-badge.pill.unknown { - background: var(--fonu-bg-muted); - color: var(--fonu-text-secondary); + background: var(--havline-bg-muted); + color: var(--havline-text-secondary); } .dot { @@ -67,11 +67,11 @@ const label = computed(() => props.text ?? statusLabel(props.value)) flex-shrink: 0; } -.success .dot { background: var(--fonu-success); } -.warning .dot { background: var(--fonu-warning); } -.error .dot { background: var(--fonu-error); } -.disabled .dot { background: var(--fonu-disabled); } -.unknown .dot { background: var(--fonu-text-muted); } +.success .dot { background: var(--havline-success); } +.warning .dot { background: var(--havline-warning); } +.error .dot { background: var(--havline-error); } +.disabled .dot { background: var(--havline-disabled); } +.unknown .dot { background: var(--havline-text-muted); } .success .label { color: inherit; } .warning .label { color: inherit; } @@ -79,9 +79,9 @@ const label = computed(() => props.text ?? statusLabel(props.value)) .disabled .label { color: inherit; } .unknown .label { color: inherit; } -.status-badge:not(.pill) .success .label { color: var(--fonu-success); } -.status-badge:not(.pill) .warning .label { color: var(--fonu-warning); } -.status-badge:not(.pill) .error .label { color: var(--fonu-error); } +.status-badge:not(.pill) .success .label { color: var(--havline-success); } +.status-badge:not(.pill) .warning .label { color: var(--havline-warning); } +.status-badge:not(.pill) .error .label { color: var(--havline-error); } .status-badge:not(.pill) .disabled .label, -.status-badge:not(.pill) .unknown .label { color: var(--fonu-text-secondary); } +.status-badge:not(.pill) .unknown .label { color: var(--havline-text-secondary); } diff --git a/web/src/components/TaskProviderForm.vue b/web/src/components/TaskProviderForm.vue index a21a67b..acf9641 100644 --- a/web/src/components/TaskProviderForm.vue +++ b/web/src/components/TaskProviderForm.vue @@ -174,7 +174,7 @@ function providerIcon(v: string) { .provider-form__title { font-size: 14px; font-weight: 600; - color: var(--fonu-text); + color: var(--havline-text); } .provider-form__field { @@ -185,7 +185,7 @@ function providerIcon(v: string) { display: block; margin-bottom: 6px; font-size: 12px; - color: var(--fonu-text-secondary); + color: var(--havline-text-secondary); } .provider-select { @@ -210,7 +210,7 @@ function providerIcon(v: string) { overflow: hidden; flex-shrink: 0; background: #fff; - border: 1px solid var(--fonu-border); + border: 1px solid var(--havline-border); } .provider-logo--md { @@ -237,7 +237,7 @@ function providerIcon(v: string) { align-items: center; gap: 10px; font-size: 13px; - color: var(--fonu-text); + color: var(--havline-text); } .provider-form__actions { diff --git a/web/src/components/UpdateModal.vue b/web/src/components/UpdateModal.vue index c78cec0..7aed892 100644 --- a/web/src/components/UpdateModal.vue +++ b/web/src/components/UpdateModal.vue @@ -3,7 +3,7 @@
-

升级 Fonu

+

升级 Havline

升级完成后容器会重启,数据目录不会被删除

关闭 @@ -34,7 +34,7 @@

- 升级会重新构建应用镜像并只重建 fonu 容器。构建期间管理页面可能短暂无法访问, + 升级会重新构建应用镜像并只重建 havline 容器。构建期间管理页面可能短暂无法访问, 浏览器会自动等待服务恢复。

@@ -127,7 +127,7 @@ async function pollStatus() { localMessage.value = '' if (status.value.phase === 'success') { applying.value = false - message.success('Fonu 已升级完成') + message.success('Havline 已升级完成') emit('updated') window.setTimeout(() => window.location.reload(), 1500) return @@ -153,9 +153,9 @@ useVisibilityPolling(pollStatus, 2000, { enabled: applying }) .update-modal { width: 560px; max-width: 94vw; - background: var(--fonu-surface); - border-radius: var(--fonu-radius); - box-shadow: var(--fonu-shadow-md); + background: var(--havline-surface); + border-radius: var(--havline-radius); + box-shadow: var(--havline-shadow-md); overflow: hidden; } @@ -163,54 +163,54 @@ useVisibilityPolling(pollStatus, 2000, { enabled: applying }) display: flex; align-items: flex-start; justify-content: space-between; - gap: var(--fonu-space-3); - padding: var(--fonu-space-5) var(--fonu-space-5) var(--fonu-space-3); + gap: var(--havline-space-3); + padding: var(--havline-space-5) var(--havline-space-5) var(--havline-space-3); } .update-modal__title { margin: 0; font-size: 18px; font-weight: 600; - color: var(--fonu-text); + color: var(--havline-text); } .update-modal__subtitle { margin: 4px 0 0; font-size: 12px; - color: var(--fonu-text-muted); + color: var(--havline-text-muted); } .update-modal__body { display: grid; - gap: var(--fonu-space-4); - padding: 0 var(--fonu-space-5) var(--fonu-space-4); + gap: var(--havline-space-4); + padding: 0 var(--havline-space-5) var(--havline-space-4); } .update-modal__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--fonu-space-3); + gap: var(--havline-space-3); } .update-modal__grid > div { display: grid; gap: 3px; padding: 10px 12px; - border: 1px solid var(--fonu-border); - border-radius: var(--fonu-radius-sm); - background: var(--fonu-bg); + border: 1px solid var(--havline-border); + border-radius: var(--havline-radius-sm); + background: var(--havline-bg); } .update-modal__label { font-size: 12px; - color: var(--fonu-text-muted); + color: var(--havline-text-muted); } .update-modal__hint { margin: 0; font-size: 12px; line-height: 1.6; - color: var(--fonu-text-secondary); + color: var(--havline-text-secondary); } .update-modal__log { @@ -218,10 +218,10 @@ useVisibilityPolling(pollStatus, 2000, { enabled: applying }) margin: 0; padding: 12px; overflow: auto; - border-radius: var(--fonu-radius-sm); - background: var(--fonu-bg-muted); - color: var(--fonu-text-secondary); - font-family: var(--fonu-mono); + border-radius: var(--havline-radius-sm); + background: var(--havline-bg-muted); + color: var(--havline-text-secondary); + font-family: var(--havline-mono); font-size: 12px; line-height: 1.6; white-space: pre-wrap; @@ -230,9 +230,9 @@ useVisibilityPolling(pollStatus, 2000, { enabled: applying }) .update-modal__footer { display: flex; justify-content: flex-end; - gap: var(--fonu-space-3); - padding: var(--fonu-space-4) var(--fonu-space-5); - border-top: 1px solid var(--fonu-border); + gap: var(--havline-space-3); + padding: var(--havline-space-4) var(--havline-space-5); + border-top: 1px solid var(--havline-border); } @media (max-width: 640px) { diff --git a/web/src/composables/useTheme.ts b/web/src/composables/useTheme.ts index 73a0b19..339beae 100644 --- a/web/src/composables/useTheme.ts +++ b/web/src/composables/useTheme.ts @@ -1,7 +1,7 @@ import { computed, ref, watch } from 'vue' import { darkTheme, type GlobalThemeOverrides } from 'naive-ui' -const themeKey = 'fonu-theme' +const themeKey = 'havline-theme' const themeMode = ref(localStorage.getItem(themeKey) ?? 'system') function systemDark() { @@ -14,7 +14,7 @@ const shared = { primaryColorPressed: '#047857', primaryColorSuppl: '#34D399', borderRadius: '8px', - fontFamily: 'var(--fonu-font)', + fontFamily: 'var(--havline-font)', } const lightOverrides: GlobalThemeOverrides = { diff --git a/web/src/constants/app.ts b/web/src/constants/app.ts index 969b450..05f8432 100644 --- a/web/src/constants/app.ts +++ b/web/src/constants/app.ts @@ -1,4 +1,4 @@ -export const GITHUB_REPO = 'O96u/Fonu' +export const GITHUB_REPO = 'O96u/Havline' export const GITHUB_REPO_URL = `https://github.com/${GITHUB_REPO}` export const GITHUB_REPO_DISPLAY = `github.com/${GITHUB_REPO}` export const GITHUB_RELEASES_URL = `${GITHUB_REPO_URL}/releases` diff --git a/web/src/constants/notifyEvents.ts b/web/src/constants/notifyEvents.ts index 70247d1..6740d64 100644 --- a/web/src/constants/notifyEvents.ts +++ b/web/src/constants/notifyEvents.ts @@ -100,14 +100,14 @@ export const NOTIFY_EVENT_GROUPS: NotifyEventGroup[] = [ { key: 'on_agent_unreachable', label: '公网 agent 不可达', - hint: 'VPS 上的 fonu-agent 连续无响应(重启后没起来最常见),恢复时再通知一次', + hint: 'VPS 上的 havline-agent 连续无响应(重启后没起来最常见),恢复时再通知一次', hasThreshold: true, thresholdKey: 'agent_fail', }, { key: 'on_route_drift', label: '配置漂移', - hint: 'VPS 上的 vhost 与 Fonu 记录不一致(被手工改过或重装后未重部署),同一台按提醒间隔重复提醒', + hint: 'VPS 上的 vhost 与 Havline 记录不一致(被手工改过或重装后未重部署),同一台按提醒间隔重复提醒', hasThreshold: true, thresholdKey: 'drift_cooldown', }, diff --git a/web/src/layouts/AppLayout.vue b/web/src/layouts/AppLayout.vue index 7ef4ce4..00d3621 100644 --- a/web/src/layouts/AppLayout.vue +++ b/web/src/layouts/AppLayout.vue @@ -12,9 +12,9 @@ @expand="collapsed = false" > - +
-
Fonu
+
Havline
让 NAS 访问更简单
@@ -31,7 +31,7 @@ - + @@ -356,7 +356,7 @@
- + @@ -371,7 +371,7 @@

- 由 Fonu 本地生成;点「下发 frps 配置」后写入 VPS 的 + 由 Havline 本地生成;点「下发 frps 配置」后写入 VPS 的 {{ agentStatus?.frps_config_path || '/etc/frp/frps.toml' }},重启 frps 后生效。
密钥类字段(如 auth.token)在这里显示为掩码,下发时仍使用真实值。 @@ -487,7 +487,7 @@

frps 管理接口

-

用于 Fonu 拉取穿透规则的实时连接与流量信息;需在 frps 的 frps.toml 中开启 webServer(dashboard),与 frpc 连接用的令牌无关。

+

用于 Havline 拉取穿透规则的实时连接与流量信息;需在 frps 的 frps.toml 中开启 webServer(dashboard),与 frpc 连接用的令牌无关。

@@ -693,7 +693,7 @@ import { } from '@vicons/ionicons5' import EmptyState from '../components/EmptyState.vue' import ConfiguredSecretField from '../components/ConfiguredSecretField.vue' -import FonuCard from '../components/FonuCard.vue' +import HavlineCard from '../components/HavlineCard.vue' import LoadError from '../components/LoadError.vue' import PageHeader from '../components/PageHeader.vue' import StatusBadge from '../components/StatusBadge.vue' @@ -759,8 +759,8 @@ function portMeaning(port: string) { return `${portMeanings[port] ?? '端口'}:${portOn(port) ? '正在监听' : '未监听'}` } -// frps 运行一致性(P0-3):systemd 实际执行的启动命令,是否用的是 Fonu 下发的二进制与配置文件。 -// 不一致意味着:手工改过 unit,或者 Fonu 写入的 frps.toml 并不是 frps 实际加载的那一份。 +// frps 运行一致性(P0-3):systemd 实际执行的启动命令,是否用的是 Havline 下发的二进制与配置文件。 +// 不一致意味着:手工改过 unit,或者 Havline 写入的 frps.toml 并不是 frps 实际加载的那一份。 const frpsConsistency = computed(() => { const exec = agentStatus.value?.frps_unit_exec ?? '' if (!exec) return { ok: false, kind: 'warning' as const, text: '未获取', detail: '' } @@ -768,7 +768,7 @@ const frpsConsistency = computed(() => { const config = agentStatus.value?.frps_config_path ?? '' const problems: string[] = [] if (binary && !exec.includes(binary)) problems.push(`启动命令未使用配置的二进制(${binary})`) - if (config && !exec.includes(config)) problems.push(`启动命令未使用 Fonu 下发的配置文件(${config})`) + if (config && !exec.includes(config)) problems.push(`启动命令未使用 Havline 下发的配置文件(${config})`) if (problems.length > 0) { return { ok: false, kind: 'warning' as const, text: '不一致', detail: problems.join(';') } } @@ -782,10 +782,10 @@ const frpsVersionOptions = computed(() => releases.value.map((release) => ({ lab // O:最后检查时间(PageHeader 副标题,判断数据新鲜度) const headerDescription = computed(() => { - if (!server.value) return '管理公网 VPS 服务端(frps)与其上的 fonu-agent' + if (!server.value) return '管理公网 VPS 服务端(frps)与其上的 havline-agent' if (!statusLoaded.value) return '正在读取 VPS 状态…' const checkedAt = agentStatus.value?.checked_at - return checkedAt ? `管理部署在公网 VPS 上的 fonu-agent 与 frps · 检查于 ${formatRelativeTime(checkedAt)}` : '管理部署在公网 VPS 上的 fonu-agent 与 frps 服务' + return checkedAt ? `管理部署在公网 VPS 上的 havline-agent 与 frps · 检查于 ${formatRelativeTime(checkedAt)}` : '管理部署在公网 VPS 上的 havline-agent 与 frps 服务' }) // 三态:未加载 / 加载中 / 失败——避免把"没拉到"显示成"未连接" @@ -821,7 +821,7 @@ const statusAlert = computed<{ type: 'error' | 'warning'; text: string; action?: if (!agentStatusOk.value) { return { type: 'error', - text: 'Agent 未连接:请确认 VPS 上 fonu-agent 正在运行,且 Agent 地址与 Token 与页面一致。', + text: 'Agent 未连接:请确认 VPS 上 havline-agent 正在运行,且 Agent 地址与 Token 与页面一致。', action: { label: '探测环境', run: () => probe(current) }, } } @@ -891,7 +891,7 @@ async function load() { void loadServerDiagnostics(servers.value) void loadRuntimeVersion() if (server.value) await refreshAll({ silent: false }) - void loadFonuCerts() + void loadHavlineCerts() } catch (error) { loadError.value = error instanceof Error ? error.message : '读取服务端失败' } } @@ -949,12 +949,12 @@ const metricsUptimeText = computed(() => agentMetrics.value?.uptime_seconds ? formatUptime(agentMetrics.value.uptime_seconds) : '—', ) -// VPS 证书清单(需 agent ≥ 0.11.0):与 Fonu 证书库交叉核对,宝塔自签的证书也能看到 +// VPS 证书清单(需 agent ≥ 0.11.0):与 Havline 证书库交叉核对,宝塔自签的证书也能看到 const agentCerts = ref([]) const certWarnings = ref([]) const certsError = ref('') const certsLoading = ref(false) -const fonuCertDomains = ref>(new Set()) +const havlineCertDomains = ref>(new Set()) async function loadAgentCerts(serverID: number) { certsLoading.value = true @@ -972,8 +972,8 @@ async function loadAgentCerts(serverID: number) { } } -// Fonu 证书库里的域名(含通配符),用于判断「VPS 上有但 Fonu 没有」 -async function loadFonuCerts() { +// Havline 证书库里的域名(含通配符),用于判断「VPS 上有但 Havline 没有」 +async function loadHavlineCerts() { try { const records = await api.listCertificates() const domains = new Set() @@ -981,9 +981,9 @@ async function loadFonuCerts() { domains.add(record.domain) for (const item of record.domains ?? []) domains.add(item) } - fonuCertDomains.value = domains + havlineCertDomains.value = domains } catch { - fonuCertDomains.value = new Set() + havlineCertDomains.value = new Set() } } @@ -1074,7 +1074,7 @@ function pushAgentConfig(target: FrpServer) { () => ({ text: 'frps 同源配置已下发到 VPS,点「重启 frps」后生效', ok: true })) } -// 配置查看:只读,展示 Fonu 本地生成的 frps.toml(即「下发」会写入 VPS 的内容) +// 配置查看:只读,展示 Havline 本地生成的 frps.toml(即「下发」会写入 VPS 的内容) async function viewConfig(target: FrpServer) { configLoading.value = true try { @@ -1241,8 +1241,8 @@ const serverHelpItems = computed(() => { const common = [ { title: '基本信息', text: '主机地址和端口对应 frps 的 bindAddr / bindPort,多个服务端可并行配置,规则保存时选择归属。' }, { title: '认证方式', text: '与 frps 的 auth 配置保持一致:无认证、令牌(auth.token)或 OIDC;令牌与密钥加密存储,编辑时留空保持不变。' }, - { title: 'frps 管理接口', text: 'frps 开启 webServer(dashboard)后填写,Fonu 借此拉取穿透规则的实时连接与流量;不写入 frpc.toml,与连接认证无关。' }, - { title: '描述', text: '仅用于在 Fonu 中区分多台服务端的备注信息。' }, + { title: 'frps 管理接口', text: 'frps 开启 webServer(dashboard)后填写,Havline 借此拉取穿透规则的实时连接与流量;不写入 frpc.toml,与连接认证无关。' }, + { title: '描述', text: '仅用于在 Havline 中区分多台服务端的备注信息。' }, ] if (serverForm.options.auth_method === 'oidc') return [...common, { title: 'OIDC', text: '客户端 ID、密钥和令牌端点由身份提供商提供;受众与权限范围需与 frps 的 OIDC 校验配置匹配。' }] return common @@ -1675,90 +1675,90 @@ useVisibilityPolling(() => { diff --git a/web/src/views/ExampleView.vue b/web/src/views/ExampleView.vue index 5d2158e..d768118 100644 --- a/web/src/views/ExampleView.vue +++ b/web/src/views/ExampleView.vue @@ -38,24 +38,24 @@
- +
  • 页面版本提示更新为 v1.9.0。
  • 「本次升级验证」区块和页面状态同步更新为 v1.9.0。
  • 验证标识更新为 v1.9.0-example-page。
-
+ - +
  1. 在旧版本页面点击侧栏的「一键升级」。
  2. 等待容器重建完成,页面自动刷新或恢复访问。
  3. 打开侧栏「示例页面」,确认本页可访问。
  4. 确认页面上的版本号为 v{{ version }}。
-
+ - +
/example
已加载「示例页面」
@@ -64,7 +64,7 @@
{{ checkedAt || '刚刚' }}
-
+ - - diff --git a/web/src/views/FrpView.vue b/web/src/views/FrpView.vue index 8b78509..71ffdd7 100644 --- a/web/src/views/FrpView.vue +++ b/web/src/views/FrpView.vue @@ -133,9 +133,9 @@
- +
{{ status.last_error }}
-
+
  • IP 来源留空(默认):每次同步自动检测本机公网 IP——家庭宽带 NAS 场景,域名指向家里出口。
  • IP 来源自定义:DDNS 页选中任务 → 「自定义 IP」填 VPS 的 IP,域名直接解析到 VPS(Web 服务经 VPS 对外时使用)。须为公网地址,内网 IP 会被拒绝。
  • -
  • VPS 动态 IP:DDNS 自定义 IP 填当前 VPS IP,FRP 服务端「主机地址」填 DDNS 域名而非裸 IP——VPS 换 IP 后域名自动跟随,frpc 重连无需改 Fonu 配置。
  • +
  • VPS 动态 IP:DDNS 自定义 IP 填当前 VPS IP,FRP 服务端「主机地址」填 DDNS 域名而非裸 IP——VPS 换 IP 后域名自动跟随,frpc 重连无需改 Havline 配置。
  • 三、与反向代理的组合(按需求选型)

      -
    • 多 Web 应用统一入口(推荐):FRP 一条 tcp 规则,远程端口 443,内网目标 127.0.0.1:443(Fonu Nginx 的 HTTPS 监听);反向代理页按域名分流到各内网应用,TLS 与证书都在本地 Nginx 终止。加规则只改反代,不动 FRP。
    • +
    • 多 Web 应用统一入口(推荐):FRP 一条 tcp 规则,远程端口 443,内网目标 127.0.0.1:443(Havline Nginx 的 HTTPS 监听);反向代理页按域名分流到各内网应用,TLS 与证书都在本地 Nginx 终止。加规则只改反代,不动 FRP。
    • HTTP/HTTPS 直通:frps 按域名分流,不需要本地反代。存在 http/https 规则时,本页生成的 frps.toml 会自动带上 vhostHTTPPort / vhostHTTPSPort;FRP 规则选 http / https 类型并填自定义域名。TLS 终止在 VPS 侧或上游。
    • 端口映射:SSH、远程桌面、数据库等非 HTTP 服务——tcp / udp 规则 + 远程端口,访问 VPS_IP:远程端口 即达内网。
    • STCP 安全穿透:管理入口不想暴露公网端口时用 stcp,只放行 frps 的 bindPort;访问端运行同 secretKey 的 visitor 配置连回内网。
    • @@ -440,13 +440,13 @@ → DNS 解析到 VPS IP → VPS:443 → frps → FRP TCP 隧道(远程端口 443) → NAS frpc → 127.0.0.1:443 - → Fonu Nginx(本地证书终止 TLS,按域名分流)→ 内网应用 + → Havline Nginx(本地证书终止 TLS,按域名分流)→ 内网应用
      1. VPS 部署 frps:服务端「详情 → frps.toml」生成同源配置,放行 bindPort 与 443;
      2. DDNS:解析 example.com / *.example.com 到 VPS IP(「自定义 IP」填 VPS IP);
      3. 证书:为 *.example.com 申请通配符证书(DNS-01 复用 DDNS 凭证);
      4. FRP 服务端:添加 VPS,Token 与 frps 一致;
      5. -
      6. FRP 规则:类型 tcp,远程端口 443,内网目标 127.0.0.1:443(Fonu Nginx 的 HTTPS 监听);
      7. +
      8. FRP 规则:类型 tcp,远程端口 443,内网目标 127.0.0.1:443(Havline Nginx 的 HTTPS 监听);
      9. 反向代理:按域名建规则(HTTPS + 强制跳转 + 选证书),目标为各内网应用;以后加应用只改反代,不动 FRP;
      10. 「应用配置」校验并应用 → 服务端「详情」看诊断与日志验证。
      @@ -457,7 +457,7 @@
      1. DDNS「自定义 IP」填当前 VPS IP(注意:留空检测的是 NAS 本机出口 IP,两者语义不同);
      2. FRP 服务端「主机地址」填 DDNS 域名(如 vps.example.com)而非裸 IP;
      3. -
      4. VPS IP 变化后 DDNS 定时任务自动跟随,frpc 重连无需改 Fonu 配置;
      5. +
      6. VPS IP 变化后 DDNS 定时任务自动跟随,frpc 重连无需改 Havline 配置;
      7. 服务端「日志」确认重新出现 login to server success。

      通常与场景 A / C 叠加:DDNS 保证「地址跟着 VPS 走」,FRP 保证「隧道通」。

      @@ -499,7 +499,7 @@
      用户 → https://app.example.com
            → DNS(DDNS 自动更新到宽带出口 IP)
            → 家庭宽带路由器端口映射 :443
      -     → NAS Fonu Nginx(本地证书)→ 内网应用
      + → NAS Havline Nginx(本地证书)→ 内网应用
      1. 确认宽带有公网 IP(没有则只能走 FRP 中转或 IPv6 直连);
      2. DDNS IP 来源留空(自动检测本机出口);
      3. @@ -534,10 +534,10 @@

        适用:域名挂在 Cloudflare 等 CDN 后面,隐藏源站 IP、边缘加速。

        用户 → CDN(边缘 TLS)
              → 回源 → VPS frps :远程端口(FRP TCP 隧道)
        -     → NAS Fonu Nginx(HTTP 或本地证书)→ 内网应用
        + → NAS Havline Nginx(HTTP 或本地证书)→ 内网应用
        1. CDN 侧添加域名,DNS 由 CDN 托管(此时可不建 DDNS,或 DDNS 只用于 FRP 服务端地址,见场景 B);
        2. -
        3. FRP tcp 规则:远程端口如 8443,内网目标指向 Fonu Nginx 的对应监听;
        4. +
        5. FRP tcp 规则:远程端口如 8443,内网目标指向 Havline Nginx 的对应监听;
        6. 反代 HTTPS 按回源模式决定(Flexible → HTTP 回源可关 443;Full → 需本地证书);
        7. 真实客户端 IP 来自 CDN,需在设置中配置信任代理网段,IP 黑白名单 / 限流才基于真实 IP 生效。
        @@ -656,7 +656,7 @@ import { useVisibilityPolling } from '../composables/useVisibilityPolling' import { formatBytes, formatDate, formatRate } from '../utils/format' import type { FrpBinary, FrpDownloadTask, FrpProxy, FrpProxyOptions, FrpProxyPayload, FrpRelease, FrpRuntimeInfo, FrpServer, FrpStatus } from '../api/types' -import FonuCard from '../components/FonuCard.vue' +import HavlineCard from '../components/HavlineCard.vue' import EmptyState from '../components/EmptyState.vue' import LoadError from '../components/LoadError.vue' import PageHeader from '../components/PageHeader.vue' @@ -1203,89 +1203,89 @@ useVisibilityPolling(refreshDownloadTask, 500, { enabled: downloadPolling }) diff --git a/web/src/views/LoginView.vue b/web/src/views/LoginView.vue index a509f42..5a82ef5 100644 --- a/web/src/views/LoginView.vue +++ b/web/src/views/LoginView.vue @@ -6,11 +6,11 @@
        - +
        -

        Fonu

        +

        Havline

        让 NAS 访问更简单

        @@ -57,7 +57,7 @@ import { useRouter } from 'vue-router' import { NButton, NForm, NFormItem, NInput, useMessage } from 'naive-ui' import { api } from '../api/client' -import logoImg from '../assets/brand/logo.png' +import logoImg from '../assets/brand/havline-logo.svg' @@ -111,9 +111,9 @@ async function submit() { place-items: center; - padding: var(--fonu-space-5); + padding: var(--havline-space-5); - background: var(--fonu-bg); + background: var(--havline-bg); } @@ -125,15 +125,15 @@ async function submit() { max-width: 420px; - background: var(--fonu-surface); + background: var(--havline-surface); - border: 1px solid var(--fonu-border); + border: 1px solid var(--havline-border); - border-radius: var(--fonu-radius); + border-radius: var(--havline-radius); - box-shadow: var(--fonu-shadow-md); + box-shadow: var(--havline-shadow-md); - padding: var(--fonu-space-6) var(--fonu-space-5); + padding: var(--havline-space-6) var(--havline-space-5); } @@ -145,9 +145,9 @@ async function submit() { align-items: center; - gap: var(--fonu-space-4); + gap: var(--havline-space-4); - margin-bottom: var(--fonu-space-5); + margin-bottom: var(--havline-space-5); } @@ -187,7 +187,7 @@ async function submit() { font-size: 13px; - color: var(--fonu-text-muted); + color: var(--havline-text-muted); } @@ -195,23 +195,22 @@ async function submit() { .auth-title { - margin: 0 0 var(--fonu-space-5); + margin: 0 0 var(--havline-space-5); font-size: 16px; font-weight: 600; - color: var(--fonu-text); + color: var(--havline-text); } .auth-hint { - margin: var(--fonu-space-4) 0 0; + margin: var(--havline-space-4) 0 0; font-size: 13px; line-height: 1.5; - color: var(--fonu-text-muted); + color: var(--havline-text-muted); } - diff --git a/web/src/views/LogsView.vue b/web/src/views/LogsView.vue index d5e1635..d08435b 100644 --- a/web/src/views/LogsView.vue +++ b/web/src/views/LogsView.vue @@ -1,12 +1,12 @@