diff --git a/telegram/tg-bot.js b/telegram/tg-bot.js index 6309fcf..cc3a81d 100644 --- a/telegram/tg-bot.js +++ b/telegram/tg-bot.js @@ -732,6 +732,9 @@ async function relayToTopic(msg, u, env) { } try { + // 资料卡片必须先落到话题里,后续用户消息才不会把卡片顶到下面。 + await ensureInfoCardBeforeRelay(env, u, msg.from, tid, msg.date); + let forwardedMsg; const rawText = msg.text || ""; let topicReplyToMsgId = undefined; @@ -784,37 +787,6 @@ async function relayToTopic(msg, u, env) { } } - // 推送资料卡片流程补偿机制 - try { - let infoDirty = false; - if (!u.user_info.card_msg_id) { - try { - const cardId = await sendInfoCardToTopic(env, u, msg.from, tid, msg.date); - if (cardId) { - u.user_info.card_msg_id = cardId; - u.user_info.join_date = msg.date || (Date.now()/1000); - infoDirty = true; - } - } catch (innerErr) { - if (innerErr.message && (innerErr.message.includes("thread") || innerErr.message.includes("not found"))) { - throw innerErr; - } - } - } - // 回收临时占位符提升界面整洁度 - if (u.user_info.dummy_msg_id) { - await api(env.BOT_TOKEN, "deleteMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.dummy_msg_id }).catch(() => {}); - delete u.user_info.dummy_msg_id; infoDirty = true; - } - if (infoDirty) await updUser(uid, { user_info: u.user_info }, env); - } catch (processErr) { - if (processErr.message && (processErr.message.includes("thread") || processErr.message.includes("not found"))) { - await updUser(uid, { topic_id: null }, env); - u.topic_id = null; - return relayToTopic(msg, u, env); // 进行一次自愈递归重试 - } - } - // --------------------------------------------------------------------- // * 修改点:注释/移除了面向用户的 “✅ 已送达” 反馈回执 API 调用。 // 此动作去除了多余的底层 fetch() 开销。 @@ -841,6 +813,29 @@ async function relayToTopic(msg, u, env) { } } +async function ensureInfoCardBeforeRelay(env, u, tgUser, tid, date) { + let infoDirty = false; + if (!u.user_info) u.user_info = {}; + + if (!u.user_info.card_msg_id) { + const cardId = await sendInfoCardToTopic(env, u, tgUser, tid, date); + if (cardId) { + u.user_info.card_msg_id = cardId; + u.user_info.join_date = date || (Date.now()/1000); + infoDirty = true; + } + } + + // 回收临时占位符提升界面整洁度;先发卡片再删占位,确保新 topic 里卡片始终早于用户消息。 + if (u.user_info.dummy_msg_id) { + await api(env.BOT_TOKEN, "deleteMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.dummy_msg_id }).catch(() => {}); + delete u.user_info.dummy_msg_id; + infoDirty = true; + } + + if (infoDirty) await updUser(u.user_id, { user_info: u.user_info }, env); +} + // --- 核心:发送用户信息复合卡片 --- async function sendInfoCardToTopic(env, u, tgUser, tid, date) { const meta = getUMeta(tgUser, u, date || (Date.now()/1000)); @@ -936,6 +931,60 @@ const withReplyTarget = (replyToMsgId) => { return Number.isFinite(id) ? { reply_to_message_id: id } : {}; }; +const isReplyTargetError = (e) => { + const msg = (e?.message || "").toLowerCase(); + const mentionsReply = msg.includes("reply") || msg.includes("replied"); + return mentionsReply && (msg.includes("not found") || msg.includes("invalid")); +}; + +const isEntityPayloadError = (e) => { + const msg = (e?.message || "").toLowerCase(); + return msg.includes("entity") || msg.includes("entities") || msg.includes("parse"); +}; + +const withoutReplyTarget = (body) => { + const next = { ...body }; + delete next.reply_to_message_id; + return next; +}; + +const withoutEntityPayload = (body) => { + const next = { ...body }; + delete next.entities; + delete next.caption_entities; + return next; +}; + +async function apiWithDeliveryFallback(env, method, body) { + try { + return await api(env.BOT_TOKEN, method, body); + } catch (firstErr) { + const candidates = []; + const hasReplyTarget = body.reply_to_message_id !== undefined; + const hasEntityPayload = body.entities || body.caption_entities; + + if (hasReplyTarget && isReplyTargetError(firstErr)) { + candidates.push({ body: withoutReplyTarget(body), reason: "reply target missing" }); + if (hasEntityPayload) candidates.push({ body: withoutEntityPayload(withoutReplyTarget(body)), reason: "reply target missing + entity fallback" }); + } + if (hasEntityPayload && isEntityPayloadError(firstErr)) { + candidates.push({ body: withoutEntityPayload(body), reason: "entity fallback" }); + if (hasReplyTarget) candidates.push({ body: withoutReplyTarget(withoutEntityPayload(body)), reason: "entity fallback + reply target removed" }); + } + + let lastErr = firstErr; + for (const candidate of candidates) { + try { + console.warn(`Delivery fallback [${method}]: ${candidate.reason}`); + return await api(env.BOT_TOKEN, method, candidate.body); + } catch (candidateErr) { + lastErr = candidateErr; + } + } + throw lastErr; + } +} + async function deliverAdminMessageToUser(msg, uid, replyToMsgId, env) { const reply = withReplyTarget(replyToMsgId); const base = { chat_id: uid, ...reply }; @@ -943,14 +992,14 @@ async function deliverAdminMessageToUser(msg, uid, replyToMsgId, env) { if (msg.text) { const body = { ...base, text: msg.text }; if (msg.entities) body.entities = msg.entities; - return api(env.BOT_TOKEN, "sendMessage", body); + return apiWithDeliveryFallback(env, "sendMessage", body); } if (msg.photo) { const body = { ...base, photo: msg.photo[msg.photo.length - 1].file_id }; if (msg.caption) body.caption = msg.caption; if (msg.caption_entities) body.caption_entities = msg.caption_entities; - return api(env.BOT_TOKEN, "sendPhoto", body); + return apiWithDeliveryFallback(env, "sendPhoto", body); } const mediaMap = [ @@ -968,11 +1017,11 @@ async function deliverAdminMessageToUser(msg, uid, replyToMsgId, env) { const body = { ...base, [param]: msg[field].file_id }; if (msg.caption) body.caption = msg.caption; if (msg.caption_entities) body.caption_entities = msg.caption_entities; - return api(env.BOT_TOKEN, method, body); + return apiWithDeliveryFallback(env, method, body); } if (msg.location) { - return api(env.BOT_TOKEN, "sendLocation", { + return apiWithDeliveryFallback(env, "sendLocation", { ...base, latitude: msg.location.latitude, longitude: msg.location.longitude @@ -980,7 +1029,7 @@ async function deliverAdminMessageToUser(msg, uid, replyToMsgId, env) { } if (msg.contact) { - return api(env.BOT_TOKEN, "sendContact", { + return apiWithDeliveryFallback(env, "sendContact", { ...base, phone_number: msg.contact.phone_number, first_name: msg.contact.first_name, @@ -988,7 +1037,7 @@ async function deliverAdminMessageToUser(msg, uid, replyToMsgId, env) { }); } - return api(env.BOT_TOKEN, "copyMessage", { + return apiWithDeliveryFallback(env, "copyMessage", { chat_id: uid, from_chat_id: msg.chat.id, message_id: msg.message_id,