From 96d8a4074f5bd512b5a0db5db2d30614c07c09e7 Mon Sep 17 00:00:00 2001 From: chiyi Date: Sat, 19 Sep 2026 15:39:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(docs):=20=E6=96=87=E6=A1=A3=E9=A1=B5?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20=E2=80=94=20=E7=B1=BB=E5=9E=8B=E6=A0=91/?= =?UTF-8?q?=E5=8F=AF=E7=BC=96=E8=BE=91=E8=AF=B4=E6=98=8E/=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=B1=BB=E5=9E=8B/=E5=88=86=E4=BA=AB=E6=B7=B1?= =?UTF-8?q?=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 单文件 docs UI 全面重写:卡片化参数表、语法高亮、方法深链 - 说明可编辑:方法级 + 字段级备注防抖自动保存到 fun-docs-edits.json (-edits 可改路径,GET /fun-docs-edits.json + POST /fun-docs-edits) - 类型列可展开树:枚举成员/嵌套 struct/slice 类型结构 - 返回类型区块:struct 出表、标量出类型标签,流式标注首条消息类型 - 标题旁分享按钮:#Svc.Method?req= 深链携带请求体,打开即还原 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .gitignore | 1 + README.md | 7 +- cmd/fun/docs.go | 61 ++++- cmd/fun/docsui/index.html | 509 +++++++++++++++++++++++++++++--------- 4 files changed, 457 insertions(+), 121 deletions(-) diff --git a/.gitignore b/.gitignore index e57407c..96d9e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ fun-cli *.test +/fun diff --git a/README.md b/README.md index a79c679..545d07f 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,15 @@ fun docs -upstream http://127.0.0.1:9000 # 指定后端地址 fun docs -no-run # 后端已在别处运行 ``` -- 浅色标准文档风格:侧栏服务/方法树(stream/void 徽标)+ 搜索 +- 浅色文档风格:侧栏服务/方法树(stream 徽标)+ 搜索(`/` 快速聚焦) +- **分享**:标题旁一键复制深链(`#服务.方法?req=...`),自带请求体,打开即还原 - 参数表:字段/类型/示例值/说明;枚举下拉、嵌套 DTO 分层、可空标注 +- **说明**:方法说明与字段备注可编辑,自动保存到 `fun-docs-edits.json` + (启动时自动创建,`-edits` 可改路径);返回类型递归展示 - **请求**:表单实时生成 JSON,可直接编辑后发送 - **State**:"添加字段"式键值编辑(存 localStorage),值注入请求 `state` 对象—— 键以后端 Guard 读取为准,前端只负责填值 -- **响应**:格式化 JSON;NDJSON 流式方法逐行实时追加 +- **响应**:格式化 JSON 语法高亮;NDJSON 流式方法逐行实时追加 - 文档站反向代理 `/cell` 到后端,同源无 CORS 问题 ## 元数据 JSON diff --git a/cmd/fun/docs.go b/cmd/fun/docs.go index 6e804de..8da747d 100644 --- a/cmd/fun/docs.go +++ b/cmd/fun/docs.go @@ -15,6 +15,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "sync" "syscall" ) @@ -28,6 +29,7 @@ func cmdDocs(args []string) { from := fs.String("from", "", "从元数据 JSON 文件读取") pkg := fs.String("p", ".", "业务 main 包路径(用于采集元数据与自动启动后端)") noStart := fs.Bool("no-run", false, "不自动启动后端(后端已在别处运行时使用)") + editsPath := fs.String("edits", "fun-docs-edits.json", "说明/备注编辑存档文件(启动时自动创建)") fs.Parse(args) var meta *FunMeta @@ -76,6 +78,8 @@ func cmdDocs(args []string) { } proxy := httputil.NewSingleHostReverseProxy(target) + loadOrCreateEdits(*editsPath) + metaJSON := renderMetaJSON(meta) mux := http.NewServeMux() @@ -88,6 +92,39 @@ func cmdDocs(args []string) { case "/fun-meta.json": w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Write(metaJSON) + case "/fun-docs-edits.json": + editsMu.RLock() + data, _ := json.Marshal(edits) + editsMu.RUnlock() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write(data) + case "/fun-docs-edits": + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + Key string `json:"key"` + Entry json.RawMessage `json:"entry"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Key == "" { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + editsMu.Lock() + if len(req.Entry) == 0 || string(req.Entry) == "null" { + delete(edits, req.Key) + } else { + edits[req.Key] = req.Entry + } + data, _ := json.MarshalIndent(edits, "", " ") + editsMu.Unlock() + if err := os.WriteFile(*editsPath, data, 0o644); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write([]byte(`{"ok":true}`)) default: proxy.ServeHTTP(w, r) } @@ -110,9 +147,10 @@ func cmdDocs(args []string) { ───────────────────────────────────────── 文档站: http://%s 后端代理: %s → %s + 说明存档: %s ───────────────────────────────────────── %d 个服务 / %d 个方法。Ctrl+C 退出(连同自动拉起的后端) -`, *addr, *upstream, *upstream, len(meta.Services), nMethods) +`, *addr, *upstream, *upstream, *editsPath, len(meta.Services), nMethods) if err := http.ListenAndServe(*addr, mux); err != nil { fmt.Fprintln(os.Stderr, "fun: 文档站启动失败:", err) @@ -121,6 +159,27 @@ func cmdDocs(args []string) { } } +// 说明/备注编辑存档:map[服务.方法] -> {note, fields:{路径:备注}} +var ( + editsMu sync.RWMutex + edits = map[string]json.RawMessage{} +) + +func loadOrCreateEdits(path string) { + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + os.WriteFile(path, []byte("{}\n"), 0o644) + return + } + fmt.Fprintln(os.Stderr, "fun: 读取编辑存档失败:", err) + return + } + if err := json.Unmarshal(data, &edits); err != nil { + fmt.Fprintln(os.Stderr, "fun: 编辑存档格式无效,按空处理:", err) + } +} + func renderMetaJSON(meta *FunMeta) []byte { data, err := json.Marshal(meta) if err != nil { diff --git a/cmd/fun/docsui/index.html b/cmd/fun/docsui/index.html index 55f9fe5..88f1d7a 100644 --- a/cmd/fun/docsui/index.html +++ b/cmd/fun/docsui/index.html @@ -6,122 +6,278 @@ fun docs
- 加载中… + +
-
在左侧选择一个方法查看文档
+
+
+

在左侧选择一个方法查看文档

+

按/快速搜索

+