1725 功能大改

This commit is contained in:
Vaica committed 2026-06-24 17:25:54 +08:00
1 parent df1395ded7
commit 0893b8998f
10 files changed
+1743 -10

No files matched your search

+413 -8
View File
@@ -79,18 +79,21 @@ export async function startPolling(): Promise<void> {
const chatId = msg.chat.id;
if (!allowed(chatId)) continue;
// 处理语音消息(转文字写入文章)
if ((msg.voice || msg.audio) && hasSession(chatId)) {
const voiceFile = msg.voice || msg.audio;
if (voiceFile) {
await handleVoice(chatId, voiceFile.file_id);
}
// 处理语音消息(增强版:保存文件 + 转文字 + 生成短代码)
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 handlePhoto(chatId, msg.photo, msg.caption);
await handlePhotoEnhanced(chatId, msg.photo, msg.caption);
continue;
}
@@ -110,6 +113,13 @@ export async function startPolling(): Promise<void> {
// 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; }
@@ -351,6 +361,13 @@ async function handleCommand(chatId: number, cmd: string, arg?: string) {
}
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")
@@ -373,3 +390,391 @@ async function handleInlineCallback(chatId: number, queryId: string, data: strin
await handleCommand(chatId, cmd, arg);
try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {}
}
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 handleAudio(chatId: number, fileId: string, fileName?: string, mimeType?: 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 };
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 handleAudioInfo(chatId: number, text: string): Promise<boolean> {
try {
const { getSession } = await import("./sessions");
const s = getSession(chatId);
if (!s._pendingAudio) {
return false;
}
const { filename } = s._pendingAudio;
delete s._pendingAudio;
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();
}
co// ============================================================
// 增强版 handlePhoto:支持封面图上传 + 实况照片引导
// ============================================================
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 } = 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));
// 更新音频信息
const audioInfo = s._pendingAudio;
delete s._pendingAudio;
// 生成带封面的音乐卡片
let name = "未知歌曲";
let artist = "未知艺术家";
if (audioInfo.name && audioInfo.artist) {
name = audioInfo.name;
artist = audioInfo.artist;
}
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, "❌ 封面图保存失败");
}
}
// ============================================================
// 增强版 handleAudio:支持封面图询问
// ============================================================
async function handleAudioEnhanced(chatId: number, fileId: string, fileName?: string, mimeType?: 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 : "未知错误"));
}
}
// ============================================================
// 增强版 handleAudioInfo:支持封面图询问
// ============================================================
async function handleAudioInfoEnhanced(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));
}
}