Files
HomeDash/admin-server/schema.go
HomeDash Bot 3d18fb59ce fix(ui): 登录页页脚被当纯文本显示(html/template 转义)+ 三处页脚统一
用户发现登录页出现 `<div class="navi-footer">…` 这串标记文字。

① 根因:auth.go 把 beianFooter() 的字符串塞进 Go html/template(map[string]string),
   模板自动转义整段 HTML → 页面上直接显示标记源码。
   修法:改用 map[string]any + template.HTML(beianFooter 内部已对备案号做 html.EscapeString,
   注入合法)。
② 登录页与门户页此前**都无视**后台的「页脚」自定义字段(后台提示语写的是"填写后以这里为准")。
   新增统一入口 siteFooter(info):自定义 footer 优先,否则按 icp/gongan/座右铭生成;
   pages.go(门户页)与 auth.go(登录页)都改走它。
③ 页脚座右铭此前只由 navi.js 在 Dashy 页客户端注入 → 登录页/门户页永远没有。
   新增 pageInfo 字段 motto(换行或 | 分行,留空用内置默认),
   footerMarkup() 生成时一并输出 .navi-quote 两行;navi.js 已有幂等判断不会重复注入。
   后台「站点信息」新增座右铭输入框,并登记进 /api/schema。
④ 登录页 / 门户页补页脚样式:左列两行备案号、右列两行座右铭;窄屏退化为居中单列。
2026-09-13 07:56:38 +08:00

153 lines
6.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"net/http"
"sort"
)
// schema.go —— 可配置项总目录(给 agent 用)
//
// 目的:让 agent/脚本不必猜就有据可依——把所有可配置项连同**怎么改**(方法、路径、
// 请求体字段)一次性暴露出来。人用后台界面,agent 用这里的接口,改的是同一份数据。
//
// GET /api/schema → {"note":…,"items":[…],"resources":[…]}
//
// items : 单值配置项(站点信息、认证页/门户页文案、备案号…),读用 GET、写用 POST。
// resources : 结构化资源(导航分组与条目、服务转发路由表、门户卡片、壁纸、门户口令、历史…)。
// pageInfoFields 描述 conf.yml → pageInfo 下所有可配置文案字段。
// 列全"即使当前为空"的字段,agent 才能发现它们的存在。
var pageInfoFields = []struct{ Key, Label, Hint string }{
{"title", "站点标题", "浏览器标题与导航页标题"},
{"description", "站点描述", "导航页副标题/搜索页描述"},
{"authTitle", "登录页标题", "留空回落内置默认"},
{"authSubtitle", "登录页副标题", "留空回落内置默认"},
{"authLogo", "登录页徽标", "短文字,如 JF"},
{"portalTitle", "门户页浏览器标题", "留空回落内置默认"},
{"heroTitle", "门户页主标题", "留空回落内置默认"},
{"heroSubtitle", "门户页副标题", "留空回落内置默认"},
{"icp", "ICP 备案号", "显示于各页页脚;被代理页面也会注入"},
{"gongan", "公安备案号", "可选,显示于页脚"},
{"motto", "页脚座右铭", "每行一句(换行或 | 分隔);留空用内置默认"},
{"footer", "自定义页脚 HTML", "高级:整段替换页脚内容(登录页/门户页同样生效)"},
{"navLinks", "页脚导航链接数组", "数组,元素形如 {name,url}"},
}
type schemaItem struct {
Key string `json:"key"`
Label string `json:"label"`
Type string `json:"type"`
Value any `json:"value"`
Hint string `json:"hint,omitempty"`
Write struct {
Method string `json:"method"`
Path string `json:"path"`
Body any `json:"bodyExample"`
} `json:"write"`
}
type schemaResource struct {
ID string `json:"id"`
Label string `json:"label"`
Read struct {
Method string `json:"method"`
Path string `json:"path"`
} `json:"read"`
Write struct {
Method string `json:"method"`
Path string `json:"path"`
Body any `json:"bodyExample,omitempty"`
} `json:"write"`
Hint string `json:"hint,omitempty"`
}
func newSchemaItem(key, label, typ string, value any, hint, path string, body any) schemaItem {
it := schemaItem{Key: key, Label: label, Type: typ, Value: value, Hint: hint}
it.Write.Method = http.MethodPost
it.Write.Path = path
it.Write.Body = body
return it
}
// handleSchema 返回全部可配置项及其写入口,供 agent 直接按图配置。
func handleSchema(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
jsonErr(w, 405, "只支持 GET")
return
}
info := map[string]any{}
if cfg, err := loadConfig(); err == nil && cfg != nil && cfg.PageInfo != nil {
info = cfg.PageInfo
}
items := make([]schemaItem, 0, len(pageInfoFields))
for _, f := range pageInfoFields {
typ := "string"
if f.Key == "navLinks" {
typ = "array"
}
items = append(items, newSchemaItem(
"pageInfo."+f.Key, f.Label, typ, info[f.Key], f.Hint,
"/api/pageinfo", map[string]any{f.Key: "<新值>"},
))
}
// 实际存在于配置但未在 catalog 里登记的键也一并暴露(避免"改了看不到")
var extra []string
for k := range info {
known := false
for _, f := range pageInfoFields {
if f.Key == k {
known = true
break
}
}
if !known {
extra = append(extra, k)
}
}
sort.Strings(extra)
for _, k := range extra {
items = append(items, newSchemaItem("pageInfo."+k, "自定义键 "+k, "any", info[k], "配置里已存在的额外键",
"/api/pageinfo", map[string]any{k: "<新值>"}))
}
res := func(id, label, readPath, writePath string, body any, hint string) schemaResource {
var sr schemaResource
sr.ID, sr.Label, sr.Hint = id, label, hint
sr.Read.Method, sr.Read.Path = http.MethodGet, readPath
sr.Write.Method, sr.Write.Path = http.MethodPost, writePath
sr.Write.Body = body
return sr
}
resources := []schemaResource{
res("navSections", "导航分组与条目", "/api/sections", "/api/item",
map[string]any{"name": "服务名", "url": "https://…", "section": "常用服务", "icon": "auto", "description": "说明"},
"分组:/api/section(POST 新建、PUT 改名)、/api/section/update、DELETE /api/section;条目:/api/item(POST 新建)、/api/item/delete"),
res("gatewayRoutes", "服务转发路由表", "/api/gateway/routes", "/api/gateway/routes",
map[string]any{"host": "app.example.com", "upstream": "http://127.0.0.1:3000", "requireLogin": true, "websocket": false, "maxBodyMB": 64, "kind": "proxy", "navEnabled": true, "navSection": "常用服务", "navName": "我的应用"},
"写完后必须 POST /api/gateway/apply 才会生效(内部会 nginx -t 校验,失败保留旧配置)。导入现有 nginx 配置:POST /api/gateway/import"),
res("portalCards", "门户页服务卡片 HTML", "/api/portal-cards", "/api/portal-cards",
map[string]any{"html": "<a class=\"card\" href=\"https://example.com\">示例</a>"},
"替换门户页 <!--NAVI_CARDS_START/END--> 之间的内容;空串=恢复内置卡片。整页替换可写数据卷 portal/index.html"),
res("wallpaper", "导航页壁纸", "/api/wallpaper", "/api/wallpaper",
map[string]any{"url": "https://…/wall.jpg"},
"置空字符串表示移除壁纸;深浅色模式下都会保留壁纸"),
res("portalAuth", "统一登录账号", "/api/portal-auth", "/api/portal-auth",
map[string]any{"username": "admin", "password": "<新口令>"},
"口令以加盐哈希存数据卷,改完旧会话失效"),
res("history", "浏览器历史记录", "/api/history", "/api/history/item",
nil,
"删除单条:DELETE /api/history/item?url=…;清空:DELETE /api/history;同步接口 /api/history/sync 用 NAVI_SYNC_TOKEN"),
res("reload", "重载导航配置", "/api/sync-info", "/api/reload", nil,
"改完 conf.yml 或需要重建导航分区时调用"),
res("icon", "上传图标", "/api/schema", "/api/icon", nil,
"multipart/form-data,字段名 file;支持 png/svg/jpg/ico"),
}
jsonResp(w, map[string]any{
"note": "所有可配置项集中于此。读用 GET;写用 POST,需 Authorization: Bearer <NAVI_ADMIN_TOKEN>(后台页面已登录则可直接用界面)。结构化资源的写入口见 resources。",
"items": items,
"resources": resources,
})
}