feat(connectors): add Salesforce CRM data source connector (#15462)

### What problem does this PR solve?

Closes #15461.

RAGFlow had no way to ingest Salesforce CRM data, so support / sales
teams couldn't ground responses on live Accounts, Contacts,
Opportunities, Cases, or Knowledge articles. This adds a first-class
Salesforce data source connector that authenticates against a Connected
App via OAuth 2.0 client-credentials, queries selected SObjects via
SOQL, and turns each record into an indexable document with incremental
sync.

**Highlights**
- `common/data_source/salesforce_connector.py`: new
`SalesforceConnector` (`CheckpointedConnectorWithPermSync` +
`SlimConnectorWithPermSync`).
- OAuth 2.0 client-credentials flow; canonical `instance_url` from the
token response so multi-pod orgs route correctly.
- Per-object `SystemModstamp` cursor stored in
`SalesforceCheckpoint.cursors` — a failure mid-object doesn't rewind
sibling objects, and re-syncs only fetch changed rows.
- Deterministic record-to-text formatter (sorted keys) so SOQL field
reordering on the server doesn't mark every row "changed" on each poll.
- `_get_json` raises on non-2xx so 429 / 5xx never silently advance the
checkpoint past missing data.
- `Knowledge__kav` is in the default object set but is skipped silently
when the org doesn't have Salesforce Knowledge enabled (404 on
describe).
- Slim-doc IDs are scoped as `<Object>/<Id>` so prune deletes can't
collide across object types.
- `common/constants.py`, `common/data_source/config.py`,
`common/data_source/__init__.py`: register `salesforce` in `FileSource`
/ `DocumentSource` and export `SalesforceConnector`.
- `rag/svr/sync_data_source.py`: new `Salesforce(SyncBase)` class routed
through `load_from_checkpoint` (poll_source would re-walk every object
each run) and added to `func_factory`.
- Frontend:
- `web/src/pages/user-setting/data-source/constant/index.tsx`: new
`DataSourceKey.SALESFORCE`, form fields (instance URL, client ID/secret,
objects, api_version, batch size), `syncDeletedFiles` capability,
default form values, and tile entry with the new icon.
  - `web/src/locales/{en,zh}.ts`: description + per-field tooltips.
- `web/src/assets/svg/data-source/salesforce.svg`: 48x48 brand-style
icon to match the other Microsoft / cloud tiles.

**Verification**
- `npm run build` (vite + esbuild) passes (1m 26s).

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
web-dev0521
2026-06-04 23:24:36 -06:00
committed by GitHub
parent e629c0203b
commit 1d7e45115b
9 changed files with 750 additions and 0 deletions

View File

@@ -45,6 +45,7 @@ export enum DataSourceKey {
RSS = 'rss',
ONEDRIVE = 'onedrive',
OUTLOOK = 'outlook',
SALESFORCE = 'salesforce',
AZURE_BLOB = 'azure_blob',
TEAMS = 'teams',
SLACK = 'slack',
@@ -138,6 +139,9 @@ export const DataSourceFeatureVisibilityMap: Partial<
[DataSourceKey.OUTLOOK]: {
syncDeletedFiles: true,
},
[DataSourceKey.SALESFORCE]: {
syncDeletedFiles: true,
},
[DataSourceKey.AZURE_BLOB]: {
syncDeletedFiles: true,
},
@@ -339,6 +343,11 @@ export const generateDataSourceInfo = (t: TFunction) => {
description: t(`setting.${DataSourceKey.OUTLOOK}Description`),
icon: <Mail className="text-text-primary" size={22} />,
},
[DataSourceKey.SALESFORCE]: {
name: 'Salesforce',
description: t(`setting.${DataSourceKey.SALESFORCE}Description`),
icon: <SvgIcon name={'data-source/salesforce'} width={38} />,
},
[DataSourceKey.AZURE_BLOB]: {
name: 'Azure Blob Storage',
description: t(`setting.${DataSourceKey.AZURE_BLOB}Description`),
@@ -524,6 +533,65 @@ export const DataSourceFormFields = {
},
},
],
[DataSourceKey.SALESFORCE]: [
{
label: 'Instance URL',
name: 'config.credentials.instance_url',
type: FormFieldType.Text,
required: true,
placeholder: 'https://your-domain.my.salesforce.com',
tooltip: t('setting.salesforceInstanceUrlTip'),
validation: {
pattern: /^https:\/\/[a-zA-Z0-9.-]+\.salesforce\.com$/,
message:
'Must be a valid Salesforce domain (https://...salesforce.com)',
},
},
{
label: 'Client ID',
name: 'config.credentials.client_id',
type: FormFieldType.Text,
required: true,
tooltip: t('setting.salesforceClientIdTip'),
},
{
label: 'Client Secret',
name: 'config.credentials.client_secret',
type: FormFieldType.Password,
required: true,
tooltip: t('setting.salesforceClientSecretTip'),
},
{
label: 'Objects',
name: 'config.objects',
type: FormFieldType.Text,
required: false,
placeholder: 'Account,Contact,Opportunity,Case,Knowledge__kav',
tooltip: t('setting.salesforceObjectsTip'),
},
{
label: 'API Version',
name: 'config.api_version',
type: FormFieldType.Text,
required: false,
placeholder: 'v59.0',
tooltip: t('setting.salesforceApiVersionTip'),
validation: {
pattern: /^v\d+\.\d+$/,
message: 'API version must match format like v59.0',
},
},
{
label: 'Batch Size',
name: 'config.batch_size',
type: FormFieldType.Number,
required: false,
validation: {
min: 1,
message: 'Batch Size must be at least 1',
},
},
],
[DataSourceKey.AZURE_BLOB]: [
{
label: 'Auth Mode',
@@ -2109,6 +2177,20 @@ export const DataSourceFormDefaultValues = {
},
},
},
[DataSourceKey.SALESFORCE]: {
name: '',
source: DataSourceKey.SALESFORCE,
config: {
objects: '',
api_version: 'v59.0',
batch_size: 2,
credentials: {
instance_url: '',
client_id: '',
client_secret: '',
},
},
},
[DataSourceKey.AZURE_BLOB]: {
name: '',
source: DataSourceKey.AZURE_BLOB,