fix(agent): 探测阶段自愈 nginx 版本目录并发布 v1.4.5
CI / verify (push) Canceled after 0s

This commit is contained in:
2026-09-28 14:43:59 +08:00
parent c92d06642f
commit 87c072f5d4
14 changed files with 104 additions and 20 deletions
+16
View File
@@ -2,6 +2,22 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [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 ## [v1.4.4] - 2026-09-28
### 修复 ### 修复
+1 -1
View File
@@ -1 +1 @@
1.4.4 1.4.5
+1 -1
View File
@@ -1,7 +1,7 @@
# Fonu 文档索引 # Fonu 文档索引
> 维护约定:新增方案 / 审查文档时在下方登记;方案落地后在「状态」列标注实施结果,并给已被推翻的描述加时效说明。 > 维护约定:新增方案 / 审查文档时在下方登记;方案落地后在「状态」列标注实施结果,并给已被推翻的描述加时效说明。
> 最近更新:2026-09-28(v1.4.4) > 最近更新:2026-09-28(v1.4.5)
## 方案与设计(本目录) ## 方案与设计(本目录)
+2 -2
View File
@@ -2,10 +2,10 @@
> 方案编号:P0-5 > 方案编号:P0-5
> 制定日期:2026-09-28 > 制定日期:2026-09-28
> 当前 AgentVersion:0.13.1 > 当前 AgentVersion:0.13.2
> 目标:默认不暴露 Agent 管理接口,安装和升级过程自动建立安全连接,不要求用户手工配置端口、隧道或证书 > 目标:默认不暴露 Agent 管理接口,安装和升级过程自动建立安全连接,不要求用户手工配置端口、隧道或证书
> 实施状态(2026-09-28):阶段 1-4 已随 v1.4.0 实施;0.13.1 修复 nginx 版本目录被 `include *` 误加载的问题;仅剩 2.0.0 的旧 HTTP 兼容清理和可选主机防火墙辅助。 > 实施状态(2026-09-28):阶段 1-4 已随 v1.4.0 实施;0.13.1 / 0.13.2 修复 nginx 版本目录被 `include *` 误加载及探测阶段无法自愈的问题;仅剩 2.0.0 的旧 HTTP 兼容清理和可选主机防火墙辅助。
--- ---
+4 -4
View File
@@ -238,16 +238,16 @@ FONU_VERSION=<旧版本> docker compose up -d --no-deps --force-recreate fonu
## 10. 在线升级验证 ## 10. 在线升级验证
示例页面用于验证升级结果。v1.4.4 的验证流程: 示例页面用于验证升级结果。v1.4.5 的验证流程:
1. 确认当前线上实例为 v1.3.0,且 `fonu-updater` sidecar 已运行。 1. 确认当前线上实例为 v1.3.0,且 `fonu-updater` sidecar 已运行。
2. 确认远端 `main` 分支的 `VERSION` 为 `1.4.4`。 2. 确认远端 `main` 分支的 `VERSION` 为 `1.4.5`。
3. 在侧栏看到「一键升级」后点击并确认。 3. 在侧栏看到「一键升级」后点击并确认。
4. 等待升级弹窗显示成功,页面自动刷新。 4. 等待升级弹窗显示成功,页面自动刷新。
5. 打开侧栏「示例页面」,确认: 5. 打开侧栏「示例页面」,确认:
- 页面路由为 `/example`; - 页面路由为 `/example`;
- 页面显示的版本为 `1.4.4`; - 页面显示的版本为 `1.4.5`;
- 页面显示验证标识 `v1.4.4-example-page`; - 页面显示验证标识 `v1.4.5-example-page`;
- 页面显示独立的「本次升级验证」区块; - 页面显示独立的「本次升级验证」区块;
- 「升级验证」显示为「通过」。 - 「升级验证」显示为「通过」。
+36 -1
View File
@@ -54,7 +54,9 @@ import (
// 0.13.0:默认只监听回环地址,非回环监听必须显式允许;状态接口暴露监听范围与传输安全。 // 0.13.0:默认只监听回环地址,非回环监听必须显式允许;状态接口暴露监听范围与传输安全。
// 0.13.1:nginx 版本备份目录改为隐藏目录(.<name>.versions)并自动迁移旧目录, // 0.13.1:nginx 版本备份目录改为隐藏目录(.<name>.versions)并自动迁移旧目录,
// 修掉旧 <name>.versions 目录被 include * 当配置文件读取、nginx -t 报 pread() "Is a directory" 的问题。 // 修掉旧 <name>.versions 目录被 include * 当配置文件读取、nginx -t 报 pread() "Is a directory" 的问题。
const AgentVersion = "0.13.1" // 0.13.2:nginx -T 探测失败时先从报错文本解析旧 <name>.versions 目录并迁移后重试,
// 修掉「配置目录还没探测出来、迁移逻辑没机会执行」导致 HTTPS 直连持续失败的问题。
const AgentVersion = "0.13.2"
// Config 是 fonu-agent 的运行配置。agent 只在 VPS 本地管理自己的 Nginx 片段、证书和 frps 配置。 // Config 是 fonu-agent 的运行配置。agent 只在 VPS 本地管理自己的 Nginx 片段、证书和 frps 配置。
type Config struct { type Config struct {
@@ -728,7 +730,14 @@ func detectNginxConfDir(binary string) (string, error) {
if nginx == "" { if nginx == "" {
return "", fmt.Errorf("未找到可用的 nginx 可执行文件;请先在 VPS 安装 Nginx(apt install nginx / yum install nginx),或用 FONU_AGENT_NGINX_BIN 显式指定路径") return "", fmt.Errorf("未找到可用的 nginx 可执行文件;请先在 VPS 安装 Nginx(apt install nginx / yum install nginx),或用 FONU_AGENT_NGINX_BIN 显式指定路径")
} }
// 旧版 Agent 可能把 <配置>.versions 目录留在 include 范围内,先按常见目录清理,避免 nginx -T 直接失败。
for _, dir := range []string{"/etc/nginx/sites-enabled", "/etc/nginx/conf.d", "/etc/nginx"} {
migrateLegacyVersionDirs(dir)
}
out, err := exec.Command(nginx, "-T").CombinedOutput() out, err := exec.Command(nginx, "-T").CombinedOutput()
if err != nil && migrateVersionDirsFromNginxOutput(string(out)) {
out, err = exec.Command(nginx, "-T").CombinedOutput()
}
if err != nil { if err != nil {
return "", fmt.Errorf("nginx -T 执行失败: %w", err) return "", fmt.Errorf("nginx -T 执行失败: %w", err)
} }
@@ -1302,6 +1311,32 @@ func migrateLegacyVersionDirs(dir string) {
} }
} }
// migrateVersionDirsFromNginxOutput 从 nginx 报错文本里解析被误当配置文件读取的 .versions 目录并迁移。
// nginx -T 在探测目录前就失败时,这是拿到具体路径的唯一线索。
func migrateVersionDirsFromNginxOutput(output string) bool {
changed := false
for _, line := range strings.Split(output, "\n") {
idx := strings.Index(line, ".versions")
if idx < 0 {
continue
}
start := strings.LastIndex(line[:idx], "\"")
if start < 0 {
start = strings.LastIndexAny(line[:idx], " '")
}
path := line[start+1 : idx+len(".versions")]
if !filepath.IsAbs(path) {
continue
}
base := strings.TrimSuffix(path, ".versions")
if err := fsutil.MigrateLegacyVersions(base); err != nil {
continue
}
changed = true
}
return changed
}
func (s *Server) reloadNginx() error { func (s *Server) reloadNginx() error {
nginx := s.nginxPath() nginx := s.nginxPath()
if nginx == "" { if nginx == "" {
+15
View File
@@ -28,3 +28,18 @@ func TestMigrateLegacyVersionDirs(t *testing.T) {
t.Fatalf("隐藏版本目录应保留 1 个版本:%v %v", entries, err) t.Fatalf("隐藏版本目录应保留 1 个版本:%v %v", entries, err)
} }
} }
func TestMigrateVersionDirsFromNginxOutput(t *testing.T) {
dir := t.TempDir()
legacy := filepath.Join(dir, "fonuagent-https.conf.versions")
if err := os.MkdirAll(legacy, 0o700); err != nil {
t.Fatal(err)
}
output := "2026/09/28 14:38:16 [crit] 6053#6053: pread() \"" + legacy + "\" failed (21: Is a directory)"
if !migrateVersionDirsFromNginxOutput(output) {
t.Fatal("应从 nginx 报错文本中识别并迁移旧版本目录")
}
if _, err := os.Stat(legacy); !os.IsNotExist(err) {
t.Fatalf("旧版本目录应被移出 include 范围:%v", err)
}
}
+9
View File
@@ -130,6 +130,15 @@ mkdir -p "$CONF_DIR" /var/lib/fonu-agent/certs
# 先停服务,再原子替换;安装后必须确认目标存在、可执行且能输出版本 # 先停服务,再原子替换;安装后必须确认目标存在、可执行且能输出版本
systemctl stop fonu-agent 2>/dev/null || true systemctl stop fonu-agent 2>/dev/null || true
TMP_PREFIX="${PREFIX}.new" TMP_PREFIX="${PREFIX}.new"
# 迁移旧版 Agent 留在 nginx include 范围内的 <配置>.versions 目录(保留历史版本内容)
for NGINX_DIR in /etc/nginx/sites-enabled /etc/nginx/conf.d; do
[ -d "$NGINX_DIR" ] || continue
for LEGACY in "$NGINX_DIR"/*.versions; do
[ -d "$LEGACY" ] || continue
TARGET="$NGINX_DIR/.$(basename "$LEGACY")"
[ -e "$TARGET" ] || mv "$LEGACY" "$TARGET" || true
done
done
install -m 0755 "$AGENT_BINARY" "$TMP_PREFIX" install -m 0755 "$AGENT_BINARY" "$TMP_PREFIX"
if [ ! -x "$TMP_PREFIX" ]; then if [ ! -x "$TMP_PREFIX" ]; then
echo "agent 二进制安装失败:$TMP_PREFIX 不存在或不可执行" >&2 echo "agent 二进制安装失败:$TMP_PREFIX 不存在或不可执行" >&2
Binary file not shown.
Binary file not shown.
+9
View File
@@ -14,6 +14,15 @@ UNIT=/etc/systemd/system/fonu-agent.service
mkdir -p "$CONF_DIR" /var/lib/fonu-agent/certs mkdir -p "$CONF_DIR" /var/lib/fonu-agent/certs
systemctl stop fonu-agent 2>/dev/null || true systemctl stop fonu-agent 2>/dev/null || true
TMP_PREFIX="${PREFIX}.new" TMP_PREFIX="${PREFIX}.new"
# 迁移旧版 Agent 留在 nginx include 范围内的 <配置>.versions 目录(保留历史版本内容)
for NGINX_DIR in /etc/nginx/sites-enabled /etc/nginx/conf.d; do
[ -d "$NGINX_DIR" ] || continue
for LEGACY in "$NGINX_DIR"/*.versions; do
[ -d "$LEGACY" ] || continue
TARGET="$NGINX_DIR/.$(basename "$LEGACY")"
[ -e "$TARGET" ] || mv "$LEGACY" "$TARGET" || true
done
done
install -m 0755 "$AGENT_BINARY" "$TMP_PREFIX" install -m 0755 "$AGENT_BINARY" "$TMP_PREFIX"
if [ ! -x "$TMP_PREFIX" ]; then if [ ! -x "$TMP_PREFIX" ]; then
echo "agent 二进制安装失败:$TMP_PREFIX 不存在或不可执行" >&2 echo "agent 二进制安装失败:$TMP_PREFIX 不存在或不可执行" >&2
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "fonu-web", "name": "fonu-web",
"version": "1.4.4", "version": "1.4.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "fonu-web", "name": "fonu-web",
"version": "1.4.4", "version": "1.4.5",
"dependencies": { "dependencies": {
"@codemirror/commands": "^6.11.1", "@codemirror/commands": "^6.11.1",
"@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-javascript": "^6.2.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "fonu-web", "name": "fonu-web",
"private": true, "private": true,
"version": "1.4.4", "version": "1.4.5",
"packageManager": "npm@10.9.4", "packageManager": "npm@10.9.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
+8 -8
View File
@@ -8,7 +8,7 @@
</template> </template>
</PageHeader> </PageHeader>
<n-alert type="success" :bordered="false" class="example-alert" title="v1.4.4 示例页面验证标识已生效"> <n-alert type="success" :bordered="false" class="example-alert" title="v1.4.5 示例页面验证标识已生效">
当前页面由 v{{ version }} 提供,验证标识为 <span class="mono">{{ verificationMarker }}</span>。 当前页面由 v{{ version }} 提供,验证标识为 <span class="mono">{{ verificationMarker }}</span>。
如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。 如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。
</n-alert> </n-alert>
@@ -19,7 +19,7 @@
<strong class="example-proof__value">{{ verificationRound }}</strong> <strong class="example-proof__value">{{ verificationRound }}</strong>
</div> </div>
<div class="example-proof__hint"> <div class="example-proof__hint">
看到这个新区块,说明前端资源已经从旧版本切换到 v1.4.4。 看到这个新区块,说明前端资源已经从旧版本切换到 v1.4.5。
</div> </div>
</div> </div>
@@ -33,16 +33,16 @@
<StatCard label="本次验证标识" :value="verificationMarker" sub="用于区分升级前后的前端资源" tone="blue" value-small> <StatCard label="本次验证标识" :value="verificationMarker" sub="用于区分升级前后的前端资源" tone="blue" value-small>
<template #icon><n-icon :component="FlashOutline" /></template> <template #icon><n-icon :component="FlashOutline" /></template>
</StatCard> </StatCard>
<StatCard label="页面状态" value="已更新" sub="v1.4.4 前端资源已加载" tone="teal"> <StatCard label="页面状态" value="已更新" sub="v1.4.5 前端资源已加载" tone="teal">
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template> <template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
</StatCard> </StatCard>
</div> </div>
<FonuCard title="本次示例页变化" subtitle="用于确认升级后的页面内容确实发生变化"> <FonuCard title="本次示例页变化" subtitle="用于确认升级后的页面内容确实发生变化">
<ul class="example-changes"> <ul class="example-changes">
<li>页面版本提示更新为 v1.4.4。</li> <li>页面版本提示更新为 v1.4.5。</li>
<li>「本次升级验证」区块和页面状态同步更新为 v1.4.4。</li> <li>「本次升级验证」区块和页面状态同步更新为 v1.4.5。</li>
<li>验证标识更新为 <span class="mono">v1.4.4-example-page</span>。</li> <li>验证标识更新为 <span class="mono">v1.4.5-example-page</span>。</li>
</ul> </ul>
</FonuCard> </FonuCard>
@@ -87,8 +87,8 @@ const message = useMessage()
const version = ref(__APP_VERSION__) const version = ref(__APP_VERSION__)
const checkedAt = ref('') const checkedAt = ref('')
const refreshing = ref(false) const refreshing = ref(false)
const verificationRound = 'v1.4.4' const verificationRound = 'v1.4.5'
const verificationMarker = 'v1.4.4-example-page' const verificationMarker = 'v1.4.5-example-page'
async function refreshVersion() { async function refreshVersion() {
refreshing.value = true refreshing.value = true