snapshot before merge
This commit is contained in:
1 parent
2b3b2850f9
commit
21fc65b3ae
145 files changed
+9786
-59
No files matched your search
@@ -0,0 +1,125 @@
|
||||
// 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 = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||
<title>WriteBot</title>
|
||||
<style>
|
||||
:root{color-scheme:dark;--bg:#111827;--card:#1f2937;--text:#f3f4f6;--muted:#9ca3af;--accent:#60a5fa;--accent-hover:#93bbfd;--danger:#fca5a5;--success:#86efac;--line:#374151}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:28px;max-width:420px;width:100%}
|
||||
h1{font-size:18px;font-weight:700;margin-bottom:6px}
|
||||
.sub{font-size:13px;color:var(--muted);margin-bottom:20px}
|
||||
.status-row{display:flex;align-items:center;gap:10px;padding:12px 16px;background:rgba(255,255,255,0.03);border-radius:8px;margin-bottom:16px}
|
||||
.dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||||
.dot.running{background:var(--success)}
|
||||
.dot.starting{background:#fbbf24}
|
||||
.dot.stopped{background:var(--muted)}
|
||||
.status-text{font-size:14px;font-weight:500}
|
||||
.status-ip{font-size:12px;color:var(--muted);margin-top:2px}
|
||||
.actions{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.btn{padding:10px 20px;border-radius:8px;font-size:14px;font-weight:600;border:none;cursor:pointer;transition:all 0.15s;color:#fff}
|
||||
.btn-primary{background:var(--accent)}
|
||||
.btn-primary:hover{background:var(--accent-hover)}
|
||||
.btn-primary:disabled{opacity:0.4;cursor:not-allowed}
|
||||
.btn-secondary{background:transparent;border:1px solid var(--line);color:var(--text)}
|
||||
.btn-secondary:hover{background:rgba(255,255,255,0.05)}
|
||||
.btn-danger{background:transparent;border:1px solid var(--danger);color:var(--danger)}
|
||||
.btn-danger:hover{background:rgba(252,165,165,0.1)}
|
||||
.link-row{margin-top:16px;padding:10px 14px;background:rgba(255,255,255,0.03);border-radius:8px;font-size:13px;color:var(--muted);word-break:break-all}
|
||||
.link-row a{color:var(--accent);text-decoration:none}
|
||||
.quick-links{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
|
||||
.ql-btn{padding:6px 14px;border-radius:6px;background:rgba(255,255,255,0.05);border:1px solid var(--line);color:var(--accent);text-decoration:none;font-size:13px;font-weight:500;transition:all 0.15s}
|
||||
.ql-btn:hover{background:rgba(255,255,255,0.1)}
|
||||
.footer{margin-top:20px;font-size:11px;color:var(--muted);text-align:center}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>WriteBot</h1>
|
||||
<div class="sub">Ctrl+D bookmark this page</div>
|
||||
<div class="quick-links">
|
||||
<a href="http://localhost:1313/posts/" class="ql-btn" target="_blank">Posts</a>
|
||||
<a href="http://localhost:8016/" class="ql-btn" target="_blank">Write</a>
|
||||
</div>
|
||||
<div class="sub" id="sub">Loading...</div>
|
||||
<div class="status-row">
|
||||
<div class="dot" id="dot"></div>
|
||||
<div>
|
||||
<div class="status-text" id="statusText">-</div>
|
||||
<div class="status-ip" id="statusIp"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" id="btnStart" onclick="action('start')">Start Write</button>
|
||||
<button class="btn btn-secondary" id="btnOpen" onclick="openWrite()">Open</button>
|
||||
<button class="btn btn-danger" id="btnStop" onclick="action('stop')">Stop</button>
|
||||
</div>
|
||||
<div class="link-row" id="linkRow" style="display:none">
|
||||
Copy: <a href="#" id="lanLink" onclick="copyLink(event)"></a>
|
||||
</div>
|
||||
<div class="footer"><a href="https://usj.cc" style="color:var(--muted)">usj.cc</a></div>
|
||||
</div>
|
||||
<script>
|
||||
var port=8016;
|
||||
var ipCache="";
|
||||
function updateUI(s){var d=document.getElementById("dot"),st=document.getElementById("statusText"),si=document.getElementById("statusIp"),bs=document.getElementById("btnStart"),bo=document.getElementById("btnOpen"),lr=document.getElementById("linkRow"),ll=document.getElementById("lanLink");d.className="dot";if(s.running){d.classList.add("running");st.textContent="Running";if(s.ip)ipCache=s.ip;si.textContent="http://"+ipCache+":"+s.port;bs.disabled=true;bo.disabled=false;lr.style.display="block";ll.href="http://"+ipCache+":"+s.port;ll.textContent="http://"+ipCache+":"+s.port;document.getElementById("sub").textContent="Write is serving at"}else if(s.starting){d.classList.add("starting");st.textContent="Starting...";si.textContent="";bs.disabled=true;bo.disabled=true;lr.style.display="none";document.getElementById("sub").textContent="Please wait..."}else{d.classList.add("stopped");st.textContent="Stopped";si.textContent="";bs.disabled=false;bo.disabled=true;lr.style.display="none";document.getElementById("sub").textContent="Click Start to launch Write"}}
|
||||
async function fetchStatus(){try{var r=await fetch("/status");var s=await r.json();updateUI(s)}catch(e){document.getElementById("sub").textContent="Cannot reach API"}}
|
||||
async function action(a){document.getElementById("sub").textContent="Working...";await fetch("/"+a,{method:"POST"});fetchStatus()}
|
||||
function openWrite(){window.open("http://"+(ipCache||"localhost:"+port))}
|
||||
function copyLink(e){e.preventDefault();navigator.clipboard.writeText(document.getElementById("lanLink").textContent).catch(function(){})}
|
||||
fetchStatus();
|
||||
setInterval(fetchStatus,5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>`.replace("var port=8016", "var port=${config.WRITE_PORT}");
|
||||
|
||||
function createControlServer() {
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url, "http://127.0.0.1:" + config.CONTROL_PORT);
|
||||
const path = url.pathname;
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
if (path === "/" && req.method === "GET") {
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.end(ADMIN_HTML);
|
||||
} else if (path === "/status" && req.method === "GET") {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
const s = getWriteStatus();
|
||||
s.ip = getLanIP();
|
||||
s.port = config.WRITE_PORT;
|
||||
res.end(JSON.stringify(s));
|
||||
} else if (path === "/start" && req.method === "POST") {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify(startWriteServer()));
|
||||
} else if (path === "/stop" && req.method === "POST") {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify({ ok: stopWriteServer() }));
|
||||
} else if (path === "/ip" && req.method === "GET") {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify({ ip: getLanIP(), port: config.WRITE_PORT }));
|
||||
} else if (path === "/health" && req.method === "GET") {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify({ error: "not found" }));
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(config.CONTROL_PORT, "127.0.0.1", () => {
|
||||
console.log("[control] http://127.0.0.1:" + config.CONTROL_PORT);
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
module.exports = { createControlServer };
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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"),
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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 <b>" + (s.title || "\\u672A\\u547D\\u540D") + "</b>",
|
||||
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 };
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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 };
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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 };
|
||||
@@ -0,0 +1,413 @@
|
||||
// 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 <b>Write \\u5DF2\\u542F\\u52A8</b>\\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 <b>WriteBot \\u547D\\u4EE4</b>\\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<b>" + title + "</b>\\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<b>" + s.title + "</b>\\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 <slug>\\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 <slug>\\nFind slugs with /list");
|
||||
const ip = getLanIP();
|
||||
const url = "http://" + ip + ":1313/" + slug + ".html";
|
||||
return sendMessage(chatId, "\\uD83D\\uDCC9 <b>Preview</b>\\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<b>" + newTitle + "</b>");
|
||||
};
|
||||
|
||||
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 <b>" + s.title + "</b>\\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<b>" + s.title + "</b>");
|
||||
} 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 <b>" + s.title + "</b>\\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 <b>\\u6587\\u7AE0\\u5217\\u8868</b>\\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 <b>\\u53CB\\u94FE</b> \\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 <url> <name>\\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 <url>\\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 <url>\\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 <b>\\u8BA2\\u9605\\u6E90</b> \\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 <url>\\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 <url>\\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 <b>\\u7F51\\u7AD9\\u6570\\u636E</b>\\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;
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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 };
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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,
|
||||
};
|
||||
Reference in new issue
Block a user