Files
blog/write-server/src/lib/bot/handlers.ts
T
2026-06-21 20:42:44 +08:00

657 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getPost, listPosts } from "@/lib/posts";
import { getLinks, addLink, updateLink, deleteLink, getFeeds, addFeed, updateFeed, deleteFeed, getFeedsHealth, getArticles } from "@/lib/rssapi";
import { moveToRecycle } from "@/lib/recycle";
import { deployQueue } from "@/lib/git";
import { hugo } from "@/lib/hugo";
import { getLanIP, DEFAULT_AUTHOR } from "@/lib/config";
import { sendMessage, tg } from "./tg";
import { getSession, hasSession, clearSession, saveSession, undoLastChunk, initSessionDir } from "./sessions";
import { makeSlug, sessionSummary, truncateText } from "./helpers";
import { getCached, setCache, invalidateByPrefix } from "./cache";
type Handler = (chatId: number, arg?: string) => Promise<void>;
const handlers: Record<string, Handler> = {};
// ---- Cached API wrappers ----
const CACHE_TTL_LINKS = 5 * 60 * 1000;
const CACHE_TTL_FEEDS = 5 * 60 * 1000;
const CACHE_TTL_ARTICLES = 3 * 60 * 1000;
const CACHE_TTL_HEALTH = 10 * 60 * 1000;
async function cachedLinks(all = true) {
const key = "links_" + (all ? "all" : "visible");
const hit = getCached<ReturnType<typeof getLinks> extends Promise<infer R> ? R : never>(key);
if (hit) return hit;
const data = await getLinks(all);
setCache(key, data, CACHE_TTL_LINKS);
return data;
}
async function cachedFeeds() {
const key = "feeds";
const hit = getCached<ReturnType<typeof getFeeds> extends Promise<infer R> ? R : never>(key);
if (hit) return hit;
const data = await getFeeds();
setCache(key, data, CACHE_TTL_FEEDS);
return data;
}
async function cachedArticles(limit: number) {
const key = "articles_" + limit;
const hit = getCached<ReturnType<typeof getArticles> extends Promise<infer R> ? R : never>(key);
if (hit) return hit;
const data = await getArticles(limit);
setCache(key, data, CACHE_TTL_ARTICLES);
return data;
}
async function cachedHealth() {
const key = "health";
const hit = getCached<ReturnType<typeof getFeedsHealth> extends Promise<infer R> ? R : never>(key);
if (hit) return hit;
const data = await getFeedsHealth();
setCache(key, data, CACHE_TTL_HEALTH);
return data;
}
// ---- 键盘 ----
function mainKeyboard() {
return {
keyboard: [
[{ text: "/new" }, { text: "/list" }],
[{ text: "/links" }, { text: "/feeds" }],
[{ text: "/stats" }, { text: "/help" }],
],
resize_keyboard: true,
one_time_keyboard: false,
};
}
function writingKeyboard() {
return {
keyboard: [
[{ text: "/preview" }, { text: "/undo" }],
[{ text: "/publish" }, { text: "/deploy" }],
[{ text: "/draft" }, { text: "/cancel" }],
[{ text: "/title" }, { text: "/categories" }],
],
resize_keyboard: true,
one_time_keyboard: false,
};
}
async function sendMain(chatId: number, text: string) {
await sendMessage(chatId, text, { reply_markup: mainKeyboard() });
}
async function sendWrite(chatId: number, text: string) {
await sendMessage(chatId, text, { reply_markup: writingKeyboard() });
}
// ---- System ----
handlers.start = async (chatId) => {
const ok = await hugo.ensureRunning();
const ip = getLanIP();
await sendMain(chatId, "✅ <b>已启动</b>\n\n📱 手机: http://" + ip + ":8016/\n💻 电脑: http://localhost:8016/");
await tg("sendMessage", {
chat_id: chatId,
text: "点下方按钮打开编辑器:",
reply_markup: { inline_keyboard: [[{ text: "🌐 打开编辑器", web_app: { url: "http://" + ip + ":8016/" } }]] },
});
};
handlers.stop = async (chatId) => { hugo.stop(); await sendMain(chatId, "⚡ Hugo 已停止。"); };
handlers.status = async (chatId) => {
const s = hugo.status();
const status = s.running ? "✅ 运行中" : s.starting ? "⏳ 启动中..." : "⏸ 已停止";
const lines = ["<b>Hugo:</b> " + status];
if (s.running) { const ip = getLanIP(); lines.push("📱 http://" + ip + ":8016", "💻 http://localhost:8016"); }
await sendMain(chatId, lines.join("\n"));
};
handlers.ip = async (chatId) => { await sendMain(chatId, "Phone: http://" + getLanIP() + ":8016"); };
handlers.shutdown = async (chatId) => {
await tg("sendMessage", {
chat_id: chatId,
text: "⚠️ 确认关机?\n\n电脑将在确认后 10 秒关机。",
parse_mode: "HTML",
reply_markup: {
inline_keyboard: [[
{ text: "✅ 确认关机", callback_data: "confirm_shutdown" },
{ text: "❌ 取消", callback_data: "cancel_shutdown" },
]],
},
});
};
handlers.confirm_shutdown = async (chatId) => {
await sendMessage(chatId, "⏻ 电脑将在 10 秒后关机...");
const { exec } = await import("child_process");
exec("shutdown /s /t 10", (err) => {
if (err) console.error("[shutdown]", err.message);
});
};
handlers.cancel_shutdown = async (chatId) => {
const { exec } = await import("child_process");
exec("shutdown /a", () => {});
await sendMain(chatId, "已取消关机。");
};
handlers.reboot = async (chatId) => {
await tg("sendMessage", {
chat_id: chatId,
text: "⚠️ 确认重启?\n\n电脑将在确认后 10 秒重启。",
parse_mode: "HTML",
reply_markup: {
inline_keyboard: [[
{ text: "✅ 确认重启", callback_data: "confirm_reboot" },
{ text: "❌ 取消", callback_data: "cancel_reboot" },
]],
},
});
};
handlers.confirm_reboot = async (chatId) => {
await sendMessage(chatId, "🔄 电脑将在 10 秒后重启...");
const { exec } = await import("child_process");
exec("shutdown /r /t 10", (err) => {
if (err) console.error("[reboot]", err.message);
});
};
handlers.cancel_reboot = async (chatId) => {
const { exec } = await import("child_process");
exec("shutdown /a", () => {});
await sendMain(chatId, "已取消重启。");
};
handlers.help = async (chatId) => {
if (hasSession(chatId)) {
await sendWrite(chatId,
"✍ <b>写作中</b>\n\n" +
"/publish 保存 /draft 草稿 /deploy 部署\n" +
"/preview 预览 /undo 撤销 /cancel 取消\n" +
"/title 标题 /categories 分类 /tags 标签\n\n" +
"直接发消息追加正文,发照片追加图片"
);
} else {
await sendMain(chatId,
"🤖 <b>WriteBot 命令</b>\n\n" +
"📝 <b>文章</b>\n" +
"/new 标题 — 新建\n" +
"/edit slug — 编辑\n" +
"/delete slug — 删除\n" +
"/list — 文章列表\n\n" +
"🔗 <b>友链</b>\n" +
"/links — 列表\n" +
"/link_add url 名称 — 添加\n" +
"/link_edit url — 编辑\n" +
"/link_del url — 删除\n" +
"/link_toggle url — 显示/隐藏\n\n" +
"📡 <b>订阅</b>\n" +
"/feeds — 列表\n" +
"/feed_add url — 添加\n" +
"/feed_edit url — 编辑\n" +
"/feed_del url — 删除\n" +
"/feed_health — 健康检查\n\n" +
"📰 /read — 友链最新文章\n" +
"📊 /stats — 网站数据\n\n" +
"⏻ /shutdown — 关机 🔄 /reboot — 重启"
);
}
};
// ---- Articles: New ----
handlers.new = async (chatId, title) => {
if (title) {
const s = getSession(chatId);
s.title = title;
s.slug = makeSlug(title);
s.content = "";
s.categories = [];
s.tags = [];
s.draft = true;
s.author = DEFAULT_AUTHOR;
s.chunks = [];
initSessionDir(chatId, title);
await tg("sendMessage", {
chat_id: chatId,
text: "📝 开始写:<b>" + title + "</b>\n\n📂 设置分类(可选)",
parse_mode: "HTML",
reply_markup: {
inline_keyboard: [[
{ text: "跳过", callback_data: "skip_cat" },
{ text: "设置分类", callback_data: "skip_cat" },
]],
},
});
await tg("sendMessage", {
chat_id: chatId,
text: "请输入分类(逗号分隔,或发「跳过」):",
reply_markup: { force_reply: true, input_field_placeholder: "技术、生活" },
});
} else {
await tg("sendMessage", { chat_id: chatId, text: "📝 请输入文章标题:", reply_markup: { force_reply: true, input_field_placeholder: "在这里输入标题..." } });
}
};
handlers.skip_categories = async (chatId) => {
const s = getSession(chatId);
if (!s.title) return;
s.categories = [];
await tg("sendMessage", {
chat_id: chatId,
text: "🏷 设置标签(可选)",
reply_markup: {
inline_keyboard: [[
{ text: "跳过", callback_data: "skip_tag" },
{ text: "设置标签", callback_data: "skip_tag" },
]],
},
});
await tg("sendMessage", {
chat_id: chatId,
text: "请输入标签(逗号分隔,或发「跳过」):",
reply_markup: { force_reply: true, input_field_placeholder: "Hugo、博客" },
});
};
handlers.skip_tags = async (chatId) => {
const s = getSession(chatId);
if (!s.title) return;
s.tags = [];
await sendWrite(chatId, "✅ 准备就绪,开始写正文吧!\n\n📝 <b>" + s.title + "</b>\n📂 " + (s.categories.join("、") || "无分类") + "\n🏷 " + (s.tags.join("、") || "无标签"));
};
handlers.categories = async (chatId, cats) => {
const s = getSession(chatId);
s.categories = cats ? cats.split(/[,;",、]+/).map((c) => c.trim()).filter(Boolean) : [];
await tg("sendMessage", {
chat_id: chatId,
text: "📂 分类:" + (s.categories.join("、") || "无") + "\n\n🏷 设置标签(可选)",
parse_mode: "HTML",
reply_markup: {
inline_keyboard: [[
{ text: "跳过", callback_data: "skip_tag" },
{ text: "设置标签", callback_data: "skip_tag" },
]],
},
});
await tg("sendMessage", {
chat_id: chatId,
text: "请输入标签(逗号分隔,或发「跳过」):",
reply_markup: { force_reply: true, input_field_placeholder: "Hugo、博客" },
});
};
handlers.tags = async (chatId, tags) => {
const s = getSession(chatId);
s.tags = tags ? tags.split(/[,;",、]+/).map((t) => t.trim()).filter(Boolean) : [];
await sendWrite(chatId, "✅ 准备就绪,开始写正文吧!\n\n📝 <b>" + s.title + "</b>\n📂 " + (s.categories.join("、") || "无分类") + "\n🏷 " + (s.tags.join("、") || "无标签"));
};
// ---- Articles: Edit (方案 B) ----
handlers.edit = async (chatId, slug) => {
if (!slug) { await sendMain(chatId, "格式:/edit slug\n先用 /list 查看文章。"); return; }
const post = getPost(slug);
if (!post) { await sendMain(chatId, "❌ 未找到该文章"); return; }
const originalContent = post.content || "";
if (originalContent) {
const maxLen = 3800;
for (let i = 0; i < originalContent.length; i += maxLen) {
const chunk = originalContent.slice(i, i + maxLen);
const prefix = originalContent.length > maxLen ? "(" + (Math.floor(i / maxLen) + 1) + "/" + Math.ceil(originalContent.length / maxLen) + ")\n" : "";
await sendMessage(chatId, prefix + chunk);
}
}
const s = getSession(chatId);
s.title = post.frontMatter.title;
s.slug = post.frontMatter.slug;
s.originalSlug = post.frontMatter.slug;
s.content = "";
s.chunks = [];
s.dirPath = post.dirPath;
s.categories = (post.frontMatter.categories as string[]) || [];
s.tags = (post.frontMatter.tags as string[]) || [];
s.author = post.frontMatter.author || DEFAULT_AUTHOR;
s.draft = post.frontMatter.draft ?? true;
await sendWrite(chatId,
"✏ 编辑:<b>" + s.title + "</b>\n\n" +
"原文已显示,现在重新写正文\n" +
"📂 " + (s.categories.join("、") || "无分类") + "\n" +
"🏷 " + (s.tags.join("、") || "无标签") + "\n\n" +
"直接发消息写正文,完成后点「发布」"
);
};
// ---- Articles: Delete (二次确认) ----
handlers.delete = async (chatId, slug) => {
if (!slug) { await sendMain(chatId, "格式:/delete slug\n先用 /list 查看文章。"); return; }
const post = getPost(slug);
if (!post) { await sendMain(chatId, "❌ 未找到该文章"); return; }
await tg("sendMessage", {
chat_id: chatId,
text: "⚠️ 确认删除《" + post.frontMatter.title + "》?\n\n删除后移入回收站,30天内可恢复。",
parse_mode: "HTML",
reply_markup: {
inline_keyboard: [[
{ text: "✅ 确认删除", callback_data: "confirm_del_" + slug },
{ text: "❌ 取消", callback_data: "cancel_del_" + slug },
]],
},
});
};
handlers.confirm_del = async (chatId, slug) => {
if (!slug) return;
const post = getPost(slug);
if (!post) { await sendMain(chatId, "❌ 未找到该文章"); return; }
moveToRecycle(slug, post.dirPath, post.frontMatter.title);
await sendMain(chatId, "✂ 已移入回收站:" + post.frontMatter.title + "\n30天内可恢复。");
};
handlers.cancel_del = async (chatId) => {
await sendMain(chatId, "已取消删除。");
};
// ---- Articles: List ----
handlers.list = async (chatId, pageStr) => {
const page = parseInt(pageStr || "1") || 1;
const perPage = 10;
try {
const { posts, total, totalPages } = listPosts({ page, perPage });
if (total === 0) { await sendMain(chatId, "📖 还没有文章,发 /new 开始写。"); return; }
const lines = ["📖 <b>文章列表</b> 共 " + total + " 篇 · 第 " + page + "/" + totalPages + " 页\n"];
for (const p of posts) {
const draft = p.draft ? " 📝" : "";
lines.push(p.title + draft + "\n <code>" + p.slug + "</code>");
}
const navRow: { text: string; callback_data: string }[] = [];
if (page > 1) navRow.push({ text: "◀ 上一页", callback_data: "list_" + (page - 1) });
if (page < totalPages) navRow.push({ text: "下一页 ▶", callback_data: "list_" + (page + 1) });
await tg("sendMessage", {
chat_id: chatId, text: lines.join("\n"), parse_mode: "HTML", disable_web_page_preview: true,
reply_markup: navRow.length ? { inline_keyboard: [navRow] } : undefined,
});
} catch (e: unknown) { await sendMain(chatId, "❌ " + (e instanceof Error ? e.message : String(e))); }
};
// ---- Articles: View / Title / Author / Preview ----
handlers.view = async (chatId, slug) => {
if (!slug) { await sendMain(chatId, "格式:/view slug"); return; }
const ip = getLanIP();
await tg("sendMessage", {
chat_id: chatId,
text: "🔖 <b>" + slug + "</b>\n\n📱 http://" + ip + ":1313/" + slug + ".html\n💻 http://localhost:1313/" + slug + ".html",
parse_mode: "HTML", disable_web_page_preview: true,
reply_markup: { inline_keyboard: [[{ text: "🌐 浏览器打开", url: "http://localhost:1313/" + slug + ".html" }]] },
});
};
handlers.title = async (chatId, newTitle) => {
if (!newTitle) { await tg("sendMessage", { chat_id: chatId, text: "请输入新标题:", reply_markup: { force_reply: true, input_field_placeholder: "新标题..." } }); return; }
const s = getSession(chatId);
s.title = newTitle;
await sendWrite(chatId, "✏ 标题已更新:<b>" + newTitle + "</b>");
};
handlers.author = async (chatId, author) => {
getSession(chatId).author = author || DEFAULT_AUTHOR;
await sendWrite(chatId, "✍ 作者:" + getSession(chatId).author);
};
handlers.preview = async (chatId) => {
const s = getSession(chatId);
if (!s || !s.title) { await sendMain(chatId, "还没开始写。发 /new 开始。"); return; }
if (!s.content) { await sendWrite(chatId, "正文为空。"); return; }
await sendWrite(chatId, "📝 <b>" + s.title + "</b>\n\n" + truncateText(s.content, 3800));
};
// ---- Writing: Publish / Draft / Deploy / Sync / Undo / Cancel ----
handlers.publish = async (chatId) => {
const s = getSession(chatId);
if (!s || !s.title) { await sendMain(chatId, "还没开始写。发 /new 开始。"); return; }
if (!s.content) { await sendWrite(chatId, "正文为空,先写点内容再发布。"); return; }
try {
const title = s.title;
await saveSession(s, false);
clearSession(chatId);
await sendMain(chatId, "✅ 已保存:<b>" + title + "</b>\n\n发 /deploy 推送到线上");
} catch (e: unknown) { await sendWrite(chatId, "❌ 保存失败:" + (e instanceof Error ? e.message : String(e))); }
};
handlers.draft = async (chatId) => {
const s = getSession(chatId);
if (!s || !s.title) { await sendMain(chatId, "还没开始写。发 /new 开始。"); return; }
if (!s.content) { await sendWrite(chatId, "正文为空,先写点内容再保存。"); return; }
try {
const title = s.title;
await saveSession(s, true);
clearSession(chatId);
await sendMain(chatId, "📄 草稿已保存:<b>" + title + "</b>");
} catch (e: unknown) { await sendWrite(chatId, "❌ 保存失败:" + (e instanceof Error ? e.message : String(e))); }
};
handlers.deploy = async (chatId) => {
await sendMessage(chatId, "⏳ 正在部署...", { reply_markup: { remove_keyboard: true } });
const result = await deployQueue.enqueue("发布文章");
if (result.success) await sendMain(chatId, "🚀 <b>已部署!</b>\n\n🌐 https://usj.cc");
else if (result.conflict) await sendMain(chatId, "⚠️ " + (result.error || "远程冲突"));
else await sendMain(chatId, "❌ 部署失败:" + (result.error || "未知错误"));
};
handlers.sync = async (chatId) => {
await sendMessage(chatId, "⏳ 正在同步远程...");
const result = await deployQueue.sync();
if (result.success) await sendMain(chatId, "✅ 同步完成\n\n" + (result.output || "").slice(0, 500));
else await sendMain(chatId, "❌ " + (result.error || "同步失败"));
};
handlers.undo = async (chatId) => {
if (!hasSession(chatId)) { await sendMain(chatId, "没有活动的草稿。"); return; }
const ok = undoLastChunk(chatId);
if (!ok) { await sendWrite(chatId, "没有可撤销的内容。"); return; }
const s = getSession(chatId);
await sendWrite(chatId, "↩ 已撤销\n\n📝 <b>" + s.title + "</b>\n\n" + (s.content ? truncateText(s.content, 3800) : "(正文为空)"));
};
handlers.cancel = async (chatId) => {
if (!hasSession(chatId)) { await sendMain(chatId, "没有活动的草稿。"); return; }
clearSession(chatId);
await sendMain(chatId, "✂ 草稿已丢弃。");
};
// ---- Links ----
handlers.links = async (chatId, pageStr) => {
const page = parseInt(pageStr || "1") || 1;
const perPage = 10;
try {
const allLinks = await cachedLinks(true);
if (allLinks.length === 0) { await sendMain(chatId, "还没有友链。\n\n添加:/link_add url 名称"); return; }
const totalPages = Math.ceil(allLinks.length / perPage);
const p = Math.min(page, totalPages);
const start = (p - 1) * perPage;
const pageLinks = allLinks.slice(start, start + perPage);
const lines = ["🔗 <b>友链</b> 共 " + allLinks.length + " 个 · 第 " + p + "/" + totalPages + " 页\n"];
for (const l of pageLinks) {
const icon = l.hidden ? "🔒" : "🌐";
lines.push(icon + " " + (l.name || l.url) + "\n " + l.url);
}
lines.push("\n/link_add /link_edit /link_del /link_toggle");
const navRow: { text: string; callback_data: string }[] = [];
if (p > 1) navRow.push({ text: "◀ 上一页", callback_data: "links_" + (p - 1) });
if (p < totalPages) navRow.push({ text: "下一页 ▶", callback_data: "links_" + (p + 1) });
await tg("sendMessage", {
chat_id: chatId, text: lines.join("\n"), parse_mode: "HTML", disable_web_page_preview: true,
reply_markup: navRow.length ? { inline_keyboard: [navRow] } : undefined,
});
} catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
handlers.link_add = async (chatId, arg) => {
if (!arg) { await tg("sendMessage", { chat_id: chatId, text: "请输入友链信息:\n格式:url 名称", reply_markup: { force_reply: true, input_field_placeholder: "url 名称" } }); return; }
const parts = arg.split(/\s+/);
let url = parts[0];
const name = parts.slice(1).join(" ") || url;
if (!url.startsWith("http")) url = "https://" + url;
try { await addLink({ name, url }); invalidateByPrefix("links"); await sendMain(chatId, "✅ 友链已添加:" + name); }
catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
handlers.link_edit = async (chatId, arg) => {
if (!arg) { await sendMain(chatId, "格式:/link_edit url\n用 /links 查看友链。"); return; }
const s = getSession(chatId);
s.originalSlug = arg;
await tg("sendMessage", {
chat_id: chatId,
text: "当前 URL:" + arg + "\n请输入新名称(空则保持不变):",
reply_markup: { force_reply: true, input_field_placeholder: "新名称" },
});
};
handlers.link_del = async (chatId, url) => {
if (!url) { await sendMain(chatId, "格式:/link_del url"); return; }
try { await deleteLink(url); invalidateByPrefix("links"); await sendMain(chatId, "✅ 已删除:" + url); }
catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
handlers.link_toggle = async (chatId, url) => {
if (!url) { await sendMain(chatId, "格式:/link_toggle url"); return; }
try {
const allLinks = await cachedLinks(true);
const link = allLinks.find((l) => l.url === url);
if (!link) { await sendMain(chatId, "❌ 未找到:" + url); return; }
await updateLink(url, { hidden: !link.hidden });
invalidateByPrefix("links");
await sendMain(chatId, (!link.hidden ? "🔒 已隐藏" : "🌐 已显示") + ":" + (link.name || url));
} catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
// ---- Feeds ----
handlers.feeds = async (chatId, pageStr) => {
const page = parseInt(pageStr || "1") || 1;
const perPage = 10;
try {
const allFeeds = await cachedFeeds();
if (allFeeds.length === 0) { await sendMain(chatId, "还没有订阅源。\n\n添加:/feed_add url"); return; }
const totalPages = Math.ceil(allFeeds.length / perPage);
const p = Math.min(page, totalPages);
const start = (p - 1) * perPage;
const pageFeeds = allFeeds.slice(start, start + perPage);
const lines = ["📡 <b>订阅源</b> 共 " + allFeeds.length + " 个 · 第 " + p + "/" + totalPages + " 页\n"];
for (const f of pageFeeds) {
lines.push("📰 " + (f.feedTitle || f.url) + "\n " + f.url);
}
lines.push("\n/feed_add /feed_edit /feed_del /feed_health");
const navRow: { text: string; callback_data: string }[] = [];
if (p > 1) navRow.push({ text: "◀ 上一页", callback_data: "feeds_" + (p - 1) });
if (p < totalPages) navRow.push({ text: "下一页 ▶", callback_data: "feeds_" + (p + 1) });
await tg("sendMessage", {
chat_id: chatId, text: lines.join("\n"), parse_mode: "HTML", disable_web_page_preview: true,
reply_markup: navRow.length ? { inline_keyboard: [navRow] } : undefined,
});
} catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
handlers.feed_add = async (chatId, arg) => {
if (!arg) { await tg("sendMessage", { chat_id: chatId, text: "请输入订阅源 URL:", reply_markup: { force_reply: true, input_field_placeholder: "https://example.com/rss.xml" } }); return; }
const parts = arg.split(/\s+/);
const url = parts[0];
const feedTitle = parts.slice(1).join(" ") || "";
try { await addFeed(url, feedTitle); invalidateByPrefix("feeds"); await sendMain(chatId, "✅ 订阅源已添加:" + url); }
catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
handlers.feed_edit = async (chatId, arg) => {
if (!arg) { await sendMain(chatId, "格式:/feed_edit url\n用 /feeds 查看。"); return; }
const s = getSession(chatId);
s.originalSlug = arg;
await tg("sendMessage", {
chat_id: chatId,
text: "当前 URL:" + arg + "\n请输入新的订阅信息:\n格式:新URL 新名称",
reply_markup: { force_reply: true, input_field_placeholder: "新URL 新名称" },
});
};
handlers.feed_del = async (chatId, url) => {
if (!url) { await sendMain(chatId, "格式:/feed_del url"); return; }
try { await deleteFeed(url); invalidateByPrefix("feeds"); await sendMain(chatId, "✅ 已删除:" + url); }
catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
handlers.feed_health = async (chatId) => {
try {
await sendMessage(chatId, "⏳ 正在检测订阅源...");
const result = await cachedHealth();
const lines = ["📊 <b>订阅源健康报告</b>\n", "✅ 存活:" + result.alive + " ❌ 异常:" + result.dead];
const dead = (result.results || []).filter((r) => !r.ok);
if (dead.length > 0) {
lines.push("\n❌ 异常站点:");
for (const d of dead.slice(0, 10)) {
lines.push("🔴 " + d.url + " — " + (d.error || String(d.status)));
}
}
await sendMain(chatId, lines.join("\n"));
} catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
// ---- Data ----
handlers.stats = async (chatId) => {
try {
const { total } = listPosts({ perPage: 1 });
const allLinks = await cachedLinks(true);
const allFeeds = await cachedFeeds();
const visibleLinks = allLinks.filter((l) => !l.hidden).length;
let extra = "";
try {
const healthRes = await cachedHealth();
extra = "\n\n📰 友链博客:" + healthRes.total + " 个";
} catch (e) { /* ignore */ }
await sendMain(chatId,
"📊 <b>网站数据</b>\n\n" +
"📝 本地文章:" + total + " 篇\n" +
"🔗 友链:" + visibleLinks + " 个\n" +
"📡 订阅源:" + allFeeds.length + " 个" +
extra + "\n\n🌐 usj.cc"
);
} catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
handlers.read = async (chatId, limitStr) => {
const limit = parseInt(limitStr || "10") || 10;
try {
const articles = await cachedArticles(Math.min(limit, 20));
if (articles.length === 0) { await sendMain(chatId, "暂无最新文章。"); return; }
const lines = ["📰 <b>友链最新文章</b>(" + articles.length + " 篇)\n"];
for (const a of articles) {
lines.push((a.feedName || "未知") + " — " + a.title + "\n " + a.link);
}
await tg("sendMessage", {
chat_id: chatId, text: lines.join("\n"), parse_mode: "HTML", disable_web_page_preview: true,
});
} catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
};
export default handlers;