mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 19:30:46 +08:00
fix: preserve quoted message context across channels
This commit is contained in:
parent
c2be2389b0
commit
94e114b9b3
46 changed files with 4242 additions and 321 deletions
|
|
@ -4,6 +4,7 @@ import { createEditableMessageStream, splitMessageText } from '../shared/editabl
|
|||
import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
|
||||
import { recoverAssistantTextByTimestamp } from '../shared/session-reply-recovery.mjs';
|
||||
import { COMMANDS_MENU_BUTTON, TelegramApi } from './telegram-api.mjs';
|
||||
import { createTelegramHttpTransport } from './telegram-http.mjs';
|
||||
import { createTelegramBridgeStatus, TelegramHarnessBridge } from './telegram-bridge.mjs';
|
||||
|
|
@ -138,11 +139,94 @@ function telegramFileSource(message, loadFile) {
|
|||
};
|
||||
}
|
||||
|
||||
function telegramReplyAttachment(kind, file, fallbackName) {
|
||||
if (!file || typeof file !== 'object') return null;
|
||||
const name = typeof file.file_name === 'string' && file.file_name
|
||||
? file.file_name : typeof fallbackName === 'string' && fallbackName
|
||||
? fallbackName : undefined;
|
||||
return { kind, ...(name ? { name } : {}) };
|
||||
}
|
||||
|
||||
function telegramReplyAttachments(message) {
|
||||
const attachments = [];
|
||||
if (Array.isArray(message?.photo) && message.photo.length > 0) {
|
||||
const largest = message.photo.reduce((best, candidate) => (
|
||||
photoScore(candidate) > photoScore(best) ? candidate : best
|
||||
));
|
||||
attachments.push(telegramReplyAttachment(
|
||||
'image',
|
||||
largest,
|
||||
`${largest.file_unique_id ?? largest.file_id ?? 'telegram-photo'}.jpg`,
|
||||
));
|
||||
} else if (message?.document) {
|
||||
attachments.push(telegramReplyAttachment(
|
||||
imageTypeForDocument(message.document) ? 'image' : 'file',
|
||||
message.document,
|
||||
));
|
||||
}
|
||||
for (const [field, kind] of [
|
||||
['audio', 'audio'],
|
||||
['voice', 'audio'],
|
||||
['video', 'video'],
|
||||
['video_note', 'video'],
|
||||
['animation', 'video'],
|
||||
]) {
|
||||
if (message?.[field]) attachments.push(telegramReplyAttachment(kind, message[field]));
|
||||
}
|
||||
if (message?.sticker) {
|
||||
attachments.push(telegramReplyAttachment(
|
||||
message.sticker.is_video === true ? 'video' : 'image',
|
||||
message.sticker,
|
||||
message.sticker.file_unique_id ?? message.sticker.file_id,
|
||||
));
|
||||
}
|
||||
return attachments.filter(Boolean);
|
||||
}
|
||||
|
||||
function telegramReplyReference(message, { quote, loadReplyContent } = {}) {
|
||||
if ((!message || typeof message !== 'object')
|
||||
&& (!quote || typeof quote !== 'object')) return undefined;
|
||||
const authorId = message?.from?.id === undefined ? undefined : String(message.from.id);
|
||||
const authorName = [message?.from?.first_name, message?.from?.last_name]
|
||||
.filter((value) => typeof value === 'string' && value.trim())
|
||||
.map((value) => value.trim()).join(' ') || (
|
||||
typeof message?.from?.username === 'string' && message.from.username
|
||||
? message.from.username : undefined
|
||||
);
|
||||
const content = typeof message?.text === 'string'
|
||||
? message.text : typeof message?.caption === 'string'
|
||||
? message.caption : typeof quote?.text === 'string' ? quote.text : '';
|
||||
const attachments = telegramReplyAttachments(message);
|
||||
const messageId = Number.isSafeInteger(message?.message_id)
|
||||
? String(message.message_id) : undefined;
|
||||
const createdAt = Number.isSafeInteger(message?.date) && message.date >= 0
|
||||
? message.date * 1_000 : undefined;
|
||||
const load = !content.trim() && attachments.length === 0
|
||||
&& typeof loadReplyContent === 'function'
|
||||
? ({ signal } = {}) => loadReplyContent({
|
||||
...(messageId ? { messageId } : {}),
|
||||
...(createdAt === undefined ? {} : { createdAt }),
|
||||
}, { signal })
|
||||
: null;
|
||||
return {
|
||||
...(messageId ? { messageId } : {}),
|
||||
...(authorId ? { authorId } : {}),
|
||||
...(authorName ? { authorName } : {}),
|
||||
...(content.trim() ? { content } : {}),
|
||||
...(attachments.length > 0 ? { attachments } : {}),
|
||||
...(load ? { load } : {}),
|
||||
...(!content.trim() && attachments.length === 0 && !load
|
||||
? { unavailableReason: 'not-delivered' }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeTelegramUpdate(update, {
|
||||
botId,
|
||||
username,
|
||||
loadFile = async () => { throw new Error('Telegram file downloader is unavailable'); },
|
||||
loadFileStream = loadFile,
|
||||
loadReplyContent,
|
||||
}) {
|
||||
const message = update?.message;
|
||||
const chatId = message?.chat?.id;
|
||||
|
|
@ -159,6 +243,21 @@ export function normalizeTelegramUpdate(update, {
|
|||
? message.message_thread_id : undefined;
|
||||
const image = telegramImageSource(message, loadFile);
|
||||
const file = telegramFileSource(message, loadFileStream);
|
||||
const conversationId = messageThreadId === undefined
|
||||
? String(chatId) : `${chatId}:${messageThreadId}`;
|
||||
const key = `${direct ? 'direct' : 'group'}:${conversationId}`;
|
||||
const replyTo = telegramReplyReference(
|
||||
message.reply_to_message ?? message.external_reply,
|
||||
{
|
||||
quote: message.quote,
|
||||
...(typeof loadReplyContent === 'function' ? {
|
||||
loadReplyContent: (reference, options) => loadReplyContent({
|
||||
conversationKey: key,
|
||||
...reference,
|
||||
}, options),
|
||||
} : {}),
|
||||
},
|
||||
);
|
||||
return {
|
||||
messageId: String(update.update_id),
|
||||
senderId: String(senderId),
|
||||
|
|
@ -170,12 +269,12 @@ export function normalizeTelegramUpdate(update, {
|
|||
}),
|
||||
senderIsBot: message.from?.is_bot === true,
|
||||
kind: direct ? 'direct' : 'group',
|
||||
conversationId: messageThreadId === undefined
|
||||
? String(chatId) : `${chatId}:${messageThreadId}`,
|
||||
conversationId,
|
||||
content: withoutBotMention(message.text ?? message.caption ?? '', username),
|
||||
plainText: typeof message.text === 'string',
|
||||
images: image ? [image] : [],
|
||||
files: file ? [file] : [],
|
||||
...(replyTo ? { replyTo } : {}),
|
||||
addressed,
|
||||
reactionTarget: { chatId, messageId },
|
||||
replyTarget: {
|
||||
|
|
@ -839,6 +938,21 @@ export class TelegramRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
async #loadReplyContent(reference, { signal } = {}) {
|
||||
const key = typeof reference?.conversationKey === 'string'
|
||||
? reference.conversationKey.trim() : '';
|
||||
const quotedAt = Number(reference?.createdAt);
|
||||
if (!key || !Number.isFinite(quotedAt)) {
|
||||
return { unavailableReason: 'not-delivered' };
|
||||
}
|
||||
const sessionId = this.#state.sessionFor(key);
|
||||
const session = typeof sessionId === 'string' && sessionId
|
||||
? this.#harness.workspaceSession?.(sessionId)
|
||||
: null;
|
||||
const text = await recoverAssistantTextByTimestamp({ session, quotedAt, signal });
|
||||
return text ? { content: text } : { unavailableReason: 'not-delivered' };
|
||||
}
|
||||
|
||||
async #poll(initialCursor, signal) {
|
||||
let cursor = initialCursor;
|
||||
while (!signal.aborted) {
|
||||
|
|
@ -863,6 +977,7 @@ export class TelegramRuntime {
|
|||
username: this.#config.username,
|
||||
loadFile: (fileId, options) => this.#api.downloadFile({ fileId, ...options }),
|
||||
loadFileStream: (fileId, options) => this.#api.downloadFileStream({ fileId, ...options }),
|
||||
loadReplyContent: (reference, options) => this.#loadReplyContent(reference, options),
|
||||
});
|
||||
if (message) {
|
||||
void this.#bridge.accept(message, { contextSnapshot }).catch((error) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue