Files
blog/write-server/src/lib/bot/handlers.ts
T
2026-06-24 15:55:06 +08:00

609 lines
23 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) => {
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, "⚠️ 会话已