fix(core): 🐛 确保转发消息前先发送资料卡片

在转发用户消息到话题之前,强制执行资料卡片发送逻辑。这样可以确保用户信息卡片始终处于话题内容的顶部,避免后续回复消息将卡片内容淹没。

主要变更:
- 在消息中继逻辑中插入 ensureInfoCardBeforeRelay 调用
- 优化了消息回复 ID 的查询与映射处理逻辑
This commit is contained in:
2026-05-08 23:30:45 +08:00
parent 917a214161
commit 02b6cb1735

View File

@@ -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,