feat: AI verification code recognition settings

This commit is contained in:
eoao
2026-05-10 02:17:42 +08:00
parent baa70a863c
commit 92e18ea683
8 changed files with 119 additions and 4 deletions

View File

@@ -148,6 +148,9 @@ const en = {
sendEmail: 'Send Email', sendEmail: 'Send Email',
resendToken: 'Resend Token', resendToken: 'Resend Token',
blackList: 'Blocklist', blackList: 'Blocklist',
codeRecognition: 'Verification Code Recognition',
codeRecognitionRules: 'Recognition Rules',
codeRecognitionRulesDesc: 'Only recognize emails containing the following info. Empty means all.',
oss: 'Object Storage', oss: 'Object Storage',
osDomain: 'Domain', osDomain: 'Domain',
ossDomainDesc: 'Leave empty if using KV storage.', ossDomainDesc: 'Leave empty if using KV storage.',
@@ -315,6 +318,7 @@ const en = {
blackSubjectDesc: 'Subject', blackSubjectDesc: 'Subject',
blackContentDesc: 'Content', blackContentDesc: 'Content',
blackFromDesc: 'Sender or domain', blackFromDesc: 'Sender or domain',
senderRules: 'Sender address or domain',
blackListDesc: 'Reject emails containing the following information', blackListDesc: 'Reject emails containing the following information',
setSuccess: 'Settings saved successfully', setSuccess: 'Settings saved successfully',
details: 'Details', details: 'Details',

View File

@@ -148,6 +148,9 @@ const zh = {
sendEmail: '邮件发送', sendEmail: '邮件发送',
resendToken: 'Resend Token', resendToken: 'Resend Token',
blackList: '邮件黑名单', blackList: '邮件黑名单',
codeRecognition: '识别验证码',
codeRecognitionRules: '识别规则',
codeRecognitionRulesDesc: '只识别包含以下信息的邮件,为空识别全部',
oss: '对象存储', oss: '对象存储',
osDomain: '访问域名', osDomain: '访问域名',
ossDomainDesc: '如果是KV存储不要填', ossDomainDesc: '如果是KV存储不要填',
@@ -315,6 +318,7 @@ const zh = {
blackSubjectDesc: '邮件标题', blackSubjectDesc: '邮件标题',
blackContentDesc: '邮件内容', blackContentDesc: '邮件内容',
blackFromDesc: '发件人邮箱或域名', blackFromDesc: '发件人邮箱或域名',
senderRules: '发件人邮箱或域名',
blackListDesc: '拒收包含以下信息的邮件', blackListDesc: '拒收包含以下信息的邮件',
setSuccess: '设置成功', setSuccess: '设置成功',
details: '详情', details: '详情',

View File

@@ -361,6 +361,27 @@
</div> </div>
</div> </div>
<div class="settings-card">
<div class="card-title">Workers AI</div>
<div class="card-content">
<div class="setting-item">
<div><span>{{ $t('codeRecognition') }}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.aiCode"/>
</div>
</div>
<div class="setting-item">
<div><span>{{ $t('codeRecognitionRules') }}</span></div>
<div class="forward">
<el-button class="opt-button" size="small" type="primary" @click="openAiCodeFilter">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
</div>
</div>
</div>
</div>
<div class="settings-card about"> <div class="settings-card about">
<div class="card-title">{{ $t('about') }}</div> <div class="card-title">{{ $t('about') }}</div>
<div class="card-content"> <div class="card-content">
@@ -759,6 +780,22 @@
</el-form> </el-form>
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveBlackList">{{ $t('save') }}</el-button> <el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveBlackList">{{ $t('save') }}</el-button>
</el-dialog> </el-dialog>
<el-dialog v-model="aiCodeFilterShow" class="forward-dialog" @closed="resetAiCodeFilter">
<template #header>
<div class="forward-head">
<span class="forward-set-title">{{ $t('codeRecognitionRules') }}</span>
<el-tooltip effect="dark" :content="$t('codeRecognitionRulesDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
</template>
<el-form>
<el-form-item :label="t('senderRules')" label-position="top">
<el-input-tag v-model="aiCodeFilter" @add-tag="aiCodeFilterAddTag"/>
</el-form-item>
</el-form>
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveAiCodeFilter">{{ $t('save') }}</el-button>
</el-dialog>
</el-scrollbar> </el-scrollbar>
</div> </div>
</template> </template>
@@ -797,6 +834,7 @@ const userStore = useUserStore();
const editTitleShow = ref(false) const editTitleShow = ref(false)
const resendTokenFormShow = ref(false) const resendTokenFormShow = ref(false)
const blackFormShow = ref(false) const blackFormShow = ref(false)
const aiCodeFilterShow = ref(false)
const r2DomainShow = ref(false) const r2DomainShow = ref(false)
const turnstileShow = ref(false) const turnstileShow = ref(false)
const tgSettingShow = ref(false) const tgSettingShow = ref(false)
@@ -864,6 +902,7 @@ const blackListForm = ref({
blackContent: [], blackContent: [],
blackFrom: [] blackFrom: []
}) })
const aiCodeFilter = ref([])
const authRefreshOptions = computed(() => [ const authRefreshOptions = computed(() => [
{label: t('disable'), value: 0}, {label: t('disable'), value: 0},
@@ -913,6 +952,7 @@ function getSettings() {
resetAddS3Form() resetAddS3Form()
resetEmailPrefix() resetEmailPrefix()
resetBlackList() resetBlackList()
resetAiCodeFilter()
}) })
} }
@@ -1192,6 +1232,11 @@ function resetBlackList() {
blackListForm.value.blackContent = setting.value.blackContent ? setting.value.blackContent.split(',') : [] blackListForm.value.blackContent = setting.value.blackContent ? setting.value.blackContent.split(',') : []
blackListForm.value.blackSubject = setting.value.blackSubject ? setting.value.blackSubject.split(',') : [] blackListForm.value.blackSubject = setting.value.blackSubject ? setting.value.blackSubject.split(',') : []
} }
function resetAiCodeFilter() {
aiCodeFilter.value = setting.value.aiCodeFilter ? setting.value.aiCodeFilter.split(',') : []
}
function saveEmailPrefix() { function saveEmailPrefix() {
const form = {} const form = {}
form.minEmailPrefix = minEmailPrefix.value form.minEmailPrefix = minEmailPrefix.value
@@ -1199,6 +1244,10 @@ function saveEmailPrefix() {
editSetting(form, true) editSetting(form, true)
} }
function saveAiCodeFilter() {
editSetting({aiCodeFilter: aiCodeFilter.value + ''})
}
const opacityChange = debounce(doOpacityChange, 1000, { const opacityChange = debounce(doOpacityChange, 1000, {
leading: false, leading: false,
trailing: true trailing: true
@@ -1241,6 +1290,20 @@ function banEmailAddTag(val) {
}) })
} }
function aiCodeFilterAddTag(val) {
const emails = Array.from(new Set(
val.split(/[,]/).map(item => item.trim()).filter(item => item)
));
aiCodeFilter.value.splice(aiCodeFilter.value.length - 1, 1)
emails.forEach(email => {
if ((isEmail(email) || isDomain(email)) && !aiCodeFilter.value.includes(email)) {
aiCodeFilter.value.push(email)
}
})
}
function delBackground() { function delBackground() {
ElMessageBox.confirm(t('delBackgroundConfirm'), { ElMessageBox.confirm(t('delBackgroundConfirm'), {
@@ -1331,6 +1394,10 @@ function openBlackListForm() {
blackFormShow.value = true blackFormShow.value = true
} }
function openAiCodeFilter() {
aiCodeFilterShow.value = true
}
function saveResendToken() { function saveResendToken() {
const settingForm = { const settingForm = {
resendTokens: {} resendTokens: {}
@@ -1408,6 +1475,7 @@ function editSetting(settingForm, refreshStatus = true) {
noticePopupShow.value = false noticePopupShow.value = false
addS3Show.value = false addS3Show.value = false
emailPrefixShow.value = false emailPrefixShow.value = false
aiCodeFilterShow.value = false
}).catch((e) => { }).catch((e) => {
loginOpacity.value = setting.value.loginOpacity loginOpacity.value = setting.value.loginOpacity
setting.value = {...setting.value, ...JSON.parse(backup)} setting.value = {...setting.value, ...JSON.parse(backup)}

View File

@@ -128,6 +128,10 @@ export const settingConst = {
OPEN: 0, OPEN: 0,
CLOSE: 1 CLOSE: 1
}, },
aiCode: {
OPEN: 0,
CLOSE: 1
},
authRefresh: { authRefresh: {
OPEN: 1, OPEN: 1,
CLOSE: 0 CLOSE: 0

View File

@@ -28,7 +28,9 @@ export async function email(message, env, ctx) {
noRecipient, noRecipient,
blackSubject, blackSubject,
blackContent, blackContent,
blackFrom blackFrom,
aiCode,
aiCodeFilter
} = await settingService.query({ env }); } = await settingService.query({ env });
if (receive === settingConst.receive.CLOSE) { if (receive === settingConst.receive.CLOSE) {
@@ -90,7 +92,7 @@ export async function email(message, env, ctx) {
} }
const toName = email.to.find(item => item.address === message.to)?.name || ''; const toName = email.to.find(item => item.address === message.to)?.name || '';
const code = await aiService.extractCode({ env }, email); const code = aiCode === settingConst.aiCode.OPEN && checkAiCodeFilter(aiCodeFilter, email) ? await aiService.extractCode({ env }, email) : '';
const params = { const params = {
toEmail: message.to, toEmail: message.to,
@@ -183,6 +185,19 @@ export async function email(message, env, ctx) {
} }
} }
function checkAiCodeFilter(aiCodeFilterStr, email) {
const filterList = aiCodeFilterStr ? aiCodeFilterStr.split(',').map(item => item.trim().toLowerCase()).filter(Boolean) : [];
if (filterList.length === 0) {
return true;
}
const fromEmail = (email.from?.address || '').trim().toLowerCase();
const fromDomain = emailUtils.getDomain(fromEmail).toLowerCase();
return filterList.some(item => item === fromEmail || item === fromDomain);
}
function checkBlock(blackSubjectStr, blackContentStr, blackFromStr, email) { function checkBlock(blackSubjectStr, blackContentStr, blackFromStr, email) {
const blackFromList = blackFromStr ? blackFromStr.split(',') : [] const blackFromList = blackFromStr ? blackFromStr.split(',') : []

View File

@@ -49,6 +49,8 @@ export const setting = sqliteTable('setting', {
emailPrefixFilter: text('email_prefix_filter').default('').notNull(), emailPrefixFilter: text('email_prefix_filter').default('').notNull(),
blackSubject: text('black_subject').default('').notNull(), blackSubject: text('black_subject').default('').notNull(),
blackContent: text('black_content').default('').notNull(), blackContent: text('black_content').default('').notNull(),
blackFrom: text('black_from').default('').notNull() blackFrom: text('black_from').default('').notNull(),
aiCode: integer('ai_code').default(1).notNull(),
aiCodeFilter: text('ai_code_filter').default('').notNull()
}); });
export default setting export default setting

View File

@@ -49,6 +49,18 @@ const dbInit = {
} catch (e) { } catch (e) {
console.warn(`跳过字段:${e.message}`); console.warn(`跳过字段:${e.message}`);
} }
try {
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN ai_code INTEGER NOT NULL DEFAULT 1;`).run();
} catch (e) {
console.warn(`跳过字段:${e.message}`);
}
try {
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN ai_code_filter TEXT NOT NULL DEFAULT '';`).run();
} catch (e) {
console.warn(`跳过字段:${e.message}`);
}
}, },
async v2_9DB(c) { async v2_9DB(c) {
@@ -592,7 +604,9 @@ const dbInit = {
title TEXT NOT NULL, title TEXT NOT NULL,
auto_refresh INTEGER NOT NULL, auto_refresh INTEGER NOT NULL,
register_verify INTEGER NOT NULL, register_verify INTEGER NOT NULL,
add_email_verify INTEGER NOT NULL add_email_verify INTEGER NOT NULL,
ai_code INTEGER NOT NULL DEFAULT 1,
ai_code_filter TEXT NOT NULL DEFAULT ''
) )
`).run(); `).run();

View File

@@ -133,6 +133,10 @@ const settingService = {
params.emailPrefixFilter = params.emailPrefixFilter + ''; params.emailPrefixFilter = params.emailPrefixFilter + '';
} }
if (Array.isArray(params.aiCodeFilter)) {
params.aiCodeFilter = params.aiCodeFilter + '';
}
params.resendTokens = JSON.stringify(resendTokens); params.resendTokens = JSON.stringify(resendTokens);
await orm(c).update(setting).set({ ...params }).returning().get(); await orm(c).update(setting).set({ ...params }).returning().get();
await this.refresh(c); await this.refresh(c);