snapshot before merge
This commit is contained in:
1 parent
06a5f3da4d
commit
671871e0b2
145 files changed
+9786
-59
No files matched your search
+396
@@ -0,0 +1,396 @@
|
||||
# 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 = ` + "`" + `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||
<title>WriteApp</title>
|
||||
<style>:root{color-scheme:dark;--bg:#111827;--card:#1f2937;--text:#f3f4f6;--muted:#9ca3af;--accent:#60a5fa;--accent-hover:#93bbfd;--line:#374151;--success:#86efac}*{box-sizing:border-box;margin:0;padding:0}body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px}.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:28px;max-width:420px;width:100%%}h1{font-size:18px;font-weight:700;margin-bottom:4px}.sub{font-size:13px;color:var(--muted);margin-bottom:20px}.row{display:flex;align-items:center;gap:10px;padding:12px 16px;background:rgba(255,255,255,0.03);border-radius:8px;margin-bottom:16px}.dot{width:10px;height:10px;border-radius:50%%;flex-shrink:0;background:var(--success)}.text{font-size:14px;font-weight:500}.ip{font-size:12px;color:var(--muted);margin-top:2px}.actions{display:flex;gap:8px;flex-wrap:wrap}.btn{padding:10px 20px;border-radius:8px;font-size:14px;font-weight:600;border:none;cursor:pointer;transition:all 0.15s;color:#fff;text-decoration:none;display:inline-block}.btn-primary{background:var(--accent)}.btn-primary:hover{background:var(--accent-hover)}.btn-secondary{background:transparent;border:1px solid var(--line);color:var(--text)}.btn-secondary:hover{background:rgba(255,255,255,0.05)}.link-row{margin-top:16px;padding:10px 14px;background:rgba(255,255,255,0.03);border-radius:8px;font-size:13px;color:var(--muted);word-break:break-all}.link-row a{color:var(--accent);text-decoration:none}.footer{margin-top:20px;font-size:11px;color:var(--muted);text-align:center}</style></head>
|
||||
<body><div class="card"><h1>WriteApp</h1><p class="sub">Blog CMS + Telegram Bot — Go Edition</p>
|
||||
<div class="row"><div class="dot"></div><div><div class="text">All Systems Running</div><div class="ip">Port 8016 · LAN: %s</div></div></div>
|
||||
<div class="actions"><a class="btn btn-primary" href="/">Open Editor</a><a class="btn btn-secondary" href="/api/stats" target="_blank">API Stats</a></div>
|
||||
<div class="link-row">Next.js Frontend: <a href="/">/</a></div>
|
||||
<div class="link-row">API: <a href="/api/posts">/api/posts</a> · <a href="/api/stats">/api/stats</a></div>
|
||||
<div class="footer">Single Process · Go 1.26</div></div></body></html>` + "`" + `
|
||||
|
||||
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))
|
||||
Reference in new issue
Block a user