683 lines
26 KiB
TypeScript
683 lines
26 KiB
TypeScript
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, BLOG_ROOT } 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) => {
|
||
await sendMain(chatId, [
|
||
`👋 <b>欢迎使用写作后台!</b>`,
|
||
``,
|
||
`📝 <b>快速开始:</b>`,
|
||
`├─ /new 标题 - 新建文章`,
|
||
`├─ /edit slug - 编辑文章`,
|
||
`└─ /list - 查看文章列表`,
|
||
``,
|
||
`🔗 <b>友链 & 订阅:</b>`,
|
||
`├─ /links - 友链管理`,
|
||
`└─ /feeds - 订阅管理`,
|
||
``,
|
||
`🌐 <b>访问地址:</b>`,
|
||
`├─ 📊 后台管理:https://post.usj.cc`,
|
||
`└─ 🌍 博客首页:https://usj.cc`,
|
||
``,
|
||
`💡 <b>帮助:</b>输入 /help 查看所有命令`,
|
||
``,
|
||
`🎯 <b>开始写作吧!</b>`,
|
||
].join("\n"));
|
||
await tg("sendMessage", {
|
||
chat_id: chatId,
|
||
text: "点下方按钮打开编辑器:",
|
||
reply_markup: { inline_keyboard: [[{ text: "🌐 打开编辑器", web_app: { url: "https://post.usj.cc" } }]] },
|
||
});
|
||
};
|
||
|
||
|
||
handlers.ip = async (chatId) => {
|
||
await sendMain(chatId, [
|
||
`🌐 <b>访问地址</b>`,
|
||
``,
|
||
`├─ 📊 后台管理:https://post.usj.cc`,
|
||
`└─ 🌍 博客首页:https://usj.cc`,
|
||
].join("\n"));
|
||
};
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
handlers.help = async (chatId) => {
|
||
if (hasSession(chatId)) {
|
||
await sendWrite(chatId, [
|
||
`✍ <b>写作模式</b>`,
|
||
``,
|
||
`📝 <b>编辑命令:</b>`,
|
||
`├─ /publish - 保存并发布`,
|
||
`├─ /draft - 保存草稿`,
|
||
`├─ /deploy - 部署到服务器`,
|
||
`├─ /preview - 预览文章`,
|
||
`└─ /undo - 撤销最后一段`,
|
||
``,
|
||
`⚙️ <b>设置命令:</b>`,
|
||
`├─ /title 标题 - 设置标题`,
|
||
`├─ /categories 分类 - 设置分类`,
|
||
`├─ /tags 标签 - 设置标签`,
|
||
`└─ /author 作者 - 设置作者`,
|
||
``,
|
||
`💡 <b>提示:</b>`,
|
||
`• 直接发送文字追加到正文`,
|
||
`• 发送图片添加到文章`,
|
||
`• 发送语音自动转文字`,
|
||
`• 输入 /cancel 取消写作`,
|
||
].join("\n"));
|
||
} else {
|
||
await sendMain(chatId, [
|
||
`🤖 <b>WriteBot 命令指南</b>`,
|
||
``,
|
||
`📝 <b>文章管理:</b>`,
|
||
`├─ /new 标题 - 新建文章`,
|
||
`├─ /edit slug - 编辑文章`,
|
||
`├─ /delete slug - 删除文章`,
|
||
`└─ /list - 文章列表`,
|
||
``,
|
||
`🔗 <b>友链管理:</b>`,
|
||
`├─ /links - 查看友链列表`,
|
||
`├─ /link_add url 名称 - 添加友链`,
|
||
`├─ /link_edit url - 编辑友链`,
|
||
`├─ /link_del url - 删除友链`,
|
||
`└─ /link_toggle url - 显示/隐藏`,
|
||
``,
|
||
`📡 <b>订阅管理:</b>`,
|
||
`├─ /feeds - 查看订阅列表`,
|
||
`├─ /feed_add url - 添加订阅`,
|
||
`├─ /feed_edit url - 编辑订阅`,
|
||
`├─ /feed_del url - 删除订阅`,
|
||
`└─ /feed_health - 健康检查`,
|
||
``,
|
||
`📰 <b>其他功能:</b>`,
|
||
`├─ /read - 友链最新文章`,
|
||
`├─ /stats - 网站统计数据`,
|
||
`└─ /ip - 访问地址`,
|
||
``,
|
||
`🌐 <b>快速访问:</b>`,
|
||
`├─ 📊 后台:https://post.usj.cc`,
|
||
`└─ 🌍 博客:https://usj.cc`,
|
||
].join("\n"));
|
||
}
|
||
};
|
||
|
||
// ---- 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.handleLinkEditReply = async (chatId, newName) => {
|
||
const s = getSession(chatId);
|
||
const oldUrl = s.originalSlug;
|
||
if (!oldUrl) { await sendMain(chatId, "⚠️ 会话已过期,请重新执行 /link_edit"); return; }
|
||
|
||
try {
|
||
// 如果用户输入了新名称,更新友链
|
||
if (newName && newName.trim()) {
|
||
await updateLink(oldUrl, { name: newName.trim() });
|
||
await sendMain(chatId, "✅ 友链已更新:" + newName.trim());
|
||
} else {
|
||
await sendMain(chatId, "ℹ️ 未做修改");
|
||
}
|
||
// 清除保存的 URL
|
||
s.originalSlug = undefined;
|
||
invalidateByPrefix("links");
|
||
} catch (e) {
|
||
await sendMain(chatId, "⚠️ 更新失败,请稍后再试");
|
||
}
|
||
};
|
||
|
||
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.handleFeedEditReply = async (chatId, newText) => {
|
||
const s = getSession(chatId);
|
||
const oldUrl = s.originalSlug;
|
||
if (!oldUrl) { await sendMain(chatId, "⚠️ 会话已过期,请重新执行 /feed_edit"); return; }
|
||
|
||
try {
|
||
if (newText && newText.trim()) {
|
||
const parts = newText.trim().split(/\s+/);
|
||
const newUrl = parts[0] || oldUrl;
|
||
const feedTitle = parts.slice(1).join(" ") || "";
|
||
|
||
await updateFeed(oldUrl, newUrl, feedTitle);
|
||
await sendMain(chatId, "✅ 订阅源已更新:" + (feedTitle || newUrl));
|
||
} else {
|
||
await sendMain(chatId, "ℹ️ 未做修改");
|
||
}
|
||
s.originalSlug = undefined;
|
||
invalidateByPrefix("feeds");
|
||
} catch (e) {
|
||
await sendMain(chatId, "⚠️ 更新失败,请稍后再试");
|
||
}
|
||
};
|
||
|
||
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) {
|
||
lines.push("🔴 " + d.url + " — " + (d.error || String(d.status)));
|
||
}
|
||
}
|
||
await sendMain(chatId, lines.join("\n"));
|
||
} catch (e) { await sendMain(chatId, "⚠️ API 暂时不可用,请稍后再试"); }
|
||
};
|
||
|
||
// ---- Read ----
|
||
|
||
handlers.read = async (chatId) => {
|
||
try {
|
||
await sendMessage(chatId, "📰 正在获取友链最新文章...");
|
||
const articles = await getArticles(10);
|
||
if (articles.length === 0) { await sendMain(chatId, "暂时没有新文章。"); return; }
|
||
const lines = ["📰 <b>友链最新文章</b>\n"];
|
||
for (const a of articles) {
|
||
const date = new Date(a.pubDate).toLocaleDateString("zh-CN", { timeZone: "Asia/Shanghai" });
|
||
lines.push("• <a href=\"" + a.link + "\">" + a.title + "</a>");
|
||
lines.push(" " + (a.feedName || a.siteUrl) + " · " + date);
|
||
}
|
||
await tg("sendMessage", {
|
||
chat_id: chatId, text: lines.join("\n"), parse_mode: "HTML", disable_web_page_preview: true,
|
||
});
|
||
} catch (e) { await sendMain(chatId, "⚠️ 获取失败,请稍后再试"); }
|
||
};
|
||
|
||
// ---- Stats ----
|
||
|
||
handlers.stats = async (chatId) => {
|
||
try {
|
||
const posts = await listPosts({ page: 1, perPage: 10000 });
|
||
const lines = ["📊 <b>网站数据</b>\n"];
|
||
lines.push("📝 文章总数:" + posts.total + " 篇");
|
||
lines.push("🌐 访问地址:https://post.usj.cc");
|
||
await sendMain(chatId, lines.join("\n"));
|
||
} catch (e) { await sendMain(chatId, "⚠️ 获取失败,请稍后再试"); }
|
||
};
|
||
|
||
export default handlers;
|