From c636c9ec2e6b863c086be327a31917ec04b61aa7 Mon Sep 17 00:00:00 2001 From: Vaica <94612053+zqlit@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:07:07 +0800 Subject: [PATCH] =?UTF-8?q?bug=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .next/trace | 1 - .next/trace-build | 1 - bot/.gitignore | 2 - bot/README.md | 46 ----- bot/index.js | 75 -------- bot/package-lock.json | 12 -- bot/package.json | 10 - bot/src/admin-panel.js | 125 ------------ bot/src/config.js | 21 -- bot/src/helpers.js | 76 -------- bot/src/sessions.js | 59 ------ bot/src/tg-core.js | 54 ------ bot/src/tg-handlers.js | 413 ---------------------------------------- bot/src/tg-poll.js | 103 ---------- bot/src/write-server.js | 118 ------------ bot/start.bat | 5 - bot/writebot.ico | Bin 20177 -> 0 bytes 17 files changed, 1121 deletions(-) delete mode 100644 .next/trace delete mode 100644 .next/trace-build delete mode 100644 bot/.gitignore delete mode 100644 bot/README.md delete mode 100644 bot/index.js delete mode 100644 bot/package-lock.json delete mode 100644 bot/package.json delete mode 100644 bot/src/admin-panel.js delete mode 100644 bot/src/config.js delete mode 100644 bot/src/helpers.js delete mode 100644 bot/src/sessions.js delete mode 100644 bot/src/tg-core.js delete mode 100644 bot/src/tg-handlers.js delete mode 100644 bot/src/tg-poll.js delete mode 100644 bot/src/write-server.js delete mode 100644 bot/start.bat delete mode 100644 bot/writebot.ico diff --git a/.next/trace b/.next/trace deleted file mode 100644 index de78b322..00000000 --- a/.next/trace +++ /dev/null @@ -1 +0,0 @@ -[{"name":"generate-buildid","duration":145,"timestamp":29782277938,"id":4,"parentId":1,"tags":{},"startTime":1779266763571,"traceId":"3a8c4178be1354d5"},{"name":"load-custom-routes","duration":254,"timestamp":29782278150,"id":5,"parentId":1,"tags":{},"startTime":1779266763572,"traceId":"3a8c4178be1354d5"},{"name":"create-dist-dir","duration":423,"timestamp":29782278422,"id":6,"parentId":1,"tags":{},"startTime":1779266763572,"traceId":"3a8c4178be1354d5"},{"name":"clean","duration":708,"timestamp":29782279361,"id":7,"parentId":1,"tags":{},"startTime":1779266763573,"traceId":"3a8c4178be1354d5"},{"name":"next-build","duration":25156,"timestamp":29782255096,"id":1,"tags":{"buildMode":"default","version":"16.2.6","bundler":"turbopack","failed":true},"startTime":1779266763549,"traceId":"3a8c4178be1354d5"}] diff --git a/.next/trace-build b/.next/trace-build deleted file mode 100644 index 33aa9265..00000000 --- a/.next/trace-build +++ /dev/null @@ -1 +0,0 @@ -[{"name":"next-build","duration":25156,"timestamp":29782255096,"id":1,"tags":{"buildMode":"default","version":"16.2.6","bundler":"turbopack","failed":true},"startTime":1779266763549,"traceId":"3a8c4178be1354d5"}] diff --git a/bot/.gitignore b/bot/.gitignore deleted file mode 100644 index 2d7ec5ce..00000000 --- a/bot/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.env -node_modules/ diff --git a/bot/README.md b/bot/README.md deleted file mode 100644 index 9d56cf62..00000000 --- a/bot/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# WriteBot - -纯 Node.js,零 npm 依赖。后台管理 `write` 开发服务,提供 Web 控制面板 + Telegram 机器人。 - -## 快速开始 - -### 1. 配置 - -```powershell -cd bot -copy .env.example .env -``` - -### 2. 启动 - -```powershell -node index.js -``` - -或双击 `start.bat`。 - -### 3. 控制面板 - -浏览器打开:`http://127.0.0.1:8017/` - -一键启动/停止 Write、查看状态、复制局域网地址。 - -### 4. Telegram 机器人 - -1. 联系 [@BotFather](https://t.me/BotFather),`/newbot` 获得 token。 -2. 联系 [@userinfobot](https://t.me/userinfobot) 获得 chat id。 -3. 填入 `.env`。 - -命令:`/start` `/stop` `/status` `/ip` `/help` - -## 开机自启 - -已配好启动文件夹的快捷方式(WriteBot.lnk)。开机登录后自动启动。 - -## 防火墙 - -首次手机访问不了要放行 8016: - -```powershell -New-NetFirewallRule -DisplayName "Write Dev 8016" -Direction Inbound -Protocol TCP -LocalPort 8016 -Action Allow -``` diff --git a/bot/index.js b/bot/index.js deleted file mode 100644 index e5222783..00000000 --- a/bot/index.js +++ /dev/null @@ -1,75 +0,0 @@ -// writebot - Telegram bot + HTTP API + Web Admin -// Zero npm dependencies. Node 18+ built-in everything. - -const config = require("./src/config"); -const { getLanIP } = require("./src/helpers"); -const { startWriteServer, stopWriteServer } = require("./src/write-server"); -const { createControlServer } = require("./src/admin-panel"); -const { tg, allowed } = require("./src/tg-core"); -const { tgPoll } = require("./src/tg-poll"); - -async function main() { - const ip = getLanIP(); - console.log(""); - console.log(" \\u250C\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2510"); - console.log(" \\u2502 WriteBot \\u2502"); - console.log(" \\u251C\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2524"); - console.log(" \\u2502 Admin: http://127.0.0.1:" + config.CONTROL_PORT + "/ \\u2502"); - console.log(" \\u2502 Write: http://" + ip + ":" + config.WRITE_PORT + " \\u2502"); - console.log(" \\u2514\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2518"); - console.log(""); - - // Start HTTP control server - const controlServer = createControlServer(); - - // Connect Telegram - if (config.TG_TOKEN) { - try { - const me = await tg("getMe", {}); - if (me.ok) { - console.log("[bot] Telegram: @" + me.result.username); - - try { - await tg("setMyCommands", { - commands: [ - { command: "start", description: "\\u542F\\u52A8\\u5199\\u4F5C\\u540E\\u53F0" }, - { command: "new", description: "\\u5F00\\u59CB\\u5199\\u4E00\\u7BC7" }, - { command: "edit", description: "\\u7F16\\u8F91\\u5DF2\\u6709\\u6587\\u7AE0" }, - { command: "list", description: "\\u6587\\u7AE0\\u5217\\u8868" }, - { command: "stats", description: "\\u7F51\\u7AD9\\u6570\\u636E\\u7EDF\\u8BA1" }, - { command: "publish", description: "\\u53D1\\u5E03\\u5230\\u7F51\\u7AD9" }, - { command: "draft", description: "\\u4FDD\\u5B58\\u4E3A\\u8349\\u7A3F" }, - { command: "preview", description: "\\u9884\\u89C8\\u5168\\u6587" }, - { command: "links", description: "\\u53CB\\u94FE\\u5217\\u8868" }, - { command: "feeds", description: "\\u8BA2\\u9605\\u6E90\\u5217\\u8868" }, - { command: "help", description: "\\u5168\\u90E8\\u547D\\u4EE4" }, - ], - }); - console.log("[bot] commands registered"); - } catch (e) { - console.error("[bot] failed to register commands:", e.message); - } - - tgPoll(); - } else { - console.log("[bot] TG token invalid - HTTP-only mode"); - } - } catch { - console.log("[bot] TG connection failed - HTTP-only mode"); - } - } else { - console.log("[bot] no TG_TOKEN - HTTP-only mode"); - } - - // Graceful shutdown - for (const sig of ["SIGINT", "SIGTERM"]) { - process.on(sig, () => { - console.log("\\n[bot] " + sig + ", stopping..."); - stopWriteServer(); - controlServer.close(); - process.exit(0); - }); - } -} - -main(); diff --git a/bot/package-lock.json b/bot/package-lock.json deleted file mode 100644 index 8df4b821..00000000 --- a/bot/package-lock.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "writebot", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "writebot", - "version": "1.0.0" - } - } -} diff --git a/bot/package.json b/bot/package.json deleted file mode 100644 index d3beb48e..00000000 --- a/bot/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "writebot", - "version": "1.0.0", - "private": true, - "description": "Telegram bot to start the write dev server on LAN", - "main": "index.js", - "scripts": { - "start": "node index.js" - } -} diff --git a/bot/src/admin-panel.js b/bot/src/admin-panel.js deleted file mode 100644 index 2443621f..00000000 --- a/bot/src/admin-panel.js +++ /dev/null @@ -1,125 +0,0 @@ -// 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 = ` - - - - -WriteBot - - - -
-

WriteBot

-
Ctrl+D bookmark this page
- -
Loading...
-
-
-
-
-
-
-
-
-
- - - -
- - -
- - -`.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 }; diff --git a/bot/src/config.js b/bot/src/config.js deleted file mode 100644 index a96bf358..00000000 --- a/bot/src/config.js +++ /dev/null @@ -1,21 +0,0 @@ -// config.js - Environment and constants -const { existsSync, readFileSync } = require("fs"); -const { resolve } = require("path"); - -const envPath = resolve(__dirname, ".env"); -if (existsSync(envPath)) { - for (const line of readFileSync(envPath, "utf-8").split("\n")) { - const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.+?)\s*$/); - if (m && !process.env[m[1]]) process.env[m[1]] = m[2]; - } -} - -module.exports = { - TG_TOKEN: process.env.TG_BOT_TOKEN || "", - ALLOWED_IDS: (process.env.TG_ALLOWED_CHAT_IDS || "") - .split(",").map((s) => s.trim()).filter(Boolean).map(Number), - WRITE_PORT: parseInt(process.env.WRITE_PORT || "8016", 10), - CONTROL_PORT: parseInt(process.env.BOT_PORT || "8017", 10), - PREFER_IFACE: process.env.PREFER_IFACE || "WLAN", - WRITE_DIR: resolve(__dirname, "..", "write"), -}; diff --git a/bot/src/helpers.js b/bot/src/helpers.js deleted file mode 100644 index 44a25541..00000000 --- a/bot/src/helpers.js +++ /dev/null @@ -1,76 +0,0 @@ -// helpers.js - Utility functions -const os = require("os"); -const net = require("net"); -const config = require("./config"); - -function getLanIP() { - const now = Date.now(); - if (getLanIP._cache && getLanIP._cache.ip && now - getLanIP._cache.ts < 30000) - return getLanIP._cache.ip; - let ip = "0.0.0.0"; - try { - const ifs = os.networkInterfaces(); - const candidates = []; - for (const [name, addrs] of Object.entries(ifs)) { - if (!addrs) continue; - for (const a of addrs) { - if (a.family !== "IPv4" || a.internal) continue; - if (a.address.startsWith("169.254.")) continue; - candidates.push({ name, address: a.address }); - } - } - const pref = candidates.find((c) => - c.name.toLowerCase().includes(config.PREFER_IFACE.toLowerCase()) - ); - if (pref) ip = pref.address; - else { - const real = candidates.find((c) => { - const n = c.name.toLowerCase(); - return !n.includes("vmware") && !n.includes("virtual") && !n.includes("vethernet"); - }); - if (real) ip = real.address; - else if (candidates[0]) ip = candidates[0].address; - } - } catch {} - getLanIP._cache = { ip, ts: now }; - return ip; -} - -function checkPort(port) { - return new Promise((resolve) => { - const s = net.createConnection(port, "127.0.0.1", () => { s.destroy(); resolve(true); }); - s.on("error", () => { s.destroy(); resolve(false); }); - s.setTimeout(1500, () => { s.destroy(); resolve(false); }); - }); -} - -async function waitForReady(port, timeoutMs) { - const start = Date.now(); - while (Date.now() - start < (timeoutMs || 30000)) { - if (await checkPort(port)) return true; - await new Promise((r) => setTimeout(r, 1500)); - } - return false; -} - -function makeSlug(title) { - const dateStr = new Date().toISOString().slice(0, 10); - const slugPart = (title || "untitled") - .replace(/[^\w\u4e00-\u9fff]+/g, "-") - .replace(/^-+|-+$/g, "") - .toLowerCase() || "post"; - return dateStr + "-" + slugPart; -} - -function sessionSummary(s) { - const lines = s.content ? s.content.split("\\n").filter(Boolean).length : 0; - const chars = s.content.length; - return [ - "\\uD83D\\uDCDD " + (s.title || "\\u672A\\u547D\\u540D") + "", - s.categories.length ? "\\uD83D\\uDCC2 " + s.categories.join("\\u3001") : "", - s.tags.length ? "\\uD83C\\uDFF7 " + s.tags.join("\\u3001") : "", - chars ? "\\u270D " + lines + " \\u884C " + chars + " \\u5B57" : "", - ].filter(Boolean).join(" \\u00B7 "); -} - -module.exports = { getLanIP, checkPort, waitForReady, makeSlug, sessionSummary }; diff --git a/bot/src/sessions.js b/bot/src/sessions.js deleted file mode 100644 index c5a2f99c..00000000 --- a/bot/src/sessions.js +++ /dev/null @@ -1,59 +0,0 @@ -// sessions.js - Per-chat writing sessions -const config = require("./config"); -const { makeSlug } = require("./helpers"); - -const sessions = new Map(); - -function getSession(chatId) { - if (!sessions.has(chatId)) { - sessions.set(chatId, { - title: "", - slug: "", - content: "", - categories: [], - tags: [], - author: "qunlin", - draft: true, - }); - } - return sessions.get(chatId); -} - -function clearSession(chatId) { - sessions.delete(chatId); -} - -// Save session via Write API -async function saveViaAPI(s, isDraft) { - const body = JSON.stringify({ - frontMatter: { - title: s.title, - slug: s.slug, - date: new Date().toISOString().slice(0, 10), - draft: isDraft, - categories: s.categories, - tags: s.tags, - author: s.author, - }, - content: s.content, - }); - - const checkRes = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/posts/" + s.slug); - if (checkRes.ok) { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/posts/" + s.slug, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body, - }); - if (!res.ok) throw new Error((await res.json()).error || "API error (PUT)"); - } else { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/posts", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body, - }); - if (!res.ok) throw new Error((await res.json()).error || "API error (POST)"); - } -} - -module.exports = { sessions, getSession, clearSession, saveViaAPI }; diff --git a/bot/src/tg-core.js b/bot/src/tg-core.js deleted file mode 100644 index 4151ee22..00000000 --- a/bot/src/tg-core.js +++ /dev/null @@ -1,54 +0,0 @@ -// tg-core.js - Telegram API helpers -const config = require("./config"); - -const API = config.TG_TOKEN ? "https://api.telegram.org/bot" + config.TG_TOKEN : ""; - -async function tg(method, body) { - const res = await fetch(API + "/" + method, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - return res.json(); -} - -function allowed(chatId) { - if (config.ALLOWED_IDS.length === 0) return true; - return config.ALLOWED_IDS.includes(chatId); -} - -async function sendMessage(chatId, text, opts) { - const safeText = String(text || ""); - try { - return await tg("sendMessage", Object.assign( - { chat_id: chatId, text: safeText, parse_mode: "HTML", disable_web_page_preview: true }, - opts || {} - )); - } catch (e) { - if (e.message && (e.message.includes("parse") || e.message.includes("HTML"))) { - return await tg("sendMessage", Object.assign( - { chat_id: chatId, text: safeText }, - opts || {} - )); - } - throw e; - } -} - -async function handleCallback(chatId, queryId, data, handlers) { - if (!data) return; - const parts = data.split("_"); - const cmd = parts[0]; - const page = parseInt(parts[1]) || 1; - - if (cmd === "list" && handlers.list) { - await handlers.list(chatId, String(page)); - } else if (cmd === "links" && handlers.links) { - await handlers.links(chatId, String(page)); - } else if (cmd === "feeds" && handlers.feeds) { - await handlers.feeds(chatId, String(page)); - } - try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch {} -} - -module.exports = { tg, allowed, sendMessage, handleCallback }; diff --git a/bot/src/tg-handlers.js b/bot/src/tg-handlers.js deleted file mode 100644 index 5a838a75..00000000 --- a/bot/src/tg-handlers.js +++ /dev/null @@ -1,413 +0,0 @@ -// tg-handlers.js - All Telegram /command handlers -const config = require("./config"); -const { getLanIP, makeSlug, sessionSummary } = require("./helpers"); -const { getSession, clearSession, sessions, saveViaAPI } = require("./sessions"); -const { getWriteStatus, ensureWriteReady } = require("./write-server"); -const { sendMessage } = require("./tg-core"); - -const CMD_HANDLERS = {}; - -// ---------- System ---------- -CMD_HANDLERS.start = async function (chatId) { - const ip = getLanIP(); - const ready = await ensureWriteReady(chatId, sendMessage); - if (ready) { - return sendMessage(chatId, - "\\u2705 Write \\u5DF2\\u542F\\u52A8\\n\\n" + - "\\uD83D\\uDCF1 \\u624B\\u673A: http://" + ip + ":8016/\\n" + - "\\uD83D\\uDCDD \\u6587\\u7AE0: http://" + ip + ":1313/posts/\\n" + - "\\uD83D\\uDCBB \\u7535\\u8111: http://localhost:8016/\\n\\n" + - "/help \\u67E5\\u770B\\u5168\\u90E8\\u547D\\u4EE4" - ); - } - return sendMessage(chatId, "\\u274C \\u542F\\u52A8\\u8D85\\u65F6\\uFF0C\\u67E5\\u770B\\u65E5\\u5FD7 http://127.0.0.1:" + config.CONTROL_PORT + "/log"); -}; - -CMD_HANDLERS.stop = async function (chatId) { - const { stopWriteServer } = require("./write-server"); - if (stopWriteServer()) { - return sendMessage(chatId, "\\u26A1 Write stopped."); - } - return sendMessage(chatId, "\\u23F8 Write is not running."); -}; - -CMD_HANDLERS.status = async function (chatId) { - const s = getWriteStatus(); - const status = s.running ? "\\u2705 Running" : s.starting ? "\\u23F3 Starting..." : "\\u23F8 Stopped"; - const lines = ["Status: " + status]; - if (s.running) { - const ip = getLanIP(); - lines.push("Local: http://localhost:" + config.WRITE_PORT); - lines.push("Phone: http://" + ip + ":" + config.WRITE_PORT); - } - return sendMessage(chatId, lines.join("\\n")); -}; - -CMD_HANDLERS.ip = async function (chatId) { - const ip = getLanIP(); - return sendMessage(chatId, "Phone: http://" + ip + ":" + config.WRITE_PORT + " (" + config.PREFER_IFACE + ")"); -}; - -CMD_HANDLERS.help = async function (chatId) { - if (sessions.has(chatId)) { - return sendMessage(chatId, - "\\u270D \\u6B63\\u5728\\u5199\\u4F5C\\u4E2D\\n\\n" + - "\\u26A1 /publish \\u53D1\\u5E03 \\uD83D\\uDCC4 /draft \\u8349\\u7A3F \\uD83D\\uDD0D /preview \\u9884\\u89C8\\n" + - "\\u270F /title /categories /tags /author\\n" + - "\\u274C /cancel \\u4E22\\u5F03" - ); - } - return sendMessage(chatId, - "\\uD83E\\uDD16 WriteBot \\u547D\\u4EE4\\n\\n" + - "\\uD83D\\uDCDD \\u5199\\u4F5C:\\n" + - " /new \\u6807\\u9898 - \\u5F00\\u59CB\\u5199\\u4E00\\u7BC7\\n" + - " /edit slug - \\u7F16\\u8F91\\u5DF2\\u6709\\u6587\\u7AE0\\n" + - " /delete slug - \\u5220\\u9664\\u6587\\u7AE0\\n" + - " /list - \\u6587\\u7AE0\\u5217\\u8868\\n\\n" + - "\\uD83D\\uDD17 \\u53CB\\u94FE:\\n" + - " /links - \\u67E5\\u770B\\u53CB\\u94FE\\n" + - " /link_add url name - \\u6DFB\\u52A0\\u53CB\\u94FE\\n" + - " /link_del url - \\u5220\\u9664\\u53CB\\u94FE\\n" + - " /link_toggle url - \\u663E\\u793A/\\u9690\\u85CF\\n\\n" + - "\\uD83D\\uDCE1 \\u8BA2\\u9605:\\n" + - " /feeds - \\u67E5\\u770B\\u8BA2\\u9605\\u6E90\\n" + - " /feed_add url - \\u6DFB\\u52A0\\u8BA2\\u9605\\u6E90\\n" + - " /feed_del url - \\u5220\\u9664\\u8BA2\\u9605\\u6E90\\n\\n" + - "\\uD83D\\uDCCA /stats \\u7F51\\u7AD9\\u6570\\u636E" - ); -}; - -// ---------- Articles ---------- -CMD_HANDLERS.new = async function (chatId, title) { - if (!title) return sendMessage(chatId, "\\u683C\\u5F0F\\uFF1A\\u5199\\u4E00\\u7BC7 \\u6807\\u9898"); - const s = getSession(chatId); - s.title = title; - s.slug = makeSlug(title); - s.content = ""; - s.categories = []; - s.tags = []; - s.draft = true; - s.author = "qunlin"; - return sendMessage(chatId, - "\\uD83D\\uDCDD \\u5F00\\u59CB\\u5199\\uFF1A" + title + "\\n\\n" + - "\\u76F4\\u63A5\\u53D1\\u6D88\\u606F\\u5199\\u4F5C\\uFF0C\\u5B8C\\u6210\\u540E /publish\\n" + - "\\u968F\\u65F6 /preview \\u67E5\\u770B\\uFF0C/help \\u5168\\u90E8\\u547D\\u4EE4" - ); -}; - -CMD_HANDLERS.edit = async function (chatId, slug) { - if (!slug) return sendMessage(chatId, "\\u683C\\u5F0F\\uFF1A/edit slug\\n\\u5148\\u7528 /list \\u67E5\\u770B\\u6587\\u7AE0 slug\\u3002"); - try { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/posts/" + slug); - if (!res.ok) throw new Error("\\u672A\\u627E\\u5230\\u8BE5\\u6587\\u7AE0"); - const post = await res.json(); - const s = getSession(chatId); - s.title = post.frontMatter.title; - s.slug = post.frontMatter.slug; - s.content = post.content || ""; - s.categories = post.frontMatter.categories || []; - s.tags = post.frontMatter.tags || []; - s.author = post.frontMatter.author || "qunlin"; - s.draft = post.frontMatter.draft; - return sendMessage(chatId, "\\u270F \\u7F16\\u8F91\\uFF1A" + s.title + "\\n\\n" + sessionSummary(s) + "\\n\\n\\u76F4\\u63A5\\u53D1\\u6D88\\u606F\\u8FFD\\u52A0\\u5185\\u5BB9\\uFF0C\\u5199\\u5B8C\\u53D1\\u300C\\u53D1\\u5E03\\u300D\\u3002"); - } catch (e) { - return sendMessage(chatId, "\\u274C " + e.message); - } -}; - -CMD_HANDLERS.delete = async function (chatId, slug) { - if (!slug) return sendMessage(chatId, "Usage: /delete \\nFind slugs with /list\\n\\nArticles are moved to recycle bin (30-day retention)."); - try { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/posts/" + slug, { method: "DELETE" }); - if (!res.ok) throw new Error((await res.json()).error || "Delete failed"); - return sendMessage(chatId, "\\u2702 Moved to recycle bin: " + slug + "\\nRecoverable within 30 days."); - } catch (e) { - return sendMessage(chatId, "\\u274C " + e.message); - } -}; - -CMD_HANDLERS.view = async function (chatId, slug) { - if (!slug) return sendMessage(chatId, "Usage: /view \\nFind slugs with /list"); - const ip = getLanIP(); - const url = "http://" + ip + ":1313/" + slug + ".html"; - return sendMessage(chatId, "\\uD83D\\uDCC9 Preview\\n\\n" + url + "\\n\\n\\uD83D\\uDCBB http://localhost:1313/" + slug + ".html"); -}; - -CMD_HANDLERS.title = async function (chatId, newTitle) { - const s = getSession(chatId); - if (!newTitle) return sendMessage(chatId, "\\u683C\\u5F0F\\uFF1A\\u6807\\u9898 \\u65B0\\u6807\\u9898"); - s.title = newTitle; - s.slug = makeSlug(newTitle); - return sendMessage(chatId, "\\u270F \\u6807\\u9898\\u5DF2\\u66F4\\u65B0\\uFF1A" + newTitle + ""); -}; - -CMD_HANDLERS.categories = async function (chatId, cats) { - const s = getSession(chatId); - s.categories = cats ? cats.split(/[,;\"\\",\\"\\uFF0C\\u3001]+/).map(function(c){return c.trim();}).filter(Boolean) : []; - return sendMessage(chatId, "\\uD83D\\uDCC2 \\u5206\\u7C7B\\uFF1A" + (s.categories.join("\\u3001") || "\\u5DF2\\u6E05\\u7A7A")); -}; - -CMD_HANDLERS.tags = async function (chatId, tags) { - const s = getSession(chatId); - s.tags = tags ? tags.split(/[,;\"\\",\\"\\uFF0C\\u3001]+/).map(function(t){return t.trim();}).filter(Boolean) : []; - return sendMessage(chatId, "\\uD83C\\uDFF7 \\u6807\\u7B7E\\uFF1A" + (s.tags.join("\\u3001") || "\\u5DF2\\u6E05\\u7A7A")); -}; - -CMD_HANDLERS.author = async function (chatId, author) { - const s = getSession(chatId); - s.author = author || "qunlin"; - return sendMessage(chatId, "\\u270D \\u4F5C\\u8005\\uFF1A" + s.author); -}; - -CMD_HANDLERS.preview = async function (chatId) { - const s = sessions.get(chatId); - if (!s || !s.title) return sendMessage(chatId, "\\u8FD8\\u6CA1\\u5F00\\u59CB\\u5199\\u3002\\u53D1\\u300C\\u5199\\u4E00\\u7BC7\\u300D\\u5F00\\u59CB\\u3002"); - if (!s.content) return sendMessage(chatId, "\\u6B63\\u6587\\u4E3A\\u7A7A\\u3002"); - const maxLen = 3800; - const preview = s.content.slice(0, maxLen); - return sendMessage(chatId, "\\uD83D\\uDCDD " + s.title + "\\n\\n" + preview + (s.content.length > maxLen ? "\\n\\n... \\u524D " + maxLen + " \\u5B57" : "")); -}; - -CMD_HANDLERS.draft = async function (chatId) { - const s = sessions.get(chatId); - if (!s || !s.title) return sendMessage(chatId, "\\u8FD8\\u6CA1\\u5F00\\u59CB\\u5199\\u3002\\u53D1\\u300C\\u5199\\u4E00\\u7BC7\\u300D\\u5F00\\u59CB\\u3002"); - if (!s.content) return sendMessage(chatId, "\\u6B63\\u6587\\u4E3A\\u7A7A\\uFF0C\\u5148\\u5199\\u70B9\\u5185\\u5BB9\\u518D\\u4FDD\\u5B58\\u3002"); - try { - await saveViaAPI(s, true); - clearSession(chatId); - return sendMessage(chatId, "\\uD83D\\uDCC4 \\u8349\\u7A3F\\u5DF2\\u4FDD\\u5B58\\uFF1A" + s.title + ""); - } catch (e) { - return sendMessage(chatId, "\\u274C \\u4FDD\\u5B58\\u5931\\u8D25\\uFF1A" + e.message + "\\n\\n\\u8BF7\\u5148 /start \\u542F\\u52A8\\u540E\\u53F0\\u670D\\u52A1\\u3002"); - } -}; - -CMD_HANDLERS.publish = async function (chatId) { - const s = sessions.get(chatId); - if (!s || !s.title) return sendMessage(chatId, "\\u8FD8\\u6CA1\\u5F00\\u59CB\\u5199\\u3002\\u53D1\\u300C\\u5199\\u4E00\\u7BC7\\u300D\\u5F00\\u59CB\\u3002"); - if (!s.content) return sendMessage(chatId, "\\u6B63\\u6587\\u4E3A\\u7A7A\\uFF0C\\u5148\\u5199\\u70B9\\u5185\\u5BB9\\u518D\\u53D1\\u5E03\\u3002"); - try { - await saveViaAPI(s, false); - const slug = s.slug; - clearSession(chatId); - const ip = getLanIP(); - return sendMessage(chatId, "\\u2705 \\u5DF2\\u53D1\\u5E03\\uFF01\\n\\n\\uD83D\\uDCDD " + s.title + "\\n\\u9884\\u89C8\\uFF1Ahttp://localhost:1313/" + slug + ".html\\n\\u7EBF\\u4E0A\\uFF1Ahttps://usj.cc/" + slug + ".html"); - } catch (e) { - return sendMessage(chatId, "\\u274C \\u53D1\\u5E03\\u5931\\u8D25\\uFF1A" + e.message + "\\n\\n\\u8BF7\\u5148 /start \\u542F\\u52A8\\u540E\\u53F0\\u670D\\u52A1\\u3002"); - } -}; - -CMD_HANDLERS.cancel = async function (chatId) { - const s = sessions.get(chatId); - if (!s || !s.title) return sendMessage(chatId, "\\u6CA1\\u6709\\u6D3B\\u52A8\\u7684\\u8349\\u7A3F\\u3002"); - clearSession(chatId); - return sendMessage(chatId, "\\u2702 \\u8349\\u7A3F\\u5DF2\\u4E22\\u5F03\\u3002"); -}; - -CMD_HANDLERS.list = async function (chatId, pageStr) { - const ip = getLanIP(); - return sendMessage(chatId, - "\\uD83D\\uDCD1 \\u6587\\u7AE0\\u5217\\u8868\\n\\n" + - "\\uD83D\\uDCF1 \\u624B\\u673A\\u6253\\u5F00: http://" + ip + ":1313/posts/\\n" + - "\\uD83D\\uDCBB \\u7535\\u8111\\u6253\\u5F00: http://localhost:1313/posts/\\n\\n" + - "\\u2705 \\u65B0\\u5EFA: /new \\u300A\\u6807\\u9898\\u300B\\n" + - "\\uD83D\\uDCD1 \\u7F16\\u8F91: /edit slug\\n" + - "\\uD83D\\uDCC4 \\u8349\\u7A3F: /draft\\n" + - "\\uD83D\\uDE80 \\u53D1\\u5E03: /publish" - ); -}; - -// ---------- Links (友链) ---------- -CMD_HANDLERS.links = async function (chatId, pageStr) { - const page = parseInt(pageStr) || 1; - const perPage = 10; - try { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links?all=1"); - const data = await res.json(); - const allLinks = data.links || []; - if (allLinks.length === 0) return sendMessage(chatId, "No links found."); - - const totalPages = Math.ceil(allLinks.length / perPage); - const p = page > totalPages ? totalPages : page; - const start = (p - 1) * perPage; - const pageLinks = allLinks.slice(start, start + perPage); - - const lines = ["\\uD83D\\uDD17 \\u53CB\\u94FE \\u5171" + allLinks.length + " \\u4E2A \\u00B7 \\u7B2C" + p + "/" + totalPages + " \\u9875"]; - pageLinks.forEach(function(l) { - const icon = l.hidden ? "\\uD83D\\uDD12" : "\\uD83C\\uDF10"; - const name = (l.name || l.url).slice(0, 40); - lines.push(icon + " " + name + " " + l.url); - }); - - const keyboard = { inline_keyboard: [] }; - const navRow = []; - if (p > 1) navRow.push({ text: "\\u25C0 \\u4E0A\\u4E00\\u9875", callback_data: "links_" + (p - 1) }); - if (p < totalPages) navRow.push({ text: "\\u4E0B\\u4E00\\u9875 \\u25B6", callback_data: "links_" + (p + 1) }); - if (navRow.length) keyboard.inline_keyboard.push(navRow); - - const payload = { - chat_id: chatId, - text: lines.join("\\n"), - parse_mode: "HTML", - reply_markup: navRow.length ? keyboard : undefined, - disable_web_page_preview: true, - }; - const { tg } = require("./tg-core"); - return tg("sendMessage", payload); - } catch (e) { - return sendMessage(chatId, "Please /start Write first."); - } -}; - -CMD_HANDLERS.link_add = async function (chatId, arg) { - if (!arg) return sendMessage(chatId, "Usage: /link_add \\nExample: /link_add https://example.com My Blog"); - const parts = arg.split(/\\s+/); - let url = parts[0]; - const name = parts.slice(1).join(" ") || url; - if (!url.startsWith("http")) url = "https://" + url; - try { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ url, name }), - }); - if (!res.ok) throw new Error((await res.json()).error || "Add failed"); - return sendMessage(chatId, "\\u2705 Link added: " + name + "\\n" + url); - } catch (e) { - return sendMessage(chatId, "\\u274C " + e.message); - } -}; - -CMD_HANDLERS.link_del = async function (chatId, url) { - if (!url) return sendMessage(chatId, "Usage: /link_del \\nUse /links to find the URL."); - try { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links?url=" + encodeURIComponent(url), { - method: "DELETE", - }); - if (!res.ok) throw new Error((await res.json()).error || "Delete failed"); - return sendMessage(chatId, "\\u2705 Link deleted: " + url); - } catch (e) { - return sendMessage(chatId, "\\u274C " + e.message); - } -}; - -CMD_HANDLERS.link_toggle = async function (chatId, url) { - if (!url) return sendMessage(chatId, "Usage: /link_toggle \\nToggle visibility of a link."); - try { - const listRes = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links?all=1"); - const listData = await listRes.json(); - const link = (listData.links || []).find(function(l) { return l.url === url; }); - if (!link) return sendMessage(chatId, "\\u274C Link not found: " + url); - const newHidden = !link.hidden; - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ oldUrl: url, hidden: newHidden }), - }); - if (!res.ok) throw new Error((await res.json()).error || "Toggle failed"); - const label = newHidden ? "\\uD83D\\uDD12 Hidden" : "\\uD83C\\uDF10 Visible"; - return sendMessage(chatId, label + ": " + url); - } catch (e) { - return sendMessage(chatId, "\\u274C " + e.message); - } -}; - -// ---------- Feeds (订阅) ---------- -CMD_HANDLERS.feeds = async function (chatId, pageStr) { - const page = parseInt(pageStr) || 1; - const perPage = 10; - try { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/feeds"); - const data = await res.json(); - const allFeeds = data.feeds || []; - if (allFeeds.length === 0) return sendMessage(chatId, "No feeds configured."); - - const totalPages = Math.ceil(allFeeds.length / perPage); - const p = page > totalPages ? totalPages : page; - const start = (p - 1) * perPage; - const pageFeeds = allFeeds.slice(start, start + perPage); - - const lines = ["\\uD83D\\uDCE1 \\u8BA2\\u9605\\u6E90 \\u5171" + allFeeds.length + " \\u4E2A \\u00B7 \\u7B2C" + p + "/" + totalPages + " \\u9875"]; - pageFeeds.forEach(function(f) { - const title = (f.feedTitle || f.url).slice(0, 50); - lines.push("\\uD83D\\uDCDC " + title + "\\n " + f.url); - }); - - const keyboard = { inline_keyboard: [] }; - const navRow = []; - if (p > 1) navRow.push({ text: "\\u25C0 \\u4E0A\\u4E00\\u9875", callback_data: "feeds_" + (p - 1) }); - if (p < totalPages) navRow.push({ text: "\\u4E0B\\u4E00\\u9875 \\u25B6", callback_data: "feeds_" + (p + 1) }); - if (navRow.length) keyboard.inline_keyboard.push(navRow); - - const { tg } = require("./tg-core"); - return tg("sendMessage", { - chat_id: chatId, - text: lines.join("\\n"), - parse_mode: "HTML", - reply_markup: navRow.length ? keyboard : undefined, - disable_web_page_preview: true, - }); - } catch (e) { - return sendMessage(chatId, "Please /start Write first."); - } -}; - -CMD_HANDLERS.feed_add = async function (chatId, arg) { - if (!arg) return sendMessage(chatId, "Usage: /feed_add \\nExample: /feed_add https://example.com/rss.xml"); - const parts = arg.split(/\\s+/); - const url = parts[0]; - const feedTitle = parts.slice(1).join(" ") || ""; - try { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/feeds", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ url, feedTitle }), - }); - if (!res.ok) throw new Error((await res.json()).error || "Add failed"); - return sendMessage(chatId, "\\u2705 Feed added: " + url); - } catch (e) { - return sendMessage(chatId, "\\u274C " + e.message); - } -}; - -CMD_HANDLERS.feed_del = async function (chatId, url) { - if (!url) return sendMessage(chatId, "Usage: /feed_del \\nUse /feeds to find the URL."); - try { - const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/feeds?url=" + encodeURIComponent(url), { - method: "DELETE", - }); - if (!res.ok) throw new Error((await res.json()).error || "Delete failed"); - return sendMessage(chatId, "\\u2705 Feed deleted: " + url); - } catch (e) { - return sendMessage(chatId, "\\u274C " + e.message); - } -}; - -// ---------- Stats ---------- -CMD_HANDLERS.stats = async function (chatId) { - try { - const [resPosts, resLinks, resFeeds] = await Promise.all([ - fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/posts?perPage=1"), - fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links?all=1"), - fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/feeds"), - ]); - const postsData = await resPosts.json(); - const linksData = await resLinks.json(); - const feedsData = await resFeeds.json(); - - const totalPosts = postsData.totalCount || postsData.total || 0; - const totalLinks = (linksData.links || []).filter(function(l) { return !l.hidden; }).length; - const totalFeeds = (feedsData.feeds || []).length; - - return sendMessage(chatId, - "\\uD83D\\uDCCA \\u7F51\\u7AD9\\u6570\\u636E\\n\\n" + - "\\uD83D\\uDCDD \\u6587\\u7AE0\\uFF1A" + totalPosts + " \\u7BC7\\n" + - "\\uD83D\\uDD17 \\u53CB\\u94FE\\uFF1A" + totalLinks + " \\u4E2A\\n" + - "\\uD83D\\uDCE1 \\u8BA2\\u9605\\u6E90\\uFF1A" + totalFeeds + " \\u4E2A\\n\\n" + - "\\uD83C\\uDF10 usj.cc" - ); - } catch (e) { - return sendMessage(chatId, "\\u8BF7\\u5148 /start \\u542F\\u52A8\\u540E\\u53F0\\u670D\\u52A1\\u3002"); - } -}; - -module.exports = CMD_HANDLERS; diff --git a/bot/src/tg-poll.js b/bot/src/tg-poll.js deleted file mode 100644 index 2989b339..00000000 --- a/bot/src/tg-poll.js +++ /dev/null @@ -1,103 +0,0 @@ -// tg-poll.js - Telegram polling loop and message dispatch -const config = require("./config"); -const { tg, allowed, sendMessage, handleCallback } = require("./tg-core"); -const { sessions, getSession } = require("./sessions"); -const CMD_HANDLERS = require("./tg-handlers"); - -let lastOffset = 0; - -// Commands that accept an argument -const ARG_COMMANDS = new Set([ - "new", "title", "categories", "tags", "author", - "edit", "view", "delete", - "link_add", "link_del", "link_toggle", - "feed_add", "feed_del", - "list", "links", "feeds", -]); - -async function tgPoll() { - if (!config.TG_TOKEN) return; - try { - const data = await tg("getUpdates", { - offset: lastOffset + 1, - timeout: 30, - allowed_updates: ["message", "callback_query"], - }); - if (data.ok && data.result) { - for (const update of data.result) { - lastOffset = update.update_id; - - // Handle callback queries (inline buttons) - if (update.callback_query) { - const cb = update.callback_query; - const cbChatId = cb.message ? cb.message.chat.id : (cb.from ? cb.from.id : 0); - handleCallback(cbChatId, cb.id, cb.data, CMD_HANDLERS); - continue; - } - - const msg = update.message; - const chatId = msg.chat.id; - if (!allowed(chatId)) continue; - - // Handle photo in writing session - append as Markdown image - if (msg.photo && sessions.has(chatId)) { - try { - const s = getSession(chatId); - const largestPhoto = msg.photo[msg.photo.length - 1]; - const fileRes = await tg("getFile", { file_id: largestPhoto.file_id }); - if (fileRes.ok) { - const filePath = fileRes.result.file_path; - const caption = msg.caption || ""; - const imgUrl = "https://api.telegram.org/file/bot" + config.TG_TOKEN + "/" + filePath; - s.content += "\\n![" + (caption || "image") + "](" + imgUrl + ")\\n"; - } - } catch (e) { - console.error("[photo]", e.message); - } - continue; - } - - if (!msg?.text) continue; - - const fullText = msg.text ? msg.text.trim() : (msg.caption || "").trim(); - const parts = fullText.split("@")[0].split(/\\s+/); - const slashCmd = parts[0].toLowerCase().slice(1); - const slashArg = parts.slice(1).join(" "); - const hasSession = sessions.has(chatId); - - // 1) If in writing session and NOT a slash command, append text silently - if (hasSession && !fullText.startsWith("/")) { - try { - const s = getSession(chatId); - s.content += fullText + "\\n"; - } catch (e) { - console.error("[append]", e.message); - } - continue; - } - - // 2) Slash commands - each wrapped in try/catch - try { - if (ARG_COMMANDS.has(slashCmd)) { - const handler = CMD_HANDLERS[slashCmd]; - if (handler) await handler(chatId, slashArg); - else if (slashCmd) await sendMessage(chatId, "\\u274C Unknown: /" + slashCmd); - } else { - const handler = CMD_HANDLERS[slashCmd]; - if (handler) await handler(chatId); - else if (slashCmd) await sendMessage(chatId, "\\u274C Unknown: /" + slashCmd); - } - } catch (e) { - console.error("[handler " + slashCmd + "]", e.message); - try { await sendMessage(chatId, "\\u274C " + (e.message || "error").slice(0, 200)); } catch {} - } - } - } - } catch (e) { - console.error("[tg poll]", e.message); - await new Promise((r) => setTimeout(r, 3000)); - } - setImmediate(tgPoll); -} - -module.exports = { tgPoll }; diff --git a/bot/src/write-server.js b/bot/src/write-server.js deleted file mode 100644 index 1e02401e..00000000 --- a/bot/src/write-server.js +++ /dev/null @@ -1,118 +0,0 @@ -// write-server.js - Manage the Write (Next.js) and Hugo processes -const { spawn, execSync } = require("child_process"); -const config = require("./config"); -const { waitForReady } = require("./helpers"); - -let writeProcess = null; -let writeReady = false; -let hugoProcess = null; - -function startWriteServer() { - if (writeProcess) { - return { started: false, reason: writeReady ? "already_running" : "starting" }; - } - writeReady = false; - const isWin = process.platform === "win32"; - writeProcess = spawn( - isWin ? "npm.cmd" : "npm", - ["run", "dev", "--", "-p", String(config.WRITE_PORT), "-H", "0.0.0.0"], - { cwd: config.WRITE_DIR, shell: isWin, stdio: "pipe", windowsHide: true } - ); - writeProcess.stdout?.on("data", (d) => process.stdout.write(d)); - writeProcess.stderr?.on("data", (d) => process.stderr.write(d)); - writeProcess.on("exit", (code) => { - console.log("[write] exited " + code); - writeProcess = null; - writeReady = false; - }); - return { started: true }; -} - -function stopWriteServer() { - if (hugoProcess) { - try { - const hpid = hugoProcess.pid; - if (process.platform === "win32" && hpid) { - execSync("taskkill /PID " + hpid + " /T /F", { stdio: "ignore" }); - } else { - hugoProcess.kill(); - } - } catch {} - hugoProcess = null; - } - if (!writeProcess) return false; - const pid = writeProcess.pid; - try { - if (process.platform === "win32" && pid) { - execSync("taskkill /PID " + pid + " /T /F", { stdio: "ignore" }); - } else { - writeProcess.kill(); - } - } catch {} - writeProcess = null; - writeReady = false; - return true; -} - -function startHugoIfNeeded() { - if (hugoProcess) return; - try { - hugoProcess = spawn("hugo", ["server", "--bind", "0.0.0.0", "--port", "1313", "--disableFastRender"], { - cwd: config.WRITE_DIR + "/..", - shell: true, - stdio: "pipe", - windowsHide: true, - }); - hugoProcess.on("exit", (code) => { - console.log("[hugo] exited " + code); - hugoProcess = null; - }); - console.log("[hugo] started on 0.0.0.0:1313"); - } catch (e) { - console.error("[hugo]", e.message); - } -} - -function getWriteStatus() { - return { - running: !!(writeProcess && writeReady), - starting: !!(writeProcess && !writeReady), - }; -} - -async function ensureWriteReady(chatId, sendMessage) { - if (writeProcess && writeReady) { - startHugoIfNeeded(); - return true; - } - if (writeProcess && !writeReady) { - await sendMessage(chatId, "\\u231B Write \\u6B63\\u5728\\u542F\\u52A8\\u4E2D..."); - const ready = await waitForReady(config.WRITE_PORT); - if (ready) { - writeReady = true; - startHugoIfNeeded(); - return true; - } - return false; - } - const result = startWriteServer(); - if (!result.started) return false; - await sendMessage(chatId, "\\u231B \\u6B63\\u5728\\u542F\\u52A8 Write..."); - const ready = await waitForReady(config.WRITE_PORT); - if (ready) { - writeReady = true; - startHugoIfNeeded(); - return true; - } - return false; -} - -module.exports = { - get writeReady() { return writeReady; }, - get writePort() { return config.WRITE_PORT; }, - startWriteServer, - stopWriteServer, - startHugoIfNeeded, - getWriteStatus, - ensureWriteReady, -}; diff --git a/bot/start.bat b/bot/start.bat deleted file mode 100644 index 5ed68ea4..00000000 --- a/bot/start.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -chcp 65001 >nul -cd /d "%~dp0" -node index.js -pause diff --git a/bot/writebot.ico b/bot/writebot.ico deleted file mode 100644 index 8a595e740a77b1bfc4e429eaaf40463d5359725f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20177 zcmb5VWlS8-7d5;T3beTEVvD=GySux)J4H%ycXuhaxWi(_9g4fd;_j5v=l4(EU67fBll^~A zV7`3$f{cWWkAi~FPEJhD{{L+MegQBLVUl2yU|}c#pDeU@-t9fd0gz9}a68E5`wuWS(D_QaIa&SJWJKlW%RTYn;y~ z53*NQQWKY?x?813csuMoom^CZibF%Vkh&lWKv3yPd_v$mBbDIHNif1m<`+hfP-3YS z=ITO?vz>6V+4u#%DO6`yrO}+L+ffYD8`$qu(R&xT@oB7D@ct_RIRtU-0=k(vzt-st zZ{p6sMvN}|*@^BO(LR$<2weepTms6eKP_{=?IGCns6wDs*jDlPilLKdYvqu|!_DF? zX9dX6BlM;&R(829gFL--fU~L68%>T9>ccgB^%ti`51+m51Bhj9E*GbQuQAHZi?r`h zH*%JJupHrfMYsWD&E(~cr=q1wliVwSqAk%PzC5Qcz7_rpveGi8XxgymqzoOehUqP4 z(+g`V5wR=wcCLkH5(&ik>F>E-nK7r7{Z_>sw3wqBDHZY=`UgPgA&x1OFrpHy&B3$s zmv5~qjl>L3X@Hl%GSUvRr7BBn5=%>{?i1n)YWI9u#E+xwthLi zV)D*}a}=N3^O~wkV%IrJt-Z{)cWXbu+c(Bjd}6!s?UG)%*W5m__SU@){FFk0@*;Iz zdlkpMdurbM4-lHDOc_wm60~(j{-c(3vgs3dFd;s|oE&3srx)S|KJtPo*M6|x9ttR4Bnx%_rxa%&%oFWOU=K5Yiu5Idt7M2{T=U(E zEVUsgSm}m0loUzPctURVCL-O4zp7*06vt!5l~C&QT~bSN?pz*Ppdh`9*Zyxi%c9MU z4k0WhNepRa!{9>6z(K?Y(^CgJn0SPwzZlQ@1;%AuT=)${FER#tsAu~}@qMqFb?1Se zge48mrhvam>39$eG4ha)s7b;gGVHtzvfOW=@gULix)S^mv%QTHlxPTMzl#(D#LsDL zeuEP6Q7Uc0+|GH&G}04oZ0j}bB(@U5LBQ0v8OAlUp?Q9#)hc)cA>!MXJ`2@e zKfmj7tFJ%DJUa@%rXXWF#FpN~Fi#Hjl($>maw#kXQk!eKxI(qUd8hYJz_~>TXSEH> z&m|isIY);h0x#zQ3~?F)Ut$o5>!RD1j0rmkH~$_W3F$DDMyFN0W=r*+=~a3dSj+i) zGK^lQM{ksh5S2SB1$iHxX!-6F(qJOpbpYj$Q^v_nZ@W~uNY{VbL<;w4J-7S=fU0Gp zB-Co$8wdWtf>YK|{{g(l4u*gjUB+U)wV(AC++e$!=B+zclpQKK44{AbIh$5dr1LU3 zTF@6zm?4i^i6fgVx$8U)1R39>*kqJ8+eZ*&>Y7LOyBB3J11R&8v&O=wY&#XjIEbYo zRV7o+8+|36aZrN5)ro*!Jd=w;@RfaZg!M}&AJ(Leg@^5~i{-GW@ceNyq#8#n4Rfwb zBRek-OHCOH`D0&ik={E+#Np96)7Dk#n}PbGZ(VUKGli;9Poij2xc(0i>E@vhRAB46 zv`(G273ga^dU(XtP+lX-66#`34%6YlE_bYLvra@ev3cqJ?w9#^|2=j8vG@0RcKUK( zWR<+7CDLZ9?hIRytGvk>71}tNO-Eo z{vwqXF8A`NB_sz-FKrF3j1l(sX#GR4U&lI7H5f)MH~SFF9O&nJ{M62SUO(1}OlhEw zmOp1nn#CBdL!#MP*0LC9@x68}rZ$mi#7#fzlt3^uc^M{$nddb~XzWrXcH8M?2N&tx zu#-%8VfI9$Z988-42J~wh1*NSJjMQeT zKaXfMGFSQNRFOg8y4>HzkV#C`!YvwV?Oa=?$-)%d1kgf3UecIL*@L>{m!7dc0WynD zZo+H#TsTvd+IGpJh57A3tfNe!KsBqIkM<_{2zT_!T>ZB-8m3^lEny1E^Rfv72~lqD z-Z9V-pQngtO-}BbAPA2PtNtC4pRIPhsV&QfX5~S~9O!_38nd!UzV+j`K}k;epmJMV zF-_Nk(WTSEzCgZXbPYkoAsl%!Da%D7b)j+ee!ul;P>?L*m3Py8r$woq)9?ZsahvGW z)4kF$Rgk{$A|B^~QP~8qJ6`aOf(lt}=|I&LkaDjZskv8onUv%?c>#XQU)DrG%_H{Ip;k0 zD(<)MNx>au1@OMhxK`O$jdeY?=OnCGOs$AN-~XJ>%9-)aN^b1~U_zcb7vj-noel8e}zO=zjw> z82w2&ebaZ%{-ylPJJsAuu|)#o38 zdGq_%?`LgZdEePON=hv)?nQ>QgPMUu)grlC)W-3A;x%xk)aTPXx#KIB-~4WTU*Ch} z_*2wHr6Y5W+W&Yy7XC*6R{IYCV`|kn8KcMrq7+SBGG%P>6m*JH{WW2`gH#d+-x|Tz zbh>w5@NND0__(fP#z>-zhI`;IA7vY3_w{jW1_{Z$J-8h*xmBU`-;%IE{-0_F{H3UgM7s;?+~@Z35YPg3amogoGGK{Cc`Tqn^D9p?_Z6&>MQ=ao$=UgnM! z5GHO=4!5XFQE%0?3_K6WJtYqF^Mum$O8@BAa5@TyBefp_%fzQ-#sVJPcpOi5+%jPu zJ&Vd&HOiEa@0LV)+(T1y zbm!)foGCPf@W4?GG6&1Gt3hJ6pSi;SG8T09w%eayGPHCgoftT5@>0Vh8Ok2!Mae(I zAX1H*@>Q5nx{WAKdr*?HXfeh+r{jG0-0^k(m0dNw19JZ*(D?caezo?j`zVNR5Y@{Y z?(gVGQHbtiS6ACUkW^4$7Q3U>LzfOie-I7B0DpZF4MeAcQ!Dnw07R*6QJ?6Bgaluu z<4E2JaH>N%mLkTL3cfCAzeQ&88E{V?Lr75*_r1Xqu=Pi z=E*nF>+8lf%ZdV+?hnsQ(r4@O@SgZbQQhK2(6u1xX-%W7&JNJ2rE(_9{86Dc)fbxx zvb}Cz=@G7_6qxNvr&-b3A?l}{vY2PLrRHt-ix4n4f8NUNFgG+O_3b7>+??!rEJ)IGk+?K>>eR)OKXW+ZP{Zh{~Amy zhf=(F^AA}1AszM8VBsSyfE6aW8btz@r=m$k7HEb6W>tLY*&=uKln5lcs-Hn_l-=n@u zqmIJWB=I+L@Yx9>`J+S%EDR=VY#=(p90o0hr7fbD{8Jbjd}}7q zZ7_7^{HtF=P24@?%G>fs6WhKtWVqhBD3Jo<#&JG-)b<`_jum)M+K&WTnq{;FVS?Soj$ho^)45iv}({b9=}M|*t>OZg|B${o#%meA^_Uj}1W zk>1lv5AbKh4{-n}U4zD?`zeu9Q)r z{@;%i_`xgqnf>*k#`JoU%IhECs-2U+JT&ynpD)>Sf-b6)`4SPIkCfj()1Gg?|Bi$P=`^NJb%C zOUJ@-(C&b_GI>pZVce>ZDqksVZtpSuW|oUe;K)jVV*;8O-lJjJnZvt@-qr03r1|Qa z-A|TXlT#wFylB;l@){4hgK=|oLC*X#OjDGc(mJ)2f8eOVhgsq_)xD^YOI%gKH@vLD z`i(G7yO*BcI+e1nz38ls*lF|^=KL8I-1p@;HEDV1YT6lN` z^OI`V9ZdTc??h;3&h*adkIn?Zj^v||!O33wMf!A7TCipHCvUcNQtJTeZZB=PkHMr>&m#+t1ijz95snLDJ#fsf~`HN(K^OiBT zfz-~Gw*cj%dqwR1<7I5zf7>kqQ-kB{B~g-@)thh!%DLJEuH%vp-F~&pnJ^UP(H)=r zaANSN5E$VyO>TIk{WIEP;sSPUh%|VTL%u~}jAja6<1-Q*XWRi>&@7)AIJeJ9EjF?? z=LRBZiW{KX({3Z zyv_JS^xZS=*y20HF60W{h(`%OXB4YbVd}Cjh48$pHZ=Eos1JR^tORG_%%VU;|Lr+TZ-c*zjqTIpy+$YzyH!{l)V3 z(_E2|c<;qCc0?i$=i_3tikyzMfAyxIqg2K>FQ~%Aw()qCeRCd7R5s-lK`wFynfRelkl7k z^nO1G)}EV|&=X>uV$7h);HyYh;(XB_OneEdKu$@;%wfJvFM3fKBIaTKD20rELhVi^ zuA$AP1gb{^)$Eoi#+=7xA3>6Z;Pw6z@e}&uvfpb23!0*Kp77`(N$r#HBqXxGPMTopzA#yEKa)oNv35F;(p^vmt zy8s2DW3V0}bEOHG>=y*tztr1XXv*JfYHOfl?+S02H>YL6tP#Pg&5zh|#H7k7H{73Y zkd=KsPU^D0nuOf@Pg|j~ErNB&DEfYftuvGSf#S#y9D@gskG#LyCp-#(0wz)hNA5YQ zuJ}j&^w`;XBu~jXmx;V4!q+t;o5gJ-@4jcT+Qv6`<$N9@%GJBi$W*^Dz;b|mjUU~= zCtRG8-IU_?Z};3@dO>r#s*3iu`vuE08#N{b1s_G0u3ZKWdJeon(vv^t3*uTcg`JkK zIj{y@oSdUzoHWX^MftoC(LbHm`R+3BZ&K*(*M(&NR{l$GN^v{ES22zEUQO1tE!n;2kcMjj9a4*x6x_!>FF3)X7kngi1w+2{wl^dHROk2rl$ZK3XBXwWEbo zP<$fBUIufH(QWHeus0@q$#)P|Bqo#bJ6i1CgErf30i_*f*-&3x{DA~{bB-UndVDL3 zwWgNTPQ)!ajR)4j;t#Iu$>j~~s^9rD^k3zz6Ty-}TzO=nzwhun0ANQ%;-275;N4E+dh+rdK8 z00ovQ-7Serw8(z|!acrRB$%p-=VSoaJ*idtg?|+6urjRrU%hLyzK`NR%r(DJGduKB z4bK0%j23hWj*1}cM}hn&&C(8CjYA#k_cvY6y01rhIbQtZY#bFQk+!x6WU~JOwAOt= z1o+3u8k)!5H7zc9P+a}%ixuI^L>TlHbqm%O_z8h3r`L#KJ#U4C=jlS!^j}>voFv}b z?`D$QWEJ4}n{30Ky%TfzwWHUCEkVseTu(F!rD4BBHK1b zWqeKd!$EJ%mh|~_g=mlWs0^c$Z*ysK<{JyIn=}kA#*o%xinPHs(gEl$U%XD6J!1}C z{j5q#0AK&Te_Ni0%TypK%KnF_ZA-BO#c`AF=PQ&?hZmTtNI#r#WTY{KUj$~D%}nmcXm?G0Vr+Kip`J7hnUF$81=qyDBXwS(C_1(T zAVdma?t^thUGD8O-fgnmae+mqE`!kGdR+7+DeG1J;UcsL-Nv$&B7fEX&n7TEb4}3j z0kQHy^X?B}ppUY1y-9IU%u_uer7-NI(S3_1eY!969a4wzb+N!01j-Jm(Ws09ut&P2=>n7hL0>JE)X2^}RAO%}8S?MT-68f}@D zE7Es|a1%kMX$@)rWxsal*S;ph?;AIuu&L{P+Fqo4GmSflLBq3s2i*{_!-nqUw6sTV zkVM~;p7Gf2taW|nm+U7i1?Qc6=3xy5pcUof0uQGKYvb+%!*pxf5;5H{l*xZWu9@=@ z)}=$QvDc-Y_X@uZD6`LGiD!B5ALBY*9P87!c+v4}i|?rW9arlQrKEZ$a(!ENFhNaM zwHrT2*oTn!Q40FS@dK$>#sMmUHQleL=!+Cc5@F((UK^lsoQHe)lSAAG+rVw(6}-KR z_oY`mmCQO6dm9>w8P{6p(UlomHnL~cMK`KUG?Y-1{1nk*iT6HeReLGbti80i>d8KU zq<`8pc8*~GIXzW{hR-mR0JxbvoWvP%Cjf4!xjiY1L}ydNvhdgEwCkX+{g3&1qu8+Y z?n(AzqQ4He+p8Qc!oh6f9gXVz<3|r!4Z~?3SV7=Q^pMDHTypUT4P@&j$Z0Teu;X6H za4D@*0g4Xdq*kZOub zhjM5v&E_PZa8txq=}|{+`0d{=l$+|ZQ-`q#N7|$#k@BAnNGSGt(F%A*g>lI$C)|Z` zEqUG;A9GeP*OHI-J@1SR8sjLHjC5jz+AU#fU8)T0v=9B){EsM^{{hw%J?Cx=DQI9- zk24h)dR~KK9%e-t%uM8fK?GV-Am{N?MiH$e`E{K+Z6zg;<4b|=b+K`iQ!|QA;UAp= z5tIWtY6~Zzrn#DP7Y^TFLg=QT&27TeOZ}a$iaLBZm69R6mOgGokYKn1d<&Tae^GH6V5%mDChJ$}HvPiq=9k6PBot3flh7|_rW7El&^iIK@?zfUz(CKrp$T7UkxsX0hdu|VU#qCctm+!)4nwV19Wj3xb+ zN}1GpC8W*&w!%X^-@29~i(sXMg=B;3c0BG)#|lPh+n$*2qaWAVYbjAdhRJ)-$l7VC zdhlR-NbU(EikCes=QhLdQwmmPebGbVz@P5%mK}E3AGleW1$BEj6D-y%Hj_B~1B@0n zJ^WQmF0UFi8>dA1x_>50Si%IvOo+5Zn1+NLgt=ARJY&wUv<|Tfa*Oh{T=n3QTTzn( z4S@FFMq2AYjyrU37u|JI>wa~A*S1-=X?9o4?)jHEovxy(kKc^`0r<(bc)5O!?0Ewb zL9X3@qKRdiulTFHU{DAlf&+VV1qxkzt;*n^)^2tOH?sV^j5_x3A}<-RFD)3`C5ZGVz8ZSVZ+;&A>fiCOvWXK&Gb#^e+@HW-h zyq3ph9Whz%T^2ezB*-B*#Hn;miX@h@xq9_oJh`A^%d}$_HZ{VxPFsUW(r9NvkVL09 za_r}A1+LUkM>>_V3`bYF0}0^u!zw?aTt3@0rW`DRgsPUmGRC*2FiLx3XKmA{`Fsf) z30N#-(E0nSttr@R0`1&I`gFKXl)^3gxt##PlQGqSDiJGZu9LRzd<_=}MEEjm!qD_b zva`+`1&MTnv}w^|kt=QC3)1ey-NqGaA!K2HQ+Ebyz?Rnn>8F!6<*F{Cca24D=e_O~ z)i2*d%zlGdb1g2M2%hhBaa|w;e}g}a3Xfeu7Ite|ioPaa^B%ksQcA?%Z+{sguG+Vz z&+qvz7^raktjKw?fJTkhR~frijNIDuZUDJKT1Z6AdJH+Ae((M*yTySEADEA62 zt|F*^++tihsMiv~He)vvR56gCHlpluzYtrkyZqGzzn47E7k_QH?;sKJ)Ha#DqPo{D zH?*o`466ubH~SVN?iD??nAmhK$Q3azgLIsP{>tefyZD*OSfAr7iSpl;FB^OkQ z=*FT+IjTIrBcFGq;#f~LH+^{X4lXCSTun)6pz?&mW9-Xbev-x?75wO59U%yP*CkgS zrAE|1)n)FhF(<1@0iuB&nJ5dD{EUo(-{;HFe}Ja*;Jl4fXzR*CVgkz$zEBX$;3KA# z^d1VWXI+j&n%#JhHbdk~_PYQ!G#4$zehqP4jl;u7;!Q?ttph@T@g3*q-Z%JhAbCrA z6NgyZ#nMFeZDoTX1J<6lndW?Ft2*bhEDIT6)!?FnefZAnavALKQc65@JExFa9IDTX`gAu4EYQT;Zm~+cm3 zy7hOba-`UWeSie;z~8`hE2PDd$zxEdM~9W_yAJNE@gaPnZ_DKV>N`m?@YR!0%1itK&meW8r3F5~Hzq3A`Va zTcz)OX-e%Bn%+FOGp*z;MI5STm<-F_nCRD0-{t-JIl&?a`1{;y;_=a?{h{;SZ#J*z z!t1g~W$qRvR0MH77^77+^^Hn;$@Wygc??)_^NKKV%t_t(<7nrG=y1Rsy}}JC9|ApR zIw!2!mh8R=IZ^4lrfzvCAEbV@hR6U7rjUL*d)=S5btWwk5CdSn%mEh<_7Vt^bhjh%Kwp8z#G29eD}KK5(~4@| zRcBu$7)@F!!YAdAmXKoFU57o?9(Zh(aUe`K#A|`-P;I+ECATxZG2~0 zA9`RLmWq?u(HQx)JT)8N+oSytfSvi}AOWkUw5TS=kJ8M85V(&@WLq-Hb=+aUrJx5? z;)rvJQM~m6;`t9K>V{LJLVKE(w1|$Yd}z1^hlJnIqz3G4cpYaw7w3AVw=z%NYIvI}9%8eq#Hj4mtRDPJpIa^byUhSIYfuJA{<8SY)EzJCY=ir8HD6a52d zfc`YyPjKlC1t@3?V>c`=tIJ$^mV(Ye3mgFhGfE^rZ2QYTQr}yS|8|r|b@Hh1y7(!d zTvw_^AsAGUefMFNpN*^%n25Wz!4_TP6S=p1R6@z9I1p0ogbiFufK{e{iA1RokkcSi z`UjZ8@tHGl?G6#9A0VBt6Zr=yb71aQ3K5goz|p8C*pbqM$w3e>J&#SClfL{O&DQ<3 z8M$JRzEDAhlbNQA6axW#nks{IX27wEbRdl7B>wZD05dYKL0hBEdQj){4qT+egRqoG zti_bF|MvcK7e}7695gByX+yQeD-UK^(+jmC6|Oq@*M^`PUdtPL3o+GN5V=;gjM@+* zeh?`z52`F-!t(HA+}3IT7x2e)*sT^U@S1LQy(74@Ik+q1?HPqE*(pZD{^Tf5V-Q4% z;uLIM1eaMnUC8jgF8n?K> zt(#!L2MKYnDaK=PSZ`5^O#}}TE=Nianlf_hmb4P!bkcEYx|By`=n%z3-6!^>1r<1` z4W;c16YjkVTVVb8v&A%=s%KVrc8^r+nC#D3IPG$`wKA@DuTFPLZ%+xFau!({3)K8; z(>>)-Ngh*D8=uz4B3P{^!TZZzqKe*#Qkl?5>&Da3nPRM(fH-`R&Ld;JIjqWv**cn- zm~*nJDbTf0%O=|J8Olw#e(JbyXLy4e$$h#v;Az#~{e0?$Ym8gKpSYKG%<7cIoSXEh z-#-Ku_kcWcLpm!)@Si%D5ppKyceQpC-52V$%s}9q5zGAK5Ya_@ot*o2gQqj86 zVF)z?Z`{JraWKN1jq&d8v++SnU2SDR(h__$vJ zbrZS+3h?%k9jVQ_S+Y|{OEk=x5LYMHsCZ7T%@9smW*+k|=T5T!4!Rko;Y7M73| zp@j~jr1_E&LzI!|fxCUV#R7Xc6rVW(6(K3gi-H`VVFoUTRY!5LC76;gk?0Y1kh*Ft z3ut8?)WzF9ZcCFiMsS&YnigY!YHOs*GBz@x&hX+}x6Vc`-J4z$0>@!;r_!SDTADDP z^MYv{$>nhF>Pd`Vz1EHu3iFo9N8`aifN@g_LDn}iqU>=OKFSrtfm5;$$gVB}^Qdq| zy%xsvjB`qUn%>oyvJP81U&Z`o4hfpWN{hZn+Cs|?qpiiE4<}c;?+CR8jw%cuigJqM zb!Uf))3+Gc-Mwr2YT~)EnR8GqYDw%6T`9}^9rDbd zbn0i^w#pXDQ!q91A#U^M?&Rbn7tLJv-b$EChy?sXc@(;A&+{69W#X?8rU$BCP`DQT z$|D5ze$i7j*YmE4tccZ_n3+>FH#hd|2lvChD6iMTwCd;U5^BTs4LD0*F(9|}#!d<@ zFO4Ii+r>Y?2kv*TkX5E*M}of5R6N)kjO#~T1tWXD-|$1+j{}E|^m0j0WY8 z&uo!=ht*)#w=8K_4vw)}i%1V`Hf7!Sx$amGq>>wJavedch*CD9#$(e?4$GV`Fl1}G z3;WV!D~)SL4xkKUhqiIby=mqE)6Mq@Cn1TS%|>ZWJ9LJ09fy}mB)6l+)e|YH= z1tJadGurj@cV5oC9@7@tQaqX9UU7a5GgMS3KVl*{)Pa#-hFaD1uTxX~!eJJ9hh4(h z6(duQr-q+fuE^uqY9@*5T$(DMa?a+`GXb^AXurJJBZZ4Ia5ux7iuUB`O`U(s9$C$s zTcoMq94X$R-p&B3(Qa0}sKR3d@ytSzN-P{#3Hc~{Gg6rR%k6WP`5b+Ln$0r1TPDrn zVB-<=h2xJ&BjuNT-R0I=ggHGK>&?r<<_n971D81rflZM2N|REwJwK+>Rg%{nW7|hm zlYJ5E#wY&bOd}>2&QfU1Asn1Iz=%k>jgEyR=CiawEo4}cuH>N&djpUS0)Y%a??{a4W zD~Xz)TKt>aHFxWRk~4UCj{G~mC{maht`3-$Fhj=_KTrcTWubAWq=2VgxEf)Nn!=vf zXi=U2y29Jq>}~Z|&n1Nz_bH0p=VdJOMjPgtsD;%OxZdS>W3@GVTqUOwA22 z-GSY;wS`KK?oI(X6mO3|K-x!hkTH4cvfquI{+{ZuV%0t}btYR%V^Y% zHOO~r1vOt=I`@Yjl>~~zP%`H`PK38u-<-dOri=WVssO!0WUZaB;XleETv9NU^GRdu4`@14+gx98uK*+gaV&ih9 z!>=90r`}3^;3=81zkiK${hiahci>2H-R89L7n+uyquuS;&!R|)T-u&g8QdtyOkc?i z0ykT7cW3$36|98k;z&d(MtNYQkH1HgSoNT+OYly9MnKQJEweLYh3anlYUrpBZ0+>& zob|G!e3IFn$RH?t{=o8s*zTY@$>uy5EoR^^AcKd!fM&tcN{j2Qe70s{25@Gvz5okmzTs9H^@V^ctmWBR>GO+IuKjK|k?^>w;6 z5NPWb*6vlX^{KXg|7_+4X=4nyBJ50mcaU;yh}vKmRO1h7g#^TQ(fvM||^z-mzoVQE0MY4e;W%CRAlnh~>&9Yum8(X%~8t zn)P{01w~vTi>W-EQg%7STlzTGnh*?w7)4kzC9fPfFv-i-n_Ow>{0@Rdn?Z(YW+f!S0FKAO&ia|H-o@*Z?6Kx_YvM$wuQ{&FboseW;Q@kGe4Ukm4y{afinM}G%=iBP#(&t{7M zzN*O~YOp;&;X-NijMU+ZhrMu%iVQ{QSX9?bMB8DZ==5Um!JH(_C5coaC}61cCPiS$&@;|G(8 zP&4kdb`tg~w5N{!o|QpQ>Q&%V@LEoe?~%(xrw#0KPj~lkSPr@EQ-e)C^c@4DJJI-P zh=EenWV@vH@)no$1|Cd@kapzP%eb9*qJ_1MKk2hFIVZrz!jBG-k1i4S&R0An!wGFUv3_6n&g{ zDuOtqe8&hXF*)T)Lx4+d+7mnXi$0P66}GhK^%sHAK=n{#_+n&52dNnd)dutERVl2s z?~Vn8T7HxW!5RkdvsU<=Hs>Ej49H*cvHhOB-N)(guE&!1?NW^-4CfABa&8WS6Zi+d zh1R_3s5bNCJFM4E%kG9q7=uPRx>Oc9K5dh8DCwP^WFA{G;Ifd;{LMIv9`S6<(Oj!a z-!763{^w40R>;Ze`qi7@;iX5@#Y;P24?k241F>)^7BAY{L`Ql1F2RiK)G8Na3MqOa zg(VD>!ZWfAB`k;vH6#L`VcvU}nsuyrUa)=v+KA8zP~v7_qES&K{0Yk&a{S>H;uSYB zb@e+xq!Oj#lR&RpP;?`Sj5At^(Uo%w%FG$8tz@tO<(<|S-`0f&$R%9`XR@bT%Cs|j zG*2I=v8>}pv6FLbnCus)h_TZk%rws}YI0N2IT-n=xt%OEoJcKH-8(*ebcn`*>w=ZT zz`K;%K%N-Tldllhv1v%yn?yb*f6dyCZlbN7mn=SIq@Zf>_N$?G2>m*pWP|KRB#Opc z{_v=J_ffy0o8Ev|-Q2=&Ss8z{XBY7v*s%RY-Yb&zDQ+`PN(ZNn!*9$!2R~1lSB@NB zOLqn^?xgm6$4X#y%<&n0!xc5;bW~@q_bgo+`b@MerNaBRP+Te(rsWwTkW}jIA*aM> z)0+PPk4l4sx|&*X)t2gn5s-ZehUmYK+QsX-j^RzA^-H3RpWEJw-B~QzcPu;+Ti^WX zgSi$<0^%aF?ij9{6RH>5CV_HBMzke+yZa4+pwdv^_5mse&{p%gyZ#l zeL11E3u^4Rt3Vb=gH;Z@@ezccP>;UU%yHrTxHc`C{xE!(tN&M3$->a{OREc|a|-_S zOt^%{QR;qvP48(Zs9BH&?m7qz_6eGoR%ybNhlc!tJpotI#}#}bL0ViO?fSj51RIy% ziqgsL;bV{K-{AO^psnRTs(B3IUHv>$Mo&!b-8nCNW9VlhT8wqzzg0c>OW8WAU5_zf z(T|AHg4*$?b20xH0@e(@A1-8S+B4C!BnT)nl$E6z_R8JU&sulp=p%##NQZ93DTPsj zLmTR2%zSB*6-#2dWJ!FR171yBnI7R2mE}pp(|mgt#UY2=)w3G5jGytV7{MnH?d>}1lcPhUp1mA_Nc=2$EIr`w6Nt>DAI;3rWrs%-(9TOo$sJhe5`J%|ANmoip^_1Yl+#rQA_-3OP;fDukoOD$38s#z7j3 zo=Qtg8gm-GC1CAQEG~~|t<`pn`MrA2|H{q{PFScrP{9l$%;CT~=YIf#`Y8CbF^?C$ z2WYcHfZOF;@g+mtEwVKJNtK7jUbAoOOIf=K3)M7$u-1jxTvIphp-`o}Rv?^wQ)-jbhFQpw zkJ>y+eAlVyBwa14u@_5v4e{$Gz|H8|8uDIOShi0`<%afsPJK3dJ`-Lcl0A3$HeR!m zY;flqlswze6RII~xY}keD4O#5?K6Dq{p7XoQy<+6>O>}HUcUeV?4g6);*o}(Jsl|q z=E{V$0sqvUl2$DVpW=(MH&=p%qdp%U=gX#+8V1Ic3^}AEh$vhxAXT2_fga{3XKFU> z88Opqp*;lnDK zfEo2ZYnt!7ymz2XyUE1ZRgbc`%Un&UKeUS)t~@Q6Axf%2A}GyG?%lag`~zd=SR z$xchC_Mudt0_vhbf=j`49ZYmJBbBF-vkX!nV3doN?k^XY`?~h_c|^o8vz_PT^am|@ zSYN9?f^^W~H3Bt)rb&PxP=yDzMavjkG#Ft&jXg6N#_*IVIMYd;7F~$ zI(j;O7CV8A$5hMcKUo3plf9g~qoH#2tAo7*or;^*)~12>$G9&QGhL)cgp?ZAT3^f? z2meGmh?g20V02M3^CA)Wu6B^UX`|T_*mVjuNADpGI)~Rjc1YBhq|M z{O-SYkI((Sb2g-E(fztl(a2K*e*^=T3u6A0-f-uP)6X*ETkDxg+L9uLo3RAM3a1oB-3i3SSO@!-tgad6Mrm4aXzr zVcd6@E9)yttUK2w{BVd+hh5+HHfJmntX~XQa%rHvh8V{wzH5_9u9fxd%M4Vsi&?sp9l}L$Z>YjVc^W3NT?^F|LLQ-iHNE>c*g3pos z)3y%Z;Fcn^bL|tQ+5q=oXq0{Ay_#avCR@1?G}YHXYF=BRSWHeaKHQOD_|W+S z0qRYb%7t-wA{h5dgZPZML(DG}|FOoRGLql6XiPY(H@w7-z7Sy-S4l$GhOjjw z1US6JxRAPKq?|%cy>6HxpoFVegEQ@LH+RXYRNCYzUxaMu^PZbNYhB|nMAY>t zbv009L1bYuz1!Zen>wKmr^Vsf`!@dQ4o>DfPm+H11;wOi(ILOKfLCgg&XL)YUap8^ z5CC8CIQsmlL`hLQiLFUSC0S5qv6Cv%IT%mWF^X*tcZ1xYyyz=HZH6~zq zgfr39-;a{NPc0Y>U_LsjP8c@ab2}<_6A@_BGKiyzW6d(R^m@Q-oF*l<-mQ%w(23Y* z!cyO>&(0TvpG@q`JU_`);!?uidL+Kn=)0G`!({<=wa9#V_O*NT@KTSJbhBnXI<*>S z{g`t*qaYC2(4IY?+wp4u_0oj=O@X}{Qm{?$$B4E7o~_K((jl873whfDo(gOMAgZ^ z!AN5GCCrSyjnCLf-~)fHIwp~c1;hNo5+bHTKsqsP@hpX-gKwtjEnA%aU%<@|E6h@Z zWP#Km6hRkKWM#NDF)MhW46NBu<*>l(gtTc8*i(>4Oby`ajLE*yP$gc)>kpq`(7-M8 zqk7v}zTkofIWX(YcN=ip^q&R>S@im7R#16ECLbr98skFeKjL{uB3p@|^OTNP0S_l> z5W9s70W_HFfmgEXFGbuKEy z&L-RY*SX$kRPRh_(yR?Ie198~W2}z-fos|^awlMp=R0atp0khW7oIcBb|Omn7enXx z3MCvQ+jf>Etl)nDOT8Ehxg)PngE{zo1y$tR21TpenOnLpwM&R&hMt5KL|5vOqB21%gb|iGFVrz!bN;o8QF{GRZcqwthsp5t7|wuE5~Lhn z+oD+*eHXFZI^iE+$d9==WREupP4-QZJ3C2wVbX`>XEoDS*&8i(I?U3N=IDVP+&4Wn z&SWQ;4pA#lXNSn}fgwK%w+SbQ^Knupv^6zoFaNK;!FMe!`H`a#8J~uKfV7%jqm|ZP zpf_EJOw6yHK$+WM9xexD?a)dUC6!SwzF!It2@k+E`!kP)U@{d9nHi|XWRdjr$M z`=rmIok98n$K7ZbxxMeC{Nwv+-%3`@JPk26?J*4pvqacXH!+r2l%kt77|WvLO_0q6 zrQ(+eX>ZZzkUWeodt+Uv@Gz2lwye20B4u;am$PL40}b>glZ;c?nJOqYdw?(4BQYl z{hRBhK6?v=ugpe{t?SQoF5(*RN&MpV`@MIWgRK4EQ`v#`pf5+=8s&?}|EbF1Zz*Do z{Fxl6xm!8zLYuyJ?`ICowF8j0oi*RTS>|UpngPANFnRefodKm5YwMa4sY_0 z^@&lF(gnEalUo^dv!Sd>cTWeV=zT*endo5|97Qfn!NemJ8E|{u0i$qC^wx2AQX=A4 zQdA54fR^}NO7k-4HRVf&+E5n2?ksg>30+btQ~SRbTEn^EV@J#|mgBNH*`B(z)p0`G zpX}5>+84{L@VG@+-1g?y%SKH%3^Z>hFcATmyCO^IqEMjf1XGi=frG1HlDD%gGE9)I z9JNUp+?<~oL~1hHjfVo%DH>Yf?T9PQylukr@V+}GHd1M&IpBS_Z%?B1o$TNCT=TX(G1Z(oO+atD|kWIo{nz$Zq1ch?rwxj5(uyJ{%cSfs1IwHXxdFqG_ z$0T>KJ9R~=)f$0qd^)3GxUl_@AF5kMS2f3_&`{!U&0hVyIw-?Ya8by6$z3N>Q34xu zWv}4wIH}?Jg*91p)DLjGN!6ZVcB*Vf8raz-KfI1c=4-~pFEgEe7e(y9i#>+1_%&pp zK{31zdC`vP=vHbvtk6i}=Mi8zm6oo@X+#pzM<29FYjQijm~$(*SA8s*t)2}I6_Rm*LLeG>a_H@Ju4Uwa?^dylsT(0 zIy%=rqKmw5GZ{*siKt+N=6sPGTPvAl&~?}y6s$h!j0dfjR}kUIY2TU#nE8jsbShPM z6jD?(BWgh|>#`~;*(8U&6_Iy2w(m7ktC2P*TJF#%SsMVJ~GGHy6O`C>}gBL|Q$0DYhq6rkuNJWk-^vjnPXaPZoO|TH)t(n=O}V z*df9Ax_7a*g0J5&^10n(3Izl-L^@i?&?ha?Wj=|(fkicyz@=H%Mop22A`(~%0j;&Q zDM(vFR0ZMusB^L#qP?uBJAIJkh`Pvbg4+gfx}E(dk zcF^4dK`7hs?vi&3Ms$4lMjq(g*EhoEkU}DW7AlqK4jHVA5aIf2xa4)-!JJ>_8>?LJ z2G=g-s^#6BaU~udPgO%-E1DeO_^dClHPXzjeK&gyZg6n84m++^We-auGJs=iiRu+) zMLZ5|pB7+Fx+84$UeTIJnAY`8)zN!%S>Dvj)>_$P(PoHp;Fuh-``F-R^}DU2$bowoC_)7+m4)o0TU*dF{sPywash7SIjSvz>HEWMB)DmuCG`1%7A)=9B={6@ezm03*o} zcm*rM8>&WDynx+2)Ibge6W?Z?Q>}@ka8nfkH%*mOW8u&wpNTKzO}lld7T#&xg%h25=#EQ& zPs+CzHdzEOZh;kr?v-nK1Y3itK%jIwA%D>#+Su-u%i+~%U^`yHjv>P^ViB~NXrFgj ze5ZfxxKWNnrbqpktZFG@t)rHjVR)utYpyrC^VMw}*hVE)hs~Hi7nYAP$e_vyWc-f% zA}w=wVf!sumE6fm5*;pn=rY0I(o#uUcQvvPfrtM93T4z3(hn3u{z;3Ko(G~Ht4D|M zq`1v=kWk3T`?&MCyryWV;ffKNjeGMOld=j%wePrV2|htMDQY(EUOtOUdkZ$ebR3dP z&A0R$p(25}eO=GEdLM>XANzXxtJ3ZC`=OY$>-wVNkYQC! z>!M9VgCTJS!pLE&A!;@cE2r!LI{BvIW=NRZ^0HQ>YpL^511`Va3u}JAM5d4a5fBc( zR#m7r>&V;9Q{s|`*JZuKTCM|{*B7|h0diJuNB6Tm{K_2-w(_z;_>!COq_k_Ij1)$Q zfU-TgAz{^0EqHDyXUD0an3lFzWX?Ggvg%Rdb=Wmqsp?v1jk-1aE;&mvr>lx^E@OCg zy$bC0JViU;52wNmqaZqqhc13gh9<$pn0-X-Yu;JQ$DmgyyOoWVvPMT-z%E^hHxS1i zkS!zL(0Ca9~EO@pi#KViF*5ul-Oy`CU{&K7TrIEsd+3}iGV?x<{m z&2Vk(sfG^fXIx?7#PfM2N~NNyWV!vAGsUf&Wi)c^{z!q$;1N7*G`dq;mce!k1r1Q% z{SlGY$OWv0?WZJ?>s;I<2`#X?AlzCoF?w*_RLu4~nB?&&y3zJEz}~KM7ui16z$3k}g3c zXh${aE(6EEAK_|Q-u(4Zj_Pi?hd-*6Xhw}2nsjd1I)2YK|XFOnGrq+u-}?{uuzcajB*k#lfQBnycN2U}SY7HJ%jrmuh&0A8~a>+Hu>m8H@V7rXEmtS&jf2kCVMo#XDz*ss>JnX2! z8AgiN5{7AD6Tc`8&@ggUdn*~D($+@Jl1GWjX3IDA1AUTib0gnmJdl8N8ZPm{xV^khYF&5c$z6ZnUtGIFUvS-6t45hi)cL!yj{;P9cLiyzHnAWg2ot7huzi<2Jfj zWyWVaR3>kR{SaZy+n_OFxIh~sNg$4h6o5$tgkp>0|Iwv=7me~;06iqfI0bOo;F5**yaXfUBJc>Hc1OEV}-z&f8Rfab7R(M@DXx}Zj z&=ek~w@^l@1bVFB0r3lQ?jMLq@$FRp2}fRqX*nKB1rOKF73_D=BDYP^SPe)PKxbX= z>tv|x4aJEFuc72OHbw=k`K_CUviX6}tmqhHscN2(0#%C=JeZJvy zyK1$)o#L?Jc=XPwov}kCC8W7dzY)-}CQePvH;&dK%Q;hn)f$mD9UJ22S+H?hN%)Hj zmO#jAJI6TaBNtcHQTNo5&gb`Rwt$US4ORu5Q2Z|~HYGtPcs%SDj}3kyUlVHKr3{J0 z7Twzfz9GS>v6_Z=(*cu9g;x&^;#4eaC^4xUYj^{6LCJ;fvIAs8JB7fqPj+vsmunCn z{cMe;w)6EsFd>v5BmkRjx+|nO7M=7?+UDpIN16#iwsHUfPzGq!$vK?jpAGk9>;jGQ zD*>X{{uB^e0B(qUNDWch03zV6B|IjcjwV=>mov0fRFQYpk+YTeSMnB~cL=g-&xNts zIDZ|6tP|W5l6H@otG&Z4buhMAp4Z1GiKKG{d9BuSZJTWuOYF;v32U$#=`WYXOyYTS zYJ;S>@1jsYl@?|T-RB0(`!xog3kM;gn^2U6sSl7Gak9s5N{DK${iU z{{Xf>-DIf&?m_CcoCe?~#co4SvlFk?WNFJpR_3aoaq#~Dx=GY>DBAD%P%Z_>nN7LIBuK-s>iYjbbaB)D4l1S7aDxISor zMZE_^27{VOdkN%~p4ts9$rPs86R*)$;_+qfr5=S?wcu&Yg&P!RdJ^HTQWO-2Z8t*J z2!-=*k7XGEP`;?{HAU2KWuOL^D*J;?5#owQSH{=V$ozymV||qieA452hDJ)mV#9D$ zqtI@`=xO9}rlgJ_Ib}_VTpg9}unBm4=D!T2tPcURMAsE9lpXhsJben_u@*{5w`nI| g2}opa12C|)?R7ZD8bEei`Jg0gv@aIuqgk*2+44ElrT_o{