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\\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
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