From 67c6c1f82c2061e39fcefa9659d3491df991f2e6 Mon Sep 17 00:00:00 2001 From: dreamhunter2333 Date: Thu, 17 Sep 2026 12:52:08 +0800 Subject: [PATCH] fix: fallback to local extract on ai allowlist miss --- CHANGELOG.md | 4 +- CHANGELOG_EN.md | 4 +- e2e/tests/api/telegram-ai-extract.spec.ts | 159 +++++++++++++++++- e2e/tests/unit/extract-code.test.mjs | 14 +- .../docs/en/guide/feature/ai-extract.md | 13 +- .../docs/zh/guide/feature/ai-extract.md | 13 +- worker/src/email/ai_extract.ts | 72 ++++---- worker/src/email/extract_mode.ts | 18 +- worker/wrangler.toml.template | 2 +- 9 files changed, 233 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cfbbe2c..fc243ee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ ### Features -- feat: |AI 识别| 新增 `AI_EXTRACT_MODE`,可显式选择仅用本地规则(`local`)或仅用 Workers AI(`ai`)识别邮件,两者不再互相回退;不填默认使用本地规则,邮件内容不会发送给 AI。**升级注意**:原先依赖 Workers AI 绑定自动启用 AI 识别的部署需设置 `AI_EXTRACT_MODE = "ai"` +- feat: |AI 识别| 新增 `AI_EXTRACT_MODE`,可显式选择仅用本地规则(`local`)或优先用 Workers AI(`ai`)识别邮件;不填默认使用本地规则,邮件内容不会发送给 AI。**升级注意**:原先依赖 Workers AI 绑定自动启用 AI 识别的部署需设置 `AI_EXTRACT_MODE = "ai"` ### Bug Fixes +- fix: |AI 识别| `ai` 模式下地址未命中 AI 提取白名单时只跳过 Workers AI 调用,仍回退到本地规则提取验证码 + ### Improvements - feat: |AI 识别| 本地验证码规则增强:同时识别邮件标题,支持验证码在关键词前(如 `116352(动态验证码)`、`ABC123 is your code`)、`G-123456` 前缀、分组 / 空格 / 零宽字符 / 全角数字,新增俄西葡法德意土希伯来语等关键词;排除超过 8 位数字、小数金额、时间、URL 与邮箱地址中的数字、tracking / order / voucher code 及纯字母单词,收紧无关键词时的数字识别,并限制分析长度、消除正则回溯风险 diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 8d9cab85..f43f84d4 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -10,10 +10,12 @@ ### Features -- feat: |AI Extract| Add `AI_EXTRACT_MODE` to explicitly choose local rules only (`local`) or Workers AI only (`ai`), with no fallback between them; defaults to local rules when unset so mail content is never sent to AI. **Upgrade note**: deployments that relied on the Workers AI binding to enable AI extraction automatically must set `AI_EXTRACT_MODE = "ai"` +- feat: |AI Extract| Add `AI_EXTRACT_MODE` to explicitly choose local rules only (`local`) or prefer Workers AI (`ai`); defaults to local rules when unset so mail content is never sent to AI. **Upgrade note**: deployments that relied on the Workers AI binding to enable AI extraction automatically must set `AI_EXTRACT_MODE = "ai"` ### Bug Fixes +- fix: |AI Extract| In `ai` mode, an address allowlist miss now skips only the Workers AI call and still falls back to local verification-code extraction + ### Improvements - feat: |AI Extract| Improve local verification-code rules: also read the mail subject; support codes before keywords (e.g. `116352(动态验证码)`, `ABC123 is your code`), `G-123456` prefixes, grouped / spaced / zero-width-split / full-width codes, and Russian, Spanish, Portuguese, French, German, Italian, Turkish and Hebrew keywords; reject numbers longer than 8 digits, decimals and amounts, times, digits in URLs and email addresses, tracking / order / voucher codes and letters-only words; only accept keyword-less numbers in stricter positions; bound input length and remove regex backtracking risks diff --git a/e2e/tests/api/telegram-ai-extract.spec.ts b/e2e/tests/api/telegram-ai-extract.spec.ts index e2c52401..93a08470 100644 --- a/e2e/tests/api/telegram-ai-extract.spec.ts +++ b/e2e/tests/api/telegram-ai-extract.spec.ts @@ -1,5 +1,7 @@ import { test, expect } from '@playwright/test'; -import { WORKER_URL, createTestAddress, deleteAddress } from '../../fixtures/test-helpers'; +import { WORKER_URL, WORKER_URL_ENV_OFF, createTestAddress, deleteAddress } from '../../fixtures/test-helpers'; + +const ADMIN_HEADERS = { 'x-admin-auth': 'e2e-admin-pass' }; test.describe('Telegram AI extraction rendering', () => { test('realtime mail stores AI extraction metadata for Telegram rendering', async ({ request }) => { @@ -102,4 +104,159 @@ test.describe('Telegram AI extraction rendering', () => { await deleteAddress(request, jwt); } }); + + test('ai allowlist only gates Workers AI and falls back to local rules on miss', async ({ request }) => { + const { jwt, address } = await createTestAddress(request, 'tg-ai-local-fallback'); + + try { + const offSettingsRes = await request.post(`${WORKER_URL}/admin/ai_extract/settings`, { + headers: ADMIN_HEADERS, + data: { + enableAllowList: false, + allowList: [], + }, + }); + expect(offSettingsRes.ok()).toBe(true); + + const aiRaw = [ + 'From: sender@test.example.com', + `To: ${address}`, + 'Subject: AI allowlist off', + `Message-ID: `, + 'MIME-Version: 1.0', + 'Content-Type: text/plain; charset=utf-8', + '', + 'Your verification code is: 593817', + ].join('\r\n'); + + const aiReceiveRes = await request.post(`${WORKER_URL}/__test/receive_mail`, { + data: { + from: 'sender@test.example.com', + to: address, + raw: aiRaw, + extract_mode: 'ai', + ai_extract_result: { + type: 'auth_link', + result: 'https://example.com/ai-used', + result_text: '', + }, + }, + }); + expect(aiReceiveRes.ok()).toBe(true); + expect((await aiReceiveRes.json()).success).toBe(true); + + const aiMailsRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, { + headers: { Authorization: `Bearer ${jwt}` }, + }); + expect(aiMailsRes.ok()).toBe(true); + const aiMailsBody = await aiMailsRes.json(); + expect(aiMailsBody.results[0].metadata).toBeTruthy(); + expect(JSON.parse(aiMailsBody.results[0].metadata).ai_extract).toEqual({ + type: 'auth_link', + result: 'https://example.com/ai-used', + result_text: '', + }); + + const onSettingsRes = await request.post(`${WORKER_URL}/admin/ai_extract/settings`, { + headers: ADMIN_HEADERS, + data: { + enableAllowList: true, + allowList: ['allowed@example.com'], + }, + }); + expect(onSettingsRes.ok()).toBe(true); + + const raw = [ + 'From: sender@test.example.com', + `To: ${address}`, + 'Subject: AI allowlist fallback', + `Message-ID: `, + 'MIME-Version: 1.0', + 'Content-Type: text/plain; charset=utf-8', + '', + 'Your verification code is: 593817', + ].join('\r\n'); + + const receiveRes = await request.post(`${WORKER_URL}/__test/receive_mail`, { + data: { + from: 'sender@test.example.com', + to: address, + raw, + extract_mode: 'ai', + // If Workers AI were called, this would win. An allowlist miss must + // skip AI and use local extraction instead. + ai_extract_result: { + type: 'auth_link', + result: 'https://example.com/should-not-be-used', + result_text: '', + }, + }, + }); + expect(receiveRes.ok()).toBe(true); + expect((await receiveRes.json()).success).toBe(true); + + const mailsRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, { + headers: { Authorization: `Bearer ${jwt}` }, + }); + expect(mailsRes.ok()).toBe(true); + const { results } = await mailsRes.json(); + expect(results.length).toBeGreaterThanOrEqual(2); + + const metadata = JSON.parse(results[0].metadata); + expect(metadata.ai_extract).toEqual({ + type: 'auth_code', + result: '593817', + result_text: '', + }); + } finally { + await request.post(`${WORKER_URL}/admin/ai_extract/settings`, { + headers: ADMIN_HEADERS, + data: { + enableAllowList: false, + allowList: [], + }, + }); + await deleteAddress(request, jwt); + } + }); + + test('env-off worker keeps extraction disabled without test overrides', async ({ request }) => { + test.skip(!WORKER_URL_ENV_OFF, 'WORKER_URL_ENV_OFF is not configured'); + + const { jwt, address, address_id } = await createTestAddress(request, 'tg-extract-off', 'test.example.com', WORKER_URL_ENV_OFF); + + try { + const raw = [ + 'From: sender@test.example.com', + `To: ${address}`, + 'Subject: Extraction disabled', + `Message-ID: `, + 'MIME-Version: 1.0', + 'Content-Type: text/plain; charset=utf-8', + '', + 'Your verification code is: 374829', + ].join('\r\n'); + + const receiveRes = await request.post(`${WORKER_URL_ENV_OFF}/__test/receive_mail`, { + data: { + from: 'sender@test.example.com', + to: address, + raw, + }, + }); + expect(receiveRes.ok()).toBe(true); + expect((await receiveRes.json()).success).toBe(true); + + const mailsRes = await request.get(`${WORKER_URL_ENV_OFF}/api/mails?limit=10&offset=0`, { + headers: { Authorization: `Bearer ${jwt}` }, + }); + expect(mailsRes.ok()).toBe(true); + const { results } = await mailsRes.json(); + expect(results).toHaveLength(1); + expect(results[0].metadata).toBeFalsy(); + } finally { + const deleteRes = await request.delete(`${WORKER_URL_ENV_OFF}/admin/delete_address/${address_id}`); + expect(deleteRes.ok()).toBe(true); + } + }); }); diff --git a/e2e/tests/unit/extract-code.test.mjs b/e2e/tests/unit/extract-code.test.mjs index 7a51a54a..81db2e15 100644 --- a/e2e/tests/unit/extract-code.test.mjs +++ b/e2e/tests/unit/extract-code.test.mjs @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { extractCode, joinSubjectAndBody } from '../../../worker/src/email/extract_code.ts'; -import { resolveExtractMode } from '../../../worker/src/email/extract_mode.ts'; +import { ExtractMode, resolveExtractMode } from '../../../worker/src/email/extract_mode.ts'; // Messages marked "2FHey" are adapted from https://github.com/SoFriendly/2fhey tests (CC0-1.0). const codeCases = [ @@ -161,15 +161,15 @@ for (const [name, text] of Object.entries(hostileInputs)) { } test('extract mode defaults to local when unset', () => { - assert.equal(resolveExtractMode(undefined), 'local'); - assert.equal(resolveExtractMode(''), 'local'); - assert.equal(resolveExtractMode(' '), 'local'); + assert.equal(resolveExtractMode(undefined), ExtractMode.Local); + assert.equal(resolveExtractMode(''), ExtractMode.Local); + assert.equal(resolveExtractMode(' '), ExtractMode.Local); }); test('extract mode accepts explicit ai and local', () => { - assert.equal(resolveExtractMode('ai'), 'ai'); - assert.equal(resolveExtractMode(' AI '), 'ai'); - assert.equal(resolveExtractMode('local'), 'local'); + assert.equal(resolveExtractMode('ai'), ExtractMode.Ai); + assert.equal(resolveExtractMode(' AI '), ExtractMode.Ai); + assert.equal(resolveExtractMode('local'), ExtractMode.Local); }); test('extract mode rejects unknown values', () => { diff --git a/vitepress-docs/docs/en/guide/feature/ai-extract.md b/vitepress-docs/docs/en/guide/feature/ai-extract.md index 8bdbe925..1ee391c4 100644 --- a/vitepress-docs/docs/en/guide/feature/ai-extract.md +++ b/vitepress-docs/docs/en/guide/feature/ai-extract.md @@ -29,16 +29,17 @@ Extraction results are automatically saved to the `metadata` field in the databa | Variable Name | Type | Description | Example | | -------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | `ENABLE_AI_EMAIL_EXTRACT` | Text/JSON | Whether to enable email recognition (master switch, required by both modes) | `true` | -| `AI_EXTRACT_MODE` | Text | Recognition mode: `local` uses built-in rules only, `ai` uses Workers AI only. Defaults to `local` when unset; any other value logs an error and skips recognition | `local` | +| `AI_EXTRACT_MODE` | Text | Recognition mode: `local` uses built-in rules only, `ai` prefers Workers AI. Defaults to `local` when unset; any other value logs an error and skips recognition | `local` | | `AI_EXTRACT_MODEL` | Text | `ai` mode only. AI model name, choose from [models supporting JSON mode](https://developers.cloudflare.com/workers-ai/features/json-mode/#supported-models) | `@cf/meta/llama-3.1-8b-instruct-fast` | > [!WARNING] Upgrading from older versions > Older versions automatically used AI recognition whenever a Workers AI binding was configured. Now, when `AI_EXTRACT_MODE` is unset, local rules are used by default. To keep using AI recognition, explicitly set `AI_EXTRACT_MODE = "ai"`. -The two modes **never fall back to each other**: +The two modes behave as follows: - `local` mode never calls AI, even if a Workers AI binding is configured - `ai` mode logs an error and skips recognition for that mail when the Workers AI binding is missing or the model call fails; it does not switch to local rules +- In `ai` mode, an address allowlist miss skips only the Workers AI call; local rules still run to try extracting verification codes ## Local Rule Mode (local) @@ -80,12 +81,12 @@ Or add in Cloudflare Dashboard Worker settings: ## Address Allowlist (Optional) -To control costs and resource usage, you can configure an address allowlist in the Admin console's **AI Extract Settings** page (applies to both `local` and `ai` modes): +To control costs and resource usage, you can configure an address allowlist in the Admin console's **AI Extract Settings** page. The allowlist controls only Workers AI calls, not local rule mode; in `ai` mode, addresses outside the allowlist still use local rules to try extracting verification codes. ### Configuration -- **Allowlist Disabled**: AI extraction will process all email addresses -- **Allowlist Enabled**: AI extraction will only process addresses in the allowlist +- **Allowlist Disabled**: Workers AI extraction can process all email addresses +- **Allowlist Enabled**: Workers AI is called only for addresses in the allowlist; addresses outside it skip Workers AI and fall back to local verification-code extraction ### Allowlist Format @@ -105,7 +106,7 @@ user@example.com admin*@company.com ``` -This configuration will only perform AI extraction for: +This configuration will only call Workers AI for: - `user@example.com` (exact match) - All emails under `@mydomain.com` (e.g., `test@mydomain.com`, `admin@mydomain.com`) - All emails starting with `admin` under `@company.com` (e.g., `admin@company.com`, `admin123@company.com`) diff --git a/vitepress-docs/docs/zh/guide/feature/ai-extract.md b/vitepress-docs/docs/zh/guide/feature/ai-extract.md index 1d42b04a..518dbc95 100644 --- a/vitepress-docs/docs/zh/guide/feature/ai-extract.md +++ b/vitepress-docs/docs/zh/guide/feature/ai-extract.md @@ -29,16 +29,17 @@ | 变量名 | 类型 | 说明 | 示例 | | ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- | | `ENABLE_AI_EMAIL_EXTRACT` | 文本/JSON | 是否启用邮件识别功能(总开关,两种模式都需要) | `true` | -| `AI_EXTRACT_MODE` | 文本 | 识别模式:`local` 仅用内置规则,`ai` 仅用 Workers AI。不填默认为 `local`,填写其他值会记录错误日志并跳过识别 | `local` | +| `AI_EXTRACT_MODE` | 文本 | 识别模式:`local` 仅用内置规则,`ai` 优先使用 Workers AI。不填默认为 `local`,填写其他值会记录错误日志并跳过识别 | `local` | | `AI_EXTRACT_MODEL` | 文本 | 仅 `ai` 模式生效。AI 模型名称,从[支持 JSON 模式的模型](https://developers.cloudflare.com/workers-ai/features/json-mode/#supported-models)中选择 | `@cf/meta/llama-3.1-8b-instruct-fast` | > [!WARNING] 从旧版本升级 > 旧版本在配置了 Workers AI 绑定时会自动使用 AI 识别。现在不填 `AI_EXTRACT_MODE` 时默认使用本地规则,如需继续使用 AI 识别,请显式设置 `AI_EXTRACT_MODE = "ai"`。 -两种模式之间**不会互相回退**: +两种模式的主要行为: - `local` 模式即使配置了 Workers AI 绑定,也不会调用 AI - `ai` 模式未配置 Workers AI 绑定或调用模型失败时,会记录错误日志并跳过本封邮件的识别,不会改用本地规则 +- `ai` 模式下如果地址未命中 AI 提取白名单,只会跳过 Workers AI 调用,仍会改用本地规则尝试提取验证码 ## 本地规则模式(local) @@ -80,12 +81,12 @@ binding = "AI" ## 地址白名单(可选) -为了控制成本和资源使用,可以在 Admin 控制台的 **AI 提取设置** 页面配置地址白名单(对 `local` 与 `ai` 两种模式都生效): +为了控制成本和资源使用,可以在 Admin 控制台的 **AI 提取设置** 页面配置地址白名单。白名单只控制 Workers AI 调用,不限制本地规则模式;`ai` 模式下未命中白名单的地址仍会使用本地规则尝试提取验证码。 ### 配置说明 -- **未启用白名单**:所有邮箱地址都可使用 AI 提取功能 -- **启用白名单**:仅白名单中的邮箱地址会进行 AI 提取 +- **未启用白名单**:所有邮箱地址都可使用 Workers AI 提取 +- **启用白名单**:仅白名单中的邮箱地址会调用 Workers AI;未命中的地址会跳过 Workers AI,并回退到本地验证码提取 ### 白名单格式 @@ -105,7 +106,7 @@ user@example.com admin*@company.com ``` -此配置将只对以下邮箱进行 AI 提取: +此配置将只对以下邮箱调用 Workers AI: - `user@example.com`(精确匹配) - 所有 `@mydomain.com` 的邮箱(如 `test@mydomain.com`、`admin@mydomain.com`) - 所有 `admin` 开头的 `@company.com` 邮箱(如 `admin@company.com`、`admin123@company.com`) diff --git a/worker/src/email/ai_extract.ts b/worker/src/email/ai_extract.ts index c84e0bcf..701705c9 100644 --- a/worker/src/email/ai_extract.ts +++ b/worker/src/email/ai_extract.ts @@ -8,7 +8,7 @@ import { commonParseMail } from "../common"; import { extractCode, joinSubjectAndBody } from "./extract_code"; -import { resolveExtractMode } from "./extract_mode"; +import { ExtractMode, resolveExtractMode } from "./extract_mode"; import { getBooleanValue, getJsonSetting } from "../utils"; import { CONSTANTS } from "../constants"; import { Context } from "hono"; @@ -227,12 +227,24 @@ function getEmailContentForExtract(parsedEmail: Awaited { + if (!pattern.includes('*')) return address === pattern; + const escapedPattern = pattern + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*'); + return new RegExp('^' + escapedPattern + '$').test(address); + }); +} + /** * Main extraction function * Checks if extraction is enabled, processes the email content, and saves to database. - * `AI_EXTRACT_MODE` selects exactly one extractor, with no fallback between them: + * `AI_EXTRACT_MODE` selects the preferred extractor: * - `local` (default): built-in rules, verification codes only, content never sent to AI - * - `ai`: Cloudflare Workers AI, verification codes and links + * - `ai`: Cloudflare Workers AI, verification codes and links; if the address is not + * in the AI allowlist, only the AI call is skipped and local code extraction still runs * * @param parsedEmailContext - The parsed email context * @param env - Cloudflare Workers environment bindings @@ -257,55 +269,41 @@ export async function extractEmailInfo( console.error(`Email extraction skipped: unsupported AI_EXTRACT_MODE "${env.AI_EXTRACT_MODE}", expected "local" or "ai"`); return null; } - if (mode === 'ai' && !env.AI) { - console.error('Email extraction skipped: AI_EXTRACT_MODE is "ai" but the Workers AI binding "AI" is not configured'); - return null; - } - - // Check allowlist if enabled (applies to both modes) const aiSettings = await getJsonSetting( { env: env } as Context, CONSTANTS.AI_EXTRACT_SETTINGS_KEY ); - - if (aiSettings?.enableAllowList && aiSettings.allowList?.length > 0) { - const isAllowed = aiSettings.allowList.some(pattern => { - // Support wildcard matching - if (pattern.includes('*')) { - // Escape special regex characters except * - const escapedPattern = pattern - .replace(/[.+?^${}()|[\]\\]/g, '\\$&') - .replace(/\*/g, '.*'); - const regex = new RegExp('^' + escapedPattern + '$'); - return regex.test(address); - } - // Exact match - return address === pattern; - }); - - if (!isAllowed) { - console.log(`Email extraction skipped for ${address}: not in allowlist`); - return null; - } - } + const isAiAllowed = isAddressInAiAllowlist(aiSettings, address); // Parse email to get content (shared by both modes) const parsedEmail = await commonParseMail(parsedEmailContext); const emailContent = getEmailContentForExtract(parsedEmail); - // Local mode: built-in rules only, mail content is never sent to any AI model. - // The subject is included because many services put the code there. - // Telegram / webhook reuse the same ExtractResult. - if (mode === 'local') { + const runLocalExtract = async () => { const localContent = joinSubjectAndBody(parsedEmail?.subject, emailContent); const code = localContent ? extractCode(localContent) : null; - if (!code) { - return null; - } + if (!code) return null; const result: ExtractResult = { type: 'auth_code', result: code, result_text: '' }; await saveExtractMetadata(env, message_id, result); console.log(`Local code extraction completed for ${message_id}`); return result; + }; + + // Local mode: built-in rules only, mail content is never sent to any AI model. + // The subject is included because many services put the code there. + // Telegram / webhook reuse the same ExtractResult. + if (mode === ExtractMode.Local) { + return await runLocalExtract(); + } + + if (!isAiAllowed) { + console.log(`Workers AI extraction skipped for ${address}: not in AI allowlist; trying local code extraction`); + return await runLocalExtract(); + } + + if (!env.AI) { + console.error('Email extraction skipped: AI_EXTRACT_MODE is "ai" but the Workers AI binding "AI" is not configured'); + return null; } if (!emailContent) { diff --git a/worker/src/email/extract_mode.ts b/worker/src/email/extract_mode.ts index 55618c57..1fb46933 100644 --- a/worker/src/email/extract_mode.ts +++ b/worker/src/email/extract_mode.ts @@ -3,10 +3,15 @@ * * - `local`: built-in rule-based extraction only; mail content is never sent * to any AI model. This is the default when the variable is unset. - * - `ai`: Workers AI only; no local fallback when the binding is missing or - * the model call fails. + * - `ai`: prefer Workers AI; when the AI allowlist misses, local code extraction + * still runs because it never sends mail content to AI. */ -export type ExtractMode = 'local' | 'ai'; +export const ExtractMode = { + Local: 'local', + Ai: 'ai', +} as const; + +export type ExtractMode = typeof ExtractMode[keyof typeof ExtractMode]; /** * Resolve the configured extraction mode. @@ -14,10 +19,11 @@ export type ExtractMode = 'local' | 'ai'; * @returns the mode, or null when the value is not a supported mode */ export function resolveExtractMode(value: unknown): ExtractMode | null { - if (value === undefined || value === null) return 'local'; + if (value === undefined || value === null) return ExtractMode.Local; if (typeof value !== 'string') return null; const normalized = value.trim().toLowerCase(); - if (normalized === '') return 'local'; - if (normalized === 'local' || normalized === 'ai') return normalized; + if (normalized === '') return ExtractMode.Local; + if (normalized === ExtractMode.Local) return ExtractMode.Local; + if (normalized === ExtractMode.Ai) return ExtractMode.Ai; return null; } diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template index d3c0dbf9..45c640cf 100644 --- a/worker/wrangler.toml.template +++ b/worker/wrangler.toml.template @@ -153,7 +153,7 @@ ENABLE_AUTO_REPLY = false # Email extraction, automatically extract verification codes, auth links, etc. # ENABLE_AI_EMAIL_EXTRACT = true # Extraction mode: "local" (default, built-in rules, verification codes only, mail never sent to AI) -# or "ai" (Workers AI only, codes and links, requires the [ai] binding, no local fallback) +# or "ai" (prefer Workers AI for codes and links; allowlist misses fall back to local code extraction) # AI_EXTRACT_MODE = "local" # AI model name, choose from https://developers.cloudflare.com/workers-ai/features/json-mode/#supported-models # Recommended JSON Mode model: "@cf/meta/llama-3.1-8b-instruct-fast"