fix(knowledge-fs): correct source provider branding

This commit is contained in:
Stephen Zhou 2026-08-12 16:28:01 +08:00
parent d8dde7fb5e
commit 077cdf5a54
No known key found for this signature in database
11 changed files with 169 additions and 59 deletions

View File

@ -24,6 +24,7 @@
"@eslint-react/eslint-plugin": "catalog:",
"@eslint/markdown": "catalog:",
"@iconify-json/heroicons": "catalog:",
"@iconify-json/logos": "catalog:",
"@iconify-json/ri": "catalog:",
"@tanstack/eslint-plugin-query": "catalog:",
"@types/node": "catalog:",

16
pnpm-lock.yaml generated
View File

@ -57,6 +57,9 @@ catalogs:
'@iconify-json/heroicons':
specifier: 1.2.3
version: 1.2.3
'@iconify-json/logos':
specifier: 1.2.12
version: 1.2.12
'@iconify-json/ri':
specifier: 1.2.10
version: 1.2.10
@ -671,6 +674,9 @@ importers:
'@iconify-json/heroicons':
specifier: 'catalog:'
version: 1.2.3
'@iconify-json/logos':
specifier: 'catalog:'
version: 1.2.12
'@iconify-json/ri':
specifier: 'catalog:'
version: 1.2.10
@ -1501,6 +1507,9 @@ importers:
'@iconify-json/heroicons':
specifier: 'catalog:'
version: 1.2.3
'@iconify-json/logos':
specifier: 'catalog:'
version: 1.2.12
'@iconify-json/ri':
specifier: 'catalog:'
version: 1.2.10
@ -2354,6 +2363,9 @@ packages:
'@iconify-json/heroicons@1.2.3':
resolution: {integrity: sha512-n+vmCEgTesRsOpp5AB5ILB6srsgsYK+bieoQBNlafvoEhjVXLq8nIGN4B0v/s4DUfa0dOrjwE/cKJgIKdJXOEg==}
'@iconify-json/logos@1.2.12':
resolution: {integrity: sha512-zUi/AoezU2F3L65nPVd2smiU6Y+ZI7RjdVPlGfeAeYbPbZ9kWn7Ucxj+KshmyQRBYwLtKoqAlUyoGgMqWG1T8g==}
'@iconify-json/ri@1.2.10':
resolution: {integrity: sha512-WWMhoncVVM+Xmu9T5fgu2lhYRrKTEWhKk3Com0KiM111EeEsRLiASjpsFKnC/SrB6covhUp95r2mH8tGxhgd5Q==}
@ -10129,6 +10141,10 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/logos@1.2.12':
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/ri@1.2.10':
dependencies:
'@iconify/types': 2.0.0

View File

@ -67,6 +67,7 @@ catalog:
'@hey-api/openapi-ts': 0.98.2
'@hono/node-server': 2.1.0
'@iconify-json/heroicons': 1.2.3
'@iconify-json/logos': 1.2.12
'@iconify-json/ri': 1.2.10
'@lexical/code': 0.47.0
'@lexical/link': 0.47.0

View File

@ -9,7 +9,7 @@ import { getIconCollections, iconsPlugin } from '@egoist/tailwindcss-icons'
export default iconsPlugin({
collections: {
...getIconCollections(['heroicons', 'ri']),
...getIconCollections(['heroicons', 'logos', 'ri']),
'custom-public': customPublicIcons,
'custom-vender': customVenderIcons,
},

View File

@ -0,0 +1,21 @@
import { sourceProviderPresentation } from '../source-provider-options'
describe('sourceProviderPresentation', () => {
it.each([
['Firecrawl', 'websiteCrawl', 'Firecrawl', 'i-custom-public-common-firecrawl'],
['Jina Reader', 'websiteCrawl', 'Jina Reader', 'i-custom-public-llm-jina'],
['WaterCrawl', 'websiteCrawl', 'WaterCrawl', 'i-custom-public-knowledge-watercrawl'],
['OneDrive', 'onlineDrive', 'OneDrive', 'i-logos-microsoft-onedrive'],
['aws_s3', 'onlineDrive', 'Amazon S3', 'i-logos-aws-s3'],
['notion_datasource', 'onlineDocuments', 'Notion', 'i-custom-public-common-notion'],
] as const)(
'returns the canonical presentation for %s',
(provider, sourceType, label, fallbackIcon) => {
expect(sourceProviderPresentation(provider, sourceType)).toEqual({ fallbackIcon, label })
},
)
it('does not collapse a custom provider name that merely contains a built-in brand', () => {
expect(sourceProviderPresentation('Notion Backup', 'onlineDocuments')).toBeUndefined()
})
})

View File

@ -1610,6 +1610,31 @@ describe('SourcesPage', () => {
expect(within(row).queryByText('notion_datasource')).not.toBeInTheDocument()
})
it('preserves a custom provider name containing a built-in brand', () => {
sourcesQuery.data = {
pages: [
{
items: [
source({
metadata: {
providerKind: 'online-document',
providerName: 'Notion Backup',
},
name: 'Archived workspace',
type: 'connector',
}),
],
},
],
}
render(<SourcesPage knowledgeSpaceId="space-1" />)
const row = screen.getByRole('row', { name: /Archived workspace/ })
expect(within(row).getByText('Notion Backup')).toBeInTheDocument()
expect(within(row).queryByText('Notion')).not.toBeInTheDocument()
})
it('uses provider kind to distinguish legacy Google Drive sources', () => {
sourcesQuery.data = {
pages: [
@ -1635,6 +1660,29 @@ describe('SourcesPage', () => {
expect(within(row).getByText('dataset.newKnowledge.onlineDrive')).toBeInTheDocument()
})
it('restores the OneDrive name, type, and brand icon', () => {
sourcesQuery.data = {
pages: [
{
items: [
source({
metadata: { providerName: 'microsoft_onedrive' },
name: 'Sales enablement',
type: 'connector',
}),
],
},
],
}
render(<SourcesPage knowledgeSpaceId="space-1" />)
const row = screen.getByRole('row', { name: /Sales enablement/ })
expect(within(row).getByText('OneDrive')).toBeInTheDocument()
expect(within(row).getByText('dataset.newKnowledge.onlineDrive')).toBeInTheDocument()
expect(row.querySelector('.i-logos-microsoft-onedrive')).toBeInTheDocument()
})
it('does not report ordinary source updates as successful syncs', () => {
const syncPolicy: SourceSyncPolicy = {
createdAt: '2026-07-20T10:00:00Z',

View File

@ -39,6 +39,8 @@ import {
NEW_KNOWLEDGE_SOURCE_NAME_MAX_LENGTH,
NEW_KNOWLEDGE_SOURCE_URL_MAX_LENGTH,
} from './routes'
import { sourceProviderPresentation } from './source-provider-options'
import { SourceProviderIcon } from './source-setup-fields'
const connectedProviders = {
onlineDocuments: [
@ -48,8 +50,8 @@ const connectedProviders = {
],
onlineDrive: [
{ icon: 'i-custom-public-common-google-drive', label: 'Google Drive' },
{ icon: 'i-ri-cloud-line', label: 'OneDrive' },
{ icon: 'i-ri-box-3-line', label: 'Amazon S3' },
{ icon: 'i-logos-microsoft-onedrive', label: 'OneDrive' },
{ icon: 'i-logos-aws-s3', label: 'Amazon S3' },
],
} as const
@ -210,20 +212,23 @@ export function UnavailableConnectedSourceSetup({
className="grid grid-cols-1 gap-2 sm:grid-cols-3"
onValueChange={selectProvider}
>
{providers.map((option) => (
<RadioItem<string>
key={option.label}
value={option.label}
className={cn(
'flex min-h-9 items-center justify-center gap-2 rounded-lg border border-divider-subtle px-3 system-xs-medium text-text-secondary outline-hidden',
'hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid',
'data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg data-checked:text-text-primary',
)}
>
<span aria-hidden className={`${option.icon} size-4 shrink-0`} />
<span className="truncate">{option.label}</span>
</RadioItem>
))}
{providers.map((option) => {
const presentation = sourceProviderPresentation(option.label, sourceType)
return (
<RadioItem<string>
key={option.label}
value={option.label}
className={cn(
'flex min-h-9 items-center justify-center gap-2 rounded-lg border border-divider-subtle px-3 system-xs-medium text-text-secondary outline-hidden',
'hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid',
'data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg data-checked:text-text-primary',
)}
>
<SourceProviderIcon fallbackIcon={presentation?.fallbackIcon ?? option.icon} />
<span className="truncate">{option.label}</span>
</RadioItem>
)
})}
</RadioGroup>
</Fieldset>

View File

@ -35,6 +35,7 @@ import {
import {
SourceConnectionRequiredCard,
SourceNameField,
SourceProviderIcon,
SourceProviderNotInstalledCard,
SourceProviderRadioGroup,
SourceSyncPolicyField,
@ -434,7 +435,7 @@ export function CreateSourceSetup({
disabled={disabled}
layout={sourceType === 'websiteCrawl' ? 'grid-four' : 'grid-three'}
options={providerOptions.map((option) => ({
icon: <span aria-hidden className={`${option.fallbackIcon} size-4 shrink-0`} />,
icon: <SourceProviderIcon fallbackIcon={option.fallbackIcon} />,
label: option.label,
value: option.key,
}))}
@ -463,7 +464,7 @@ export function CreateSourceSetup({
</div>
) : providerOption && !providerOption.installed ? (
<SourceProviderNotInstalledCard
icon={<span aria-hidden className={`${providerOption.fallbackIcon} size-4.5`} />}
icon={<SourceProviderIcon fallbackIcon={providerOption.fallbackIcon} />}
provider={providerOption.label}
onInstall={() =>
globalThis.open(
@ -482,7 +483,7 @@ export function CreateSourceSetup({
provider: installedProviderOption.label,
})}
disabled={disabled}
icon={<span aria-hidden className={`${installedProviderOption.fallbackIcon} size-4.5`} />}
icon={<SourceProviderIcon fallbackIcon={installedProviderOption.fallbackIcon} />}
title={t(($) => $['newKnowledge.providerNotConfigured'], {
provider: installedProviderOption.label,
})}

View File

@ -38,7 +38,7 @@ export type SourceProviderOption = InstalledSourceProviderOption | UninstalledSo
const recommendedProviders: RecommendedProvider[] = [
{
aliases: ['firecrawl'],
aliases: ['firecrawl', 'plugin-daemon-website', 'plugin-daemon-website-firecrawl'],
fallbackIcon: 'i-custom-public-common-firecrawl',
label: 'Firecrawl',
packageId: 'langgenius/firecrawl_datasource',
@ -62,7 +62,7 @@ const recommendedProviders: RecommendedProvider[] = [
sourceType: 'websiteCrawl',
},
{
aliases: ['notion'],
aliases: ['notion', 'notion_datasource'],
fallbackIcon: 'i-custom-public-common-notion',
label: 'Notion',
packageId: 'langgenius/notion_datasource',
@ -95,15 +95,15 @@ const recommendedProviders: RecommendedProvider[] = [
},
{
aliases: ['onedrive', 'microsoft onedrive'],
fallbackIcon: 'i-ri-cloud-line',
fallbackIcon: 'i-logos-microsoft-onedrive',
label: 'OneDrive',
packageId: 'langgenius/onedrive_datasource',
providerType: 'online_drive',
sourceType: 'onlineDrive',
},
{
aliases: ['amazon s3', 'amazons3', 's3'],
fallbackIcon: 'i-ri-box-3-line',
aliases: ['amazon s3', 'amazons3', 'aws s3', 's3'],
fallbackIcon: 'i-logos-aws-s3',
label: 'Amazon S3',
packageId: 'langgenius/aws_s3_storage',
providerType: 'online_drive',
@ -115,6 +115,30 @@ export function normalizeSourceProviderName(value: string) {
return value.toLocaleLowerCase().replace(/[^a-z0-9]+/g, '')
}
export function sourceProviderPresentation(
value: string,
sourceType?: NewKnowledgeSourceType,
): Pick<RecommendedProvider, 'fallbackIcon' | 'label'> | undefined {
const normalized = normalizeSourceProviderName(value)
if (!normalized) return undefined
const candidates = sourceType
? recommendedProviders.filter((provider) => provider.sourceType === sourceType)
: recommendedProviders
const provider =
(!sourceType
? candidates.find((candidate) => normalizeSourceProviderName(candidate.label) === normalized)
: undefined) ??
candidates.find((candidate) =>
[candidate.label, candidate.packageId, ...candidate.aliases]
.map(normalizeSourceProviderName)
.includes(normalized),
)
if (!provider) return undefined
return { fallbackIcon: provider.fallbackIcon, label: provider.label }
}
function providerKey(
sourceType: NewKnowledgeSourceType,
plugin: DataSourceItem,

View File

@ -52,6 +52,8 @@ import {
sourceWorkflowFromApi,
sourceWorkflowStatus,
} from './source-models'
import { normalizeSourceProviderName, sourceProviderPresentation } from './source-provider-options'
import { SourceProviderIcon } from './source-setup-fields'
import { useKnowledgeModelSetupGuard } from './use-knowledge-model-setup-guard'
type SourceStatus = Source['status']
@ -114,32 +116,32 @@ function metadataRecord(metadata: Source['metadata'], key: string) {
: undefined
}
function knownSourceProviderName(value: string, providerKind?: string) {
const normalized = value.toLocaleLowerCase().replace(/[^a-z0-9]+/g, '')
if (normalized.includes('firecrawl')) return 'Firecrawl'
if (normalized.includes('jinareader') || normalized === 'jina') return 'Jina Reader'
if (normalized.includes('watercrawl')) return 'WaterCrawl'
if (normalized.includes('notion')) return 'Notion'
if (normalized.includes('googledocs')) return 'Google Docs'
if (normalized.includes('googledrive'))
return providerKind === 'online-document' ? 'Google Docs' : 'Google Drive'
if (normalized.includes('confluence')) return 'Confluence'
if (normalized.includes('amazons3') || normalized.includes('awss3')) return 'Amazon S3'
function sourceProviderType(source: Source, providerKind?: string) {
if (source.type === 'web' || providerKind === 'website') return 'websiteCrawl' as const
if (providerKind === 'online-document') return 'onlineDocuments' as const
if (providerKind === 'online-drive') return 'onlineDrive' as const
return undefined
}
function sourceProviderName(source: Source) {
function sourceProviderDetails(source: Source) {
const providerKind = metadataString(source.metadata, 'providerKind')
const providerType = sourceProviderType(source, providerKind)
const explicitName = metadataString(source.metadata, 'providerName')
if (explicitName) return knownSourceProviderName(explicitName, providerKind) ?? explicitName
if (explicitName) {
const presentation = sourceProviderPresentation(explicitName, providerType)
return {
iconClass: presentation?.fallbackIcon,
name: presentation?.label ?? explicitName,
}
}
const providerId = metadataString(source.metadata, 'providerId')
if (!providerId) return undefined
if (providerId === 'plugin-daemon-website') return 'Firecrawl'
if (providerId.toLocaleLowerCase().includes('fakecrawler')) return 'FakeCrawler'
const knownName = knownSourceProviderName(providerId, providerKind)
if (knownName) return knownName
return undefined
if (!providerId) return {}
const presentation = sourceProviderPresentation(providerId, providerType)
if (presentation) return { iconClass: presentation.fallbackIcon, name: presentation.label }
if (normalizeSourceProviderName(providerId).includes('fakecrawler'))
return { name: 'FakeCrawler' }
return {}
}
function sourceLastSyncAt(source: Source) {
@ -404,7 +406,8 @@ function SourceRow({
const [pendingAction, setPendingAction] = useState<SourceAction>()
const syncWorkflow = source.syncWorkflow
const providerName = sourceProviderName(source)
const provider = sourceProviderDetails(source)
const providerName = provider.name
const providerKind = metadataString(source.metadata, 'providerKind')
const sourceSyncPolicy = source.syncPolicy
const syncPolicy = sourceSyncPolicy
@ -425,23 +428,12 @@ function SourceRow({
: source.type === 'connector' &&
(providerKind === 'online-drive' ||
providerName === 'Google Drive' ||
providerName === 'OneDrive' ||
providerName === 'Amazon S3')
? t(($) => $['newKnowledge.onlineDrive'])
: t(($) => $[`newKnowledge.sourceType.${source.type}`])
const sourceIcon =
source.type === 'web'
? 'i-ri-global-line'
: providerName === 'Notion'
? 'i-custom-public-common-notion'
: providerName === 'Amazon S3'
? 'i-ri-folder-line'
: providerName === 'Google Drive'
? 'i-custom-public-common-google-drive'
: providerName === 'Google Docs'
? 'i-ri-file-text-fill text-[#4d8bf5]'
: providerName === 'Confluence'
? 'i-custom-public-common-confluence'
: 'i-ri-links-line'
provider.iconClass ?? (source.type === 'web' ? 'i-ri-global-line' : 'i-ri-links-line')
const runAction = async <Result,>(
action: SourceAction,
@ -560,7 +552,7 @@ function SourceRow({
</td>
<td className="min-w-0 py-2 pr-3">
<div className="flex min-w-0 items-center gap-2.5">
<span aria-hidden className={cn('size-4.5 shrink-0 text-text-tertiary', sourceIcon)} />
<SourceProviderIcon fallbackIcon={sourceIcon} />
<div className="min-w-0">
<p className="truncate system-xs-medium text-text-primary">{source.name}</p>
</div>

View File

@ -150,6 +150,7 @@
"@dify/tsconfig": "workspace:*",
"@egoist/tailwindcss-icons": "catalog:",
"@iconify-json/heroicons": "catalog:",
"@iconify-json/logos": "catalog:",
"@iconify-json/ri": "catalog:",
"@langgenius/dev-proxy": "workspace:*",
"@langgenius/dify-ui": "workspace:*",