Files
HomeDash/admin-server/gateway_nav.go
HomeDash Bot da7a56e7ec fix(status): 修掉 Dashy 可达性检查"全黄"——改用免认证探测路径
用户反馈可达性全黄。实测:appConfig.statusCheck 已开启,但等 6 秒仍无状态
节点渲染;上游直连全部 200。机制:Dashy 用 no-cors 探测条目 URL,而受保护域名
会 302 跳到另一个域(登录页),no-cors 下跨域重定向直接报错 → 检查永远停在
"进行中" → 全黄。

修法:
- gateway 为每条路由生成 location = /.navi-ping:auth_request off + 反代到
  真实上游(上游挂了返回 5xx → 红;正常 2xx → 绿),portal 类型直接 204
- 导航同步时设置 NavItem.StatusCheck = true 与 statusCheckUrl 指向该路径
- 注意:statusCheck 是 NavItem 的结构体字段,不能塞进 inline Extra(yaml 会
  panic: conflicts with struct field)——已改为设置字段本身

测试:新增断言——探测路径存在、免认证、反代到真实上游、portal 返回 204。
2026-09-12 08:23:33 +08:00

146 lines
3.8 KiB
Go

package main
import (
"bytes"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
const (
gatewayManagedKey = "naviGateway"
gatewayHostKey = "naviGatewayHost"
gatewayAutoIconKey = "naviGatewayAutoIcon"
)
func normalizedHost(raw string) string {
u, err := url.Parse(raw)
if err != nil {
return ""
}
return strings.ToLower(u.Hostname())
}
func navURLForRoute(r Route) string {
if r.NavURL != "" {
return strings.TrimSpace(r.NavURL)
}
return "https://" + strings.TrimSpace(r.Host)
}
// validateGatewayNavigation ensures every selected Dashy group exists before either
// gateway.yml or conf.yml is changed.
func validateGatewayNavigation(gw *GatewayConfig, dashy *Config) error {
sections := make(map[string]bool, len(dashy.Sections))
for _, s := range dashy.Sections {
sections[strings.TrimSpace(s.Name)] = true
}
for _, r := range gw.Routes {
if !r.NavEnabled {
continue
}
if !sections[strings.TrimSpace(r.NavSection)] {
return fmt.Errorf("%s: 导航分组 %q 不存在", r.Host, r.NavSection)
}
}
return nil
}
// syncGatewayNavigation makes gateway.yml the source of truth for proxy-managed
// Dashy items. Manually-created items that are unrelated to a managed host remain
// untouched. Existing entries for a newly-managed host are adopted instead of
// duplicated.
func syncGatewayNavigation(gw *GatewayConfig) error {
dashy, err := loadConfig()
if err != nil {
return err
}
if err := validateGatewayNavigation(gw, dashy); err != nil {
return err
}
managedHosts := make(map[string]bool)
for _, r := range gw.Routes {
if r.NavEnabled {
managedHosts[strings.ToLower(strings.TrimSpace(r.Host))] = true
}
}
// Remove stale proxy-managed items and adopt existing items for hosts that are
// now managed by the gateway. The system-owned history group is never touched.
for si := range dashy.Sections {
if isHistorySection(dashy.Sections[si]) {
continue
}
kept := dashy.Sections[si].Items[:0]
for _, item := range dashy.Sections[si].Items {
itemHost := normalizedHost(item.URL)
wasManaged, _ := item.Extra[gatewayManagedKey].(bool)
if wasManaged || managedHosts[itemHost] {
continue
}
kept = append(kept, item)
}
dashy.Sections[si].Items = kept
}
for _, r := range gw.Routes {
if !r.NavEnabled {
continue
}
idx := -1
for i := range dashy.Sections {
if strings.TrimSpace(dashy.Sections[i].Name) == strings.TrimSpace(r.NavSection) {
idx = i
break
}
}
if idx < 0 {
return fmt.Errorf("%s: 导航分组 %q 不存在", r.Host, r.NavSection)
}
icon := strings.TrimSpace(r.NavIcon)
if icon == "" {
icon = "fas fa-globe"
}
statusOn := true
item := NavItem{
StatusCheck: &statusOn,
Title: strings.TrimSpace(r.NavName),
Description: strings.TrimSpace(r.NavDescription),
Icon: icon,
URL: navURLForRoute(r),
Extra: map[string]any{
gatewayManagedKey: true,
gatewayHostKey: strings.ToLower(strings.TrimSpace(r.Host)),
gatewayAutoIconKey: r.NavAutoIcon,
// 可达性检查指向网关生成的免认证路径:避免命中登录 302 导致
// no-cors 探测报错、状态永远"进行中"(用户反馈的"全黄")。
"statusCheckUrl": "https://" + strings.TrimSpace(r.Host) + "/.navi-ping",
},
}
dashy.Sections[idx].Items = append(dashy.Sections[idx].Items, item)
}
if err := saveConfig(dashy); err != nil {
return err
}
triggerHistoryRebuild()
return nil
}
func triggerHistoryRebuild() {
req, err := http.NewRequest(http.MethodPost, historyBaseURL+"/rebuild", bytes.NewReader(nil))
if err != nil {
return
}
if adminToken != "" {
req.Header.Set("Authorization", "Bearer "+adminToken)
}
client := &http.Client{Timeout: 3 * time.Second}
res, err := client.Do(req)
if err == nil {
_ = res.Body.Close()
}
}