Files
HomeDash/admin-server/schema.go
HomeDash Bot 92fd32e7a9 feat(api): 全量配置项暴露为接口 + 后台增加门户卡片编辑框
用户要求:所有可配置项都应当是 API,方便 agent 直接把站点配成用户喜欢的样子。

- 新增 GET /api/schema:一次返回
  · items:全部单值配置项(站点信息、登录页/门户页文案、备案号、页脚…),
    含当前值、类型、写入口与请求体示例;**当前为空的键也会列出**,否则 agent 发现不了
  · resources:结构化资源与其真正的写入口(导航、路由表+apply、门户卡片、壁纸、
    门户口令、历史、图标上传、重载),并在 hint 里写明"路由表改完必须 apply"这类约束
- 后台「站点信息」新增「门户页服务卡片」编辑框:载入 / 保存 / 清空即回内置,
  与 /api/portal-cards 同一份数据(界面能改的 agent 都能改)
- README 新增「配置接口(agent 可直接改)」:schema 用法、写入口对照表、可直接复制的示例
- 测试:schema 必须列出关键项与写入口;卡片接口写入生效、清空返回 reset 并回落内置
2026-09-12 10:19:22 +08:00

152 lines
6.5 KiB
Go
Raw 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", "公安备案号", "可选,显示于页脚"},
{"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,
})
}