From fb57b2aef82c6e2c5f02ce60017008a97944ce13 Mon Sep 17 00:00:00 2001
From: mrlee <470349233@qq.com>
Date: Sun, 11 Oct 2026 13:23:56 +0800
Subject: [PATCH] =?UTF-8?q?chore:=20=E5=90=8C=E6=AD=A5=E7=89=88=E6=9C=AC?=
=?UTF-8?q?=E5=8F=B7=E5=88=B0=201.9.2=E3=80=81=E6=95=B4=E7=90=86=20CHANGEL?=
=?UTF-8?q?OG=20=E5=B9=B6=E7=A7=BB=E9=99=A4=E8=AF=AF=E6=8F=90=E4=BA=A4?=
=?UTF-8?q?=E7=9A=84=20exe?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- README 版本徽章与飞牛安装包名对齐到 1.9.2
- web/package-lock.json 根版本与 web/package.json 保持一致(原为 1.9.0)
- CHANGELOG 合并整理 1.1.0 起的相近补丁版本并重新编排版本号
- 取消跟踪 Windows 构建产物 havline-agent.exe,并加入 .gitignore
---
.gitignore | 2 +
CHANGELOG.md | 879 +++++++++---------------------------------
README.md | 4 +-
havline-agent.exe | Bin 10972160 -> 0 bytes
web/package-lock.json | 4 +-
5 files changed, 179 insertions(+), 710 deletions(-)
delete mode 100644 havline-agent.exe
diff --git a/.gitignore b/.gitignore
index 6e8dbfa..f02c537 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,8 @@ dist/
.cursor/
.playwright-cli/
output/
+# Windows 本地构建产物不入库(历史上误提交过 havline-agent.exe,10MB 级二进制)
+*.exe
web/public/111favicon.png
web/src/assets/brand/logo00.png
# scripts/ 下的本地辅助脚本不入库,但随仓库分发的 agent 安装脚本需要跟踪
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40adea5..df8691e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
All notable changes to this project will be documented in this file.
+> 说明:1.1.0 起的相近补丁版本已合并整理并重新编排版本号;0.1.0-1.0.2 保持原样。
+
## [v1.9.2] - 2026-10-07
### 新增
@@ -18,6 +20,12 @@ All notable changes to this project will be documented in this file.
- **公网反代隧道提示修正**:frps 管理接口可访问但没有任何注册代理时,不再显示 `frps 已注册代理: ;`,改为提示 frpc 未登录或尚未注册该域名
- **旧服务端配置的布尔默认值补齐**:`config_json` 缺少 `tcp_mux` / `login_fail_exit` 键时(016 迁移给已有服务端补的默认值是 `{}`)按表单默认值补成 `true`,不再静默生成 `transport.tcpMux = false`(每条新连接都要重建 TCP+TLS,首次建连明显变慢)与 `loginFailExit = false`;显式配置为 `false` 的仍然保留
+### 变更
+
+- **品牌全量切换为 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 来源记录
+
### 测试
- 新增 Agent 操作结果解析、Agent 版本解析、frps vhost 端口冲突校验和隧道提示文案测试
@@ -25,12 +33,6 @@ All notable changes to this project will be documented in this file.
- 新增服务端选项缺省 `tcp_mux` / `login_fail_exit` 键时的默认值兜底测试
- `go test ./...`、`npm run typecheck` 通过
-### 变更
-
-- **品牌全量切换为 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
### 新增
@@ -54,15 +56,15 @@ All notable changes to this project will be documented in this file.
- **Cloudflare DNS 安全清理**:只删除 Fonu 接管或创建的 DNS 记录,不覆盖用户已有的其他目标 CNAME
- **Cloudflare 隧道删除保护**:仍被反向代理规则引用的托管隧道不允许删除或切换为手工隧道
+### 变更
+
+- `AgentVersion` 保持 **0.14.1**,本次无 Agent 行为变化
+
### 测试
- 新增 Cloudflare 出口默认值与持久化、托管规则生成 ingress、健康状态与可用率、路由解析、通配 path、DNS 记录归属、漂移比较、备份包含隧道配置等测试
- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test -- --run`、`npm run build` 通过
-### 变更
-
-- 版本号提升到 1.9.0;`AgentVersion` 保持 **0.14.1**,本次无 Agent 行为变化
-
## [v1.8.0] - 2026-09-30
### 新增
@@ -75,7 +77,7 @@ All notable changes to this project will be documented in this file.
### 变更
-- 版本号提升到 1.8.0;`AgentVersion` 保持 **0.14.1**
+- `AgentVersion` 保持 **0.14.1**
### 测试
@@ -95,7 +97,7 @@ All notable changes to this project will be documented in this file.
### 变更
-- 版本号提升到 1.7.0;`AgentVersion` 保持 **0.14.1**
+- `AgentVersion` 保持 **0.14.1**
### 测试
@@ -103,39 +105,6 @@ All notable changes to this project will be documented in this file.
## [v1.6.9] - 2026-09-28
-### 修复
-
-- **测试连接未使用当前输入的 Account ID**:测试连接原先只读取数据库中已保存的 Account ID,未保存直接点测试会误报「请先填写 Account ID」。现在前端会把输入框里的 Account ID 与 API Token 一起提交,后端优先使用输入值,留空时才回退到已保存配置
-
-### 变更
-
-- 版本号提升到 1.6.9;`AgentVersion` 保持 **0.14.1**
-
-### 测试
-
-- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
-
-## [v1.6.8] - 2026-09-28
-
-### 变更
-
-- **Cloudflare 凭据模型改为 Account ID + API Token**:应用配置不再要求 Tunnel Token;Fonu 用 Account ID + API Token 调用 Cloudflare API:
- - 「测试连接」调用 `GET /accounts/{account_id}/tokens/verify`
- - 新建隧道时自动创建 `config_src=local` 的命名隧道、获取凭据、生成本地 `config.yml` 与 `creds.json`
- - 删除隧道时同步删除云端隧道;编辑时同步推送 ingress
-- 隧道弹窗去掉 Token 与模式选择,只保留名称、暴露域名、回源 service 与自动启动
-- 新增 `cf_settings.account_id` 字段(迁移 `033`)
-
-### 变更
-
-- 版本号提升到 1.6.8;`AgentVersion` 保持 **0.14.1**
-
-### 测试
-
-- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
-
-## [v1.6.7] - 2026-09-28
-
### 新增
- **API Token 连接验证**:应用配置的 Token 区域拆分为两个输入:
@@ -144,26 +113,24 @@ All notable changes to this project will be documented in this file.
- 「测试连接」优先使用 API Token;未配置 API Token 时回退到隧道 Token 的临时连接测试;输入以 `cfat_` 开头时也会自动走 API 验证
- 新增 `cf_settings.api_token_enc` 字段(迁移 `032`),API Token 独立加密存储与掩码回显
-### 变更
-
-- 版本号提升到 1.6.7;`AgentVersion` 保持 **0.14.1**
-
-### 测试
-
-- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
-
-## [v1.6.6] - 2026-09-28
-
### 修复
+- **测试连接未使用当前输入的 Account ID**:测试连接原先只读取数据库中已保存的 Account ID,未保存直接点测试会误报「请先填写 Account ID」。现在前端会把输入框里的 Account ID 与 API Token 一起提交,后端优先使用输入值,留空时才回退到已保存配置
- **Token 解码兼容 URL-safe base64**:Cloudflare 面板生成的 Tunnel Token 使用 URL-safe base64(含 `-` / `_`),原先只按标准 base64 解码,会报 `illegal base64 data at input byte N` 并返回 502。现在同时兼容标准 / URL-safe 与有无填充四种形式,失败时给出更明确的提示
### 变更
-- 版本号提升到 1.6.6;`AgentVersion` 保持 **0.14.1**
+- `AgentVersion` 保持 **0.14.1**
+- **Cloudflare 凭据模型改为 Account ID + API Token**:应用配置不再要求 Tunnel Token;Fonu 用 Account ID + API Token 调用 Cloudflare API:
+ - 「测试连接」调用 `GET /accounts/{account_id}/tokens/verify`
+ - 新建隧道时自动创建 `config_src=local` 的命名隧道、获取凭据、生成本地 `config.yml` 与 `creds.json`
+ - 删除隧道时同步删除云端隧道;编辑时同步推送 ingress
+- 隧道弹窗去掉 Token 与模式选择,只保留名称、暴露域名、回源 service 与自动启动
+- 新增 `cf_settings.account_id` 字段(迁移 `033`)
### 测试
+- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
- 新增 URL-safe 与无填充 token 解码测试
## [v1.6.5] - 2026-09-28
@@ -174,68 +141,20 @@ All notable changes to this project will be documented in this file.
- 「测试连接」:后端用 cloudflared 临时启动一次连接,最多等待 25 秒,确认 token 能注册到 Cloudflare 边缘;成功后显示隧道 ID,失败给出明确原因
- 「获取 Token」:直达 Cloudflare Zero Trust 面板
- 新增 `POST /api/cloudflare/settings/test` 接口;请求体 token 留空时使用已保存的默认 token
-
-### 变更
-
-- 版本号提升到 1.6.5;`AgentVersion` 保持 **0.14.1**
-
-### 测试
-
-- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
-
-## [v1.6.4] - 2026-09-28
-
-### 变更
-
-- **新建 / 编辑隧道弹窗去重**:移除与应用配置重复的 Token 输入与网络调优字段;Token、传输协议、边缘地址族、HA 连接数、代理模式统一在「应用配置」中维护,保存时自动应用到新建或编辑的隧道
-- 新建隧道前若未配置默认 Token,弹窗内直接提示并阻止提交;编辑已有隧道不受影响
-- 隧道弹窗现在只保留名称、模式、暴露域名、回源 service 与自动启动
-
-### 变更
-
-- 版本号提升到 1.6.4;`AgentVersion` 保持 **0.14.1**
-
-### 测试
-
-- `npm run typecheck`、`npm test`、`npm run build` 通过
-
-## [v1.6.3] - 2026-09-28
-
-### 新增
-
- **Cloudflare 应用配置**:「cloudflared 二进制」弹窗升级为「应用配置」,包含三块:
- 程序安装:当前版本、下载镜像、下载并安装
- Token 配置:保存模块级默认 token,掩码回显;新建隧道时可以留空,直接使用默认 token
- 默认网络设置:传输协议、边缘地址族、HA 连接数、代理模式,作为新建隧道的默认值
- 新增 `cf_settings` 表(迁移 `031`)与 `GET / PUT /api/cloudflare/settings` 接口
-
-### 变更
-
-- 版本号提升到 1.6.3;`AgentVersion` 保持 **0.14.1**
-
-### 测试
-
-- 新增应用配置与默认 token 的端到端单元测试
-- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
-
-## [v1.6.2] - 2026-09-28
+- **Cloudflare 隧道独立模块(阶段 1–2)**:新增 `internal/cloudflared` 包,覆盖 token 解码、`creds.json` / `config.yml` 生成、ingress 生成器、SQLite 存储、cloudflared 二进制下载(镜像表 + 大小与可执行校验)、子进程托管、日志追加、metrics 端口发现与健康抓取
+- 新增 `cf_tunnels` 表与 `/api/cloudflare/*` 接口:列表、创建、更新、删除、启动、停止、重启、状态、日志、配置预览、二进制信息与下载、ingress 候选域名
+- 前端新增「Cloudflare 隧道」页面:隧道列表、创建 / 编辑、启停、配置预览、日志、二进制下载与网络调优设置
+- 支持 `token-local`(Fonu 生成本地 ingress)与 `token-remote`(Cloudflare 面板管理规则)两种模式;界面显示模式差异
+- 网络调优默认值:`http2` / IPv4 / HA 2 / 跟随系统代理;ingress 锁定 catch-all、`127.0.0.1` 归一化与稳定排序三条不变式
### 修复
- **Cloudflare 隧道弹窗宽度**:`n-modal` 的宽度原先写在 scoped class 上,没有传到 naive-ui 的 modal 根节点,部分视口下弹窗被拉成 100%。现在改为与其他页面一致的 `:style` 写法:编辑弹窗 `min(720px, 96vw)`、详情弹窗 `min(760px, 96vw)`、二进制弹窗 `min(560px, 94vw)`
-
-### 变更
-
-- 版本号提升到 1.6.2;`AgentVersion` 保持 **0.14.1**
-
-### 测试
-
-- `npm run typecheck`、`npm run build` 通过
-
-## [v1.6.1] - 2026-09-28
-
-### 修复
-
- **Cloudflare 隧道弹窗完善**:编辑弹窗回填已配置的暴露域名与回源 service(后端从 `config.yml` 解析);按「基本信息 / 暴露规则 / 网络调优」分组,增加模式说明;云端规则模式隐藏本地域名配置并提示规则由 Cloudflare 面板管理
- **详情弹窗**:改为状态卡 + `config.yml` / 日志分页,日志每 3 秒自动刷新,支持一键复制配置,刷新按钮带加载状态
- **二进制弹窗**:显示当前版本与目标文件;镜像列表为空时回退官方源
@@ -243,30 +162,16 @@ All notable changes to this project will be documented in this file.
### 变更
-- 版本号提升到 1.6.1;`AgentVersion` 保持 **0.14.1**,本次无 Agent 行为变化
+- **新建 / 编辑隧道弹窗去重**:移除与应用配置重复的 Token 输入与网络调优字段;Token、传输协议、边缘地址族、HA 连接数、代理模式统一在「应用配置」中维护,保存时自动应用到新建或编辑的隧道
+- 新建隧道前若未配置默认 Token,弹窗内直接提示并阻止提交;编辑已有隧道不受影响
+- 隧道弹窗现在只保留名称、模式、暴露域名、回源 service 与自动启动
+- `AgentVersion` 保持 **0.14.1**,本次无 Agent 行为变化
### 测试
- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
-
-## [v1.6.0] - 2026-09-28
-
-### 新增
-
-- **Cloudflare 隧道独立模块(阶段 1–2)**:新增 `internal/cloudflared` 包,覆盖 token 解码、`creds.json` / `config.yml` 生成、ingress 生成器、SQLite 存储、cloudflared 二进制下载(镜像表 + 大小与可执行校验)、子进程托管、日志追加、metrics 端口发现与健康抓取
-- 新增 `cf_tunnels` 表与 `/api/cloudflare/*` 接口:列表、创建、更新、删除、启动、停止、重启、状态、日志、配置预览、二进制信息与下载、ingress 候选域名
-- 前端新增「Cloudflare 隧道」页面:隧道列表、创建 / 编辑、启停、配置预览、日志、二进制下载与网络调优设置
-- 支持 `token-local`(Fonu 生成本地 ingress)与 `token-remote`(Cloudflare 面板管理规则)两种模式;界面显示模式差异
-- 网络调优默认值:`http2` / IPv4 / HA 2 / 跟随系统代理;ingress 锁定 catch-all、`127.0.0.1` 归一化与稳定排序三条不变式
-
-### 变更
-
-- 版本号提升到 1.6.0;`AgentVersion` 保持 **0.14.1**,本次无 Agent 行为变化
-
-### 测试
-
+- 新增应用配置与默认 token 的端到端单元测试
- `internal/cloudflared`:token 解码、ingress 不变式、config.yml、metrics 解析、启动参数、镜像 URL、代理禁用单元测试
-- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
### 未实施
@@ -275,23 +180,6 @@ All notable changes to this project will be documented in this file.
## [v1.5.4] - 2026-09-28
-### 变更
-
-- **P1-5 统一前端轮询组合式函数**:`useVisibilityPolling` 重写为支持 `enabled` / `immediate` / `onError` 选项,加入 in-flight 互斥,返回 `start` / `stop` / `busy` / `lastError`;核心逻辑抽成 `createVisibilityPoller`,可在无 DOM 环境下测试
-- Dashboard、Proxy、Logs、Settings、Frp、UpdateModal、Agent 的轮询全部迁移到组合式函数;仅 AppLayout 时钟保留原生计时器
-- 慢接口不再堆积:上一轮未结束时跳过下一轮;页面隐藏时暂停,回到前台立即补一次
-
-### 测试
-
-- 新增 6 项组合式函数单元测试:隐藏不执行、回到前台补一次、in-flight 互斥、`enabled=false`、卸载清理、`onError` 后继续轮询
-- `npm run typecheck`、`npm test`(25 项)、`npm run build` 通过
-
-### 变更
-
-- 版本号提升到 1.5.4;`AgentVersion` 保持 **0.14.1**,本次无 Agent 行为变化
-
-## [v1.5.3] - 2026-09-28
-
### 新增
- **P1-4 路由鉴权覆盖测试**:API 路由收敛为可遍历路由表,新增 `internal/api/router_auth_test.go` 遍历 100+ 受保护路由,匿名请求必须返回 401,公开路由不被鉴权误拦;Agent 路由收敛为 `agentRoutes` 声明,新增 `internal/agent/router_auth_test.go` 遍历全部 `/api/v1/*` 验证无 Token 必须 401
@@ -299,18 +187,6 @@ All notable changes to this project will be documented in this file.
- **Agent 版本兼容测试**:新增 `internal/frp/agent_versions_compat_test.go`,覆盖 404 / 405 判定端点缺失并提示最低版本与「升级 Agent」、400 / 500 不误判、状态接口 `agent_version` 解析
- **前端版本字段降级测试**:新增 `agentVersionText` 工具与测试,旧 Agent 缺少 `agent_version` 时显示占位符而不是空白
-### 变更
-
-- 版本号提升到 1.5.3;`AgentVersion` 保持 **0.14.1**,本次无 Agent 行为变化
-- Agent 的 nginx 校验与 reload 增加测试注入钩子;生产路径仍调用真实 nginx,行为不变
-
-### 测试
-
-- `go test ./...`、`go vet ./...` 通过
-- `npm run typecheck`、`npm test`(19 项)、`npm run build` 通过
-
-## [v1.5.2] - 2026-09-28
-
### 修复
- **升级 Agent 不再强制切回 SSH 隧道**:HTTPS 直连模式下点击「升级 Agent」原先会无条件建立隧道并把 transport 改回 `tunnel`,用户升级后需要重新切换 HTTPS。现在升级前是 `https-pin` 时会保持 HTTPS 直连,只更新 Token 密文并重新验证连通
@@ -318,24 +194,21 @@ All notable changes to this project will be documented in this file.
### 变更
-- 版本号提升到 1.5.2;`AgentVersion` 保持 **0.14.1**,本次是中央端修复,不需要重新升级 Agent
-
-### 测试
-
-- `internal/frp` 现有单元测试与编译校验通过;升级分支依赖真实 SSH/Agent 环境,未新增本地集成测试
-
-## [v1.5.1] - 2026-09-28
-
-### 变更
-
+- **P1-5 统一前端轮询组合式函数**:`useVisibilityPolling` 重写为支持 `enabled` / `immediate` / `onError` 选项,加入 in-flight 互斥,返回 `start` / `stop` / `busy` / `lastError`;核心逻辑抽成 `createVisibilityPoller`,可在无 DOM 环境下测试
+- Dashboard、Proxy、Logs、Settings、Frp、UpdateModal、Agent 的轮询全部迁移到组合式函数;仅 AppLayout 时钟保留原生计时器
+- 慢接口不再堆积:上一轮未结束时跳过下一轮;页面隐藏时暂停,回到前台立即补一次
+- Agent 的 nginx 校验与 reload 增加测试注入钩子;生产路径仍调用真实 nginx,行为不变
- **P1-1 共享 Nginx 模板包**:新增纯生成包 `internal/nginxtmpl`,统一 TLS 指令、安全响应头、allow / deny、WebSocket 头、限流指令与 zone 命名;本机 `internal/nginx` 与公网 Agent 均改为调用共享函数,重复模板实现已删除
- **安全响应头对齐**:两端统一输出 HSTS、`X-Content-Type-Options`、`X-Frame-Options`、`Referrer-Policy`,本机此前缺少 `Referrer-Policy`
- **访问控制顺序统一**:统一为黑名单 `deny` → 仅大陆规则 → 白名单 `allow` → `deny all`,黑名单优先匹配
- **TLS 指令显式化**:两端统一显式输出 `ssl_protocols TLSv1.2 TLSv1.3` 或 `TLSv1.3`
-- 版本号提升到 1.5.1;`AgentVersion` 0.14.0 → **0.14.1**,内嵌 amd64 / arm64 二进制已重编;VPS 上需点一次「升级 Agent」才会生效
+- `AgentVersion` 0.14.0 → **0.14.1**(由 1.5.1 引入;1.5.2-1.5.4 为中央端修复,不需要再次升级 Agent),内嵌 amd64 / arm64 二进制已重编;VPS 上需点一次「升级 Agent」才会生效
### 测试
+- 新增 6 项组合式函数单元测试:隐藏不执行、回到前台补一次、in-flight 互斥、`enabled=false`、卸载清理、`onError` 后继续轮询
+- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
+- `internal/frp` 现有单元测试与编译校验通过;升级分支依赖真实 SSH/Agent 环境,未新增本地集成测试
- `internal/nginxtmpl`:新增共享函数的 TLS、安全头、访问控制、限流、zone 与 WebSocket 特征化测试
- `internal/nginx`:本机生成测试继续通过;`internal/agent`:新增 vhost 顺序与共享语义测试
@@ -353,7 +226,6 @@ All notable changes to this project will be documented in this file.
### 变更
-- 版本号提升到 1.5.0;`web/package.json` 与 `web/package-lock.json` 同步对齐
- `AgentVersion` 0.13.3 → **0.14.0**,内嵌 amd64 / arm64 二进制已重编;VPS 上需点一次「升级 Agent」才会生效
- Agent 页轮询改用 `useVisibilityPolling`,页面隐藏时暂停
@@ -369,92 +241,24 @@ All notable changes to this project will be documented in this file.
- **HTTPS 直连证书指纹不匹配可定位**:nginx 配置校验通过、但公网 `7443` 返回的证书与刚写入的不一致时,原来只提示“证书指纹不匹配”,无法判断是 nginx 未生效还是端口被其他服务接管。现在 HTTPS server 使用 `default_server` 绑定,Agent 会在 reload 后从本机 `127.0.0.1:<端口>` 握手自检并重试 5 秒;指纹不一致时回滚,并回报实际指纹与 `ss -tlnp` 端口占用进程
- **公网链路错误提示**:主程序在 Agent 本机自检通过、但公网地址校验失败时,会明确提示检查 `7443` 的端口转发、防火墙或其他服务接管
-
-### 变更
-
-- 版本号提升到 1.4.6;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 0.13.2 → **0.13.3**,内嵌 amd64 / arm64 二进制已重编;VPS 上需重新点一次「升级 Agent」才会生效
-
-### 测试
-
-- `internal/agent`:新增证书 PEM 指纹计算测试
-
-## [v1.4.5] - 2026-09-28
-
-### 修复
-
- **Agent 探测阶段无法自愈 nginx 残留目录**:0.13.1 的迁移只在配置目录已知或 `nginx -t` 校验前执行;当 `nginx -T` 在探测目录阶段就因 `.versions` 目录失败时,Agent 仍返回 `nginx -T 执行失败: exit status 1`。现在会先从 `nginx -T` 报错文本解析出 `<配置>.versions` 路径并迁移,再重试一次;安装 / 升级 Agent 的脚本也会先把 `sites-enabled`、`conf.d` 下的旧 `.versions` 目录移出 include 范围
-
-### 变更
-
-- 版本号提升到 1.4.5;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 0.13.1 → **0.13.2**,内嵌 amd64 / arm64 二进制已重编;VPS 上需重新点一次「升级 Agent」才会生效
-
-### 测试
-
-- `internal/agent`:新增从 nginx 报错文本解析并迁移旧版本目录的测试
-- `scripts/agent-install.sh`:`bash -n` 语法校验通过
-
-## [v1.4.4] - 2026-09-28
-
-### 修复
-
- **Agent nginx 版本目录被误加载**:切换 HTTPS 直连时,配置备份会写到 `<配置文件>.versions` 目录;该目录位于 `sites-enabled` 时会被 nginx 的 `include .../sites-enabled/*` 当成配置文件读取,导致 `nginx -t` 报 `pread() ".../fonuagent-https.conf.versions" failed (21: Is a directory)`。现在版本目录改为隐藏目录 `.<文件名>.versions`,`include *` 不再匹配;旧目录会在读取、备份或 `nginx -t` 前自动迁移,历史版本内容保留
-
-### 变更
-
-- 版本号提升到 1.4.4;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 0.13.0 → **0.13.1**,内嵌 amd64 / arm64 二进制已重编;VPS 上需点一次「升级 Agent」才会生效
-
-### 测试
-
-- `internal/fsutil`:新增隐藏版本目录与旧目录迁移测试
-- `internal/agent`:新增 `migrateLegacyVersionDirs` 迁移测试
-
-## [v1.4.3] - 2026-09-28
-
-### 修复
-
- **HTTPS 直连旧地址自愈**:从 SSH 隧道切换到「HTTPS + 证书固定」后,历史实例的 `agent_url` 可能仍为已失效的 `http://127.0.0.1:17701`,隧道关闭后直接报 `connection refused`。现在读取 HTTPS transport 时会依据 `ssh_host` 与 `agent_listen_addr` 自动重建 `https://:`,并在地址变化时回写数据库;切回 SSH 隧道也使用同一重建逻辑,无需手工修改地址
-
-### 变更
-
-- 版本号提升到 1.4.3;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.13.0
-
-### 测试
-
-- `internal/frp`:新增 `TestAgentHTTPSBaseURL`,覆盖旧隧道地址重建、已是 HTTPS 地址保持、默认端口回退与缺少 SSH 主机的情况
-
-## [v1.4.2] - 2026-09-28
-
-### 修复
-
- **切换 HTTPS 后 Agent URL 未更新**:启用 HTTPS 直连时只更新了 transport 和证书 pin,数据库里的 `agent_url` 仍保留旧的 `http://127.0.0.1:17701`。隧道关闭后仍访问旧本地地址,导致 `connect: connection refused`。现在 transport、URL、端口和 pin 同批更新,并在切换后清理旧 AgentClient 缓存
- **切回 SSH 隧道时同步恢复本地 URL**:恢复隧道会重新写入 `http://127.0.0.1:`,避免残留 HTTPS 地址影响后续状态读取
-
-### 变更
-
-- 版本号提升到 1.4.2;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.13.0
-
-### 测试
-
-- `internal/frp`:transport 切换与缓存失效编译验证
-
-## [v1.4.1] - 2026-09-28
-
-### 修复
-
- **SSH 隧道端口分配竞态**:原来先探测端口再关闭 listener,随后才真正启动隧道,两个安装动作或残留占用可能在两步之间抢到同一端口,报 `bind: Only one usage of each socket address`。现在改为分配端口时直接持有 listener 并交给隧道使用;17700 被占用时自动尝试后续端口
### 变更
-- 版本号提升到 1.4.1;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.13.0
+- `AgentVersion` 0.13.0 → **0.13.3**(0.13.1:隐藏版本目录;0.13.2:探测阶段迁移残留目录;0.13.3:证书指纹定位),内嵌 amd64 / arm64 二进制已重编;VPS 上需重新点一次「升级 Agent」才会生效
### 测试
+- `internal/agent`:新增证书 PEM 指纹计算、从 nginx 报错文本解析并迁移旧版本目录,以及 `migrateLegacyVersionDirs` 迁移测试
+- `scripts/agent-install.sh`:`bash -n` 语法校验通过
+- `internal/fsutil`:新增隐藏版本目录与旧目录迁移测试
+- `internal/frp`:新增 `TestAgentHTTPSBaseURL`,覆盖旧隧道地址重建、已是 HTTPS 地址保持、默认端口回退与缺少 SSH 主机的情况
+- `internal/frp`:transport 切换与缓存失效编译验证
- `internal/frp`:隧道 listener 复用、端口占用与释放回归测试
## [v1.4.0] - 2026-09-28
@@ -472,7 +276,6 @@ All notable changes to this project will be documented in this file.
- 数据库新增 Agent transport / 本地端口 / TLS pin / 监听地址 / 连接状态 / 连接错误字段(迁移 028)
- 前端「公网 Agent」页显示传输方式、隧道状态、本地入口和 Agent 监听地址;旧公网 HTTP 显示迁移警告
-- 版本号提升到 1.4.0;`web/package.json` 与 `web/package-lock.json` 同步对齐
- `AgentVersion` 0.12.9 → **0.13.0**,内嵌 amd64 / arm64 二进制已重编;VPS 上需升级 Agent
### 测试
@@ -486,263 +289,84 @@ All notable changes to this project will be documented in this file.
- **限流状态损坏时拒绝覆盖**:`readLimitState` 原来忽略 JSON 解析错误并返回空 map,保存任一规则时可能把其他域名的限流配置清空。现在文件不存在仍视为空状态,读取失败、空文件或 JSON 损坏则返回错误,`applyLimitState` / `clearLimitState` 会保留原文件与现有 Nginx zone,不再覆盖
- **一键升级宿主机目录自动发现**:当 Compose 标签里的 `working_dir` 是 `/workspace` 这类容器路径时,不再直接把它当宿主机目录;优先使用 `project.config_files` 推导真实目录,无法得到宿主机路径时明确报错,避免 helper 挂载 `/workspace:/workspace` 后找不到 Compose 文件
+- **复用原 Compose 项目名重建容器**:updater 在通过 helper 容器执行 `docker compose` 时,先从现有 `fonu` 容器读取 `com.docker.compose.project` 标签,并显式传入同一个项目名,避免 Compose 将现有容器视为另一个项目后重新创建网络、数据卷并撞上 `/fonu` 容器名冲突。现有容器不是 Compose 管理时,updater 会拒绝升级并提示按 Compose 方式重新部署,避免误建空数据卷
+- **自动定位宿主机 Compose 目录**:当 updater 容器内的 `/workspace` 没有挂载到真实 Compose 目录时,不再直接失败。updater 会从 `fonu` 容器的 `com.docker.compose.project.working_dir` / `project.config_files` 标签读取宿主机路径,并通过临时 helper 容器挂载真实目录执行 `docker compose`。`FONU_UPDATE_HOST_DIR` 仍可作为显式覆盖
### 变更
-- 版本号提升到 1.3.9;`web/package.json` 与 `web/package-lock.json` 同步对齐
- `AgentVersion` 0.12.8 → **0.12.9**,内嵌 amd64 / arm64 二进制已重编;VPS 上需点一次「升级 Agent」才会生效
### 测试
- `internal/agent`:新增限流状态文件缺失、正常解析、空文件、截断 JSON、null 内容与失败后原文件不变的测试
-
-## [v1.3.8] - 2026-09-28
-
-### 新增
-
-- **示例页面 v1.3.8 验证标识**:页面版本提示、「本次升级验证」区块、页面状态和验证标识同步更新为 `v1.3.8-example-page`,用于再次验证线上升级后的前端资源切换
-
-### 变更
-
-- 版本号提升到 1.3.8;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.12.8
-
-### 测试
-
- 前端类型检查、单元测试与生产构建
-
-## [v1.3.7] - 2026-09-28
-
-### 新增
-
-- **示例页面新增独立升级验证区块**:页面增加「本次升级验证」高亮区块,显示 `v1.3.7`,验证标识更新为 `v1.3.7-example-page`,用于直观确认升级后的前端资源已切换
-
-### 修复
-
-- **复用原 Compose 项目名重建容器**:updater 在通过 helper 容器执行 `docker compose` 时,先从现有 `fonu` 容器读取 `com.docker.compose.project` 标签,并显式传入同一个项目名,避免 Compose 将现有容器视为另一个项目后重新创建网络、数据卷并撞上 `/fonu` 容器名冲突。现有容器不是 Compose 管理时,updater 会拒绝升级并提示按 Compose 方式重新部署,避免误建空数据卷
-
-### 变更
-
-- 版本号提升到 1.3.7;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.12.8
-
-### 测试
-
-- 前端类型检查、单元测试与生产构建
-
-## [v1.3.6] - 2026-09-28
-
-### 修复
-
-- **自动定位宿主机 Compose 目录**:当 updater 容器内的 `/workspace` 没有挂载到真实 Compose 目录时,不再直接失败。updater 会从 `fonu` 容器的 `com.docker.compose.project.working_dir` / `project.config_files` 标签读取宿主机路径,并通过临时 helper 容器挂载真实目录执行 `docker compose`。`FONU_UPDATE_HOST_DIR` 仍可作为显式覆盖
-- 示例页验证标识更新为 `v1.3.6-example-page`
-
-### 变更
-
-- 版本号提升到 1.3.6;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.12.8
-
-### 测试
-
- `internal/updater`:覆盖 Compose 文件直挂载与宿主机目录 helper 模式
## [v1.3.5] - 2026-09-28
### 新增
-- **示例页面升级验证内容**:页面版本提示与验证标识更新为 `v1.3.5-example-page`,新增「页面状态」统计卡和「本次示例页变化」说明,便于确认升级后的前端资源已经切换
-
-### 变更
-
-- 版本号提升到 1.3.5;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.12.8
-
-### 测试
-
-- 前端类型检查、单元测试与生产构建
-
-## [v1.3.4] - 2026-09-28
-
-### 修复
-
-- **一键升级找不到 Compose 文件**:fnOS 或 Docker UI 启动 Compose 时,updater 的 `.:/workspace` 可能解析到错误目录,构建时报 `open /workspace/docker-compose.hub.yml: no such file or directory`。Compose 文件现支持用 `FONU_UPDATE_HOST_DIR` 显式指定宿主机 Compose 目录,并在状态接口中提前报告缺少 Compose 文件,避免排到升级后才失败
-- **示例页验证标识更新为 v1.3.4-example-page**,用于确认升级后的前端资源确实生效
-
-### 变更
-
-- 版本号提升到 1.3.4;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.12.8
-
-### 测试
-
-- `internal/updater`:补充 Compose 文件缺失检查
-
-## [v1.3.3] - 2026-09-27
-
-### 新增
-
-- **示例页面升级验证标识**:示例页面新增固定的 `v1.3.3-example-page` 验证标识与展示项,升级后可直接通过标识区分新旧前端资源,确认页面、路由和静态资源均已更新
-
-### 变更
-
-- 版本号提升到 1.3.3;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.12.8,本次无 Agent 行为变化
-
-### 测试
-
-- 前端类型检查、单元测试与生产构建
-
-## [v1.3.2] - 2026-09-27
-
-### 修复
-
-- **一键升级无法读取远端版本**:`fonu-updater` 检查更新时原来会 `git clone --depth 1` 整个仓库,而状态接口的父级超时只有 20 秒,克隆未完成即被 `signal: killed`,表现为 `fatal: early EOF`,前端因此一直不显示「一键升级」。现改为优先通过 Gitea API / raw 地址或 GitHub raw 读取 `VERSION`,显著降低检查耗时,不再依赖完整克隆
-
-### 变更
-
-- 版本号提升到 1.3.2;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.12.8,本次无 Agent 行为变化
-- `fonu-updater` 位于 sidecar,不能通过自身升级自身;已有部署需要先手动重建一次 `updater` 服务,之后该修复才生效
-
-### 测试
-
-- `internal/updater`:新增 Gitea API / raw 地址解析、文本与 base64 JSON 版本内容解析测试
-
-## [v1.3.1] - 2026-09-27
-
-### 新增
-
-- **示例页面(在线升级验证页)**:新增 `/example` 页面与侧栏「示例页面」入口,页面实时读取 `/api/version`,展示当前版本、验证步骤和验证结果,用于确认一键升级后前端路由、菜单与后端版本均已更新
-
-### 变更
-
-- 版本号提升到 1.3.1;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.12.8,本次无 Agent 行为变化
-
-### 测试
-
-- 前端类型检查、单元测试与生产构建
-
-## [v1.3.0] - 2026-09-27
-
-### 新增
-
- **Web 一键升级(Docker Compose)**:新增受限的 `fonu-updater` 升级 sidecar,支持 `docker-compose.yml`(本机源码构建)与 `docker-compose.hub.yml`(Git 仓库构建)两种部署。主程序只通过共享 Unix Socket 调用 sidecar,不直接挂载 Docker Socket;升级时只重建 `fonu` 服务,构建或健康检查失败会自动尝试回滚到升级前的镜像 tag
- 前端侧栏在检测到可升级版本时显示「一键升级」入口,升级弹窗展示当前版本、目标版本、升级方式、阶段消息与最近日志;服务重启期间浏览器会自动等待恢复
- 新增 `docs/system-upgrade.md`,说明首次部署 sidecar、安全边界、升级流程、回滚和常见问题
+### 修复
+
+- **一键升级找不到 Compose 文件**:fnOS 或 Docker UI 启动 Compose 时,updater 的 `.:/workspace` 可能解析到错误目录,构建时报 `open /workspace/docker-compose.hub.yml: no such file or directory`。Compose 文件现支持用 `FONU_UPDATE_HOST_DIR` 显式指定宿主机 Compose 目录,并在状态接口中提前报告缺少 Compose 文件,避免排到升级后才失败
+- **一键升级无法读取远端版本**:`fonu-updater` 检查更新时原来会 `git clone --depth 1` 整个仓库,而状态接口的父级超时只有 20 秒,克隆未完成即被 `signal: killed`,表现为 `fatal: early EOF`,前端因此一直不显示「一键升级」。现改为优先通过 Gitea API / raw 地址或 GitHub raw 读取 `VERSION`,显著降低检查耗时,不再依赖完整克隆
+
### 变更
-- 版本号提升到 1.3.0;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 保持 0.12.8,本次无 Agent 行为变化
+- `AgentVersion` 保持 **0.12.8**,本次无 Agent 行为变化
+- `fonu-updater` 位于 sidecar,不能通过自身升级自身;已有部署需要先手动重建一次 `updater` 服务,之后该修复才生效
+
+### 测试
+
+- 前端类型检查、单元测试与生产构建
+- `internal/updater`:覆盖 Compose 文件缺失检查、版本比较、compose 路径白名单与配置校验,并新增 Gitea API / raw 地址解析、文本与 base64 JSON 版本内容解析测试
### 注意
- 已经运行旧版本的实例,需要先手动更新 Compose 文件并执行一次 `docker compose up -d --build`,让 `fonu-updater` sidecar 启动。此后才可以在 Web 页面一键升级
-### 测试
-
-- `internal/updater`:版本比较、compose 路径白名单、配置校验
-
## [v1.2.21] - 2026-09-27
-### 修复
-
-- **Agent Bearer Token 改为常量时间比较**:`internal/agent` 原来用普通字符串比较校验 Token,现将 Authorization 解析与 Token 比较拆分,改用 `crypto/subtle.ConstantTimeCompare`,避免认证路径上的长度与内容时序泄漏
-
-### 变更
-
-- 版本号提升到 1.2.21;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- `AgentVersion` 0.12.7 → **0.12.8**,内嵌 amd64 / arm64 二进制已重编;VPS 上需点一次「升级 Agent」才会生效
-
-### 测试
-
-- `internal/agent`:新增认证测试,覆盖正确 Token、大小写 Bearer、多余空白、错误 Token、长度不同、缺失 Authorization、错误认证方案
-
-## [v1.2.20] - 2026-09-27
-
-### 修复
-
-- **DDNS 页 FRP 提示恢复**:`DdnsView.vue` 原来调用已退役的 `/api/frp`,请求失败后会把 `frpEnabled` 固定为 `false`,导致“FRP 内网穿透已启用”提示不再出现。现改用 `/api/frpmulti/status`,仅在“已配置服务端且存在启用规则”时显示,并补充判定函数单元测试
-
-### 变更
-
-- 版本号提升到 1.2.20;`web/package.json` 与 `web/package-lock.json` 同步对齐
-- 本轮不涉及 Go 与 agent 行为,`AgentVersion` 仍为 0.12.7
-
-## [v1.2.19] - 2026-09-27
-
### 新增
- **数据目录可选(默认行为不变)**:三个 compose 的挂载改为 `${FONU_DATA_PATH:-fonu-data}:/data`——默认仍是命名卷 `fonu-data`,**老用户升级后不受影响**;在同层 `.env` 里写 `FONU_DATA_PATH=/vol1/docker/fonu-data` 即可让数据落到指定宿主机目录(以 `/` 或 `.` 开头的写法会被 compose 当成 bind mount,其余字符串仍是命名卷)
- **新增 `docs/container-deploy-and-data.md`(容器部署与数据指南)**:三个编排文件的分工与选择、干净机器单文件部署、数据目录(默认卷 / 指定目录 / 从卷迁移)、备份与恢复的三条注意、升级与回滚、环境变量速查、常见问题(401 加速器、`FONU_SESSION_SECRET` 报错、`fonu Pulling` 超时、换目录后「数据没了」、仪表盘三张卡为空)、与原生安装之间的迁移
-
-### 变更
-
-- 版本号提升到 1.2.19;`web/package.json` 同步对齐
-- 本轮不涉及 Go 代码(只改 compose 与文档),因此安装包未重出,`dist/` 里 1.2.17 的包仍可用;`AgentVersion` 仍为 0.12.7
-
-## [v1.2.18] - 2026-09-27
-
-### 修复
-
-- **国内加速器上构建失败(401 Unauthorized)**:`Dockerfile` 顶部原来显式写了 `# syntax=docker/dockerfile:1`,这会让 BuildKit 额外去拉 `docker/dockerfile` 镜像,而在飞牛的 `docker.fnnas.com` 这类加速器上会被拦成 401(`resolve image config for docker-image://docker/dockerfile:1` 失败)。该行已移除:`RUN --mount=type=cache` 由 Docker 内建的 BuildKit frontend 已支持,不需要显式指定,缓存挂载保持不变
-- **compose 无谓地尝试拉镜像**:`docker-compose.hub.yml` 增加 `pull_policy: build`,跳过日志里那条 "fonu Pulling"(国内直连 Docker Hub 的超时告警),直接构建
-
-### 变更
-
-- 版本号提升到 1.2.18;`web/package.json` 同步对齐
-- 本轮**不涉及 Go 代码**(只改 Dockerfile 与 compose),因此安装包未重出,`dist/fonu-1.2.17-linux-*.tar.gz` 仍可用;`AgentVersion` 仍为 0.12.7
-
-## [v1.2.17] - 2026-09-27
-
-### 新增
-
- **会话密钥自动生成**:`FONU_SESSION_SECRET` 未配置时,应用首次启动会在数据目录里生成 32 字节随机密钥(64 字符)并持久化到 `<数据目录>/session_secret`(权限 0600,先写临时文件再改名;读到损坏内容会重新生成)。启动日志会写明密钥来源(环境变量 / 文件路径 / 仍在使用历史默认值的提示)
- **compose 改为「留空即自动」**:三个 compose 统一 `FONU_SESSION_SECRET: ${FONU_SESSION_SECRET:-}`;`docker-compose.hub.yml` 的单文件部署现在只要 `docker compose up -d --build`,**不再需要预先准备 `.env`**
-
-### 变更
-
-- `FONU_SESSION_SECRET` 的默认值由固定的 `change-me-in-production` 改为空(空 = 未配置 → 自动生成)
-- **不会自动替换已配置的值**:这个密钥同时是 `secret.NewBox` 的加密密钥,替换会让库里已加密的凭据(frp token、通知密码等)无法解密;对仍在使用历史默认值的实例只提示、不改数据
-- `.gitignore` 补上 `.env` 与 `.env.local`(`.env.example` 这类模板仍可入库)
-- 版本号提升到 1.2.17;`web/package.json` 同步对齐
-- 本轮不含 agent 行为变更,`AgentVersion` 仍为 0.12.7
-
-### 测试
-
-- `internal/config`:密钥生成与复用、文件权限(类 Unix 上校验 0600)、显式配置优先且不写文件、历史默认值保留并提示、损坏文件重新生成
-
-## [v1.2.16] - 2026-09-27
+- **仪表盘「实时流量」支持切换数据源(本机 / 公网)**:本机读 Fonu 管理的本机 Nginx 访问日志;公网读 frps 侧逐规则的 `today_traffic_in/out` 与 `cur_conns`(速率由后端两次采样算出,口径与「公网反代」页的当前上传/下载一致)。切换时会清空 5 分钟曲线,避免两个来源的采样混在一张图里;还没有穿透规则时「公网」按钮禁用并给出说明
### 改进
- **`docker-compose.hub.yml` 改为「单文件从仓库拉取源码构建」**:`build.context` 直接用 Git 地址(`http://45.207.223.190:10082/mrlee/-Fonu.git#main`),由 BuildKit 自己克隆后再构建——干净机器上**只需要这一个 compose 文件 + 一个 `.env`**,不必先手动 `git clone`。升级也是回该目录重跑 `docker compose up -d --build`(会重新拉 `main` 的当前代码)。文件头补齐了完整用法
- **会话密钥不再静默回退**:这一份把 `FONU_SESSION_SECRET` 写成必填(`${FONU_SESSION_SECRET:?...}`),没在 `.env` 里给值就直接拒绝启动并给出生成命令,避免用 `change-me-in-production` 直接跑在公网
-
-### 变更
-
-- 版本号提升到 1.2.16;`web/package.json` 同步对齐
-- 本轮不含 agent 行为变更,`AgentVersion` 仍为 0.12.7
-- 三个 compose 的分工:`docker-compose.yml`(本地源码上下文)、`docker-compose.hub.yml`(**从仓库拉取**)、`docker-compose.host.yml`(host 网络,仍需本地源码)
-
-## [v1.2.15] - 2026-09-27
-
-### 新增
-
-- **仪表盘「实时流量」支持切换数据源(本机 / 公网)**:本机读 Fonu 管理的本机 Nginx 访问日志;公网读 frps 侧逐规则的 `today_traffic_in/out` 与 `cur_conns`(速率由后端两次采样算出,口径与「公网反代」页的当前上传/下载一致)。切换时会清空 5 分钟曲线,避免两个来源的采样混在一张图里;还没有穿透规则时「公网」按钮禁用并给出说明
-
-### 改进
-
- **容器交付:升级改为在部署机(NAS)上直接构建**(不再需要 save / 传文件 / load 镜像包)
- `Dockerfile`:加 `# syntax=docker/dockerfile:1` + 四处 `RUN --mount=type=cache`(npm 与 Go 模块/编译缓存跨次构建复用,重复构建从几分钟降到几十秒)
- 三个 compose 统一 `name: fonu`(否则改了目录名/项目名后 compose 会把它当成新项目,`fonu-data` 变成新空卷,现象就是「数据丢了」)与 `image: fonu:${FONU_VERSION:-local}`(镜像 tag 随版本走,便于回滚);`.host.yml` 补上原本缺失的 `image:`
- 新增 `scripts/upgrade.sh`:一条命令完成「拉代码 → 构建 → 重建 → 健康检查」,支持 `--no-pull`、`--rollback `(不重新构建)与 `COMPOSE_FILE=`(host 网络那仃)
+### 修复
+
+- **Agent Bearer Token 改为常量时间比较**:`internal/agent` 原来用普通字符串比较校验 Token,现将 Authorization 解析与 Token 比较拆分,改用 `crypto/subtle.ConstantTimeCompare`,避免认证路径上的长度与内容时序泄漏
+- **DDNS 页 FRP 提示恢复**:`DdnsView.vue` 原来调用已退役的 `/api/frp`,请求失败后会把 `frpEnabled` 固定为 `false`,导致“FRP 内网穿透已启用”提示不再出现。现改用 `/api/frpmulti/status`,仅在“已配置服务端且存在启用规则”时显示,并补充判定函数单元测试
+- **国内加速器上构建失败(401 Unauthorized)**:`Dockerfile` 顶部原来显式写了 `# syntax=docker/dockerfile:1`,这会让 BuildKit 额外去拉 `docker/dockerfile` 镜像,而在飞牛的 `docker.fnnas.com` 这类加速器上会被拦成 401(`resolve image config for docker-image://docker/dockerfile:1` 失败)。该行已移除:`RUN --mount=type=cache` 由 Docker 内建的 BuildKit frontend 已支持,不需要显式指定,缓存挂载保持不变
+- **compose 无谓地尝试拉镜像**:`docker-compose.hub.yml` 增加 `pull_policy: build`,跳过日志里那条 "fonu Pulling"(国内直连 Docker Hub 的超时告警),直接构建
+
### 变更
-- 版本号提升到 1.2.15;`web/package.json` 同步对齐
-- 本轮不含 agent 行为变更,`AgentVersion` 仍为 0.12.7
+- `AgentVersion` 0.12.7 → **0.12.8**(仅 1.2.21 涉及 agent 行为),内嵌 amd64 / arm64 二进制已重编;VPS 上需点一次「升级 Agent」才会生效。1.2.15-1.2.20 不涉及 Go 或 agent 行为,`AgentVersion` 保持 0.12.7,其中部分补丁未重出安装包
+- `FONU_SESSION_SECRET` 的默认值由固定的 `change-me-in-production` 改为空(空 = 未配置 → 自动生成)
+- **不会自动替换已配置的值**:这个密钥同时是 `secret.NewBox` 的加密密钥,替换会让库里已加密的凭据(frp token、通知密码等)无法解密;对仍在使用历史默认值的实例只提示、不改数据
+- `.gitignore` 补上 `.env` 与 `.env.local`(`.env.example` 这类模板仍可入库)
+- 三个 compose 的分工:`docker-compose.yml`(本地源码上下文)、`docker-compose.hub.yml`(**从仓库拉取**)、`docker-compose.host.yml`(host 网络,仍需本地源码)
+
+### 测试
+
+- `internal/agent`:新增认证测试,覆盖正确 Token、大小写 Bearer、多余空白、错误 Token、长度不同、缺失 Authorization、错误认证方案
+- `internal/config`:密钥生成与复用、文件权限(类 Unix 上校验 0600)、显式配置优先且不写文件、历史默认值保留并提示、损坏文件重新生成
## [v1.2.14] - 2026-09-27
@@ -750,6 +374,34 @@ All notable changes to this project will be documented in this file.
- **证书颁发机构:LiteSSL 替换 Buypass**。LiteSSL(亚数 TrustAsia 的公益免费 DV 证书,90 天,官方支持单域名 / 通配符 / 多域名)已接入,ACME 目录 `https://acme.litessl.com/acme/v2/directory`。该目录里 `meta.externalAccountRequired = true`,所以**强制 EAB**:在 FreeSSL 平台「证书自动化 → EAB 管理」创建后,把 Kid 与 HMAC 填进设置即可(不需要像 ZeroSSL 那样用 API Key 去换取)。EAB HMAC 只回掩码。
Buypass 保留常量与展示元数据(历史证书记录仍能正常显示),但不再可选,直接提交会被拦下并提示替代项。域名若有 CAA 记录,需允许 `trustasia.com`。
+- **Linux 原生安装包**:`scripts/build-linux.ps1` 一条命令产出 `fonu-<版本>-linux-{amd64,arm64}.tar.gz`(内含二进制、migrations、systemd 安装脚本、前端运行时资源与 sha256 校验文件,无需 Docker/WSL,依赖纯 Go 的 modernc.org/sqlite);`scripts/fonu-install.sh` 负责目标机安装与卸载:装到 `/opt/fonu`、数据在 `/var/lib/fonu`、环境变量在 `/etc/fonu/fonu.env`(首次生成随机会话密钥,重装保留)并注册 `fonu.service`
+- **`--reset-admin`**:忘了管理员口令时清空 `admins`/`sessions` 后重启,启动流程会重新生成随机口令;装完也会直接打印初始口令(以前只提示访问地址,用户找不到口令)
+- **Cloudflare Tunnel 融合方案文档** `docs/cloudflared-integration-plan.md`:同类项目对比、两种托管模型、可复用面盘点、参考实现分析(含用户自有项目 cf-tunnel-manager)与 IP 优选可行性边界
+- 设置页「ACME 证书」新增 **DNS 解析器(高级)**设置项
+- **定时自动备份 + 保留策略**:每天自动备份一份到 `/backups`,超过保留份数自动清理最旧的;重启不会重复备份(按「最新备份是否在 24 小时内」判定)。设置页「数据」卡可开关、改份数、立即备份,并列出/下载/恢复/删除备份,**恢复前先校验归档**
+- **健康检查持续化 + 公开状态页**:规则的四段健康结论按**状态变更**落库(采样间隔即巡检轮次),由此推导可用率与不可用区间;设置页「高级」新增「公开状态页」——默认关闭,可设访问码(只存 SHA-256、不下发明文),按 IP 限流,只暴露域名与可用率
+- **指标历史与趋势图**:新增纵向点表 `metrics_samples`,记录本机 CPU / 内存 / 磁盘与每条规则的上行/下行速率(采样跟随巡检,30 天保留);仪表盘新增「资源与流量趋势」卡,支持 1 小时 / 24 小时 / 7 天
+- **告警事件新增**:自动备份失败(默认开启);以下三项巡检默认关闭,设置页「告警事件 → 系统」可开启:
+ - **公网服务端掉线**:服务端进程没在跑、或隧道连不上 frps 时通知,恢复时再通知一次;只对「用户要它运行」的服务端判定,手动停止的不报
+ - **公网 agent 不可达**:VPS 上的 fonu-agent 连续无响应(重启后没起来最常见),恢复时再通知一次;没配 agent 的服务端不参与判定
+ - **配置漂移**:VPS 上的 vhost 与 Fonu 记录不一致(被手工改过、或 agent 重装后没重部署)时通知,漂移清空时补一条恢复
+- 通知图标与分类补齐:📈 系统(资源占用)、🔌🛑📡 隧道(隧道 / 服务端 / agent)、🧭 配置(漂移)
+- 通知方式的 `Webhook` 改名为「Webhook / IM 机器人」:企业微信、钉钉、飞书机器人挂在 Webhook 预设下,原标签没说明白
+
+### 改进
+
+- 日志页「FRP 日志」的**来源**列不再显示 frpc 的 Go 源码文件行号(如 `client/control.go:174`),改为中文模块名(登录 / 连接 / 代理 / 服务 / 配置 / 健康检查 / 服务端);认不出来的值(包括回落到的组件名)原样保留
+- 「公网服务端」日志弹窗改为显示 **VPS 上的 frps 服务日志**(该页职责是管理 frps 端),标题与说明同步;本机 frpc 日志仍在日志页「FRP 日志」页签
+- frps 日志从写死 6 行改为**最近 20 行、最新在最上面**,并把 `2026-09-22T18:43:34+08:00 RainYun-XXX systemd[1]: …` 重写成 `2026-09-22 18:43:34 systemd[1]: …`(去掉 T 分隔、时区偏移与每行重复的主机名);`frps/action` 失败时带回的日志一并统一格式
+- 巡检每轮只采一次主机资源,同时喂给「资源阀值告警」与「指标历史」(CPU 占用率靠两次采样的差值,采两次会互相干扰)
+- 流量聚合抽出 `aggregateHostKeys`,端点聚合与按域名聚合共用同一套速率算法,避免两处漂移
+- 仪表盘:新增趋势卡;「运行状态」并入顶部统计区(运行时长补进「服务」卡);「性能」并入系统资源那一行,整行 4 列
+- 趋势图:图例与绘图区留白调整;点少时画点(此前只有一两个点时线根本画不出来);空桶断开而不是补 0
+- 巡检改为 Source 接口(Routes / Servers / Agents / Drift),`internal/app/monitor_source.go` 负责把 frp 服务适配进来,`internal/monitor` 因此不依赖 frp 包
+- 「连续 N 次失败」抽成共用的 `failureTransition`(原 `tunnelTransition` 泛化):三条新巡检与原有隧道巡检共用「刚好达标报一次、恢复报一次」;漂移不是连续失败型,单独用冷却(默认 24 小时,可配)
+- 避免误报的三条约定:服务端只在「用户要它运行」时判定;`ConnectionState` 为 `unknown` 按健康处理;没配 agent 的服务端不参与可达性判定
+- 开关关闭时连数据采集都跳过(关掉的功能不该有后台开销);所有 `notify.Alert` 一律在锁外调用
+- 新增设置项:`notify_on_frps_down` / `notify_on_agent_unreachable` / `notify_on_route_drift`,以及 `notify_frps_fail_threshold`(2) / `notify_agent_fail_threshold`(3) / `notify_drift_cooldown_hours`(24)
### 修复
@@ -759,127 +411,31 @@ All notable changes to this project will be documented in this file.
- `nginxPath()` 探测不到时不再回落到配置里的裸名字 `nginx`:那会让「未安装」被误判为「配置校验未通过(nginx -t 执行失败)」,且「VPS 未安装 Nginx」告警与一键安装按钮**永远不会出现**。
- vhost 目录探测不再要求被加载文件以 `.conf` 结尾:Debian/Ubuntu 的 `sites-enabled/default` 不带后缀、而 `nginx.conf` 里是 `include .../sites-enabled/*`,过滤掉它之后只剩主配置目录可试,报「在 1 个被加载目录中均无法放置完整 server 配置」;现在失败信息会带上最后一次的具体原因与常见显式指定目录。
- **前端申请证书弹窗的颁发机构卡片**改为前端可控清单:原先硬编码为 Let's Encrypt / ZeroSSL / Buypass,后端 `/api/certs/options` 的结果不参与渲染,导致后端增删 CA 时弹窗不变(本次 LiteSSL 替换就是这幺被发现的)。现在卡片为 Let's Encrypt / ZeroSSL / LiteSSL。
-
-### 测试
-
-- `internal/frp`:校验文件候选名顺序、sha256sum 格式解析(含 `*` 前缀与不存在的文件负例)
-- `internal/acme`:LiteSSL 目录地址 / 展示名 / 别名归一化、通配符与多域名不被拦、Buypass 停用提示里含替代项
-
-### 变更
-
-- `AgentVersion` 0.12.4 → **0.12.7**(0.12.5 修 frps 校验文件名;0.12.6 修 nginxPath;0.12.7 修 vhost 目录探测),双架构内嵌二进制已重编;**VPS 上需点一次「升级 Agent」**
-- 版本号提升到 1.2.14;`web/package.json` 同步对齐
-
-## [v1.2.13] - 2026-09-27
-
-### 新增
-
-- **Linux 原生安装包**:`scripts/build-linux.ps1` 一条命令产出 `fonu-<版本>-linux-{amd64,arm64}.tar.gz`(内含二进制、migrations、systemd 安装脚本、前端运行时资源与 sha256 校验文件,无需 Docker/WSL,依赖纯 Go 的 modernc.org/sqlite);`scripts/fonu-install.sh` 负责目标机安装与卸载:装到 `/opt/fonu`、数据在 `/var/lib/fonu`、环境变量在 `/etc/fonu/fonu.env`(首次生成随机会话密钥,重装保留)并注册 `fonu.service`
-- **`--reset-admin`**:忘了管理员口令时清空 `admins`/`sessions` 后重启,启动流程会重新生成随机口令;装完也会直接打印初始口令(以前只提示访问地址,用户找不到口令)
-- **Cloudflare Tunnel 融合方案文档** `docs/cloudflared-integration-plan.md`:同类项目对比、两种托管模型、可复用面盘点、参考实现分析(含用户自有项目 cf-tunnel-manager)与 IP 优选可行性边界
-- 设置页「ACME 证书」新增 **DNS 解析器(高级)**设置项
-
-### 修复
-
- **证书申请:DNS-01 传播检查用错解析器**:lego 默认读系统 `/etc/resolv.conf`,容器里那是 Docker 的 `127.0.0.11`,它不返回权威 NS,于是报 `could not determine authoritative nameservers`。现在显式传入 `dns01.AddRecursiveNameservers`(默认 `223.5.5.5:53,1.1.1.1:53,8.8.8.8:53`,可用 `acme_dns_resolvers` 覆盖,填 `system` 回到系统解析器;配置无效时报明确错误而不是静默降级)
- **Buypass 下线**:该 CA 已于 2025-10-15 停止签发、2026-04-15 终止 ACME 服务,已从可选 CA 列表移除;历史记录仍可展示,直接提交会被拦下并给出中文原因(不再让用户面对一个看不懂的 ACME 报错)
- **ZeroSSL 报错信息不全**:返回 422 时把 ZeroSSL 的 `error.type` / `error.code` 带进错误;非 JSON 响应退回压平截断的正文,便于定位
- **密钥泄露**:ZeroSSL EAB 请求失败时,`url.Error` 里带 `access_key` 的完整 URL 会被写进任务日志与 `<数据目录>/logs/app.log`;现在只保留底层错误原因
-
-### 测试
-
-- `internal/acme`:递归解析器列表解析(分隔符/自动补 53/上限 8 条/默认值可用);ZeroSSL EAB 四条(成功、422 带错误类型、非 JSON 退回正文、网络错误不泄露密钥)
-
-### 变更
-
-- 版本号提升到 1.2.13;`web/package.json` 同步对齐
-- 本轮不含 agent 行为变更,`AgentVersion` 仍为 0.12.4
-
-## [v1.2.12] - 2026-09-25
-
-### 修复
-
- **frps.toml 同源配置预览泄露明文密钥**:`FRPSServerConfig` 是对外**预览**入口,之前直接把 builder 输出返回,而模板里含解密后的 `auth.token`——「即将下发的内容」弹窗拿到的是明文,与同文件「密钥不入其他 API 响应」的约定矛盾。现在预览统一走 `maskFRPSSecrets`(`auth.token` / `auth.oidc.clientSecret` 的值换成 `********`,键名与行结构保留、注释行不动);**真正下发路径不受影响**(`PushFRPSConfigToAgent` 与安装流程仍用未脱敏内容)。预览弹窗补了说明,「复制」改为「复制(已脱敏)」
- 规则详情「日志」页签改为**倒序**:加载后反转,最新一条在最上面(与日志页「FRP 日志」页签一致)
-
-### 改进
-
-- 日志页「FRP 日志」的**来源**列不再显示 frpc 的 Go 源码文件行号(如 `client/control.go:174`),改为中文模块名(登录 / 连接 / 代理 / 服务 / 配置 / 健康检查 / 服务端);认不出来的值(包括回落到的组件名)原样保留
-- 「公网服务端」日志弹窗改为显示 **VPS 上的 frps 服务日志**(该页职责是管理 frps 端),标题与说明同步;本机 frpc 日志仍在日志页「FRP 日志」页签
-- frps 日志从写死 6 行改为**最近 20 行、最新在最上面**,并把 `2026-09-22T18:43:34+08:00 RainYun-XXX systemd[1]: …` 重写成 `2026-09-22 18:43:34 systemd[1]: …`(去掉 T 分隔、时区偏移与每行重复的主机名);`frps/action` 失败时带回的日志一并统一格式
-
-### 测试
-
-- `internal/frp`:`maskFRPSSecrets` 脱敏(明文不出现、非敏感行不被误伤)与注释行不被改写
-
-### 变更
-
-- **agent 行为变更**:`AgentVersion` 0.12.2 → **0.12.4**(0.12.3:20 行 + 倒序 + ISO;0.12.4:重写日期格式),双架构内嵌二进制已重编;**需在 VPS 上点一次「升级 Agent」**才生效
-- 版本号提升到 1.2.12;`web/package.json` 同步对齐
-
-## [v1.2.11] - 2026-09-25
-
-### 新增
-
-- **定时自动备份 + 保留策略**:每天自动备份一份到 `/backups`,超过保留份数自动清理最旧的;重启不会重复备份(按「最新备份是否在 24 小时内」判定)。设置页「数据」卡可开关、改份数、立即备份,并列出/下载/恢复/删除备份,**恢复前先校验归档**
-- **健康检查持续化 + 公开状态页**:规则的四段健康结论按**状态变更**落库(采样间隔即巡检轮次),由此推导可用率与不可用区间;设置页「高级」新增「公开状态页」——默认关闭,可设访问码(只存 SHA-256、不下发明文),按 IP 限流,只暴露域名与可用率
-- **指标历史与趋势图**:新增纵向点表 `metrics_samples`,记录本机 CPU / 内存 / 磁盘与每条规则的上行/下行速率(采样跟随巡检,30 天保留);仪表盘新增「资源与流量趋势」卡,支持 1 小时 / 24 小时 / 7 天
-- 告警事件新增「自动备份失败」(默认开启)
-
-### 修复
-
- **隧道探活:「判不了」不再当成「不通」**:`RouteHealthCandidate` 新增 `tunnel_known`,frps 管理接口未配置或不可用时不再把 `tunnel_ok=false` 当成故障——此前会对这类用户误报「隧道连续不通」
- 健康汇总同样区分「无法判定」与「不通」:新增 `tunnel_unknown` 计数,规则表徽标用警告色并说明「不代表隧道不通」,「无法判定」不再算作不健康
-### 改进
+### 变更
-- 巡检每轮只采一次主机资源,同时喂给「资源阀值告警」与「指标历史」(CPU 占用率靠两次采样的差值,采两次会互相干扰)
-- 流量聚合抽出 `aggregateHostKeys`,端点聚合与按域名聚合共用同一套速率算法,避免两处漂移
-- 仪表盘:新增趋势卡;「运行状态」并入顶部统计区(运行时长补进「服务」卡);「性能」并入系统资源那一行,整行 4 列
-- 趋势图:图例与绘图区留白调整;点少时画点(此前只有一两个点时线根本画不出来);空桶断开而不是补 0
+- `AgentVersion` 0.12.2 → **0.12.7**(0.12.3:日志 20 行 + 倒序 + ISO;0.12.4:重写日期格式;0.12.5:frps 校验文件名;0.12.6:nginxPath;0.12.7:vhost 目录探测),双架构内嵌二进制已重编;**VPS 上需点一次「升级 Agent」**
+- 新增迁移 `026_route_health.sql`、`027_metrics_samples.sql`(启动时自动执行)
### 测试
+- `internal/frp`:校验文件候选名顺序、sha256sum 格式解析(含 `*` 前缀与不存在的文件负例);`maskFRPSSecrets` 脱敏(明文不出现、非敏感行不被误伤)与注释行不被改写
+- `internal/acme`:LiteSSL 目录地址 / 展示名 / 别名归一化、通配符与多域名不被拦、Buypass 停用提示里含替代项;递归解析器列表解析(分隔符/自动补 53/上限 8 条/默认值可用);ZeroSSL EAB 四条(成功、422 带错误类型、非 JSON 退回正文、网络错误不泄露密钥)
- `internal/backup`:导出 + 校验往返、校验拒绝非 gzip、列表/剪枝/名称白名单、到期判断
- `internal/metrics`:分桶平均与空桶不产点、窗口外的点、退化输入
- `internal/frp`:可用率推导(区间 + unknown、仍在持续、无记录不出结果)、健康汇总的 unknown 用例
- `internal/api`:状态页限流与访问码哈希
- `internal/app`:monitor 与 frp 两侧状态字面量的翻译
-
-### 变更
-
-- 新增迁移 `026_route_health.sql`、`027_metrics_samples.sql`(启动时自动执行)
-- agent 无改动,`AgentVersion` 保持 **0.12.2**,本轮不需要在 VPS 上重新升级 Agent
-- 版本号提升到 1.2.11;`web/package.json` 同步对齐
-
-## [v1.2.10] - 2026-09-25
-
-### 新增
-
-- **告警事件新增三项巡检**(默认关闭,设置页「告警事件 → 系统」):
- - **公网服务端掉线**:服务端进程没在跑、或隧道连不上 frps 时通知,恢复时再通知一次;只对「用户要它运行」的服务端判定,手动停止的不报
- - **公网 agent 不可达**:VPS 上的 fonu-agent 连续无响应(重启后没起来最常见),恢复时再通知一次;没配 agent 的服务端不参与判定
- - **配置漂移**:VPS 上的 vhost 与 Fonu 记录不一致(被手工改过、或 agent 重装后没重部署)时通知,漂移清空时补一条恢复
-- 通知图标与分类补齐:📈 系统(资源占用)、🔌🛑📡 隧道(隧道 / 服务端 / agent)、🧭 配置(漂移)
-- 通知方式的 `Webhook` 改名为「Webhook / IM 机器人」:企业微信、钉钉、飞书机器人挂在 Webhook 预设下,原标签没说明白
-
-### 改进
-
-- 巡检改为 Source 接口(Routes / Servers / Agents / Drift),`internal/app/monitor_source.go` 负责把 frp 服务适配进来,`internal/monitor` 因此不依赖 frp 包
-- 「连续 N 次失败」抽成共用的 `failureTransition`(原 `tunnelTransition` 泛化):三条新巡检与原有隧道巡检共用「刚好达标报一次、恢复报一次」;漂移不是连续失败型,单独用冷却(默认 24 小时,可配)
-- 避免误报的三条约定:服务端只在「用户要它运行」时判定;`ConnectionState` 为 `unknown` 按健康处理;没配 agent 的服务端不参与可达性判定
-- 开关关闭时连数据采集都跳过(关掉的功能不该有后台开销);所有 `notify.Alert` 一律在锁外调用
-- 新增设置项:`notify_on_frps_down` / `notify_on_agent_unreachable` / `notify_on_route_drift`,以及 `notify_frps_fail_threshold`(2) / `notify_agent_fail_threshold`(3) / `notify_drift_cooldown_hours`(24)
-
-### 测试
-
- `internal/monitor/monitor_test.go` 增加 `serverDown`(手动停止不报、进程未运行或连接断开要报、连接状态未知不误报)、`serverLabel`、`stateOr` 用例
-### 变更
-
-- 版本号提升到 1.2.10(版本单一来源仍是根目录 `VERSION`);`web/package.json` 同步对齐
-- agent 无改动,`AgentVersion` 保持 **0.12.2**,本轮不需要在 VPS 上重新升级 Agent
-
## [v1.2.9] - 2026-09-24
### 新增
@@ -890,36 +446,24 @@ All notable changes to this project will be documented in this file.
- **阈值型告警**(默认关闭):CPU / 内存 / 磁盘持续超阈值、隧道连续 N 次未在 frps 上注册时通知,恢复时再通知一次;阈值与开关在设置页「告警事件」里配置
- **API Token**:新增 `api_tokens` 表与 `Authorization: Bearer ` 认证,供脚本 / HomeAssistant / 手机快捷指令调用;设置页「高级」可签发(明文只显示一次)、查看最近使用时间与撤销
-### 修复
-
-- agent 写 vhost 时 `nginx -t` 校验失败**不再直接删文件**(那等于把在跑的站点从磁盘上抹掉),改为恢复上一个可用版本
-- 配置版本名做路径校验,拒绝带 `/` 或 `..` 的名称,避免接口被用来读版本目录以外的文件
-- 通知:切换 Webhook 预设时服务地址自动换成该预设默认值(此前会把 Bark 的 `api.day.app` 带到企业微信上)
-- API Token 的令牌管理、改密、退出**只允许登录会话**——否则一个 write 令牌能给自己提权或无限续期
-
### 改进
- `internal/fsutil` 新增 `BackupVersioned` / `ListVersions` / `ReadVersion`;`internal/nginx` 的回滚与 `Apply` 共用抽出的 `activate()`,两条路径行为一致
- 新增 `internal/monitor` 巡检包(每 2 分钟一轮,注册进 `internal/scheduler`);资源类告警同一项 30 分钟静默期,回落到阈值以下即清冷却,开关关闭时连采集与探测都跳过
- `providerUsesKey()` 收敛「凭据放 Key 还是 WebhookSecret」的判定;前端新增 `ConfigDiffView.vue` 与 `utils/diff.ts`(LCS 行级差异,超长文本退化为整体替换)
- API Token 的 `last_used_at` 限频回写(1 分钟一次),单连接 SQLite 上不做每请求写
-
-### 测试
-
-- `internal/auth/token_test.go`(令牌生命周期 / 过期 / 非法输入 / 明文不落盘)、`internal/api/middleware_test.go`(只读令牌只读、写令牌可写、令牌不能管理令牌、错误凭据 401)
-- `internal/monitor/monitor_test.go`(连续失败只报一次、恢复报一次、未达阈值静默)
-- `internal/notify/push_test.go` 增加企业微信 / 钉钉 / 飞书三家的端点与 payload 用例;`internal/fsutil` 增加版本列表 / 内容去重 / 路径校验用例
-
-### 变更
-
-- `AgentVersion` 0.12.1 → **0.12.2**(域名 vhost 的版本列表 / 对比 / 回滚端点,写配置前留版本);内嵌 `fonu-agent-linux-amd64` 与 `-arm64` 已重编,VPS 上需点「升级 Agent」才会生效
-- 版本号提升到 1.2.9(版本单一来源仍是根目录 `VERSION`);`web/package.json` 同步对齐
-- 新增 `docs/feature-benchmark-2026-09-24.md`:同类项目对标与增补建议,含 §九 本轮 P0 实施状态
-
-## [v1.2.8] - 2026-09-24
+- 新增 `internal/fsutil`:`WriteFileAtomic`(同目录随机名临时文件 + rename + 显式 chmod)与 `Backup`(文件不存在视为无需备份),收敛 agent / frp / nginx / chinacidr 各处自建的 `.tmp` 与 `.bak` 逻辑(其中 nginx 原来用固定 `.tmp` 名,并发写同一路径会互相踩)
+- 流量采集:`notify.RecordAccessHit`(写库 + 判断通知)从写锁内移到锁外,不再顶住 `/api/proxies/traffic` 与 `/clients` 的读
+- `internal/frp` 的 agent 客户端改为按 `serverID` 缓存(指纹 = `AgentURL` + Token 密文),批量同步 N 条路由不再新建 N 个连接池与 TLS 握手
+- 前端新增 `useVisibilityPolling()`:标签页隐藏时暂停轮询、回前台立即补一次;公网反代页与内网穿透页已接入,仪表盘流量轮询加 in-flight 互斥避免堆积并发请求
+- 新增 `docs/README.md` 文档索引;给 `agent-capability-plan` / `agent-view-design` / `-v2` / `upstream-sync-plan` 加实施状态与时效说明
### 修复
+- agent 写 vhost 时 `nginx -t` 校验失败**不再直接删文件**(那等于把在跑的站点从磁盘上抹掉),改为恢复上一个可用版本
+- 配置版本名做路径校验,拒绝带 `/` 或 `..` 的名称,避免接口被用来读版本目录以外的文件
+- 通知:切换 Webhook 预设时服务地址自动换成该预设默认值(此前会把 Bark 的 `api.day.app` 带到企业微信上)
+- API Token 的令牌管理、改密、退出**只允许登录会话**——否则一个 write 令牌能给自己提权或无限续期
- 公网服务端:`frps` 启停失败不再假成功——之前忽略 `systemctl` 的 err 后无条件写「frps.service 已启动」,现在失败时返回 `ok:false` 与 stderr,运维不会误以为服务在跑
- frps 配置写入:`.bak` 备份失败时立即中止并删除 `.verify`,不再覆盖唯一副本(旧配置得留退路)
- 安装 frps 后下发同源 `frps.toml` 失败不再静默吞掉:结果里带 `config_warning` 并追加到 `notes`
@@ -930,29 +474,6 @@ All notable changes to this project will be documented in this file.
- 公网反代页:`refreshAll` 的 7 个独立接口由串行 `await` 改为 `Promise.all`,每次刷新不再等于 7 个往返相加
- 内网穿透页:删除 74 条随服务端 UI 迁走的死样式;公网服务端页合并逐字重复的样式块
- 文档与工程:修正 README / DEPLOYMENT / `frps_template.go` 中「Docker 镜像内置 frpc 0.71.0」的不准确说法(镜像不预装、按需下载);`.dockerignore` 删掉两行已失效的归档排除项并修掉 ` docs` 的前导空格
-
-### 改进
-
-- 新增 `internal/fsutil`:`WriteFileAtomic`(同目录随机名临时文件 + rename + 显式 chmod)与 `Backup`(文件不存在视为无需备份),收敛 agent / frp / nginx / chinacidr 各处自建的 `.tmp` 与 `.bak` 逻辑(其中 nginx 原来用固定 `.tmp` 名,并发写同一路径会互相踩)
-- 流量采集:`notify.RecordAccessHit`(写库 + 判断通知)从写锁内移到锁外,不再顶住 `/api/proxies/traffic` 与 `/clients` 的读
-- `internal/frp` 的 agent 客户端改为按 `serverID` 缓存(指纹 = `AgentURL` + Token 密文),批量同步 N 条路由不再新建 N 个连接池与 TLS 握手
-- 前端新增 `useVisibilityPolling()`:标签页隐藏时暂停轮询、回前台立即补一次;公网反代页与内网穿透页已接入,仪表盘流量轮询加 in-flight 互斥避免堆积并发请求
-- 新增 `docs/README.md` 文档索引;给 `agent-capability-plan` / `agent-view-design` / `-v2` / `upstream-sync-plan` 加实施状态与时效说明
-
-### 测试
-
-- Go 新增 4 个测试文件:`backup`(`../` 逃逸与 `../data-other/` 同前缀绕过、还原权限固定)、`scheduler`(非正间隔跳过、panic 后继续)、`frp`(`shellPath` 单引号转义、`isELFBinary`)、`agent`(`validateRoute` 表驱动、`nginxRoute` 关键行、`fileSHA256`)
-- 前端引入 vitest(`npm test` = `vitest run`),新增 13 个单测覆盖 `utils/format.ts` 与 `utils/status.ts`(含一条守住「n-tag 的 type 值不能当状态值」的回归用例);CI 增加「Run web unit tests」一步
-
-### 变更
-
-- `AgentVersion` 0.12.0 → **0.12.1**(frps 启停失败如实回报、HTTP 服务补超时);内嵌 `fonu-agent-linux-amd64` 与 `-arm64` 已重编,VPS 上需点「升级 Agent」才会生效
-- 版本号提升到 1.2.8(版本单一来源仍是根目录 `VERSION`);`web/package.json` 的版本同步对齐,本地构建不再显示 1.0.2
-
-## [v1.2.7] - 2026-09-24
-
-### 修复
-
- 安全(P0):备份恢复的上传文件名不再拼进临时路径,改用 `os.CreateTemp`——此前 multipart 文件名完全可控,`../../root/.ssh/authorized_keys` 之类可逃出临时目录(`internal/api/backup_handler.go`)
- 安全(P0):备份还原的条目路径校验从「字符串前缀比较」改为 `filepath.Rel` 判定并拒绝 `..`——此前 `DataDir=/data` 时条目 `../data-other/x` 能通过前缀匹配写到 DataDir 之外;同时不再照搬归档里的权限位(固定文件 `0o640` / 目录 `0o750`),避免构造的归档造出 0777 或带执行位的文件
- 安全(P0):agent 安装 frps 时 `proxy` 参数改走白名单(与「FRP 下载加速代理」同一组前缀),且改为先取官方 `frp__sha256sums.txt`、下载后比对 SHA-256 通过才解压安装——此前该参数未校验就拼进下载地址,等于可让 VPS 拉取任意地址的二进制并装成 frps
@@ -965,68 +486,29 @@ All notable changes to this project will be documented in this file.
### 变更
-- `AgentVersion` 0.11.0 → **0.12.0**(frps 安装校验 + Basic Auth 密码处理),内嵌 `fonu-agent-linux-amd64`(10.4 MB)与 `-arm64`(9.7 MB)已重编,VPS 上需点「升级 Agent」才会生效
+- `AgentVersion` 0.11.0 → **0.12.2**(0.12.0:frps 安装校验 + Basic Auth 密码处理;0.12.1:frps 启停失败如实回报、HTTP 服务补超时;0.12.2:域名 vhost 的版本列表 / 对比 / 回滚端点),内嵌 `fonu-agent-linux-amd64` 与 `-arm64` 已重编,VPS 上需点「升级 Agent」才会生效
+- 新增 `docs/feature-benchmark-2026-09-24.md`:同类项目对标与增补建议,含 §九 本轮 P0 实施状态
- 新增 `.github/workflows/ci.yml`:push / PR 触发,按「前端构建 → 同步 dist 到 `cmd/fonu/web/dist` → `go vet` → `go test ./... -count=1` → `go build ./...`」执行(顺序不可颠倒:`cmd/fonu` 用 `//go:embed all:web/dist`,干净检出缺该目录时编译直接失败);gofmt 卡口待全仓格式化后再开
- `web/package.json` 版本由 1.0.2 对齐到 `VERSION`,并声明 `packageManager: npm@10.9.4`(与 Dockerfile 的 `node:22-alpine` 一致);删除 `web/pnpm-workspace.yaml`(占位残留,仓库并无 pnpm 锁文件)
-- 版本号提升到 1.2.7(版本单一来源仍是根目录 `VERSION`)
+
+### 测试
+
+- `internal/auth/token_test.go`(令牌生命周期 / 过期 / 非法输入 / 明文不落盘)、`internal/api/middleware_test.go`(只读令牌只读、写令牌可写、令牌不能管理令牌、错误凭据 401)
+- `internal/monitor/monitor_test.go`(连续失败只报一次、恢复报一次、未达阈值静默)
+- `internal/notify/push_test.go` 增加企业微信 / 钉钉 / 飞书三家的端点与 payload 用例;`internal/fsutil` 增加版本列表 / 内容去重 / 路径校验用例
+- Go 新增 4 个测试文件:`backup`(`../` 逃逸与 `../data-other/` 同前缀绕过、还原权限固定)、`scheduler`(非正间隔跳过、panic 后继续)、`frp`(`shellPath` 单引号转义、`isELFBinary`)、`agent`(`validateRoute` 表驱动、`nginxRoute` 关键行、`fileSHA256`)
+- 前端引入 vitest(`npm test` = `vitest run`),新增 13 个单测覆盖 `utils/format.ts` 与 `utils/status.ts`(含一条守住「n-tag 的 type 值不能当状态值」的回归用例);CI 增加「Run web unit tests」一步
## [v1.2.6] - 2026-09-24
-### 改进
-
-- 公网服务端页(`/frp-agent`)改为 DDNS 页那样的**左列表 + 右详情**:左侧是服务端列表(名称 / 启动状态 / `主机:端口` / 延迟与位置 / TLS·Token·Agent·SSH 标签,点行即选中并高亮),右侧是选中服务端的详情;原列表行内操作(启动·停止 / 查看 / 日志 / 测速 / 校验 / 编辑 / 删除)移到右侧工具条,顶部「选择服务端」下拉去掉(左侧列表本身就是选择器),页面标题改「公网服务端」,≤1100px 收成一列
-- 内网穿透页(`/frp`)改为**左:服务端列表 / 右:该服务端的穿透规则**:左侧点服务端即筛选(显示规则数与启用数),右侧保留原来的穿透规则表格(`:data` 按选中服务端过滤、去掉冗余的「服务端」列、`:scroll-x` 2020 → 1890),工具条显示当前服务端与规则条数;规则详情仍是弹窗(表格「详情」按钮),实时速率采样只在弹窗打开期间进行
-- 服务端列表从内网穿透页整体并入公网服务端页:服务端表单 / 服务端日志 / 服务端详情 / frps.toml 同源配置四个弹窗随列表一起迁移;内网穿透页不再持有服务端的增删改与启停
-- 内网穿透页规则区在「尚未配置服务端」时给出指向「公网服务端」页的空态(原来只剩一个被禁用的「添加规则」按钮)
-- 日志中心「公网 FRP」页签改为时间倒序(新日期在前),分页与筛选跟着一致
-
-### 变更
-
-- 「Agent 管理」跳转改为深链:内网穿透页点某台服务端直接落到该台详情(`/frp-agent?server=`,id 不存在时回落为第一台)
-- 服务端列表不再按「已配 Agent / SSH」过滤——未配的服务端也要能在本页编辑与删除
-- 版本号提升到 1.2.6(版本单一来源仍是根目录 `VERSION`)
-
-## [v1.2.5] - 2026-09-24
-
### 新增
-- 公网 Agent 能力扩展(agent 0.11.0):新增 `GET /api/v1/certs`(VPS 证书清单——遍历证书目录、用标准库 `crypto/x509` 解析 `fullchain.pem` 的到期时间、按剩余天数排序、解析失败的单条记入 `warnings` 不中断整轮)与 `POST /api/v1/nginx/reload`(显式校验并重载 Nginx,分别回传 `nginx -t` 与 reload 的失败原因;成功后让可用性探测缓存失效,避免页面继续显示旧结论)
- 公网 Agent 页:新增「VPS 证书」卡(域名 / 剩余天数 / **Fonu 证书库** / 到期时间),与 Fonu 证书库交叉核对(含通配符匹配),宝塔自行申请的证书也能看到;剩余不足 30 天转橙、已过期转红加粗
- 公网 Agent 页:Nginx 卡片新增「重载 Nginx」按钮,校验 / 重载失败原因(`nginx -t` 输出或 reload 的 stderr)直接显示在卡内
-
-### 改进
-
-- 公网 Agent 页布局:「VPS 资源」与「VPS 证书」改为并排各占半行——`dep-grid` 由 3 列改为 6 列并按 span 分配(原有三张卡仍每行三张),半宽下资源三项指标等分、平均负载与运行时长落到卡底一行,证书表列宽收紧;≤1199px 复位跨度避免 `span` 在列数变少后撑破网格
-- 应用侧对 agent 端点的版本要求提升到 **≥ 0.11.0**(旧 agent 仍按 404/405 统一降级为「请升级 Agent」提示)
-
-### 变更
-
-- `AgentVersion` 0.10.0 → **0.11.0**;内嵌 `fonu-agent-linux-amd64`(10.0 MB)与 `-arm64`(9.3 MB)已重编,VPS 上需点「升级 Agent」才会生效
-- 版本号提升到 1.2.5(版本单一来源仍是根目录 `VERSION`)
-
-## [v1.2.4] - 2026-09-24
-
-### 新增
-
-- 公网 Agent 能力扩展(agent 0.10.0):新增三个只读端点——`GET /api/v1/metrics`(VPS 主机资源:CPU / 内存 / 磁盘 / 负载 / 运行时长,CPU 用两次采样差值,1 秒内的重复请求直接复用上次结果)、`GET /api/v1/logs/nginx`(access / error 日志尾部,路径只从 `nginx -T` 实际生效配置、`FONU_AGENT_NGINX_LOG_DIR` 或默认目录取;按 32KB 块倒读,单行 4KB 截断,上限 2000 行)、`GET /api/v1/frps/config`(frps.toml 回读,256KB 上限,文件不存在返回 `exists: false`)
+- **公网 Agent 能力扩展(agent 0.10.0 / 0.11.0)**:新增 `GET /api/v1/metrics`(VPS 主机资源:CPU / 内存 / 磁盘 / 负载 / 运行时长,CPU 用两次采样差值,1 秒内的重复请求直接复用上次结果)、`GET /api/v1/logs/nginx`(access / error 日志尾部,路径只从 `nginx -T` 实际生效配置、`FONU_AGENT_NGINX_LOG_DIR` 或默认目录取;按 32KB 块倒读,单行 4KB 截断,上限 2000 行)、`GET /api/v1/frps/config`(frps.toml 回读,256KB 上限,文件不存在返回 `exists: false`)、`GET /api/v1/certs`(VPS 证书清单——遍历证书目录、用标准库 `crypto/x509` 解析 `fullchain.pem` 的到期时间、按剩余天数排序、解析失败的单条记入 `warnings` 不中断整轮)与 `POST /api/v1/nginx/reload`(显式校验并重载 Nginx,分别回传 `nginx -t` 与 reload 的失败原因;成功后让可用性探测缓存失效,避免页面继续显示旧结论)
- 公网 Agent 页:新增「VPS 资源」横排带(CPU / 内存 / 磁盘三项并排 + 进度条 + 右侧平均负载与运行时长),占满「环境与依赖」整行,不做单独占格
- 日志中心:新增「公网 Nginx」页签(服务端 / 访问或错误日志 / 行数 200·500·2000 + 关键词搜索 + 分页),并显示 agent 解析出的实际日志路径与「仅显示尾部」标记
- 公网服务端「frps.toml 同源配置」弹窗:新增「比对 VPS 实际文件」——去注释与空行后逐行比对,一致给出绿色说明,不一致指出第一个差异行(下发值 / VPS 值)
-
-### 改进
-
-- `internal/sysinfo` 补 `LoadAvg()` 与 `UptimeSeconds()`:Linux 读 `/proc/loadavg`、`/proc/uptime`,Windows 用 `GetTickCount64`,其它平台返回空值
-- 老版本 agent 的降级:三个新端点遇到 HTTP 404/405 时统一转成「VPS 上的 agent 版本过低,不支持该接口(需 ≥ 0.10.0),请在服务端详情里升级 Agent」的可操作提示,而不是原始 HTTP 错误
-
-### 变更
-
-- `AgentVersion` 0.9.2 → **0.10.0**;内嵌 `fonu-agent-linux-amd64`(9.9 MB)与 `-arm64`(9.2 MB)已用新代码重编,VPS 上需点「升级 Agent」才会生效
-- 版本号提升到 1.2.4(版本单一来源仍是根目录 `VERSION`)
-
-## [v1.2.3] - 2026-09-24
-
-### 新增
-
- 仪表盘:新增「公网穿透」统计卡,并在「运行状态」面板增加「公网反代」行——数据来自新增的 `GET /api/frpmulti/routes/summary`(逐台服务端采集四段健康后聚合,单台失败只记名不中断)
- 仪表盘:新增「性能」面板(今日异常 / 平均响应,`/api/status` 早已返回但页面从未展示),「实时流量」面板新增「流量 Top 5」(直接复用已有的逐规则流量轮询)
- 仪表盘:新增一行三卡「系统资源 / 中国 IP 段 / 通知通道」;系统资源来自新增的跨平台采集包 `internal/sysinfo`(Linux `/proc`、Windows kernel32,无新增依赖;容器内为宿主机视角)
@@ -1036,6 +518,15 @@ All notable changes to this project will be documented in this file.
### 改进
+- 公网服务端页(`/frp-agent`)改为 DDNS 页那样的**左列表 + 右详情**:左侧是服务端列表(名称 / 启动状态 / `主机:端口` / 延迟与位置 / TLS·Token·Agent·SSH 标签,点行即选中并高亮),右侧是选中服务端的详情;原列表行内操作(启动·停止 / 查看 / 日志 / 测速 / 校验 / 编辑 / 删除)移到右侧工具条,顶部「选择服务端」下拉去掉(左侧列表本身就是选择器),页面标题改「公网服务端」,≤1100px 收成一列
+- 内网穿透页(`/frp`)改为**左:服务端列表 / 右:该服务端的穿透规则**:左侧点服务端即筛选(显示规则数与启用数),右侧保留原来的穿透规则表格(`:data` 按选中服务端过滤、去掉冗余的「服务端」列、`:scroll-x` 2020 → 1890),工具条显示当前服务端与规则条数;规则详情仍是弹窗(表格「详情」按钮),实时速率采样只在弹窗打开期间进行
+- 服务端列表从内网穿透页整体并入公网服务端页:服务端表单 / 服务端日志 / 服务端详情 / frps.toml 同源配置四个弹窗随列表一起迁移;内网穿透页不再持有服务端的增删改与启停
+- 内网穿透页规则区在「尚未配置服务端」时给出指向「公网服务端」页的空态(原来只剩一个被禁用的「添加规则」按钮)
+- 日志中心「公网 FRP」页签改为时间倒序(新日期在前),分页与筛选跟着一致
+- 公网 Agent 页布局:「VPS 资源」与「VPS 证书」改为并排各占半行——`dep-grid` 由 3 列改为 6 列并按 span 分配(原有三张卡仍每行三张),半宽下资源三项指标等分、平均负载与运行时长落到卡底一行,证书表列宽收紧;≤1199px 复位跨度避免 `span` 在列数变少后撑破网格
+- 应用侧对 agent 端点的版本要求提升到 **≥ 0.11.0**(旧 agent 仍按 404/405 统一降级为「请升级 Agent」提示)
+- `internal/sysinfo` 补 `LoadAvg()` 与 `UptimeSeconds()`:Linux 读 `/proc/loadavg`、`/proc/uptime`,Windows 用 `GetTickCount64`,其它平台返回空值
+- 老版本 agent 的降级:三个新端点遇到 HTTP 404/405 时统一转成「VPS 上的 agent 版本过低,不支持该接口(需 ≥ 0.10.0),请在服务端详情里升级 Agent」的可操作提示,而不是原始 HTTP 错误
- 仪表盘「服务」卡:原先写死「正常 / 4 个」,现按 Fonu / Nginx / DDNS / 证书四项实际状态聚合,异常时点名并把徽标降级为 warning/error(此前 Nginx 停止、DDNS 异常时该卡仍显示「正常」,与右侧「运行状态」面板自相矛盾)
- 仪表盘轮询:`/api/status` 每 15 秒刷新一次(CPU 占用率需要两次采样才有意义),并在页面隐藏时暂停「实时流量」与「状态」两个轮询
- 公网反代健康列:证书徽标补「证书来源」——`Fonu 证书库:有(剩余 N 天)` 或 `无(需先在证书管理申请)`,用于区分「没有证书可推送」与「VPS 上证书过期或未覆盖」
@@ -1047,7 +538,9 @@ All notable changes to this project will be documented in this file.
### 变更
-- 版本号提升到 1.2.3(版本单一来源仍是根目录 `VERSION`)
+- 「Agent 管理」跳转改为深链:内网穿透页点某台服务端直接落到该台详情(`/frp-agent?server=`,id 不存在时回落为第一台)
+- 服务端列表不再按「已配 Agent / SSH」过滤——未配的服务端也要能在本页编辑与删除
+- `AgentVersion` 0.9.2 → **0.11.0**(0.10.0:metrics / nginx 日志 / frps 配置只读端点;0.11.0:VPS 证书清单与 Nginx 显式重载),内嵌 `fonu-agent-linux-amd64` 与 `-arm64` 已重编,VPS 上需点「升级 Agent」才会生效
## [v1.2.2] - 2026-09-23
@@ -1055,61 +548,6 @@ All notable changes to this project will be documented in this file.
- 日志中心:新增「FRP 日志」Tab——把 frpc 运行日志解析为 时间 / 级别 / 来源 / 消息 四列,支持关键词与级别筛选、分页、错误/警告行着色,与系统日志、访问日志、Nginx 日志同一套交互(对齐上游 1.0.3)
- 新增 `web/src/utils/clipboard.ts`:统一的剪贴板复制工具(异步剪贴板 API 优先,失败或非安全上下文时回退 `execCommand`)
-
-### 改进
-
-- 剪贴板复制在非 HTTPS 环境可用:Fonu 常以 `http://内网IP:6893` 访问,属非安全上下文,此时 `navigator.clipboard` 为 undefined,复制必然失败;公网反代(复制域名 / 复制配置)、公网 Agent(配置查看)、DDNS、本地反向代理(复制访问链接)共 5 处调用点统一改用上述工具(对齐上游 1.0.4)
-- 容器时区:`Dockerfile` 新增 `TZ=Asia/Shanghai` 并在运行时阶段安装 `tzdata`(只设 `TZ` 而镜像内无 zoneinfo 时 Go 仍会回落 UTC),三个 compose 文件均支持 `TZ` 覆盖——通知消息时间按系统时区显示(对齐上游 1.0.4)
-- 反代配置查看弹窗:补齐内容区内边距(与头部 24px 对齐),新增来源说明(由 VPS 上的 fonu-agent 生成、文件名 `fonu-<域名>.conf`、手工修改会被下次部署覆盖),并改为说明固定 + 代码块内部滚动
-- 反向代理:复制规则不再自动追加「-复制」后缀,保留原名称与域名、由用户在表单里确认修改(原先一点复制就得到一个改过名的副本,容易误建同名规则)(对齐上游 1.0.5)
-- FRP 穿透页:frps 同源配置弹窗新增「VPS 防火墙 / 安全组放行 TCP 远程端口」提示,端口列表由已启用的 TCP/UDP 规则实时推导(与 frps 的 `allowPorts` 同源;未放行时隧道连不上,但日志不会点明原因)(对齐上游 1.0.3)
-- 通知:Telegram 代理地址只填 `host:port` 时自动补全 `http://`,并校验 scheme 仅支持 http / https / socks5,错误信息带上原始地址便于自查(对齐上游 1.0.3)
-
-### 修复
-
-- 通知:单个通道密文解密失败不再让整份配置加载失败——原先 SMTP / Webhook / Telegram 任一密文损坏(例如更换过加密密钥)都会直接返回错误,导致其它已配置通道一并无法告警;现改为仅把该通道标记为「未配置」并继续解析其它通道(对齐上游 1.0.5)
-- 反向代理:监听占用冲突提示写明占用方——优先规则备注名,其次该规则首个域名,最后回落 `规则 #id`;原先只说「已被其他规则使用」,用户得自己翻列表找是哪一条
-- 反向代理:手动(custom)Nginx 配置的监听占用纳入校验。custom 配置的正本在文件系统(数据库只记 `nginx_mode` 开关),其 `listen` / `server_name` 原先完全不参与校验,与其它规则「同域名 + 同端口」的冲突只会在 Nginx reload 时静默失效(第一个 server 块生效、后面的被忽略);现于保存 / 回滚时解析并写入新表 `proxy_custom_endpoints`,与表单规则共用同一条判定(对齐上游 1.0.5)
-- 反向代理:并发下修改监听端口可能绕过保存前校验(`UPDATE proxy_hosts` 的唯一约束冲突此前直接抛出原始 SQL 错误),现改为按域名复检并给出带规则名的提示
-
-### 变更
-
-- 新增 `docs/upstream-sync-plan.md`:上游 1.0.2 → 1.0.5 的逐版本对照方案,含 8 项待核对项的代码级核实结论、不照搬项及原因、以及「可增加项清单」(11 项,按价值排序)
-- 新增迁移 `024_proxy_custom_endpoints.sql`(手动 Nginx 配置的占用表,`UNIQUE(rule_id, hostname, listen_port)` + `ON DELETE CASCADE`),解析器 `internal/nginx/endpoints.go`(`ParseServerEndpoints`,保守解析:`include` 片段 / 正则 `server_name` / `listen unix:` 一律忽略,宁可漏报不误报)与 `internal/proxy/custom_endpoints.go`
-- 补测试:`internal/nginx/endpoints_test.go`(多 server 块、`[::]` 写法、通配与正则、无 `listen` 默认 80)、`internal/proxy/custom_endpoints_test.go`(走真实迁移建内存库:冲突含规则名、排除自身、同域名不同端口放行、custom 占用对表单规则可见、级联清理)
-- `docs/upstream-sync-plan.md` 补 §八「第 5 项详细整理」:Nginx 端口占用语义(一条规则占用哪些端口)、现有校验链逐跳、三处真实缺口与方案 A/B/C 对比、实施记录与未覆盖边界
-
-
-## [v1.2.1] - 2026-09-23
-
-### 改进
-
-- 公网 Agent 页:按「功能清单」重排为五段分区(运行状态 / 环境与依赖 / frps 控制 / Agent 生命周期 / 输出与日志);4 张状态卡统一为标准 `.stats-row` + `.stat-card` 规格;补齐 5 项「数据已返回但页面未显示」的能力——Agent 凭据状态、8080 端口、已注册反代路由数、最后检查时间、Nginx 可用性
-- 公网 Agent 页:「环境与依赖」改为三列并排(Fonu Agent / Nginx / frps 运行时),卡内短字段双列、长路径整行;「frps 控制」与「Agent 生命周期」两列并排;新增「配置查看」按钮(只读弹窗展示即将下发的 frps 配置,含复制),卸载(含数据)增加二次确认
-- 公网反代页:规则台账改用 `n-data-table`(与本地反向代理页一致,表头与数据天然对齐),列为 规则/域名(含复制与打开按钮)、协议、上游/回源、健康(DNS·证书·隧道·服务)、连接、当前上传、当前下载、总上传、总下载、更新时间、操作
-- 公网反代页:新增统计区(可部署规则 / 已生效 / 健康通过 / 当前连接)与环境与 Nginx 卡(4 列);「使用说明」整卡改为页面级提示与入口行;部署设置弹窗对齐本地反向代理页规格(900px、三段式、页脚分隔线、开关行底色)并新增右侧「配置说明」栏
-- 公网反代页:协议列改为按部署设置的「启用 HTTPS」自动识别(规则 type 恒为 http,不能代表入口协议),HTTPS 绿色 / HTTP 灰色并带悬停说明
-- FRP 穿透页:4 张状态卡统一为标准统计区;公网服务端列表由自建网格改为 `n-data-table`(8 列,含启动/停止/查看/日志/测速/校验/Agent 管理/编辑/删除)
-- FRP 穿透页:6 个弹窗统一到同一外壳(`.frp-card-modal` / `.frp-detail-card`),运行日志、导出配置、frps 同源配置、FRP 应用配置的头部/滚动区/页脚一致
-- 统一设计令牌:清理硬编码色值(公网反代页 6 处、FRP 穿透页 13 处)与内联样式,删除死样式;统计区、弹窗、表格单元格全部走 `--fonu-*` 令牌与共享组件(`StatusBadge` / `EmptyState` / `LoadError` / `FonuCard`)
-- 表格与列表交互:数字列右对齐、操作按钮右对齐并按行禁用、长文本换行、单元格内容垂直居中;窄屏按 1200 / 900 / 760 断点降列
-
-### 修复
-
-- FRP 穿透页「运行中」状态显示为灰色:`statusKind` 返回 `'success'` 却被当作 `value` 传给 `StatusBadge`(`utils/status.ts` 不识别该值,落到 `unknown`),且其中含非法的 `'default'`;现改为返回合法 `StatusKind` 并改用 `:kind`,服务端详情弹窗同类问题一并修复
-- 表格单元格样式整体失效:`n-data-table` 的 render 函数用 `h()` 创建的节点不带 scoped 属性,导致 `table-muted` / `type-chip` / `mono` 与各 `cell-*` 样式未生效(表现为「上游/回源」两行变一行、域名与按钮错位、徽标无间距);统一改用 `:deep()` 选择器
-- 公网反代页刷新失败静默:`loadErrorMsg` 此前写入但从未渲染,现改为顶部可见提示,并让单个接口失败不再中断整轮数据加载
-- 公网反代页切换服务端残留上一台数据、轮询在页面隐藏时仍请求、loading 粒度粗(任一操作全页按钮转圈)等问题修复
-
-### 变更
-
-- 新增设计文档:`docs/agent-view-design-v2.md`(公网 Agent 页)、`docs/proxy-remote-design.md`(公网反代页),以及被取代留档的 `docs/agent-view-design.md`、`docs/agent-view-refactor-plan.md`
-- 新增页面 UI 标准 skill(工作区级 `.breezell/skills/fonu-page-ui`,不在仓库内):页面骨架、共享组件契约、设计令牌、类名约定、弹窗标准结构、脚本约定与验收清单
-
-## [v1.2.0] - 2026-09-23
-
-### 新增
-
- FRP 公网反代:独立「公网反代」页(`/public-proxy`)——服务端选择、规则列表(名称 / 域名 / 内网目标 / 回源端口)、部署与移除、配置查看、15 秒自动刷新
- FRP 公网反代:**部署后回源自检**——agent 写入配置后模拟 Nginx 回源(`Host: 域名` → `127.0.0.1:`)并分级提示:0=连接不上 vhost、404=隧道未注册、5xx=内网服务异常,不再出现「部署成功但打开 502」
- FRP 公网反代:**状态徽章实时化**——基于 `nginx -T` 同时判定「文件存在 / 语法通过 / 实际被加载」,展示已生效 / 已写入未加载 / 语法错误 / 未部署
@@ -1129,6 +567,21 @@ All notable changes to this project will be documented in this file.
### 改进
+- 剪贴板复制在非 HTTPS 环境可用:Fonu 常以 `http://内网IP:6893` 访问,属非安全上下文,此时 `navigator.clipboard` 为 undefined,复制必然失败;公网反代(复制域名 / 复制配置)、公网 Agent(配置查看)、DDNS、本地反向代理(复制访问链接)共 5 处调用点统一改用上述工具(对齐上游 1.0.4)
+- 容器时区:`Dockerfile` 新增 `TZ=Asia/Shanghai` 并在运行时阶段安装 `tzdata`(只设 `TZ` 而镜像内无 zoneinfo 时 Go 仍会回落 UTC),三个 compose 文件均支持 `TZ` 覆盖——通知消息时间按系统时区显示(对齐上游 1.0.4)
+- 反代配置查看弹窗:补齐内容区内边距(与头部 24px 对齐),新增来源说明(由 VPS 上的 fonu-agent 生成、文件名 `fonu-<域名>.conf`、手工修改会被下次部署覆盖),并改为说明固定 + 代码块内部滚动
+- 反向代理:复制规则不再自动追加「-复制」后缀,保留原名称与域名、由用户在表单里确认修改(原先一点复制就得到一个改过名的副本,容易误建同名规则)(对齐上游 1.0.5)
+- FRP 穿透页:frps 同源配置弹窗新增「VPS 防火墙 / 安全组放行 TCP 远程端口」提示,端口列表由已启用的 TCP/UDP 规则实时推导(与 frps 的 `allowPorts` 同源;未放行时隧道连不上,但日志不会点明原因)(对齐上游 1.0.3)
+- 通知:Telegram 代理地址只填 `host:port` 时自动补全 `http://`,并校验 scheme 仅支持 http / https / socks5,错误信息带上原始地址便于自查(对齐上游 1.0.3)
+- 公网 Agent 页:按「功能清单」重排为五段分区(运行状态 / 环境与依赖 / frps 控制 / Agent 生命周期 / 输出与日志);4 张状态卡统一为标准 `.stats-row` + `.stat-card` 规格;补齐 5 项「数据已返回但页面未显示」的能力——Agent 凭据状态、8080 端口、已注册反代路由数、最后检查时间、Nginx 可用性
+- 公网 Agent 页:「环境与依赖」改为三列并排(Fonu Agent / Nginx / frps 运行时),卡内短字段双列、长路径整行;「frps 控制」与「Agent 生命周期」两列并排;新增「配置查看」按钮(只读弹窗展示即将下发的 frps 配置,含复制),卸载(含数据)增加二次确认
+- 公网反代页:规则台账改用 `n-data-table`(与本地反向代理页一致,表头与数据天然对齐),列为 规则/域名(含复制与打开按钮)、协议、上游/回源、健康(DNS·证书·隧道·服务)、连接、当前上传、当前下载、总上传、总下载、更新时间、操作
+- 公网反代页:新增统计区(可部署规则 / 已生效 / 健康通过 / 当前连接)与环境与 Nginx 卡(4 列);「使用说明」整卡改为页面级提示与入口行;部署设置弹窗对齐本地反向代理页规格(900px、三段式、页脚分隔线、开关行底色)并新增右侧「配置说明」栏
+- 公网反代页:协议列改为按部署设置的「启用 HTTPS」自动识别(规则 type 恒为 http,不能代表入口协议),HTTPS 绿色 / HTTP 灰色并带悬停说明
+- FRP 穿透页:4 张状态卡统一为标准统计区;公网服务端列表由自建网格改为 `n-data-table`(8 列,含启动/停止/查看/日志/测速/校验/Agent 管理/编辑/删除)
+- FRP 穿透页:6 个弹窗统一到同一外壳(`.frp-card-modal` / `.frp-detail-card`),运行日志、导出配置、frps 同源配置、FRP 应用配置的头部/滚动区/页脚一致
+- 统一设计令牌:清理硬编码色值(公网反代页 6 处、FRP 穿透页 13 处)与内联样式,删除死样式;统计区、弹窗、表格单元格全部走 `--fonu-*` 令牌与共享组件(`StatusBadge` / `EmptyState` / `LoadError` / `FonuCard`)
+- 表格与列表交互:数字列右对齐、操作按钮右对齐并按行禁用、长文本换行、单元格内容垂直居中;窄屏按 1200 / 900 / 760 断点降列
- FRP 公网反代:部署选项(WebSocket / 强制跳转 / IP 白名单 / Basic Auth 等)从穿透规则弹窗迁移到公网反代页「部署设置」,语义归位(这些是公网反代属性,与隧道无关)
- FRP 公网反代:隧道匹配优先按域名(`spec.http.customDomains`),比按代理名猜测可靠;同时兼容 `user` 前缀(`admin.<规则名>`)与裸规则名
- FRP 公网反代:frps 管理接口调用失败时把具体原因透出到前端悬停(请求 URL、状态码、响应体、密码读取 / 解密环节),不再笼统提示「接口不可用」
@@ -1139,6 +592,14 @@ All notable changes to this project will be documented in this file.
### 修复
+- 通知:单个通道密文解密失败不再让整份配置加载失败——原先 SMTP / Webhook / Telegram 任一密文损坏(例如更换过加密密钥)都会直接返回错误,导致其它已配置通道一并无法告警;现改为仅把该通道标记为「未配置」并继续解析其它通道(对齐上游 1.0.5)
+- 反向代理:监听占用冲突提示写明占用方——优先规则备注名,其次该规则首个域名,最后回落 `规则 #id`;原先只说「已被其他规则使用」,用户得自己翻列表找是哪一条
+- 反向代理:手动(custom)Nginx 配置的监听占用纳入校验。custom 配置的正本在文件系统(数据库只记 `nginx_mode` 开关),其 `listen` / `server_name` 原先完全不参与校验,与其它规则「同域名 + 同端口」的冲突只会在 Nginx reload 时静默失效(第一个 server 块生效、后面的被忽略);现于保存 / 回滚时解析并写入新表 `proxy_custom_endpoints`,与表单规则共用同一条判定(对齐上游 1.0.5)
+- 反向代理:并发下修改监听端口可能绕过保存前校验(`UPDATE proxy_hosts` 的唯一约束冲突此前直接抛出原始 SQL 错误),现改为按域名复检并给出带规则名的提示
+- FRP 穿透页「运行中」状态显示为灰色:`statusKind` 返回 `'success'` 却被当作 `value` 传给 `StatusBadge`(`utils/status.ts` 不识别该值,落到 `unknown`),且其中含非法的 `'default'`;现改为返回合法 `StatusKind` 并改用 `:kind`,服务端详情弹窗同类问题一并修复
+- 表格单元格样式整体失效:`n-data-table` 的 render 函数用 `h()` 创建的节点不带 scoped 属性,导致 `table-muted` / `type-chip` / `mono` 与各 `cell-*` 样式未生效(表现为「上游/回源」两行变一行、域名与按钮错位、徽标无间距);统一改用 `:deep()` 选择器
+- 公网反代页刷新失败静默:`loadErrorMsg` 此前写入但从未渲染,现改为顶部可见提示,并让单个接口失败不再中断整轮数据加载
+- 公网反代页切换服务端残留上一台数据、轮询在页面隐藏时仍请求、loading 粒度粗(任一操作全页按钮转圈)等问题修复
- FRP 公网反代:隧道状态误报 ✗——`RouteHealth` 静默忽略 dashboard 密码解密错误导致空密码 401,现逐环节记录并透出
- FRP 公网反代:隧道检测端点错配——旧代码请求 `/api/proxies`(405)或按类型子端点(404),现按 frps 版本自适应并解析 V2 的 `data.items`
- FRP 公网反代:流量恒为 0——V2 返回的键是 `admin.ssssss` 而代码只查裸规则名,现按「裸名 / user 前缀名 / 域名索引」三级匹配
@@ -1147,6 +608,12 @@ All notable changes to this project will be documented in this file.
### 变更
+- 新增 `docs/upstream-sync-plan.md`:上游 1.0.2 → 1.0.5 的逐版本对照方案,含 8 项待核对项的代码级核实结论、不照搬项及原因、以及「可增加项清单」(11 项,按价值排序)
+- 新增迁移 `024_proxy_custom_endpoints.sql`(手动 Nginx 配置的占用表,`UNIQUE(rule_id, hostname, listen_port)` + `ON DELETE CASCADE`),解析器 `internal/nginx/endpoints.go`(`ParseServerEndpoints`,保守解析:`include` 片段 / 正则 `server_name` / `listen unix:` 一律忽略,宁可漏报不误报)与 `internal/proxy/custom_endpoints.go`
+- 补测试:`internal/nginx/endpoints_test.go`(多 server 块、`[::]` 写法、通配与正则、无 `listen` 默认 80)、`internal/proxy/custom_endpoints_test.go`(走真实迁移建内存库:冲突含规则名、排除自身、同域名不同端口放行、custom 占用对表单规则可见、级联清理)
+- `docs/upstream-sync-plan.md` 补 §八「第 5 项详细整理」:Nginx 端口占用语义(一条规则占用哪些端口)、现有校验链逐跳、三处真实缺口与方案 A/B/C 对比、实施记录与未覆盖边界
+- 新增设计文档:`docs/agent-view-design-v2.md`(公网 Agent 页)、`docs/proxy-remote-design.md`(公网反代页),以及被取代留档的 `docs/agent-view-design.md`、`docs/agent-view-refactor-plan.md`
+- 新增页面 UI 标准 skill(工作区级 `.breezell/skills/fonu-page-ui`,不在仓库内):页面骨架、共享组件契约、设计令牌、类名约定、弹窗标准结构、脚本约定与验收清单
- FRP 公网反代管理从「公网 Agent」页拆分为独立页面(`/public-proxy`),侧栏新增「公网反代」
- 新增 agent 端点:`GET /api/v1/routes/{domain}/conf`、`PUT /api/v1/china-cidr`、`POST /api/v1/nginx/install`
- 新增 Fonu 接口:`GET /api/frpmulti/servers/{id}/agent/routes/health`、`GET /api/frpmulti/servers/{id}/agent/routes/{proxyID}/conf`、`POST /api/frpmulti/servers/{id}/agent/nginx/install`
diff --git a/README.md b/README.md
index 8e7dd7e..bab5b20 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
-
+
@@ -288,7 +288,7 @@ Havline 提供两种飞牛应用形态,均支持统一网关入口 `/app/havli
### Docker 应用(离线镜像包)
- 独立仓库:
-- 安装包:`havline-1.9.0.fpk`
+- 安装包:`havline-1.9.2.fpk`
- 包内包含 amd64 / arm64 离线镜像,安装时执行 `docker load`
- Compose 使用本地镜像并设置 `pull_policy: never`,不依赖外部 Registry
- 管理界面通过统一网关 `/app/havline` 访问
diff --git a/havline-agent.exe b/havline-agent.exe
deleted file mode 100644
index dfe085e88e49684e878774a0c596f22c63913173..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 10972160
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 Ei)>p#3$JUPT`(@ev@bV)7wUm8@%GIdX7N
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 |