Files
blog/tmp_gen_main.py
T
2026-06-01 17:57:22 +08:00

355 lines
12 KiB
Python

# Reconstruct main.go from known-working old version
# We know it had all the API handlers plus withCORS/panic recovery/parseDate fix
main_go = r'''package main
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"mime"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"write-server/internal/ai"
"write-server/internal/adminpanel"
"write-server/internal/artalk"
"write-server/internal/botconfig"
"write-server/internal/config"
"write-server/internal/helpers"
"write-server/internal/images"
"write-server/internal/posts"
"write-server/internal/recycle"
"write-server/internal/rssapi"
"write-server/internal/sessions"
"write-server/internal/tgcore"
"write-server/internal/tghandlers"
"write-server/internal/wechat"
"write-server/internal/writeserver"
)
var hugoStarting bool
var lg *slog.Logger
func main() {
config.Init()
recycle.InitRecycleBin()
posts.Init()
appCfg := botconfig.Load()
writeserver.Init(appCfg.WritePort, appCfg.WriteDir)
adminpanel.Init(appCfg.WritePort, appCfg.ControlPort)
lg = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})).With("svc", "writeapp")
ip := helpers.GetLanIP(appCfg.PreferIface)
lg.Info("WriteApp starting", "writePort", appCfg.WritePort)
fmt.Println("")
fmt.Println(" =========================================")
fmt.Println(" WriteApp - Blog CMS + Telegram Bot")
fmt.Println(" =========================================")
fmt.Printf(" Frontend : http://127.0.0.1:%d/\n", appCfg.WritePort)
fmt.Printf(" Admin : http://127.0.0.1:%d/admin\n", appCfg.WritePort)
fmt.Printf(" LAN : http://%s:%d/\n", ip, appCfg.WritePort)
fmt.Println(" =========================================")
fmt.Println("")
mux := http.NewServeMux()
mux.HandleFunc("/api/posts/batch", handlePostsBatch)
mux.HandleFunc("/api/posts/", handlePostBySlug)
mux.HandleFunc("/api/posts", handlePosts)
mux.HandleFunc("/api/recycle", handleRecycle)
mux.HandleFunc("/api/ai", handleAI)
mux.HandleFunc("/api/images", handleImages)
mux.HandleFunc("/api/upload", handleUpload)
mux.HandleFunc("/api/hugo", handleHugo)
mux.HandleFunc("/api/deploy", handleDeploy)
mux.HandleFunc("/api/stats", handleStats)
mux.HandleFunc("/api/rss/links", handleRSSLinks)
mux.HandleFunc("/api/rss/feeds", handleRSSFeeds)
mux.HandleFunc("/api/rss/wechat", handleWechatHandler)
mux.HandleFunc("/api/comments/stats", handleCommentsStats)
mux.HandleFunc("/api/comments/cleanup", handleCommentsCleanup)
mux.HandleFunc("/api/comments/login", handleCommentsLogin)
mux.HandleFunc("/api/comments/", handleCommentsSub)
mux.HandleFunc("/api/comments", handleComments)
// Admin panel on /admin
mux.HandleFunc("/admin", handleAdmin)
mux.HandleFunc("/admin/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`+"`"+`{"ok":true}`+"`"+`))
})
// Reverse proxy to Next.js for non-API routes
nextURL, _ := url.Parse("http://localhost:3000")
proxy := httputil.NewSingleHostReverseProxy(nextURL)
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(fallbackHTML))
}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/admin") {
http.NotFound(w, r)
return
}
proxy.ServeHTTP(w, r)
})
// Auto-start Next.js
go startNextJS()
// Start Telegram bot
if appCfg.TGToken != "" {
tgcore.Init(appCfg.TGToken)
go runTelegramBot(appCfg)
} else {
lg.Warn("no TG_TOKEN configured, bot disabled")
}
port := 8016
if p := os.Getenv("WRITE_PORT"); p != "" {
if n, err := strconv.Atoi(p); err == nil {
port = n
}
}
// Graceful shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
addr := fmt.Sprintf("0.0.0.0:%d", port)
fmt.Printf("[write-app] listening on %s\n", addr)
go func() {
if err := http.ListenAndServe(addr, withCORS(mux)); err != nil {
lg.Error("server error", "err", err)
}
}()
sig := <-sigCh
lg.Info("shutting down", "signal", sig.String())
}
const fallbackHTML = `+"`"+`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WriteApp</title>
<style>body{background:#0f172a;color:#e2e8f0;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center}h1{font-size:24px}p{color:#94a3b8;margin-top:8px}a{color:#38bdf8}</style>
</head><body><div><h1>WriteApp</h1><p>Next.js frontend is starting...</p><p><a href="/admin">Open Admin</a></p></div></body></html>`+"`"+`
func startNextJS() {
time.Sleep(2 * time.Second)
if _, err := net.DialTimeout("tcp", "127.0.0.1:3000", 2*time.Second); err == nil {
lg.Info("Next.js already running on :3000")
return
}
lg.Info("starting Next.js frontend")
cmd := exec.Command("npx.cmd", "next", "start", "-p", "3000")
cmd.Dir = filepath.Join(config.BLOG_ROOT, "write")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
lg.Warn("failed to start Next.js", "err", err)
} else {
lg.Info("Next.js started", "pid", cmd.Process.Pid)
}
}
func runTelegramBot(appCfg *botconfig.Config) {
tgcore.TG("setMyCommands", map[string]interface{}{
"commands": []map[string]string{
{"command": "start", "description": "启动"},
{"command": "new", "description": "写新文章"},
{"command": "list", "description": "文章列表"},
{"command": "edit", "description": "编辑文章"},
{"command": "save", "description": "保存草稿"},
{"command": "publish", "description": "发布到 GitHub"},
{"command": "stats", "description": "统计"},
{"command": "help", "description": "帮助"},
},
})
h := tghandlers.New(&tghandlers.Config{
WritePort: appCfg.WritePort,
ControlPort: appCfg.ControlPort,
PreferIface: appCfg.PreferIface,
})
argCommands := map[string]bool{
"new": true, "title": true, "categories": true, "tags": true, "author": true,
"edit": true, "view": true, "delete": true,
"link_add": true, "link_del": true, "link_toggle": true,
"feed_add": true, "feed_del": true,
"list": true, "links": true, "feeds": true,
}
var lastOffset int64
for {
data, err := tgcore.TG("getUpdates", map[string]interface{}{
"offset": lastOffset + 1, "timeout": 30,
"allowed_updates": []string{"message", "callback_query"},
})
if err != nil {
lg.Warn("tg poll error", "err", err)
time.Sleep(3 * time.Second)
continue
}
ok, _ := data["ok"].(bool)
if !ok { continue }
results, _ := data["result"].([]interface{})
for _, updateRaw := range results {
update, _ := updateRaw.(map[string]interface{})
if updateID, ok := update["update_id"].(float64); ok {
lastOffset = int64(updateID)
}
if cbRaw, ok := update["callback_query"]; ok {
cb, _ := cbRaw.(map[string]interface{})
cbMsg, _ := cb["message"].(map[string]interface{})
cbChat, _ := cbMsg["chat"].(map[string]interface{})
cbFrom, _ := cb["from"].(map[string]interface{})
var cbChatID int64
if cbChat != nil { cbChatID = tgcore.GetInt64(cbChat, "id") }
if cbChatID == 0 && cbFrom != nil { cbChatID = tgcore.GetInt64(cbFrom, "id") }
queryID := tgcore.GetString(cb, "id")
cbData := tgcore.GetString(cb, "data")
tgcore.HandleCallback(cbChatID, queryID, cbData, h)
continue
}
msg, _ := update["message"].(map[string]interface{})
if msg == nil { continue }
chat, _ := msg["chat"].(map[string]interface{})
if chat == nil { continue }
chatID := tgcore.GetInt64(chat, "id")
if !tgcore.Allowed(chatID, appCfg.AllowedIDs) { continue }
if photos, ok := msg["photo"]; ok {
photoArr, _ := photos.([]interface{})
if len(photoArr) > 0 && sessions.HasSession(chatID) {
s := sessions.GetSession(chatID)
largest := photoArr[len(photoArr)-1].(map[string]interface{})
fileID := tgcore.GetString(largest, "file_id")
fileRes, err := tgcore.TG("getFile", map[string]interface{}{"file_id": fileID})
if err == nil && fileRes["ok"] == true {
if fileResult, ok := fileRes["result"].(map[string]interface{}); ok {
filePath := tgcore.GetString(fileResult, "file_path")
caption := tgcore.GetString(msg, "caption")
if caption == "" { caption = "image" }
imgURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", appCfg.TGToken, filePath)
s.Content += "\n![" + caption + "](" + imgURL + ")\n"
}
}
}
continue
}
text := tgcore.GetString(msg, "text")
if text == "" { continue }
fullText := strings.TrimSpace(text)
if idx := strings.Index(fullText, "@"); idx >= 0 {
afterAt := fullText[idx:]
if spaceIdx := strings.Index(afterAt, " "); spaceIdx >= 0 { afterAt = afterAt[spaceIdx:] } else { afterAt = "" }
fullText = fullText[:idx] + afterAt
fullText = strings.TrimSpace(fullText)
}
parts := strings.SplitN(fullText, " ", 2)
slashCmd := ""; slashArg := ""
if strings.HasPrefix(parts[0], "/") {
slashCmd = strings.ToLower(parts[0][1:])
if len(parts) > 1 { slashArg = parts[1] }
}
if sessions.HasSession(chatID) && !strings.HasPrefix(fullText, "/") {
s := sessions.GetSession(chatID)
s.Content += fullText + "\n"
continue
}
if slashCmd == "" { continue }
lg.Info("tg command", "chat", chatID, "cmd", slashCmd)
handleTGCommand(h, chatID, slashCmd, slashArg, argCommands)
}
}
}
func handleTGCommand(h *tghandlers.Handlers, chatID int64, cmd, arg string, argCmds map[string]bool) {
defer func() {
if r := recover(); r != nil { lg.Error("handler panic", "cmd", cmd, "panic", r) }
}()
if argCmds[cmd] {
switch cmd {
case "new": h.New(chatID, arg)
case "title": h.Title(chatID, arg)
case "categories": h.Categories(chatID, arg)
case "tags": h.Tags(chatID, arg)
case "author": h.Author(chatID, arg)
case "edit": h.Edit(chatID, arg)
case "view": h.View(chatID, arg)
case "delete": h.Delete(chatID, arg)
case "link_add": h.LinkAdd(chatID, arg)
case "link_del": h.LinkDel(chatID, arg)
case "link_toggle": h.LinkToggle(chatID, arg)
case "feed_add": h.FeedAdd(chatID, arg)
case "feed_del": h.FeedDel(chatID, arg)
case "list": h.List(chatID, arg)
case "links": h.Links(chatID, arg)
case "feeds": h.Feeds(chatID, arg)
default: tgcore.SendMessage(chatID, "? Unknown: /"+cmd, nil)
}
} else {
switch cmd {
case "start": h.Start(chatID)
case "stop": h.Stop(chatID)
case "status": h.Status(chatID)
case "ip": h.IP(chatID)
case "help": h.Help(chatID)
case "save": h.Save(chatID)
case "publish": h.Publish(chatID)
case "draft": h.Draft(chatID)
case "preview": h.Preview(chatID)
case "cancel": h.Cancel(chatID)
case "stats": h.Stats(chatID)
default: tgcore.SendMessage(chatID, "? Unknown: /"+cmd, nil)
}
}
}
func handleAdmin(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(fmt.Sprintf(adminHTML, helpers.GetLanIP("WLAN"))))
}
func withCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
lg.Error("panic in handler", "path", r.URL.Path, "method", r.Method, "panic", fmt.Sprintf("%v", rec))
http.Error(w, fmt.Sprintf("Internal Server Error: %v", rec), http.StatusInternalServerError)
}
}()
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(204)
return
}
next.ServeHTTP(w, r)
})
}
'''
with open(r"E:\GitHub\blog\write-go\cmd\writeserver\main_new.go", "w", encoding="utf-8") as f:
f.write(main_go)
print("main_new.go written, length:", len(main_go))