From 308fbe2f9a3ddcc6b6fa17a7831afffc68754483 Mon Sep 17 00:00:00 2001 From: Wolf-L <50284298+Wolf-L@users.noreply.github.com> Date: Fri, 29 May 2026 01:13:31 +0800 Subject: [PATCH] feat: show AI extraction results in Telegram Show AI extraction results in Telegram notifications and /mails views. --- worker/src/email/ai_extract.ts | 13 +++-- worker/src/email/index.ts | 8 +-- worker/src/i18n/en.ts | 6 +++ worker/src/i18n/type.ts | 6 +++ worker/src/i18n/zh.ts | 6 +++ worker/src/telegram_api/telegram.ts | 76 +++++++++++++++++++++++++++-- 6 files changed, 101 insertions(+), 14 deletions(-) diff --git a/worker/src/email/ai_extract.ts b/worker/src/email/ai_extract.ts index 3bf5a50f..3b318963 100644 --- a/worker/src/email/ai_extract.ts +++ b/worker/src/email/ai_extract.ts @@ -151,17 +151,17 @@ export async function extractEmailInfo( env: Bindings, message_id: string | null, address: string -): Promise { +): Promise { try { // Check if AI extraction is enabled via environment variable if (!getBooleanValue(env.ENABLE_AI_EMAIL_EXTRACT)) { - return; + return null; } // Ensure AI binding is available if (!env.AI) { console.error('AI binding not available'); - return; + return null; } // Check allowlist if enabled @@ -187,7 +187,7 @@ export async function extractEmailInfo( if (!isAllowed) { console.log(`AI extraction skipped for ${address}: not in allowlist`); - return; + return null; } } @@ -196,7 +196,7 @@ export async function extractEmailInfo( const emailContent = parsedEmail?.text || parsedEmail?.html || ""; if (!emailContent) { - return; + return null; } // Truncate content if too long (max 4000 characters to avoid token limits) @@ -219,9 +219,12 @@ export async function extractEmailInfo( ).bind(metadata, message_id).run(); console.log(`AI extraction completed for ${message_id}: ${result.type}`); + return result; } + return result; } catch (e) { console.error('AI email extraction error:', e); + return null; } } diff --git a/worker/src/email/index.ts b/worker/src/email/index.ts index 128e2a7b..8593cd7d 100644 --- a/worker/src/email/index.ts +++ b/worker/src/email/index.ts @@ -122,11 +122,14 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu // forward email await forwardEmail(message, env); + // AI email content extraction + const aiExtractResult = await extractEmailInfo(parsedEmailContext, env, message_id, toAddress); + // send email to telegram try { await sendMailToTelegram( { env: env } as Context, - toAddress, parsedEmailContext, message_id); + toAddress, parsedEmailContext, message_id, aiExtractResult); } catch (error) { console.error("send mail to telegram error", error); } @@ -158,9 +161,6 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu // auto reply email await auto_reply(message, env, toAddress); - - // AI email content extraction - await extractEmailInfo(parsedEmailContext, env, message_id, toAddress); } export { email } diff --git a/worker/src/i18n/en.ts b/worker/src/i18n/en.ts index eef09dd3..c92b85ba 100644 --- a/worker/src/i18n/en.ts +++ b/worker/src/i18n/en.ts @@ -174,6 +174,12 @@ const messages: LocaleMessages = { TgNoPermissionViewMailMsg: "No permission to view this mail", TgBotTokenRequiredMsg: "TELEGRAM_BOT_TOKEN is required", TgLangFeatureDisabledMsg: "Language setting feature is disabled. System default language is used.", + TgAiExtractResultMsg: "AI extracted", + TgAiExtractAuthCodeMsg: "Verification code", + TgAiExtractAuthLinkMsg: "Verification link", + TgAiExtractServiceLinkMsg: "Service link", + TgAiExtractSubscriptionLinkMsg: "Subscription link", + TgAiExtractOtherLinkMsg: "Other link", } export default messages; diff --git a/worker/src/i18n/type.ts b/worker/src/i18n/type.ts index 8ea83ab8..a677c0af 100644 --- a/worker/src/i18n/type.ts +++ b/worker/src/i18n/type.ts @@ -172,4 +172,10 @@ export type LocaleMessages = { TgNoPermissionViewMailMsg: string TgBotTokenRequiredMsg: string TgLangFeatureDisabledMsg: string + TgAiExtractResultMsg: string + TgAiExtractAuthCodeMsg: string + TgAiExtractAuthLinkMsg: string + TgAiExtractServiceLinkMsg: string + TgAiExtractSubscriptionLinkMsg: string + TgAiExtractOtherLinkMsg: string } diff --git a/worker/src/i18n/zh.ts b/worker/src/i18n/zh.ts index 6bb2223d..c4837e3a 100644 --- a/worker/src/i18n/zh.ts +++ b/worker/src/i18n/zh.ts @@ -174,6 +174,12 @@ const messages: LocaleMessages = { TgNoPermissionViewMailMsg: "无权查看此邮件", TgBotTokenRequiredMsg: "需要设置 TELEGRAM_BOT_TOKEN", TgLangFeatureDisabledMsg: "语言设置功能已禁用,使用系统默认语言", + TgAiExtractResultMsg: "AI 提取", + TgAiExtractAuthCodeMsg: "验证码", + TgAiExtractAuthLinkMsg: "验证链接", + TgAiExtractServiceLinkMsg: "服务链接", + TgAiExtractSubscriptionLinkMsg: "订阅链接", + TgAiExtractOtherLinkMsg: "其他链接", } export default messages; diff --git a/worker/src/telegram_api/telegram.ts b/worker/src/telegram_api/telegram.ts index 94550032..33b21438 100644 --- a/worker/src/telegram_api/telegram.ts +++ b/worker/src/telegram_api/telegram.ts @@ -14,6 +14,7 @@ import { RawMailRow } from "../models"; import { UserFromGetMe } from "telegraf/types"; import i18n from "../i18n"; import { LocaleMessages } from "../i18n/type"; +import type { ExtractResult } from "../email/ai_extract"; // Helper to get messages by userId @@ -75,6 +76,62 @@ const COMMANDS = [ }, ] +const getAiExtractLabel = ( + msgs: LocaleMessages, + type: ExtractResult["type"] +): string => { + switch (type) { + case "auth_code": + return msgs.TgAiExtractAuthCodeMsg; + case "auth_link": + return msgs.TgAiExtractAuthLinkMsg; + case "service_link": + return msgs.TgAiExtractServiceLinkMsg; + case "subscription_link": + return msgs.TgAiExtractSubscriptionLinkMsg; + case "other_link": + return msgs.TgAiExtractOtherLinkMsg; + default: + return msgs.TgAiExtractResultMsg; + } +} + +const parseAiExtractMetadata = ( + metadata: string | undefined | null +): ExtractResult | null => { + if (!metadata) return null; + try { + const parsed = JSON.parse(metadata); + const result = parsed?.ai_extract; + if ( + result + && typeof result.type === "string" + && result.type !== "none" + && typeof result.result === "string" + && result.result + ) { + return result as ExtractResult; + } + } catch (error) { + console.warn("Failed to parse AI extraction metadata", error); + } + return null; +} + +const formatAiExtractForTelegram = ( + msgs: LocaleMessages, + aiExtract: ExtractResult | null | undefined +): string => { + if (!aiExtract || aiExtract.type === "none" || !aiExtract.result) { + return ""; + } + const label = getAiExtractLabel(msgs, aiExtract.type); + const displayText = aiExtract.type !== "auth_code" && aiExtract.result_text + ? ` (${aiExtract.result_text})` + : ""; + return `${msgs.TgAiExtractResultMsg}\n${label}: ${aiExtract.result}${displayText}\n\n`; +} + export const getTelegramCommands = (c: Context) => { return getBooleanValue(c.env.TG_ALLOW_USER_LANG) ? COMMANDS @@ -312,7 +369,10 @@ export function newTelegramBot(c: Context, token: string): Teleg const raw = mailRow ? await resolveRawEmail(mailRow) : undefined; const mailId = mailRow?.id; const created_at = mailRow?.created_at; - const { mail } = raw ? await parseMail(msgs, { rawEmail: raw }, queryAddress, created_at) : { mail: msgs.TgNoMoreMailsMsg }; + const aiExtract = parseAiExtractMetadata(mailRow?.metadata); + const { mail } = raw + ? await parseMail(msgs, { rawEmail: raw }, queryAddress, created_at, aiExtract) + : { mail: msgs.TgNoMoreMailsMsg }; const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json"); const miniAppButtons = [] if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) { @@ -381,7 +441,9 @@ export async function initTelegramBotCommands(c: Context, bot: T const parseMail = async ( msgs: LocaleMessages, parsedEmailContext: ParsedEmailContext, - address: string, created_at: string | undefined | null + address: string, + created_at: string | undefined | null, + aiExtract?: ExtractResult | null ) => { if (!parsedEmailContext.rawEmail) { return {}; @@ -394,7 +456,8 @@ const parseMail = async ( } return { isHtml: false, - mail: `From: ${parsedEmail?.sender || msgs.TgNoSenderMsg}\n` + mail: formatAiExtractForTelegram(msgs, aiExtract) + + `From: ${parsedEmail?.sender || msgs.TgNoSenderMsg}\n` + `To: ${address}\n` + (created_at ? `Date: ${created_at}\n` : "") + `Subject: ${parsedEmail?.subject}\n` @@ -412,7 +475,8 @@ const parseMail = async ( export async function sendMailToTelegram( c: Context, address: string, parsedEmailContext: ParsedEmailContext, - message_id: string | null + message_id: string | null, + aiExtract?: ExtractResult | null ) { if (!c.env.TELEGRAM_BOT_TOKEN || !c.env.KV) { return; @@ -429,7 +493,9 @@ export async function sendMailToTelegram( const bot = newTelegramBot(c, c.env.TELEGRAM_BOT_TOKEN); const buildAndSend = async (targetUserId: string, msgs: LocaleMessages) => { - const { mail } = await parseMail(msgs, parsedEmailContext, address, new Date().toUTCString()); + const { mail } = await parseMail( + msgs, parsedEmailContext, address, new Date().toUTCString(), aiExtract + ); if (!mail) return; const attachments = parsedEmailContext.parsedEmail?.attachments || []; const buttons = [];