botProviderLogos.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. import type { BotMcpPlugin } from './workspacePlugins.js'
  2. /** Bundled logos keep the built-in directory usable without a third-party request. */
  3. const providers: Record<string, { hosts: string[] }> = {
  4. gmail: { hosts: ['gmail.com'] },
  5. googledrive: { hosts: ['drive.google.com'] },
  6. googlecalendar: { hosts: ['calendar.google.com'] },
  7. github: { hosts: ['github.com', 'githubcopilot.com'] },
  8. notion: { hosts: ['notion.so', 'notion.com'] },
  9. slack: { hosts: ['slack.com'] },
  10. linear: { hosts: ['linear.app'] },
  11. discord: { hosts: ['discord.com'] },
  12. outlook: { hosts: ['outlook.com', 'outlook.office.com'] },
  13. trello: { hosts: ['trello.com'] },
  14. airtable: { hosts: ['airtable.com'] },
  15. twitter: { hosts: ['twitter.com', 'x.com'] },
  16. }
  17. const aliases: Record<string, string> = { gdrive: 'googledrive', x: 'twitter', microsoftoutlook: 'outlook' }
  18. export const BOT_LOGO_ORIGINS = ['https://logos.composio.dev', 'https://assets.composio.dev'] as const
  19. export function bundledProviderLogo(slug: string): string | undefined {
  20. const key = slug.toLowerCase().replace(/[\s_-]/g, '')
  21. const provider = aliases[key] ?? key
  22. return Object.hasOwn(providers, provider) ? `/provider-logos/${provider}.svg` : undefined
  23. }
  24. /** Match the image CSP; never forward credentials or arbitrary image origins. */
  25. export function safeProviderLogo(value: unknown): string | undefined {
  26. if (typeof value !== 'string' || value.length > 2048) return undefined
  27. if (Object.keys(providers).some(slug => value === bundledProviderLogo(slug))) return value
  28. try {
  29. const url = new URL(value)
  30. if (url.username || url.password || !BOT_LOGO_ORIGINS.some(origin => url.origin === origin)) return undefined
  31. return url.href
  32. } catch { return undefined }
  33. }
  34. export function mcpProviderLogo(plugin: BotMcpPlugin): string | undefined {
  35. if (plugin.transport === 'http' && plugin.url) {
  36. try {
  37. const host = new URL(plugin.url).hostname.toLowerCase()
  38. const provider = Object.entries(providers).find(([, { hosts }]) => hosts.some(domain => host === domain || host.endsWith(`.${domain}`)))
  39. if (provider) return bundledProviderLogo(provider[0])
  40. } catch { /* Older invalid definitions still get a readable placeholder. */ }
  41. }
  42. if (plugin.transport === 'stdio') {
  43. for (const arg of [plugin.command ?? '', ...(plugin.args ?? [])]) {
  44. const packageName = arg.toLowerCase().replace(/^--package=/, '').replace(/@[^/@]+$/, '')
  45. if (packageName === '@notionhq/notion-mcp-server') return bundledProviderLogo('notion')
  46. const slug = /^@modelcontextprotocol\/server-([a-z-]+)$/.exec(packageName)?.[1]
  47. const logo = slug && bundledProviderLogo(slug)
  48. if (logo) return logo
  49. }
  50. }
  51. return bundledProviderLogo(plugin.name.replace(/\s*(?:mcp(?:\s*(?:server|服务))?|server|服务)$/i, '').trim())
  52. }