1410
This commit is contained in:
1 parent
cf06e72348
commit
14f473f89b
4 files changed
+171
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { startPolling } from "./poll";
|
||||
import { startCommentNotifier } from "./notify";
|
||||
import { TG_TOKEN } from "./config";
|
||||
|
||||
const g = globalThis as Record<string, unknown>;
|
||||
@@ -14,6 +15,7 @@ export async function startBot(): Promise<void> {
|
||||
|
||||
console.log("[bot] initializing...");
|
||||
await startPolling();
|
||||
startCommentNotifier();
|
||||
}
|
||||
|
||||
export { TG_TOKEN } from "./config";
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { ARTALK_SERVER, ARTALK_SITE } from "@/lib/config";
|
||||
import { sendMessage } from "./tg";
|
||||
import { ALLOWED_IDS } from "./config";
|
||||
|
||||
interface ArtalkComment {
|
||||
id: number;
|
||||
content: string;
|
||||
page_key: string;
|
||||
page_title?: string;
|
||||
nick: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
let lastCheckedId = 0;
|
||||
let notifierStarted = false;
|
||||
|
||||
async function fetchLatestComments(): Promise<ArtalkComment[]> {
|
||||
try {
|
||||
const url = `${ARTALK_SERVER}/api/v2/comments?site_name=${encodeURIComponent(ARTALK_SITE)}&limit=5&sort_by=created_at&sort_order=desc`;
|
||||
const res = await fetch(url, {
|
||||
headers: { "Authorization": "Bearer " },
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json() as { comments?: ArtalkComment[] };
|
||||
return data.comments || [];
|
||||
} catch (e) {
|
||||
console.error("[comment-notify] fetch error:", e instanceof Error ? e.message : String(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNewComments(): Promise<void> {
|
||||
const comments = await fetchLatestComments();
|
||||
if (comments.length === 0) return;
|
||||
|
||||
// 首次运行,记录最新 ID,不发通知
|
||||
if (lastCheckedId === 0) {
|
||||
lastCheckedId = comments[0].id;
|
||||
return;
|
||||
}
|
||||
|
||||
// 找出新评论
|
||||
const newComments = comments.filter(c => c.id > lastCheckedId);
|
||||
if (newComments.length === 0) return;
|
||||
|
||||
// 更新最新 ID
|
||||
lastCheckedId = Math.max(lastCheckedId, comments[0].id);
|
||||
|
||||
// 发送通知(倒序,最早的先发)
|
||||
const targets = ALLOWED_IDS.length > 0 ? ALLOWED_IDS : [];
|
||||
if (targets.length === 0) return;
|
||||
|
||||
for (const comment of newComments.reverse()) {
|
||||
const text = comment.content.length > 100
|
||||
? comment.content.slice(0, 100) + "..."
|
||||
: comment.content;
|
||||
|
||||
const msg = [
|
||||
`💬 <b>新评论</b>`,
|
||||
`👤 ${comment.nick}`,
|
||||
`📄 ${comment.page_title || comment.page_key}`,
|
||||
``,
|
||||
`${text}`,
|
||||
``,
|
||||
`🕐 ${new Date(comment.created_at).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`,
|
||||
].join("\n");
|
||||
|
||||
for (const chatId of targets) {
|
||||
try {
|
||||
await sendMessage(chatId, msg, { parse_mode: "HTML" });
|
||||
} catch (e) {
|
||||
console.error("[comment-notify] send error:", e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function startCommentNotifier(): void {
|
||||
if (notifierStarted) return;
|
||||
notifierStarted = true;
|
||||
|
||||
console.log("[comment-notify] starting comment notifier...");
|
||||
|
||||
// 首次检查(记录当前最新 ID,不发通知)
|
||||
checkNewComments();
|
||||
|
||||
// 每 60 秒检查一次
|
||||
setInterval(checkNewComments, 60_000);
|
||||
}
|
||||
@@ -73,6 +73,8 @@ export async function startPolling(): Promise<void> {
|
||||
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 };
|
||||
@@ -81,6 +83,15 @@ 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);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理图片
|
||||
if (msg.photo && hasSession(chatId)) {
|
||||
await handlePhoto(chatId, msg.photo, msg.caption);
|
||||
@@ -261,6 +272,40 @@ async function handleDocument(chatId: number, fileId: string, fileName?: string,
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL } from "@/lib/config";
|
||||
|
||||
// 语音转文字:使用 OpenAI Whisper API 或其他兼容服务
|
||||
const WHISPER_API_KEY = process.env.WHISPER_API_KEY || DEEPSEEK_API_KEY || "";
|
||||
const WHISPER_API_BASE = process.env.WHISPER_API_BASE || (DEEPSEEK_API_KEY ? DEEPSEEK_BASE_URL : "https://api.openai.com/v1");
|
||||
const WHISPER_MODEL = process.env.WHISPER_MODEL || "whisper-1";
|
||||
|
||||
export async function transcribeAudio(audioBuffer: ArrayBuffer, filename: string): Promise<string> {
|
||||
if (!WHISPER_API_KEY) {
|
||||
throw new Error("未配置语音转文字 API(设置 WHISPER_API_KEY 或 DEEPSEEK_API_KEY)");
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([audioBuffer], { type: "audio/ogg" });
|
||||
formData.append("file", blob, filename);
|
||||
formData.append("model", WHISPER_MODEL);
|
||||
formData.append("language", "zh");
|
||||
|
||||
const url = `${WHISPER_API_BASE}/audio/transcriptions`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${WHISPER_API_KEY}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`语音转文字失败: ${res.status} ${err}`);
|
||||
}
|
||||
|
||||
const data = await res.json() as { text?: string };
|
||||
return data.text || "";
|
||||
}
|
||||
Reference in new issue
Block a user