diff --git a/bot/_apply_fixes.js b/bot/_apply_fixes.js deleted file mode 100644 index 1c32403f..00000000 --- a/bot/_apply_fixes.js +++ /dev/null @@ -1,105 +0,0 @@ -const fs = require("fs"); -const path = "E:/GitHub/blog/bot/index.js"; -let c = fs.readFileSync(path, "utf8"); - -function findBlock(name) { - var idx = c.indexOf("CMD_HANDLERS." + name); - if (idx === -1) return null; - var depth = 0; - var i = idx; - for (; i < c.length; i++) { - if (c[i] === "{") depth++; - else if (c[i] === "}") { - depth--; - if (depth === 0) break; - } - } - return { start: idx, end: i + 2 }; -} - -// Fix 2: link_toggle -var b = findBlock("link_toggle"); -if (!b) { console.log("ERR: link_toggle not found"); process.exit(1); } -var nb = -"CMD_HANDLERS.link_toggle = async function (chatId, url) {\n" + -" if (!url) return sendMessage(chatId, \"Usage: /link_toggle \\nToggle visibility of a link.\");\n" + -" try {\n" + -" // Fetch all links to find current hidden state\n" + -" const listRes = await fetch(\"http://127.0.0.1:\" + WRITE_PORT + \"/api/rss/links?all=1\");\n" + -" const listData = await listRes.json();\n" + -" var link = (listData.links || []).find(function(l) { return l.url === url; });\n" + -" if (!link) return sendMessage(chatId, \"\u274c Link not found: \" + url);\n" + -" var newHidden = !link.hidden;\n" + -" const res = await fetch(\"http://127.0.0.1:\" + WRITE_PORT + \"/api/rss/links\", {\n" + -" method: \"PUT\",\n" + -" headers: { \"Content-Type\": \"application/json\" },\n" + -" body: JSON.stringify({ oldUrl: url, hidden: newHidden }),\n" + -" });\n" + -" if (!res.ok) throw new Error((await res.json()).error || \"Toggle failed\");\n" + -" var label = newHidden ? \"\uD83D\uDD12 Hidden\" : \"\uD83C\uDF10 Visible\";\n" + -" return sendMessage(chatId, label + \": \" + url);\n" + -" } catch (e) {\n" + -" return sendMessage(chatId, \"\u274c \" + e.message);\n" + -" }\n" + -"};\n"; -c = c.substring(0, b.start) + nb + c.substring(b.end); -console.log("2/4: link_toggle fixed"); - -// Fix 3: feed_check -b = findBlock("feed_check"); -if (!b) { console.log("ERR: feed_check not found"); process.exit(1); } -nb = -"CMD_HANDLERS.feed_check = async function (chatId) {\n" + -" try {\n" + -" // First get all feeds\n" + -" const feedsRes = await fetch(\"http://127.0.0.1:\" + WRITE_PORT + \"/api/rss/feeds\");\n" + -" const feedsData = await feedsRes.json();\n" + -" var feeds = feedsData.feeds || [];\n" + -" if (feeds.length === 0) return sendMessage(chatId, \"No feeds to check.\");\n" + -" var results = [];\n" + -" for (var i = 0; i < feeds.length; i++) {\n" + -" var f = feeds[i];\n" + -" try {\n" + -" var hRes = await fetch(\"http://127.0.0.1:\" + WRITE_PORT + \"/api/rss/feeds?health=\" + encodeURIComponent(f.url));\n" + -" var hData = await hRes.json();\n" + -" results.push({ feedTitle: f.feedTitle, url: f.url, ok: hData.ok, status: hData.status, latency: hData.latency, error: hData.error });\n" + -" } catch (e2) {\n" + -" results.push({ feedTitle: f.feedTitle, url: f.url, ok: false, error: e2.message });\n" + -" }\n" + -" }\n" + -" var lines = [\"\uD83D\uDCE1 Feed Health\\n\"];\n" + -" results.forEach(function(r) {\n" + -" var icon = r.ok ? \"\u2705\" : \"\u274c\";\n" + -" var label = (r.feedTitle || r.url).slice(0, 40);\n" + -" var info = r.ok ? (r.latency || 0) + \"ms\" : (r.error || \"error\");\n" + -" lines.push(icon + \" \" + label + \" (\" + info + \")\");\n" + -" });\n" + -" return sendMessage(chatId, lines.join(\"\\n\"));\n" + -" } catch (e) {\n" + -" return sendMessage(chatId, \"Please /start Write first.\");\n" + -" }\n" + -"};\n"; -c = c.substring(0, b.start) + nb + c.substring(b.end); -console.log("3/4: feed_check fixed"); - -// Fix 4: feed_del -b = findBlock("feed_del"); -if (!b) { console.log("ERR: feed_del not found"); process.exit(1); } -nb = -"CMD_HANDLERS.feed_del = async function (chatId, url) {\n" + -" if (!url) return sendMessage(chatId, \"Usage: /feed_del \\nUse /feeds to find the URL.\");\n" + -" try {\n" + -" const res = await fetch(\"http://127.0.0.1:\" + WRITE_PORT + \"/api/rss/feeds?url=\" + encodeURIComponent(url), {\n" + -" method: \"DELETE\",\n" + -" });\n" + -" if (!res.ok) throw new Error((await res.json()).error || \"Delete failed\");\n" + -" return sendMessage(chatId, \"\u2702 Feed deleted: \" + url);\n" + -" } catch (e) {\n" + -" return sendMessage(chatId, \"\u274c \" + e.message);\n" + -" }\n" + -"};\n"; -c = c.substring(0, b.start) + nb + c.substring(b.end); -console.log("4/4: feed_del fixed"); - -fs.writeFileSync(path, c, "utf8"); -console.log("All fixes applied successfully!"); diff --git a/bot/_apply_fixes2.js b/bot/_apply_fixes2.js deleted file mode 100644 index f13db2e0..00000000 --- a/bot/_apply_fixes2.js +++ /dev/null @@ -1,40 +0,0 @@ -const fs = require("fs"); -const path = "E:/GitHub/blog/bot/index.js"; -let c = fs.readFileSync(path, "utf8"); - -function findBlock(name) { - var idx = c.indexOf("CMD_HANDLERS." + name); - if (idx === -1) return null; - var depth = 0; - var i = idx; - for (; i < c.length; i++) { - if (c[i] === "{") depth++; - else if (c[i] === "}") { - depth--; - if (depth === 0) break; - } - } - return { start: idx, end: i + 2 }; -} - -// Fix 1: link_del (was missed earlier) -var b = findBlock("link_del"); -if (!b) { console.log("ERR: link_del not found"); process.exit(1); } -var nb = -"CMD_HANDLERS.link_del = async function (chatId, url) {\n" + -" if (!url) return sendMessage(chatId, \"Usage: /link_del \\nUse /links to find the URL.\");\n" + -" try {\n" + -" const res = await fetch(\"http://127.0.0.1:\" + WRITE_PORT + \"/api/rss/links?url=\" + encodeURIComponent(url), {\n" + -" method: \"DELETE\",\n" + -" });\n" + -" if (!res.ok) throw new Error((await res.json()).error || \"Delete failed\");\n" + -" return sendMessage(chatId, \"\u2702 Link deleted: \" + url);\n" + -" } catch (e) {\n" + -" return sendMessage(chatId, \"\u274c \" + e.message);\n" + -" }\n" + -"};\n"; -c = c.substring(0, b.start) + nb + c.substring(b.end); -console.log("1/1: link_del fixed"); - -fs.writeFileSync(path, c, "utf8"); -console.log("Done!"); diff --git a/bot/_gen_admin.js b/bot/_gen_admin.js deleted file mode 100644 index 60d7ed49..00000000 --- a/bot/_gen_admin.js +++ /dev/null @@ -1,134 +0,0 @@ -const fs = require("fs"); -const path = require("path"); - -const html = ` - - - - -WriteBot - - - -
-

WriteBot

-
Ctrl+D bookmark this page
- -
Loading...
-
-
-
-
-
-
-
-
-
- - - -
- - -
- - -`; - -const fileContent = `// admin-panel.js - Minimal web admin panel + HTTP control API -const http = require("http"); -const config = require("./config"); -const { getLanIP } = require("./helpers"); -const { startWriteServer, stopWriteServer, getWriteStatus } = require("./write-server"); - -const ADMIN_HTML = \`${html}\`.replace("var port=8016", "var port=\${config.WRITE_PORT}"); - -function createControlServer() { - const server = http.createServer((req, res) => { - const url = new URL(req.url, "http://127.0.0.1:" + config.CONTROL_PORT); - const path = url.pathname; - res.setHeader("Access-Control-Allow-Origin", "*"); - - if (path === "/" && req.method === "GET") { - res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.end(ADMIN_HTML); - } else if (path === "/status" && req.method === "GET") { - res.setHeader("Content-Type", "application/json"); - const s = getWriteStatus(); - s.ip = getLanIP(); - s.port = config.WRITE_PORT; - res.end(JSON.stringify(s)); - } else if (path === "/start" && req.method === "POST") { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify(startWriteServer())); - } else if (path === "/stop" && req.method === "POST") { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ ok: stopWriteServer() })); - } else if (path === "/ip" && req.method === "GET") { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ ip: getLanIP(), port: config.WRITE_PORT })); - } else if (path === "/health" && req.method === "GET") { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ ok: true })); - } else { - res.statusCode = 404; - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ error: "not found" })); - } - }); - - server.listen(config.CONTROL_PORT, "127.0.0.1", () => { - console.log("[control] http://127.0.0.1:" + config.CONTROL_PORT); - }); - - return server; -} - -module.exports = { createControlServer }; -`; - -fs.writeFileSync(path.join(__dirname, "src", "admin-panel.js"), fileContent, "utf8"); -console.log("admin-panel.js written (" + fileContent.length + " bytes)"); diff --git a/bot/_go.js b/bot/_go.js deleted file mode 100644 index 2ffdb8a5..00000000 --- a/bot/_go.js +++ /dev/null @@ -1,3 +0,0 @@ -var fs=require("fs"); -var c=fs.readFileSync("E:/GitHub/blog/bot/index.js","utf8"); -function fb(n){var i=c.indexOf("CMD_HANDLERS."+n);if(i===-1)return null;var d=0;for(var j=i;j|]', '' -replace '\s+', '-' -$folderName = "$dateStr-$safeSlug" - -# 路径结构:content/post/2024/2024-01-01-Slug/index.md -$yearPath = Join-Path "content/post" $year -$folderPath = Join-Path $yearPath $folderName -$filePath = Join-Path $folderPath "index.md" - -# 7. 生成 Front Matter 内容 -$content = @" ---- -title: "$title" -date: $dateTimeStr -slug: $safeSlug -draft: false -categories: $categoriesStr -tags: $tagsStr -author: "$author" ---- - -这里开始写你的精彩内容... - -"@ - -# 8. 写入文件 -try { - # 确保父目录存在 - if (-not (Test-Path "content/post")) { - New-Item -ItemType Directory -Path "content/post" | Out-Null - } - - # 确保年份目录存在 - if (-not (Test-Path $yearPath)) { - New-Item -ItemType Directory -Path $yearPath | Out-Null - } - - # 创建文章专属目录 - if (-not (Test-Path $folderPath)) { - New-Item -ItemType Directory -Path $folderPath | Out-Null - } - - # 检查文件是否已存在 - if (Test-Path $filePath) { - Write-Host "⚠️ 文件已存在: $filePath" -ForegroundColor Yellow - $overwrite = Read-Host "是否覆盖? (y/n)" - if ($overwrite -ne 'y') { - Write-Host "❌ 操作已取消" -ForegroundColor Red - exit - } - } - - # 写入文件 (使用 UTF8 编码) - $content | Set-Content -Path $filePath -Encoding UTF8 - - Write-Host "" - Write-Host "✅ 文章创建成功!" -ForegroundColor Green - Write-Host "📂 文件路径: $filePath" -ForegroundColor Gray - Write-Host "🔗 文章标题: $title" -ForegroundColor Gray -} catch { - Write-Host "❌ 创建失败: $_" -ForegroundColor Red -} diff --git a/patch.ps1 b/patch.ps1 deleted file mode 100644 index f94d6425..00000000 --- a/patch.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -$f = "E:\GitHub\blog\bot\tray.ps1" -$s = [System.IO.File]::ReadAllText($f, [System.Text.Encoding]::UTF8) -# Find the MessageBox catch and replace it -$old = ' [System.Windows.Forms.MessageBox]::Show(' + [char]10 + ' "Node.js' + [char]0x672A + [char]0x5B89 + [char]0x88C5 + [char]0x6216 + [char]0x65E0 + [char]0x6CD5 + [char]0x542F + [char]0x52A8' + [char]0x3002 + '"`n`n$_",' -Write-Host "searching..." -$idx = $s.IndexOf("catch {`n [System.Windows.Forms.MessageBox]") -if ($idx -lt 0) { $idx = $s.IndexOf("MessageBox") } -Write-Host "idx: $idx" -if ($idx -gt 0) { - $before = $s.Substring(0, $idx) - $afterMarker = " exit 1`n}`n`n|notifyIcon" - # find the closing } after the MessageBox catch - $afterIdx = $idx + 200 - $remaining = $s.Substring($afterIdx) - $closePos = $remaining.IndexOf("exit 1") + 7 - $after = $s.Substring($afterIdx + $closePos) - $replacement = "} catch {`n `$log = Join-Path `$BotDir `".tray-error.log`"`n `"tray.ps1 failed: `$_`" | Out-File `$log -Encoding UTF8`n `$notifyIcon.Visible = `$false`n exit 1`n}`n" - $s = $before + $replacement + $after - [System.IO.File]::WriteAllText($f, $s, [System.Text.UTF8Encoding]::new($true)) - Write-Host "PATCHED" -} else { - Write-Host "NOT FOUND" -} \ No newline at end of file diff --git a/tango.css b/tango.css deleted file mode 100644 index 8af635c3..00000000 --- a/tango.css +++ /dev/null @@ -1,86 +0,0 @@ -/* Background */ .bg { background-color:#f8f8f8; } -/* PreWrapper */ .chroma { background-color:#f8f8f8; } -/* Other */ .chroma .x { color:#000 } -/* Error */ .chroma .err { color:#a40000 } -/* CodeLine */ .chroma .cl { } -/* LineLink */ .chroma .lnlinks { outline:none;text-decoration:none;color:inherit } -/* LineTableTD */ .chroma .lntd { vertical-align:top;padding:0;margin:0;border:0; } -/* LineTable */ .chroma .lntable { border-spacing:0;padding:0;margin:0;border:0; } -/* LineHighlight */ .chroma .hl { background-color:#dfdfdf } -/* LineNumbersTable */ .chroma .lnt { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f } -/* LineNumbers */ .chroma .ln { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f } -/* Line */ .chroma .line { display:flex; } -/* Keyword */ .chroma .k { color:#204a87;font-weight:bold } -/* KeywordConstant */ .chroma .kc { color:#204a87;font-weight:bold } -/* KeywordDeclaration */ .chroma .kd { color:#204a87;font-weight:bold } -/* KeywordNamespace */ .chroma .kn { color:#204a87;font-weight:bold } -/* KeywordPseudo */ .chroma .kp { color:#204a87;font-weight:bold } -/* KeywordReserved */ .chroma .kr { color:#204a87;font-weight:bold } -/* KeywordType */ .chroma .kt { color:#204a87;font-weight:bold } -/* Name */ .chroma .n { color:#000 } -/* NameAttribute */ .chroma .na { color:#c4a000 } -/* NameBuiltin */ .chroma .nb { color:#204a87 } -/* NameBuiltinPseudo */ .chroma .bp { color:#3465a4 } -/* NameClass */ .chroma .nc { color:#000 } -/* NameConstant */ .chroma .no { color:#000 } -/* NameDecorator */ .chroma .nd { color:#5c35cc;font-weight:bold } -/* NameEntity */ .chroma .ni { color:#ce5c00 } -/* NameException */ .chroma .ne { color:#c00;font-weight:bold } -/* NameFunction */ .chroma .nf { color:#000 } -/* NameFunctionMagic */ .chroma .fm { color:#000 } -/* NameLabel */ .chroma .nl { color:#f57900 } -/* NameNamespace */ .chroma .nn { color:#000 } -/* NameOther */ .chroma .nx { color:#000 } -/* NameProperty */ .chroma .py { color:#000 } -/* NameTag */ .chroma .nt { color:#204a87;font-weight:bold } -/* NameVariable */ .chroma .nv { color:#000 } -/* NameVariableClass */ .chroma .vc { color:#000 } -/* NameVariableGlobal */ .chroma .vg { color:#000 } -/* NameVariableInstance */ .chroma .vi { color:#000 } -/* NameVariableMagic */ .chroma .vm { color:#000 } -/* Literal */ .chroma .l { color:#000 } -/* LiteralDate */ .chroma .ld { color:#000 } -/* LiteralString */ .chroma .s { color:#4e9a06 } -/* LiteralStringAffix */ .chroma .sa { color:#4e9a06 } -/* LiteralStringBacktick */ .chroma .sb { color:#4e9a06 } -/* LiteralStringChar */ .chroma .sc { color:#4e9a06 } -/* LiteralStringDelimiter */ .chroma .dl { color:#4e9a06 } -/* LiteralStringDoc */ .chroma .sd { color:#8f5902;font-style:italic } -/* LiteralStringDouble */ .chroma .s2 { color:#4e9a06 } -/* LiteralStringEscape */ .chroma .se { color:#4e9a06 } -/* LiteralStringHeredoc */ .chroma .sh { color:#4e9a06 } -/* LiteralStringInterpol */ .chroma .si { color:#4e9a06 } -/* LiteralStringOther */ .chroma .sx { color:#4e9a06 } -/* LiteralStringRegex */ .chroma .sr { color:#4e9a06 } -/* LiteralStringSingle */ .chroma .s1 { color:#4e9a06 } -/* LiteralStringSymbol */ .chroma .ss { color:#4e9a06 } -/* LiteralNumber */ .chroma .m { color:#0000cf;font-weight:bold } -/* LiteralNumberBin */ .chroma .mb { color:#0000cf;font-weight:bold } -/* LiteralNumberFloat */ .chroma .mf { color:#0000cf;font-weight:bold } -/* LiteralNumberHex */ .chroma .mh { color:#0000cf;font-weight:bold } -/* LiteralNumberInteger */ .chroma .mi { color:#0000cf;font-weight:bold } -/* LiteralNumberIntegerLong */ .chroma .il { color:#0000cf;font-weight:bold } -/* LiteralNumberOct */ .chroma .mo { color:#0000cf;font-weight:bold } -/* Operator */ .chroma .o { color:#ce5c00;font-weight:bold } -/* OperatorWord */ .chroma .ow { color:#204a87;font-weight:bold } -/* Punctuation */ .chroma .p { color:#000;font-weight:bold } -/* Comment */ .chroma .c { color:#8f5902;font-style:italic } -/* CommentHashbang */ .chroma .ch { color:#8f5902;font-style:italic } -/* CommentMultiline */ .chroma .cm { color:#8f5902;font-style:italic } -/* CommentSingle */ .chroma .c1 { color:#8f5902;font-style:italic } -/* CommentSpecial */ .chroma .cs { color:#8f5902;font-style:italic } -/* CommentPreproc */ .chroma .cp { color:#8f5902;font-style:italic } -/* CommentPreprocFile */ .chroma .cpf { color:#8f5902;font-style:italic } -/* Generic */ .chroma .g { color:#000 } -/* GenericDeleted */ .chroma .gd { color:#a40000 } -/* GenericEmph */ .chroma .ge { color:#000;font-style:italic } -/* GenericError */ .chroma .gr { color:#ef2929 } -/* GenericHeading */ .chroma .gh { color:#000080;font-weight:bold } -/* GenericInserted */ .chroma .gi { color:#00a000 } -/* GenericOutput */ .chroma .go { color:#000;font-style:italic } -/* GenericPrompt */ .chroma .gp { color:#8f5902 } -/* GenericStrong */ .chroma .gs { color:#000;font-weight:bold } -/* GenericSubheading */ .chroma .gu { color:#800080;font-weight:bold } -/* GenericTraceback */ .chroma .gt { color:#a40000;font-weight:bold } -/* GenericUnderline */ .chroma .gl { color:#000;text-decoration:underline } -/* TextWhitespace */ .chroma .w { color:#f8f8f8;text-decoration:underline } diff --git a/tmp_bot.txt b/tmp_bot.txt deleted file mode 100644 index 6f6a3d46..00000000 --- a/tmp_bot.txt +++ /dev/null @@ -1,17 +0,0 @@ -=== BOT LENGTH: 9922 === -package main - -import ( - "fmt" - "log/slog" - "os" - "os/exec" - "os/signal" - "strings" - "syscall" - "time" - - "writebot/internal/adminpanel" - "writebot/internal/config" - "writebot/internal/helpers" - "writeb \ No newline at end of file diff --git a/tmp_bot2.py b/tmp_bot2.py deleted file mode 100644 index 62d1c2d2..00000000 --- a/tmp_bot2.py +++ /dev/null @@ -1,208 +0,0 @@ -# Write minimal bot-only main.go -bot_main = '''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 += "\n![" + caption + "](" + imgURL + ")\n" - } - } - } - continue - } - - text := tgcore.GetString(msg, "text") - if text == "" { continue } - fullText := strings.TrimSpace(text) - if idx := strings.Index(fullText, "@"); idx >= 0 { - afterAt := fullText[idx:] - if spaceIdx := strings.Index(afterAt, " "); spaceIdx >= 0 { afterAt = afterAt[spaceIdx:] } else { afterAt = "" } - fullText = 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 + "\n" - 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) - } - } -} -''' - -with open(r"E:\GitHub\blog\bot-go\cmd\writebot\main.go", "w", encoding="utf-8") as f: - f.write(bot_main) - -print("Bot main.go: simplified, size:", len(bot_main)) diff --git a/tmp_botcfg.py b/tmp_botcfg.py deleted file mode 100644 index 4decd17b..00000000 --- a/tmp_botcfg.py +++ /dev/null @@ -1,9 +0,0 @@ -base = r"E:\GitHub\blog" -with open(base + "\\bot-go\\internal\\config\\config.go", "rb") as f: - content = f.read() -text = content.decode("utf-8") -text = text.replace("package config", "package botconfig") -text = text.replace("writebot/internal/", "write-server/internal/") -with open(base + "\\write-go\\internal\\botconfig\\config.go", "wb") as f: - f.write(text.encode("utf-8")) -print("botconfig fixed") diff --git a/tmp_botcfg2.py b/tmp_botcfg2.py deleted file mode 100644 index 3f6c2804..00000000 --- a/tmp_botcfg2.py +++ /dev/null @@ -1,80 +0,0 @@ -botcfg = '''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 -} -''' - -import os -dst = r"E:\GitHub\blog\bot-go\internal\botcfg" -os.makedirs(dst, exist_ok=True) -with open(os.path.join(dst, "config.go"), "w", encoding="utf-8") as f: - f.write(botcfg) -print("botcfg created") diff --git a/tmp_check.py b/tmp_check.py deleted file mode 100644 index 8d18de49..00000000 --- a/tmp_check.py +++ /dev/null @@ -1,3 +0,0 @@ -import urllib.request -r = urllib.request.urlopen('http://127.0.0.1:8017/', timeout=5) -print('127.0.0.1 OK:', r.status, len(r.read().decode()), 'bytes') diff --git a/tmp_check2.py b/tmp_check2.py deleted file mode 100644 index bec86423..00000000 --- a/tmp_check2.py +++ /dev/null @@ -1,11 +0,0 @@ -import urllib.request -try: - r = urllib.request.urlopen('http://localhost:8017/', timeout=5) - print('localhost OK:', r.status) -except Exception as e: - print('localhost FAIL:', e) -try: - r = urllib.request.urlopen('http://127.0.0.1:8017/', timeout=5) - print('127.0.0.1 OK:', r.status) -except Exception as e: - print('127.0.0.1 FAIL:', e) diff --git a/tmp_client.go b/tmp_client.go deleted file mode 100644 index d233d0ca..00000000 --- a/tmp_client.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "os" -) - -func main() { - resp, err := http.Get("http://localhost:8016/api/posts") - if err != nil { - fmt.Println("ERROR:", err) - os.Exit(1) - } - defer resp.Body.Close() - fmt.Println("Status:", resp.Status) - body, _ := io.ReadAll(resp.Body) - if len(body) > 500 { - body = body[:500] - } - fmt.Println("Body:", string(body)) -} diff --git a/tmp_client_test.go b/tmp_client_test.go deleted file mode 100644 index d233d0ca..00000000 --- a/tmp_client_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "os" -) - -func main() { - resp, err := http.Get("http://localhost:8016/api/posts") - if err != nil { - fmt.Println("ERROR:", err) - os.Exit(1) - } - defer resp.Body.Close() - fmt.Println("Status:", resp.Status) - body, _ := io.ReadAll(resp.Body) - if len(body) > 500 { - body = body[:500] - } - fmt.Println("Body:", string(body)) -} diff --git a/tmp_diag.py b/tmp_diag.py deleted file mode 100644 index 68680f87..00000000 --- a/tmp_diag.py +++ /dev/null @@ -1,39 +0,0 @@ -import urllib.request, json - -# 1. write-go root -try: - r = urllib.request.urlopen('http://localhost:8016/', timeout=5) - print('8016 / :', r.status) -except Exception as e: - print('8016 / : ERROR -', e) - -# 2. write-go admin page (fe) -try: - r = urllib.request.urlopen('http://localhost:8016/admin', timeout=5) - print('8016 /admin:', r.status) -except Exception as e: - print('8016 /admin: ERROR -', e) - -# 3. bot-go status -try: - r = urllib.request.urlopen('http://127.0.0.1:8017/status', timeout=5) - d = json.loads(r.read().decode()) - print('8017 /status:', d) -except Exception as e: - print('8017 /status: ERROR -', e) - -# 4. bot-go start -try: - req = urllib.request.Request('http://127.0.0.1:8017/start', method='POST') - r = urllib.request.urlopen(req, timeout=5) - print('8017 /start:', r.status, r.read().decode()[:200]) -except Exception as e: - print('8017 /start: ERROR -', e) - -# 5. bot-go stop -try: - req = urllib.request.Request('http://127.0.0.1:8017/stop', method='POST') - r = urllib.request.urlopen(req, timeout=5) - print('8017 /stop:', r.status, r.read().decode()[:200]) -except Exception as e: - print('8017 /stop: ERROR -', e) diff --git a/tmp_extract.py b/tmp_extract.py deleted file mode 100644 index 5308304c..00000000 --- a/tmp_extract.py +++ /dev/null @@ -1,18 +0,0 @@ -# Read bot-go main.go to get TG poll and admin panel startup code -with open(r"E:\GitHub\blog\bot-go\cmd\writebot\main.go", "r", encoding="utf-8", errors="replace") as f: - bot_main = f.read() - -# Read current write-go main.go -with open(r"E:\GitHub\blog\write-go\cmd\writeserver\main.go", "r", encoding="utf-8", errors="replace") as f: - write_main = f.read() - -# Save for inspection -with open(r"E:\GitHub\blog\tmp_bot.txt", "w", encoding="utf-8") as f: - f.write("=== BOT LENGTH: " + str(len(bot_main)) + " ===\n") - f.write(bot_main[:200]) - -with open(r"E:\GitHub\blog\tmp_write.txt", "w", encoding="utf-8") as f: - f.write("=== WRITE LENGTH: " + str(len(write_main)) + " ===\n") - f.write(write_main[:200]) - -print("Extracted both files, lengths:", len(bot_main), len(write_main)) diff --git a/tmp_final.py b/tmp_final.py deleted file mode 100644 index c8ed3e3f..00000000 --- a/tmp_final.py +++ /dev/null @@ -1,46 +0,0 @@ -import urllib.request, json - -# 1. write-go root (admin page) -try: - r = urllib.request.urlopen('http://localhost:8016/', timeout=5) - body = r.read().decode() - print('8016 / :', r.status, 'HTML length:', len(body)) -except Exception as e: - print('8016 / : ERROR -', e) - -# 2. write-go stats -try: - r = urllib.request.urlopen('http://localhost:8016/api/stats', timeout=5) - print('8016 stats:', r.read().decode()) -except Exception as e: - print('8016 stats: ERROR -', e) - -# 3. bot-go health -try: - r = urllib.request.urlopen('http://127.0.0.1:8017/health', timeout=5) - print('8017 health:', r.read().decode()) -except Exception as e: - print('8017 health: ERROR -', e) - -# 4. bot-go status -try: - r = urllib.request.urlopen('http://127.0.0.1:8017/status', timeout=5) - print('8017 status:', r.read().decode()) -except Exception as e: - print('8017 status: ERROR -', e) - -# 5. bot-go start (should say already running) -try: - req = urllib.request.Request('http://127.0.0.1:8017/start', method='POST') - r = urllib.request.urlopen(req, timeout=5) - print('8017 start:', r.read().decode()) -except Exception as e: - print('8017 start: ERROR -', e) - -# 6. bot-go stop (just test it works, then restart) -try: - req = urllib.request.Request('http://127.0.0.1:8017/stop', method='POST') - r = urllib.request.urlopen(req, timeout=5) - print('8017 stop:', r.read().decode()) -except Exception as e: - print('8017 stop: ERROR -', e) diff --git a/tmp_final2.py b/tmp_final2.py deleted file mode 100644 index c467bddb..00000000 --- a/tmp_final2.py +++ /dev/null @@ -1,24 +0,0 @@ -import urllib.request, json - -print("=== WriteServer (8016) ===") -r = urllib.request.urlopen('http://localhost:8016/', timeout=5) -print(" / :", r.status, "- HTML page OK") - -r = urllib.request.urlopen('http://localhost:8016/api/stats', timeout=5) -print(" stats :", r.read().decode()) - -r = urllib.request.urlopen('http://localhost:8016/api/posts?perPage=1', timeout=5) -print(" posts :", r.status, "- OK") - -print("") -print("=== WriteBot (8017) ===") -r = urllib.request.urlopen('http://127.0.0.1:8017/health', timeout=5) -print(" health :", r.read().decode()) - -r = urllib.request.urlopen('http://127.0.0.1:8017/status', timeout=5) -d = json.loads(r.read().decode()) -print(" status : running=" + str(d.get('running')) + " ip=" + str(d.get('ip'))) -print("") -print("Admin Panel: http://127.0.0.1:8017/") -print("WriteServer: http://localhost:8016/") -print("LAN Access : http://" + str(d.get('ip')) + ":8016/") diff --git a/tmp_find.py b/tmp_find.py deleted file mode 100644 index 0442b24b..00000000 --- a/tmp_find.py +++ /dev/null @@ -1,26 +0,0 @@ -# Reconstruct the full main.go by merging: -# 1. The new skeleton from main_new.go -# 2. Handler functions extracted from a known working old copy - -# Since old main.go was overwritten, I need to reconstruct handlers from what we know. -# The handlers are standard CRUD + deploy/hugo/stats etc - about 500 lines. -# Let's take a shortcut: use the write-go API that already works, -# and just add the bot startup in a separate file that doesn't redefine handlers. - -# Better approach: keep old main.go handlers separate in a handlers.go file in cmd/writeserver/ -# and have the new main.go just wire them together. - -# Let me check if the old main.go got saved anywhere -import os -for root, dirs, files in os.walk(r"E:\GitHub\blog\write-go"): - for f in files: - if "main" in f.lower() and f.endswith(".go"): - fp = os.path.join(root, f) - sz = os.path.getsize(fp) - print(f" {fp} ({sz} bytes)") - -print("\nChecking for .bak files...") -for root, dirs, files in os.walk(r"E:\GitHub\blog\write-go"): - for f in files: - if f.endswith(".bak") or "backup" in f.lower(): - print(f" {os.path.join(root, f)}") diff --git a/tmp_fixcfg.py b/tmp_fixcfg.py deleted file mode 100644 index 943e9420..00000000 --- a/tmp_fixcfg.py +++ /dev/null @@ -1,17 +0,0 @@ -# Fix remaining writebot/internal/config -> writebot/internal/botcfg in all bot-go files -import os, re - -for root, dirs, files in os.walk(r"E:\GitHub\blog\bot-go"): - for fn in files: - if fn.endswith(".go"): - fp = os.path.join(root, fn) - with open(fp, "rb") as f: - content = f.read().decode("utf-8") - # Don't modify files in the old internal directories we're replacing - if "old_" in fp: - continue - content = content.replace('"writebot/internal/config"', '"writebot/internal/botcfg"') - content = re.sub(r'writebot/internal/config\b', 'writebot/internal/botcfg', content) - with open(fp, "wb") as f: - f.write(content.encode("utf-8")) -print("All config references updated to botcfg") diff --git a/tmp_fixlines.py b/tmp_fixlines.py deleted file mode 100644 index 858a1900..00000000 --- a/tmp_fixlines.py +++ /dev/null @@ -1,33 +0,0 @@ -path = r"E:\GitHub\blog\bot-go\internal\tghandlers\tghandlers.go" -with open(path, "rb") as f: - content = f.read().decode("utf-8") - -# Fix line 117: non-boolean condition -# The Stop function was using writeserver.StopWriteServer() which returned bool -# Now it's replaced with a SendMessage call which returns error -# Fix: - -old_stop = ''' if tgcore.SendMessage(chatID, "WriteServer stop not available in bot-only mode.", nil) { - tgcore.SendMessage(chatID, "⚡ Write 已停止。", nil) - } else { - tgcore.SendMessage(chatID, "⏸ Write 未运行。", nil) - }''' - -new_stop = ''' tgcore.SendMessage(chatID, "WriteServer 已与 Bot 合并,无需单独启停。", nil)''' - -content = content.replace(old_stop, new_stop) - -# Fix line 578: cannot index result -# Find the problematic line around 578 -lines = content.split("\n") -for i, line in enumerate(lines): - if i >= 575 and i <= 582: - print(f" {i+1}: {line}") - -# Fix writeserver.GetWriteStatus() replacement - it was replaced with a map literal -# but used in an if condition -content = content.replace('if s := map[string]interface{}{"running": true, "starting": false};', 'if s := tgcore.GetWriteStatus();') - -with open(path, "wb") as f: - f.write(content.encode("utf-8")) -print("\nFixed Stop and GetWriteStatus") diff --git a/tmp_fixstop.py b/tmp_fixstop.py deleted file mode 100644 index 90600f51..00000000 --- a/tmp_fixstop.py +++ /dev/null @@ -1,30 +0,0 @@ -# Fix stop function - the old replacement didn't work due to line breaks -path = r"E:\GitHub\blog\bot-go\internal\tghandlers\tghandlers.go" -with open(path, "rb") as f: - content = f.read().decode("utf-8") - -# The Stop function has: -# if tgcore.SendMessage(chatID, "WriteServer stop not available...", nil) { -# This is wrong because SendMessage returns error not bool -# Fix: just call SendMessage directly - -import re - -# Match the whole if block -pattern = r'if tgcore\.SendMessage\(chatID, "WriteServer stop not available in bot-only mode\..*?\n\t\}' -content = re.sub(pattern, 'tgcore.SendMessage(chatID, "WriteServer \u5df2\u4e0e Bot \u5408\u5e76\uff0c\u65e0\u9700\u5355\u72ec\u542f\u505c\u3002", nil)', content, flags=re.DOTALL) - -# Fix the Status function - line 578/578 -# Looking for: if s := map[string]interface{}{"running": true, ...}; !s["running"] -# This was the replacement for writeserver.GetWriteStatus() -old_status = 'if s := map[string]interface{}{"running": true, "starting": false};' -new_status = 's := map[string]interface{}{"running": true, "starting": false}; if' -content = content.replace(old_status, new_status) - -# Also fix other Status references -content = content.replace('"running"].(bool)', '["running"].(bool)') -content = content.replace('s["running"]', 's["running"]') - -with open(path, "wb") as f: - f.write(content.encode("utf-8")) -print("Fixed Stop and Status") diff --git a/tmp_fixtg.py b/tmp_fixtg.py deleted file mode 100644 index d031b228..00000000 --- a/tmp_fixtg.py +++ /dev/null @@ -1,15 +0,0 @@ -import re - -# Fix tgcore - remove helpers dependency -path = r"E:\GitHub\blog\bot-go\internal\tgcore\tgcore.go" -with open(path, "rb") as f: - content = f.read().decode("utf-8") - -content = re.sub(r'\t"writebot/internal/helpers"\r?\n', '', content) -# Replace helpers.CheckPort with inline check -content = content.replace('helpers.CheckPort', 'checkPort') -content = content.replace('helpers.GetLanIP', 'getLanIP') - -with open(path, "wb") as f: - f.write(content.encode("utf-8")) -print("tgcore fixed") diff --git a/tmp_fixth.py b/tmp_fixth.py deleted file mode 100644 index 07944af2..00000000 --- a/tmp_fixth.py +++ /dev/null @@ -1,27 +0,0 @@ -import re - -# Fix tghandlers to remove writeserver/adminpanel deps -path = r"E:\GitHub\blog\bot-go\internal\tghandlers\tghandlers.go" -with open(path, "rb") as f: - content = f.read().decode("utf-8") - -# Remove writeserver import -content = re.sub(r'\t"writebot/internal/writeserver"\r?\n', '', content) - -# Remove adminpanel import -content = re.sub(r'\t"writebot/internal/adminpanel"\r?\n', '', content) - -# Remove helpers import -content = re.sub(r'\t"writebot/internal/helpers"\r?\n', '', content) - -# Remove unused package references in code -# writeserver.StopWriteServer() => no-op (tgcore.SendMessage) -content = content.replace('writeserver.StopWriteServer()', 'tgcore.SendMessage(chatID, "WriteServer stop not available in bot-only mode.", nil)') -content = content.replace('writeserver.StartWriteServer()', 'tgcore.SendMessage(chatID, "WriteServer start not available in bot-only mode.", nil)') -content = content.replace('writeserver.GetWriteStatus()', 'map[string]interface{}{"running": true, "starting": false}') -content = content.replace('writeserver.SetWriteReady(true)', '// noop: merged mode') -content = content.replace('writeserver.StartHugoIfNeeded()', '// noop: handled by writeserver') - -with open(path, "wb") as f: - f.write(content.encode("utf-8")) -print("tghandlers fixed") diff --git a/tmp_fixth2.py b/tmp_fixth2.py deleted file mode 100644 index 5aa67bf3..00000000 --- a/tmp_fixth2.py +++ /dev/null @@ -1,13 +0,0 @@ -path = r"E:\GitHub\blog\bot-go\internal\tghandlers\tghandlers.go" -with open(path, "rb") as f: - content = f.read().decode("utf-8") - -# Replace helpers.GetLanIP -> tgcore.getLanIP (or just inline) -# Replace helpers.CheckPort -> tgcore.checkPort -content = content.replace("helpers.GetLanIP", "tgcore.GetLanIP") -content = content.replace("helpers.CheckPort", "tgcore.CheckPort") -content = content.replace("helpers.", "tgcore.") - -with open(path, "wb") as f: - f.write(content.encode("utf-8")) -print("Fixed helpers refs in tghandlers") diff --git a/tmp_fixutf8.py b/tmp_fixutf8.py deleted file mode 100644 index 5b444672..00000000 --- a/tmp_fixutf8.py +++ /dev/null @@ -1,27 +0,0 @@ -import re, shutil - -# Copy with correct encoding -files_to_fix = [ - ("bot-go/internal/helpers/helpers.go", "write-go/internal/helpers/helpers.go"), - ("bot-go/internal/tgcore/tgcore.go", "write-go/internal/tgcore/tgcore.go"), - ("bot-go/internal/tghandlers/tghandlers.go", "write-go/internal/tghandlers/tghandlers.go"), - ("bot-go/internal/sessions/sessions.go", "write-go/internal/sessions/sessions.go"), - ("bot-go/internal/adminpanel/adminpanel.go", "write-go/internal/adminpanel/adminpanel.go"), - ("bot-go/internal/writeserver/writeserver.go", "write-go/internal/writeserver/writeserver.go"), -] - -base = r"E:\GitHub\blog" - -for src_rel, dst_rel in files_to_fix: - src = base + "\\" + src_rel - dst = base + "\\" + dst_rel - with open(src, "rb") as f: - content = f.read() - text = content.decode("utf-8") - text = re.sub(r'writebot/internal/', 'write-server/internal/', text) - text = text.replace('writebot/internal/config', 'write-server/internal/botconfig') - with open(dst, "wb") as f: - f.write(text.encode("utf-8")) - print(f"OK: {dst_rel}") - -print("All fixed with proper UTF-8") diff --git a/tmp_gen_main.py b/tmp_gen_main.py deleted file mode 100644 index c9292d7b..00000000 --- a/tmp_gen_main.py +++ /dev/null @@ -1,354 +0,0 @@ -# Reconstruct main.go from known-working old version -# We know it had all the API handlers plus withCORS/panic recovery/parseDate fix - -main_go = r'''package main - -import ( - "encoding/json" - "fmt" - "io" - "log/slog" - "mime" - "net" - "net/http" - "net/http/httputil" - "net/url" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strconv" - "strings" - "syscall" - "time" - - "write-server/internal/ai" - "write-server/internal/adminpanel" - "write-server/internal/artalk" - "write-server/internal/botconfig" - "write-server/internal/config" - "write-server/internal/helpers" - "write-server/internal/images" - "write-server/internal/posts" - "write-server/internal/recycle" - "write-server/internal/rssapi" - "write-server/internal/sessions" - "write-server/internal/tgcore" - "write-server/internal/tghandlers" - "write-server/internal/wechat" - "write-server/internal/writeserver" -) - -var hugoStarting bool -var lg *slog.Logger - -func main() { - config.Init() - recycle.InitRecycleBin() - posts.Init() - appCfg := botconfig.Load() - writeserver.Init(appCfg.WritePort, appCfg.WriteDir) - adminpanel.Init(appCfg.WritePort, appCfg.ControlPort) - - lg = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})).With("svc", "writeapp") - - ip := helpers.GetLanIP(appCfg.PreferIface) - lg.Info("WriteApp starting", "writePort", appCfg.WritePort) - - fmt.Println("") - fmt.Println(" =========================================") - fmt.Println(" WriteApp - Blog CMS + Telegram Bot") - fmt.Println(" =========================================") - fmt.Printf(" Frontend : http://127.0.0.1:%d/\n", appCfg.WritePort) - fmt.Printf(" Admin : http://127.0.0.1:%d/admin\n", appCfg.WritePort) - fmt.Printf(" LAN : http://%s:%d/\n", ip, appCfg.WritePort) - fmt.Println(" =========================================") - fmt.Println("") - - mux := http.NewServeMux() - - mux.HandleFunc("/api/posts/batch", handlePostsBatch) - mux.HandleFunc("/api/posts/", handlePostBySlug) - mux.HandleFunc("/api/posts", handlePosts) - mux.HandleFunc("/api/recycle", handleRecycle) - mux.HandleFunc("/api/ai", handleAI) - mux.HandleFunc("/api/images", handleImages) - mux.HandleFunc("/api/upload", handleUpload) - mux.HandleFunc("/api/hugo", handleHugo) - mux.HandleFunc("/api/deploy", handleDeploy) - mux.HandleFunc("/api/stats", handleStats) - mux.HandleFunc("/api/rss/links", handleRSSLinks) - mux.HandleFunc("/api/rss/feeds", handleRSSFeeds) - mux.HandleFunc("/api/rss/wechat", handleWechatHandler) - mux.HandleFunc("/api/comments/stats", handleCommentsStats) - mux.HandleFunc("/api/comments/cleanup", handleCommentsCleanup) - mux.HandleFunc("/api/comments/login", handleCommentsLogin) - mux.HandleFunc("/api/comments/", handleCommentsSub) - mux.HandleFunc("/api/comments", handleComments) - - // Admin panel on /admin - mux.HandleFunc("/admin", handleAdmin) - mux.HandleFunc("/admin/health", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`+"`"+`{"ok":true}`+"`"+`)) - }) - - // Reverse proxy to Next.js for non-API routes - nextURL, _ := url.Parse("http://localhost:3000") - proxy := httputil.NewSingleHostReverseProxy(nextURL) - proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(fallbackHTML)) - } - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/admin") { - http.NotFound(w, r) - return - } - proxy.ServeHTTP(w, r) - }) - - // Auto-start Next.js - go startNextJS() - - // Start Telegram bot - if appCfg.TGToken != "" { - tgcore.Init(appCfg.TGToken) - go runTelegramBot(appCfg) - } else { - lg.Warn("no TG_TOKEN configured, bot disabled") - } - - port := 8016 - if p := os.Getenv("WRITE_PORT"); p != "" { - if n, err := strconv.Atoi(p); err == nil { - port = n - } - } - - // Graceful shutdown - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) - - addr := fmt.Sprintf("0.0.0.0:%d", port) - fmt.Printf("[write-app] listening on %s\n", addr) - go func() { - if err := http.ListenAndServe(addr, withCORS(mux)); err != nil { - lg.Error("server error", "err", err) - } - }() - - sig := <-sigCh - lg.Info("shutting down", "signal", sig.String()) -} - -const fallbackHTML = `+"`"+` - - - - -WriteApp - -

WriteApp

Next.js frontend is starting...

Open Admin

`+"`"+` - -func startNextJS() { - time.Sleep(2 * time.Second) - if _, err := net.DialTimeout("tcp", "127.0.0.1:3000", 2*time.Second); err == nil { - lg.Info("Next.js already running on :3000") - return - } - lg.Info("starting Next.js frontend") - cmd := exec.Command("npx.cmd", "next", "start", "-p", "3000") - cmd.Dir = filepath.Join(config.BLOG_ROOT, "write") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Start(); err != nil { - lg.Warn("failed to start Next.js", "err", err) - } else { - lg.Info("Next.js started", "pid", cmd.Process.Pid) - } -} - -func runTelegramBot(appCfg *botconfig.Config) { - tgcore.TG("setMyCommands", map[string]interface{}{ - "commands": []map[string]string{ - {"command": "start", "description": "启动"}, - {"command": "new", "description": "写新文章"}, - {"command": "list", "description": "文章列表"}, - {"command": "edit", "description": "编辑文章"}, - {"command": "save", "description": "保存草稿"}, - {"command": "publish", "description": "发布到 GitHub"}, - {"command": "stats", "description": "统计"}, - {"command": "help", "description": "帮助"}, - }, - }) - - h := tghandlers.New(&tghandlers.Config{ - WritePort: appCfg.WritePort, - ControlPort: appCfg.ControlPort, - PreferIface: appCfg.PreferIface, - }) - - argCommands := map[string]bool{ - "new": true, "title": true, "categories": true, "tags": true, "author": true, - "edit": true, "view": true, "delete": true, - "link_add": true, "link_del": true, "link_toggle": true, - "feed_add": true, "feed_del": true, - "list": true, "links": true, "feeds": true, - } - - var lastOffset int64 - for { - data, err := tgcore.TG("getUpdates", map[string]interface{}{ - "offset": lastOffset + 1, "timeout": 30, - "allowed_updates": []string{"message", "callback_query"}, - }) - if err != nil { - lg.Warn("tg poll error", "err", err) - time.Sleep(3 * time.Second) - continue - } - ok, _ := data["ok"].(bool) - if !ok { continue } - results, _ := data["result"].([]interface{}) - for _, updateRaw := range results { - update, _ := updateRaw.(map[string]interface{}) - if updateID, ok := update["update_id"].(float64); ok { - lastOffset = int64(updateID) - } - if cbRaw, ok := update["callback_query"]; ok { - cb, _ := cbRaw.(map[string]interface{}) - cbMsg, _ := cb["message"].(map[string]interface{}) - cbChat, _ := cbMsg["chat"].(map[string]interface{}) - cbFrom, _ := cb["from"].(map[string]interface{}) - var cbChatID int64 - if cbChat != nil { cbChatID = tgcore.GetInt64(cbChat, "id") } - if cbChatID == 0 && cbFrom != nil { cbChatID = tgcore.GetInt64(cbFrom, "id") } - queryID := tgcore.GetString(cb, "id") - cbData := tgcore.GetString(cb, "data") - tgcore.HandleCallback(cbChatID, queryID, cbData, h) - continue - } - msg, _ := update["message"].(map[string]interface{}) - if msg == nil { continue } - chat, _ := msg["chat"].(map[string]interface{}) - if chat == nil { continue } - chatID := tgcore.GetInt64(chat, "id") - if !tgcore.Allowed(chatID, appCfg.AllowedIDs) { continue } - if photos, ok := msg["photo"]; ok { - photoArr, _ := photos.([]interface{}) - if len(photoArr) > 0 && sessions.HasSession(chatID) { - s := sessions.GetSession(chatID) - largest := photoArr[len(photoArr)-1].(map[string]interface{}) - fileID := tgcore.GetString(largest, "file_id") - fileRes, err := tgcore.TG("getFile", map[string]interface{}{"file_id": fileID}) - if err == nil && fileRes["ok"] == true { - if fileResult, ok := fileRes["result"].(map[string]interface{}); ok { - filePath := tgcore.GetString(fileResult, "file_path") - caption := tgcore.GetString(msg, "caption") - if caption == "" { caption = "image" } - imgURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", appCfg.TGToken, filePath) - s.Content += "\n![" + caption + "](" + imgURL + ")\n" - } - } - } - continue - } - text := tgcore.GetString(msg, "text") - if text == "" { continue } - fullText := strings.TrimSpace(text) - if idx := strings.Index(fullText, "@"); idx >= 0 { - afterAt := fullText[idx:] - if spaceIdx := strings.Index(afterAt, " "); spaceIdx >= 0 { afterAt = afterAt[spaceIdx:] } else { afterAt = "" } - fullText = fullText[:idx] + afterAt - fullText = strings.TrimSpace(fullText) - } - parts := strings.SplitN(fullText, " ", 2) - slashCmd := ""; slashArg := "" - if strings.HasPrefix(parts[0], "/") { - slashCmd = strings.ToLower(parts[0][1:]) - if len(parts) > 1 { slashArg = parts[1] } - } - if sessions.HasSession(chatID) && !strings.HasPrefix(fullText, "/") { - s := sessions.GetSession(chatID) - s.Content += fullText + "\n" - continue - } - if slashCmd == "" { continue } - lg.Info("tg command", "chat", chatID, "cmd", slashCmd) - handleTGCommand(h, chatID, slashCmd, slashArg, argCommands) - } - } -} - -func handleTGCommand(h *tghandlers.Handlers, chatID int64, cmd, arg string, argCmds map[string]bool) { - defer func() { - if r := recover(); r != nil { lg.Error("handler panic", "cmd", cmd, "panic", r) } - }() - if argCmds[cmd] { - switch cmd { - case "new": h.New(chatID, arg) - case "title": h.Title(chatID, arg) - case "categories": h.Categories(chatID, arg) - case "tags": h.Tags(chatID, arg) - case "author": h.Author(chatID, arg) - case "edit": h.Edit(chatID, arg) - case "view": h.View(chatID, arg) - case "delete": h.Delete(chatID, arg) - case "link_add": h.LinkAdd(chatID, arg) - case "link_del": h.LinkDel(chatID, arg) - case "link_toggle": h.LinkToggle(chatID, arg) - case "feed_add": h.FeedAdd(chatID, arg) - case "feed_del": h.FeedDel(chatID, arg) - case "list": h.List(chatID, arg) - case "links": h.Links(chatID, arg) - case "feeds": h.Feeds(chatID, arg) - default: tgcore.SendMessage(chatID, "? Unknown: /"+cmd, nil) - } - } else { - switch cmd { - case "start": h.Start(chatID) - case "stop": h.Stop(chatID) - case "status": h.Status(chatID) - case "ip": h.IP(chatID) - case "help": h.Help(chatID) - case "save": h.Save(chatID) - case "publish": h.Publish(chatID) - case "draft": h.Draft(chatID) - case "preview": h.Preview(chatID) - case "cancel": h.Cancel(chatID) - case "stats": h.Stats(chatID) - default: tgcore.SendMessage(chatID, "? Unknown: /"+cmd, nil) - } - } -} - -func handleAdmin(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(fmt.Sprintf(adminHTML, helpers.GetLanIP("WLAN")))) -} - -func withCORS(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rec := recover(); rec != nil { - lg.Error("panic in handler", "path", r.URL.Path, "method", r.Method, "panic", fmt.Sprintf("%v", rec)) - http.Error(w, fmt.Sprintf("Internal Server Error: %v", rec), http.StatusInternalServerError) - } - }() - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization") - if r.Method == "OPTIONS" { - w.WriteHeader(204) - return - } - next.ServeHTTP(w, r) - }) -} -''' - -with open(r"E:\GitHub\blog\write-go\cmd\writeserver\main_new.go", "w", encoding="utf-8") as f: - f.write(main_go) - -print("main_new.go written, length:", len(main_go)) diff --git a/tmp_handlers.py b/tmp_handlers.py deleted file mode 100644 index b9901d2c..00000000 --- a/tmp_handlers.py +++ /dev/null @@ -1,396 +0,0 @@ -# Generate handlers.go based on the known API routes -handlers = r'''package main - -import ( - "encoding/json" - "fmt" - "io" - "mime" - "net/http" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - - "write-server/internal/ai" - "write-server/internal/artalk" - "write-server/internal/config" - "write-server/internal/images" - "write-server/internal/posts" - "write-server/internal/recycle" - "write-server/internal/rssapi" - "write-server/internal/wechat" -) - -const adminHTML = ` + "`" + ` - - -WriteApp - -

WriteApp

Blog CMS + Telegram Bot — Go Edition

-
All Systems Running
Port 8016 · LAN: %s
- - - -
` + "`" + ` - -func jsonOK(w http.ResponseWriter, data interface{}) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(data) -} - -func jsonErr(w http.ResponseWriter, status int, msg string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(map[string]string{"error": msg}) -} - -func readJSON(r *http.Request) (map[string]interface{}, error) { - body, _ := io.ReadAll(r.Body) - defer r.Body.Close() - var data map[string]interface{} - err := json.Unmarshal(body, &data) - return data, err -} - -func gs(m map[string]interface{}, key string) string { - if v, ok := m[key]; ok { - if s, ok := v.(string); ok { return s } - } - return "" -} - -func gb(m map[string]interface{}, key string) bool { - if v, ok := m[key]; ok { - if b, ok := v.(bool); ok { return b } - } - return false -} - -func gsa(m map[string]interface{}, key string) []string { - if v, ok := m[key]; ok { - switch arr := v.(type) { - case []interface{}: - var result []string - for _, item := range arr { - if s, ok := item.(string); ok { result = append(result, s) } - } - return result - case []string: - return arr - } - } - return nil -} - -func handlePosts(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - search := r.URL.Query().Get("search") - page, _ := strconv.Atoi(r.URL.Query().Get("page")) - if page < 1 { page = 1 } - perPage, _ := strconv.Atoi(r.URL.Query().Get("perPage")) - if perPage < 1 { perPage = 20 } - includeDrafts := r.URL.Query().Get("includeDrafts") != "false" - recycle.CleanExpired() - list, total, totalPages := posts.ListPosts(search, page, perPage, includeDrafts) - totalCount := posts.GetPostCount() - jsonOK(w, map[string]interface{}{ - "posts": list, "total": total, "totalPages": totalPages, "totalCount": totalCount, - }) - case "POST": - body, _ := readJSON(r) - fmRaw, _ := body["frontMatter"].(map[string]interface{}) - if fmRaw == nil { jsonErr(w, 400, "frontMatter required"); return } - fm := posts.FrontMatter{ - Title: gs(fmRaw, "title"), Date: gs(fmRaw, "date"), Slug: gs(fmRaw, "slug"), - Draft: gb(fmRaw, "draft"), Categories: gsa(fmRaw, "categories"), - Tags: gsa(fmRaw, "tags"), Author: gs(fmRaw, "author"), - Layout: gs(fmRaw, "layout"), AiComment: gs(fmRaw, "ai_comment"), - } - if fm.Layout == "" { fm.Layout = "post" } - if fm.Title == "" || fm.Slug == "" { jsonErr(w, 400, "Title and slug required"); return } - dirPath, err := posts.CreatePost(fm, gs(body, "content")) - if err != nil { jsonErr(w, 500, err.Error()); return } - jsonOK(w, map[string]interface{}{"success": true, "dirPath": dirPath}) - default: - jsonErr(w, 405, "method not allowed") - } -} - -func handlePostBySlug(w http.ResponseWriter, r *http.Request) { - slug := extractSlug(r.URL.Path, "/api/posts/") - switch r.Method { - case "GET": - p := posts.GetPost(slug) - if p == nil { jsonErr(w, 404, "Post not found"); return } - jsonOK(w, p) - case "PUT": - body, _ := readJSON(r) - fmRaw, _ := body["frontMatter"].(map[string]interface{}) - if fmRaw == nil { fmRaw = make(map[string]interface{}) } - var contentPtr *string - if c, ok := body["content"]; ok { - if s, ok := c.(string); ok { contentPtr = &s } - } - result, err := posts.UpdatePost(slug, fmRaw, contentPtr) - if err != nil { jsonErr(w, 404, err.Error()); return } - jsonOK(w, map[string]interface{}{"success": true, "dirPath": result.DirPath}) - case "DELETE": - p := posts.GetPost(slug) - if p == nil { jsonErr(w, 404, "Post not found"); return } - if err := recycle.MoveToRecycle(slug, p.DirPath, p.FrontMatter.Title); err != nil { - jsonErr(w, 500, err.Error()); return - } - jsonOK(w, map[string]interface{}{"success": true}) - default: - jsonErr(w, 405, "method not allowed") - } -} - -func handlePostsBatch(w http.ResponseWriter, r *http.Request) { - if r.Method != "PUT" { jsonErr(w, 405, "method not allowed"); return } - body, _ := readJSON(r) - slugsArr, _ := body["slugs"].([]interface{}) - if len(slugsArr) == 0 { jsonErr(w, 400, "slugs array required"); return } - action := gs(body, "action") - categories := gsa(body, "categories") - tags := gsa(body, "tags") - draft := gb(body, "draft") - var results []string - for _, s := range slugsArr { - slug := fmt.Sprintf("%v", s) - if posts.GetPost(slug) == nil { results = append(results, "Not found: "+slug); continue } - updates := make(map[string]interface{}) - switch action { - case "categories": updates["categories"] = categories - case "tags": updates["tags"] = tags - case "draft": updates["draft"] = draft - case "all": updates["categories"] = categories; updates["tags"] = tags; updates["draft"] = draft - default: jsonErr(w, 400, "invalid action"); return - } - posts.UpdatePost(slug, updates, nil) - results = append(results, "Updated: "+slug) - } - jsonOK(w, map[string]interface{}{"results": results}) -} - -func handleRecycle(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - jsonOK(w, map[string]interface{}{"items": recycle.ListRecycle()}) - case "POST": - body, _ := readJSON(r) - slug := gs(body, "slug") - if slug == "" { jsonErr(w, 400, "slug required"); return } - if recycle.Restore(slug) { jsonOK(w, map[string]interface{}{"success": true}) } else { jsonErr(w, 404, "not found") } - case "DELETE": - body, _ := readJSON(r) - slug := gs(body, "slug") - if slug == "" { jsonErr(w, 400, "slug required"); return } - if recycle.PermanentDelete(slug) { jsonOK(w, map[string]interface{}{"success": true}) } else { jsonErr(w, 404, "not found") } - default: - jsonErr(w, 405, "method not allowed") - } -} - -func handleAI(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } - body, _ := readJSON(r) - prompt := gs(body, "prompt") - if prompt == "" { jsonErr(w, 400, "prompt required"); return } - resp, err := ai.Chat(prompt) - if err != nil { jsonErr(w, 500, err.Error()); return } - jsonOK(w, map[string]interface{}{"response": resp}) -} - -func handleImages(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - jsonOK(w, map[string]interface{}{"images": images.ScanImages()}) - case "DELETE": - body, _ := readJSON(r) - filename := gs(body, "filename") - if filename == "" { jsonErr(w, 400, "filename required"); return } - if images.DeleteImage(filename) { jsonOK(w, map[string]interface{}{"success": true}) } else { jsonErr(w, 404, "not found") } - default: - jsonErr(w, 405, "method not allowed") - } -} - -func handleUpload(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } - r.ParseMultipartForm(32 << 20) - file, header, err := r.FormFile("file") - if err != nil { jsonErr(w, 400, "file required"); return } - defer file.Close() - ext := filepath.Ext(header.Filename) - filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext) - destPath := filepath.Join(config.BLOG_ROOT, "static", "uploads", filename) - os.MkdirAll(filepath.Dir(destPath), 0755) - dst, err := os.Create(destPath) - if err != nil { jsonErr(w, 500, err.Error()); return } - defer dst.Close() - io.Copy(dst, file) - url := "/uploads/" + filename - jsonOK(w, map[string]interface{}{"url": url, "success": true}) -} - -func handleHugo(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } - if hugoStarting { jsonOK(w, map[string]interface{}{"running": false, "starting": true}); return } - hugoStarting = true - defer func() { hugoStarting = false }() - exec.Command("taskkill", "/F", "/IM", "hugo.exe").Run() - cmd := exec.Command("hugo", "serve", "--port", "1313", "--noHTTPCache") - cmd.Dir = config.BLOG_ROOT - if err := cmd.Start(); err != nil { jsonErr(w, 500, err.Error()); return } - jsonOK(w, map[string]interface{}{"running": true, "url": "http://localhost:1313"}) -} - -func handleDeploy(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } - body, _ := readJSON(r) - title := gs(body, "title") - if title == "" { jsonErr(w, 400, "Title required"); return } - runGit := func(args ...string) (string, error) { - cmd := exec.Command("git", args...) - cmd.Dir = config.BLOG_ROOT - out, err := cmd.CombinedOutput() - return string(out), err - } - var output string - if o, err := runGit("add", "content/post", "static"); err != nil { jsonErr(w, 500, string(o)); return } else { output += o } - if _, err := runGit("diff", "--cached", "--quiet"); err != nil { - if o, err2 := runGit("commit", "-m", "发布: "+title); err2 != nil { jsonErr(w, 500, string(o)); return } else { output += o } - } - o, _ := runGit("push", "origin", "main") - output += o - jsonOK(w, map[string]interface{}{"success": true, "output": output}) -} - -func handleStats(w http.ResponseWriter, r *http.Request) { - postCount := posts.GetPostCount() - linkCount := 0; feedCount := 0 - if links, err := rssapi.GetLinks(true); err == nil { linkCount = len(links) } - if feeds, err := rssapi.GetFeeds(); err == nil { feedCount = len(feeds) } - jsonOK(w, map[string]interface{}{"posts": postCount, "feeds": feedCount, "links": linkCount}) -} - -func handleRSSLinks(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - all := r.URL.Query().Get("all") == "1" - links, err := rssapi.GetLinks(all) - if err != nil { jsonErr(w, 500, err.Error()); return } - jsonOK(w, map[string]interface{}{"links": links}) - case "POST": - body, _ := readJSON(r) - url := gs(body, "url"); title := gs(body, "title"); desc := gs(body, "description") - if url == "" || title == "" { jsonErr(w, 400, "url and title required"); return } - rssapi.AddLink(url, title, desc) - jsonOK(w, map[string]interface{}{"success": true}) - case "PUT": - body, _ := readJSON(r) - id := gs(body, "id") - enabled := gb(body, "enabled") - rssapi.ToggleLink(id, enabled) - jsonOK(w, map[string]interface{}{"success": true}) - case "DELETE": - body, _ := readJSON(r) - id := gs(body, "id") - rssapi.DeleteLink(id) - jsonOK(w, map[string]interface{}{"success": true}) - default: - jsonErr(w, 405, "method not allowed") - } -} - -func handleRSSFeeds(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - feeds, err := rssapi.GetFeeds() - if err != nil { jsonErr(w, 500, err.Error()); return } - jsonOK(w, map[string]interface{}{"feeds": feeds}) - case "POST": - body, _ := readJSON(r) - url := gs(body, "url"); title := gs(body, "title") - if url == "" || title == "" { jsonErr(w, 400, "url and title required"); return } - rssapi.AddFeed(url, title) - jsonOK(w, map[string]interface{}{"success": true}) - case "DELETE": - body, _ := readJSON(r) - id := gs(body, "id") - rssapi.DeleteFeed(id) - jsonOK(w, map[string]interface{}{"success": true}) - default: - jsonErr(w, 405, "method not allowed") - } -} - -func handleWechatHandler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - articles, err := wechat.GetArticles() - if err != nil { jsonErr(w, 500, err.Error()); return } - jsonOK(w, map[string]interface{}{"articles": articles}) - case "POST": - msg := r.URL.Query().Get("msg") - jsonOK(w, map[string]interface{}{"ok": true, "msg": msg}) - default: - jsonErr(w, 405, "method not allowed") - } -} - -func handleCommentsStats(w http.ResponseWriter, r *http.Request) { - stats := artalk.GetStats() - jsonOK(w, stats) -} - -func handleCommentsCleanup(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } - body, _ := readJSON(r) - slug := gs(body, "slug") - if slug == "" { jsonErr(w, 400, "slug required"); return } - result := artalk.CleanupComments(slug) - jsonOK(w, result) -} - -func handleCommentsLogin(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } - body, _ := readJSON(r) - name := gs(body, "name"); email := gs(body, "email"); password := gs(body, "password") - if name == "" || email == "" || password == "" { jsonErr(w, 400, "name, email, password required"); return } - result := artalk.Login(name, email, password) - jsonOK(w, result) -} - -func handleCommentsSub(w http.ResponseWriter, r *http.Request) { - slug := extractSlug(r.URL.Path, "/api/comments/") - comments := artalk.GetComments(slug) - jsonOK(w, comments) -} - -func handleComments(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return } - body, _ := readJSON(r) - slug := gs(body, "slug"); content := gs(body, "content"); pageKey := gs(body, "page_key") - if slug == "" { jsonErr(w, 400, "slug required"); return } - result := artalk.PostComment(slug, content, pageKey) - jsonOK(w, result) -} - -func extractSlug(path, prefix string) string { - p := strings.TrimPrefix(path, prefix) - p = strings.TrimSuffix(p, "/") - return p -} -''' - -import "time" # needed below - -with open(r"E:\GitHub\blog\write-go\cmd\writeserver\handlers.go", "w", encoding="utf-8") as f: - f.write(handlers) - -print("handlers.go written, size:", len(handlers)) diff --git a/tmp_imp2.py b/tmp_imp2.py deleted file mode 100644 index 12e68532..00000000 --- a/tmp_imp2.py +++ /dev/null @@ -1,26 +0,0 @@ -path = r"E:\GitHub\blog\bot-go\internal\tgcore\tgcore.go" -with open(path, "rb") as f: - content = f.read().decode("utf-8") - -old = '''import ( - "bytes" - "encoding/json" - "io" - "net/http" - "strings" -)''' -new = '''import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "strings" - "time" -)''' -content = content.replace(old, new) - -with open(path, "wb") as f: - f.write(content.encode("utf-8")) -print("Fixed imports properly") diff --git a/tmp_import.py b/tmp_import.py deleted file mode 100644 index 56ed56af..00000000 --- a/tmp_import.py +++ /dev/null @@ -1,27 +0,0 @@ -path = r"E:\GitHub\blog\bot-go\internal\tgcore\tgcore.go" -with open(path, "rb") as f: - content = f.read().decode("utf-8") - -# Add missing imports -old_import = '''import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strings" -)''' -new_import = '''import ( - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "strings" - "time" -)''' - -content = content.replace(old_import, new_import) - -with open(path, "wb") as f: - f.write(content.encode("utf-8")) -print("Added net, time imports to tgcore") diff --git a/tmp_inline.py b/tmp_inline.py deleted file mode 100644 index e40c45b9..00000000 --- a/tmp_inline.py +++ /dev/null @@ -1,23 +0,0 @@ -# Read helpers.go and append its functions to tgcore.go -helpers_path = r"E:\GitHub\blog\bot-go\internal\helpers\helpers.go" -tgcore_path = r"E:\GitHub\blog\bot-go\internal\tgcore\tgcore.go" - -with open(helpers_path, "rb") as f: - helpers = f.read().decode("utf-8") - -with open(tgcore_path, "rb") as f: - tgcore = f.read().decode("utf-8") - -# Strip package declaration and imports from helpers -helpers = helpers.replace("package helpers\n", "") -# Remove import block -import_end = helpers.find(")\n") -if import_end > 0: - helpers = helpers[import_end+2:] - -# Add helper functions to end of tgcore -tgcore = tgcore.rstrip() + "\n\n// === Inlined from helpers ===\n" + helpers - -with open(tgcore_path, "wb") as f: - f.write(tgcore.encode("utf-8")) -print("helpers inlined into tgcore") diff --git a/tmp_patch.py b/tmp_patch.py deleted file mode 100644 index 063e5487..00000000 --- a/tmp_patch.py +++ /dev/null @@ -1,20 +0,0 @@ -with open(r"E:\GitHub\blog\bot-go\cmd\writebot\main.go", "r", encoding="utf-8", errors="replace") as f: - content = f.read() - -# 1. Remove writeconfig line -import re -content = re.sub(r'\t+writeconfig "write-server/internal/config"\r?\n', '', content) - -# 2. Replace writeconfig.Init() with posts.Init() -content = content.replace('writeconfig.Init()', 'posts.Init()') - -# 3. Add posts import after writeserver -content = content.replace( - '"writebot/internal/writeserver"', - '"writebot/internal/writeserver"\n\t"write-server/pkg/posts"' -) - -with open(r"E:\GitHub\blog\bot-go\cmd\writebot\main.go", "w", encoding="utf-8", errors="replace") as f: - f.write(content) - -print("Done") diff --git a/tmp_patch2.py b/tmp_patch2.py deleted file mode 100644 index 348af4cb..00000000 --- a/tmp_patch2.py +++ /dev/null @@ -1,12 +0,0 @@ -with open(r"E:\GitHub\blog\bot-go\cmd\writebot\main.go", "r", encoding="utf-8", errors="replace") as f: - content = f.read() - -# Fix the import block formatting -content = content.replace('"writebot/internal/writeserver"\n\t"write-server/pkg/posts"\n\n)', '"writebot/internal/writeserver"\n\t"write-server/pkg/posts"\n)') - -# Fix the weird indentation on "os" -content = content.replace('\t\t"os"', '\t"os"') - -with open(r"E:\GitHub\blog\bot-go\cmd\writebot\main.go", "w", encoding="utf-8", errors="replace") as f: - f.write(content) -print("Fixed formatting") diff --git a/tmp_patch3.py b/tmp_patch3.py deleted file mode 100644 index 48e02bc0..00000000 --- a/tmp_patch3.py +++ /dev/null @@ -1,19 +0,0 @@ -with open(r"E:\GitHub\blog\write-go\internal\posts\posts.go", "r", encoding="utf-8", errors="replace") as f: - content = f.read() - -# Simplify Init - just call config.Init, not recycle -old_init = '''func Init() { - config.Init() - recycle.InitRecycleBin() -}''' -new_init = '''func Init() { - config.Init() -}''' -content = content.replace(old_init, new_init) - -# Remove recycle import -content = content.replace('\t"write-server/internal/recycle"\n', '') - -with open(r"E:\GitHub\blog\write-go\internal\posts\posts.go", "w", encoding="utf-8", errors="replace") as f: - f.write(content) -print("Done") diff --git a/tmp_patch4.py b/tmp_patch4.py deleted file mode 100644 index b3b3be99..00000000 --- a/tmp_patch4.py +++ /dev/null @@ -1,84 +0,0 @@ -with open(r"E:\GitHub\blog\write-go\cmd\writeserver\main.go", "r", encoding="utf-8", errors="replace") as f: - content = f.read() - -# Find the root handler and replace it with a reverse proxy to Next.js -# Remove the embedded HTML page approach, replace with reverse proxy - -# 1. Remove the embed directive -content = content.replace('\n//go:embed admin_page.html\nvar adminPageHTML string\n', '\n') -content = content.replace('//go:embed admin_page.html\nvar adminPageHTML string\n', '\n') - -# 2. Remove _ "embed" from imports -content = content.replace('\t_ "embed"\n\t"net"\n', '\t"net"\n') -content = content.replace('\t_ "embed"\n\t"log/slog"\n', '\t"log/slog"\n') - -# 3. Add net/http/httputil and net/url for reverse proxy -content = content.replace( - '\t"net/http"\n', - '\t"net/http"\n\t"net/http/httputil"\n\t"net/url"\n' -) - -# 4. Replace the root handler with reverse proxy -old_root = ''' // Root page - admin dashboard - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) - w.Write([]byte("{\"error\":\"not found\"}")) - return - } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(adminPageHTML)) - })''' - -new_root = ''' // Root and non-API routes -> reverse proxy to Next.js (localhost:3000) - nextURL, _ := url.Parse("http://localhost:3000") - proxy := httputil.NewSingleHostReverseProxy(nextURL) - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/api/") { - // Let API handlers deal with it - http.NotFound(w, r) - return - } - proxy.ServeHTTP(w, r) - })''' - -content = content.replace(old_root, new_root) - -# 5. Add Next.js auto-start goroutine -old_goroutine = 'go autoStartBot()' -new_goroutine = '''go autoStartBot() - go autoStartNextJS()''' - -content = content.replace(old_goroutine, new_goroutine) - -# 6. Add the autoStartNextJS function before the last } -old_end = '\n\taddr := fmt.Sprintf("0.0.0.0:%d", port)' -new_end = ''' -// autoStartNextJS starts Next.js if not running -func autoStartNextJS() { - time.Sleep(1 * time.Second) - // Check if Next.js is already running - if _, err := net.DialTimeout("tcp", "127.0.0.1:3000", 2*time.Second); err == nil { - lg.Info("Next.js already running on :3000") - return - } - lg.Info("starting Next.js frontend") - cmd := exec.Command("npx.cmd", "next", "start", "-p", "3000") - cmd.Dir = filepath.Join(config.BLOG_ROOT, "write") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Start(); err != nil { - lg.Warn("failed to start Next.js", "err", err) - } else { - lg.Info("Next.js started", "pid", cmd.Process.Pid) - } -} - - addr := fmt.Sprintf("0.0.0.0:%d", port)' - -content = content.replace(old_end, new_end) - -with open(r"E:\GitHub\blog\write-go\cmd\writeserver\main.go", "w", encoding="utf-8", errors="replace") as f: - f.write(content) -print("Done") diff --git a/tmp_route2.exe b/tmp_route2.exe deleted file mode 100644 index a1fdccec..00000000 Binary files a/tmp_route2.exe and /dev/null differ diff --git a/tmp_route2.go b/tmp_route2.go deleted file mode 100644 index c1b51b8b..00000000 --- a/tmp_route2.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "log" - "net/http" - "os" -) - -var lg *log.Logger - -func main() { - lg = log.New(os.Stdout, "[TEST] ", log.LstdFlags) - - mux := http.NewServeMux() - mux.HandleFunc("/api/posts/", func(w http.ResponseWriter, r *http.Request) { - lg.Printf("GOT posts/ handler: path=%s method=%s", r.URL.Path, r.Method) - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"handler":"posts/","path":"` + r.URL.Path + `"}`)) - }) - mux.HandleFunc("/api/posts", func(w http.ResponseWriter, r *http.Request) { - lg.Printf("GOT posts handler: path=%s method=%s", r.URL.Path, r.Method) - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"handler":"posts","path":"` + r.URL.Path + `"}`)) - }) - - lg.Println("listening on :8701") - log.Fatal(http.ListenAndServe(":8701", mux)) -} diff --git a/tmp_router_test.go b/tmp_router_test.go deleted file mode 100644 index df5081f2..00000000 --- a/tmp_router_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" -) - -func main() { - mux := http.NewServeMux() - mux.HandleFunc("/api/posts/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "GOT posts/ handler: path=%s\n", r.URL.Path) - }) - mux.HandleFunc("/api/posts", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "GOT posts handler: path=%s\n", r.URL.Path) - }) - log.Fatal(http.ListenAndServe(":8700", mux)) -} diff --git a/tmp_show.py b/tmp_show.py deleted file mode 100644 index 4b27d744..00000000 --- a/tmp_show.py +++ /dev/null @@ -1,11 +0,0 @@ -path = r"E:\GitHub\blog\bot-go\internal\tghandlers\tghandlers.go" -with open(path, "rb") as f: - lines = f.read().decode("utf-8").split("\n") - -# Show lines 115-120 -for i in range(114, 122): - print(f"{i+1}: {repr(lines[i][:80])}") - -# Show lines 575-580 -for i in range(574, 582): - print(f"{i+1}: {repr(lines[i][:80])}") diff --git a/tmp_test.py b/tmp_test.py deleted file mode 100644 index b6168c92..00000000 --- a/tmp_test.py +++ /dev/null @@ -1,8 +0,0 @@ -import urllib.request, urllib.error -try: - r = urllib.request.urlopen('http://localhost:8016/api/posts', timeout=5) - print('STATUS:', r.status) - print(r.read().decode()[:500]) -except urllib.error.HTTPError as e: - print('ERROR STATUS:', e.code) - print(e.read().decode()[:500]) diff --git a/tmp_test2.py b/tmp_test2.py deleted file mode 100644 index 3cd5e0c8..00000000 --- a/tmp_test2.py +++ /dev/null @@ -1,20 +0,0 @@ -import urllib.request, json - -# Test write-go: create a post endpoint -req = urllib.request.Request('http://localhost:8016/api/posts', - data=json.dumps({"frontMatter":{"title":"Test Article","slug":"test-article"},"content":"Hello world"}).encode(), - headers={"Content-Type":"application/json"}, - method='POST') -try: - r = urllib.request.urlopen(req, timeout=5) - print('CREATE POST:', r.status, r.read().decode()[:200]) -except Exception as e: - print('CREATE POST ERROR:', e) - -# Cleanup -req2 = urllib.request.Request('http://localhost:8016/api/posts/test-article', method='DELETE') -try: - r2 = urllib.request.urlopen(req2, timeout=5) - print('DELETE POST:', r2.status, r2.read().decode()[:200]) -except Exception as e: - print('DELETE POST ERROR:', e) diff --git a/tmp_verify.py b/tmp_verify.py deleted file mode 100644 index 52567f02..00000000 --- a/tmp_verify.py +++ /dev/null @@ -1,10 +0,0 @@ -import urllib.request, json -# Quick check via API -r = urllib.request.urlopen('http://localhost:8016/api/stats', timeout=5) -print('stats:', r.read().decode()) - -r = urllib.request.urlopen('http://localhost:8016/api/posts?perPage=3', timeout=5) -data = json.loads(r.read().decode()) -print('posts count:', data['total']) -for p in data['posts']: - print(' -', p['slug'], p['title'][:30]) diff --git a/tmp_write.txt b/tmp_write.txt deleted file mode 100644 index 8fddb7ab..00000000 --- a/tmp_write.txt +++ /dev/null @@ -1,21 +0,0 @@ -=== WRITE LENGTH: 24294 === -package main - -import ( - _ "embed" - "net" - "log/slog" - "encoding/json" - "fmt" - "io" - "log" - "mime" - "net/http" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "time" - - "write-server/internal/a \ No newline at end of file diff --git a/update_css.py b/update_css.py deleted file mode 100644 index acbc32ef..00000000 --- a/update_css.py +++ /dev/null @@ -1,62 +0,0 @@ -import os - -def process_css(filename, scope=''): - if not os.path.exists(filename): - print(f"File not found: {filename}") - return "" - - with open(filename, 'r') as f: - lines = f.readlines() - - new_lines = [] - for line in lines: - line = line.strip() - if not line: continue - - # Split selector and style - parts = line.split('{') - if len(parts) < 2: continue - - selector_part = parts[0].strip() - style_part = '{' + '{'.join(parts[1:]) - - # Add scope to each selector in the comma-separated list - selectors = [s.strip() for s in selector_part.split(',')] - scoped_selectors = [] - - for sel in selectors: - if scope: - # If scope is provided (e.g., [data-theme="dark"]) - # We want: [data-theme="dark"] .chroma ... - if sel.startswith('.bg'): - scoped_selectors.append(f'{scope} {sel}') - elif sel.startswith('.chroma'): - scoped_selectors.append(f'{scope} {sel}') - else: - # Some styles might not start with .chroma or .bg? - # Usually they do in Hugo Chroma output. - scoped_selectors.append(f'{scope} {sel}') - else: - # No scope (Default/Day mode) - scoped_selectors.append(sel) - - new_selector_part = ', '.join(scoped_selectors) - new_lines.append(new_selector_part + ' ' + style_part + '\n') - - return ''.join(new_lines) - -# Generate styles first -# os.system('hugo gen chromastyles --style=friendly > friendly.css') -# os.system('hugo gen chromastyles --style=nord > nord.css') - -friendly = process_css('friendly.css', scope='') # Default -nord = process_css('nord.css', scope='[data-theme="dark"]') # Night - -with open('chroma_final.css', 'w') as f: - f.write('/* Hugo Chroma Syntax Highlighting */\n') - f.write('/* Day Mode (Friendly - Vibrant/Bright) */\n') - f.write(friendly) - f.write('\n/* Night Mode (Nord - Deep/Muted) */\n') - f.write(nord) - -print("chroma_final.css created.") diff --git a/write-go/.gitignore b/write-go/.gitignore deleted file mode 100644 index d4bf51da..00000000 --- a/write-go/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -writeserver.exe -.env - diff --git a/write-go/.recycle/meta/test-article.json b/write-go/.recycle/meta/test-article.json deleted file mode 100644 index 0d722c04..00000000 --- a/write-go/.recycle/meta/test-article.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "deletedAt": "2026-06-01T17:01:39+08:00", - "expiresAt": "2026-07-01T17:01:39+08:00", - "originalPath": "E:\\GitHub\\blog\\content\\post\\2026\\2026-06-01-test-article", - "slug": "test-article", - "title": "Test Article" -} \ No newline at end of file diff --git a/write-go/.recycle/posts/test-article/index.md b/write-go/.recycle/posts/test-article/index.md deleted file mode 100644 index 6ac62f67..00000000 --- a/write-go/.recycle/posts/test-article/index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Test Article -date: 2026-06-01 -slug: test-article -draft: false ---- -Hello world diff --git a/write-go/_create_shortcuts.vbs b/write-go/_create_shortcuts.vbs deleted file mode 100644 index 0102eedc..00000000 --- a/write-go/_create_shortcuts.vbs +++ /dev/null @@ -1,14 +0,0 @@ -Set WshShell = CreateObject("WScript.Shell") -Set lnk = WshShell.CreateShortcut("C:\Users\qunlin\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\WriteBot.lnk") -lnk.TargetPath = "E:\GitHub\blog\bot-go\start_bot.bat" -lnk.WorkingDirectory = "E:\GitHub\blog\bot-go" -lnk.WindowStyle = 7 -lnk.Description = "WriteBot - Telegram Bot + Admin Panel" -lnk.Save - -Set lnk2 = WshShell.CreateShortcut("C:\Users\qunlin\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\WriteServer.lnk") -lnk2.TargetPath = "E:\GitHub\blog\write-go\start_write.bat" -lnk2.WorkingDirectory = "E:\GitHub\blog\write-go" -lnk2.WindowStyle = 7 -lnk2.Description = "Write Server - Blog CMS API" -lnk2.Save diff --git a/write-go/_err.txt b/write-go/_err.txt deleted file mode 100644 index 59cec9e9..00000000 --- a/write-go/_err.txt +++ /dev/null @@ -1,14 +0,0 @@ -panic: runtime error: invalid memory address or nil pointer dereference -[signal 0xc0000005 code=0x0 addr=0x0 pc=0x7ff752d05b18] - -goroutine 1 [running]: -log/slog.(*Logger).Handler(...) - C:/Program Files/Go/src/log/slog/logger.go:121 -log/slog.(*Logger).Enabled(0x7ff752c1ca98?, {0x7ff752fbae60?, 0x7ff7532f9640?}, 0x30?) - C:/Program Files/Go/src/log/slog/logger.go:168 +0x18 -log/slog.(*Logger).log(0x0, {0x7ff752fbae60?, 0x7ff7532f9640?}, 0x0, {0x7ff752f96aad, 0x14}, {0x0, 0x0, 0x0}) - C:/Program Files/Go/src/log/slog/logger.go:244 +0x98 -log/slog.(*Logger).Info(...) - C:/Program Files/Go/src/log/slog/logger.go:209 -main.main() - E:/GitHub/blog/write-go/cmd/writeserver/main.go:39 +0x7f diff --git a/write-go/_fix_inline.py b/write-go/_fix_inline.py deleted file mode 100644 index c3996253..00000000 --- a/write-go/_fix_inline.py +++ /dev/null @@ -1,16 +0,0 @@ -p = r"E:\GitHub\blog\write-go\cmd\writeserver\main.go" -with open(p, "r", encoding="utf-8") as f: - c = f.read() - -# Replace: lg = logutil.Logger -# With: lg = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})).With("svc", "writeserver") -old = "lg = logutil.Logger" -new = 'lg = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})).With("svc", "writeserver")' -c = c.replace(old, new) - -# Also remove logutil import if present -c = c.replace('\t"write-server/internal/logutil"\n', '') - -with open(p, "w", encoding="utf-8", newline="\n") as f: - f.write(c) -print("Fixed: inline slog init") diff --git a/write-go/_fix_lg.py b/write-go/_fix_lg.py deleted file mode 100644 index 3f07cd99..00000000 --- a/write-go/_fix_lg.py +++ /dev/null @@ -1,13 +0,0 @@ -p = r"E:\GitHub\blog\write-go\cmd\writeserver\main.go" -with open(p, "r", encoding="utf-8") as f: - c = f.read() - -# Add "var lg *slog.Logger" back at top level (before main) -c = c.replace( - 'func main() {', - 'var lg *slog.Logger\n\nfunc main() {' -) - -with open(p, "w", encoding="utf-8", newline="\n") as f: - f.write(c) -print("Fixed: added var lg declaration") diff --git a/write-go/_fix_log.py b/write-go/_fix_log.py deleted file mode 100644 index 87095258..00000000 --- a/write-go/_fix_log.py +++ /dev/null @@ -1,11 +0,0 @@ -p = r"E:\GitHub\blog\write-go\cmd\writeserver\main.go" -with open(p, "r", encoding="utf-8") as f: - c = f.read() - -c = c.replace("var log = logutil.Logger", "var lg = logutil.Logger") -c = c.replace("log.Info(", "lg.Info(") -c = c.replace("log.Error(", "lg.Error(") - -with open(p, "w", encoding="utf-8", newline="\n") as f: - f.write(c) -print("Fixed log variable") diff --git a/write-go/_fix_nil.py b/write-go/_fix_nil.py deleted file mode 100644 index f3aad196..00000000 --- a/write-go/_fix_nil.py +++ /dev/null @@ -1,28 +0,0 @@ -p = r"E:\GitHub\blog\write-go\cmd\writeserver\main.go" -with open(p, "r", encoding="utf-8") as f: - c = f.read() - -# Move lg.Info after config.Init() + recycle.InitRecycleBin() -old = '''func main() { -\tlg.Info("WriteServer starting") -\tconfig.Init() -\trecycle.InitRecycleBin() - -\tgo autoStartBot()''' - -new = '''func main() { -\tconfig.Init() -\trecycle.InitRecycleBin() -\tlg = logutil.Logger -\tlg.Info("WriteServer starting") - -\tgo autoStartBot()''' - -c = c.replace(old, new) - -# Also remove the "var lg = logutil.Logger" at top level (now in main) -c = c.replace("var lg = logutil.Logger\n", "") - -with open(p, "w", encoding="utf-8", newline="\n") as f: - f.write(c) -print("Fixed nil logger: lg.Info moved after config.Init()") diff --git a/write-go/_fix_slog.py b/write-go/_fix_slog.py deleted file mode 100644 index c7dd5f13..00000000 --- a/write-go/_fix_slog.py +++ /dev/null @@ -1,13 +0,0 @@ -p = r"E:\GitHub\blog\write-go\cmd\writeserver\main.go" -with open(p, "r", encoding="utf-8") as f: - c = f.read() - -# Need "log/slog" import. It was removed when "log" was dropped. -c = c.replace( - '"encoding/json"', - '"log/slog"\n\t"encoding/json"' -) - -with open(p, "w", encoding="utf-8", newline="\n") as f: - f.write(c) -print("Added slog import") diff --git a/write-go/_out.txt b/write-go/_out.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/write-go/_patch_main.py b/write-go/_patch_main.py deleted file mode 100644 index eef0d21a..00000000 --- a/write-go/_patch_main.py +++ /dev/null @@ -1,71 +0,0 @@ -import os - -p = r"E:\GitHub\blog\write-go\cmd\writeserver\main.go" -with open(p, "r", encoding="utf-8") as f: - c = f.read() - -# 1. Add logutil import -old = '"write-server/internal/images"' -new = '"write-server/internal/logutil"\n\t"write-server/internal/images"' -c = c.replace(old, new, 1) - -# 2. Replace main start to add logging -old_func = 'func main() {\n\tconfig.Init()\n\trecycle.InitRecycleBin()' -new_func = '''var log = logutil.Logger - -func main() { -\tlog.Info("WriteServer starting") -\tconfig.Init() -\trecycle.InitRecycleBin() - -\tgo autoStartBot()''' -c = c.replace(old_func, new_func, 1) - -# 3. Append autoStartBot and helper at end of file -auto_start = ''' - -func autoStartBot() { -\ttime.Sleep(2 * time.Second) -\tbotPort := 8017 -\tif helpersCheckPort(botPort) { -\t\tlog.Info("bot-go already running", "port", botPort) -\t\treturn -\t} -\tlog.Info("bot-go not responding, starting") -\tpaths := []string{ -\t\tfilepath.Join(config.BLOG_ROOT, "bot-go", "writebot.exe"), -\t\tfilepath.Join(config.BLOG_ROOT, "write-go", "writebot.exe"), -\t} -\tfor _, p := range paths { -\t\tif _, err := os.Stat(p); err == nil { -\t\t\tcmd := exec.Command(p) -\t\t\tcmd.Dir = filepath.Dir(p) -\t\t\tif err := cmd.Start(); err != nil { -\t\t\t\tlog.Error("failed to start bot-go", "err", err) -\t\t\t} else { -\t\t\t\tlog.Info("bot-go started", "pid", cmd.Process.Pid) -\t\t\t} -\t\t\treturn -\t\t} -\t} -} - -func helpersCheckPort(port int) bool { -\tconn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Second) -\tif err != nil { -\t\treturn false -\t} -\tconn.Close() -\treturn true -} -''' -c += auto_start - -# 4. Ensure "net" is imported -if '"net"' not in c[:c.find("func main")]: - c = c.replace('import (\n\t"encoding/json"', 'import (\n\t"net"\n\t"encoding/json"', 1) - -with open(p, "w", encoding="utf-8", newline="\n") as f: - f.write(c) -print("Patched write-server main.go") -print("New file size:", len(c.splitlines()), "lines") diff --git a/write-go/cmd/writeserver/admin_page.html b/write-go/cmd/writeserver/admin_page.html deleted file mode 100644 index efc19bf8..00000000 --- a/write-go/cmd/writeserver/admin_page.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - -WriteServer - Blog CMS - - - -
-

WriteServer

-

Blog CMS - Go Edition

-
Server running
-
-
Posts
-
-
RSS Feeds
-
-
Links
-
Recycle Bin
-
-
- - - -
- - - \ No newline at end of file diff --git a/write-go/cmd/writeserver/main.go b/write-go/cmd/writeserver/main.go deleted file mode 100644 index 203a5289..00000000 --- a/write-go/cmd/writeserver/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "write-server/internal/app" - -func main() { - app.New().Run() -} \ No newline at end of file diff --git a/write-go/cmd/writeserver/main_new.go b/write-go/cmd/writeserver/main_new.go deleted file mode 100644 index d4946d03..00000000 --- a/write-go/cmd/writeserver/main_new.go +++ /dev/null @@ -1,345 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "log/slog" - "mime" - "net" - "net/http" - "net/http/httputil" - "net/url" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strconv" - "strings" - "syscall" - "time" - - "write-server/internal/ai" - "write-server/internal/adminpanel" - "write-server/internal/artalk" - "write-server/internal/botconfig" - "write-server/internal/config" - "write-server/internal/helpers" - "write-server/internal/images" - "write-server/internal/posts" - "write-server/internal/recycle" - "write-server/internal/rssapi" - "write-server/internal/sessions" - "write-server/internal/tgcore" - "write-server/internal/tghandlers" - "write-server/internal/wechat" - "write-server/internal/writeserver" -) - -var hugoStarting bool -var lg *slog.Logger - -func main() { - config.Init() - recycle.InitRecycleBin() - posts.Init() - appCfg := botconfig.Load() - writeserver.Init(appCfg.WritePort, appCfg.WriteDir) - adminpanel.Init(appCfg.WritePort, appCfg.ControlPort) - - lg = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})).With("svc", "writeapp") - - ip := helpers.GetLanIP(appCfg.PreferIface) - lg.Info("WriteApp starting", "writePort", appCfg.WritePort) - - fmt.Println("") - fmt.Println(" =========================================") - fmt.Println(" WriteApp - Blog CMS + Telegram Bot") - fmt.Println(" =========================================") - fmt.Printf(" Frontend : http://127.0.0.1:%d/\n", appCfg.WritePort) - fmt.Printf(" Admin : http://127.0.0.1:%d/admin\n", appCfg.WritePort) - fmt.Printf(" LAN : http://%s:%d/\n", ip, appCfg.WritePort) - fmt.Println(" =========================================") - fmt.Println("") - - mux := http.NewServeMux() - - mux.HandleFunc("/api/posts/batch", handlePostsBatch) - mux.HandleFunc("/api/posts/", handlePostBySlug) - mux.HandleFunc("/api/posts", handlePosts) - mux.HandleFunc("/api/recycle", handleRecycle) - mux.HandleFunc("/api/ai", handleAI) - mux.HandleFunc("/api/images", handleImages) - mux.HandleFunc("/api/upload", handleUpload) - mux.HandleFunc("/api/hugo", handleHugo) - mux.HandleFunc("/api/deploy", handleDeploy) - mux.HandleFunc("/api/stats", handleStats) - mux.HandleFunc("/api/rss/links", handleRSSLinks) - mux.HandleFunc("/api/rss/feeds", handleRSSFeeds) - mux.HandleFunc("/api/rss/wechat", handleWechatHandler) - mux.HandleFunc("/api/comments/stats", handleCommentsStats) - mux.HandleFunc("/api/comments/cleanup", handleCommentsCleanup) - mux.HandleFunc("/api/comments/login", handleCommentsLogin) - mux.HandleFunc("/api/comments/", handleCommentsSub) - mux.HandleFunc("/api/comments", handleComments) - - // Admin panel on /admin - mux.HandleFunc("/admin", handleAdmin) - mux.HandleFunc("/admin/health", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`+"`"+`{"ok":true}`+"`"+`)) - }) - - // Reverse proxy to Next.js for non-API routes - nextURL, _ := url.Parse("http://localhost:3000") - proxy := httputil.NewSingleHostReverseProxy(nextURL) - proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(fallbackHTML)) - } - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/admin") { - http.NotFound(w, r) - return - } - proxy.ServeHTTP(w, r) - }) - - // Auto-start Next.js - go startNextJS() - - // Start Telegram bot - if appCfg.TGToken != "" { - tgcore.Init(appCfg.TGToken) - go runTelegramBot(appCfg) - } else { - lg.Warn("no TG_TOKEN configured, bot disabled") - } - - port := 8016 - if p := os.Getenv("WRITE_PORT"); p != "" { - if n, err := strconv.Atoi(p); err == nil { - port = n - } - } - - // Graceful shutdown - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) - - addr := fmt.Sprintf("0.0.0.0:%d", port) - fmt.Printf("[write-app] listening on %s\n", addr) - go func() { - if err := http.ListenAndServe(addr, withCORS(mux)); err != nil { - lg.Error("server error", "err", err) - } - }() - - sig := <-sigCh - lg.Info("shutting down", "signal", sig.String()) -} - -const fallbackHTML = `+"`"+` - - - - -WriteApp - -

WriteApp

Next.js frontend is starting...

Open Admin

`+"`"+` - -func startNextJS() { - time.Sleep(2 * time.Second) - if _, err := net.DialTimeout("tcp", "127.0.0.1:3000", 2*time.Second); err == nil { - lg.Info("Next.js already running on :3000") - return - } - lg.Info("starting Next.js frontend") - cmd := exec.Command("npx.cmd", "next", "start", "-p", "3000") - cmd.Dir = filepath.Join(config.BLOG_ROOT, "write") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Start(); err != nil { - lg.Warn("failed to start Next.js", "err", err) - } else { - lg.Info("Next.js started", "pid", cmd.Process.Pid) - } -} - -func runTelegramBot(appCfg *botconfig.Config) { - tgcore.TG("setMyCommands", map[string]interface{}{ - "commands": []map[string]string{ - {"command": "start", "description": "启动"}, - {"command": "new", "description": "写新文章"}, - {"command": "list", "description": "文章列表"}, - {"command": "edit", "description": "编辑文章"}, - {"command": "save", "description": "保存草稿"}, - {"command": "publish", "description": "发布到 GitHub"}, - {"command": "stats", "description": "统计"}, - {"command": "help", "description": "帮助"}, - }, - }) - - h := tghandlers.New(&tghandlers.Config{ - WritePort: appCfg.WritePort, - ControlPort: appCfg.ControlPort, - PreferIface: appCfg.PreferIface, - }) - - argCommands := map[string]bool{ - "new": true, "title": true, "categories": true, "tags": true, "author": true, - "edit": true, "view": true, "delete": true, - "link_add": true, "link_del": true, "link_toggle": true, - "feed_add": true, "feed_del": true, - "list": true, "links": true, "feeds": true, - } - - var lastOffset int64 - for { - data, err := tgcore.TG("getUpdates", map[string]interface{}{ - "offset": lastOffset + 1, "timeout": 30, - "allowed_updates": []string{"message", "callback_query"}, - }) - if err != nil { - lg.Warn("tg poll error", "err", err) - time.Sleep(3 * time.Second) - continue - } - ok, _ := data["ok"].(bool) - if !ok { continue } - results, _ := data["result"].([]interface{}) - for _, updateRaw := range results { - update, _ := updateRaw.(map[string]interface{}) - if updateID, ok := update["update_id"].(float64); ok { - lastOffset = int64(updateID) - } - if cbRaw, ok := update["callback_query"]; ok { - cb, _ := cbRaw.(map[string]interface{}) - cbMsg, _ := cb["message"].(map[string]interface{}) - cbChat, _ := cbMsg["chat"].(map[string]interface{}) - cbFrom, _ := cb["from"].(map[string]interface{}) - var cbChatID int64 - if cbChat != nil { cbChatID = tgcore.GetInt64(cbChat, "id") } - if cbChatID == 0 && cbFrom != nil { cbChatID = tgcore.GetInt64(cbFrom, "id") } - queryID := tgcore.GetString(cb, "id") - cbData := tgcore.GetString(cb, "data") - tgcore.HandleCallback(cbChatID, queryID, cbData, h) - continue - } - msg, _ := update["message"].(map[string]interface{}) - if msg == nil { continue } - chat, _ := msg["chat"].(map[string]interface{}) - if chat == nil { continue } - chatID := tgcore.GetInt64(chat, "id") - if !tgcore.Allowed(chatID, appCfg.AllowedIDs) { continue } - if photos, ok := msg["photo"]; ok { - photoArr, _ := photos.([]interface{}) - if len(photoArr) > 0 && sessions.HasSession(chatID) { - s := sessions.GetSession(chatID) - largest := photoArr[len(photoArr)-1].(map[string]interface{}) - fileID := tgcore.GetString(largest, "file_id") - fileRes, err := tgcore.TG("getFile", map[string]interface{}{"file_id": fileID}) - if err == nil && fileRes["ok"] == true { - if fileResult, ok := fileRes["result"].(map[string]interface{}); ok { - filePath := tgcore.GetString(fileResult, "file_path") - caption := tgcore.GetString(msg, "caption") - if caption == "" { caption = "image" } - imgURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", appCfg.TGToken, filePath) - s.Content += "\n![" + caption + "](" + imgURL + ")\n" - } - } - } - continue - } - text := tgcore.GetString(msg, "text") - if text == "" { continue } - fullText := strings.TrimSpace(text) - if idx := strings.Index(fullText, "@"); idx >= 0 { - afterAt := fullText[idx:] - if spaceIdx := strings.Index(afterAt, " "); spaceIdx >= 0 { afterAt = afterAt[spaceIdx:] } else { afterAt = "" } - fullText = fullText[:idx] + afterAt - fullText = strings.TrimSpace(fullText) - } - parts := strings.SplitN(fullText, " ", 2) - slashCmd := ""; slashArg := "" - if strings.HasPrefix(parts[0], "/") { - slashCmd = strings.ToLower(parts[0][1:]) - if len(parts) > 1 { slashArg = parts[1] } - } - if sessions.HasSession(chatID) && !strings.HasPrefix(fullText, "/") { - s := sessions.GetSession(chatID) - s.Content += fullText + "\n" - continue - } - if slashCmd == "" { continue } - lg.Info("tg command", "chat", chatID, "cmd", slashCmd) - handleTGCommand(h, chatID, slashCmd, slashArg, argCommands) - } - } -} - -func handleTGCommand(h *tghandlers.Handlers, chatID int64, cmd, arg string, argCmds map[string]bool) { - defer func() { - if r := recover(); r != nil { lg.Error("handler panic", "cmd", cmd, "panic", r) } - }() - if argCmds[cmd] { - switch cmd { - case "new": h.New(chatID, arg) - case "title": h.Title(chatID, arg) - case "categories": h.Categories(chatID, arg) - case "tags": h.Tags(chatID, arg) - case "author": h.Author(chatID, arg) - case "edit": h.Edit(chatID, arg) - case "view": h.View(chatID, arg) - case "delete": h.Delete(chatID, arg) - case "link_add": h.LinkAdd(chatID, arg) - case "link_del": h.LinkDel(chatID, arg) - case "link_toggle": h.LinkToggle(chatID, arg) - case "feed_add": h.FeedAdd(chatID, arg) - case "feed_del": h.FeedDel(chatID, arg) - case "list": h.List(chatID, arg) - case "links": h.Links(chatID, arg) - case "feeds": h.Feeds(chatID, arg) - default: tgcore.SendMessage(chatID, "? Unknown: /"+cmd, nil) - } - } else { - switch cmd { - case "start": h.Start(chatID) - case "stop": h.Stop(chatID) - case "status": h.Status(chatID) - case "ip": h.IP(chatID) - case "help": h.Help(chatID) - case "save": h.Save(chatID) - case "publish": h.Publish(chatID) - case "draft": h.Draft(chatID) - case "preview": h.Preview(chatID) - case "cancel": h.Cancel(chatID) - case "stats": h.Stats(chatID) - default: tgcore.SendMessage(chatID, "? Unknown: /"+cmd, nil) - } - } -} - -func handleAdmin(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(fmt.Sprintf(adminHTML, helpers.GetLanIP("WLAN")))) -} - -func withCORS(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rec := recover(); rec != nil { - lg.Error("panic in handler", "path", r.URL.Path, "method", r.Method, "panic", fmt.Sprintf("%v", rec)) - http.Error(w, fmt.Sprintf("Internal Server Error: %v", rec), http.StatusInternalServerError) - } - }() - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization") - if r.Method == "OPTIONS" { - w.WriteHeader(204) - return - } - next.ServeHTTP(w, r) - }) -} diff --git a/write-go/go.mod b/write-go/go.mod deleted file mode 100644 index ce419dc8..00000000 --- a/write-go/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module write-server - -go 1.26.3 diff --git a/write-go/internal/adminpanel/adminpanel.go b/write-go/internal/adminpanel/adminpanel.go deleted file mode 100644 index 2529e006..00000000 --- a/write-go/internal/adminpanel/adminpanel.go +++ /dev/null @@ -1,158 +0,0 @@ -package adminpanel - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "write-server/internal/helpers" - "write-server/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/write-go/internal/ai/ai.go b/write-go/internal/ai/ai.go deleted file mode 100644 index 5b6004ab..00000000 --- a/write-go/internal/ai/ai.go +++ /dev/null @@ -1,73 +0,0 @@ -package ai - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - - "write-server/internal/config" -) - -var AI_PROMPTS = map[string]string{ - "polish": "\u4f60\u662f\u4e00\u4e2a\u7f16\u8f91\u3002\u6da6\u8272\u4ee5\u4e0b\u6587\u672c\uff0c\u4f7f\u5176\u66f4\u6d41\u7545\u3001\u66f4\u4e13\u4e1a\u3002\u4fdd\u6301\u539f\u610f\u548c\u8bed\u6c14\uff0c\u53ea\u8fd4\u56de\u6da6\u8272\u540e\u7684\u6587\u672c\uff0c\u4e0d\u8981\u89e3\u91ca\u3002", - "expand": "\u4f60\u662f\u4e00\u4e2a\u7f16\u8f91\u3002\u6269\u5c55\u4ee5\u4e0b\u6587\u672c\uff0c\u6dfb\u52a0\u66f4\u591a\u7ec6\u8282\u548c\u89e3\u91ca\uff0c\u540c\u65f6\u4fdd\u6301\u6e05\u6670\u548c\u5f15\u4eba\u5165\u80dc\u3002\u53ea\u8fd4\u56de\u6269\u5c55\u540e\u7684\u6587\u672c\uff0c\u4e0d\u8981\u89e3\u91ca\u3002", - "shorten": "\u4f60\u662f\u4e00\u4e2a\u7f16\u8f91\u3002\u7f29\u77ed\u4ee5\u4e0b\u6587\u672c\uff0c\u4fdd\u6301\u6838\u5fc3\u610f\u601d\uff0c\u4f7f\u8868\u8fbe\u66f4\u7b80\u6d01\u3002\u53ea\u8fd4\u56de\u7f29\u77ed\u540e\u7684\u6587\u672c\uff0c\u4e0d\u8981\u89e3\u91ca\u3002", - "fix": "\u4f60\u662f\u4e00\u4e2a\u7f16\u8f91\u3002\u4fee\u590d\u4ee5\u4e0b\u6587\u672c\u4e2d\u7684\u8bed\u6cd5\u9519\u8bef\u3001\u62fc\u5199\u9519\u8bef\u548c\u4e0d\u81ea\u7136\u8868\u8fbe\uff0c\u4fdd\u6301\u539f\u610f\u3002\u53ea\u8fd4\u56de\u4fee\u6b63\u540e\u7684\u6587\u672c\uff0c\u4e0d\u8981\u89e3\u91ca\u3002", -} - -type ChatMessage struct { - Role string `json:"role"` - Content string `json:"content"` -} - -type ChatRequest struct { - Model string `json:"model"` - Messages []ChatMessage `json:"messages"` -} - -type ChatResponse struct { - Choices []struct { - Message ChatMessage `json:"message"` - } `json:"choices"` -} - -func AIChat(systemPrompt, userMessage string) (string, error) { - if config.DEEPSEEK_API_KEY == "" { - return "", fmt.Errorf("DEEPSEEK_API_KEY not configured") - } - - req := ChatRequest{ - Model: config.DEEPSEEK_MODEL, - Messages: []ChatMessage{ - {Role: "system", Content: systemPrompt}, - {Role: "user", Content: userMessage}, - }, - } - - data, _ := json.Marshal(req) - httpReq, _ := http.NewRequest("POST", config.DEEPSEEK_BASE_URL+"/chat/completions", bytes.NewReader(data)) - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", "Bearer "+config.DEEPSEEK_API_KEY) - - resp, err := http.DefaultClient.Do(httpReq) - if err != nil { - return "", fmt.Errorf("AI request failed: %v", err) - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - if resp.StatusCode != 200 { - return "", fmt.Errorf("AI error: %s", string(body)) - } - - var chatResp ChatResponse - if err := json.Unmarshal(body, &chatResp); err != nil { - return "", err - } - if len(chatResp.Choices) == 0 { - return "", fmt.Errorf("no response from AI") - } - return chatResp.Choices[0].Message.Content, nil -} diff --git a/write-go/internal/app/app.go b/write-go/internal/app/app.go deleted file mode 100644 index 7f97789d..00000000 --- a/write-go/internal/app/app.go +++ /dev/null @@ -1,453 +0,0 @@ -package app - -import ( - "fmt" - "log/slog" - "net" - "net/http" - "net/http/httputil" - "net/url" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strings" - "syscall" - "time" - - "write-server/internal/adminpanel" - "write-server/internal/botconfig" - "write-server/internal/config" - "write-server/internal/helpers" - "write-server/internal/sessions" - "write-server/internal/tgcore" - "write-server/internal/tghandlers" - "write-server/internal/writeserver" - - "write-server/pkg/posts" -) - -var lg *slog.Logger - -type App struct { - cfg *botconfig.Config - controlSrv *http.Server -} - -func New() *App { - return &App{} -} - -func (a *App) Run() { - // Init - config.Init() - posts.Init() - a.cfg = botconfig.Load() - writeserver.Init(a.cfg.WritePort, a.cfg.WriteDir) - adminpanel.Init(a.cfg.WritePort, a.cfg.ControlPort) - tgcore.Init(a.cfg.TGToken) - - lg = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})).With("svc", "writeapp") - lg.Info("WriteApp starting", - "writePort", a.cfg.WritePort, - "controlPort", a.cfg.ControlPort, - ) - - ip := helpers.GetLanIP(a.cfg.PreferIface) - - fmt.Println("") - fmt.Println(" =========================================") - fmt.Println(" WriteApp - Blog CMS + Telegram Bot") - fmt.Println(" =========================================") - fmt.Printf(" Frontend : http://127.0.0.1:%d/\n", a.cfg.WritePort) - fmt.Printf(" Admin : http://127.0.0.1:%d/admin\n", a.cfg.WritePort) - fmt.Printf(" LAN : http://%s:%d/\n", ip, a.cfg.WritePort) - fmt.Println(" =========================================") - fmt.Println("") - - // Root mux on writePort - mux := http.NewServeMux() - - // API routes - mux.HandleFunc("/api/posts/batch", handlePostsBatch) - mux.HandleFunc("/api/posts/", handlePostBySlug) - mux.HandleFunc("/api/posts", handlePosts) - mux.HandleFunc("/api/recycle", handleRecycle) - mux.HandleFunc("/api/ai", handleAI) - mux.HandleFunc("/api/images", handleImages) - mux.HandleFunc("/api/upload", handleUpload) - mux.HandleFunc("/api/hugo", handleHugo) - mux.HandleFunc("/api/deploy", handleDeploy) - mux.HandleFunc("/api/stats", handleStats) - mux.HandleFunc("/api/rss/links", handleRSSLinks) - mux.HandleFunc("/api/rss/feeds", handleRSSFeeds) - mux.HandleFunc("/api/rss/wechat", handleWechatHandler) - mux.HandleFunc("/api/comments/stats", handleCommentsStats) - mux.HandleFunc("/api/comments/cleanup", handleCommentsCleanup) - mux.HandleFunc("/api/comments/login", handleCommentsLogin) - mux.HandleFunc("/api/comments/", handleCommentsSub) - mux.HandleFunc("/api/comments", handleComments) - - // Admin panel route - mux.HandleFunc("/admin", a.handleAdmin) - mux.HandleFunc("/admin/", a.handleAdmin) - mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"ok":true}`)) - }) - mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"running":true,"port":%d,"ip":"%s"}`, a.cfg.WritePort, ip) - }) - - // Reverse proxy to Next.js for everything else - nextURL, _ := url.Parse("http://localhost:3000") - proxy := httputil.NewSingleHostReverseProxy(nextURL) - proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(`WriteApp

WriteApp

Next.js frontend is starting...

Open Admin Panel

`)) - } - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/admin" || strings.HasPrefix(r.URL.Path, "/admin/") || r.URL.Path == "/health" || r.URL.Path == "/status" { - http.NotFound(w, r) - return - } - proxy.ServeHTTP(w, r) - }) - - // Start Next.js - go a.startNextJS() - - // Start Telegram bot - go a.startTelegramBot() - - // HTTP server - addr := fmt.Sprintf("0.0.0.0:%d", a.cfg.WritePort) - go func() { - lg.Info("HTTP server listening", "addr", addr) - if err := http.ListenAndServe(addr, withCORS(mux)); err != nil { - lg.Error("HTTP server error", "err", err) - } - }() - - // Wait for interrupt - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) - sig := <-sigCh - lg.Info("shutting down", "signal", sig.String()) -} - -func (a *App) startNextJS() { - time.Sleep(2 * time.Second) - if _, err := net.DialTimeout("tcp", "127.0.0.1:3000", 2*time.Second); err == nil { - lg.Info("Next.js already running on :3000") - return - } - lg.Info("starting Next.js frontend") - cmd := exec.Command("npx.cmd", "next", "start", "-p", "3000") - cmd.Dir = filepath.Join(config.BLOG_ROOT, "write") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Start(); err != nil { - lg.Warn("failed to start Next.js", "err", err) - } else { - lg.Info("Next.js started", "pid", cmd.Process.Pid) - } -} - -func (a *App) startTelegramBot() { - if a.cfg.TGToken == "" { - lg.Warn("no TG_TOKEN configured, bot disabled") - return - } - - time.Sleep(1 * time.Second) - - // 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 { - username := tgcore.GetString(result, "username") - lg.Info("telegram connected", "username", username) - } - } - tgcore.TG("setMyCommands", map[string]interface{}{ - "commands": []map[string]string{ - {"command": "start", "description": "启动写作后台"}, - {"command": "new", "description": "开始写一篇"}, - {"command": "edit", "description": "编辑已有文章"}, - {"command": "list", "description": "文章列表"}, - {"command": "save", "description": "保存草稿到本地"}, - {"command": "publish", "description": "发布到 GitHub"}, - {"command": "stats", "description": "网站数据统计"}, - {"command": "preview", "description": "预览全文"}, - {"command": "links", "description": "友链列表"}, - {"command": "feeds", "description": "订阅源列表"}, - {"command": "help", "description": "全部命令"}, - }, - }) - lg.Info("telegram commands registered") - } else { - lg.Warn("telegram connection failed", "err", err) - } - - h := tghandlers.New(&tghandlers.Config{ - WritePort: a.cfg.WritePort, - ControlPort: a.cfg.ControlPort, - PreferIface: a.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, - } - - var lastOffset int64 - - for { - data, err := tgcore.TG("getUpdates", map[string]interface{}{ - "offset": lastOffset + 1, - "timeout": 30, - "allowed_updates": []string{"message", "callback_query"}, - }) - if err != nil { - lg.Warn("tg poll error", "err", err) - time.Sleep(3 * time.Second) - continue - } - - ok, _ := data["ok"].(bool) - if !ok { - continue - } - - results, _ := data["result"].([]interface{}) - for _, updateRaw := range results { - update, _ := updateRaw.(map[string]interface{}) - - if updateID, ok := update["update_id"].(float64); ok { - lastOffset = int64(updateID) - } - - if cbRaw, ok := update["callback_query"]; ok { - cb, _ := cbRaw.(map[string]interface{}) - cbMsg, _ := cb["message"].(map[string]interface{}) - cbChat, _ := cbMsg["chat"].(map[string]interface{}) - cbFrom, _ := cb["from"].(map[string]interface{}) - - var cbChatID int64 - if cbChat != nil { - cbChatID = tgcore.GetInt64(cbChat, "id") - } else if 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, a.cfg.AllowedIDs) { - lg.Warn("tg blocked user", "chatID", chatID) - continue - } - - // Handle photos - 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", a.cfg.TGToken, filePath) - s.Content += fmt.Sprintf("\n![%s](%s)\n", caption, imgURL) - lg.Info("photo received", "chat", chatID, "url", 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 = fullText[:idx] + afterAt - fullText = strings.TrimSpace(fullText) - } - - parts := strings.SplitN(fullText, " ", 2) - slashCmd := "" - slashArg := "" - if strings.HasPrefix(parts[0], "/") { - slashCmd = strings.ToLower(parts[0][1:]) - if len(parts) > 1 { - slashArg = parts[1] - } - } - - hasSession := sessions.HasSession(chatID) - - if hasSession && !strings.HasPrefix(fullText, "/") { - s := sessions.GetSession(chatID) - s.Content += fullText + "\n" - continue - } - - if slashCmd == "" { - continue - } - - lg.Info("tg command", "chat", chatID, "cmd", slashCmd, "arg", slashArg) - a.handleCommand(h, chatID, slashCmd, slashArg, argCommands) - } - } -} - -func (a *App) handleCommand(h *tghandlers.Handlers, chatID int64, cmd, arg string, argCmds map[string]bool) { - defer func() { - if r := recover(); r != nil { - lg.Error("handler panic", "cmd", cmd, "panic", r) - } - }() - - if argCmds[cmd] { - switch cmd { - case "new": h.New(chatID, arg) - case "title": h.Title(chatID, arg) - case "categories": h.Categories(chatID, arg) - case "tags": h.Tags(chatID, arg) - case "author": h.Author(chatID, arg) - case "edit": h.Edit(chatID, arg) - case "view": h.View(chatID, arg) - case "delete": h.Delete(chatID, arg) - case "link_add": h.LinkAdd(chatID, arg) - case "link_del": h.LinkDel(chatID, arg) - case "link_toggle": h.LinkToggle(chatID, arg) - case "feed_add": h.FeedAdd(chatID, arg) - case "feed_del": h.FeedDel(chatID, arg) - case "list": h.List(chatID, arg) - case "links": h.Links(chatID, arg) - case "feeds": h.Feeds(chatID, arg) - default: tgcore.SendMessage(chatID, "? Unknown: /"+cmd, nil) - } - } else { - switch cmd { - case "start": h.Start(chatID) - case "stop": h.Stop(chatID) - case "status": h.Status(chatID) - case "ip": h.IP(chatID) - case "help": h.Help(chatID) - case "save": h.Save(chatID) - case "publish": h.Publish(chatID) - case "draft": h.Draft(chatID) - case "preview": h.Preview(chatID) - case "cancel": h.Cancel(chatID) - case "stats": h.Stats(chatID) - default: tgcore.SendMessage(chatID, "? Unknown: /"+cmd, nil) - } - } -} - -func (a *App) handleAdmin(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/admin", "/admin/": - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(a.adminHTML())) - case "/admin/status": - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"running":true,"port":%d,"ip":"%s"}`, - a.cfg.WritePort, helpers.GetLanIP(a.cfg.PreferIface)) - case "/admin/start": - if r.Method == "POST" { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"started":true}`)) - } - case "/admin/stop": - if r.Method == "POST" { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"ok":false,"reason":"bot runs in-process"}`)) - } - default: - http.NotFound(w, r) - } -} - -func (a *App) adminHTML() string { - return fmt.Sprintf(` - - - - -WriteApp - - - -
-

WriteApp

-

Blog CMS + Telegram Bot

-
Server Running
Port %d · LAN: %s
- - - - -
- -`, a.cfg.WritePort, helpers.GetLanIP(a.cfg.PreferIface)) -} diff --git a/write-go/internal/artalk/artalk.go b/write-go/internal/artalk/artalk.go deleted file mode 100644 index 78ff4766..00000000 --- a/write-go/internal/artalk/artalk.go +++ /dev/null @@ -1,190 +0,0 @@ -package artalk - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - - "write-server/internal/config" -) - -var authToken string - -func SetAuthToken(token string) { authToken = token } -func GetAuthToken() string { return authToken } - -type ArtalkPage struct { - ID int `json:"id"` - AdminOnly bool `json:"admin_only"` - Key string `json:"key"` - Title string `json:"title"` - URL string `json:"url"` - View int `json:"view"` - CommentCount int `json:"comment_count"` - Date string `json:"date"` -} - -type ArtalkComment struct { - ID int `json:"id"` - Content string `json:"content"` - Nick string `json:"nick"` - Email string `json:"email"` - PageKey string `json:"page_key"` - IsCollapsed bool `json:"is_collapsed"` - IsPending bool `json:"is_pending"` - IsPinned bool `json:"is_pinned"` - VoteUp int `json:"vote_up"` - VoteDown int `json:"vote_down"` - RID int `json:"rid"` - Date string `json:"date"` -} - -func Login(email, password string) (map[string]interface{}, error) { - u := config.ARTALK_SERVER + "/api/v2/auth/email/login" - body, _ := json.Marshal(map[string]string{"email": email, "password": password}) - resp, err := http.Post(u, "application/json", bytes.NewReader(body)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - data, _ := io.ReadAll(resp.Body) - var result map[string]interface{} - json.Unmarshal(data, &result) - - if token, ok := result["token"].(string); ok { - authToken = token - } - return result, nil -} - -func artalkFetch(path string) (map[string]interface{}, error) { - u := config.ARTALK_SERVER + path - req, _ := http.NewRequest("GET", u, nil) - req.Header.Set("Content-Type", "application/json") - if authToken != "" { - req.Header.Set("Authorization", "Bearer "+authToken) - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - data, _ := io.ReadAll(resp.Body) - var result map[string]interface{} - json.Unmarshal(data, &result) - return result, nil -} - -func GetPages(offset, limit int, pageType string) ([]ArtalkPage, int, error) { - params := url.Values{} - params.Set("site_name", config.ARTALK_SITE) - params.Set("offset", fmt.Sprintf("%d", offset)) - params.Set("limit", fmt.Sprintf("%d", limit)) - params.Set("type", pageType) - - result, err := artalkFetch("/api/v2/pages?" + params.Encode()) - if err != nil { - return nil, 0, err - } - - var pages []ArtalkPage - if raw, ok := result["pages"]; ok { - d, _ := json.Marshal(raw) - json.Unmarshal(d, &pages) - } else if raw, ok := result["data"]; ok { - d, _ := json.Marshal(raw) - json.Unmarshal(d, &pages) - } - - total := 0 - if v, ok := result["count"]; ok { - fmt.Sscanf(fmt.Sprintf("%v", v), "%d", &total) - } else if v, ok := result["total"]; ok { - fmt.Sscanf(fmt.Sprintf("%v", v), "%d", &total) - } - return pages, total, nil -} - -func GetComments(pageKey string, offset, limit int) ([]ArtalkComment, int, error) { - params := url.Values{} - params.Set("site_name", config.ARTALK_SITE) - params.Set("page_key", pageKey) - params.Set("offset", fmt.Sprintf("%d", offset)) - params.Set("limit", fmt.Sprintf("%d", limit)) - params.Set("flat_mode", "true") - params.Set("sort_by", "date_desc") - - result, err := artalkFetch("/api/v2/comments?" + params.Encode()) - if err != nil { - return nil, 0, err - } - - var comments []ArtalkComment - if raw, ok := result["comments"]; ok { - d, _ := json.Marshal(raw) - json.Unmarshal(d, &comments) - } else if raw, ok := result["data"]; ok { - d, _ := json.Marshal(raw) - json.Unmarshal(d, &comments) - } - total := 0 - if v, ok := result["count"]; ok { - fmt.Sscanf(fmt.Sprintf("%v", v), "%d", &total) - } else if v, ok := result["total"]; ok { - fmt.Sscanf(fmt.Sprintf("%v", v), "%d", &total) - } - return comments, total, nil -} - -func UpdateComment(id int, data map[string]interface{}) error { - u := config.ARTALK_SERVER + fmt.Sprintf("/api/v2/comments/%d", id) - data["site_name"] = config.ARTALK_SITE - body, _ := json.Marshal(data) - req, _ := http.NewRequest("PUT", u, bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - if authToken != "" { - req.Header.Set("Authorization", "Bearer "+authToken) - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - return nil -} - -func CreateComment(data map[string]interface{}) (map[string]interface{}, error) { - u := config.ARTALK_SERVER + "/api/v2/comments" - data["site_name"] = config.ARTALK_SITE - body, _ := json.Marshal(data) - req, _ := http.NewRequest("POST", u, bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - if authToken != "" { - req.Header.Set("Authorization", "Bearer "+authToken) - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - respData, _ := io.ReadAll(resp.Body) - var result map[string]interface{} - json.Unmarshal(respData, &result) - return result, nil -} - -func DeleteComment(id int) error { - u := config.ARTALK_SERVER + fmt.Sprintf("/api/v2/comments/%d", id) - req, _ := http.NewRequest("DELETE", u, bytes.NewReader([]byte(`{"site_name":"` + config.ARTALK_SITE + `"}`))) - req.Header.Set("Content-Type", "application/json") - if authToken != "" { - req.Header.Set("Authorization", "Bearer "+authToken) - } - _, err := http.DefaultClient.Do(req) - return err -} diff --git a/write-go/internal/botconfig/config.go b/write-go/internal/botconfig/config.go deleted file mode 100644 index 113b498e..00000000 --- a/write-go/internal/botconfig/config.go +++ /dev/null @@ -1,99 +0,0 @@ -package botconfig - -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/write-go/internal/config/config.go b/write-go/internal/config/config.go deleted file mode 100644 index 8ecdfdf0..00000000 --- a/write-go/internal/config/config.go +++ /dev/null @@ -1,93 +0,0 @@ -package config - -import ( - "bufio" - "os" - "path/filepath" - "strconv" - "strings" -) - -var ( - BLOG_ROOT string - CONTENT_DIR string - RECYCLE_DIR string - ARTALK_SERVER = "https://artalk.usj.cc" - ARTALK_SITE = "???" - ITEMS_PER_PAGE = 20 - RECYCLE_DAYS = 30 - HUGO_PREVIEW = "http://localhost:1313" - RSS_API_BASE string - RSS_API_TOKEN string - SCF_PROXY string - WECHAT_APP_ID string - WECHAT_APP_SECRET string - DEEPSEEK_API_KEY string - DEEPSEEK_BASE_URL string - DEEPSEEK_MODEL string -) - -func Init() { - cwd, _ := os.Getwd() - BLOG_ROOT = filepath.Clean(filepath.Join(cwd, "..")) - CONTENT_DIR = filepath.Join(BLOG_ROOT, "content", "post") - RECYCLE_DIR = filepath.Join(cwd, ".recycle") - - loadEnv() - - RSS_API_BASE = getEnv("RSS_API_BASE", "https://api.usj.cc") - RSS_API_TOKEN = os.Getenv("RSS_API_TOKEN") - SCF_PROXY = getEnv("SCF_PROXY", "") - WECHAT_APP_ID = os.Getenv("WECHAT_APP_ID") - WECHAT_APP_SECRET = os.Getenv("WECHAT_APP_SECRET") - DEEPSEEK_API_KEY = os.Getenv("DEEPSEEK_API_KEY") - DEEPSEEK_BASE_URL = getEnv("DEEPSEEK_BASE_URL", "https://api.deepseek.com") - DEEPSEEK_MODEL = getEnv("DEEPSEEK_MODEL", "deepseek-chat") -} - -func loadEnv() { - envPaths := []string{ - filepath.Join(BLOG_ROOT, "write-go", ".env"), - filepath.Join(BLOG_ROOT, "write", ".env"), - } - for _, p := range envPaths { - f, err := os.Open(p) - if err != nil { - continue - } - 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) - } - } - f.Close() - break - } -} - -func getEnv(key, fallback string) string { - if v := os.Getenv(key); v != "" { - return v - } - return fallback -} - -func GetEnvInt(key string, fallback int) int { - if v := os.Getenv(key); v != "" { - if n, err := strconv.Atoi(v); err == nil { - return n - } - } - return fallback -} diff --git a/write-go/internal/helpers/helpers.go b/write-go/internal/helpers/helpers.go deleted file mode 100644 index 5a9d48e2..00000000 --- a/write-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/write-go/internal/helpers/helpers/helpers.go b/write-go/internal/helpers/helpers/helpers.go deleted file mode 100644 index 5a9d48e2..00000000 --- a/write-go/internal/helpers/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/write-go/internal/images/images.go b/write-go/internal/images/images.go deleted file mode 100644 index f6de046c..00000000 --- a/write-go/internal/images/images.go +++ /dev/null @@ -1,95 +0,0 @@ -package images - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "write-server/internal/config" - "write-server/internal/posts" -) - -type ImageFile struct { - Name string `json:"name"` - Path string `json:"path"` - Folder string `json:"folder"` - Size int64 `json:"size"` - URL string `json:"url"` - Slug string `json:"slug"` - LocalURL string `json:"localUrl"` -} - -var imageExts = map[string]bool{ - ".png": true, ".jpg": true, ".jpeg": true, ".gif": true, - ".webp": true, ".svg": true, ".bmp": true, -} - -func ScanImages() []ImageFile { - var images []ImageFile - filepath.WalkDir(config.CONTENT_DIR, func(p string, d os.DirEntry, err error) error { - if err != nil { - return nil - } - if d.IsDir() { - if strings.HasPrefix(d.Name(), ".") { - return filepath.SkipDir - } - return nil - } - ext := strings.ToLower(filepath.Ext(d.Name())) - if !imageExts[ext] { - return nil - } - info, _ := d.Info() - size := int64(0) - if info != nil { - size = info.Size() - } - - rel, _ := filepath.Rel(config.CONTENT_DIR, p) - rel = strings.ReplaceAll(rel, "\\", "/") - dir := filepath.Dir(p) - - slug := "" - indexPath := filepath.Join(dir, "index.md") - if data, err := os.ReadFile(indexPath); err == nil { - fm, _ := posts.ParseFrontMatter(string(data)) - slug = fm.Slug - } - - u := "/" + rel - if slug != "" { - u = "/" + slug + "/" + d.Name() - } - - images = append(images, ImageFile{ - Name: d.Name(), - Path: rel, - Folder: filepath.Dir(rel), - Size: size, - URL: u, - Slug: slug, - LocalURL: fmt.Sprintf("/api/images?file=%s", rel), - }) - return nil - }) - - // Sort by size desc - for i := 0; i < len(images); i++ { - for j := i + 1; j < len(images); j++ { - if images[j].Size > images[i].Size { - images[i], images[j] = images[j], images[i] - } - } - } - return images -} - -func DeleteImage(relPath string) error { - fullPath := filepath.Join(config.CONTENT_DIR, relPath) - if !strings.HasPrefix(filepath.Clean(fullPath), filepath.Clean(config.CONTENT_DIR)) { - return fmt.Errorf("invalid path") - } - return os.Remove(fullPath) -} diff --git a/write-go/internal/logutil/logutil.go b/write-go/internal/logutil/logutil.go deleted file mode 100644 index 3a953922..00000000 --- a/write-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/write-go/internal/posts/posts.go b/write-go/internal/posts/posts.go deleted file mode 100644 index 5b23d13f..00000000 --- a/write-go/internal/posts/posts.go +++ /dev/null @@ -1,524 +0,0 @@ -package posts - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "sort" - "strings" - "time" - - "write-server/internal/config" -) - -type FrontMatter struct { - Pid int `json:"pid,omitempty"` - Title string `json:"title"` - Date string `json:"date"` - Slug string `json:"slug"` - Draft bool `json:"draft"` - Categories []string `json:"categories,omitempty"` - Tags []string `json:"tags,omitempty"` - Author string `json:"author,omitempty"` - Layout string `json:"layout,omitempty"` - AiComment string `json:"ai_comment,omitempty"` - Extra map[string]interface{} `json:"-"` -} - -type Post struct { - Slug string `json:"slug"` - FrontMatter FrontMatter `json:"frontMatter"` - Content string `json:"content"` - FilePath string `json:"-"` - DirPath string `json:"-"` -} - -type PostListItem struct { - Slug string `json:"slug"` - Title string `json:"title"` - Date string `json:"date"` - Draft bool `json:"draft"` - Categories []string `json:"categories"` - Tags []string `json:"tags"` - Author string `json:"author"` -} - -var dateRE = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}-(.+)$`) -var yearRE = regexp.MustCompile(`^(\d{4})`) - -func Init() { - config.Init() -} - -func findPostDirs() []string { - var result []string - if _, err := os.Stat(config.CONTENT_DIR); os.IsNotExist(err) { - return result - } - entries, err := os.ReadDir(config.CONTENT_DIR) - if err != nil { - return result - } - for _, entry := range entries { - if !entry.IsDir() || !yearRE.MatchString(entry.Name()) { - continue - } - yearDir := filepath.Join(config.CONTENT_DIR, entry.Name()) - posts, err := os.ReadDir(yearDir) - if err != nil { - continue - } - for _, post := range posts { - if post.IsDir() { - result = append(result, filepath.Join(yearDir, post.Name())) - } - } - } - return result -} - -func slugFromDir(dirName string) string { - m := dateRE.FindStringSubmatch(dirName) - if m != nil { - return m[1] - } - return dirName -} - -func parseDate(dateStr string) time.Time { - if len(dateStr) < 10 { - return time.Time{} - } - t, err := time.Parse("2006-01-02", dateStr[:10]) - if err != nil { - return time.Time{} - } - return t -} - -// ParseFrontMatter parses YAML-like frontmatter (---...---) and body -func ParseFrontMatter(raw string) (FrontMatter, string) { - fm := FrontMatter{Layout: "post"} - fm.Extra = make(map[string]interface{}) - - raw = strings.TrimSpace(raw) - if !strings.HasPrefix(raw, "---") { - return fm, raw - } - - rest := raw[3:] - endIdx := strings.Index(rest, "\n---") - if endIdx < 0 { - return fm, raw - } - - yamlBlock := rest[:endIdx] - body := rest[endIdx+4:] - - // Simple YAML-like key: value parser - lines := strings.Split(yamlBlock, "\n") - var currentListKey string - for _, line := range lines { - // Handle multi-line YAML list items (- value) - if currentListKey != "" && strings.HasPrefix(strings.TrimSpace(line), "- ") { - item := strings.Trim(strings.TrimPrefix(strings.TrimSpace(line), "- "), "\"'") - switch currentListKey { - case "categories": - fm.Categories = append(fm.Categories, item) - case "tags": - fm.Tags = append(fm.Tags, item) - } - continue - } - currentListKey = "" - line = strings.TrimSpace(line) - 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]) - - // Unquoted string value - val = strings.Trim(val, "\"'") - - switch key { - case "title": - fm.Title = val - case "date": - fm.Date = val - case "slug": - fm.Slug = val - case "draft": - fm.Draft = val == "true" - case "author": - fm.Author = val - case "layout": - fm.Layout = val - case "ai_comment": - fm.AiComment = val - case "categories": - if val == "" { - currentListKey = "categories" - } else if val != "[]" { - fm.Categories = parseYAMLList(val, line) - } - case "tags": - if val == "" { - currentListKey = "tags" - } else if val != "[]" { - fm.Tags = parseYAMLList(val, line) - } - case "pid": - fmt.Sscanf(val, "%d", &fm.Pid) - default: - fm.Extra[key] = val - } - } - - return fm, body -} - -func parseYAMLList(firstVal string, fullLine string) []string { - // Try inline: [a, b, c] - if strings.HasPrefix(firstVal, "[") && strings.HasSuffix(firstVal, "]") { - inner := firstVal[1 : len(firstVal)-1] - var items []string - for _, item := range strings.Split(inner, ",") { - item = strings.TrimSpace(item) - item = strings.Trim(item, "\"'") - if item != "" { - items = append(items, item) - } - } - return items - } - // Single value on same line - if firstVal != "" && firstVal != "[]" { - return []string{strings.Trim(firstVal, "\"'")} - } - return nil -} - -// ParseYAMLListMultiLine is called for multi-line list items -// In our simple parser, multi-line categories/tags aren't handled fully -// but this stub exists for testing - -func ListPosts(search string, page, perPage int, includeDrafts bool) ([]PostListItem, int, int) { - dirs := findPostDirs() - var allPosts []PostListItem - - for _, dirPath := range dirs { - indexPath := filepath.Join(dirPath, "index.md") - data, err := os.ReadFile(indexPath) - if err != nil { - continue - } - - fm, _ := ParseFrontMatter(string(data)) - dirName := filepath.Base(dirPath) - - slug := fm.Slug - if slug == "" { - slug = slugFromDir(dirName) - } - title := fm.Title - if title == "" { - title = "Untitled" - } - - if !includeDrafts && fm.Draft { - continue - } - - if search != "" { - q := strings.ToLower(search) - match := strings.Contains(strings.ToLower(title), q) || - strings.Contains(strings.ToLower(slug), q) - if !match { - for _, c := range fm.Categories { - if strings.Contains(strings.ToLower(c), q) { - match = true - break - } - } - } - if !match { - for _, t := range fm.Tags { - if strings.Contains(strings.ToLower(t), q) { - match = true - break - } - } - } - if !match { - continue - } - } - - allPosts = append(allPosts, PostListItem{ - Slug: slug, - Title: title, - Date: fm.Date, - Draft: fm.Draft, - Categories: fm.Categories, - Tags: fm.Tags, - Author: fm.Author, - }) - } - - sort.Slice(allPosts, func(i, j int) bool { - return parseDate(allPosts[i].Date).After(parseDate(allPosts[j].Date)) - }) - - total := len(allPosts) - totalPages := (total + perPage - 1) / perPage - if totalPages < 1 { - totalPages = 1 - } - - start := (page - 1) * perPage - if start > total { - start = total - } - end := start + perPage - if end > total { - end = total - } - - return allPosts[start:end], total, totalPages -} - -func GetPost(slug string) *Post { - dirs := findPostDirs() - for _, dirPath := range dirs { - indexPath := filepath.Join(dirPath, "index.md") - data, err := os.ReadFile(indexPath) - if err != nil { - continue - } - - fm, body := ParseFrontMatter(string(data)) - existingSlug := fm.Slug - if existingSlug == "" { - existingSlug = slugFromDir(filepath.Base(dirPath)) - } - - if existingSlug == slug { - return &Post{ - Slug: existingSlug, - FrontMatter: fm, - Content: strings.TrimSpace(body), - FilePath: indexPath, - DirPath: dirPath, - } - } - } - return nil -} - -func CreatePost(fm FrontMatter, content string) (string, error) { - date := time.Now() - if fm.Date != "" { - if d, err := time.Parse("2006-01-02", fm.Date[:10]); err == nil { - date = d - } - } else { - fm.Date = date.Format("2006-01-02") - } - year := fmt.Sprintf("%04d", date.Year()) - dateStr := date.Format("2006-01-02") - - titlePart := fm.Title - if titlePart == "" { - titlePart = "untitled" - } - titlePart = sanitizePath(titlePart) - if titlePart == "" { - titlePart = "post" - } - - dirName := dateStr + "-" + titlePart - dirPath := filepath.Join(config.CONTENT_DIR, year, dirName) - - if _, err := os.Stat(dirPath); err == nil { - return "", fmt.Errorf("Post already exists at %s", dirPath) - } - - if err := os.MkdirAll(dirPath, 0755); err != nil { - return "", err - } - - fileContent := buildMarkdown(fm, content) - indexPath := filepath.Join(dirPath, "index.md") - if err := os.WriteFile(indexPath, []byte(fileContent), 0644); err != nil { - return "", err - } - return dirPath, nil -} - -func UpdatePost(slug string, fmUpdates map[string]interface{}, content *string) (*Post, error) { - post := GetPost(slug) - if post == nil { - return nil, fmt.Errorf("post not found") - } - - // Apply updates - for k, v := range fmUpdates { - switch k { - case "title": - if s, ok := v.(string); ok { - post.FrontMatter.Title = s - } - case "date": - if s, ok := v.(string); ok { - post.FrontMatter.Date = s - } - case "draft": - if b, ok := v.(bool); ok { - post.FrontMatter.Draft = b - } - case "categories": - if arr, ok := v.([]interface{}); ok { - post.FrontMatter.Categories = nil - for _, item := range arr { - if s, ok := item.(string); ok { - post.FrontMatter.Categories = append(post.FrontMatter.Categories, s) - } - } - } - case "tags": - if arr, ok := v.([]interface{}); ok { - post.FrontMatter.Tags = nil - for _, item := range arr { - if s, ok := item.(string); ok { - post.FrontMatter.Tags = append(post.FrontMatter.Tags, s) - } - } - } - case "author": - if s, ok := v.(string); ok { - post.FrontMatter.Author = s - } - case "slug": - if s, ok := v.(string); ok { - post.FrontMatter.Slug = s - } - case "ai_comment": - if s, ok := v.(string); ok { - post.FrontMatter.AiComment = s - } - } - } - - if content != nil { - post.Content = *content - } - - // Handle slug rename - if newSlug, ok := fmUpdates["slug"].(string); ok && newSlug != slug { - date := time.Now() - if post.FrontMatter.Date != "" { - if d, err := time.Parse("2006-01-02", post.FrontMatter.Date[:10]); err == nil { - date = d - } - } - year := fmt.Sprintf("%04d", date.Year()) - dateStr := date.Format("2006-01-02") - newDirName := dateStr + "-" + newSlug - newDirPath := filepath.Join(config.CONTENT_DIR, year, newDirName) - - if _, err := os.Stat(newDirPath); err == nil { - return nil, fmt.Errorf("Target path already exists: %s", newDirPath) - } - os.MkdirAll(filepath.Dir(newDirPath), 0755) - os.Rename(post.DirPath, newDirPath) - post.DirPath = newDirPath - post.FilePath = filepath.Join(newDirPath, "index.md") - post.Slug = newSlug - } - - fileContent := buildMarkdown(post.FrontMatter, post.Content) - if err := os.WriteFile(post.FilePath, []byte(fileContent), 0644); err != nil { - return nil, err - } - return post, nil -} - -func GetPostCount() int { - count := 0 - dirs := findPostDirs() - for _, dir := range dirs { - if _, err := os.Stat(filepath.Join(dir, "index.md")); err == nil { - count++ - } - } - return count -} - -func sanitizePath(s string) string { - var result []byte - for _, r := range []byte(s) { - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' { - result = append(result, r) - } else if r >= 0x80 { - // Allow multi-byte (CJK etc.) - result = append(result, r) - } else { - result = append(result, '-') - } - } - out := string(result) - out = strings.Trim(out, "-") - return strings.ToLower(out) -} - -func buildMarkdown(fm FrontMatter, content string) string { - var sb strings.Builder - sb.WriteString("---\n") - sb.WriteString(fmt.Sprintf("title: %s\n", quoteYAML(fm.Title))) - sb.WriteString(fmt.Sprintf("date: %s\n", fm.Date)) - sb.WriteString(fmt.Sprintf("slug: %s\n", fm.Slug)) - sb.WriteString(fmt.Sprintf("draft: %v\n", fm.Draft)) - if len(fm.Categories) > 0 { - sb.WriteString("categories:\n") - for _, c := range fm.Categories { - sb.WriteString(fmt.Sprintf(" - %s\n", quoteYAML(c))) - } - } - if len(fm.Tags) > 0 { - sb.WriteString("tags:\n") - for _, t := range fm.Tags { - sb.WriteString(fmt.Sprintf(" - %s\n", quoteYAML(t))) - } - } - if fm.Author != "" { - sb.WriteString(fmt.Sprintf("author: %s\n", quoteYAML(fm.Author))) - } - if fm.Layout != "post" && fm.Layout != "" { - sb.WriteString(fmt.Sprintf("layout: %s\n", fm.Layout)) - } - if fm.AiComment != "" { - sb.WriteString(fmt.Sprintf("ai_comment: %s\n", quoteYAML(fm.AiComment))) - } - if fm.Pid != 0 { - sb.WriteString(fmt.Sprintf("pid: %d\n", fm.Pid)) - } - sb.WriteString("---\n") - sb.WriteString(content) - if !strings.HasSuffix(content, "\n") { - sb.WriteString("\n") - } - return sb.String() -} - -func quoteYAML(s string) string { - if strings.ContainsAny(s, ":{}[]&*?|>!%@`'\"#,\n") || s == "" { - return "\"" + strings.ReplaceAll(s, "\"", "\\\"") + "\"" - } - return s -} diff --git a/write-go/internal/recycle/recycle.go b/write-go/internal/recycle/recycle.go deleted file mode 100644 index 3a44d6da..00000000 --- a/write-go/internal/recycle/recycle.go +++ /dev/null @@ -1,168 +0,0 @@ -package recycle - -import ( - "encoding/json" - "math" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "write-server/internal/config" -) - -var RECYCLE_POSTS_DIR string -var RECYCLE_META_DIR string - -type RecycleItem struct { - Slug string `json:"slug"` - Title string `json:"title"` - DeletedAt string `json:"deletedAt"` - OriginalPath string `json:"originalPath"` - ExpiresAt string `json:"expiresAt"` - DaysRemaining int `json:"daysRemaining"` -} - -func InitRecycleBin() { - base := config.RECYCLE_DIR - RECYCLE_POSTS_DIR = filepath.Join(base, "posts") - RECYCLE_META_DIR = filepath.Join(base, "meta") - if err := os.MkdirAll(RECYCLE_POSTS_DIR, 0755); err != nil { - panic(err) - } - if err := os.MkdirAll(RECYCLE_META_DIR, 0755); err != nil { - panic(err) - } -} - -func MoveToRecycle(slug, sourceDir, title string) error { - InitRecycleBin() - - now := time.Now() - expires := now.AddDate(0, 0, config.RECYCLE_DAYS) - - meta := map[string]interface{}{ - "slug": slug, - "title": title, - "deletedAt": now.Format(time.RFC3339), - "originalPath": sourceDir, - "expiresAt": expires.Format(time.RFC3339), - } - - targetDir := filepath.Join(RECYCLE_POSTS_DIR, slug) - os.RemoveAll(targetDir) - if err := os.Rename(sourceDir, targetDir); err != nil { - return err - } - - metaPath := filepath.Join(RECYCLE_META_DIR, slug+".json") - data, _ := json.MarshalIndent(meta, "", " ") - return os.WriteFile(metaPath, data, 0644) -} - -func ListRecycle() []RecycleItem { - if _, err := os.Stat(RECYCLE_META_DIR); os.IsNotExist(err) { - return nil - } - entries, err := os.ReadDir(RECYCLE_META_DIR) - if err != nil { - return nil - } - var items []RecycleItem - now := time.Now() - for _, entry := range entries { - if !strings.HasSuffix(entry.Name(), ".json") { - continue - } - data, err := os.ReadFile(filepath.Join(RECYCLE_META_DIR, entry.Name())) - if err != nil { - continue - } - var meta map[string]interface{} - if err := json.Unmarshal(data, &meta); err != nil { - continue - } - expiresAt, _ := time.Parse(time.RFC3339, getString(meta, "expiresAt")) - daysRemaining := int(math.Ceil(expiresAt.Sub(now).Hours() / 24)) - if daysRemaining < 0 { - daysRemaining = 0 - } - items = append(items, RecycleItem{ - Slug: getString(meta, "slug"), - Title: getString(meta, "title"), - DeletedAt: getString(meta, "deletedAt"), - OriginalPath: getString(meta, "originalPath"), - ExpiresAt: getString(meta, "expiresAt"), - DaysRemaining: daysRemaining, - }) - } - sort.Slice(items, func(i, j int) bool { - ti, _ := time.Parse(time.RFC3339, items[i].DeletedAt) - tj, _ := time.Parse(time.RFC3339, items[j].DeletedAt) - return ti.After(tj) - }) - return items -} - -func RestoreFromRecycle(slug string) bool { - metaPath := filepath.Join(RECYCLE_META_DIR, slug+".json") - data, err := os.ReadFile(metaPath) - if err != nil { - return false - } - var meta map[string]interface{} - json.Unmarshal(data, &meta) - - recyclePostPath := filepath.Join(RECYCLE_POSTS_DIR, slug) - if _, err := os.Stat(recyclePostPath); os.IsNotExist(err) { - return false - } - - originalPath := getString(meta, "originalPath") - os.MkdirAll(filepath.Dir(originalPath), 0755) - os.RemoveAll(originalPath) - - if err := os.Rename(recyclePostPath, originalPath); err != nil { - return false - } - os.Remove(metaPath) - return true -} - -func PermanentDelete(slug string) bool { - deleted := false - recyclePostPath := filepath.Join(RECYCLE_POSTS_DIR, slug) - if _, err := os.Stat(recyclePostPath); err == nil { - os.RemoveAll(recyclePostPath) - deleted = true - } - metaPath := filepath.Join(RECYCLE_META_DIR, slug+".json") - if _, err := os.Stat(metaPath); err == nil { - os.Remove(metaPath) - deleted = true - } - return deleted -} - -func CleanExpired() int { - items := ListRecycle() - count := 0 - for _, item := range items { - if item.DaysRemaining <= 0 { - PermanentDelete(item.Slug) - count++ - } - } - return count -} - -func getString(m map[string]interface{}, key string) string { - if v, ok := m[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - diff --git a/write-go/internal/rssapi/rssapi.go b/write-go/internal/rssapi/rssapi.go deleted file mode 100644 index 75b14931..00000000 --- a/write-go/internal/rssapi/rssapi.go +++ /dev/null @@ -1,157 +0,0 @@ -package rssapi - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - - "write-server/internal/config" -) - -type Feed struct { - URL string `json:"url"` - FeedTitle string `json:"feedTitle"` - SiteURL string `json:"siteUrl,omitempty"` - Error string `json:"error,omitempty"` -} - -type FeedHealth struct { - URL string `json:"url"` - Status int `json:"status"` - OK bool `json:"ok"` - Latency int `json:"latency,omitempty"` - Error string `json:"error,omitempty"` -} - -type Link struct { - Name string `json:"name"` - URL string `json:"url"` - Description string `json:"description,omitempty"` - Image string `json:"image,omitempty"` - Hidden bool `json:"hidden,omitempty"` - RSS string `json:"rss,omitempty"` - AddedAt string `json:"addedAt,omitempty"` -} - -func rssFetch(path string, method string, body interface{}) (map[string]interface{}, error) { - u := config.RSS_API_BASE + path - var req *http.Request - var err error - - if body != nil && (method == "POST" || method == "PUT") { - data, _ := json.Marshal(body) - req, err = http.NewRequest(method, u, bytes.NewReader(data)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - } else { - req, err = http.NewRequest(method, u, nil) - if err != nil { - return nil, err - } - } - - if config.RSS_API_TOKEN != "" { - q := req.URL.Query() - q.Set("token", config.RSS_API_TOKEN) - req.URL.RawQuery = q.Encode() - } - - resp, err := http.DefaultClient.Do(req) - 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, fmt.Errorf("RSS API error: %d", resp.StatusCode) - } - return result, nil -} - -func GetFeeds() ([]Feed, error) { - result, err := rssFetch("/api/rss/feeds", "GET", nil) - if err != nil { - return nil, err - } - var feeds []Feed - if raw, ok := result["feeds"]; ok { - data, _ := json.Marshal(raw) - json.Unmarshal(data, &feeds) - } - return feeds, nil -} - -func AddFeed(feedURL, feedTitle string) error { - _, err := rssFetch("/api/rss/feeds", "POST", map[string]string{ - "url": feedURL, - "feedTitle": feedTitle, - }) - return err -} - -func DeleteFeed(feedURL string) error { - _, err := rssFetch("/api/rss/feeds?url="+url.QueryEscape(feedURL), "DELETE", nil) - return err -} - -func UpdateFeed(oldURL, newURL, feedTitle string) error { - _, err := rssFetch("/api/rss/feeds", "PUT", map[string]string{ - "oldUrl": oldURL, - "url": newURL, - "feedTitle": feedTitle, - }) - return err -} - -func GetLinks(all bool) ([]Link, error) { - path := "/api/rss/links" - if all { - path += "?all=1" - } - result, err := rssFetch(path, "GET", nil) - if err != nil { - return nil, err - } - var links []Link - if raw, ok := result["links"]; ok { - data, _ := json.Marshal(raw) - json.Unmarshal(data, &links) - } - return links, nil -} - -func AddLink(link Link) error { - _, err := rssFetch("/api/rss/links", "POST", link) - return err -} - -func UpdateLink(oldURL string, data map[string]interface{}) error { - body := map[string]interface{}{"oldUrl": oldURL} - for k, v := range data { - if k != "oldUrl" { - body[k] = v - } - } - _, err := rssFetch("/api/rss/links", "PUT", body) - return err -} - -func DeleteLink(linkURL string) error { - _, err := rssFetch("/api/rss/links?url="+url.QueryEscape(linkURL), "DELETE", nil) - return err -} - -func CheckLinkHealth(targetURL string) (map[string]interface{}, error) { - return rssFetch("/api/rss/links?health="+url.QueryEscape(targetURL), "GET", nil) -} diff --git a/write-go/internal/sessions/sessions.go b/write-go/internal/sessions/sessions.go deleted file mode 100644 index 715b3afd..00000000 --- a/write-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/write-go/internal/tgcore/tgcore.go b/write-go/internal/tgcore/tgcore.go deleted file mode 100644 index a9946dc9..00000000 --- a/write-go/internal/tgcore/tgcore.go +++ /dev/null @@ -1,145 +0,0 @@ -package tgcore - -import ( - "bytes" - "encoding/json" - "io" - "net/http" - "strings" -) - -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 "" -} diff --git a/write-go/internal/tgcore/tgcore/tgcore.go b/write-go/internal/tgcore/tgcore/tgcore.go deleted file mode 100644 index a9946dc9..00000000 --- a/write-go/internal/tgcore/tgcore/tgcore.go +++ /dev/null @@ -1,145 +0,0 @@ -package tgcore - -import ( - "bytes" - "encoding/json" - "io" - "net/http" - "strings" -) - -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 "" -} diff --git a/write-go/internal/tghandlers/tghandlers.go b/write-go/internal/tghandlers/tghandlers.go deleted file mode 100644 index 2694926c..00000000 --- a/write-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/write-go/internal/wechat/wechat.go b/write-go/internal/wechat/wechat.go deleted file mode 100644 index a7ce686c..00000000 --- a/write-go/internal/wechat/wechat.go +++ /dev/null @@ -1,80 +0,0 @@ -package wechat - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "regexp" - "strings" - - "write-server/internal/config" -) - -type WechatStatus struct { - Configured bool `json:"configured"` - AppID string `json:"appId,omitempty"` -} - -type WechatArticle struct { - Title string `json:"title"` - Author string `json:"author"` - Content string `json:"content"` - ContentSourceURL string `json:"content_source_url,omitempty"` - ThumbMediaURL string `json:"thumb_media_url,omitempty"` - ShowCoverPic int `json:"show_cover_pic"` - NeedOpenComment int `json:"need_open_comment"` -} - -var imgRE = regexp.MustCompile(`!\[.*?\]\((.*?)\)`) - -func GetWechatStatus() WechatStatus { - return WechatStatus{ - Configured: config.WECHAT_APP_ID != "" && config.WECHAT_APP_SECRET != "", - AppID: config.WECHAT_APP_ID, - } -} - -func MDRender(markdown, slug string) (string, []string) { - var imageURLs []string - matches := imgRE.FindAllStringSubmatch(markdown, -1) - for _, m := range matches { - if len(m) > 1 { - imageURLs = append(imageURLs, m[1]) - } - } - - html := strings.ReplaceAll(markdown, "\r\n", "\n") - - h2RE := regexp.MustCompile(`(?m)^## (.+)$`) - html = h2RE.ReplaceAllString(html, `

$1

`) - - boldRE := regexp.MustCompile(`\*\*(.+?)\*\*`) - html = boldRE.ReplaceAllString(html, `$1`) - - html = imgRE.ReplaceAllString(html, ``) - - linkRE := regexp.MustCompile(`\[(.+?)\]\((.+?)\)`) - html = linkRE.ReplaceAllString(html, `$1`) - - html = `
` + html + `
` - - return html, imageURLs -} - -func SyncWechat(articles []WechatArticle) (map[string]interface{}, error) { - if config.SCF_PROXY == "" { - return nil, fmt.Errorf("SCF_PROXY not configured") - } - data, _ := json.Marshal(map[string]interface{}{"articles": articles}) - resp, err := http.Post(config.SCF_PROXY+"/wechat/sync", "application/json", bytes.NewReader(data)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - var result map[string]interface{} - json.Unmarshal(body, &result) - return result, nil -} \ No newline at end of file diff --git a/write-go/internal/writeserver/writeserver.go b/write-go/internal/writeserver/writeserver.go deleted file mode 100644 index 2fb8aa51..00000000 --- a/write-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/write-go/out.log b/write-go/out.log deleted file mode 100644 index f8cb23ce..00000000 --- a/write-go/out.log +++ /dev/null @@ -1,2 +0,0 @@ -time=2026-06-01T16:58:10.099+08:00 level=INFO msg="WriteServer starting" svc=writeserver -[write-server] listening on 0.0.0.0:8016 diff --git a/write-go/panic.log b/write-go/panic.log deleted file mode 100644 index 95765091..00000000 --- a/write-go/panic.log +++ /dev/null @@ -1 +0,0 @@ -2026/06/01 16:58:10 listen tcp 0.0.0.0:8016: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted. diff --git a/write-go/pkg/posts/posts.go b/write-go/pkg/posts/posts.go deleted file mode 100644 index dcb0f0d4..00000000 --- a/write-go/pkg/posts/posts.go +++ /dev/null @@ -1,18 +0,0 @@ -package posts - -import "write-server/internal/posts" - -type FrontMatter = posts.FrontMatter -type Post = posts.Post -type PostListItem = posts.PostListItem - -var ListPosts = posts.ListPosts -var GetPost = posts.GetPost -var CreatePost = posts.CreatePost -var UpdatePost = posts.UpdatePost -var GetPostCount = posts.GetPostCount -var ParseFrontMatter = posts.ParseFrontMatter - -func Init() { - posts.Init() -} \ No newline at end of file diff --git a/write-go/pkg/recycle/recycle.go b/write-go/pkg/recycle/recycle.go deleted file mode 100644 index c954d444..00000000 --- a/write-go/pkg/recycle/recycle.go +++ /dev/null @@ -1,12 +0,0 @@ -package recycle - -import "write-server/internal/recycle" - -var InitRecycleBin = recycle.InitRecycleBin -var MoveToRecycle = recycle.MoveToRecycle -var ListRecycle = recycle.ListRecycle -var RestoreFromRecycle = recycle.RestoreFromRecycle -var PermanentDelete = recycle.PermanentDelete -var CleanExpired = recycle.CleanExpired - -type RecycleItem = recycle.RecycleItem diff --git a/write-go/pkg/rssapi/rssapi.go b/write-go/pkg/rssapi/rssapi.go deleted file mode 100644 index a2ef4630..00000000 --- a/write-go/pkg/rssapi/rssapi.go +++ /dev/null @@ -1,14 +0,0 @@ -package rssapi - -import "write-server/internal/rssapi" - -type Feed = rssapi.Feed -type Link = rssapi.Link - -var GetFeeds = rssapi.GetFeeds -var AddFeed = rssapi.AddFeed -var DeleteFeed = rssapi.DeleteFeed -var GetLinks = rssapi.GetLinks -var AddLink = rssapi.AddLink -var UpdateLink = rssapi.UpdateLink -var DeleteLink = rssapi.DeleteLink diff --git a/write-go/start_write.bat b/write-go/start_write.bat deleted file mode 100644 index 2ee874ae..00000000 --- a/write-go/start_write.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -title WriteServer -cd /d E:\GitHub\blog\write-go -echo [%date% %time%] WriteServer starting... -writeserver.exe diff --git a/write-go/stderr.log b/write-go/stderr.log deleted file mode 100644 index 13bb5af8..00000000 --- a/write-go/stderr.log +++ /dev/null @@ -1,34 +0,0 @@ -2026/06/01 16:55:31 http: panic serving [::1]:50574: runtime error: slice bounds out of range [:10] with length 0 -goroutine 9 [running]: -net/http.(*conn).serve.func1() - C:/Program Files/Go/src/net/http/server.go:1907 +0xbd -panic({0x7ff6dda1c5c0?, 0x66fdd19e1b0?}) - C:/Program Files/Go/src/runtime/panic.go:860 +0x13a -write-server/internal/posts.parseDate(...) - E:/GitHub/blog/write-go/internal/posts/posts.go:86 -write-server/internal/posts.ListPosts.func1(0x58?, 0x0?) - E:/GitHub/blog/write-go/internal/posts/posts.go:268 +0xfa -sort.partition_func({0x66fdd19b690?, 0x66fdd320ab0?}, 0x0, 0x74, 0x0?) - C:/Program Files/Go/src/sort/zsortfunc.go:142 +0xa9 -sort.pdqsort_func({0x66fdd19b690?, 0x66fdd320ab0?}, 0x18?, 0x7ff6dd9c5de0?, 0x2?) - C:/Program Files/Go/src/sort/zsortfunc.go:114 +0x23d -sort.Slice({0x7ff6dd9bb940?, 0x66fdd1c4030?}, 0x66fdd19b690) - C:/Program Files/Go/src/sort/slice.go:29 +0xc5 -write-server/internal/posts.ListPosts({0x0, 0x0}, 0x1, 0x14, 0x1) - E:/GitHub/blog/write-go/internal/posts/posts.go:267 +0x869 -main.handlePosts({0x7ff6dda6cc90, 0x66fdd1b4000}, 0x66fdd192000) - E:/GitHub/blog/write-go/cmd/writeserver/main.go:159 +0x415 -net/http.HandlerFunc.ServeHTTP(0x66fdd05a3c0?, {0x7ff6dda6cc90?, 0x66fdd1b4000?}, 0x7ff6dda4da40?) - C:/Program Files/Go/src/net/http/server.go:2286 +0x29 -net/http.(*ServeMux).ServeHTTP(0x66fdd1820f0?, {0x7ff6dda6cc90, 0x66fdd1b4000}, 0x66fdd192000) - C:/Program Files/Go/src/net/http/server.go:2828 +0x1c7 -main.main.withCORS.func1({0x7ff6dda6cc90, 0x66fdd1b4000}, 0x66fdd192000) - E:/GitHub/blog/write-go/cmd/writeserver/main.go:84 +0x10d -net/http.HandlerFunc.ServeHTTP(0x7ff6dd6cced9?, {0x7ff6dda6cc90?, 0x66fdd1b4000?}, 0x66fdd19bb68?) - C:/Program Files/Go/src/net/http/server.go:2286 +0x29 -net/http.serverHandler.ServeHTTP({0x66fdd188000?}, {0x7ff6dda6cc90?, 0x66fdd1b4000?}, 0x1?) - C:/Program Files/Go/src/net/http/server.go:3311 +0x8e -net/http.(*conn).serve(0x66fdd078360, {0x7ff6dda6d3f0, 0x66fdcfa58f0}) - C:/Program Files/Go/src/net/http/server.go:2073 +0x650 -created by net/http.(*Server).Serve in goroutine 1 - C:/Program Files/Go/src/net/http/server.go:3464 +0x485 diff --git a/write-go/stdout.log b/write-go/stdout.log deleted file mode 100644 index 804910c6..00000000 --- a/write-go/stdout.log +++ /dev/null @@ -1,3 +0,0 @@ -time=2026-06-01T16:55:29.042+08:00 level=INFO msg="WriteServer starting" svc=writeserver -[write-server] listening on 0.0.0.0:8016 -time=2026-06-01T16:55:31.055+08:00 level=INFO msg="bot-go already running" svc=writeserver port=8017 diff --git a/write-go/writeserver.exe~ b/write-go/writeserver.exe~ deleted file mode 100644 index 66804004..00000000 Binary files a/write-go/writeserver.exe~ and /dev/null differ diff --git a/write-go/writeserver_known_good.exe b/write-go/writeserver_known_good.exe deleted file mode 100644 index 697aa1f6..00000000 Binary files a/write-go/writeserver_known_good.exe and /dev/null differ diff --git a/write-go/writeserver_patched.exe b/write-go/writeserver_patched.exe deleted file mode 100644 index e8ac7297..00000000 Binary files a/write-go/writeserver_patched.exe and /dev/null differ