feat: show AI extraction results in Telegram

Show AI extraction results in Telegram notifications and /mails views.
This commit is contained in:
Wolf-L
2026-05-29 01:13:31 +08:00
committed by GitHub
parent 44b29aa646
commit 308fbe2f9a
6 changed files with 101 additions and 14 deletions
+8 -5
View File
@@ -151,17 +151,17 @@ export async function extractEmailInfo(
env: Bindings, env: Bindings,
message_id: string | null, message_id: string | null,
address: string address: string
): Promise<void> { ): Promise<ExtractResult | null> {
try { try {
// Check if AI extraction is enabled via environment variable // Check if AI extraction is enabled via environment variable
if (!getBooleanValue(env.ENABLE_AI_EMAIL_EXTRACT)) { if (!getBooleanValue(env.ENABLE_AI_EMAIL_EXTRACT)) {
return; return null;
} }
// Ensure AI binding is available // Ensure AI binding is available
if (!env.AI) { if (!env.AI) {
console.error('AI binding not available'); console.error('AI binding not available');
return; return null;
} }
// Check allowlist if enabled // Check allowlist if enabled
@@ -187,7 +187,7 @@ export async function extractEmailInfo(
if (!isAllowed) { if (!isAllowed) {
console.log(`AI extraction skipped for ${address}: not in allowlist`); 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 || ""; const emailContent = parsedEmail?.text || parsedEmail?.html || "";
if (!emailContent) { if (!emailContent) {
return; return null;
} }
// Truncate content if too long (max 4000 characters to avoid token limits) // 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(); ).bind(metadata, message_id).run();
console.log(`AI extraction completed for ${message_id}: ${result.type}`); console.log(`AI extraction completed for ${message_id}: ${result.type}`);
return result;
} }
return result;
} catch (e) { } catch (e) {
console.error('AI email extraction error:', e); console.error('AI email extraction error:', e);
return null;
} }
} }
+4 -4
View File
@@ -122,11 +122,14 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
// forward email // forward email
await forwardEmail(message, env); await forwardEmail(message, env);
// AI email content extraction
const aiExtractResult = await extractEmailInfo(parsedEmailContext, env, message_id, toAddress);
// send email to telegram // send email to telegram
try { try {
await sendMailToTelegram( await sendMailToTelegram(
{ env: env } as Context<HonoCustomType>, { env: env } as Context<HonoCustomType>,
toAddress, parsedEmailContext, message_id); toAddress, parsedEmailContext, message_id, aiExtractResult);
} catch (error) { } catch (error) {
console.error("send mail to telegram error", 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 // auto reply email
await auto_reply(message, env, toAddress); await auto_reply(message, env, toAddress);
// AI email content extraction
await extractEmailInfo(parsedEmailContext, env, message_id, toAddress);
} }
export { email } export { email }
+6
View File
@@ -174,6 +174,12 @@ const messages: LocaleMessages = {
TgNoPermissionViewMailMsg: "No permission to view this mail", TgNoPermissionViewMailMsg: "No permission to view this mail",
TgBotTokenRequiredMsg: "TELEGRAM_BOT_TOKEN is required", TgBotTokenRequiredMsg: "TELEGRAM_BOT_TOKEN is required",
TgLangFeatureDisabledMsg: "Language setting feature is disabled. System default language is used.", 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; export default messages;
+6
View File
@@ -172,4 +172,10 @@ export type LocaleMessages = {
TgNoPermissionViewMailMsg: string TgNoPermissionViewMailMsg: string
TgBotTokenRequiredMsg: string TgBotTokenRequiredMsg: string
TgLangFeatureDisabledMsg: string TgLangFeatureDisabledMsg: string
TgAiExtractResultMsg: string
TgAiExtractAuthCodeMsg: string
TgAiExtractAuthLinkMsg: string
TgAiExtractServiceLinkMsg: string
TgAiExtractSubscriptionLinkMsg: string
TgAiExtractOtherLinkMsg: string
} }
+6
View File
@@ -174,6 +174,12 @@ const messages: LocaleMessages = {
TgNoPermissionViewMailMsg: "无权查看此邮件", TgNoPermissionViewMailMsg: "无权查看此邮件",
TgBotTokenRequiredMsg: "需要设置 TELEGRAM_BOT_TOKEN", TgBotTokenRequiredMsg: "需要设置 TELEGRAM_BOT_TOKEN",
TgLangFeatureDisabledMsg: "语言设置功能已禁用,使用系统默认语言", TgLangFeatureDisabledMsg: "语言设置功能已禁用,使用系统默认语言",
TgAiExtractResultMsg: "AI 提取",
TgAiExtractAuthCodeMsg: "验证码",
TgAiExtractAuthLinkMsg: "验证链接",
TgAiExtractServiceLinkMsg: "服务链接",
TgAiExtractSubscriptionLinkMsg: "订阅链接",
TgAiExtractOtherLinkMsg: "其他链接",
} }
export default messages; export default messages;
+71 -5
View File
@@ -14,6 +14,7 @@ import { RawMailRow } from "../models";
import { UserFromGetMe } from "telegraf/types"; import { UserFromGetMe } from "telegraf/types";
import i18n from "../i18n"; import i18n from "../i18n";
import { LocaleMessages } from "../i18n/type"; import { LocaleMessages } from "../i18n/type";
import type { ExtractResult } from "../email/ai_extract";
// Helper to get messages by userId // 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<HonoCustomType>) => { export const getTelegramCommands = (c: Context<HonoCustomType>) => {
return getBooleanValue(c.env.TG_ALLOW_USER_LANG) return getBooleanValue(c.env.TG_ALLOW_USER_LANG)
? COMMANDS ? COMMANDS
@@ -312,7 +369,10 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
const raw = mailRow ? await resolveRawEmail(mailRow) : undefined; const raw = mailRow ? await resolveRawEmail(mailRow) : undefined;
const mailId = mailRow?.id; const mailId = mailRow?.id;
const created_at = mailRow?.created_at; 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<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json"); const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
const miniAppButtons = [] const miniAppButtons = []
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) { if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
@@ -381,7 +441,9 @@ export async function initTelegramBotCommands(c: Context<HonoCustomType>, bot: T
const parseMail = async ( const parseMail = async (
msgs: LocaleMessages, msgs: LocaleMessages,
parsedEmailContext: ParsedEmailContext, parsedEmailContext: ParsedEmailContext,
address: string, created_at: string | undefined | null address: string,
created_at: string | undefined | null,
aiExtract?: ExtractResult | null
) => { ) => {
if (!parsedEmailContext.rawEmail) { if (!parsedEmailContext.rawEmail) {
return {}; return {};
@@ -394,7 +456,8 @@ const parseMail = async (
} }
return { return {
isHtml: false, isHtml: false,
mail: `From: ${parsedEmail?.sender || msgs.TgNoSenderMsg}\n` mail: formatAiExtractForTelegram(msgs, aiExtract)
+ `From: ${parsedEmail?.sender || msgs.TgNoSenderMsg}\n`
+ `To: ${address}\n` + `To: ${address}\n`
+ (created_at ? `Date: ${created_at}\n` : "") + (created_at ? `Date: ${created_at}\n` : "")
+ `Subject: ${parsedEmail?.subject}\n` + `Subject: ${parsedEmail?.subject}\n`
@@ -412,7 +475,8 @@ const parseMail = async (
export async function sendMailToTelegram( export async function sendMailToTelegram(
c: Context<HonoCustomType>, address: string, c: Context<HonoCustomType>, address: string,
parsedEmailContext: ParsedEmailContext, parsedEmailContext: ParsedEmailContext,
message_id: string | null message_id: string | null,
aiExtract?: ExtractResult | null
) { ) {
if (!c.env.TELEGRAM_BOT_TOKEN || !c.env.KV) { if (!c.env.TELEGRAM_BOT_TOKEN || !c.env.KV) {
return; return;
@@ -429,7 +493,9 @@ export async function sendMailToTelegram(
const bot = newTelegramBot(c, c.env.TELEGRAM_BOT_TOKEN); const bot = newTelegramBot(c, c.env.TELEGRAM_BOT_TOKEN);
const buildAndSend = async (targetUserId: string, msgs: LocaleMessages) => { 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; if (!mail) return;
const attachments = parsedEmailContext.parsedEmail?.attachments || []; const attachments = parsedEmailContext.parsedEmail?.attachments || [];
const buttons = []; const buttons = [];