# Generate handlers.go based on the known API routes handlers = r'''package main import ( "encoding/json" "fmt" "io" "mime" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "write-server/internal/ai" "write-server/internal/artalk" "write-server/internal/config" "write-server/internal/images" "write-server/internal/posts" "write-server/internal/recycle" "write-server/internal/rssapi" "write-server/internal/wechat" ) const adminHTML = ` + "`" + ` WriteApp

WriteApp

Blog CMS + Telegram Bot — Go Edition

All Systems Running
Port 8016 · LAN: %s
Open EditorAPI Stats
` + "`" + ` func jsonOK(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) } func jsonErr(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]string{"error": msg}) } func readJSON(r *http.Request) (map[string]interface{}, error) { body, _ := io.ReadAll(r.Body) defer r.Body.Close() var data map[string]interface{} err := json.Unmarshal(body, &data) return data, err } func gs(m map[string]interface{}, key string) string { if v, ok := m[key]; ok { if s, ok := v.(string); ok { return s } } return "" } func gb(m map[string]interface{}, key string) bool { if v, ok := m[key]; ok { if b, ok := v.(bool); ok { return b } } return false } func gsa(m map[string]interface{}, key string) []string { if v, ok := m[key]; ok { switch arr := v.(type) { case []interface{}: var result []string for _, item := range arr { if s, ok := item.(string); ok { result = append(result, s) } } return result case []string: return arr } } return nil } func handlePosts(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": search := r.URL.Query().Get("search") page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } perPage, _ := strconv.Atoi(r.URL.Query().Get("perPage")) if perPage < 1 { perPage = 20 } includeDrafts := r.URL.Query().Get("includeDrafts") != "false" recycle.CleanExpired() list, total, totalPages := posts.ListPosts(search, page, perPage, includeDrafts) totalCount := posts.GetPostCount() jsonOK(w, map[string]interface{}{ "posts": list, "total": total, "totalPages": totalPages, "totalCount": totalCount, }) case "POST": body, _ := readJSON(r) fmRaw, _ := body["frontMatter"].(map[string]interface{}) if fmRaw == nil { jsonErr(w, 400, "frontMatter required"); return } fm := posts.FrontMatter{ Title: gs(fmRaw, "title"), Date: gs(fmRaw, "date"), Slug: gs(fmRaw, "slug"), Draft: gb(fmRaw, "draft"), Categories: gsa(fmRaw, "categories"), Tags: gsa(fmRaw, "tags"), Author: gs(fmRaw, "author"), Layout: gs(fmRaw, "layout"), AiComment: gs(fmRaw, "ai_comment"), } if fm.Layout == "" { fm.Layout = "post" } if fm.Title == "" || fm.Slug == "" { jsonErr(w, 400, "Title and slug required"); return } dirPath, err := posts.CreatePost(fm, gs(body, "content")) if err != nil { jsonErr(w, 500, err.Error()); return } jsonOK(w, map[string]interface{}{"success": true, "dirPath": dirPath}) default: jsonErr(w, 405, "method not allowed") } } func handlePostBySlug(w http.ResponseWriter, r *http.Request) { slug := extractSlug(r.URL.Path, "/api/posts/") switch r.Method { case "GET": p := posts.GetPost(slug) if p == nil { jsonErr(w, 404, "Post not found"); return } jsonOK(w, p) case "PUT": body, _ := readJSON(r) fmRaw, _ := body["frontMatter"].(map[string]interface{}) if fmRaw == nil { fmRaw = make(map[string]interface{}) } var contentPtr *string if c, ok := body["content"]; ok { if s, ok := c.(string); ok { contentPtr = &s } } result, err := posts.UpdatePost(slug, fmRaw, contentPtr) if err != nil { jsonErr(w, 404, err.Error()); return } jsonOK(w, map[string]interface{}{"success": true, "dirPath": result.DirPath}) case "DELETE": p := posts.GetPost(slug) if p == nil { jsonErr(w, 404, "Post not found"); return } if err := recycle.MoveToRecycle(slug, p.DirPath, p.FrontMatter.Title); err != nil { jsonErr(w, 500, err.Error()); return } jsonOK(w, map[string]interface{}{"success": true}) default: jsonErr(w, 405, "method not allowed") } } func handlePostsBatch(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" { jsonErr(w, 405, "method not allowed"); return } body, _ := readJSON(r) slugsArr, _ := body["slugs"].([]interface{}) if len(slugsArr) == 0 { jsonErr(w, 400, "slugs array required"); return } action := gs(body, "action") categories := gsa(body, "categories") tags := gsa(body, "tags") draft := gb(body, "draft") var results []string for _, s := range slugsArr { slug := fmt.Sprintf("%v", s) if posts.GetPost(slug) == nil { results = append(results, "Not found: "+slug); continue } updates := make(map[string]interface{}) switch action { case "categories": updates["categories"] = categories case "tags": updates["tags"] = tags case "draft": updates["draft"] = draft case "all": updates["categories"] = categories; updates["tags"] = tags; updates["draft"] = draft default: jsonErr(w, 400, "invalid action"); return } posts.UpdatePost(slug, updates, nil) results = append(results, "Updated: "+slug) } jsonOK(w, map[string]interface{}{"results": results}) } func handleRecycle(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": jsonOK(w, map[string]interface{}{"items": recycle.ListRecycle()}) case "POST": body, _ := readJSON(r) slug := gs(body, "slug") if slug == "" { jsonErr(w, 400, "slug required"); return } if recycle.Restore(slug) { jsonOK(w, map[string]interface{}{"success": true}) } else { jsonErr(w, 404, "not found") } case "DELETE": body, _ := readJSON(r) slug := gs(body, "slug") if slug == "" { jsonErr(w, 400, "slug required"); return } if recycle.PermanentDelete(slug) { jsonOK(w, map[string]interface{}{"success": true}) } else { jsonErr(w, 404, "not found") } default: jsonErr(w, 405, "method not allowed") } } func handleAI(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } body, _ := readJSON(r) prompt := gs(body, "prompt") if prompt == "" { jsonErr(w, 400, "prompt required"); return } resp, err := ai.Chat(prompt) if err != nil { jsonErr(w, 500, err.Error()); return } jsonOK(w, map[string]interface{}{"response": resp}) } func handleImages(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": jsonOK(w, map[string]interface{}{"images": images.ScanImages()}) case "DELETE": body, _ := readJSON(r) filename := gs(body, "filename") if filename == "" { jsonErr(w, 400, "filename required"); return } if images.DeleteImage(filename) { jsonOK(w, map[string]interface{}{"success": true}) } else { jsonErr(w, 404, "not found") } default: jsonErr(w, 405, "method not allowed") } } func handleUpload(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } r.ParseMultipartForm(32 << 20) file, header, err := r.FormFile("file") if err != nil { jsonErr(w, 400, "file required"); return } defer file.Close() ext := filepath.Ext(header.Filename) filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext) destPath := filepath.Join(config.BLOG_ROOT, "static", "uploads", filename) os.MkdirAll(filepath.Dir(destPath), 0755) dst, err := os.Create(destPath) if err != nil { jsonErr(w, 500, err.Error()); return } defer dst.Close() io.Copy(dst, file) url := "/uploads/" + filename jsonOK(w, map[string]interface{}{"url": url, "success": true}) } func handleHugo(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } if hugoStarting { jsonOK(w, map[string]interface{}{"running": false, "starting": true}); return } hugoStarting = true defer func() { hugoStarting = false }() exec.Command("taskkill", "/F", "/IM", "hugo.exe").Run() cmd := exec.Command("hugo", "serve", "--port", "1313", "--noHTTPCache") cmd.Dir = config.BLOG_ROOT if err := cmd.Start(); err != nil { jsonErr(w, 500, err.Error()); return } jsonOK(w, map[string]interface{}{"running": true, "url": "http://localhost:1313"}) } func handleDeploy(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } body, _ := readJSON(r) title := gs(body, "title") if title == "" { jsonErr(w, 400, "Title required"); return } runGit := func(args ...string) (string, error) { cmd := exec.Command("git", args...) cmd.Dir = config.BLOG_ROOT out, err := cmd.CombinedOutput() return string(out), err } var output string if o, err := runGit("add", "content/post", "static"); err != nil { jsonErr(w, 500, string(o)); return } else { output += o } if _, err := runGit("diff", "--cached", "--quiet"); err != nil { if o, err2 := runGit("commit", "-m", "发布: "+title); err2 != nil { jsonErr(w, 500, string(o)); return } else { output += o } } o, _ := runGit("push", "origin", "main") output += o jsonOK(w, map[string]interface{}{"success": true, "output": output}) } func handleStats(w http.ResponseWriter, r *http.Request) { postCount := posts.GetPostCount() linkCount := 0; feedCount := 0 if links, err := rssapi.GetLinks(true); err == nil { linkCount = len(links) } if feeds, err := rssapi.GetFeeds(); err == nil { feedCount = len(feeds) } jsonOK(w, map[string]interface{}{"posts": postCount, "feeds": feedCount, "links": linkCount}) } func handleRSSLinks(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": all := r.URL.Query().Get("all") == "1" links, err := rssapi.GetLinks(all) if err != nil { jsonErr(w, 500, err.Error()); return } jsonOK(w, map[string]interface{}{"links": links}) case "POST": body, _ := readJSON(r) url := gs(body, "url"); title := gs(body, "title"); desc := gs(body, "description") if url == "" || title == "" { jsonErr(w, 400, "url and title required"); return } rssapi.AddLink(url, title, desc) jsonOK(w, map[string]interface{}{"success": true}) case "PUT": body, _ := readJSON(r) id := gs(body, "id") enabled := gb(body, "enabled") rssapi.ToggleLink(id, enabled) jsonOK(w, map[string]interface{}{"success": true}) case "DELETE": body, _ := readJSON(r) id := gs(body, "id") rssapi.DeleteLink(id) jsonOK(w, map[string]interface{}{"success": true}) default: jsonErr(w, 405, "method not allowed") } } func handleRSSFeeds(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": feeds, err := rssapi.GetFeeds() if err != nil { jsonErr(w, 500, err.Error()); return } jsonOK(w, map[string]interface{}{"feeds": feeds}) case "POST": body, _ := readJSON(r) url := gs(body, "url"); title := gs(body, "title") if url == "" || title == "" { jsonErr(w, 400, "url and title required"); return } rssapi.AddFeed(url, title) jsonOK(w, map[string]interface{}{"success": true}) case "DELETE": body, _ := readJSON(r) id := gs(body, "id") rssapi.DeleteFeed(id) jsonOK(w, map[string]interface{}{"success": true}) default: jsonErr(w, 405, "method not allowed") } } func handleWechatHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": articles, err := wechat.GetArticles() if err != nil { jsonErr(w, 500, err.Error()); return } jsonOK(w, map[string]interface{}{"articles": articles}) case "POST": msg := r.URL.Query().Get("msg") jsonOK(w, map[string]interface{}{"ok": true, "msg": msg}) default: jsonErr(w, 405, "method not allowed") } } func handleCommentsStats(w http.ResponseWriter, r *http.Request) { stats := artalk.GetStats() jsonOK(w, stats) } func handleCommentsCleanup(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } body, _ := readJSON(r) slug := gs(body, "slug") if slug == "" { jsonErr(w, 400, "slug required"); return } result := artalk.CleanupComments(slug) jsonOK(w, result) } func handleCommentsLogin(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } body, _ := readJSON(r) name := gs(body, "name"); email := gs(body, "email"); password := gs(body, "password") if name == "" || email == "" || password == "" { jsonErr(w, 400, "name, email, password required"); return } result := artalk.Login(name, email, password) jsonOK(w, result) } func handleCommentsSub(w http.ResponseWriter, r *http.Request) { slug := extractSlug(r.URL.Path, "/api/comments/") comments := artalk.GetComments(slug) jsonOK(w, comments) } func handleComments(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } body, _ := readJSON(r) slug := gs(body, "slug"); content := gs(body, "content"); pageKey := gs(body, "page_key") if slug == "" { jsonErr(w, 400, "slug required"); return } result := artalk.PostComment(slug, content, pageKey) jsonOK(w, result) } func extractSlug(path, prefix string) string { p := strings.TrimPrefix(path, prefix) p = strings.TrimSuffix(p, "/") return p } ''' import "time" # needed below with open(r"E:\GitHub\blog\write-go\cmd\writeserver\handlers.go", "w", encoding="utf-8") as f: f.write(handlers) print("handlers.go written, size:", len(handlers))