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

@ -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]);