Email sending tool (#3837)

### What problem does this PR solve?

_Briefly describe what this PR aims to solve. Include background context
that will help reviewers understand the purpose of the PR._
Added the function of sending emails through SMTP
Instructions for use-
Corresponding parameters need to be configured
Need to output upstream in a fixed format

![image](https://github.com/user-attachments/assets/93bc1af7-6d4f-4406-bd1d-bc042535dd82)


### Type of change


- [√] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
This commit is contained in:
小黑马
2024-12-04 11:21:17 +08:00
committed by GitHub
parent 285bc58364
commit efae7afd62
12 changed files with 426 additions and 0 deletions

View File

@ -0,0 +1 @@
<svg t="1733148906323" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2383" width="200" height="200"><path d="M883.36 784H49.76a32 32 0 0 1-32-32V176a32 32 0 0 1 32-32h832a32 32 0 0 1 32 32l1.76 576a32 32 0 0 1-32.16 32z" fill="#FAEFDE" p-id="2384"></path><path d="M913.76 204.32l-448 320L17.76 208v-16c0-17.6 14.4-48 32-48h832c17.6 0 32 26.72 32 44.32z" fill="#FFF7F0" p-id="2385"></path><path d="M897.76 784h-864c-8.8 0-16-3.52-16-12.32V768a64 64 0 0 1 64-64h769.6a64 64 0 0 1 64 64v3.68c0 8.8-8.8 12.32-17.6 12.32z" fill="#EFD8BE" p-id="2386"></path><path d="M816.8 752m-192 0a192 192 0 1 0 384 0 192 192 0 1 0-384 0Z" fill="#72CAAF" p-id="2387"></path><path d="M48 140.32h833.76a32 32 0 0 1 32 32 35.36 35.36 0 0 1-32 35.68h-832A36.8 36.8 0 0 1 16 172.32a32 32 0 0 1 32-32z" fill="#FFFFFF" p-id="2388"></path><path d="M144.8 674.72a16 16 0 0 0-16 16v32a16 16 0 1 0 32 0v-32a16 16 0 0 0-16-16zM64.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM224.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM304.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM384.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM464.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16zM544.8 674.72a16 16 0 0 0-16 16v32a16 16 0 0 0 32 0v-32a16 16 0 0 0-16-16z" fill="#8D6C9F" p-id="2389"></path><path d="M928.8 576.96V178.72a48 48 0 0 0-48-48h-832a48 48 0 0 0-48 48v576a48 48 0 0 0 48 48h566.24a208 208 0 1 0 313.76-225.76z m-880-414.24h832a16 16 0 0 1 16 16v19.52l-413.12 299.2a32 32 0 0 1-37.6 0L32.8 198.24v-19.52a16 16 0 0 1 16-16z m0 608a16 16 0 0 1-16-16v-86.88L314.08 496a16 16 0 0 0-16-27.36L32.8 630.24V237.76l394.56 285.6a64 64 0 0 0 75.04 0l394.4-285.6V560a208 208 0 0 0-131.2-9.6l-133.28-81.28a16 16 0 0 0-21.92 5.28 16 16 0 0 0 5.28 21.6l112 67.84A208 208 0 0 0 608.8 752a183.68 183.68 0 0 0 0.96 18.72z m768 157.28a176 176 0 0 1-168.64-125.28 161.76 161.76 0 0 1-6.24-32 147.2 147.2 0 0 1-1.12-18.72 176 176 0 0 1 120.16-166.88 181.6 181.6 0 0 1 46.88-9.12h8.96a174.72 174.72 0 0 1 80 19.2 164.8 164.8 0 0 1 32 20.96 176 176 0 0 1-112 311.84z" fill="#8D6C9F" p-id="2390"></path><path d="M860.16 660.64a16 16 0 0 0-22.56 22.56l52.64 52.8h-57.44a112 112 0 0 0-112 112 16 16 0 0 0 32 0 80 80 0 0 1 80-80h57.44l-52.64 52.64a16 16 0 1 0 22.56 22.56l80-80a16 16 0 0 0 0-22.56z" fill="#F9EFDE" p-id="2391"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1050,6 +1050,31 @@ When you want to search the given knowledge base at first place, set a higher pa
template: 'Template',
templateDescription:
'This component is used for typesetting the outputs of various components.',
emailComponent: 'Email',
emailDescription: 'Send email to specified address',
smtpServer: 'SMTP Server',
smtpPort: 'SMTP Port',
senderEmail: 'Sender Email',
authCode: 'Authorization Code',
senderName: 'Sender Name',
toEmail: 'Recipient Email',
ccEmail: 'CC Email',
emailSubject: 'Subject',
emailContent: 'Content',
smtpServerRequired: 'Please input SMTP server address',
senderEmailRequired: 'Please input sender email',
authCodeRequired: 'Please input authorization code',
toEmailRequired: 'Please input recipient email',
emailContentRequired: 'Please input email content',
emailSentSuccess: 'Email sent successfully',
emailSentFailed: 'Failed to send email',
dynamicParameters: 'Dynamic Parameters',
jsonFormatTip:
'Upstream component should provide JSON string in following format:',
toEmailTip: 'to_email: Recipient email (Required)',
ccEmailTip: 'cc_email: CC email (Optional)',
subjectTip: 'subject: Email subject (Optional)',
contentTip: 'content: Email content (Optional)',
},
footer: {
profile: 'All rights reserved @ React',

View File

@ -1029,6 +1029,30 @@ export default {
testRun: '试运行',
template: '模板转换',
templateDescription: '该组件用于排版各种组件的输出。',
emailComponent: '邮件',
emailDescription: '发送邮件到指定邮箱',
smtpServer: 'SMTP服务器',
smtpPort: 'SMTP端口',
senderEmail: '发件人邮箱',
authCode: '授权码',
senderName: '发件人名称',
toEmail: '收件人邮箱',
ccEmail: '抄送邮箱',
emailSubject: '邮件主题',
emailContent: '邮件内容',
smtpServerRequired: '请输入SMTP服务器地址',
senderEmailRequired: '请输入发件人邮箱',
authCodeRequired: '请输入授权码',
toEmailRequired: '请输入收件人邮箱',
emailContentRequired: '请输入邮件内容',
emailSentSuccess: '邮件发送成功',
emailSentFailed: '邮件发送失败',
dynamicParameters: '动态参数说明',
jsonFormatTip: '上游组件需要传入以下格式的JSON字符串:',
toEmailTip: 'to_email: 收件人邮箱(必填)',
ccEmailTip: 'cc_email: 抄送邮箱(可选)',
subjectTip: 'subject: 邮件主题(可选)',
contentTip: 'content: 邮件内容(可选)',
},
footer: {
profile: 'All rights reserved @ React',

View File

@ -25,6 +25,7 @@ import styles from './index.less';
import { RagNode } from './node';
import { BeginNode } from './node/begin-node';
import { CategorizeNode } from './node/categorize-node';
import { EmailNode } from './node/email-node';
import { GenerateNode } from './node/generate-node';
import { InvokeNode } from './node/invoke-node';
import { KeywordNode } from './node/keyword-node';
@ -52,6 +53,7 @@ const nodeTypes = {
keywordNode: KeywordNode,
invokeNode: InvokeNode,
templateNode: TemplateNode,
emailNode: EmailNode,
};
const edgeTypes = {

View File

@ -0,0 +1,78 @@
import { Flex } from 'antd';
import classNames from 'classnames';
import { useState } from 'react';
import { Handle, NodeProps, Position } from 'reactflow';
import { NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function EmailNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const [showDetails, setShowDetails] = useState(false);
return (
<section
className={classNames(styles.ragNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<Flex vertical gap={8} className={styles.emailNodeContainer}>
<div
className={styles.emailConfig}
onClick={() => setShowDetails(!showDetails)}
>
<div className={styles.configItem}>
<span className={styles.configLabel}>SMTP:</span>
<span className={styles.configValue}>{data.form?.smtp_server}</span>
</div>
<div className={styles.configItem}>
<span className={styles.configLabel}>Port:</span>
<span className={styles.configValue}>{data.form?.smtp_port}</span>
</div>
<div className={styles.configItem}>
<span className={styles.configLabel}>From:</span>
<span className={styles.configValue}>{data.form?.email}</span>
</div>
<div className={styles.expandIcon}>{showDetails ? '▼' : '▶'}</div>
</div>
{showDetails && (
<div className={styles.jsonExample}>
<div className={styles.jsonTitle}>Expected Input JSON:</div>
<pre className={styles.jsonContent}>
{`{
"to_email": "...",
"cc_email": "...",
"subject": "...",
"content": "..."
}`}
</pre>
</div>
)}
</Flex>
</section>
);
}

View File

@ -193,3 +193,80 @@
.conditionLine;
}
}
.emailNodeContainer {
padding: 8px;
font-size: 12px;
.emailConfig {
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
padding: 8px;
position: relative;
cursor: pointer;
&:hover {
background: rgba(0, 0, 0, 0.04);
}
.configItem {
display: flex;
align-items: center;
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
.configLabel {
color: #666;
width: 45px;
flex-shrink: 0;
}
.configValue {
color: #333;
word-break: break-all;
}
}
.expandIcon {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
color: #666;
font-size: 12px;
}
}
.jsonExample {
background: #f5f5f5;
border-radius: 4px;
padding: 8px;
margin-top: 4px;
animation: slideDown 0.2s ease-out;
.jsonTitle {
color: #666;
margin-bottom: 4px;
}
.jsonContent {
margin: 0;
color: #333;
font-family: monospace;
}
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -8,6 +8,7 @@ import { ReactComponent as ConcentratorIcon } from '@/assets/svg/concentrator.sv
import { ReactComponent as CrawlerIcon } from '@/assets/svg/crawler.svg';
import { ReactComponent as DeepLIcon } from '@/assets/svg/deepl.svg';
import { ReactComponent as DuckIcon } from '@/assets/svg/duck.svg';
import { ReactComponent as EmailIcon } from '@/assets/svg/email.svg';
import { ReactComponent as ExeSqlIcon } from '@/assets/svg/exesql.svg';
import { ReactComponent as GithubIcon } from '@/assets/svg/github.svg';
import { ReactComponent as GoogleScholarIcon } from '@/assets/svg/google-scholar.svg';
@ -25,6 +26,8 @@ import { ReactComponent as WenCaiIcon } from '@/assets/svg/wencai.svg';
import { ReactComponent as WikipediaIcon } from '@/assets/svg/wikipedia.svg';
import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.svg';
// 邮件功能
import { variableEnabledFieldMap } from '@/constants/chat';
import i18n from '@/locales/config';
@ -87,6 +90,7 @@ export enum Operator {
Crawler = 'Crawler',
Invoke = 'Invoke',
Template = 'Template',
Email = 'Email',
}
export const CommonOperatorList = Object.values(Operator).filter(
@ -127,6 +131,7 @@ export const operatorIconMap = {
[Operator.Crawler]: CrawlerIcon,
[Operator.Invoke]: InvokeIcon,
[Operator.Template]: TemplateIcon,
[Operator.Email]: EmailIcon,
};
export const operatorMap: Record<
@ -259,6 +264,7 @@ export const operatorMap: Record<
[Operator.Template]: {
backgroundColor: '#dee0e2',
},
[Operator.Email]: { backgroundColor: '#e6f7ff' },
};
export const componentMenuList = [
@ -358,6 +364,9 @@ export const componentMenuList = [
{
name: Operator.Invoke,
},
{
name: Operator.Email,
},
];
const initialQueryBaseValues = {
@ -580,6 +589,18 @@ export const initialTemplateValues = {
parameters: [],
};
export const initialEmailValues = {
smtp_server: '',
smtp_port: 587,
email: '',
password: '',
sender_name: '',
to_email: '',
cc_email: '',
subject: '',
content: '',
};
export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 },
{ top: 8, right: 18 },
@ -660,6 +681,7 @@ export const RestrictedUpstreamMap = {
[Operator.Note]: [],
[Operator.Invoke]: [Operator.Begin],
[Operator.Template]: [Operator.Begin, Operator.Relevant],
[Operator.Email]: [Operator.Begin],
};
export const NodeMap = {
@ -696,6 +718,7 @@ export const NodeMap = {
[Operator.Crawler]: 'ragNode',
[Operator.Invoke]: 'invokeNode',
[Operator.Template]: 'templateNode',
[Operator.Email]: 'emailNode',
};
export const LanguageOptions = [

View File

@ -81,6 +81,7 @@ const FormMap = {
[Operator.Concentrator]: () => <></>,
[Operator.Note]: () => <></>,
[Operator.Template]: TemplateForm,
[Operator.Email]: EmailForm,
};
const EmptyContent = () => <div></div>;

View File

@ -0,0 +1,53 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const EmailForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
{/* SMTP服务器配置 */}
<Form.Item label={t('smtpServer')} name={'smtp_server'}>
<Input placeholder="smtp.example.com" />
</Form.Item>
<Form.Item label={t('smtpPort')} name={'smtp_port'}>
<Input type="number" placeholder="587" />
</Form.Item>
<Form.Item label={t('senderEmail')} name={'email'}>
<Input placeholder="sender@example.com" />
</Form.Item>
<Form.Item label={t('authCode')} name={'password'}>
<Input.Password placeholder="your_password" />
</Form.Item>
<Form.Item label={t('senderName')} name={'sender_name'}>
<Input placeholder="Sender Name" />
</Form.Item>
{/* 动态参数说明 */}
<div style={{ marginBottom: 24 }}>
<h4>{t('dynamicParameters')}</h4>
<div>{t('jsonFormatTip')}</div>
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4 }}>
{`{
"to_email": "recipient@example.com",
"cc_email": "cc@example.com",
"subject": "Email Subject",
"content": "Email Content"
}`}
</pre>
</div>
</Form>
);
};
export default EmailForm;

View File

@ -44,6 +44,7 @@ import {
initialCrawlerValues,
initialDeepLValues,
initialDuckValues,
initialEmailValues,
initialExeSqlValues,
initialGenerateValues,
initialGithubValues,
@ -141,6 +142,7 @@ export const useInitializeOperatorParams = () => {
[Operator.Crawler]: initialCrawlerValues,
[Operator.Invoke]: initialInvokeValues,
[Operator.Template]: initialTemplateValues,
[Operator.Email]: initialEmailValues,
};
}, [llmId]);