mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-17 13:24:13 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67c6c1f82c |
+3
-1
@@ -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 及纯字母单词,收紧无关键词时的数字识别,并限制分析长度、消除正则回溯风险
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -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: <ai-allowlist-off-${Date.now()}@test>`,
|
||||
'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: <ai-allowlist-fallback-${Date.now()}@test>`,
|
||||
'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: <extract-off-${Date.now()}@test>`,
|
||||
'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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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<ReturnType<typeof common
|
||||
return htmlToTextForAi(parsedEmail.html) || parsedEmail.html;
|
||||
}
|
||||
|
||||
function isAddressInAiAllowlist(settings: AiExtractSettings | null | undefined, address: string): boolean {
|
||||
if (!settings?.enableAllowList || !settings.allowList?.length) return true;
|
||||
return settings.allowList.some(pattern => {
|
||||
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<AiExtractSettings>(
|
||||
{ env: env } as Context<HonoCustomType>,
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user