From db87655c8725634d6d0c9fbb4e0ce5f80e7c96e5 Mon Sep 17 00:00:00 2001 From: Vaica <94612053+zqlit@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:23:04 +0800 Subject: [PATCH] 6.2 --- .idea/.gitignore | 5 - .idea/blog.iml | 12 - .idea/modules.xml | 8 - .idea/vcs.xml | 6 - bot-go/.gitignore | 2 - bot-go/_fix_import.py | 7 - bot-go/_fix_inline.py | 14 - bot-go/_fix_nil.py | 11 - bot-go/cmd/writebot/main.go | 204 ---- bot-go/config.go | 89 -- bot-go/go.mod | 3 - bot-go/internal/adminpanel/adminpanel.go | 158 --- bot-go/internal/botcfg/config.go | 72 -- bot-go/internal/config/config.go | 99 -- bot-go/internal/helpers/helpers.go | 159 --- bot-go/internal/logutil/logutil.go | 14 - bot-go/internal/sessions/sessions.go | 71 -- bot-go/internal/tgcore/tgcore.go | 301 ------ bot-go/internal/tghandlers/tghandlers.go | 622 ------------ bot-go/internal/writeserver/writeserver.go | 156 --- bot-go/start_bot.bat | 5 - .../meta/2026-06-01-深圳真的好热.json | 7 + .../.recycle/meta/2026-06-01-深圳真的热.json | 7 + write/.recycle/meta/202605311204.json | 7 + .../posts/2026-06-01-深圳真的好热/index.md | 24 + .../1780325877248-AgACAgQA.jpg | Bin 0 -> 166422 bytes .../posts/2026-06-01-深圳真的热/index.md | 20 + write/.recycle/posts/202605311204/index.md | 35 + write/docs/bot_design/design.md | 914 ++++++++++++++++++ write/docs/integration-plan.md | 477 +++++++++ write/install-startup.bat | 13 + write/install-startup.ps1 | 21 + write/package-lock.json | 13 + write/package.json | 1 + write/setup-autostart.bat | 33 + write/src/app/api/bot/restart/route.ts | 15 + write/src/app/api/bot/status/route.ts | 14 + write/src/app/api/deploy/route.ts | 77 +- write/src/app/api/hugo/route.ts | 57 +- write/src/app/bot/page.tsx | 101 ++ write/src/components/Sidebar.tsx | 3 +- write/src/instrumentation.ts | 8 + write/src/lib/bot/cache.ts | 30 + write/src/lib/bot/config.ts | 5 + write/src/lib/bot/handlers.ts | 654 +++++++++++++ write/src/lib/bot/helpers.ts | 37 + write/src/lib/bot/index.ts | 21 + write/src/lib/bot/poll.ts | 246 +++++ write/src/lib/bot/sessions.ts | 77 ++ write/src/lib/bot/tg.ts | 52 + write/src/lib/config.ts | 32 + write/src/lib/git.ts | 137 +++ write/src/lib/hugo.ts | 114 +++ write/src/lib/posts.ts | 2 +- write/src/lib/rssapi.ts | 29 +- write/src/types/marked.d.ts | 4 + write/start-silent.bat | 4 + write/startup.vbs | 3 + 58 files changed, 3184 insertions(+), 2128 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/blog.iml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 bot-go/.gitignore delete mode 100644 bot-go/_fix_import.py delete mode 100644 bot-go/_fix_inline.py delete mode 100644 bot-go/_fix_nil.py delete mode 100644 bot-go/cmd/writebot/main.go delete mode 100644 bot-go/config.go delete mode 100644 bot-go/go.mod delete mode 100644 bot-go/internal/adminpanel/adminpanel.go delete mode 100644 bot-go/internal/botcfg/config.go delete mode 100644 bot-go/internal/config/config.go delete mode 100644 bot-go/internal/helpers/helpers.go delete mode 100644 bot-go/internal/logutil/logutil.go delete mode 100644 bot-go/internal/sessions/sessions.go delete mode 100644 bot-go/internal/tgcore/tgcore.go delete mode 100644 bot-go/internal/tghandlers/tghandlers.go delete mode 100644 bot-go/internal/writeserver/writeserver.go delete mode 100644 bot-go/start_bot.bat create mode 100644 write/.recycle/meta/2026-06-01-深圳真的好热.json create mode 100644 write/.recycle/meta/2026-06-01-深圳真的热.json create mode 100644 write/.recycle/meta/202605311204.json create mode 100644 write/.recycle/posts/2026-06-01-深圳真的好热/index.md create mode 100644 write/.recycle/posts/2026-06-01-深圳真的热/1780325877248-AgACAgQA.jpg create mode 100644 write/.recycle/posts/2026-06-01-深圳真的热/index.md create mode 100644 write/.recycle/posts/202605311204/index.md create mode 100644 write/docs/bot_design/design.md create mode 100644 write/docs/integration-plan.md create mode 100644 write/install-startup.bat create mode 100644 write/install-startup.ps1 create mode 100644 write/setup-autostart.bat create mode 100644 write/src/app/api/bot/restart/route.ts create mode 100644 write/src/app/api/bot/status/route.ts create mode 100644 write/src/app/bot/page.tsx create mode 100644 write/src/instrumentation.ts create mode 100644 write/src/lib/bot/cache.ts create mode 100644 write/src/lib/bot/config.ts create mode 100644 write/src/lib/bot/handlers.ts create mode 100644 write/src/lib/bot/helpers.ts create mode 100644 write/src/lib/bot/index.ts create mode 100644 write/src/lib/bot/poll.ts create mode 100644 write/src/lib/bot/sessions.ts create mode 100644 write/src/lib/bot/tg.ts create mode 100644 write/src/lib/git.ts create mode 100644 write/src/lib/hugo.ts create mode 100644 write/src/types/marked.d.ts create mode 100644 write/start-silent.bat create mode 100644 write/startup.vbs diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index b58b603f..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/blog.iml b/.idea/blog.iml deleted file mode 100644 index 24643cc3..00000000 --- a/.idea/blog.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index e81aaae0..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/bot-go/.gitignore b/bot-go/.gitignore deleted file mode 100644 index 2514bd4e..00000000 --- a/bot-go/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -writebot.exe -.env diff --git a/bot-go/_fix_import.py b/bot-go/_fix_import.py deleted file mode 100644 index fd984f92..00000000 --- a/bot-go/_fix_import.py +++ /dev/null @@ -1,7 +0,0 @@ -p = r"E:\GitHub\blog\bot-go\cmd\writebot\main.go" -with open(p, "r", encoding="utf-8") as f: - c = f.read() -c = c.replace('"net/http"\n', '') -with open(p, "w", encoding="utf-8", newline="\n") as f: - f.write(c) -print("fixed") diff --git a/bot-go/_fix_inline.py b/bot-go/_fix_inline.py deleted file mode 100644 index e854c14a..00000000 --- a/bot-go/_fix_inline.py +++ /dev/null @@ -1,14 +0,0 @@ -p = r"E:\GitHub\blog\bot-go\cmd\writebot\main.go" -with open(p, "r", encoding="utf-8") as f: - c = f.read() - -# Replace logutil.Init + log = logutil.Logger with inline -c = c.replace( - 'logutil.Init("writebot")\n\tlog = logutil.Logger', - 'log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})).With("svc", "writebot")' -) -c = c.replace('\t"writebot/internal/logutil"\n', '') - -with open(p, "w", encoding="utf-8", newline="\n") as f: - f.write(c) -print("Fixed bot-go inline slog") diff --git a/bot-go/_fix_nil.py b/bot-go/_fix_nil.py deleted file mode 100644 index 4a53ffc4..00000000 --- a/bot-go/_fix_nil.py +++ /dev/null @@ -1,11 +0,0 @@ -p = r"E:\GitHub\blog\bot-go\cmd\writebot\main.go" -with open(p, "r", encoding="utf-8") as f: - c = f.read() - -# Fix: move log usage after logutil.Init -c = c.replace("logutil.Init(\"writebot\")\n\tlog = logutil.Logger\n\n\tcfg := config.Load()", - "cfg := config.Load()\n\tlogutil.Init(\"writebot\")\n\tlog = logutil.Logger") - -with open(p, "w", encoding="utf-8", newline="\n") as f: - f.write(c) -print("Fixed bot-go logger init order") diff --git a/bot-go/cmd/writebot/main.go b/bot-go/cmd/writebot/main.go deleted file mode 100644 index 2a58c7d2..00000000 --- a/bot-go/cmd/writebot/main.go +++ /dev/null @@ -1,204 +0,0 @@ -package main - -import ( - "fmt" - "log/slog" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "writebot/internal/botcfg" - "writebot/internal/sessions" - "writebot/internal/tgcore" - "writebot/internal/tghandlers" - - "write-server/pkg/posts" -) - -var log *slog.Logger - -func main() { - cfg := botcfg.Load() - posts.Init() - tgcore.Init(cfg.TGToken) - - log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})).With("svc", "writebot") - log.Info("WriteBot starting") - - if cfg.TGToken == "" { - log.Error("no TG_BOT_TOKEN configured") - os.Exit(1) - } - - // Register bot - me, err := tgcore.TG("getMe", map[string]interface{}{}) - if err == nil { - if ok, _ := me["ok"].(bool); ok { - if result, ok := me["result"].(map[string]interface{}); ok { - log.Info("telegram connected", "username", tgcore.GetString(result, "username")) - } - } - } - 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: cfg.WritePort, - ControlPort: cfg.ControlPort, - PreferIface: cfg.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, - } - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) - - go func() { - 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 { - log.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, cfg.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", cfg.TGToken, filePath) - s.Content += " -![" + caption + "](" + imgURL + ") -" - } - } - } - 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 = strings.TrimSpace(fullText[:idx] + afterAt) - } - 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 + " -" - continue - } - if slashCmd == "" { continue } - log.Info("tg command", "chat", chatID, "cmd", slashCmd) - handleCmd(h, chatID, slashCmd, slashArg, argCommands) - } - } - }() - - sig := <-sigCh - log.Info("shutting down", "signal", sig.String()) -} - -func handleCmd(h *tghandlers.Handlers, chatID int64, cmd, arg string, argCmds map[string]bool) { - defer func() { if r := recover(); r != nil { log.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 "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) - case "status": h.Status(chatID) - case "ip": h.IP(chatID) - default: tgcore.SendMessage(chatID, "? Unknown: /"+cmd, nil) - } - } -} diff --git a/bot-go/config.go b/bot-go/config.go deleted file mode 100644 index 849763a6..00000000 --- a/bot-go/config.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "bufio" - "os" - "path/filepath" - "strconv" - "strings" -) - -type Config struct { - TGToken string - AllowedIDs []int64 - WritePort int - ControlPort int - PreferIface string - WriteDir string - BlogDir string // parent of WriteDir, for hugo -} - -func LoadConfig() *Config { - c := &Config{ - WritePort: 8016, - ControlPort: 8017, - PreferIface: "WLAN", - } - - // Try .env next to executable first, then fallback to cwd - exe, _ := os.Executable() - envPath := filepath.Join(filepath.Dir(exe), ".env") - if _, err := os.Stat(envPath); os.IsNotExist(err) { - envPath = ".env" - } - - f, err := os.Open(envPath) - if err != nil { - return c - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - key := strings.TrimSpace(parts[0]) - val := strings.TrimSpace(parts[1]) - - switch key { - case "TG_BOT_TOKEN": - c.TGToken = val - case "TG_ALLOWED_CHAT_IDS": - for _, s := range strings.Split(val, ",") { - s = strings.TrimSpace(s) - if id, err := strconv.ParseInt(s, 10, 64); err == nil { - c.AllowedIDs = append(c.AllowedIDs, id) - } - } - case "WRITE_PORT": - if p, err := strconv.Atoi(val); err == nil { - c.WritePort = p - } - case "BOT_PORT": - if p, err := strconv.Atoi(val); err == nil { - c.ControlPort = p - } - case "PREFER_IFACE": - c.PreferIface = val - } - } - - // WriteDir and BlogDir derived from executable location - exeDir := filepath.Dir(exe) - writeDir := filepath.Join(exeDir, "..", "write") - blogDir := filepath.Join(exeDir, "..") - if abs, err := filepath.Abs(writeDir); err == nil { - c.WriteDir = abs - } - if abs, err := filepath.Abs(blogDir); err == nil { - c.BlogDir = abs - } - - return c -} diff --git a/bot-go/go.mod b/bot-go/go.mod deleted file mode 100644 index 603a46b9..00000000 --- a/bot-go/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module writebot - -go 1.26.3 diff --git a/bot-go/internal/adminpanel/adminpanel.go b/bot-go/internal/adminpanel/adminpanel.go deleted file mode 100644 index 717bfba8..00000000 --- a/bot-go/internal/adminpanel/adminpanel.go +++ /dev/null @@ -1,158 +0,0 @@ -package adminpanel - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "writebot/internal/helpers" - "writebot/internal/writeserver" -) - -var ( - writePort int - controlPort int -) - -func Init(wp, cp int) { - writePort = wp - controlPort = cp -} - -const adminHTML = ` - - - - -WriteBot - - - -
-

WriteBot

-
Ctrl+D bookmark this page
- -
Loading...
-
-
-
-
-
-
-
-
-
- - - -
- - -
- - -` - -func CreateControlServer() *http.Server { - html := strings.ReplaceAll(adminHTML, "WRITE_PORT", fmt.Sprintf("%d", writePort)) - - mux := http.NewServeMux() - - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - w.Header().Set("Access-Control-Allow-Origin", "*") - - switch path { - case "/": - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(html)) - case "/status": - w.Header().Set("Content-Type", "application/json") - s := writeserver.GetWriteStatus() - s["ip"] = helpers.GetLanIP("WLAN") - s["port"] = writePort - json.NewEncoder(w).Encode(s) - case "/start": - if r.Method == "POST" { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(writeserver.StartWriteServer()) - } else { - http.NotFound(w, r) - } - case "/stop": - if r.Method == "POST" { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]bool{"ok": writeserver.StopWriteServer()}) - } else { - http.NotFound(w, r) - } - case "/ip": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "ip": helpers.GetLanIP("WLAN"), - "port": writePort, - }) - case "/health": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]bool{"ok": true}) - default: - w.WriteHeader(http.StatusNotFound) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"error": "not found"}) - } - }) - - addr := fmt.Sprintf("127.0.0.1:%d", controlPort) - srv := &http.Server{ - Addr: addr, - Handler: mux, - } - - fmt.Printf("[control] http://%s\n", addr) - - return srv -} diff --git a/bot-go/internal/botcfg/config.go b/bot-go/internal/botcfg/config.go deleted file mode 100644 index c6bca069..00000000 --- a/bot-go/internal/botcfg/config.go +++ /dev/null @@ -1,72 +0,0 @@ -package botcfg - -import ( - "bufio" - "os" - "path/filepath" - "runtime" - "strconv" - "strings" -) - -type Config struct { - TGToken string - AllowedIDs []int64 - WritePort int - ControlPort int - PreferIface string -} - -func Load() *Config { - cfg := &Config{ - WritePort: 8016, - ControlPort: 8017, - PreferIface: "WLAN", - } - - _, filename, _, _ := runtime.Caller(0) - srcDir := filepath.Dir(filename) - rootDir := filepath.Dir(filepath.Dir(filepath.Dir(srcDir))) - - envPaths := []string{ - filepath.Join(rootDir, "bot-go", ".env"), - filepath.Join(rootDir, ".env"), - } - var f *os.File - var err error - for _, envPath := range envPaths { - f, err = os.Open(envPath) - if err == nil { break } - } - if err != nil { return cfg } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { continue } - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { continue } - key := strings.TrimSpace(parts[0]) - val := strings.TrimSpace(parts[1]) - if os.Getenv(key) == "" { os.Setenv(key, val) } - } - - cfg.TGToken = os.Getenv("TG_BOT_TOKEN") - allowedStr := os.Getenv("TG_ALLOWED_CHAT_IDS") - if allowedStr != "" { - for _, s := range strings.Split(allowedStr, ",") { - s = strings.TrimSpace(s) - if id, err := strconv.ParseInt(s, 10, 64); err == nil { cfg.AllowedIDs = append(cfg.AllowedIDs, id) } - } - } - if p := os.Getenv("WRITE_PORT"); p != "" { - if port, err := strconv.Atoi(p); err == nil { cfg.WritePort = port } - } - if p := os.Getenv("BOT_PORT"); p != "" { - if port, err := strconv.Atoi(p); err == nil { cfg.ControlPort = port } - } - if p := os.Getenv("PREFER_IFACE"); p != "" { cfg.PreferIface = p } - - return cfg -} diff --git a/bot-go/internal/config/config.go b/bot-go/internal/config/config.go deleted file mode 100644 index 4afaff69..00000000 --- a/bot-go/internal/config/config.go +++ /dev/null @@ -1,99 +0,0 @@ -package config - -import ( - "bufio" - "os" - "path/filepath" - "runtime" - "strconv" - "strings" -) - -type Config struct { - TGToken string - AllowedIDs []int64 - WritePort int - ControlPort int - PreferIface string - WriteDir string -} - -func Load() *Config { - cfg := &Config{ - WritePort: 8016, - ControlPort: 8017, - PreferIface: "WLAN", - } - - _, filename, _, _ := runtime.Caller(0) - srcDir := filepath.Dir(filename) - rootDir := filepath.Dir(filepath.Dir(filepath.Dir(srcDir))) - // Try multiple .env locations: bot-go/.env first, then blog/.env - envPaths := []string{ - filepath.Join(rootDir, "bot-go", ".env"), - filepath.Join(rootDir, ".env"), - filepath.Join(filepath.Dir(rootDir), "bot", ".env"), - } - var f *os.File - var err error - for _, envPath := range envPaths { - f, err = os.Open(envPath) - if err == nil { - break - } - } - if err != nil { - return cfg - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - key := strings.TrimSpace(parts[0]) - val := strings.TrimSpace(parts[1]) - if existing := os.Getenv(key); existing != "" { - continue - } - os.Setenv(key, val) - } - - cfg.TGToken = os.Getenv("TG_BOT_TOKEN") - allowedStr := os.Getenv("TG_ALLOWED_CHAT_IDS") - if allowedStr != "" { - for _, s := range strings.Split(allowedStr, ",") { - s = strings.TrimSpace(s) - if s == "" { - continue - } - if id, err := strconv.ParseInt(s, 10, 64); err == nil { - cfg.AllowedIDs = append(cfg.AllowedIDs, id) - } - } - } - - if p := os.Getenv("WRITE_PORT"); p != "" { - if port, err := strconv.Atoi(p); err == nil { - cfg.WritePort = port - } - } - if p := os.Getenv("BOT_PORT"); p != "" { - if port, err := strconv.Atoi(p); err == nil { - cfg.ControlPort = port - } - } - if p := os.Getenv("PREFER_IFACE"); p != "" { - cfg.PreferIface = p - } - - exe, _ := os.Executable(); exeDir := filepath.Dir(exe); cfg.WriteDir = filepath.Join(filepath.Dir(exeDir), "write-go"); if _, err := os.Stat(cfg.WriteDir); os.IsNotExist(err) { cfg.WriteDir = filepath.Join(filepath.Dir(rootDir), "write-go") } - - return cfg -} diff --git a/bot-go/internal/helpers/helpers.go b/bot-go/internal/helpers/helpers.go deleted file mode 100644 index 5a9d48e2..00000000 --- a/bot-go/internal/helpers/helpers.go +++ /dev/null @@ -1,159 +0,0 @@ -package helpers - -import ( - "fmt" - "net" - "strings" - "time" -) - -var lanIPCache struct { - ip string - ts time.Time -} - -func GetLanIP(preferIface string) string { - if lanIPCache.ip != "" && time.Since(lanIPCache.ts) < 30*time.Second { - return lanIPCache.ip - } - - ip := "0.0.0.0" - ifaces, err := net.Interfaces() - if err != nil { - lanIPCache.ip = ip - lanIPCache.ts = time.Now() - return ip - } - - type candidate struct { - name string - address string - } - var candidates []candidate - - for _, iface := range ifaces { - addrs, err := iface.Addrs() - if err != nil { - continue - } - for _, addr := range addrs { - ipNet, ok := addr.(*net.IPNet) - if !ok { - continue - } - ip4 := ipNet.IP.To4() - if ip4 == nil { - continue - } - if ip4.IsLoopback() { - continue - } - ipStr := ip4.String() - if strings.HasPrefix(ipStr, "169.254.") { - continue - } - candidates = append(candidates, candidate{name: iface.Name, address: ipStr}) - } - } - - prefLower := strings.ToLower(preferIface) - for _, c := range candidates { - if strings.Contains(strings.ToLower(c.name), prefLower) { - ip = c.address - lanIPCache.ip = ip - lanIPCache.ts = time.Now() - return ip - } - } - - for _, c := range candidates { - n := strings.ToLower(c.name) - if !strings.Contains(n, "vmware") && !strings.Contains(n, "virtual") && !strings.Contains(n, "vethernet") { - ip = c.address - lanIPCache.ip = ip - lanIPCache.ts = time.Now() - return ip - } - } - - if len(candidates) > 0 { - ip = candidates[0].address - } - - lanIPCache.ip = ip - lanIPCache.ts = time.Now() - return ip -} - -func CheckPort(port int) bool { - conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 1500*time.Millisecond) - if err != nil { - return false - } - conn.Close() - return true -} - -func WaitForReady(port int, timeoutMs int) bool { - if timeoutMs <= 0 { - timeoutMs = 30000 - } - deadline := time.Now().Add(time.Duration(timeoutMs) * time.Millisecond) - for time.Now().Before(deadline) { - if CheckPort(port) { - return true - } - time.Sleep(1500 * time.Millisecond) - } - return false -} - -func MakeSlug(title string) string { - dateStr := time.Now().Format("2006-01-02") - slugPart := title - // Replace non-word, non-CJK chars with - - slugPart = strings.Map(func(r rune) rune { - if (r >= 0x4e00 && r <= 0x9fff) || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' { - return r - } - return '-' - }, slugPart) - slugPart = strings.Trim(slugPart, "-") - slugPart = strings.ToLower(slugPart) - if slugPart == "" { - slugPart = "post" - } - return dateStr + "-" + slugPart -} - -func SessionSummary(title string, categories, tags []string, content string) string { - lines := 0 - chars := len([]rune(content)) - for _, ch := range content { - if ch == '\n' { - lines++ - } - } - if content != "" { - lines++ - } - - displayTitle := title - if displayTitle == "" { - displayTitle = "未命名" - } - - parts := []string{ - fmt.Sprintf("📝 %s", displayTitle), - } - if len(categories) > 0 { - parts = append(parts, "📂 "+strings.Join(categories, "、")) - } - if len(tags) > 0 { - parts = append(parts, "🏷 "+strings.Join(tags, "、")) - } - if chars > 0 { - parts = append(parts, fmt.Sprintf("✍ %d 行 %d 字", lines, chars)) - } - return strings.Join(parts, " · ") -} diff --git a/bot-go/internal/logutil/logutil.go b/bot-go/internal/logutil/logutil.go deleted file mode 100644 index 3a953922..00000000 --- a/bot-go/internal/logutil/logutil.go +++ /dev/null @@ -1,14 +0,0 @@ -package logutil - -import ( - "log/slog" - "os" -) - -var Logger *slog.Logger - -func Init(service string) { - Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: slog.LevelInfo, - })).With("svc", service) -} diff --git a/bot-go/internal/sessions/sessions.go b/bot-go/internal/sessions/sessions.go deleted file mode 100644 index 715b3afd..00000000 --- a/bot-go/internal/sessions/sessions.go +++ /dev/null @@ -1,71 +0,0 @@ -package sessions - -import ( - "sync" - - "write-server/pkg/posts" -) - -type Session struct { - Title string `json:"title"` - Slug string `json:"slug"` - Content string `json:"content"` - Categories []string `json:"categories"` - Tags []string `json:"tags"` - Author string `json:"author"` - Draft bool `json:"draft"` -} - -var mu sync.Mutex -var sessions = make(map[int64]*Session) - -func GetSession(chatID int64) *Session { - mu.Lock() - defer mu.Unlock() - if s, ok := sessions[chatID]; ok { - return s - } - s := &Session{Author: "qunlin", Draft: true} - sessions[chatID] = s - return s -} - -func ClearSession(chatID int64) { - mu.Lock() - defer mu.Unlock() - delete(sessions, chatID) -} - -func HasSession(chatID int64) bool { - mu.Lock() - defer mu.Unlock() - _, ok := sessions[chatID] - return ok -} - -func Save(s *Session, isDraft bool) error { - fm := posts.FrontMatter{ - Title: s.Title, - Slug: s.Slug, - Draft: isDraft, - Categories: s.Categories, - Tags: s.Tags, - Author: s.Author, - Layout: "post", - } - - existing := posts.GetPost(s.Slug) - if existing != nil { - updates := map[string]interface{}{} - updates["title"] = s.Title - updates["draft"] = isDraft - updates["categories"] = s.Categories - updates["tags"] = s.Tags - updates["author"] = s.Author - _, err := posts.UpdatePost(s.Slug, updates, &s.Content) - return err - } - - _, err := posts.CreatePost(fm, s.Content) - return err -} diff --git a/bot-go/internal/tgcore/tgcore.go b/bot-go/internal/tgcore/tgcore.go deleted file mode 100644 index fd9853ad..00000000 --- a/bot-go/internal/tgcore/tgcore.go +++ /dev/null @@ -1,301 +0,0 @@ -package tgcore - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "strings" - "time" -) - -var API string - -func Init(token string) { - API = "https://api.telegram.org/bot" + token -} - -func TG(method string, body interface{}) (map[string]interface{}, error) { - jsonBody, err := json.Marshal(body) - if err != nil { - return nil, err - } - resp, err := http.Post(API+"/"+method, "application/json", bytes.NewReader(jsonBody)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var result map[string]interface{} - if err := json.Unmarshal(data, &result); err != nil { - return nil, err - } - return result, nil -} - -func Allowed(chatID int64, allowedIDs []int64) bool { - if len(allowedIDs) == 0 { - return true - } - for _, id := range allowedIDs { - if id == chatID { - return true - } - } - return false -} - -func SendMessage(chatID int64, text string, opts map[string]interface{}) error { - safeText := text - body := map[string]interface{}{ - "chat_id": chatID, - "text": safeText, - "parse_mode": "HTML", - "disable_web_page_preview": true, - } - if opts != nil { - for k, v := range opts { - body[k] = v - } - } - - _, err := TG("sendMessage", body) - if err != nil && (strings.Contains(err.Error(), "parse") || strings.Contains(err.Error(), "HTML")) { - delete(body, "parse_mode") - _, err = TG("sendMessage", body) - } - return err -} - -// TgHandler is the callback handler interface -type TgHandler interface { - List(chatID int64, page string) - Links(chatID int64, page string) - Feeds(chatID int64, page string) - // Session callbacks - SessionCallback(chatID int64, action string) - MenuCallback(chatID int64, action string) -} - -func HandleCallback(chatID int64, queryID string, data string, h TgHandler) { - if data == "" { - return - } - - // Session callbacks - if strings.HasPrefix(data, "session_") { - action := strings.TrimPrefix(data, "session_") - if h != nil { - h.SessionCallback(chatID, action) - } - TG("answerCallbackQuery", map[string]interface{}{"callback_query_id": queryID}) - return - } - - // Menu callbacks - if strings.HasPrefix(data, "menu_") { - action := strings.TrimPrefix(data, "menu_") - if h != nil { - h.MenuCallback(chatID, action) - } - TG("answerCallbackQuery", map[string]interface{}{"callback_query_id": queryID}) - return - } - - // Pagination callbacks: list_N, links_N, feeds_N - parts := strings.SplitN(data, "_", 2) - if len(parts) == 2 { - cmd := parts[0] - page := parts[1] - if h != nil { - switch cmd { - case "list": - h.List(chatID, page) - case "links": - h.Links(chatID, page) - case "feeds": - h.Feeds(chatID, page) - } - } - } - TG("answerCallbackQuery", map[string]interface{}{"callback_query_id": queryID}) -} - -func GetInt64(m map[string]interface{}, key string) int64 { - if v, ok := m[key]; ok { - switch n := v.(type) { - case float64: - return int64(n) - case int64: - return n - } - } - return 0 -} - -func GetString(m map[string]interface{}, key string) string { - if v, ok := m[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - -// === Inlined from helpers === - -var lanIPCache struct { - ip string - ts time.Time -} - -func GetLanIP(preferIface string) string { - if lanIPCache.ip != "" && time.Since(lanIPCache.ts) < 30*time.Second { - return lanIPCache.ip - } - - ip := "0.0.0.0" - ifaces, err := net.Interfaces() - if err != nil { - lanIPCache.ip = ip - lanIPCache.ts = time.Now() - return ip - } - - type candidate struct { - name string - address string - } - var candidates []candidate - - for _, iface := range ifaces { - addrs, err := iface.Addrs() - if err != nil { - continue - } - for _, addr := range addrs { - ipNet, ok := addr.(*net.IPNet) - if !ok { - continue - } - ip4 := ipNet.IP.To4() - if ip4 == nil { - continue - } - if ip4.IsLoopback() { - continue - } - ipStr := ip4.String() - if strings.HasPrefix(ipStr, "169.254.") { - continue - } - candidates = append(candidates, candidate{name: iface.Name, address: ipStr}) - } - } - - prefLower := strings.ToLower(preferIface) - for _, c := range candidates { - if strings.Contains(strings.ToLower(c.name), prefLower) { - ip = c.address - lanIPCache.ip = ip - lanIPCache.ts = time.Now() - return ip - } - } - - for _, c := range candidates { - n := strings.ToLower(c.name) - if !strings.Contains(n, "vmware") && !strings.Contains(n, "virtual") && !strings.Contains(n, "vethernet") { - ip = c.address - lanIPCache.ip = ip - lanIPCache.ts = time.Now() - return ip - } - } - - if len(candidates) > 0 { - ip = candidates[0].address - } - - lanIPCache.ip = ip - lanIPCache.ts = time.Now() - return ip -} - -func CheckPort(port int) bool { - conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 1500*time.Millisecond) - if err != nil { - return false - } - conn.Close() - return true -} - -func WaitForReady(port int, timeoutMs int) bool { - if timeoutMs <= 0 { - timeoutMs = 30000 - } - deadline := time.Now().Add(time.Duration(timeoutMs) * time.Millisecond) - for time.Now().Before(deadline) { - if CheckPort(port) { - return true - } - time.Sleep(1500 * time.Millisecond) - } - return false -} - -func MakeSlug(title string) string { - dateStr := time.Now().Format("2006-01-02") - slugPart := title - // Replace non-word, non-CJK chars with - - slugPart = strings.Map(func(r rune) rune { - if (r >= 0x4e00 && r <= 0x9fff) || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' { - return r - } - return '-' - }, slugPart) - slugPart = strings.Trim(slugPart, "-") - slugPart = strings.ToLower(slugPart) - if slugPart == "" { - slugPart = "post" - } - return dateStr + "-" + slugPart -} - -func SessionSummary(title string, categories, tags []string, content string) string { - lines := 0 - chars := len([]rune(content)) - for _, ch := range content { - if ch == '\n' { - lines++ - } - } - if content != "" { - lines++ - } - - displayTitle := title - if displayTitle == "" { - displayTitle = "未命名" - } - - parts := []string{ - fmt.Sprintf("📝 %s", displayTitle), - } - if len(categories) > 0 { - parts = append(parts, "📂 "+strings.Join(categories, "、")) - } - if len(tags) > 0 { - parts = append(parts, "🏷 "+strings.Join(tags, "、")) - } - if chars > 0 { - parts = append(parts, fmt.Sprintf("✍ %d 行 %d 字", lines, chars)) - } - return strings.Join(parts, " · ") -} diff --git a/bot-go/internal/tghandlers/tghandlers.go b/bot-go/internal/tghandlers/tghandlers.go deleted file mode 100644 index 2694926c..00000000 --- a/bot-go/internal/tghandlers/tghandlers.go +++ /dev/null @@ -1,622 +0,0 @@ -package tghandlers - -import ( - "fmt" - "math" - "strconv" - "strings" - - "write-server/internal/helpers" - "write-server/internal/sessions" - "write-server/internal/tgcore" - "write-server/internal/writeserver" - - "write-server/pkg/posts" - "write-server/pkg/rssapi" - "write-server/pkg/recycle" -) - -type Handlers struct { - cfg *Config -} - -type Config struct { - WritePort int - ControlPort int - PreferIface string -} - -func New(cfg *Config) *Handlers { - return &Handlers{cfg: cfg} -} - -func (h *Handlers) dispatchList(chatID int64, pageStr string) error { - page, _ := strconv.Atoi(pageStr) - if page < 1 { page = 1 } - perPage := 5 - - list, total, _ := posts.ListPosts("", page, 9999, true) - if len(list) == 0 { - return tgcore.SendMessage(chatID, "📭 暂无文章。", nil) - } - - totalPages := int(math.Ceil(float64(total) / float64(perPage))) - if page > totalPages { page = totalPages } - - start := (page - 1) * perPage - end := start + perPage - if end > len(list) { end = len(list) } - pageItems := list[start:end] - - header := fmt.Sprintf("📋 文章列表 共%d篇 · 第%d/%d页", total, page, totalPages) - lines := []string{header} - for _, p := range pageItems { - emoji := "✅" - if p.Draft { emoji = "📝" } - date := "" - if len(p.Date) >= 10 { date = p.Date[:10] } - lines = append(lines, fmt.Sprintf("%s %s /edit_%s %s", emoji, p.Title, p.Slug, date)) - } - - kb := map[string]interface{}{} - var nav []map[string]interface{} - if page > 1 { - nav = append(nav, map[string]interface{}{"text": "◀ 上一页", "callback_data": fmt.Sprintf("list_%d", page-1)}) - } - if page < totalPages { - nav = append(nav, map[string]interface{}{"text": "下一页 ▶", "callback_data": fmt.Sprintf("list_%d", page+1)}) - } - if len(nav) > 0 { - kb["inline_keyboard"] = []interface{}{nav} - tgcore.TG("sendMessage", map[string]interface{}{ - "chat_id": chatID, - "text": strings.Join(lines, "\n"), - "parse_mode": "HTML", - "reply_markup": kb, - "disable_web_page_preview": true, - }) - return nil - } - return tgcore.SendMessage(chatID, strings.Join(lines, "\n"), nil) -} - -func (h *Handlers) List(chatID int64, page string) { h.dispatchList(chatID, page) } - - -func (h *Handlers) Start(chatID int64) { - ip := helpers.GetLanIP(h.cfg.PreferIface) - ready := EnsureWriteReady(chatID, h.cfg.WritePort) - if ready { - kb := map[string]interface{}{ - "inline_keyboard": []interface{}{ - []map[string]interface{}{ - {"text": "📝 开始写作", "callback_data": "menu_new"}, - {"text": "📋 文章列表", "callback_data": "list_1"}, - }, - []map[string]interface{}{ - {"text": "🔗 友链管理", "callback_data": "links_1"}, - {"text": "📡 订阅源", "callback_data": "feeds_1"}, - }, - []map[string]interface{}{ - {"text": "📊 网站数据", "callback_data": "menu_stats"}, - {"text": "ℹ️ 帮助", "callback_data": "menu_help"}, - }, - }, - } - tgcore.TG("sendMessage", map[string]interface{}{ - "chat_id": chatID, - "text": fmt.Sprintf("✅ Write 已就绪\n\n📱 手机: http://%s:%d/\n💻 电脑: http://localhost:%d/\n📰 文章: http://%s:1313/posts/", ip, h.cfg.WritePort, h.cfg.WritePort, ip), - "parse_mode": "HTML", - "reply_markup": kb, - "disable_web_page_preview": true, - }) - return - } - tgcore.SendMessage(chatID, "❌ 启动超时,请查看控制台日志。", nil) -} - -func (h *Handlers) Stop(chatID int64) { - if writeserver.StopWriteServer() { - tgcore.SendMessage(chatID, "⚡ Write 已停止。", nil) - } else { - tgcore.SendMessage(chatID, "⏸ Write 未运行。", nil) - } -} - -func (h *Handlers) Status(chatID int64) { - s := writeserver.GetWriteStatus() - status := "⏸ 未运行" - if s["running"].(bool) { status = "✅ 运行中" } - if s["starting"].(bool) { status = "⏳ 启动中..." } - lines := []string{"Write 状态: " + status} - if s["running"].(bool) { - ip := helpers.GetLanIP(h.cfg.PreferIface) - lines = append(lines, fmt.Sprintf("🌐 局域网: http://%s:%d", ip, h.cfg.WritePort)) - lines = append(lines, fmt.Sprintf("💻 本地: http://localhost:%d", h.cfg.WritePort)) - } - tgcore.SendMessage(chatID, strings.Join(lines, "\n"), nil) -} - -func (h *Handlers) IP(chatID int64) { - ip := helpers.GetLanIP(h.cfg.PreferIface) - tgcore.SendMessage(chatID, fmt.Sprintf("📱 手机访问: http://%s:%d\n💻 电脑访问: http://localhost:%d", ip, h.cfg.WritePort, h.cfg.WritePort), nil) -} - -func (h *Handlers) Help(chatID int64) { - kb := map[string]interface{}{ - "inline_keyboard": []interface{}{ - []map[string]interface{}{{"text": "📝 开始写作", "callback_data": "menu_new"}}, - []map[string]interface{}{{"text": "📋 文章列表", "callback_data": "list_1"}}, - []map[string]interface{}{{"text": "🔗 友链", "callback_data": "links_1"}, {"text": "📡 订阅", "callback_data": "feeds_1"}}, - }, - } - tgcore.TG("sendMessage", map[string]interface{}{ - "chat_id": chatID, - "text": "🤖 WriteBot 命令\n\n📝 /new 标题 — 开始写作\n✏️ /edit slug — 编辑文章\n📋 /list — 文章列表\n🔍 /view slug — 查看文章\n🗑 /delete slug — 删除文章\n\n✍️ 写作中:\n/publish — 发布\n/draft — 存草稿\n/preview — 预览\n/cancel — 丢弃\n\n🔗 友链 /links\n📡 订阅 /feeds\n📊 /stats 统计", - "parse_mode": "HTML", - "reply_markup": kb, - "disable_web_page_preview": true, - }) -} - - -// --- Writing commands --- - -func (h *Handlers) New(chatID int64, title string) { - if title == "" { - tgcore.SendMessage(chatID, "📝 请提供标题:/new 你的标题", nil) - return - } - s := sessions.GetSession(chatID) - s.Title = title - s.Slug = helpers.MakeSlug(title) - s.Content = "" - s.Categories = nil - s.Tags = nil - s.Draft = true - s.Author = "qunlin" - - kb := map[string]interface{}{ - "inline_keyboard": []interface{}{ - []map[string]interface{}{ - {"text": "📂 分类", "callback_data": "session_categories"}, - {"text": "🏷 标签", "callback_data": "session_tags"}, - }, - []map[string]interface{}{ - {"text": "🚀 发布", "callback_data": "session_publish"}, - {"text": "📄 存草稿", "callback_data": "session_draft"}, - }, - []map[string]interface{}{ - {"text": "🔍 预览", "callback_data": "session_preview"}, - {"text": "❌ 丢弃", "callback_data": "session_cancel"}, - }, - }, - } - tgcore.TG("sendMessage", map[string]interface{}{ - "chat_id": chatID, - "text": fmt.Sprintf("📝 %s\n\n直接发消息开始写作\n用下方按钮管理文章属性", title), - "parse_mode": "HTML", - "reply_markup": kb, - }) -} - -func (h *Handlers) Edit(chatID int64, slug string) { - if slug == "" { - tgcore.SendMessage(chatID, "✏️ 用法:/edit slug\n先用 /list 查看 slug。", nil) - return - } - p := posts.GetPost(slug) - if p == nil { - tgcore.SendMessage(chatID, "❌ 未找到该文章。", nil) - return - } - - s := sessions.GetSession(chatID) - s.Title = p.FrontMatter.Title - s.Slug = p.FrontMatter.Slug - s.Content = p.Content - s.Categories = p.FrontMatter.Categories - s.Tags = p.FrontMatter.Tags - s.Author = p.FrontMatter.Author - if s.Author == "" { s.Author = "qunlin" } - s.Draft = p.FrontMatter.Draft - - summary := helpers.SessionSummary(s.Title, s.Categories, s.Tags, s.Content) - kb := map[string]interface{}{ - "inline_keyboard": []interface{}{ - []map[string]interface{}{ - {"text": "📂 分类", "callback_data": "session_categories"}, - {"text": "🏷 标签", "callback_data": "session_tags"}, - }, - []map[string]interface{}{ - {"text": "🚀 发布", "callback_data": "session_publish"}, - {"text": "📄 存草稿", "callback_data": "session_draft"}, - }, - []map[string]interface{}{ - {"text": "🔍 预览", "callback_data": "session_preview"}, - {"text": "❌ 丢弃", "callback_data": "session_cancel"}, - }, - }, - } - tgcore.TG("sendMessage", map[string]interface{}{ - "chat_id": chatID, - "text": fmt.Sprintf("✏️ %s\n\n%s\n\n直接发消息继续写作", s.Title, summary), - "parse_mode": "HTML", - "reply_markup": kb, - }) -} - -func (h *Handlers) Delete(chatID int64, slug string) { - if slug == "" { - tgcore.SendMessage(chatID, "🗑 用法:/delete slug\n先用 /list 查看。", nil) - return - } - p := posts.GetPost(slug) - if p == nil { - tgcore.SendMessage(chatID, "❌ 未找到该文章。", nil) - return - } - if err := recycle.MoveToRecycle(slug, p.DirPath, p.FrontMatter.Title); err != nil { - tgcore.SendMessage(chatID, "❌ "+err.Error(), nil) - return - } - tgcore.SendMessage(chatID, "🗑 "+p.FrontMatter.Title+" 已移入回收站(30天后自动清除)。", nil) -} - -func (h *Handlers) View(chatID int64, slug string) { - if slug == "" { - tgcore.SendMessage(chatID, "🔍 用法:/view slug", nil) - return - } - p := posts.GetPost(slug) - if p == nil { - tgcore.SendMessage(chatID, "❌ 未找到该文章。", nil) - return - } - draftMark := "✅" - if p.FrontMatter.Draft { draftMark = "📝" } - header := fmt.Sprintf("%s %s\n📂 %s 🏷 %s ✍️ %s 📅 %s", - draftMark, p.FrontMatter.Title, - strings.Join(p.FrontMatter.Categories, "、"), - strings.Join(p.FrontMatter.Tags, "、"), - p.FrontMatter.Author, - p.FrontMatter.Date[:10]) - - content := p.Content - if len([]rune(content)) > 3500 { - content = string([]rune(content)[:3500]) + "\n\n..." - } - tgcore.SendMessage(chatID, header+"\n\n"+content, nil) -} - - -func (h *Handlers) Title(chatID int64, arg string) { - s := sessions.GetSession(chatID) - if arg == "" { - tgcore.SendMessage(chatID, fmt.Sprintf("当前标题:%s\n修改:/title 新标题", s.Title), nil) - return - } - s.Title = arg - tgcore.SendMessage(chatID, "✅ 标题已更新:"+arg+"", nil) -} - -func (h *Handlers) Categories(chatID int64, arg string) { - s := sessions.GetSession(chatID) - if arg == "" { - tgcore.SendMessage(chatID, fmt.Sprintf("当前分类:%s\n修改:/categories 分类1,分类2", strings.Join(s.Categories, "、")), nil) - return - } - s.Categories = nil - for _, item := range strings.Split(arg, ",") { - item = strings.TrimSpace(item) - if item != "" { s.Categories = append(s.Categories, item) } - } - tgcore.SendMessage(chatID, "✅ 分类:"+strings.Join(s.Categories, "、")+"", nil) -} - -func (h *Handlers) Tags(chatID int64, arg string) { - s := sessions.GetSession(chatID) - if arg == "" { - tgcore.SendMessage(chatID, fmt.Sprintf("当前标签:%s\n修改:/tags 标签1,标签2", strings.Join(s.Tags, "、")), nil) - return - } - s.Tags = nil - for _, item := range strings.Split(arg, ",") { - item = strings.TrimSpace(item) - if item != "" { s.Tags = append(s.Tags, item) } - } - tgcore.SendMessage(chatID, "✅ 标签:"+strings.Join(s.Tags, "、")+"", nil) -} - -func (h *Handlers) Author(chatID int64, arg string) { - s := sessions.GetSession(chatID) - if arg == "" { - tgcore.SendMessage(chatID, fmt.Sprintf("当前作者:%s\n修改:/author 新作者", s.Author), nil) - return - } - s.Author = arg - tgcore.SendMessage(chatID, "✅ 作者:"+arg+"", nil) -} - -func (h *Handlers) Publish(chatID int64) { - s := sessions.GetSession(chatID) - if s.Title == "" { - tgcore.SendMessage(chatID, "❌ 请先 /new 标题 开始写作。", nil) - return - } - if err := sessions.Save(s, false); err != nil { - tgcore.SendMessage(chatID, "❌ "+err.Error(), nil) - return - } - sessions.ClearSession(chatID) - tgcore.SendMessage(chatID, "🚀 "+s.Title+" 已发布!", nil) -} - -func (h *Handlers) Draft(chatID int64) { - s := sessions.GetSession(chatID) - if s.Title == "" { - tgcore.SendMessage(chatID, "❌ 请先 /new 标题 开始写作。", nil) - return - } - if err := sessions.Save(s, true); err != nil { - tgcore.SendMessage(chatID, "❌ "+err.Error(), nil) - return - } - sessions.ClearSession(chatID) - tgcore.SendMessage(chatID, "📄 "+s.Title+" 已存为草稿。", nil) -} - -func (h *Handlers) Preview(chatID int64) { - s := sessions.GetSession(chatID) - if s.Title == "" { - tgcore.SendMessage(chatID, "❌ 请先 /new 标题 开始写作。", nil) - return - } - summary := helpers.SessionSummary(s.Title, s.Categories, s.Tags, s.Content) - preview := s.Content - if len([]rune(preview)) > 3500 { - preview = string([]rune(preview)[:3500]) + "\n\n... (内容过长)" - } - tgcore.SendMessage(chatID, summary+"\n\n"+preview, nil) -} - -func (h *Handlers) Cancel(chatID int64) { - sessions.ClearSession(chatID) - tgcore.SendMessage(chatID, "🗑 已丢弃当前写作内容。", nil) -} - - -// --- Links --- - -func (h *Handlers) Links(chatID int64, page string) { h.dispatchLinks(chatID, page) } - -func (h *Handlers) dispatchLinks(chatID int64, pageStr string) error { - page, _ := strconv.Atoi(pageStr) - if page < 1 { page = 1 } - perPage := 8 - - links, err := rssapi.GetLinks(true) - if err != nil || len(links) == 0 { - return tgcore.SendMessage(chatID, "🔗 暂无友链。", nil) - } - - totalPages := int(math.Ceil(float64(len(links)) / float64(perPage))) - if page > totalPages { page = totalPages } - start := (page - 1) * perPage - end := start + perPage - if end > len(links) { end = len(links) } - - lines := []string{fmt.Sprintf("🔗 友链 共%d个 · 第%d/%d页", len(links), page, totalPages)} - for _, l := range links[start:end] { - icon := "🌐" - if l.Hidden { icon = "🔒" } - lines = append(lines, fmt.Sprintf("%s %s\n %s", icon, l.Name, l.URL)) - } - - kb := map[string]interface{}{} - var nav []map[string]interface{} - if page > 1 { - nav = append(nav, map[string]interface{}{"text": "◀", "callback_data": fmt.Sprintf("links_%d", page-1)}) - } - if page < totalPages { - nav = append(nav, map[string]interface{}{"text": "▶", "callback_data": fmt.Sprintf("links_%d", page+1)}) - } - if len(nav) > 0 { - kb["inline_keyboard"] = []interface{}{nav} - tgcore.TG("sendMessage", map[string]interface{}{ - "chat_id": chatID, "text": strings.Join(lines, "\n"), - "parse_mode": "HTML", "reply_markup": kb, "disable_web_page_preview": true, - }) - return nil - } - return tgcore.SendMessage(chatID, strings.Join(lines, "\n"), nil) -} - -func (h *Handlers) LinkAdd(chatID int64, arg string) { - if arg == "" { - tgcore.SendMessage(chatID, "用法:/link_add url 名称", nil) - return - } - parts := strings.SplitN(arg, " ", 2) - url := parts[0] - name := "" - if len(parts) > 1 { name = parts[1] } - if err := rssapi.AddLink(rssapi.Link{URL: url, Name: name}); err != nil { - tgcore.SendMessage(chatID, "❌ "+err.Error(), nil) - return - } - tgcore.SendMessage(chatID, "✅ 友链已添加:"+url, nil) -} - -func (h *Handlers) LinkDel(chatID int64, url string) { - if url == "" { tgcore.SendMessage(chatID, "用法:/link_del url", nil); return } - if err := rssapi.DeleteLink(url); err != nil { - tgcore.SendMessage(chatID, "❌ "+err.Error(), nil) - return - } - tgcore.SendMessage(chatID, "✅ 友链已删除:"+url, nil) -} - -func (h *Handlers) LinkToggle(chatID int64, url string) { - if url == "" { tgcore.SendMessage(chatID, "用法:/link_toggle url", nil); return } - links, _ := rssapi.GetLinks(true) - var target *rssapi.Link - for i := range links { - if links[i].URL == url { target = &links[i]; break } - } - if target == nil { tgcore.SendMessage(chatID, "❌ 未找到该友链。", nil); return } - - newHidden := !target.Hidden - rssapi.UpdateLink(url, map[string]interface{}{"hidden": newHidden}) - label := "🌐 已显示" - if newHidden { label = "🔒 已隐藏" } - tgcore.SendMessage(chatID, label+": "+url, nil) -} - -// --- Feeds --- - -func (h *Handlers) Feeds(chatID int64, page string) { h.dispatchFeeds(chatID, page) } - -func (h *Handlers) dispatchFeeds(chatID int64, pageStr string) error { - page, _ := strconv.Atoi(pageStr) - if page < 1 { page = 1 } - perPage := 8 - - feeds, err := rssapi.GetFeeds() - if err != nil || len(feeds) == 0 { - return tgcore.SendMessage(chatID, "📡 暂无订阅源。", nil) - } - - totalPages := int(math.Ceil(float64(len(feeds)) / float64(perPage))) - if page > totalPages { page = totalPages } - start := (page - 1) * perPage - end := start + perPage - if end > len(feeds) { end = len(feeds) } - - lines := []string{fmt.Sprintf("📡 订阅源 共%d个 · 第%d/%d页", len(feeds), page, totalPages)} - for _, f := range feeds[start:end] { - title := f.FeedTitle - if title == "" { title = f.URL } - if len([]rune(title)) > 50 { title = string([]rune(title)[:50]) } - lines = append(lines, fmt.Sprintf("🗜 %s\n %s", title, f.URL)) - } - - kb := map[string]interface{}{} - var nav []map[string]interface{} - if page > 1 { - nav = append(nav, map[string]interface{}{"text": "◀", "callback_data": fmt.Sprintf("feeds_%d", page-1)}) - } - if page < totalPages { - nav = append(nav, map[string]interface{}{"text": "▶", "callback_data": fmt.Sprintf("feeds_%d", page+1)}) - } - if len(nav) > 0 { - kb["inline_keyboard"] = []interface{}{nav} - tgcore.TG("sendMessage", map[string]interface{}{ - "chat_id": chatID, "text": strings.Join(lines, "\n"), - "parse_mode": "HTML", "reply_markup": kb, "disable_web_page_preview": true, - }) - return nil - } - return tgcore.SendMessage(chatID, strings.Join(lines, "\n"), nil) -} - -func (h *Handlers) FeedAdd(chatID int64, arg string) { - if arg == "" { tgcore.SendMessage(chatID, "用法:/feed_add url", nil); return } - parts := strings.SplitN(arg, " ", 2) - url := parts[0] - title := "" - if len(parts) > 1 { title = parts[1] } - if err := rssapi.AddFeed(url, title); err != nil { - tgcore.SendMessage(chatID, "❌ "+err.Error(), nil) - return - } - tgcore.SendMessage(chatID, "✅ 订阅源已添加:"+url, nil) -} - -func (h *Handlers) FeedDel(chatID int64, url string) { - if url == "" { tgcore.SendMessage(chatID, "用法:/feed_del url", nil); return } - if err := rssapi.DeleteFeed(url); err != nil { - tgcore.SendMessage(chatID, "❌ "+err.Error(), nil) - return - } - tgcore.SendMessage(chatID, "✅ 订阅源已删除:"+url, nil) -} - -// --- Stats --- - -func (h *Handlers) Stats(chatID int64) { - 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) } - - tgcore.SendMessage(chatID, fmt.Sprintf("📊 网站数据\n\n📝 文章:%d 篇\n🔗 友链:%d 个\n📡 订阅源:%d 个\n\n🌐 usj.cc", postCount, linkCount, feedCount), nil) -} - - -// --- EnsureWriteReady --- - -func EnsureWriteReady(chatID int64, writePort int) bool { - s := writeserver.GetWriteStatus() - running, _ := s["running"].(bool) - starting, _ := s["starting"].(bool) - - if running { - writeserver.StartHugoIfNeeded() - return true - } - if starting { - tgcore.SendMessage(chatID, "⌛ Write 正在启动...", nil) - if helpers.WaitForReady(writePort, 0) { - writeserver.SetWriteReady(true) - writeserver.StartHugoIfNeeded() - return true - } - return false - } - - result := writeserver.StartWriteServer() - if started, ok := result["started"].(bool); !ok || !started { - return false - } - tgcore.SendMessage(chatID, "⌛ 正在启动 Write...", nil) - if helpers.WaitForReady(writePort, 0) { - writeserver.SetWriteReady(true) - writeserver.StartHugoIfNeeded() - return true - } - return false -} - -// --- Callback handlers for inline buttons --- - -func (h *Handlers) SessionCallback(chatID int64, action string) { - switch action { - case "categories": - s := sessions.GetSession(chatID) - tgcore.SendMessage(chatID, fmt.Sprintf("📂 当前分类:%s\n\n请发送新的分类(逗号分隔):", strings.Join(s.Categories, "、")), nil) - case "tags": - s := sessions.GetSession(chatID) - tgcore.SendMessage(chatID, fmt.Sprintf("🏷 当前标签:%s\n\n请发送新的标签(逗号分隔):", strings.Join(s.Tags, "、")), nil) - case "publish": - h.Publish(chatID) - case "draft": - h.Draft(chatID) - case "preview": - h.Preview(chatID) - case "cancel": - h.Cancel(chatID) - } -} - -func (h *Handlers) MenuCallback(chatID int64, action string) { - switch action { - case "new": - tgcore.SendMessage(chatID, "📝 请输入:/new 文章标题", nil) - case "help": - h.Help(chatID) - case "stats": - h.Stats(chatID) - } -} diff --git a/bot-go/internal/writeserver/writeserver.go b/bot-go/internal/writeserver/writeserver.go deleted file mode 100644 index 2fb8aa51..00000000 --- a/bot-go/internal/writeserver/writeserver.go +++ /dev/null @@ -1,156 +0,0 @@ -package writeserver - -import ( - "fmt" - "net" - "os/exec" - "path/filepath" - "sync" - "time" -) - -var ( - mu sync.Mutex - writeProc *exec.Cmd - writeReady bool - hugoProc *exec.Cmd - WritePort int - WriteDir string -) - -func Init(writePort int, writeDir string) { - WritePort = writePort - WriteDir = writeDir -} - -func StartWriteServer() map[string]interface{} { - mu.Lock() - defer mu.Unlock() - - if writeProc != nil { - if writeReady { - return map[string]interface{}{"started": false, "reason": "already_running"} - } - return map[string]interface{}{"started": false, "reason": "starting"} - } - - writeReady = false - writeProc = exec.Command(filepath.Join(WriteDir, "writeserver.exe")) - writeProc.Dir = WriteDir - - if err := writeProc.Start(); err != nil { - writeProc = nil - return map[string]interface{}{"started": false, "reason": err.Error()} - } - - writeReady = true - - go func() { - err := writeProc.Wait() - mu.Lock() - defer mu.Unlock() - fmt.Printf("[write] exited: %v\n", err) - writeProc = nil - writeReady = false - }() - - return map[string]interface{}{"started": true} -} - -func StopWriteServer() bool { - mu.Lock() - defer mu.Unlock() - - if hugoProc != nil { - killProcessTree(hugoProc) - hugoProc = nil - } - - if writeProc == nil { - return false - } - - killProcessTree(writeProc) - writeProc = nil - writeReady = false - return true -} - -func killProcessTree(cmd *exec.Cmd) { - if cmd == nil || cmd.Process == nil { - return - } - // On Windows, use taskkill /T to kill process tree - pid := cmd.Process.Pid - killCmd := exec.Command("taskkill", "/PID", fmt.Sprintf("%d", pid), "/T", "/F") - killCmd.Run() - cmd.Process.Kill() -} - -func StartHugoIfNeeded() { - mu.Lock() - defer mu.Unlock() - - if hugoProc != nil { - return - } - - hugoDir := WriteDir + "/.." - hugoProc = exec.Command("hugo", "server", "--bind", "0.0.0.0", "--port", "1313", "--disableFastRender") - hugoProc.Dir = hugoDir - - if err := hugoProc.Start(); err != nil { - fmt.Printf("[hugo] start error: %v\n", err) - hugoProc = nil - return - } - - fmt.Println("[hugo] started on 0.0.0.0:1313") - - go func() { - err := hugoProc.Wait() - mu.Lock() - defer mu.Unlock() - fmt.Printf("[hugo] exited: %v\n", err) - hugoProc = nil - }() -} - -func GetWriteStatus() map[string]interface{} { - mu.Lock() - running := writeProc != nil && writeReady - starting := writeProc != nil && !writeReady - mu.Unlock() - - // Also check via TCP if we are not tracking a process - if !running && !starting { - conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", WritePort), 2*time.Second) - if err == nil { - conn.Close() - running = true - } - } - - return map[string]interface{}{ - "running": running, - "starting": starting, - } -} - -func SetWriteReady(ready bool) { - mu.Lock() - defer mu.Unlock() - writeReady = ready -} - -func IsWriteReady() bool { - mu.Lock() - defer mu.Unlock() - return writeReady -} - -func IsWriteRunning() bool { - mu.Lock() - defer mu.Unlock() - return writeProc != nil -} diff --git a/bot-go/start_bot.bat b/bot-go/start_bot.bat deleted file mode 100644 index 20305000..00000000 --- a/bot-go/start_bot.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -title WriteBot -cd /d E:\GitHub\blog\bot-go -echo [%date% %time%] WriteBot starting... -writebot.exe diff --git a/write/.recycle/meta/2026-06-01-深圳真的好热.json b/write/.recycle/meta/2026-06-01-深圳真的好热.json new file mode 100644 index 00000000..07465d5e --- /dev/null +++ b/write/.recycle/meta/2026-06-01-深圳真的好热.json @@ -0,0 +1,7 @@ +{ + "slug": "2026-06-01-深圳真的好热", + "title": "深圳真的好热", + "deletedAt": "2026-06-01T16:21:45.062Z", + "originalPath": "E:\\GitHub\\blog\\content\\post\\2026\\2026-06-01-深圳真的好热", + "expiresAt": "2026-07-01T16:21:45.062Z" +} \ No newline at end of file diff --git a/write/.recycle/meta/2026-06-01-深圳真的热.json b/write/.recycle/meta/2026-06-01-深圳真的热.json new file mode 100644 index 00000000..49bd22d7 --- /dev/null +++ b/write/.recycle/meta/2026-06-01-深圳真的热.json @@ -0,0 +1,7 @@ +{ + "slug": "2026-06-01-深圳真的热", + "title": "深圳真的热", + "deletedAt": "2026-06-01T14:59:57.582Z", + "originalPath": "E:\\GitHub\\blog\\content\\post\\2026\\2026-06-01-深圳真的热", + "expiresAt": "2026-07-01T14:59:57.582Z" +} \ No newline at end of file diff --git a/write/.recycle/meta/202605311204.json b/write/.recycle/meta/202605311204.json new file mode 100644 index 00000000..75f5d903 --- /dev/null +++ b/write/.recycle/meta/202605311204.json @@ -0,0 +1,7 @@ +{ + "slug": "202605311204", + "title": "周末我把手机系统降级了一个大版本", + "deletedAt": "2026-06-01T15:06:56.071Z", + "originalPath": "E:\\GitHub\\blog\\content\\post\\2026\\2026-05-31-周末我把手机系统降级了一个大版本", + "expiresAt": "2026-07-01T15:06:56.071Z" +} \ No newline at end of file diff --git a/write/.recycle/posts/2026-06-01-深圳真的好热/index.md b/write/.recycle/posts/2026-06-01-深圳真的好热/index.md new file mode 100644 index 00000000..4b83c075 --- /dev/null +++ b/write/.recycle/posts/2026-06-01-深圳真的好热/index.md @@ -0,0 +1,24 @@ +--- +title: 深圳真的好热 +date: '2026-06-01T00:00:00.000Z' +slug: 2026-06-01-深圳真的好热 +draft: false +categories: + - 技术 +tags: + - 测试 +author: 小赵 +layout: post +--- +你好 + + +![image](1780326640500-AgACAgQA.jpg) + + +今天的天气不错 + +你觉得呢 + +哈哈哈哈 + diff --git a/write/.recycle/posts/2026-06-01-深圳真的热/1780325877248-AgACAgQA.jpg b/write/.recycle/posts/2026-06-01-深圳真的热/1780325877248-AgACAgQA.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f58c5462710693cde2125a847cddba7f74eef885 GIT binary patch literal 166422 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%0A;m5On%XmKRoMkwz_EEF}3cdox9&1|b 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 Tc)|4)sg=a!mYCWO{%`F6+%3S0 literal 0 HcmV?d00001 diff --git a/write/.recycle/posts/2026-06-01-深圳真的热/index.md b/write/.recycle/posts/2026-06-01-深圳真的热/index.md new file mode 100644 index 00000000..9a8f4b37 --- /dev/null +++ b/write/.recycle/posts/2026-06-01-深圳真的热/index.md @@ -0,0 +1,20 @@ +--- +title: 深圳真的热 +date: '2026-06-01' +slug: 2026-06-01-深圳真的热 +draft: false +categories: + - 技术 +tags: + - 深圳 +author: 小赵 +layout: post +--- +好热啊 +超级热 + +![image](1780325877248-AgACAgQA.jpg) + +哈哈哈 +此文章使用 telegram 撰写 +哈哈哈 diff --git a/write/.recycle/posts/202605311204/index.md b/write/.recycle/posts/202605311204/index.md new file mode 100644 index 00000000..bf1956ba --- /dev/null +++ b/write/.recycle/posts/202605311204/index.md @@ -0,0 +1,35 @@ +--- +title: 周末我把手机系统降级了一个大版本 +date: '2026-05-31T00:00:00.000Z' +slug: '202605311204' +draft: false +categories: + - 生活 +tags: + - OPPO;ColorOS +author: 小赵 +layout: post +ai_comment: >- + 从技术角度看,ColorOS 16 的 AI + 功能堆砌是典型的版本迭代陷阱。厂商为追求差异化强行注入AI模块,却忽略了系统层级的功耗平衡优化。建议关闭非必要的AI服务,这就像给过度兴奋的CPU降频——有时候减法比加法更能体现工程智慧。续航劣化可能源于AI进程的常驻唤醒,等待后续热修复补丁或许是更理性的选择。 +--- +从 iPhone 16E 换到 OPPO Find X8s 之后,它就一直是主力机,没再变过。 + +说实话,OPPO 这套系统我用着挺满意,功能到位,稳定性也没得挑。 + +之所以彻底放弃 iPhone 当主力,理由其实很充分很现实:微信双开和通话录音。这两样,iPhone 都给不了,而我的工作又偏偏离不开。 + +中间也动过双持的念头,两台手机分工协作,听起来挺美。但真用起来,麻烦比便利多。光是琢磨哪个 App 装在哪台手机上,就够头疼的。用了一阵子发现,我根本没有那份“区分使用”的需求,反而更想要一个干净利落的统一。 + +手机到手时,预装的是 ColorOS 15。我就是被它的宣传语打动的——“流畅稳定,德芙一样丝滑”。用下来,确实名副其实,那种跟手和舒心,让我觉得自己选对了。 + +所以 ColorOS 16 一推送,我毫不犹豫就更了。我的手机一直是默认自动更新,总相信新的一定更好。 + +但很快我就发现,厂商的劲头,好像全往一个方向使了:AI。 + +到处都是 AI。系统里那些曾经让我安心的“本分”感,开始变了味。 + +带给我实际的体验就是,耗电和发热更厉害了,手机续航也来到了一天两充,看着手机里的电池健康,实际上还是百分之百 + + + diff --git a/write/docs/bot_design/design.md b/write/docs/bot_design/design.md new file mode 100644 index 00000000..d3983e89 --- /dev/null +++ b/write/docs/bot_design/design.md @@ -0,0 +1,914 @@ +# WriteBot 模块设计文档 + +## 一、架构 + +Bot 作为 Next.js 的 sidecar 模块运行,通过 `instrumentation.ts` 在服务端启动时自动拉起。 + +``` +write/src/lib/bot/ +├── index.ts # 启动入口 +├── config.ts # Bot 专用配置 +├── tg.ts # Telegram API 封装 +├── sessions.ts # 写作会话状态管理 +├── handlers.ts # 全部命令处理器 +├── poll.ts # 长轮询循环(带指数退避) +└── helpers.ts # 工具函数 +``` + +Bot 不再通过 HTTP 调 write API,直接调用 `src/lib/posts.ts`、`src/lib/rssapi.ts` 等 lib 函数。 + +--- + +## 二、交互方式 + +### 2.1 Telegram 原生组件使用规则 + +| 组件 | 用途 | 使用场景 | +|------|------|---------| +| ReplyKeyboard(底部键盘) | 常驻操作按钮 | 所有文本回复都带,分主键盘和写作键盘两套 | +| InlineKeyboard(消息内按钮) | 列表操作、确认、分页 | 文章列表编辑/删除、删除确认、分页导航、打开编辑器 | +| ForceReply(强制回复) | 引导输入 | 新建标题、设置分类/标签、添加友链/订阅源 | +| Web App | 内置浏览器打开编辑器 | /start 时提供 | + +**关键规则:ReplyKeyboard 和 InlineKeyboard 不能混在同一个 reply_markup 里。** + +### 2.2 ReplyKeyboard 布局 + +主键盘(非写作状态): +``` +[/new 写文章] [/list 文章列表] +[/links 友链] [/feeds 订阅] +[/stats 数据] [/help 帮助] +``` + +写作键盘(写作会话中): +``` +[/preview 预览] [/publish 发布] +[/draft 草稿] [/cancel 取消] +[/title 标题] [/categories 分类] +``` + +--- + +## 三、命令清单 + +### 3.1 系统命令 + +| 命令 | 参数 | 说明 | +|------|------|------| +| `/start` | 无 | 启动 Hugo,显示地址,发 Web App 编辑器按钮 | +| `/stop` | 无 | 停止 Hugo | +| `/status` | 无 | 显示 Hugo 运行状态和地址 | +| `/ip` | 无 | 显示局域网 IP | +| `/help` | 无 | 显示命令帮助(根据是否在写作中显示不同内容) | + +### 3.2 文章命令 + +| 命令 | 参数 | 说明 | +|------|------|------| +| `/new` | 标题(可选) | 新建文章,见写作流程 | +| `/edit` | slug | 编辑已有文章,见编辑流程 | +| `/delete` | slug | 删除文章(二次确认),移入回收站 | +| `/list` | 页码(可选) | 文章列表,纯文本 + 分页 | +| `/view` | slug | 显示预览链接 | +| `/preview` | 无 | 预览当前写作内容 | +| `/publish` | 无 | 保存到本地(draft: false),不触发 git push | +| `/draft` | 无 | 保存到本地(draft: true) | +| `/deploy` | 无 | Git add → commit → pull --rebase → push | +| `/sync` | 无 | Git pull --rebase,同步远程 | +| `/undo` | 无 | 撤销最后一次追加的内容,自动预览 | +| `/cancel` | 无 | 丢弃当前写作会话 | +| `/title` | 新标题 | 修改文章标题(ForceReply 引导) | +| `/categories` | 分类 | 设置分类(逗号分隔) | +| `/tags` | 标签 | 设置标签(逗号分隔) | +| `/author` | 作者名 | 修改作者 | + +### 3.3 友链命令 + +| 命令 | 参数 | 说明 | +|------|------|------| +| `/links` | 页码(可选) | 友链列表,纯文本 + 分页 + 命令提示 | +| `/link_add` | url 名称 | 添加友链(ForceReply 引导) | +| `/link_del` | url | 删除友链 | +| `/link_toggle` | url | 切换显示/隐藏 | + +### 3.4 订阅命令 + +| 命令 | 参数 | 说明 | +|------|------|------| +| `/feeds` | 页码(可选) | 订阅源列表,纯文本 + 分页 + 命令提示 | +| `/feed_add` | url | 添加订阅源(ForceReply 引导) | +| `/feed_del` | url | 删除订阅源 | + +### 3.5 数据命令 + +| 命令 | 参数 | 说明 | +|------|------|------| +| `/stats` | 无 | 显示文章数、友链数、订阅源数 | + +--- + +## 四、核心流程 + +### 4.1 新建文章 `/new` + +``` +用户: /new 标题 + ↓ (如果没有标题,ForceReply 引导输入) +Bot: 📝 开始写:《标题》 + 📂 设置分类(可选) + [跳过] [设置分类] ← InlineKeyboard + ↓ +用户: 点 [跳过] → 直接进写作 + 或: 点 [设置分类] → ForceReply 引导输入分类 + → 然后同样问标签 [跳过] [设置标签] + ↓ +Bot: 切换写作键盘 + "直接发消息写正文,完成后点「发布」" + ↓ +用户: 发消息 → 追加到 content +用户: 发照片 → 下载到文章文件夹,追加 markdown 引用 +用户: /undo → 撤销最后一次追加,自动预览 + ↓ +用户: /publish + ↓ +Bot: 保存到本地文件(draft: false) + "✅ 已保存,发 /deploy 推送到线上" + ↓ +用户: /deploy + ↓ +Bot: git add → commit → pull --rebase → push + "🚀 已发布并部署!" +``` + +Session 初始状态: +```typescript +{ + title: "标题", + slug: "2026-06-01-标题", + content: "", + categories: [], + tags: [], + author: "小赵", // DEFAULT_AUTHOR 环境变量可覆盖 + draft: true, + chunks: [], // 用于 /undo,记录每次追加的内容 + mode: "write", // write | edit +} +``` + +### 4.2 编辑文章 `/edit`(方案 B) + +``` +用户: /edit slug + ↓ +Bot: 加载文章,显示原文内容(分条,每条 ≤3800 字) + 清空 session.content(保留 title、slug、categories、tags、author) + 切换写作键盘 + "原文已显示,现在重新写正文,写完点「发布」" + ↓ +用户: 发消息 → 追加到新 content(和 /new 一样) + ↓ +后续流程同新建 +``` + +关键区别:`/new` 的 slug 是新生成的,`/edit` 保留原文章的 slug。保存时调用 `updatePost` 而不是 `createPost`。 + +### 4.3 删除文章 `/delete` + +``` +用户: /delete slug + ↓ +Bot: "确认删除《标题》?" + [确认删除] [取消] ← InlineKeyboard + ↓ +用户: 点 [确认删除] + → moveToRecycle(slug, dirPath, title) + → "✂ 已移入回收站:《标题》\n30天内可恢复。" + ↓ +用户: 点 [取消] + → "已取消" +``` + +poll.ts callback 处理新增 `confirm_del` 前缀: +```typescript +const CALLBACK_PREFIXES = { + edit: "edit", + confirm_del: "delete", // 确认删除 + cancel_del: "noop", // 取消删除 + list: "list", + links: "links", + feeds: "feeds", + skip_cat: "skip_categories", // 跳过分类 + skip_tag: "skip_tags", // 跳过标签 +}; +``` + +### 4.4 文章列表 `/list` + +纯文本显示,无 InlineKeyboard 操作按钮: +``` +📖 文章列表 共 46 篇 · 第 1/10 页 + +我的第一篇文章 + 2026-06-01-my-first-post +另一篇文章 📝(草稿) + 2026-05-28-another-post + +◀ 上一页 下一页 ▶ ← InlineKeyboard(仅分页) +``` + +用户通过 slug 操作:`/edit slug`、`/delete slug`、`/view slug`。 + +### 4.5 友链列表 `/links` + +纯文本 + 分页 + 命令提示: +``` +🔗 友链 共 46 个 · 第 1/10 页 + +🌐 强仔博客 +🌐 七栀 +🔒 若志随笔(已隐藏) + +操作命令: +/link_add url 名称 — 添加 +/link_del url — 删除 +/link_toggle url — 显示/隐藏 + +◀ 上一页 下一页 ▶ +``` + +### 4.6 订阅列表 `/feeds` + +同友链,纯文本 + 分页 + 命令提示。 + +--- + +## 五、图片处理(Hugo Page Bundle) + +### 5.1 存储策略 + +图片存到文章自己的文件夹里,符合 Hugo page bundle 规范: + +``` +content/post/2026/2026-06-01-文章标题/ +├── index.md +├── photo1.jpg +└── photo2.jpg +``` + +正文引用:`![描述](photo1.jpg)`(相对路径) + +### 5.2 实现逻辑 + +写作中收到照片时: +1. 根据 session 的 slug 计算文章文件夹路径 + - slug: `2026-06-01-文章标题` + - 文件夹: `content/post/2026/2026-06-01-文章标题/` +2. 文件夹不存在则创建 +3. 下载图片到文件夹内 +4. content 追加 `![caption](filename.jpg)` + +保存时(draft/publish): +1. index.md 写到同一个文件夹 +2. 图片已经在里面,无需移动 + +取消时: +1. 文件夹里只有图片没有 index.md +2. Hugo 不会渲染,无害 + +--- + +## 六、撤销 `/undo` + +### 6.1 实现机制 + +Session 新增 `chunks: string[]` 字段: + +```typescript +interface Session { + // ... 其他字段 + chunks: string[]; // 每次追加的内容记录 +} +``` + +每次用户发消息追加到 content 时,同时 push 到 chunks: +```typescript +s.chunks.push(fullText + "\n"); +s.content += fullText + "\n"; +``` + +### 6.2 `/undo` 流程 + +``` +用户: /undo + ↓ +如果 chunks 为空: "没有可撤销的内容" + ↓ +否则: + lastChunk = chunks.pop() + content = content 去掉 lastChunk + → 自动调用 preview 逻辑,显示当前文章内容 + → "↩ 已撤销最后一段(上方为当前内容)" +``` + +--- + +## 七、发布与部署(分离设计) + +### 7.1 与网页端保持一致 + +| 操作 | 命令 | 效果 | +|------|------|------| +| 保存草稿 | `/draft` | 保存到本地,draft: true | +| 发布 | `/publish` | 保存到本地,draft: false,不触发 git | +| 部署 | `/deploy` | git add → commit → pull --rebase → push | +| 同步 | `/sync` | git pull --rebase,拉取远程最新 | + +### 7.2 Git 冲突处理 + +`/deploy` 执行流程: +1. `git add content/post static` +2. `git diff --cached --quiet` → 有变更则 `git commit` +3. `git pull --rebase --autostash origin main` + - 成功 → 继续 + - 冲突 → `git rebase --abort` → 回复"远程有冲突,已自动放弃合并。请先 /sync 同步,手动解决后再 deploy" +4. `git push` + +--- + +## 八、配置 + +### 8.1 环境变量(write/.env) + +```env +# Bot 配置 +TG_BOT_TOKEN=xxx +TG_ALLOWED_CHAT_IDS=xxx # 留空允许所有人 +PREFER_IFACE=WLAN +DEFAULT_AUTHOR=小赵 # 新增:默认作者名 +``` + +### 8.2 config.ts 新增 + +```typescript +export const DEFAULT_AUTHOR = process.env.DEFAULT_AUTHOR || "小赵"; +``` + +--- + +## 九、命令解析 + +### 9.1 ReplyKeyboard 按钮文字处理 + +按钮发出的文字如 `/new 写文章`,解析规则: +1. `split("@")[0]` 去掉 bot mention +2. `split(/\s+/)` 拆词 +3. 第一个词去掉 `/` 前缀作为命令名 +4. 其余词作为参数 + +`/new 写文章` → 命令 `new`,参数 `写文章` +但 `new` 命令的参数是标题,所以实际行为:`/new 写文章` 会创建一篇标题为"写文章"的文章。 + +需要特殊处理:如果参数和命令的中文描述匹配(如 `new` 的参数是 `写文章`),则忽略该参数。 +或者更简单的方案:ReplyKeyboard 按钮只发命令,不带中文。 + +**最终方案:ReplyKeyboard 按钮文字改为纯命令:** +``` +[/new] [/list] +[/links] [/feeds] +[/stats] [/help] +``` + +### 9.2 ForceReply 回复识别 + +poll.ts 通过 `reply_to_message.text` 匹配关键词来识别 ForceReply 回复: + +| 关键词 | 路由到 | +|--------|--------| +| "请输入文章标题" | `handleCommand(chatId, "new", text)` | +| "请输入新标题" | `handleCommand(chatId, "title", text)` | +| "请输入友链信息" | `handleCommand(chatId, "link_add", text)` | +| "请输入订阅源 URL" | `handleCommand(chatId, "feed_add", text)` | +| "请输入分类" | `handleCommand(chatId, "categories", text)` | +| "请输入标签" | `handleCommand(chatId, "tags", text)` | + +--- + +## 十、会话状态机 + +Session 有一个 `state` 字段,控制消息处理行为: + +``` +idle → 非写作状态,普通命令处理 +writing_title → 等待 ForceReply 标题输入 +writing_cat → 等待 ForceReply 分类输入(或 InlineKeyboard 跳过) +writing_tag → 等待 ForceReply 标签输入(或 InlineKeyboard 跳过) +writing → 写作中,消息追加到 content +confirm_del → 等待删除确认(InlineKeyboard) +``` + +状态转换: +``` +idle → ( /new ) → writing_title → ( 输入标题 ) → writing_cat + → ( 跳过/输入 ) → writing_tag → ( 跳过/输入 ) → writing → idle + +idle → ( /edit slug ) → 显示原文 → 清空 content → writing → idle + +idle → ( /delete slug ) → confirm_del → ( 确认 ) → idle + → ( 取消 ) → idle +``` + + + +--- + +## 十一、友链模块 + +### 11.1 命令清单 + +| 命令 | 参数 | 说明 | +|------|------|------| +| `/links` | 页码(可选) | 友链列表,纯文本 + 分页 | +| `/link_add` | url 名称 | ForceReply 引导输入,添加友链 | +| `/link_edit` | url | ForceReply 引导输入新名称,通过 oldUrl 编辑 | +| `/link_del` | url | 删除友链 | +| `/link_toggle` | url | 切换显示/隐藏 | + +### 11.2 列表显示格式 + +``` +🔗 友链 共 46 个 · 第 1/10 页 + +🌐 吾柯 https://blog.keepke.com +🌐 强仔博客 https://loseu.cc +🔒 七栀 https://blog.qydzz.cn + +操作:/link_add /link_edit /link_del /link_toggle + +◀ 上一页 下一页 ▶ +``` + +每条显示:图标(🌐 正常 / 🔒 隐藏)+ 名称 + URL。 +每页 10 条。底部 InlineKeyboard 仅分页按钮。 +底部文字提示可用操作命令。 + +### 11.3 `/link_edit` 流程 + +``` +用户: /link_edit url + ↓ +Bot: 查找该友链,显示当前信息 + "请输入新名称(留空保持不变):" ← ForceReply + ↓ +用户: 输入新名称(或发"跳过") + ↓ +Bot: 调用 POST /api/links { oldUrl, name: newName } + "✅ 已更新:新名称" +``` + +### 11.4 API 调用 + +- 列表:`GET /api/links?all=1` +- 添加:`POST /api/links` body: `{ name, url }` +- 编辑:`POST /api/links` body: `{ oldUrl, name, ... }` +- 删除:`DELETE /api/links?url=xxx` +- 切换:先 GET 获取 hidden 状态,再 POST `{ url, hidden: !current }` + +--- + +## 十二、订阅模块 + +### 12.1 命令清单 + +| 命令 | 参数 | 说明 | +|------|------|------| +| `/feeds` | 页码(可选) | 订阅源列表,纯文本 + 分页 | +| `/feed_add` | url 名称 | ForceReply 引导输入 URL 和名称 | +| `/feed_edit` | url | ForceReply 引导编辑,通过 oldUrl | +| `/feed_del` | url | 删除订阅源 | +| `/feed_health` | 无 | 检测全部订阅源健康状态 | + +### 12.2 列表显示格式 + +``` +📡 订阅源 共 52 个 · 第 1/10 页 + +📰 吾柯 https://blog.keepke.com +📰 强仔博客 https://loseu.cc + +操作:/feed_add /feed_edit /feed_del /feed_health + +◀ 上一页 下一页 ▶ +``` + +每页 10 条。 + +### 12.3 `/feed_health` 流程 + +调用 `GET /api/health`(不传 url 参数,检测全部订阅源)。 + +``` +用户: /feed_health + ↓ +Bot: "⏳ 正在检测 52 个订阅源..." + ↓ +Bot: + 📊 订阅源健康报告 + ✅ 存活:49 ❌ 异常:3 + + ❌ 异常站点: + 🔴 dead-blog.com — timeout + 🔴 another.com — 500 + 🔴 third.com — 连接失败 +``` + +只列出异常站点,正常的不逐一显示。 + +### 12.4 `/feed_edit` 流程 + +同友链编辑,通过 oldUrl 参数调用 POST /api/feeds。 + +### 12.5 API 调用 + +- 列表:`GET /api/feeds` +- 添加:`POST /api/feeds` body: `{ url, feedTitle }` +- 编辑:`POST /api/feeds` body: `{ oldUrl, url, feedTitle }` +- 删除:`DELETE /api/feeds?url=xxx` +- 健康:`GET /api/health` + +--- + +## 十三、新增 `/read` 命令 + +利用 API 的 `GET /api/articles` 接口,获取友链博客的最新文章。 + +### 13.1 命令 + +| 命令 | 参数 | 说明 | +|------|------|------| +| `/read` | 数量(可选,默认 10) | 友链最新文章 | + +### 13.2 显示格式 + +``` +📰 友链最新文章(10 篇) + +吾柯 — 如何在 Hugo 中实现友链 + https://blog.keepke.com/xxx + +强仔博客 — 2026 年度总结 + https://loseu.cc/xxx + +七栀 — 我的第一次旅行 + https://blog.qydzz.cn/xxx +``` + +每条显示:博客名 — 文章标题 + 链接。 +Telegram 自动将 URL 转为可点击链接。 + +### 13.3 API 调用 + +`GET /api/articles?limit=10` + +响应结构: +```json +{ + "total": 380, + "articles": [ + { + "title": "...", + "link": "...", + "pubDate": "...", + "feedName": "吾柯", + "siteUrl": "...", + "favicon": "...", + "author": "..." + } + ] +} +``` + +--- + +## 十四、完整命令注册列表 + +更新 `setMyCommands` 注册的命令: + +```typescript +commands: [ + { command: "new", description: "写一篇文章" }, + { command: "edit", description: "编辑文章" }, + { command: "delete", description: "删除文章" }, + { command: "list", description: "文章列表" }, + { command: "publish", description: "保存发布" }, + { command: "draft", description: "保存草稿" }, + { command: "deploy", description: "推送到线上" }, + { command: "sync", description: "同步远程" }, + { command: "undo", description: "撤销最后一段" }, + { command: "preview", description: "预览" }, + { command: "links", description: "友链管理" }, + { command: "link_add", description: "添加友链" }, + { command: "link_edit", description: "编辑友链" }, + { command: "link_del", description: "删除友链" }, + { command: "link_toggle", description: "显示/隐藏友链" }, + { command: "feeds", description: "订阅管理" }, + { command: "feed_add", description: "添加订阅源" }, + { command: "feed_edit", description: "编辑订阅源" }, + { command: "feed_del", description: "删除订阅源" }, + { command: "feed_health", description: "订阅源健康检查" }, + { command: "read", description: "友链最新文章" }, + { command: "stats", description: "网站数据" }, + { command: "help", description: "帮助" }, +] +``` + +--- + +## 十五、其他可考虑的功能 + +### 15.1 `/random` — 随机图片 + +利用 API 的 `GET /api/random-image` 接口,可以做一个随机壁纸/图片功能。 +适合在聊天中发一张随机图片,增加趣味性。优先级低。 + +### 15.2 `/greeting` — 问候语 + +利用 API 的 `GET /api/ai/greeting` 接口,获取 AI 生成的问候语。 +适合每天早上发一条问候。可以和定时任务结合。优先级低。 + +### 15.3 公众号同步 `/wechat` + +利用 API 的 `POST /api/wechat-material` 接口,在 bot 中直接把文章同步到微信公众号草稿箱。 +当前 write 已有公众号页面,bot 可以加一个 `/wechat slug` 命令。 +优先级中——如果经常在手机上写文章,同步到公众号是个高频操作。 + +### 15.4 定时任务 + +利用 Cowork 的 scheduled-tasks 功能,可以设置: +- 每天早上 9 点发一条问候语 + 友链最新文章摘要 +- 每周检测一次订阅源健康状态 +- 每天自动部署未发布的草稿(可选) + +这些功能当前不急,后续按需添加。 + + +--- + +## 十六、确认的补充项 + +### 16.1 写作键盘更新 + +加入 `/undo` 和 `/deploy`: + +``` +[/preview] [/undo] +[/publish] [/deploy] +[/draft] [/cancel] +[/title] [/categories] +``` + +### 16.2 主键盘更新 + +按钮文字改为纯命令: + +``` +[/new] [/list] +[/links] [/feeds] +[/stats] [/help] +``` + +### 16.3 /stats 增强 + +显示本地数据 + API 数据: + +``` +📊 网站数据 + +📝 本地文章:128 篇 +🔗 友链:46 个 +📡 订阅源:52 个 + +📰 友链文章:380 篇(来自 52 个博客) +🌐 usj.cc +``` + +### 16.4 API 不可用降级 + +所有调用 `api.usj.cc` 的操作统一处理: +- 超时:10 秒 +- 失败回复:"⚠️ API 暂时不可用,请稍后再试" +- 涉及命令:/links、/link_add、/link_edit、/link_del、/link_toggle、/feeds、/feed_add、/feed_edit、/feed_del、/feed_health、/read、/stats(API 部分) + +--- + +## 十七、完整的 poll.ts 命令解析更新 + +### 17.1 ARG_COMMANDS 更新 + +```typescript +const ARG_COMMANDS = new Set([ + "new", "title", "categories", "tags", "author", + "edit", "view", "delete", + "link_add", "link_edit", "link_del", "link_toggle", + "feed_add", "feed_edit", "feed_del", + "list", "links", "feeds", "read", +]); +``` + +### 17.2 CALLBACK_PREFIXES 更新 + +```typescript +const CALLBACK_PREFIXES: Record = { + edit: "edit", + confirm_del: "delete", + cancel_del: "noop", + list: "list", + links: "links", + feeds: "feeds", + skip_cat: "skip_categories", + skip_tag: "skip_tags", +}; +``` + +### 17.3 ForceReply 关键词更新 + +```typescript +if (original.includes("请输入文章标题")) → new +if (original.includes("请输入新标题")) → title +if (original.includes("请输入分类")) → categories +if (original.includes("请输入标签")) → tags +if (original.includes("请输入友链信息")) → link_add +if (original.includes("请输入新名称")) → link_edit +if (original.includes("请输入订阅源 URL")) → feed_add +if (original.includes("请输入新的订阅信息")) → feed_edit +``` + +### 17.4 InlineKeyboard callback 处理 + +新增 `confirm_del` 和 `cancel_del` 处理: +- `confirm_del_slug` → 执行删除 +- `cancel_del_slug` → 取消,回复"已取消" + +新增 `skip_cat` 和 `skip_tag` 处理: +- `skip_cat` → 跳过分类设置 +- `skip_tag` → 跳过标签设置 + +--- + +## 十八、sessions.ts 更新 + +### 18.1 Session 接口 + +```typescript +interface Session { + title: string; + slug: string; + content: string; + categories: string[]; + tags: string[]; + author: string; + draft: boolean; + chunks: string[]; // 每次追加的内容,用于 /undo + originalSlug?: string; // /edit 时保留原 slug +} +``` + +### 18.2 默认作者 + +从 `DEFAULT_AUTHOR` 环境变量读取,默认 "小赵"。 + +--- + +## 十九、helpers.ts 更新 + +### 19.1 makeSlug 保持不变 + +`2026-06-01-标题拼音` + +### 19.2 新增 getArticleDirPath + +根据 slug 计算文章文件夹路径: + +```typescript +function getArticleDirPath(slug: string): string { + // slug: "2026-06-01-文章标题" + // 从 slug 提取年份前四位 + const year = slug.slice(0, 4); + return path.join(CONTENT_DIR, year, slug); +} +``` + +### 19.3 新增 truncateText + +截断文本到指定长度,用于 Telegram 消息: + +```typescript +function truncateText(text: string, maxLen: number): string { + if (text.length <= maxLen) return text; + return text.slice(0, maxLen) + "\n\n... 前 " + maxLen + " 字"; +} +``` + +--- + +## 二十、config.ts 更新 + +新增环境变量: + +```typescript +export const DEFAULT_AUTHOR = process.env.DEFAULT_AUTHOR || "小赵"; +export const UAPI_BASE = process.env.RSS_API_BASE || "https://api.usj.cc"; +export const UAPI_TOKEN = process.env.RSS_API_TOKEN || ""; +``` + +--- + +## 二十一、rssapi.ts 新增函数 + +### 21.1 getArticles + +```typescript +export async function getArticles(limit = 10): Promise { + const data = await rssFetch<{ articles: Article[] }>(`/api/articles?limit=${limit}`); + return data.articles || []; +} +``` + +### 21.2 checkHealth + +```typescript +export async function checkHealth(): Promise { + return rssFetch("/api/health"); +} +``` + +### 21.3 updateLink 和 updateFeed 已存在 + +rssapi.ts 已有 `updateLink(oldUrl, data)` 函数,用于友链编辑。 +需要新增 `updateFeed(oldUrl, data)` 函数用于订阅编辑。 + +--- + +## 二十二、git.ts 更新 + +### 22.1 GitDeployQueue 修改 + +`enqueue` 方法不再自动 add static 目录,改为只 add content: + +```typescript +const addResult = await this.runGit(["add", "content"]); +``` + +### 22.2 新增 sync 方法 + +```typescript +async sync(): Promise { + try { + const result = await this.runGit(["pull", "--rebase", "--autostash", "origin", "main"]); + return { success: true, output: result.stdout + result.stderr }; + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + if (this.isRebaseConflict(msg)) { + await this.runGit(["rebase", "--abort"], true); + return { success: false, conflict: true, error: "远程冲突,已放弃合并" }; + } + return { success: false, error: msg }; + } +} +``` + +--- + +## 二十三、实施清单 + +按依赖顺序: + +### 第一步:基础设施修改 +1. config.ts — 新增 DEFAULT_AUTHOR、UAPI_BASE、UAPI_TOKEN +2. git.ts — 修改 add 路径、新增 sync 方法 +3. rssapi.ts — 新增 getArticles、checkHealth、updateFeed +4. helpers.ts — 新增 getArticleDirPath、truncateText +5. sessions.ts — 新增 chunks、originalSlug 字段、默认作者从 config 读取 + +### 第二步:poll.ts 重写 +6. 更新 ARG_COMMANDS +7. 更新 CALLBACK_PREFIXES +8. 更新 ForceReply 关键词路由 +9. 新增 confirm_del / cancel_del / skip_cat / skip_tag 处理 + +### 第三步:handlers.ts 重写 +10. 系统命令(start/stop/status/ip/help) +11. 文章命令(new/edit/delete/list/view/title/categories/tags/author) +12. 写作命令(preview/publish/draft/deploy/sync/undo/cancel) +13. 友链命令(links/link_add/link_edit/link_del/link_toggle) +14. 订阅命令(feeds/feed_add/feed_edit/feed_del/feed_health) +15. 数据命令(stats/read) + +### 第四步:键盘和命令注册 +16. mainKeyboard — 纯命令 +17. writingKeyboard — 加 /undo /deploy +18. setMyCommands — 完整命令列表 + +### 第五步:验证 +19. TypeScript 类型检查 +20. 本地 npm run dev 测试 diff --git a/write/docs/integration-plan.md b/write/docs/integration-plan.md new file mode 100644 index 00000000..93d25192 --- /dev/null +++ b/write/docs/integration-plan.md @@ -0,0 +1,477 @@ +# Bot + Write 整合架构方案 + +## 目标 + +将 `bot/`(Telegram Bot)的全部功能合并进 `write/`(Next.js 16)项目,消除双进程问题,统一文件操作、Hugo 管理、Git 部署。 + +--- + +## 一、整合后的目录结构 + +``` +write/ +├── src/ +│ ├── app/ # Next.js 页面 + API 路由(现有) +│ ├── components/ # React 组件(现有) +│ ├── lib/ +│ │ ├── config.ts # 统一配置(合并 bot 的 .env 字段) +│ │ ├── posts.ts # 文章 CRUD(现有,改异步) +│ │ ├── ai.ts # AI 功能(现有) +│ │ ├── artalk.ts # 评论管理(现有) +│ │ ├── recycle.ts # 回收站(现有) +│ │ ├── rssapi.ts # 友链/订阅 API(现有) +│ │ ├── hugo.ts # ★ 新增:Hugo 进程单例管理器 +│ │ ├── git.ts # ★ 新增:Git 操作队列 +│ │ └── bot/ # ★ 新增:Telegram Bot 模块 +│ │ ├── index.ts # bot 启动入口,导出 startBot() +│ │ ├── config.ts # bot 专用配置(TG_TOKEN, ALLOWED_IDS) +│ │ ├── tg.ts # Telegram API 封装(tg(), sendMessage(), handleCallback()) +│ │ ├── sessions.ts # 写作会话状态管理 +│ │ ├── handlers.ts # 全部 /command 处理器 +│ │ ├── poll.ts # 长轮询循环(带指数退避) +│ │ └── helpers.ts # 工具函数(getLanIP, makeSlug, sessionSummary) +│ └── instrumentation.ts # ★ 新增:Next.js 服务端启动钩子,启动 bot +├── .env # 合并后的环境变量 +├── start.bat # 启动脚本(不变) +└── ...(其余不变) +``` + +--- + +## 二、三个核心共享模块 + +### 2.1 Hugo 进程管理器 — `src/lib/hugo.ts` + +**解决的问题:** 当前 bot 的 `write-server.js` 和 write 的 `hugo/route.ts` 各自管理 Hugo 进程,互不知道对方状态,可能互相杀进程。 + +```typescript +// src/lib/hugo.ts + +class HugoManager { + private process: ChildProcess | null = null; + private ready = false; + private starting = false; + private healthTimer: NodeJS.Timeout | null = null; + + /** 启动 Hugo server,如果已在运行则跳过 */ + async start(): Promise<{ ok: boolean; alreadyRunning?: boolean }> { ... } + + /** 优雅停止 Hugo */ + stop(): void { ... } + + /** 当前状态 */ + status(): { running: boolean; starting: boolean } { ... } + + /** 确保 Hugo 在运行,不在则启动并等待就绪 */ + async ensureRunning(): Promise { ... } + + /** 每 30 秒检查一次 Hugo 是否还活着,死了自动重启 */ + private startHealthCheck(): void { ... } + + /** 用 HTTP 请求检查 Hugo 是否响应 */ + private async ping(): Promise { ... } +} + +// 全局单例 +export const hugo = new HugoManager(); +``` + +关键设计点: +- `start()` 内部有互斥锁,两个并发调用不会重复启动 +- 健康检查每 30 秒 ping 一次 Hugo 的 localhost:1313,失败则标记 ready=false +- Web 端的 `/api/hugo/route.ts` 改为调用 `hugo.start()` / `hugo.stop()` / `hugo.status()` +- Bot 的 `/start` 命令改为调用 `hugo.ensureRunning()` +- 启动参数统一为 `hugo server --bind 0.0.0.0 --port 1313 --disableFastRender --noHTTPCache` + +### 2.2 Git 部署队列 — `src/lib/git.ts` + +**解决的问题:** 当前 deploy route 没有并发控制,两个同时触发的 deploy 会导致 rebase 冲突或仓库损坏。 + +```typescript +// src/lib/git.ts + +class GitDeployQueue { + private queue: Array<{ + message: string; + resolve: (result: DeployResult) => void; + reject: (err: Error) => void; + }> = []; + private running = false; + + /** 入队一次部署任务,返回 Promise 等待结果 */ + async enqueue(commitMessage: string): Promise { ... } + + /** 实际执行 git add → commit → pull --rebase → push */ + private async execute(message: string): Promise { ... } + + /** 处理队列中的下一个任务 */ + private async next(): Promise { ... } +} + +export interface DeployResult { + success: boolean; + output?: string; + error?: string; + conflict?: boolean; +} + +export const deployQueue = new GitDeployQueue(); +``` + +关键设计点: +- 同一时间只有一个 git 操作在执行,其余排队等待 +- Web 端的 `/api/deploy/route.ts` 改为调用 `deployQueue.enqueue(title)` +- Bot 的 `/publish` 保存文章后也调用 `deployQueue.enqueue()` 触发自动部署 +- 返回 `{ conflict: true }` 时前端提示用户手动解决 + +### 2.3 Bot 模块 — `src/lib/bot/` + +**解决的问题:** bot 作为独立进程通过 HTTP 调 write API,引入了不必要的网络依赖和状态不一致。 + +整合后 bot 是 Next.js 进程内的一个模块,handler 直接调用 `src/lib/posts.ts`、`src/lib/rssapi.ts` 等函数,不再走 HTTP。 + +--- + +## 三、Bot 模块详细设计 + +### 3.1 启动方式 — `src/instrumentation.ts` + +Next.js 13+ 支持 `instrumentation.ts`,在服务端进程启动时执行一次: + +```typescript +// src/instrumentation.ts +export async function register() { + // 仅在 Node.js 运行时执行(不是 Edge Runtime) + if (process.env.NEXT_RUNTIME === "nodejs") { + const { startBot } = await import("@/lib/bot"); + startBot(); + } +} +``` + +需要在 `next.config.ts` 中启用: + +```typescript +const nextConfig: NextConfig = { + devIndicators: false, + experimental: { + instrumentationHook: true, // Next.js 15+ 已默认开启,16 可能不需要 + }, +}; +``` + +### 3.2 Telegram API 封装 — `src/lib/bot/tg.ts` + +从 bot 的 `tg-core.js` 移植,改为 TypeScript: + +```typescript +const API = `https://api.telegram.org/bot${TG_TOKEN}`; + +export async function tg(method: string, body: Record): Promise> { + const res = await fetch(`${API}/${method}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + return res.json(); +} + +export function allowed(chatId: number): boolean { + if (ALLOWED_IDS.length === 0) return true; + return ALLOWED_IDS.includes(chatId); +} + +export async function sendMessage(chatId: number, text: string, opts?: Record): Promise { + // 先尝试 HTML 解析,失败则降级纯文本 + try { + await tg("sendMessage", { chat_id: chatId, text, parse_mode: "HTML", disable_web_page_preview: true, ...opts }); + } catch { + await tg("sendMessage", { chat_id: chatId, text, ...opts }); + } +} +``` + +### 3.3 写作会话 — `src/lib/bot/sessions.ts` + +从 bot 的 `sessions.js` 移植,改为直接调用 `src/lib/posts.ts`: + +```typescript +interface Session { + title: string; + slug: string; + content: string; + categories: string[]; + tags: string[]; + author: string; + draft: boolean; +} + +const sessions = new Map(); + +export function getSession(chatId: number): Session { ... } +export function clearSession(chatId: number): void { ... } + +// 关键变化:直接调用 lib/posts.ts,不再走 HTTP +export async function saveSession(s: Session, isDraft: boolean): Promise { + const { createPost, updatePost, getPost } = await import("@/lib/posts"); + const existing = getPost(s.slug); + if (existing) { + updatePost(s.slug, { + title: s.title, + date: new Date().toISOString().slice(0, 10), + draft: isDraft, + categories: s.categories, + tags: s.tags, + author: s.author, + }, s.content); + } else { + createPost({ + title: s.title, + slug: s.slug, + date: new Date().toISOString().slice(0, 10), + draft: isDraft, + categories: s.categories, + tags: s.tags, + author: s.author, + }, s.content); + } +} +``` + +### 3.4 命令处理器 — `src/lib/bot/handlers.ts` + +从 bot 的 `tg-handlers.js` 移植,核心变化: + +| 原来(HTTP 调 write API) | 现在(直接调 lib) | +|---|---| +| `fetch("http://127.0.0.1:8016/api/posts/" + slug)` | `getPost(slug)` | +| `fetch("http://127.0.0.1:8016/api/posts", { method: "POST" })` | `createPost(frontMatter, content)` | +| `fetch("http://127.0.0.1:8016/api/posts/" + slug, { method: "DELETE" })` | `moveToRecycle(slug, dirPath, title)` | +| `fetch("http://127.0.0.1:8016/api/rss/links")` | `getLinks(true)` | +| `fetch("http://127.0.0.1:8016/api/rss/feeds")` | `getFeeds()` | +| `fetch("http://127.0.0.1:8016/api/deploy", ...)` | `deployQueue.enqueue(title)` | + +另外,`/publish` 命令现在会自动触发 git 部署: + +```typescript +// /publish handler +async function handlePublish(chatId: number) { + const s = sessions.get(chatId); + await saveSession(s, false); // 保存文章 + const result = await deployQueue.enqueue(`发布: ${s.title}`); // 自动部署 + if (result.success) { + sendMessage(chatId, `✅ 已发布并部署: ${s.title}`); + } else if (result.conflict) { + sendMessage(chatId, `⚠️ 已保存但部署冲突,请手动解决`); + } else { + sendMessage(chatId, `❌ 部署失败: ${result.error}`); + } + clearSession(chatId); +} +``` + +### 3.5 长轮询 — `src/lib/bot/poll.ts` + +从 bot 的 `tg-poll.js` 移植,修复两个问题: + +1. **添加指数退避**:出错后等待 3s → 6s → 12s → 最大 60s,避免 Telegram API 限流 +2. **添加正常间隔**:`getUpdates` 返回后等待 100ms 再发起下一次,避免空转 + +```typescript +export async function startPolling(): Promise { + let lastOffset = 0; + let backoff = 3000; + + async function poll() { + if (!TG_TOKEN) return; + try { + const data = await tg("getUpdates", { + offset: lastOffset + 1, + timeout: 30, + allowed_updates: ["message", "callback_query"], + }); + if (data.ok && data.result) { + for (const update of data.result) { + lastOffset = update.update_id; + // ... 处理消息(逻辑不变) + } + } + backoff = 3000; // 成功则重置退避 + } catch (e) { + console.error("[bot:poll]", e.message); + await sleep(backoff); + backoff = Math.min(backoff * 2, 60000); + } + await sleep(100); // 正常间隔 + setImmediate(poll); + } + + poll(); +} +``` + +### 3.6 图片处理改进 + +当前 bot 直接引用 Telegram 临时 URL,改为下载到本地: + +```typescript +// 在 poll.ts 中处理 photo 消息 +if (msg.photo && sessions.has(chatId)) { + const s = getSession(chatId); + const largestPhoto = msg.photo[msg.photo.length - 1]; + const fileRes = await tg("getFile", { file_id: largestPhoto.file_id }); + if (fileRes.ok) { + const filePath = fileRes.result.file_path; + const fileUrl = `https://api.telegram.org/file/bot${TG_TOKEN}/${filePath}`; + const imgBuffer = await fetch(fileUrl).then(r => r.arrayBuffer()); + + // 保存到 static/upload/ 目录 + const filename = `${Date.now()}-${largestPhoto.file_id.slice(0, 8)}.jpg`; + const savePath = path.join(STATIC_DIR, "upload", filename); + fs.writeFileSync(savePath, Buffer.from(imgBuffer)); + + // 引用本地路径 + const caption = msg.caption || ""; + s.content += `\n![${caption || "image"}](/upload/${filename})\n`; + } +} +``` + +--- + +## 四、Web 端 API 路由适配 + +### 4.1 `/api/hugo/route.ts` — 改用 HugoManager + +```typescript +import { hugo } from "@/lib/hugo"; + +export async function GET() { + const result = await hugo.ensureRunning(); + return NextResponse.json({ running: result, url: "http://localhost:1313" }); +} +``` + +### 4.2 `/api/deploy/route.ts` — 改用 GitDeployQueue + +```typescript +import { deployQueue } from "@/lib/git"; + +export async function POST(request: NextRequest) { + const { title } = await request.json(); + const result = await deployQueue.enqueue(`发布: ${title}`); + return NextResponse.json(result); +} +``` + +--- + +## 五、配置合并 + +将 bot 的 `.env` 字段合入 write 的 `.env`: + +```env +# ---- Write 原有 ---- +RSS_API_BASE=https://api.usj.cc +RSS_API_TOKEN=xxx +WECHAT_APP_ID=xxx +WECHAT_APP_SECRET=xxx +DEEPSEEK_API_KEY=xxx +ARTALK_SERVER=https://artalk.usj.cc + +# ---- Bot 合并过来 ---- +TG_BOT_TOKEN=xxx +TG_ALLOWED_CHAT_IDS=xxx +PREFER_IFACE=WLAN +``` + +`src/lib/config.ts` 新增: + +```typescript +export const TG_BOT_TOKEN = process.env.TG_BOT_TOKEN || ""; +export const TG_ALLOWED_IDS = (process.env.TG_ALLOWED_CHAT_IDS || "") + .split(",").map(s => s.trim()).filter(Boolean).map(Number); +export const PREFER_IFACE = process.env.PREFER_IFACE || "WLAN"; +``` + +--- + +## 六、Bot 管理页面(替代 admin-panel) + +将 bot 的 `admin-panel.js`(独立 HTTP 服务器)改为 Next.js 页面 `/bot/page.tsx`: + +- 显示 Telegram Bot 连接状态(是否在轮询、最后收到消息的时间) +- 显示 Hugo 进程状态 +- 显示最近的 bot 操作日志(最近 10 条) +- 提供重启 bot 的按钮 + +对应 API 路由 `/api/bot/status/route.ts` 和 `/api/bot/restart/route.ts`。 + +这样 bot 的管理界面和 write 的其他页面共用同一个 Next.js 服务器,不再需要单独的 8017 端口。 + +--- + +## 七、实施步骤 + +按依赖关系排序: + +### 第一步:基础设施(无现有代码改动) +1. 创建 `src/lib/hugo.ts` — HugoManager 单例 +2. 创建 `src/lib/git.ts` — GitDeployQueue +3. 更新 `src/lib/config.ts` — 合并 bot 环境变量 +4. 更新 `.env` — 合并 bot 配置项 + +### 第二步:移植 Bot 模块 +5. 创建 `src/lib/bot/helpers.ts` — 工具函数 +6. 创建 `src/lib/bot/config.ts` — bot 专用配置 +7. 创建 `src/lib/bot/tg.ts` — Telegram API 封装 +8. 创建 `src/lib/bot/sessions.ts` — 会话管理 +9. 创建 `src/lib/bot/handlers.ts` — 命令处理器(直接调 lib) +10. 创建 `src/lib/bot/poll.ts` — 长轮询(带退避) +11. 创建 `src/lib/bot/index.ts` — 启动入口 + +### 第三步:Next.js 集成 +12. 创建 `src/instrumentation.ts` — 服务端启动钩子 +13. 更新 `next.config.ts` — 确保 instrumentation 启用 + +### 第四步:适配现有 API 路由 +14. 改造 `src/app/api/hugo/route.ts` — 使用 HugoManager +15. 改造 `src/app/api/deploy/route.ts` — 使用 GitDeployQueue + +### 第五步:新增 Bot 管理页面 +16. 创建 `src/app/api/bot/status/route.ts` +17. 创建 `src/app/api/bot/restart/route.ts` +18. 创建 `src/app/bot/page.tsx` — Bot 管理 UI + +### 第六步:清理 +19. 删除 `bot/` 目录(或归档) +20. 更新 `start.bat` — 不再需要分别启动两个进程 +21. 更新 `.gitignore` — 移除 bot 相关条目 + +--- + +## 八、解决的问题清单 + +| 原问题 | 解决方式 | +|---|---| +| 两个进程各自操作同一份文件系统 | 合并为一个进程,所有文件操作走同一层 lib | +| Hugo 两边各管各的 | HugoManager 单例,一个进程只启动一个 Hugo | +| Git deploy 无并发控制 | GitDeployQueue 串行执行 | +| bot 调 write API 走 HTTP 网络 | 直接调 lib 函数,零网络开销 | +| bot 的 writeReady 不会自动恢复 | HugoManager 带健康检查 + 自动重启 | +| 长轮询无退避策略 | 指数退避 3s→60s | +| Telegram 图片用临时 URL | 下载到 static/upload/ 本地引用 | +| bot 的 admin-panel 单独占一个端口 | 改为 Next.js 内页面 | + +--- + +## 九、不变的部分 + +以下保持不变,不做改动: +- `src/app/` 下所有页面(前端 UI) +- `src/components/` 下所有组件 +- `src/lib/posts.ts` 的核心逻辑(仅改为在 handler 中直接调用) +- `src/lib/ai.ts`、`src/lib/artalk.ts`、`src/lib/recycle.ts`、`src/lib/rssapi.ts` +- `hugo.toml`、`content/`、`themes/`、`static/`、`data/` +- GitHub Actions workflows 和 `scripts/` 目录 +- `package.json` 的根目录脚本依赖 diff --git a/write/install-startup.bat b/write/install-startup.bat new file mode 100644 index 00000000..667c0ebd --- /dev/null +++ b/write/install-startup.bat @@ -0,0 +1,13 @@ +@echo off +chcp 65001 >nul +set "STARTUP=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup" +set "VBS=E:\GitHub\blog\write\startup.vbs" + +REM Create shortcut in Startup folder +echo Set WshShell = CreateObject("WScript.Shell") > "%STARTUP%\WriteBot.vbs" +echo WshShell.Run Chr(34) ^& "E:\GitHub\blog\write\start-silent.bat" ^& Chr(34), 0, False >> "%STARTUP%\WriteBot.vbs" +echo Set WshShell = Nothing >> "%STARTUP%\WriteBot.vbs" + +echo Done! WriteBot will start silently on Windows login. +echo Startup folder: %STARTUP% +pause diff --git a/write/install-startup.ps1 b/write/install-startup.ps1 new file mode 100644 index 00000000..e04b3b37 --- /dev/null +++ b/write/install-startup.ps1 @@ -0,0 +1,21 @@ +# Get Startup folder path +$startup = [Environment]::GetFolderPath('Startup') +$batPath = "E:\GitHub\blog\write\start-silent.bat" +$vbsPath = Join-Path $startup "WriteBot.vbs" + +# Create VBS wrapper for silent execution (no cmd window) +$vbs = @" +Set WshShell = CreateObject("WScript.Shell") +WshShell.Run Chr(34) & "$batPath" & Chr(34), 0, False +Set WshShell = Nothing +"@ + +$vbs | Out-File -FilePath $vbsPath -Encoding ASCII + +# Verify +if (Test-Path $vbsPath) { + Write-Host "WriteBot startup script installed to: $vbsPath" + Write-Host "It will start silently on Windows login." +} else { + Write-Host "Failed to install startup script." +} diff --git a/write/package-lock.json b/write/package-lock.json index e26fe424..222f0a69 100644 --- a/write/package-lock.json +++ b/write/package-lock.json @@ -11,6 +11,7 @@ "date-fns": "^4.2.1", "gray-matter": "^4.0.3", "lucide-react": "^1.16.0", + "marked": "^18.0.4", "next": "16.2.6", "react": "19.2.4", "react-dom": "19.2.4" @@ -5020,6 +5021,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.4.tgz", + "integrity": "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/write/package.json b/write/package.json index 23eb9657..4806c33a 100644 --- a/write/package.json +++ b/write/package.json @@ -12,6 +12,7 @@ "date-fns": "^4.2.1", "gray-matter": "^4.0.3", "lucide-react": "^1.16.0", + "marked": "^18.0.4", "next": "16.2.6", "react": "19.2.4", "react-dom": "19.2.4" diff --git a/write/setup-autostart.bat b/write/setup-autostart.bat new file mode 100644 index 00000000..06e0b7b1 --- /dev/null +++ b/write/setup-autostart.bat @@ -0,0 +1,33 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo WriteBot Auto-Start Setup +echo ======================================== +echo. + +REM Check for admin rights +net session >nul 2>&1 +if %errorlevel% neq 0 ( + echo This script requires Administrator privileges. + echo Right-click this file and select "Run as administrator". + echo. + pause + exit /b 1 +) + +echo Creating scheduled task... +schtasks /create /tn "WriteBot" /tr "cmd /c cd /d E:\GitHub\blog\write && npm run dev -- -p 8016" /sc onlogon /rl highest /f + +if %errorlevel%==0 ( + echo. + echo [OK] WriteBot scheduled task created. + echo. + echo Verify: schtasks /query /tn "WriteBot" + echo Remove: schtasks /delete /tn "WriteBot" /f +) else ( + echo. + echo [FAIL] Could not create task. +) + +echo. +pause diff --git a/write/src/app/api/bot/restart/route.ts b/write/src/app/api/bot/restart/route.ts new file mode 100644 index 00000000..757b13e0 --- /dev/null +++ b/write/src/app/api/bot/restart/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { hugo } from "@/lib/hugo"; + +export async function POST() { + try { + hugo.stop(); + const result = await hugo.start(); + return NextResponse.json({ ok: result.ok }); + } catch (error: unknown) { + return NextResponse.json( + { error: error instanceof Error ? error.message : "Failed" }, + { status: 500 } + ); + } +} diff --git a/write/src/app/api/bot/status/route.ts b/write/src/app/api/bot/status/route.ts new file mode 100644 index 00000000..eea7e7a7 --- /dev/null +++ b/write/src/app/api/bot/status/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import { hugo } from "@/lib/hugo"; +import { TG_BOT_TOKEN } from "@/lib/config"; + +export async function GET() { + const hugoStatus = hugo.status(); + return NextResponse.json({ + bot: { + enabled: !!TG_BOT_TOKEN, + polling: !!TG_BOT_TOKEN, + }, + hugo: hugoStatus, + }); +} diff --git a/write/src/app/api/deploy/route.ts b/write/src/app/api/deploy/route.ts index 25bc7ed4..9bd354e5 100644 --- a/write/src/app/api/deploy/route.ts +++ b/write/src/app/api/deploy/route.ts @@ -1,39 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { execFile } from "child_process"; -import { promisify } from "util"; -import { BLOG_ROOT } from "@/lib/config"; - -const execFileAsync = promisify(execFile); - -type GitExecError = Error & { - stdout?: string; - stderr?: string; - code?: number; -}; - -async function runGit(args: string[], allowFailure = false) { - try { - const { stdout, stderr } = await execFileAsync("git", args, { - cwd: BLOG_ROOT, - windowsHide: true, - maxBuffer: 1024 * 1024, - }); - return { stdout, stderr, code: 0 }; - } catch (error) { - const err = error as GitExecError; - const result = { - stdout: err.stdout || "", - stderr: err.stderr || err.message, - code: typeof err.code === "number" ? err.code : 1, - }; - if (allowFailure) return result; - throw new Error((result.stderr || result.stdout || err.message).trim()); - } -} - -function isRebaseConflict(message: string) { - return /conflict|CONFLICT|Resolve all conflicts|Automatic merge failed|could not apply|rebase/i.test(message); -} +import { deployQueue } from "@/lib/git"; export async function POST(request: NextRequest) { try { @@ -44,40 +10,23 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Title is required" }, { status: 400 }); } - let output = ""; + const result = await deployQueue.enqueue(`发布: ${title}`); - const addResult = await runGit(["add", "content/post", "static"]); - output += addResult.stdout + addResult.stderr; - - const diffResult = await runGit(["diff", "--cached", "--quiet"], true); - if (diffResult.code === 1) { - const commitResult = await runGit(["commit", "-m", `发布: ${title}`]); - output += commitResult.stdout + commitResult.stderr; - } else if (diffResult.code !== 0) { - throw new Error((diffResult.stderr || diffResult.stdout || "检查暂存区失败").trim()); + if (result.success) { + return NextResponse.json({ success: true, output: result.output }); } - try { - const pullResult = await runGit(["pull", "--rebase", "--autostash", "origin", "main"]); - output += pullResult.stdout + pullResult.stderr; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if (isRebaseConflict(message)) { - return NextResponse.json( - { - error: "远程仓库有新内容,自动合并失败。请在终端解决冲突后执行 git rebase --continue,再重新发布。", - detail: message.slice(0, 1000), - }, - { status: 409 } - ); - } - throw error; + if (result.conflict) { + return NextResponse.json( + { + error: result.error, + detail: result.output, + }, + { status: 409 } + ); } - const pushResult = await runGit(["push"]); - output += pushResult.stdout + pushResult.stderr; - - return NextResponse.json({ success: true, output: output.slice(0, 1000) }); + return NextResponse.json({ error: result.error }, { status: 500 }); } catch (error: unknown) { return NextResponse.json( { error: error instanceof Error ? error.message : "Deploy failed" }, diff --git a/write/src/app/api/hugo/route.ts b/write/src/app/api/hugo/route.ts index afffae7e..802c6c46 100644 --- a/write/src/app/api/hugo/route.ts +++ b/write/src/app/api/hugo/route.ts @@ -1,50 +1,27 @@ import { NextResponse } from "next/server"; -import { exec } from "child_process"; -import { promisify } from "util"; -import { BLOG_ROOT, HUGO_PREVIEW } from "@/lib/config"; - -const execAsync = promisify(exec); - -async function isHugoRunning(): Promise { - try { - const res = await fetch(HUGO_PREVIEW, { signal: AbortSignal.timeout(1500) }); - return res.ok || res.status < 500; - } catch { - return false; - } -} - -let starting = false; +import { hugo } from "@/lib/hugo"; +import { HUGO_PREVIEW } from "@/lib/config"; export async function GET() { try { - const running = await isHugoRunning(); - if (running) { + const result = await hugo.start(); + if (result.alreadyRunning || result.ok) { return NextResponse.json({ running: true, url: HUGO_PREVIEW }); } - - if (starting) { - return NextResponse.json({ running: false, starting: true, url: HUGO_PREVIEW }); - } - - starting = true; - // Kill existing process on port 1313, then start hugo serve - const killCmd = `for /f "tokens=5" %a in ('netstat -ano ^| findstr :1313 ^| findstr LISTENING') do taskkill /F /PID %a 2>nul`; - const startCmd = `cd "${BLOG_ROOT}" && hugo serve --port 1313 --noHTTPCache`; - - try { await execAsync(killCmd); } catch { /* nothing to kill */ } - exec(startCmd, (err) => { - if (err) console.error("Hugo start error:", err.message); - }); - - // Wait a moment for Hugo to start - await new Promise((r) => setTimeout(r, 2000)); - starting = false; - - const started = await isHugoRunning(); - return NextResponse.json({ running: started, url: HUGO_PREVIEW }); + return NextResponse.json({ running: false, url: HUGO_PREVIEW }); + } catch (error: unknown) { + return NextResponse.json( + { error: error instanceof Error ? error.message : "Failed" }, + { status: 500 } + ); + } +} + +export async function DELETE() { + try { + hugo.stop(); + return NextResponse.json({ running: false }); } catch (error: unknown) { - starting = false; return NextResponse.json( { error: error instanceof Error ? error.message : "Failed" }, { status: 500 } diff --git a/write/src/app/bot/page.tsx b/write/src/app/bot/page.tsx new file mode 100644 index 00000000..f6cf5759 --- /dev/null +++ b/write/src/app/bot/page.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Bot, RefreshCw, Server } from "lucide-react"; + +interface BotStatus { + bot: { enabled: boolean; polling: boolean }; + hugo: { running: boolean; starting: boolean }; +} + +export default function BotPage() { + const [status, setStatus] = useState(null); + const [restarting, setRestarting] = useState(false); + + async function fetchStatus() { + try { + const res = await fetch("/api/bot/status"); + const data = await res.json(); + setStatus(data); + } catch { + setStatus(null); + } + } + + async function restartHugo() { + setRestarting(true); + try { + await fetch("/api/bot/restart", { method: "POST" }); + } catch { /* ignore */ } + setTimeout(async () => { + await fetchStatus(); + setRestarting(false); + }, 3000); + } + + useEffect(() => { + fetchStatus(); + const timer = setInterval(fetchStatus, 10000); + return () => clearInterval(timer); + }, []); + + return ( +

+

Bot 管理

+ + {/* Bot Status */} +
+

+ Telegram Bot +

+
+
+ + {status?.bot.enabled + ? "已配置,随服务自动启动" + : "未配置 TG_BOT_TOKEN"} + +
+
+ + {/* Hugo Status */} +
+

+ Hugo Server +

+
+
+ + {status?.hugo.running + ? "运行中 — localhost:1313" + : status?.hugo.starting + ? "启动中..." + : "已停止"} + +
+ +
+
+ ); +} diff --git a/write/src/components/Sidebar.tsx b/write/src/components/Sidebar.tsx index 7d3840fc..b1071ee0 100644 --- a/write/src/components/Sidebar.tsx +++ b/write/src/components/Sidebar.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; -import { FileText, PenSquare, MessageSquare, Trash2, Rss, Link2, MessageCircle, Image, Menu, X } from "lucide-react"; +import { FileText, PenSquare, MessageSquare, Trash2, Rss, Link2, MessageCircle, Image, Menu, X, Bot } from "lucide-react"; import ThemeToggle from "./ThemeToggle"; const navItems = [ @@ -15,6 +15,7 @@ const navItems = [ { href: "/images", label: "图片", icon: Image }, { href: "/wechat", label: "公众号", icon: MessageCircle }, { href: "/recycle", label: "回收站", icon: Trash2 }, + { href: "/bot", label: "Bot", icon: Bot }, ]; export default function Sidebar() { diff --git a/write/src/instrumentation.ts b/write/src/instrumentation.ts new file mode 100644 index 00000000..ded2b0a1 --- /dev/null +++ b/write/src/instrumentation.ts @@ -0,0 +1,8 @@ +export async function register() { + if (process.env.NEXT_RUNTIME === "nodejs") { + const { startBot } = await import("@/lib/bot"); + startBot().catch((e) => { + console.error("[instrumentation] bot startup failed:", e); + }); + } +} diff --git a/write/src/lib/bot/cache.ts b/write/src/lib/bot/cache.ts new file mode 100644 index 00000000..b6696c54 --- /dev/null +++ b/write/src/lib/bot/cache.ts @@ -0,0 +1,30 @@ +interface CacheEntry { + data: T; + expiresAt: number; +} + +const cache = new Map>(); + +export function getCached(key: string): T | null { + const entry = cache.get(key); + if (!entry) return null; + if (Date.now() > entry.expiresAt) { + cache.delete(key); + return null; + } + return entry.data as T; +} + +export function setCache(key: string, data: T, ttlMs: number): void { + cache.set(key, { data, expiresAt: Date.now() + ttlMs }); +} + +export function invalidateCache(key: string): void { + cache.delete(key); +} + +export function invalidateByPrefix(prefix: string): void { + for (const key of cache.keys()) { + if (key.startsWith(prefix)) cache.delete(key); + } +} diff --git a/write/src/lib/bot/config.ts b/write/src/lib/bot/config.ts new file mode 100644 index 00000000..37dddb21 --- /dev/null +++ b/write/src/lib/bot/config.ts @@ -0,0 +1,5 @@ +import { TG_BOT_TOKEN, TG_ALLOWED_IDS } from "@/lib/config"; + +export const TG_TOKEN = TG_BOT_TOKEN; +export const ALLOWED_IDS = TG_ALLOWED_IDS; +export const API = TG_TOKEN ? `https://api.telegram.org/bot${TG_TOKEN}` : ""; diff --git a/write/src/lib/bot/handlers.ts b/write/src/lib/bot/handlers.ts new file mode 100644 index 00000000..57861f88 --- /dev/null +++ b/write/src/lib/bot/handlers.ts @@ -0,0 +1,654 @@ +import { getPost, listPosts } from "@/lib/posts"; +import { getLinks, addLink, updateLink, deleteLink, getFeeds, addFeed, updateFeed, deleteFeed, getFeedsHealth, getArticles } from "@/lib/rssapi"; +import { moveToRecycle } from "@/lib/recycle"; +import { deployQueue } from "@/lib/git"; +import { hugo } from "@/lib/hugo"; +import { getLanIP, DEFAULT_AUTHOR } from "@/lib/config"; +import { sendMessage, tg } from "./tg"; +import { getSession, hasSession, clearSession, saveSession, undoLastChunk } from "./sessions"; +import { makeSlug, sessionSummary, truncateText } from "./helpers"; +import { getCached, setCache, invalidateByPrefix } from "./cache"; + +type Handler = (chatId: number, arg?: string) => Promise; + +const handlers: Record = {}; + +// ---- Cached API wrappers ---- + +const CACHE_TTL_LINKS = 5 * 60 * 1000; +const CACHE_TTL_FEEDS = 5 * 60 * 1000; +const CACHE_TTL_ARTICLES = 3 * 60 * 1000; +const CACHE_TTL_HEALTH = 10 * 60 * 1000; + +async function cachedLinks(all = true) { + const key = "links_" + (all ? "all" : "visible"); + const hit = getCached extends Promise ? R : never>(key); + if (hit) return hit; + const data = await getLinks(all); + setCache(key, data, CACHE_TTL_LINKS); + return data; +} + +async function cachedFeeds() { + const key = "feeds"; + const hit = getCached extends Promise ? R : never>(key); + if (hit) return hit; + const data = await getFeeds(); + setCache(key, data, CACHE_TTL_FEEDS); + return data; +} + +async function cachedArticles(limit: number) { + const key = "articles_" + limit; + const hit = getCached extends Promise ? R : never>(key); + if (hit) return hit; + const data = await getArticles(limit); + setCache(key, data, CACHE_TTL_ARTICLES); + return data; +} + +async function cachedHealth() { + const key = "health"; + const hit = getCached extends Promise ? R : never>(key); + if (hit) return hit; + const data = await getFeedsHealth(); + setCache(key, data, CACHE_TTL_HEALTH); + return data; +} + +// ---- 键盘 ---- + +function mainKeyboard() { + return { + keyboard: [ + [{ text: "/new" }, { text: "/list" }], + [{ text: "/links" }, { text: "/feeds" }], + [{ text: "/stats" }, { text: "/help" }], + ], + resize_keyboard: true, + one_time_keyboard: false, + }; +} + +function writingKeyboard() { + return { + keyboard: [ + [{ text: "/preview" }, { text: "/undo" }], + [{ text: "/publish" }, { text: "/deploy" }], + [{ text: "/draft" }, { text: "/cancel" }], + [{ text: "/title" }, { text: "/categories" }], + ], + resize_keyboard: true, + one_time_keyboard: false, + }; +} + +async function sendMain(chatId: number, text: string) { + await sendMessage(chatId, text, { reply_markup: mainKeyboard() }); +} + +async function sendWrite(chatId: number, text: string) { + await sendMessage(chatId, text, { reply_markup: writingKeyboard() }); +} + +// ---- System ---- + +handlers.start = async (chatId) => { + const ok = await hugo.ensureRunning(); + const ip = getLanIP(); + await sendMain(chatId, "✅ 已启动\n\n📱 手机: http://" + ip + ":8016/\n💻 电脑: http://localhost:8016/"); + await tg("sendMessage", { + chat_id: chatId, + text: "点下方按钮打开编辑器:", + reply_markup: { inline_keyboard: [[{ text: "🌐 打开编辑器", web_app: { url: "http://" + ip + ":8016/" } }]] }, + }); +}; + +handlers.stop = async (chatId) => { hugo.stop(); await sendMain(chatId, "⚡ Hugo 已停止。"); }; + +handlers.status = async (chatId) => { + const s = hugo.status(); + const status = s.running ? "✅ 运行中" : s.starting ? "⏳ 启动中..." : "⏸ 已停止"; + const lines = ["Hugo: " + status]; + if (s.running) { const ip = getLanIP(); lines.push("📱 http://" + ip + ":8016", "💻 http://localhost:8016"); } + await sendMain(chatId, lines.join("\n")); +}; + +handlers.ip = async (chatId) => { await sendMain(chatId, "Phone: http://" + getLanIP() + ":8016"); }; + +handlers.shutdown = async (chatId) => { + await tg("sendMessage", { + chat_id: chatId, + text: "⚠️ 确认关机?\n\n电脑将在确认后 10 秒关机。", + parse_mode: "HTML", + reply_markup: { + inline_keyboard: [[ + { text: "✅ 确认关机", callback_data: "confirm_shutdown" }, + { text: "❌ 取消", callback_data: "cancel_shutdown" }, + ]], + }, + }); +}; + +handlers.confirm_shutdown = async (chatId) => { + await sendMessage(chatId, "⏻ 电脑将在 10 秒后关机..."); + const { exec } = await import("child_process"); + exec("shutdown /s /t 10", (err) => { + if (err) console.error("[shutdown]", err.message); + }); +}; + +handlers.cancel_shutdown = async (chatId) => { + const { exec } = await import("child_process"); + exec("shutdown /a", () => {}); + await sendMain(chatId, "已取消关机。"); +}; + +handlers.reboot = async (chatId) => { + await tg("sendMessage", { + chat_id: chatId, + text: "⚠️ 确认重启?\n\n电脑将在确认后 10 秒重启。", + parse_mode: "HTML", + reply_markup: { + inline_keyboard: [[ + { text: "✅ 确认重启", callback_data: "confirm_reboot" }, + { text: "❌ 取消", callback_data: "cancel_reboot" }, + ]], + }, + }); +}; + +handlers.confirm_reboot = async (chatId) => { + await sendMessage(chatId, "🔄 电脑将在 10 秒后重启..."); + const { exec } = await import("child_process"); + exec("shutdown /r /t 10", (err) => { + if (err) console.error("[reboot]", err.message); + }); +}; + +handlers.cancel_reboot = async (chatId) => { + const { exec } = await import("child_process"); + exec("shutdown /a", () => {}); + await sendMain(chatId, "已取消重启。"); +}; + +handlers.help = async (chatId) => { + if (hasSession(chatId)) { + await sendWrite(chatId, + "✍ 写作中\n\n" + + "/publish 保存 /draft 草稿 /deploy 部署\n" + + "/preview 预览 /undo 撤销 /cancel 取消\n" + + "/title 标题 /categories 分类 /tags 标签\n\n" + + "直接发消息追加正文,发照片追加图片" + ); + } else { + await sendMain(chatId, + "🤖 WriteBot 命令\n\n" + + "📝 文章\n" + + "/new 标题 — 新建\n" + + "/edit slug — 编辑\n" + + "/delete slug — 删除\n" + + "/list — 文章列表\n\n" + + "🔗 友链\n" + + "/links — 列表\n" + + "/link_add url 名称 — 添加\n" + + "/link_edit url — 编辑\n" + + "/link_del url — 删除\n" + + "/link_toggle url — 显示/隐藏\n\n" + + "📡 订阅\n" + + "/feeds — 列表\n" + + "/feed_add url — 添加\n" + + "/feed_edit url — 编辑\n" + + "/feed_del url — 删除\n" + + "/feed_health — 健康检查\n\n" + + "📰 /read — 友链最新文章\n" + + "📊 /stats — 网站数据\n\n" + + "⏻ /shutdown — 关机 🔄 /reboot — 重启" + ); + } +}; + +// ---- Articles: New ---- + +handlers.new = async (chatId, title) => { + if (title) { + const s = getSession(chatId); + s.title = title; + s.slug = makeSlug(title); + s.content = ""; + s.categories = []; + s.tags = []; + s.draft = true; + s.author = DEFAULT_AUTHOR; + s.chunks = []; + await tg("sendMessage", { + chat_id: chatId, + text: "📝 开始写:" + title + "\n\n📂 设置分类(可选)", + parse_mode: "HTML", + reply_markup: { + inline_keyboard: [[ + { text: "跳过", callback_data: "skip_cat" }, + { text: "设置分类", callback_data: "skip_cat" }, + ]], + }, + }); + await tg("sendMessage", { + chat_id: chatId, + text: "请输入分类(逗号分隔,或发「跳过」):", + reply_markup: { force_reply: true, input_field_placeholder: "技术、生活" }, + }); + } else { + await tg("sendMessage", { chat_id: chatId, text: "📝 请输入文章标题:", reply_markup: { force_reply: true, input_field_placeholder: "在这里输入标题..." } }); + } +}; + +handlers.skip_categories = async (chatId) => { + const s = getSession(chatId); + if (!s.title) return; + s.categories = []; + await tg("sendMessage", { + chat_id: chatId, + text: "🏷 设置标签(可选)", + reply_markup: { + inline_keyboard: [[ + { text: "跳过", callback_data: "skip_tag" }, + { text: "设置标签", callback_data: "skip_tag" }, + ]], + }, + }); + await tg("sendMessage", { + chat_id: chatId, + text: "请输入标签(逗号分隔,或发「跳过」):", + reply_markup: { force_reply: true, input_field_placeholder: "Hugo、博客" }, + }); +}; + +handlers.skip_tags = async (chatId) => { + const s = getSession(chatId); + if (!s.title) return; + s.tags = []; + await sendWrite(chatId, "✅ 准备就绪,开始写正文吧!\n\n📝 " + s.title + "\n📂 " + (s.categories.join("、") || "无分类") + "\n🏷 " + (s.tags.join("、") || "无标签")); +}; + +handlers.categories = async (chatId, cats) => { + const s = getSession(chatId); + s.categories = cats ? cats.split(/[,;",、]+/).map((c) => c.trim()).filter(Boolean) : []; + await tg("sendMessage", { + chat_id: chatId, + text: "📂 分类:" + (s.categories.join("、") || "无") + "\n\n🏷 设置标签(可选)", + parse_mode: "HTML", + reply_markup: { + inline_keyboard: [[ + { text: "跳过", callback_data: "skip_tag" }, + { text: "设置标签", callback_data: "skip_tag" }, + ]], + }, + }); + await tg("sendMessage", { + chat_id: chatId, + text: "请输入标签(逗号分隔,或发「跳过」):", + reply_markup: { force_reply: true, input_field_placeholder: "Hugo、博客" }, + }); +}; + +handlers.tags = async (chatId, tags) => { + const s = getSession(chatId); + s.tags = tags ? tags.split(/[,;",、]+/).map((t) => t.trim()).filter(Boolean) : []; + await sendWrite(chatId, "✅ 准备就绪,开始写正文吧!\n\n📝 " + s.title + "\n📂 " + (s.categories.join("、") || "无分类") + "\n🏷 " + (s.tags.join("、") || "无标签")); +}; + +// ---- Articles: Edit (方案 B) ---- + +handlers.edit = async (chatId, slug) => { + if (!slug) { await sendMain(chatId, "格式:/edit slug\n先用 /list 查看文章。"); return; } + const post = getPost(slug); + if (!post) { await sendMain(chatId, "❌ 未找到该文章"); return; } + + const originalContent = post.content || ""; + if (originalContent) { + const maxLen = 3800; + for (let i = 0; i < originalContent.length; i += maxLen) { + const chunk = originalContent.slice(i, i + maxLen); + const prefix = originalContent.length > maxLen ? "(" + (Math.floor(i / maxLen) + 1) + "/" + Math.ceil(originalContent.length / maxLen) + ")\n" : ""; + await sendMessage(chatId, prefix + chunk); + } + } + + const s = getSession(chatId); + s.title = post.frontMatter.title; + s.slug = post.frontMatter.slug; + s.originalSlug = post.frontMatter.slug; + s.content = ""; + s.chunks = []; + s.categories = (post.frontMatter.categories as string[]) || []; + s.tags = (post.frontMatter.tags as string[]) || []; + s.author = post.frontMatter.author || DEFAULT_AUTHOR; + s.draft = post.frontMatter.draft ?? true; + + await sendWrite(chatId, + "✏ 编辑:" + s.title + "\n\n" + + "原文已显示,现在重新写正文\n" + + "📂 " + (s.categories.join("、") || "无分类") + "\n" + + "🏷 " + (s.tags.join("、") || "无标签") + "\n\n" + + "直接发消息写正文,完成后点「发布」" + ); +}; + +// ---- Articles: Delete (二次确认) ---- + +handlers.delete = async (chatId, slug) => { + if (!slug) { await sendMain(chatId, "格式:/delete slug\n先用 /list 查看文章。"); return; } + const post = getPost(slug); + if (!post) { await sendMain(chatId, "❌ 未找到该文章"); return; } + await tg("sendMessage", { + chat_id: chatId, + text: "⚠️ 确认删除《" + post.frontMatter.title + "》?\n\n删除后移入回收站,30天内可恢复。", + parse_mode: "HTML", + reply_markup: { + inline_keyboard: [[ + { text: "✅ 确认删除", callback_data: "confirm_del_" + slug }, + { text: "❌ 取消", callback_data: "cancel_del_" + slug }, + ]], + }, + }); +}; + +handlers.confirm_del = async (chatId, slug) => { + if (!slug) return; + const post = getPost(slug); + if (!post) { await sendMain(chatId, "❌ 未找到该文章"); return; } + moveToRecycle(slug, post.dirPath, post.frontMatter.title); + await sendMain(chatId, "✂ 已移入回收站:" + post.frontMatter.title + "\n30天内可恢复。"); +}; + +handlers.cancel_del = async (chatId) => { + await sendMain(chatId, "已取消删除。"); +}; + +// ---- Articles: List ---- + +handlers.list = async (chatId, pageStr) => { + const page = parseInt(pageStr || "1") || 1; + const perPage = 10; + try { + const { posts, total, totalPages } = listPosts({ page, perPage }); + if (total === 0) { await sendMain(chatId, "📖 还没有文章,发 /new 开始写。"); return; } + const lines = ["📖 文章列表 共 " + total + " 篇 · 第 " + page + "/" + totalPages + " 页\n"]; + for (const p of posts) { + const draft = p.draft ? " 📝" : ""; + lines.push(p.title + draft + "\n " + p.slug + ""); + } + const navRow: { text: string; callback_data: string }[] = []; + if (page > 1) navRow.push({ text: "◀ 上一页", callback_data: "list_" + (page - 1) }); + if (page < totalPages) navRow.push({ text: "下一页 ▶", callback_data: "list_" + (page + 1) }); + await tg("sendMessage", { + chat_id: chatId, text: lines.join("\n"), parse_mode: "HTML", disable_web_page_preview: true, + reply_markup: navRow.length ? { inline_keyboard: [navRow] } : undefined, + }); + } catch (e: unknown) { await sendMain(chatId, "❌ " + (e instanceof Error ? e.message : String(e))); } +}; + +// ---- Articles: View / Title / Author / Preview ---- + +handlers.view = async (chatId, slug) => { + if (!slug) { await sendMain(chatId, "格式:/view slug"); return; } + const ip = getLanIP(); + await tg("sendMessage", { + chat_id: chatId, + text: "🔖 " + slug + "\n\n📱 http://" + ip + ":1313/" + slug + ".html\n💻 http://localhost:1313/" + slug + ".html", + parse_mode: "HTML", disable_web_page_preview: true, + reply_markup: { inline_keyboard: [[{ text: "🌐 浏览器打开", url: "http://localhost:1313/" + slug + ".html" }]] }, + }); +}; + +handlers.title = async (chatId, newTitle) => { + if (!newTitle) { await tg("sendMessage", { chat_id: chatId, text: "请输入新标题:", reply_markup: { force_reply: true, input_field_placeholder: "新标题..." } }); return; } + const s = getSession(chatId); + s.title = newTitle; + await sendWrite(chatId, "✏ 标题已更新:" + newTitle + ""); +}; + +handlers.author = async (chatId, author) => { + getSession(chatId).author = author || DEFAULT_AUTHOR; + await sendWrite(chatId, "✍ 作者:" + getSession(chatId).author); +}; + +handlers.preview = async (chatId) => { + const s = getSession(chatId); + if (!s || !s.title) { await sendMain(chatId, "还没开始写。发 /new 开始。"); return; } + if (!s.content) { await sendWrite(chatId, "正文为空。"); return; } + await sendWrite(chatId, "📝 " + s.title + "\n\n" + truncateText(s.content, 3800)); +}; + +// ---- Writing: Publish / Draft / Deploy / Sync / Undo / Cancel ---- + +handlers.publish = async (chatId) => { + const s = getSession(chatId); + if (!s || !s.title) { await sendMain(chatId, "还没开始写。发 /new 开始。"); return; } + if (!s.content) { await sendWrite(chatId, "正文为空,先写点内容再发布。"); return; } + try { + const title = s.title; + await saveSession(s, false); + clearSession(chatId); + await sendMain(chatId, "✅ 已保存:" + title + "\n\n发 /deploy 推送到线上"); + } catch (e: unknown) { await sendWrite(chatId, "❌ 保存失败:" + (e instanceof Error ? e.message : String(e))); } +}; + +handlers.draft = async (chatId) => { + const s = getSession(chatId); + if (!s || !s.title) { await sendMain(chatId, "还没开始写。发 /new 开始。"); return; } + if (!s.content) { await sendWrite(chatId, "正文为空,先写点内容再保存。"); return; } + try { + const title = s.title; + await saveSession(s, true); + clearSession(chatId); + await sendMain(chatId, "📄 草稿已保存:" + title + ""); + } catch (e: unknown) { await sendWrite(chatId, "❌ 保存失败:" + (e instanceof Error ? e.message : String(e))); } +}; + +handlers.deploy = async (chatId) => { + await sendMessage(chatId, "⏳ 正在部署...", { reply_markup: { remove_keyboard: true } }); + const result = await deployQueue.enqueue("发布文章"); + if (result.success) await sendMain(chatId, "🚀 已部署!\n\n🌐 https://usj.cc"); + else if (result.conflict) await sendMain(chatId, "⚠️ " + (result.error || "远程冲突")); + else await sendMain(chatId, "❌ 部署失败:" + (result.error || "未知错误")); +}; + +handlers.sync = async (chatId) => { + await sendMessage(chatId, "⏳ 正在同步远程..."); + const result = await deployQueue.sync(); + if (result.success) await sendMain(chatId, "✅ 同步完成\n\n" + (result.output || "").slice(0, 500)); + else await sendMain(chatId, "❌ " + (result.error || "同步失败")); +}; + +handlers.undo = async (chatId) => { + if (!hasSession(chatId)) { await sendMain(chatId, "没有活动的草稿。"); return; } + const ok = undoLastChunk(chatId); + if (!ok) { await sendWrite(chatId, "没有可撤销的内容。"); return; } + const s = getSession(chatId); + await sendWrite(chatId, "↩ 已撤销\n\n📝 " + s.title + "\n\n" + (s.content ? truncateText(s.content, 3800) : "(正文为空)")); +}; + +handlers.cancel = async (chatId) => { + if (!hasSession(chatId)) { await sendMain(chatId, "没有活动的草稿。"); return; } + clearSession(chatId); + await sendMain(chatId, "✂ 草稿已丢弃。"); +}; + +// ---- Links ---- + +handlers.links = async (chatId, pageStr) => { + const page = parseInt(pageStr || "1") || 1; + const perPage = 10; + try { + const allLinks = await cachedLinks(true); + if (allLinks.length === 0) { await sendMain(chatId, "还没有友链。\n\n添加:/link_add url 名称"); return; } + const totalPages = Math.ceil(allLinks.length / perPage); + const p = Math.min(page, totalPages); + const start = (p - 1) * perPage; + const pageLinks = allLinks.slice(start, start + perPage); + const lines = ["🔗 友链 共 " + allLinks.length + " 个 · 第 " + p + "/" + totalPages + " 页\n"]; + for (const l of pageLinks) { + const icon = l.hidden ? "🔒" : "🌐"; + lines.push(icon + " " + (l.name || l.url) + "\n " + l.url); + } + lines.push("\n/link_add /link_edit /link_del /link_toggle"); + const navRow: { text: string; callback_data: string }[] = []; + if (p > 1) navRow.push({ text: "◀ 上一页", callback_data: "links_" + (p - 1) }); + if (p < totalPages) navRow.push({ text: "下一页 ▶", callback_data: "links_" + (p + 1) }); + await tg("sendMessage", { + chat_id: chatId, text: lines.join("\n"), parse_mode: "HTML", disable_web_page_preview: true, + reply_markup: navRow.length ? { inline_keyboard: [navRow] } : undefined, + }); + } catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +handlers.link_add = async (chatId, arg) => { + if (!arg) { await tg("sendMessage", { chat_id: chatId, text: "请输入友链信息:\n格式:url 名称", reply_markup: { force_reply: true, input_field_placeholder: "url 名称" } }); return; } + const parts = arg.split(/\s+/); + let url = parts[0]; + const name = parts.slice(1).join(" ") || url; + if (!url.startsWith("http")) url = "https://" + url; + try { await addLink({ name, url }); invalidateByPrefix("links"); await sendMain(chatId, "✅ 友链已添加:" + name); } + catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +handlers.link_edit = async (chatId, arg) => { + if (!arg) { await sendMain(chatId, "格式:/link_edit url\n用 /links 查看友链。"); return; } + const s = getSession(chatId); + s.originalSlug = arg; + await tg("sendMessage", { + chat_id: chatId, + text: "当前 URL:" + arg + "\n请输入新名称(空则保持不变):", + reply_markup: { force_reply: true, input_field_placeholder: "新名称" }, + }); +}; + +handlers.link_del = async (chatId, url) => { + if (!url) { await sendMain(chatId, "格式:/link_del url"); return; } + try { await deleteLink(url); invalidateByPrefix("links"); await sendMain(chatId, "✅ 已删除:" + url); } + catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +handlers.link_toggle = async (chatId, url) => { + if (!url) { await sendMain(chatId, "格式:/link_toggle url"); return; } + try { + const allLinks = await cachedLinks(true); + const link = allLinks.find((l) => l.url === url); + if (!link) { await sendMain(chatId, "❌ 未找到:" + url); return; } + await updateLink(url, { hidden: !link.hidden }); + invalidateByPrefix("links"); + await sendMain(chatId, (!link.hidden ? "🔒 已隐藏" : "🌐 已显示") + ":" + (link.name || url)); + } catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +// ---- Feeds ---- + +handlers.feeds = async (chatId, pageStr) => { + const page = parseInt(pageStr || "1") || 1; + const perPage = 10; + try { + const allFeeds = await cachedFeeds(); + if (allFeeds.length === 0) { await sendMain(chatId, "还没有订阅源。\n\n添加:/feed_add url"); return; } + const totalPages = Math.ceil(allFeeds.length / perPage); + const p = Math.min(page, totalPages); + const start = (p - 1) * perPage; + const pageFeeds = allFeeds.slice(start, start + perPage); + const lines = ["📡 订阅源 共 " + allFeeds.length + " 个 · 第 " + p + "/" + totalPages + " 页\n"]; + for (const f of pageFeeds) { + lines.push("📰 " + (f.feedTitle || f.url) + "\n " + f.url); + } + lines.push("\n/feed_add /feed_edit /feed_del /feed_health"); + const navRow: { text: string; callback_data: string }[] = []; + if (p > 1) navRow.push({ text: "◀ 上一页", callback_data: "feeds_" + (p - 1) }); + if (p < totalPages) navRow.push({ text: "下一页 ▶", callback_data: "feeds_" + (p + 1) }); + await tg("sendMessage", { + chat_id: chatId, text: lines.join("\n"), parse_mode: "HTML", disable_web_page_preview: true, + reply_markup: navRow.length ? { inline_keyboard: [navRow] } : undefined, + }); + } catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +handlers.feed_add = async (chatId, arg) => { + if (!arg) { await tg("sendMessage", { chat_id: chatId, text: "请输入订阅源 URL:", reply_markup: { force_reply: true, input_field_placeholder: "https://example.com/rss.xml" } }); return; } + const parts = arg.split(/\s+/); + const url = parts[0]; + const feedTitle = parts.slice(1).join(" ") || ""; + try { await addFeed(url, feedTitle); invalidateByPrefix("feeds"); await sendMain(chatId, "✅ 订阅源已添加:" + url); } + catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +handlers.feed_edit = async (chatId, arg) => { + if (!arg) { await sendMain(chatId, "格式:/feed_edit url\n用 /feeds 查看。"); return; } + const s = getSession(chatId); + s.originalSlug = arg; + await tg("sendMessage", { + chat_id: chatId, + text: "当前 URL:" + arg + "\n请输入新的订阅信息:\n格式:新URL 新名称", + reply_markup: { force_reply: true, input_field_placeholder: "新URL 新名称" }, + }); +}; + +handlers.feed_del = async (chatId, url) => { + if (!url) { await sendMain(chatId, "格式:/feed_del url"); return; } + try { await deleteFeed(url); invalidateByPrefix("feeds"); await sendMain(chatId, "✅ 已删除:" + url); } + catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +handlers.feed_health = async (chatId) => { + try { + await sendMessage(chatId, "⏳ 正在检测订阅源..."); + const result = await cachedHealth(); + const lines = ["📊 订阅源健康报告\n", "✅ 存活:" + result.alive + " ❌ 异常:" + result.dead]; + const dead = (result.results || []).filter((r) => !r.ok); + if (dead.length > 0) { + lines.push("\n❌ 异常站点:"); + for (const d of dead.slice(0, 10)) { + lines.push("🔴 " + d.url + " — " + (d.error || String(d.status))); + } + } + await sendMain(chatId, lines.join("\n")); + } catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +// ---- Data ---- + +handlers.stats = async (chatId) => { + try { + const { total } = listPosts({ perPage: 1 }); + const allLinks = await cachedLinks(true); + const allFeeds = await cachedFeeds(); + const visibleLinks = allLinks.filter((l) => !l.hidden).length; + + let extra = ""; + try { + const healthRes = await cachedHealth(); + extra = "\n\n📰 友链博客:" + healthRes.total + " 个"; + } catch (e) { /* ignore */ } + + await sendMain(chatId, + "📊 网站数据\n\n" + + "📝 本地文章:" + total + " 篇\n" + + "🔗 友链:" + visibleLinks + " 个\n" + + "📡 订阅源:" + allFeeds.length + " 个" + + extra + "\n\n🌐 usj.cc" + ); + } catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +handlers.read = async (chatId, limitStr) => { + const limit = parseInt(limitStr || "10") || 10; + try { + const articles = await cachedArticles(Math.min(limit, 20)); + if (articles.length === 0) { await sendMain(chatId, "暂无最新文章。"); return; } + const lines = ["📰 友链最新文章(" + articles.length + " 篇)\n"]; + for (const a of articles) { + lines.push((a.feedName || "未知") + " — " + a.title + "\n " + a.link); + } + await tg("sendMessage", { + chat_id: chatId, text: lines.join("\n"), parse_mode: "HTML", disable_web_page_preview: true, + }); + } catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); } +}; + +export default handlers; diff --git a/write/src/lib/bot/helpers.ts b/write/src/lib/bot/helpers.ts new file mode 100644 index 00000000..81f36cad --- /dev/null +++ b/write/src/lib/bot/helpers.ts @@ -0,0 +1,37 @@ +import path from "path"; +import { CONTENT_DIR } from "@/lib/config"; + +export function makeSlug(title: string): string { + const dateStr = new Date().toISOString().slice(0, 10); + const slugPart = (title || "untitled") + .replace(/[^\w一-鿿㐀-䶿]+/g, "-") + .replace(/^-+|-+$/g, "") + .toLowerCase() || "post"; + return dateStr + "-" + slugPart; +} + +export function sessionSummary(s: { + title: string; + categories: string[]; + tags: string[]; + content: string; +}): string { + const lines = s.content ? s.content.split("\n").filter(Boolean).length : 0; + const chars = s.content.length; + const parts: string[] = []; + parts.push(`\u{1f4dd} ${s.title || "未命名"}`); + if (s.categories.length) parts.push(`\u{1f4c2} ${s.categories.join("、")}`); + if (s.tags.length) parts.push(`\u{1f3f7} ${s.tags.join("、")}`); + if (chars) parts.push(`✍ ${lines} 行 ${chars} 字`); + return parts.filter(Boolean).join(" · "); +} + +export function getArticleDirPath(slug: string): string { + const year = slug.slice(0, 4); + return path.join(CONTENT_DIR, year, slug); +} + +export function truncateText(text: string, maxLen: number): string { + if (text.length <= maxLen) return text; + return text.slice(0, maxLen) + "\n\n... 前 " + maxLen + " 字"; +} diff --git a/write/src/lib/bot/index.ts b/write/src/lib/bot/index.ts new file mode 100644 index 00000000..6c6ecbe7 --- /dev/null +++ b/write/src/lib/bot/index.ts @@ -0,0 +1,21 @@ +import { startPolling } from "./poll"; +import { TG_TOKEN } from "./config"; + +const g = globalThis as Record; + +export async function startBot(): Promise { + if (g.__botStarted) return; + g.__botStarted = true; + + if (!TG_TOKEN) { + console.log("[bot] TG_BOT_TOKEN not configured, bot disabled"); + return; + } + + console.log("[bot] initializing..."); + await startPolling(); +} + +export { TG_TOKEN } from "./config"; +export { sendMessage } from "./tg"; +export { getSession, hasSession, clearSession } from "./sessions"; diff --git a/write/src/lib/bot/poll.ts b/write/src/lib/bot/poll.ts new file mode 100644 index 00000000..21d93069 --- /dev/null +++ b/write/src/lib/bot/poll.ts @@ -0,0 +1,246 @@ +import { TG_TOKEN } from "./config"; +import { tg, allowed, sendMessage } from "./tg"; +import { hasSession, getSession, appendContent } from "./sessions"; +import handlers from "./handlers"; + +const ARG_COMMANDS = new Set([ + "new", "title", "categories", "tags", "author", + "edit", "view", "delete", + "link_add", "link_edit", "link_del", "link_toggle", + "feed_add", "feed_edit", "feed_del", + "list", "links", "feeds", "read", +]); + +const CALLBACK_PREFIXES: Record = { + edit: "edit", + confirm_del: "delete", + cancel_del: "noop", + list: "list", + links: "links", + feeds: "feeds", + skip_cat: "skip_categories", + skip_tag: "skip_tags", + confirm_shutdown: "confirm_shutdown", + cancel_shutdown: "cancel_shutdown", + confirm_reboot: "confirm_reboot", + cancel_reboot: "cancel_reboot", +}; + +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} + +export async function startPolling(): Promise { + if (!TG_TOKEN) { + console.log("[bot] TG_BOT_TOKEN not set, skipping bot startup"); + return; + } + + console.log("[bot] starting Telegram polling..."); + + let lastOffset = 0; + let backoff = 3000; + + async function poll() { + try { + const data = await tg[]>("getUpdates", { + offset: lastOffset + 1, + timeout: 30, + allowed_updates: ["message", "callback_query"], + }); + + if (data.ok && data.result) { + for (const update of data.result) { + lastOffset = update.update_id as number; + + if (update.callback_query) { + const cb = update.callback_query as { + message?: { chat?: { id: number } }; + from?: { id: number }; + id: string; + data?: string; + }; + const cbChatId = cb.message?.chat?.id ?? cb.from?.id ?? 0; + if (cb.data && allowed(cbChatId)) { + await handleInlineCallback(cbChatId, cb.id, cb.data); + } + continue; + } + + const msg = update.message as { + chat?: { id: number }; + photo?: { file_id: string }[]; + text?: string; + caption?: string; + reply_to_message?: { text?: string }; + } | undefined; + if (!msg?.chat?.id) continue; + const chatId = msg.chat.id; + if (!allowed(chatId)) continue; + + if (msg.photo && hasSession(chatId)) { + await handlePhoto(chatId, msg.photo, msg.caption); + continue; + } + + if (!msg.text) continue; + const fullText = msg.text.trim(); + + // ForceReply routing + if (msg.reply_to_message?.text) { + const original = msg.reply_to_message.text; + if (original.includes("请输入文章标题")) { await handleCommand(chatId, "new", fullText); continue; } + if (original.includes("请输入新标题")) { await handleCommand(chatId, "title", fullText); continue; } + if (original.includes("请输入分类")) { await handleCommand(chatId, "categories", fullText); continue; } + if (original.includes("请输入标签")) { await handleCommand(chatId, "tags", fullText); continue; } + if (original.includes("请输入友链信息")) { await handleCommand(chatId, "link_add", fullText); continue; } + if (original.includes("请输入新名称")) { await handleCommand(chatId, "link_edit", fullText); continue; } + if (original.includes("请输入订阅源 URL")) { await handleCommand(chatId, "feed_add", fullText); continue; } + if (original.includes("请输入新的订阅信息")) { await handleCommand(chatId, "feed_edit", fullText); continue; } + } + + const parts = fullText.split("@")[0].split(/\s+/); + const cmdRaw = parts[0].toLowerCase(); + const slashCmd = cmdRaw.startsWith("/") ? cmdRaw.slice(1) : ""; + const slashArg = parts.slice(1).join(" "); + + // In writing session, non-slash text appends to content + if (hasSession(chatId) && !cmdRaw.startsWith("/")) { + try { appendContent(chatId, fullText); } + catch (e) { console.error("[bot:append]", e instanceof Error ? e.message : String(e)); } + continue; + } + + if (cmdRaw.startsWith("/")) { + await handleCommand(chatId, slashCmd, slashArg); + } + } + } + + backoff = 3000; + } catch (e) { + console.error("[bot:poll]", e instanceof Error ? e.message : String(e)); + await sleep(backoff); + backoff = Math.min(backoff * 2, 60000); + } + + await sleep(100); + setImmediate(poll); + } + + // Register commands with Telegram + try { + await tg("setMyCommands", { + commands: [ + { command: "new", description: "写一篇文章" }, + { command: "edit", description: "编辑文章" }, + { command: "delete", description: "删除文章" }, + { command: "list", description: "文章列表" }, + { command: "publish", description: "保存发布" }, + { command: "draft", description: "保存草稿" }, + { command: "deploy", description: "推送到线上" }, + { command: "sync", description: "同步远程" }, + { command: "undo", description: "撤销最后一段" }, + { command: "preview", description: "预览" }, + { command: "links", description: "友链管理" }, + { command: "link_add", description: "添加友链" }, + { command: "link_edit", description: "编辑友链" }, + { command: "link_del", description: "删除友链" }, + { command: "link_toggle", description: "显示/隐藏友链" }, + { command: "feeds", description: "订阅管理" }, + { command: "feed_add", description: "添加订阅源" }, + { command: "feed_edit", description: "编辑订阅源" }, + { command: "feed_del", description: "删除订阅源" }, + { command: "feed_health", description: "订阅源健康检查" }, + { command: "read", description: "友链最新文章" }, + { command: "stats", description: "网站数据" }, + { command: "shutdown", description: "关机" }, + { command: "reboot", description: "重启" }, + { command: "help", description: "帮助" }, + ], + }); + console.log("[bot] commands registered"); + } catch (e) { + console.error("[bot] failed to register commands:", e instanceof Error ? e.message : String(e)); + } + + poll(); +} + +async function handlePhoto(chatId: number, photos: { file_id: string }[], caption?: string) { + try { + const fs = await import("fs"); + const path = await import("path"); + const { CONTENT_DIR } = await import("@/lib/config"); + const { getSession } = await import("./sessions"); + const { getArticleDirPath } = await import("./helpers"); + + const s = getSession(chatId); + const largestPhoto = photos[photos.length - 1]; + const fileRes = await tg<{ file_path?: string }>("getFile", { file_id: largestPhoto.file_id }); + if (!fileRes.ok || !fileRes.result?.file_path) return; + + const filePath = fileRes.result.file_path; + const fileUrl = "https://api.telegram.org/file/bot" + TG_TOKEN + "/" + filePath; + const imgBuffer = await fetch(fileUrl).then((r) => r.arrayBuffer()); + + // Save to article's own folder (Hugo page bundle) + const articleDir = getArticleDirPath(s.slug); + if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true }); + + const ext = filePath.split(".").pop() || "jpg"; + const filename = Date.now() + "-" + largestPhoto.file_id.slice(0, 8) + "." + ext; + fs.writeFileSync(path.join(articleDir, filename), Buffer.from(imgBuffer)); + + const imgCaption = caption || "image"; + appendContent(chatId, `\n![${imgCaption}](${filename})\n`); + await sendMessage(chatId, `📷 图片已保存:${filename}`); + } catch (e) { + console.error("[bot:photo]", e instanceof Error ? e.message : String(e)); + await sendMessage(chatId, "❌ 图片保存失败"); + } +} + +async function handleCommand(chatId: number, cmd: string, arg?: string) { + try { + if (cmd === "noop") return; + if (ARG_COMMANDS.has(cmd)) { + const handler = handlers[cmd]; + if (handler) await handler(chatId, arg); + else await sendMessage(chatId, "❌ 未知命令:/" + cmd); + } else { + const handler = handlers[cmd]; + if (handler) await handler(chatId); + else if (cmd) await sendMessage(chatId, "❌ 未知命令:/" + cmd); + } + } catch (e) { + console.error("[bot:" + cmd + "]", e instanceof Error ? e.message : String(e)); + try { + await sendMessage(chatId, "❌ " + (e instanceof Error ? e.message : "error").slice(0, 200)); + } catch (e) { /* ignore */ } + } +} + +async function handleInlineCallback(chatId: number, queryId: string, data: string) { + let prefix = ""; + let arg = ""; + // First try prefix match (e.g. "edit_slug", "list_2") + for (const p of Object.keys(CALLBACK_PREFIXES)) { + if (data.startsWith(p + "_")) { + prefix = p; + arg = data.slice(p.length + 1); + break; + } + } + // Then try exact match (e.g. "confirm_shutdown", "skip_cat") + if (!prefix && CALLBACK_PREFIXES[data]) { + prefix = data; + } + if (!prefix) { + try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {} + return; + } + const cmd = CALLBACK_PREFIXES[prefix]; + await handleCommand(chatId, cmd, arg); + try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {} +} diff --git a/write/src/lib/bot/sessions.ts b/write/src/lib/bot/sessions.ts new file mode 100644 index 00000000..875b445b --- /dev/null +++ b/write/src/lib/bot/sessions.ts @@ -0,0 +1,77 @@ +import { createPost, updatePost, getPost } from "@/lib/posts"; +import { DEFAULT_AUTHOR } from "@/lib/config"; + +export interface Session { + title: string; + slug: string; + content: string; + categories: string[]; + tags: string[]; + author: string; + draft: boolean; + chunks: string[]; + originalSlug?: string; +} + +const sessions = new Map(); + +export function getSession(chatId: number): Session { + if (!sessions.has(chatId)) { + sessions.set(chatId, { + title: "", + slug: "", + content: "", + categories: [], + tags: [], + author: DEFAULT_AUTHOR, + draft: true, + chunks: [], + }); + } + return sessions.get(chatId)!; +} + +export function hasSession(chatId: number): boolean { + return sessions.has(chatId); +} + +export function clearSession(chatId: number): void { + sessions.delete(chatId); +} + +export function appendContent(chatId: number, text: string): void { + const s = getSession(chatId); + s.chunks.push(text + "\n\n"); + s.content += text + "\n\n"; +} + +export function undoLastChunk(chatId: number): boolean { + const s = getSession(chatId); + if (s.chunks.length === 0) return false; + const lastChunk = s.chunks.pop()!; + const idx = s.content.lastIndexOf(lastChunk); + if (idx >= 0) { + s.content = s.content.slice(0, idx) + s.content.slice(idx + lastChunk.length); + } + return true; +} + +export async function saveSession(s: Session, isDraft: boolean): Promise { + const frontMatter = { + title: s.title, + slug: s.slug, + date: new Date().toISOString().slice(0, 10), + draft: isDraft, + categories: s.categories, + tags: s.tags, + author: s.author, + }; + + const existing = getPost(s.slug); + if (existing) { + const result = updatePost(s.slug, frontMatter, s.content); + if (!result.success) throw new Error("更新文章失败"); + } else { + createPost(frontMatter, s.content); + } +} diff --git a/write/src/lib/bot/tg.ts b/write/src/lib/bot/tg.ts new file mode 100644 index 00000000..4543f30c --- /dev/null +++ b/write/src/lib/bot/tg.ts @@ -0,0 +1,52 @@ +import { API, ALLOWED_IDS } from "./config"; + +interface TgResponse { + ok: boolean; + result?: T; + description?: string; +} + +export async function tg( + method: string, + body: Record +): Promise> { + const res = await fetch(`${API}/${method}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + return res.json() as Promise>; +} + +export function allowed(chatId: number): boolean { + if (ALLOWED_IDS.length === 0) return true; + return ALLOWED_IDS.includes(chatId); +} + +export async function sendMessage( + chatId: number, + text: string, + opts?: Record +): Promise { + const safeText = String(text || ""); + try { + await tg("sendMessage", { + chat_id: chatId, + text: safeText, + parse_mode: "HTML", + disable_web_page_preview: true, + ...opts, + }); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + if (msg.includes("parse") || msg.includes("HTML")) { + await tg("sendMessage", { + chat_id: chatId, + text: safeText, + ...opts, + }); + } else { + throw e; + } + } +} diff --git a/write/src/lib/config.ts b/write/src/lib/config.ts index b78978dd..44c4f24c 100644 --- a/write/src/lib/config.ts +++ b/write/src/lib/config.ts @@ -1,7 +1,9 @@ import path from "path"; +import os from "os"; export const BLOG_ROOT = path.resolve(process.cwd(), ".."); export const CONTENT_DIR = path.join(BLOG_ROOT, "content", "post"); +export const STATIC_DIR = path.join(BLOG_ROOT, "static"); export const RECYCLE_DIR = path.join(process.cwd(), ".recycle"); export const RECYCLE_POSTS_DIR = path.join(RECYCLE_DIR, "posts"); export const RECYCLE_META_DIR = path.join(RECYCLE_DIR, "meta"); @@ -14,3 +16,33 @@ export const RSS_API_BASE = process.env.RSS_API_BASE || "https://api.usj.cc"; export const RSS_API_TOKEN = process.env.RSS_API_TOKEN || ""; export const WECHAT_APP_ID = process.env.WECHAT_APP_ID || ""; export const WECHAT_APP_SECRET = process.env.WECHAT_APP_SECRET || ""; + +// Bot 配置 +export const TG_BOT_TOKEN = process.env.TG_BOT_TOKEN || ""; +export const TG_ALLOWED_IDS: number[] = (process.env.TG_ALLOWED_CHAT_IDS || "") + .split(",").map((s) => s.trim()).filter(Boolean).map(Number); +export const PREFER_IFACE = process.env.PREFER_IFACE || "WLAN"; +export const DEFAULT_AUTHOR = process.env.DEFAULT_AUTHOR || "小赵"; + +export function getLanIP(): string { + const ifs = os.networkInterfaces(); + const candidates: { name: string; address: string }[] = []; + for (const [name, addrs] of Object.entries(ifs)) { + if (!addrs) continue; + for (const a of addrs) { + if (a.family !== "IPv4" || a.internal) continue; + if (a.address.startsWith("169.254.")) continue; + candidates.push({ name, address: a.address }); + } + } + const pref = candidates.find((c) => + c.name.toLowerCase().includes(PREFER_IFACE.toLowerCase()) + ); + if (pref) return pref.address; + const real = candidates.find((c) => { + const n = c.name.toLowerCase(); + return !n.includes("vmware") && !n.includes("virtual") && !n.includes("vethernet"); + }); + if (real) return real.address; + return candidates[0]?.address || "0.0.0.0"; +} diff --git a/write/src/lib/git.ts b/write/src/lib/git.ts new file mode 100644 index 00000000..aeedf914 --- /dev/null +++ b/write/src/lib/git.ts @@ -0,0 +1,137 @@ +import { execFile } from "child_process"; +import { promisify } from "util"; +import { BLOG_ROOT } from "./config"; + +const execFileAsync = promisify(execFile); + +type GitExecError = Error & { + stdout?: string; + stderr?: string; + code?: number; +}; + +export interface DeployResult { + success: boolean; + output?: string; + error?: string; + conflict?: boolean; +} + +interface QueueItem { + message: string; + resolve: (result: DeployResult) => void; + reject: (err: Error) => void; +} + +class GitDeployQueue { + private queue: QueueItem[] = []; + private running = false; + + async enqueue(commitMessage: string): Promise { + return new Promise((resolve, reject) => { + this.queue.push({ message: commitMessage, resolve, reject }); + if (!this.running) this.next(); + }); + } + + async sync(): Promise { + try { + const result = await this.runGit(["pull", "--rebase", "--autostash", "origin", "main"]); + return { success: true, output: result.stdout + result.stderr }; + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + if (this.isRebaseConflict(msg)) { + await this.runGit(["rebase", "--abort"], true); + return { success: false, conflict: true, error: "远程冲突,已放弃合并" }; + } + return { success: false, error: msg }; + } + } + + private async next(): Promise { + const item = this.queue.shift(); + if (!item) { + this.running = false; + return; + } + this.running = true; + try { + const result = await this.execute(item.message); + item.resolve(result); + } catch (e) { + item.reject(e instanceof Error ? e : new Error(String(e))); + } + this.next(); + } + + private async runGit(args: string[], allowFailure = false) { + try { + const { stdout, stderr } = await execFileAsync("git", args, { + cwd: BLOG_ROOT, + windowsHide: true, + maxBuffer: 1024 * 1024, + }); + return { stdout, stderr, code: 0 }; + } catch (error) { + const err = error as GitExecError; + const result = { + stdout: err.stdout || "", + stderr: err.stderr || err.message, + code: typeof err.code === "number" ? err.code : 1, + }; + if (allowFailure) return result; + throw new Error((result.stderr || result.stdout || err.message).trim()); + } + } + + private isRebaseConflict(message: string): boolean { + return /conflict|CONFLICT|Resolve all conflicts|Automatic merge failed|could not apply|rebase/i.test(message); + } + + private async execute(message: string): Promise { + let output = ""; + + // git add + const addResult = await this.runGit(["add", "content"]); + output += addResult.stdout + addResult.stderr; + + // git diff --cached --quiet (exit 1 = staged changes exist) + const diffResult = await this.runGit(["diff", "--cached", "--quiet"], true); + if (diffResult.code === 1) { + const commitResult = await this.runGit(["commit", "-m", message]); + output += commitResult.stdout + commitResult.stderr; + } else if (diffResult.code !== 0) { + return { success: false, error: (diffResult.stderr || diffResult.stdout || "检查暂存区失败").trim() }; + } + + // git pull --rebase --autostash + try { + const pullResult = await this.runGit(["pull", "--rebase", "--autostash", "origin", "main"]); + output += pullResult.stdout + pullResult.stderr; + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + if (this.isRebaseConflict(msg)) { + await this.runGit(["rebase", "--abort"], true); + return { + success: false, + conflict: true, + error: "远程有冲突,已自动放弃合并。请先 /sync 同步,手动解决后再 deploy。", + output: msg.slice(0, 1000), + }; + } + return { success: false, error: msg }; + } + + // git push + try { + const pushResult = await this.runGit(["push"]); + output += pushResult.stdout + pushResult.stderr; + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + + return { success: true, output: output.slice(0, 1000) }; + } +} + +export const deployQueue = new GitDeployQueue(); diff --git a/write/src/lib/hugo.ts b/write/src/lib/hugo.ts new file mode 100644 index 00000000..dbf1a3d9 --- /dev/null +++ b/write/src/lib/hugo.ts @@ -0,0 +1,114 @@ +import { spawn, execSync, type ChildProcess } from "child_process"; +import { BLOG_ROOT } from "./config"; + +class HugoManager { + private process: ChildProcess | null = null; + private ready = false; + private starting = false; + private healthTimer: NodeJS.Timeout | null = null; + + async start(): Promise<{ ok: boolean; alreadyRunning?: boolean }> { + if (this.process && this.ready) { + return { ok: true, alreadyRunning: true }; + } + if (this.starting) { + return { ok: false }; + } + + this.starting = true; + this.ready = false; + + this.process = spawn("hugo", ["server", "--bind", "0.0.0.0", "--port", "1313", "--disableFastRender", "--noHTTPCache"], { + cwd: BLOG_ROOT, + shell: true, + stdio: "pipe", + windowsHide: true, + }); + + this.process.stdout?.on("data", (d: Buffer) => process.stdout.write(`[hugo] ${d}`)); + this.process.stderr?.on("data", (d: Buffer) => process.stderr.write(`[hugo] ${d}`)); + + this.process.on("exit", (code) => { + console.log(`[hugo] exited ${code}`); + this.process = null; + this.ready = false; + this.starting = false; + }); + + // 等待 Hugo 就绪(最多 15 秒) + const ok = await this.waitForReady(15000); + this.starting = false; + if (ok) { + this.ready = true; + this.startHealthCheck(); + console.log("[hugo] ready on 0.0.0.0:1313"); + } + return { ok }; + } + + stop(): void { + this.stopHealthCheck(); + if (!this.process) return; + try { + const pid = this.process.pid; + if (process.platform === "win32" && pid) { + execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" }); + } else { + this.process.kill(); + } + } catch (_e) { /* nothing to kill */ } + this.process = null; + this.ready = false; + this.starting = false; + } + + status(): { running: boolean; starting: boolean } { + return { running: !!(this.process && this.ready), starting: this.starting }; + } + + async ensureRunning(): Promise { + if (this.process && this.ready) return true; + const result = await this.start(); + return result.ok; + } + + private async waitForReady(timeoutMs: number): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (await this.ping()) return true; + await new Promise((r) => setTimeout(r, 1500)); + } + return false; + } + + private async ping(): Promise { + try { + const res = await fetch("http://127.0.0.1:1313", { signal: AbortSignal.timeout(1500) }); + return res.ok || res.status < 500; + } catch (_e) { + return false; + } + } + + private startHealthCheck(): void { + this.stopHealthCheck(); + this.healthTimer = setInterval(async () => { + if (!this.process) return; + const alive = await this.ping(); + if (!alive) { + console.warn("[hugo] health check failed, marking as not ready"); + this.ready = false; + this.stopHealthCheck(); + } + }, 30_000); + } + + private stopHealthCheck(): void { + if (this.healthTimer) { + clearInterval(this.healthTimer); + this.healthTimer = null; + } + } +} + +export const hugo = new HugoManager(); diff --git a/write/src/lib/posts.ts b/write/src/lib/posts.ts index 4664a8ec..cb8b7f9f 100644 --- a/write/src/lib/posts.ts +++ b/write/src/lib/posts.ts @@ -164,7 +164,7 @@ export function createPost(frontMatter: PostFrontMatter, content: string): strin const dirName = `${dateStr}-${titlePart}`; const dirPath = path.join(CONTENT_DIR, year, dirName); - if (fs.existsSync(dirPath)) { + if (fs.existsSync(path.join(dirPath, "index.md"))) { throw new Error(`Post already exists at ${dirPath}`); } diff --git a/write/src/lib/rssapi.ts b/write/src/lib/rssapi.ts index ee5b1c7e..86cafded 100644 --- a/write/src/lib/rssapi.ts +++ b/write/src/lib/rssapi.ts @@ -48,6 +48,23 @@ export interface LinkHealth { error?: string; } +export interface Article { + title: string; + link: string; + pubDate: string; + feedName: string; + siteUrl: string; + favicon: string; + author: string; +} + +export interface HealthResult { + total: number; + alive: number; + dead: number; + results: FeedHealth[]; +} + // ---- Feeds API ---- export async function getFeeds(): Promise { @@ -80,13 +97,17 @@ export async function deleteFeed(url: string): Promise { await rssFetch(`/api/feeds?${qs}`, { method: "DELETE" }); } -export async function getFeedsHealth(): Promise<{ - total: number; alive: number; dead: number; - results: FeedHealth[]; -}> { +export async function getFeedsHealth(): Promise { return rssFetch("/api/health"); } +// ---- Articles API ---- + +export async function getArticles(limit = 10): Promise { + const data = await rssFetch<{ articles: Article[] }>(`/api/articles?limit=${limit}`); + return data.articles || []; +} + // ---- Links API ---- export async function getLinks(all = false): Promise { diff --git a/write/src/types/marked.d.ts b/write/src/types/marked.d.ts new file mode 100644 index 00000000..fe365636 --- /dev/null +++ b/write/src/types/marked.d.ts @@ -0,0 +1,4 @@ +declare module "marked" { + export function marked(src: string): string; + export function parse(src: string): string; +} diff --git a/write/start-silent.bat b/write/start-silent.bat new file mode 100644 index 00000000..26865a33 --- /dev/null +++ b/write/start-silent.bat @@ -0,0 +1,4 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0write" +start /b npm run dev -- -p 8016 diff --git a/write/startup.vbs b/write/startup.vbs new file mode 100644 index 00000000..a9b37fa2 --- /dev/null +++ b/write/startup.vbs @@ -0,0 +1,3 @@ +Set WshShell = CreateObject("WScript.Shell") +WshShell.Run Chr(34) & "E:\GitHub\blog\write\start-silent.bat" & Chr(34), 0, False +Set WshShell = Nothing