Files
blog/write-server/src/lib/bot/poll.ts
T
2026-06-24 17:35:07 +08:00

697 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 { TG_TOKEN } from "./config";
import { tg, allowed, sendMessage } from "./tg";
import { hasSession, getSession, appendContent } from "./sessions";
import handlers from "./handlers";
const ARG_COMMANDS = new Set([
"new", "title", "categories", "tags", "author",
"edit", "view", "delete",
"link_add", "link_edit", "link_del", "link_toggle",
"feed_add", "feed_edit", "feed_del",
"list", "links", "feeds", "read",
]);
const CALLBACK_PREFIXES: Record<string, string> = {
edit: "edit",
confirm_del: "delete",
cancel_del: "noop",
list: "list",
links: "links",
feeds: "feeds",
skip_cat: "skip_categories",
skip_tag: "skip_tags",
};
function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
export async function startPolling(): Promise<void> {
if (!TG_TOKEN) {
console.log("[bot] TG_BOT_TOKEN not set, skipping bot startup");
return;
}
console.log("[bot] starting Telegram polling...");
let lastOffset = 0;
let backoff = 3000;
async function poll() {
try {
const data = await tg<Record<string, unknown>[]>("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 as number;
if (update.callback_query) {
const cb = update.callback_query as {
message?: { chat?: { id: number } };
from?: { id: number };
id: string;
data?: string;
};
const cbChatId = cb.message?.chat?.id ?? cb.from?.id ?? 0;
if (cb.data && allowed(cbChatId)) {
await handleInlineCallback(cbChatId, cb.id, cb.data);
}
continue;
}
const msg = update.message as {
chat?: { id: number };
photo?: { file_id: string }[];
document?: { file_id: string; file_name?: string; mime_type?: string };
animation?: { file_id: string; file_name?: string; mime_type?: string };
video?: { file_id: string; file_name?: string };
voice?: { file_id: string; duration?: number };
audio?: { file_id: string; file_name?: string; duration?: number };
text?: string;
caption?: string;
reply_to_message?: { text?: string };
} | undefined;
if (!msg?.chat?.id) continue;
const chatId = msg.chat.id;
if (!allowed(chatId)) continue;
// 处理语音消息(增强版:保存文件 + 转文字 + 生成短代码)
if (msg.voice && hasSession(chatId)) {
await handleVoiceEnhanced(chatId, msg.voice.file_id, msg.voice.duration);
continue;
}
// 处理音频文件(增强版:支持封面图)
if (msg.audio && hasSession(chatId)) {
await handleAudioEnhanced(chatId, msg.audio.file_id, msg.audio.file_name);
continue;
}
// 处理图片(增强版:支持封面图上传 + 实况照片引导)
if (msg.photo && hasSession(chatId)) {
await handlePhotoEnhanced(chatId, msg.photo, msg.caption);
continue;
}
// 处理 GIF / 文档 / 视频
if ((msg.animation || msg.document || msg.video) && hasSession(chatId)) {
const fileType = msg.animation ? "animation" : msg.video ? "video" : "document";
const file = msg.animation || msg.document || msg.video;
if (file) {
await handleDocument(chatId, file.file_id, file.file_name, msg.caption, fileType);
}
continue;
}
if (!msg.text) continue;
const fullText = msg.text.trim();
// ForceReply routing
if (msg.reply_to_message?.text) {
const original = msg.reply_to_message.text;
// 处理音频信息回复(音乐卡片)
if (original.includes("请输入歌曲信息")) {
const handled = await handleAudioInfo(chatId, fullText);
if (handled) continue;
}
if (original.includes("请输入文章标题")) { await handleCommand(chatId, "new", fullText); continue; }
if (original.includes("请输入新标题")) { await handleCommand(chatId, "title", fullText); continue; }
if (original.includes("请输入分类")) { await handleCommand(chatId, "categories", fullText); continue; }
if (original.includes("请输入标签")) { await handleCommand(chatId, "tags", fullText); continue; }
if (original.includes("请输入友链信息")) { await handleCommand(chatId, "link_add", fullText); continue; }
if (original.includes("请输入新名称")) { await handlers.handleLinkEditReply(chatId, fullText); continue; }
if (original.includes("请输入订阅源 URL")) { await handleCommand(chatId, "feed_add", fullText); continue; }
if (original.includes("请输入新的订阅信息")) { await handlers.handleFeedEditReply(chatId, fullText); continue; }
}
const parts = fullText.split("@")[0].split(/\s+/);
const cmdRaw = parts[0].toLowerCase();
const slashCmd = cmdRaw.startsWith("/") ? cmdRaw.slice(1) : "";
const slashArg = parts.slice(1).join(" ");
// In writing session, non-slash text appends to content
if (hasSession(chatId) && !cmdRaw.startsWith("/")) {
try { appendContent(chatId, fullText); }
catch (e) { console.error("[bot:append]", e instanceof Error ? e.message : String(e)); }
continue;
}
if (cmdRaw.startsWith("/")) {
await handleCommand(chatId, slashCmd, slashArg);
}
}
}
backoff = 3000;
} catch (e) {
console.error("[bot:poll]", e instanceof Error ? e.message : String(e));
await sleep(backoff);
backoff = Math.min(backoff * 2, 60000);
}
await sleep(100);
setImmediate(poll);
}
// Register commands with Telegram
try {
await tg("setMyCommands", {
commands: [
{ command: "new", description: "写一篇文章" },
{ command: "edit", description: "编辑文章" },
{ command: "delete", description: "删除文章" },
{ command: "list", description: "文章列表" },
{ command: "publish", description: "保存发布" },
{ command: "draft", description: "保存草稿" },
{ command: "deploy", description: "推送到线上" },
{ command: "sync", description: "同步远程" },
{ command: "undo", description: "撤销最后一段" },
{ command: "preview", description: "预览" },
{ command: "links", description: "友链管理" },
{ command: "link_add", description: "添加友链" },
{ command: "link_edit", description: "编辑友链" },
{ command: "link_del", description: "删除友链" },
{ command: "link_toggle", description: "显示/隐藏友链" },
{ command: "feeds", description: "订阅管理" },
{ command: "feed_add", description: "添加订阅源" },
{ command: "feed_edit", description: "编辑订阅源" },
{ command: "feed_del", description: "删除订阅源" },
{ command: "feed_health", description: "订阅源健康检查" },
{ command: "read", description: "友链最新文章" },
{ command: "stats", description: "网站数据" },
{ command: "help", description: "帮助" },
],
});
console.log("[bot] commands registered");
} catch (e) {
console.error("[bot] failed to register commands:", e instanceof Error ? e.message : String(e));
}
poll();
}
async function handlePhoto(chatId: number, photos: { file_id: string }[], caption?: string) {
try {
const fs = await import("fs");
const path = await import("path");
const { getSession } = await import("./sessions");
const s = getSession(chatId);
if (!s.dirPath) {
await sendMessage(chatId, "❌ 请先发 /new 或 /edit 开始写作");
return;
}
const largestPhoto = photos[photos.length - 1];
const fileRes = await tg<{ file_path?: string }>("getFile", { file_id: largestPhoto.file_id });
if (!fileRes.ok || !fileRes.result?.file_path) return;
const filePath = fileRes.result.file_path;
const fileUrl = "https://api.telegram.org/file/bot" + TG_TOKEN + "/" + filePath;
const imgBuffer = await fetch(fileUrl).then((r) => r.arrayBuffer());
// Save to article's own folder (Hugo page bundle)
const articleDir = s.dirPath;
if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true });
const ext = filePath.split(".").pop() || "jpg";
const filename = Date.now() + "-" + largestPhoto.file_id.slice(0, 8) + "." + ext;
fs.writeFileSync(path.join(articleDir, filename), Buffer.from(imgBuffer));
const imgCaption = caption || "image";
appendContent(chatId, `\n![${imgCaption}](${filename})\n`);
await sendMessage(chatId, `📷 图片已保存:${filename}`);
} catch (e) {
console.error("[bot:photo]", e instanceof Error ? e.message : String(e));
await sendMessage(chatId, "❌ 图片保存失败");
}
}
async function handleDocument(chatId: number, fileId: string, fileName?: string, caption?: string, fileType?: string) {
try {
const fs = await import("fs");
const path = await import("path");
const { execSync } = await import("child_process");
const { getSession } = await import("./sessions");
const s = getSession(chatId);
if (!s.dirPath) {
await sendMessage(chatId, "❌ 请先发 /new 或 /edit 开始写作");
return;
}
const fileRes = await tg<{ file_path?: string }>("getFile", { file_id: fileId });
if (!fileRes.ok || !fileRes.result?.file_path) {
await sendMessage(chatId, "❌ 文件获取失败(可能文件太大,Telegram 限制 20MB)");
return;
}
const filePath = fileRes.result.file_path;
const fileUrl = "https://api.telegram.org/file/bot" + TG_TOKEN + "/" + filePath;
const fileBuffer = await fetch(fileUrl).then((r) => r.arrayBuffer());
const articleDir = s.dirPath;
if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true });
const ext = fileName?.split(".").pop() || filePath.split(".").pop() || "bin";
const baseFilename = Date.now() + "-" + fileId.slice(0, 8);
const filename = baseFilename + "." + ext;
const fullPath = path.join(articleDir, filename);
fs.writeFileSync(fullPath, Buffer.from(fileBuffer));
const fileCaption = caption || fileName || "file";
if (fileType === "animation") {
// Telegram GIF → ffmpeg 转成真正的 GIF
const gifFilename = baseFilename + ".gif";
const gifPath = path.join(articleDir, gifFilename);
try {
await sendMessage(chatId, "🎞️ 正在转换 GIF...");
execSync(`ffmpeg -y -i "${fullPath}" -vf "fps=15,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" -loop 0 "${gifPath}"`, { timeout: 60000 });
fs.unlinkSync(fullPath);
appendContent(chatId, `\n![${fileCaption}](${gifFilename})\n`);
await sendMessage(chatId, `🎞️ GIF 已转换:${gifFilename}`);
} catch (e) {
console.error("[bot:gif-convert]", e instanceof Error ? e.message : String(e));
appendContent(chatId, `\n{{< video src="${filename}" loop="true" muted="true" >}}\n`);
await sendMessage(chatId, `🎞️ GIF 转换失败,已保存为视频:${filename}`);
}
return;
} else if (fileType === "video") {
appendContent(chatId, `\n{{< video src="${filename}" caption="${fileCaption}" >}}\n`);
await sendMessage(chatId, `🎬 视频已保存:${filename}`);
return;
} else {
const extLower = ext.toLowerCase();
const isImage = ["gif", "webp", "png", "jpg", "jpeg"].includes(extLower);
const isVideo = ["mp4", "webm", "mov", "avi", "mkv", "flv"].includes(extLower);
if (isImage) {
appendContent(chatId, `\n![${fileCaption}](${filename})\n`);
} else if (isVideo) {
appendContent(chatId, `\n{{< video src="${filename}" caption="${fileCaption}" >}}\n`);
} else {
appendContent(chatId, `\n[${fileCaption}](${filename})\n`);
}
await sendMessage(chatId, `📎 文件已保存:${filename}`);
}
} catch (e) {
console.error("[bot:document]", e instanceof Error ? e.message : String(e));
await sendMessage(chatId, "❌ 文件保存失败");
}
}
async function handleVoice(chatId: number, fileId: string) {
try {
const { transcribeAudio } = await import("@/lib/voice");
await sendMessage(chatId, "🎤 正在识别语音...");
// 获取文件路径
const fileRes = await tg<{ file_path?: string }>("getFile", { file_id: fileId });
if (!fileRes.ok || !fileRes.result?.file_path) {
await sendMessage(chatId, "❌ 语音文件获取失败");
return;
}
// 下载语音文件
const fileUrl = "https://api.telegram.org/file/bot" + TG_TOKEN + "/" + fileRes.result.file_path;
const audioBuffer = await fetch(fileUrl).then(r => r.arrayBuffer());
// 转文字
const text = await transcribeAudio(audioBuffer, fileRes.result.file_path);
if (!text.trim()) {
await sendMessage(chatId, "❌ 语音识别结果为空");
return;
}
// 追加到文章内容
appendContent(chatId, text.trim());
const preview = text.trim().length > 80 ? text.trim().slice(0, 80) + "..." : text.trim();
await sendMessage(chatId, `✅ 语音已转文字:\n\n${preview}`);
} catch (e) {
console.error("[bot:voice]", e instanceof Error ? e.message : String(e));
await sendMessage(chatId, "❌ 语音转文字失败:" + (e instanceof Error ? e.message : "未知错误"));
}
}
async function handleCommand(chatId: number, cmd: string, arg?: string) {
try {
if (cmd === "noop") return;
if (ARG_COMMANDS.has(cmd)) {
const handler = handlers[cmd];
if (handler) await handler(chatId, arg);
else await sendMessage(chatId, "❌ 未知命令:/" + cmd);
} else {
const handler = handlers[cmd];
if (handler) await handler(chatId);
else if (cmd) await sendMessage(chatId, "❌ 未知命令:/" + cmd);
}
} catch (e) {
console.error("[bot:" + cmd + "]", e instanceof Error ? e.message : String(e));
try {
await sendMessage(chatId, "❌ " + (e instanceof Error ? e.message : "error").slice(0, 200));
} catch (e) { /* ignore */ }
}
}
async function handleInlineCallback(chatId: number, queryId: string, data: string) {
// 处理音频封面图回调
if (data === "audio_add_cover" || data === "audio_skip_cover") {
await handleAudioCoverCallback(chatId, data);
try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {}
return;
}
let prefix = "";
let arg = "";
// First try prefix match (e.g. "edit_slug", "list_2")
for (const p of Object.keys(CALLBACK_PREFIXES)) {
if (data.startsWith(p + "_")) {
prefix = p;
arg = data.slice(p.length + 1);
break;
}
}
// Then try exact match (e.g. "confirm_shutdown", "skip_cat")
if (!prefix && CALLBACK_PREFIXES[data]) {
prefix = data;
}
if (!prefix) {
try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {}
return;
}
const cmd = CALLBACK_PREFIXES[prefix];
await handleCommand(chatId, cmd, arg);
try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {}
}
// ============================================================
// 新增:处理语音消息(增强版:保存文件 + 转文字 + 生成短代码)
// ============================================================
async function handleVoiceEnhanced(chatId: number, fileId: string, duration?: number) {
try {
const fs = await import("fs");
const path = await import("path");
const { getSession } = await import("./sessions");
const { transcribeAudio } = await import("@/lib/voice");
const s = getSession(chatId);
if (!s.dirPath) {
await sendMessage(chatId, "❌ 请先发 /new 或 /edit 开始写作");
return;
}
await sendMessage(chatId, "🎙️ 正在处理语音...");
const fileRes = await tg<{ file_path?: string }>("getFile", { file_id: fileId });
if (!fileRes.ok || !fileRes.result?.file_path) {
await sendMessage(chatId, "❌ 语音文件获取失败");
return;
}
const filePath = fileRes.result.file_path;
const fileUrl = "https://api.telegram.org/file/bot" + TG_TOKEN + "/" + filePath;
const audioBuffer = await fetch(fileUrl).then(r => r.arrayBuffer());
const articleDir = s.dirPath;
if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true });
const ext = filePath.split(".").pop() || "ogg";
const filename = Date.now() + "-voice." + ext;
fs.writeFileSync(path.join(articleDir, filename), Buffer.from(audioBuffer));
const voiceShortcode = `\n\n{{< voice src="${filename}" >}}\n\n`;
appendContent(chatId, voiceShortcode);
try {
const text = await transcribeAudio(audioBuffer, filePath);
if (text.trim()) {
appendContent(chatId, `\n> ${text.trim()}\n\n`);
const preview = text.trim().length > 80 ? text.trim().slice(0, 80) + "..." : text.trim();
await sendMessage(chatId, `✅ 语音已保存:${filename}\n\n📝 转文字:${preview}`);
} else {
await sendMessage(chatId, `✅ 语音已保存:${filename}\n\n⚠️ 语音转文字失败,已插入语音播放器`);
}
} catch (transcribeError) {
console.error("[bot:voice-transcribe]", transcribeError);
await sendMessage(chatId, `✅ 语音已保存:${filename}\n\n⚠️ 语音转文字失败,已插入语音播放器`);
}
} catch (e) {
console.error("[bot:voice]", e instanceof Error ? e.message : String(e));
await sendMessage(chatId, "❌ 语音处理失败:" + (e instanceof Error ? e.message : "未知错误"));
}
}
// ============================================================
// 新增:处理音频文件(增强版:支持封面图)
// ============================================================
async function handleAudioEnhanced(chatId: number, fileId: string, fileName?: string) {
try {
const fs = await import("fs");
const path = await import("path");
const { getSession } = await import("./sessions");
const s = getSession(chatId);
if (!s.dirPath) {
await sendMessage(chatId, "❌ 请先发 /new 或 /edit 开始写作");
return;
}
await sendMessage(chatId, "🎵 正在处理音频文件...");
const fileRes = await tg<{ file_path?: string }>("getFile", { file_id: fileId });
if (!fileRes.ok || !fileRes.result?.file_path) {
await sendMessage(chatId, "❌ 音频文件获取失败");
return;
}
const filePath = fileRes.result.file_path;
const fileUrl = "https://api.telegram.org/file/bot" + TG_TOKEN + "/" + filePath;
const audioBuffer = await fetch(fileUrl).then(r => r.arrayBuffer());
const articleDir = s.dirPath;
if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true });
const ext = fileName?.split(".").pop() || filePath.split(".").pop() || "mp3";
const filename = Date.now() + "-music." + ext;
fs.writeFileSync(path.join(articleDir, filename), Buffer.from(audioBuffer));
const session = getSession(chatId);
session._pendingAudio = { filename, fileId, name: null, artist: null };
await tg("sendMessage", {
chat_id: chatId,
text: `✅ 音频已保存:${filename}\n\n请输入歌曲信息(格式:歌名 - 歌手):`,
reply_markup: {
force_reply: true,
input_field_placeholder: "例如:晴天 - 周杰伦"
}
});
} catch (e) {
console.error("[bot:audio]", e instanceof Error ? e.message : String(e));
await sendMessage(chatId, "❌ 音频处理失败:" + (e instanceof Error ? e.message : "未知错误"));
}
}
// ============================================================
// 新增:处理图片(增强版:支持封面图上传 + 实况照片引导)
// ============================================================
async function handlePhotoEnhanced(chatId: number, photos: { file_id: string }[], caption?: string) {
try {
const fs = await import("fs");
const path = await import("path");
const { getSession } = await import("./sessions");
const s = getSession(chatId);
if (!s.dirPath) {
await sendMessage(chatId, "❌ 请先发 /new 或 /edit 开始写作");
return;
}
// 检查是否在等待封面图
if (s._pendingAudio) {
await handleAudioCover(chatId, photos);
return;
}
// 正常图片处理
const largestPhoto = photos[photos.length - 1];
const fileRes = await tg<{ file_path?: string }>("getFile", { file_id: largestPhoto.file_id });
if (!fileRes.ok || !fileRes.result?.file_path) return;
const filePath = fileRes.result.file_path;
const fileUrl = "https://api.telegram.org/file/bot" + TG_TOKEN + "/" + filePath;
const imgBuffer = await fetch(fileUrl).then((r) => r.arrayBuffer());
const articleDir = s.dirPath;
if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true });
const ext = filePath.split(".").pop() || "jpg";
const filename = Date.now() + "-" + largestPhoto.file_id.slice(0, 8) + "." + ext;
fs.writeFileSync(path.join(articleDir, filename), Buffer.from(imgBuffer));
const imgCaption = caption || "image";
appendContent(chatId, `\n![${imgCaption}](${filename})\n`);
await sendMessage(chatId, `📷 图片已保存:${filename}
💡 <b>提示:</b>如果您发送的是 iOS 实况照片(Live Photo),
动态效果无法自动提取。如需保留动态效果,请:
1. 将实况照片转为视频/GIF 后重新发送
2. 或者单独发送视频部分
继续发送其他内容...</b>`, { parse_mode: "HTML" });
} catch (e) {
console.error("[bot:photo]", e instanceof Error ? e.message : String(e));
await sendMessage(chatId, "❌ 图片保存失败");
}
}
// ============================================================
// 新增:处理音乐封面图
// ============================================================
async function handleAudioCover(chatId: number, photos: { file_id: string }[]) {
try {
const fs = await import("fs");
const path = await import("path");
const { getSession } = await import("./sessions");
const s = getSession(chatId);
if (!s._pendingAudio) {
await sendMessage(chatId, "❌ 没有待处理的音频文件");
return;
}
const { filename, name, artist } = s._pendingAudio;
const largestPhoto = photos[photos.length - 1];
const fileRes = await tg<{ file_path?: string }>("getFile", { file_id: largestPhoto.file_id });
if (!fileRes.ok || !fileRes.result?.file_path) {
await sendMessage(chatId, "❌ 封面图获取失败");
return;
}
const filePath = fileRes.result.file_path;
const fileUrl = "https://api.telegram.org/file/bot" + TG_TOKEN + "/" + filePath;
const imgBuffer = await fetch(fileUrl).then((r) => r.arrayBuffer());
const articleDir = s.dirPath;
const ext = filePath.split(".").pop() || "jpg";
const coverFilename = Date.now() + "-cover." + ext;
fs.writeFileSync(path.join(articleDir, coverFilename), Buffer.from(imgBuffer));
delete s._pendingAudio;
const musicCardShortcode = `\n\n{{< music-card src="${filename}" name="${name}" artist="${artist}" cover="${coverFilename}" >}}\n\n`;
appendContent(chatId, musicCardShortcode);
await sendMessage(chatId, `✅ 封面图已保存:${coverFilename}
🎵 <b>音乐卡片已插入:</b>
📌 歌曲:${name}
🎤 歌手:${artist}
🖼️ 封面:${coverFilename}`, { parse_mode: "HTML" });
} catch (e) {
console.error("[bot:audio-cover]", e instanceof Error ? e.message : String(e));
await sendMessage(chatId, "❌ 封面图保存失败");
}
}
// ============================================================
// 新增:处理音频信息回复
// ============================================================
async function handleAudioInfo(chatId: number, text: string): Promise<boolean> {
try {
const { getSession } = await import("./sessions");
const s = getSession(chatId);
if (!s._pendingAudio) {
return false;
}
let name = "未知歌曲";
let artist = "未知艺术家";
const parts = text.split(/[-–—]/);
if (parts.length >= 2) {
name = parts[0].trim();
artist = parts.slice(1).join("-").trim();
} else if (text.trim()) {
name = text.trim();
}
s._pendingAudio.name = name;
s._pendingAudio.artist = artist;
await tg("sendMessage", {
chat_id: chatId,
text: `🎵 歌曲:${name}\n🎤 歌手:${artist}\n\n是否添加封面图?`,
reply_markup: {
inline_keyboard: [
[
{ text: "📷 添加封面图", callback_data: "audio_add_cover" },
{ text: "⏭️ 跳过", callback_data: "audio_skip_cover" }
]
]
}
});
return true;
} catch (e) {
console.error("[bot:audio-info]", e instanceof Error ? e.message : String(e));
return false;
}
}
// ============================================================
// 新增:处理封面图选择回调
// ============================================================
async function handleAudioCoverCallback(chatId: number, data: string) {
try {
const { getSession } = await import("./sessions");
const s = getSession(chatId);
if (!s._pendingAudio) {
await sendMessage(chatId, "❌ 没有待处理的音频文件");
return;
}
if (data === "audio_skip_cover") {
const { filename, name, artist } = s._pendingAudio;
delete s._pendingAudio;
const musicCardShortcode = `\n\n{{< music-card src="${filename}" name="${name}" artist="${artist}" >}}\n\n`;
appendContent(chatId, musicCardShortcode);
await sendMessage(chatId, `🎵 <b>音乐卡片已插入:</b>
📌 歌曲:${name}
🎤 歌手:${artist}`, { parse_mode: "HTML" });
} else if (data === "audio_add_cover") {
await tg("sendMessage", {
chat_id: chatId,
text: "🖼️ 请发送封面图片:",
reply_markup: {
force_reply: true,
input_field_placeholder: "发送一张图片作为封面"
}
});
}
} catch (e) {
console.error("[bot:audio-cover-callback]", e instanceof Error ? e.message : String(e));
}
}