diff --git a/web/app/(commonLayout)/plugins/page.spec.tsx b/web/app/(commonLayout)/plugins/page.spec.tsx
index cd56e8680d3..e34c0550631 100644
--- a/web/app/(commonLayout)/plugins/page.spec.tsx
+++ b/web/app/(commonLayout)/plugins/page.spec.tsx
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
+import { MARKETPLACE_API_PREFIX } from '@/config'
const mocks = vi.hoisted(() => ({
fetch: vi.fn(),
@@ -41,6 +42,41 @@ describe('PluginList', () => {
expect(mocks.redirect).toHaveBeenCalledWith(
'/integrations/tools/built-in?package-ids=%5B%22langgenius%2Fexample-tool%22%5D',
)
+ expect(mocks.fetch).toHaveBeenCalledWith(
+ `${MARKETPLACE_API_PREFIX}/plugins/langgenius/example-tool`,
+ { cache: 'no-store' },
+ )
+ })
+
+ it('resolves a full package identifier through the identifier endpoint', async () => {
+ const packageId = 'langgenius/example-tool:1.0.0@checksum'
+ mocks.fetch.mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue({
+ data: {
+ plugin: {
+ category: 'tool',
+ },
+ },
+ }),
+ })
+ const { default: PluginList } = await import('./page')
+
+ await expect(
+ PluginList({
+ searchParams: Promise.resolve({
+ 'package-ids': JSON.stringify([packageId]),
+ }),
+ }),
+ ).rejects.toThrow('NEXT_REDIRECT')
+
+ expect(mocks.fetch).toHaveBeenCalledWith(
+ `${MARKETPLACE_API_PREFIX}/plugins/identifier?unique_identifier=${encodeURIComponent(packageId)}`,
+ { cache: 'no-store' },
+ )
+ expect(mocks.redirect).toHaveBeenCalledWith(
+ `/integrations/tools/built-in?package-ids=${encodeURIComponent(JSON.stringify([packageId]))}`,
+ )
})
it('redirects to integrations when category resolution fails', async () => {
diff --git a/web/app/(commonLayout)/plugins/page.tsx b/web/app/(commonLayout)/plugins/page.tsx
index f22d0e2a811..e26fc91ca28 100644
--- a/web/app/(commonLayout)/plugins/page.tsx
+++ b/web/app/(commonLayout)/plugins/page.tsx
@@ -4,6 +4,7 @@ import {
getInstallRedirectPathByPluginCategory,
getInstallRedirectPathFromSearchParams,
getLegacyPluginRedirectPath,
+ parseMarketplacePluginId,
shouldResolveInstallCategoryRedirect,
} from '@/app/components/plugins/plugin-routes'
import { MARKETPLACE_API_PREFIX } from '@/config'
@@ -23,10 +24,11 @@ type MarketplaceManifestCategoryResponse = {
const fetchPluginCategoryFromMarketplace = async (packageId: string) => {
try {
- const response = await fetch(
- `${MARKETPLACE_API_PREFIX}/plugins/identifier?unique_identifier=${encodeURIComponent(packageId)}`,
- { cache: 'no-store' },
- )
+ const pluginId = parseMarketplacePluginId(packageId)
+ const path = pluginId
+ ? `/plugins/${encodeURIComponent(pluginId.org)}/${encodeURIComponent(pluginId.name)}`
+ : `/plugins/identifier?unique_identifier=${encodeURIComponent(packageId)}`
+ const response = await fetch(`${MARKETPLACE_API_PREFIX}${path}`, { cache: 'no-store' })
if (!response.ok) return undefined
diff --git a/web/app/components/integrations/__tests__/plugin-category-page.spec.tsx b/web/app/components/integrations/__tests__/plugin-category-page.spec.tsx
index 7bc623ce72a..edf6eb305e6 100644
--- a/web/app/components/integrations/__tests__/plugin-category-page.spec.tsx
+++ b/web/app/components/integrations/__tests__/plugin-category-page.spec.tsx
@@ -7,6 +7,7 @@ import PluginCategoryPage from '../plugin-category-page'
const {
mockContainerRef,
mockFetchManifestFromMarketPlace,
+ mockFetchPluginInfoFromMarketPlace,
mockSetInstallState,
mockUseUploader,
mockUsePluginInstallation,
@@ -14,6 +15,7 @@ const {
} = vi.hoisted(() => ({
mockContainerRef: { current: null },
mockFetchManifestFromMarketPlace: vi.fn(),
+ mockFetchPluginInfoFromMarketPlace: vi.fn(),
mockSetInstallState: vi.fn(),
mockUseUploader: vi.fn((_: unknown) => ({
dragging: false,
@@ -108,6 +110,8 @@ vi.mock('@/hooks/use-query-params', () => ({
vi.mock('@/service/plugins', () => ({
fetchManifestFromMarketPlace: (...args: unknown[]) => mockFetchManifestFromMarketPlace(...args),
+ fetchPluginInfoFromMarketPlace: (...args: unknown[]) =>
+ mockFetchPluginInfoFromMarketPlace(...args),
}))
type UploaderOptions = {
@@ -280,6 +284,49 @@ describe('PluginCategoryPage', () => {
expect(mockSetInstallState).toHaveBeenCalledWith(null)
})
+ it('resolves a marketplace package name before opening the installer', async () => {
+ const packageId = 'langgenius/confluence_datasource'
+ const uniqueIdentifier =
+ 'langgenius/confluence_datasource:0.2.9@923a18de89d8cdb7f419d0dff60bf08a8b81b65fef6bf606cf0ce4b0ee56a9ca'
+ mockUsePluginInstallation.mockReturnValue([
+ { packageId, bundleInfo: null },
+ mockSetInstallState,
+ ])
+ mockFetchPluginInfoFromMarketPlace.mockResolvedValue({
+ data: {
+ plugin: {
+ latest_package_identifier: uniqueIdentifier,
+ },
+ },
+ })
+ mockFetchManifestFromMarketPlace.mockResolvedValue({
+ data: {
+ plugin: {
+ org: 'langgenius',
+ name: 'confluence_datasource',
+ category: PluginCategoryEnum.datasource,
+ },
+ version: { version: '0.2.9' },
+ },
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(mockFetchPluginInfoFromMarketPlace).toHaveBeenCalledWith({
+ name: 'confluence_datasource',
+ org: 'langgenius',
+ })
+ expect(mockFetchManifestFromMarketPlace).toHaveBeenCalledWith(
+ encodeURIComponent(uniqueIdentifier),
+ )
+ expect(screen.getByTestId('install-from-marketplace')).toHaveAttribute(
+ 'data-unique-identifier',
+ uniqueIdentifier,
+ )
+ })
+ })
+
it('ignores dropped files when install permission is unavailable', () => {
render()
diff --git a/web/app/components/plugins/install-plugin/hooks/use-install-from-marketplace-query.ts b/web/app/components/plugins/install-plugin/hooks/use-install-from-marketplace-query.ts
index 881f740eb9f..17d31b2cb2f 100644
--- a/web/app/components/plugins/install-plugin/hooks/use-install-from-marketplace-query.ts
+++ b/web/app/components/plugins/install-plugin/hooks/use-install-from-marketplace-query.ts
@@ -2,15 +2,29 @@
import type { Dependency, PluginDeclaration, PluginManifestInMarket } from '../../types'
import { useEffect, useState } from 'react'
+import { parseMarketplacePluginId } from '@/app/components/plugins/plugin-routes'
import { MARKETPLACE_API_PREFIX } from '@/config'
import { usePluginInstallation } from '@/hooks/use-query-params'
-import { fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins'
+import {
+ fetchBundleInfoFromMarketPlace,
+ fetchManifestFromMarketPlace,
+ fetchPluginInfoFromMarketPlace,
+} from '@/service/plugins'
type MarketplaceInstall = {
manifest: PluginDeclaration | PluginManifestInMarket
+ packageId: string
uniqueIdentifier: string
}
+async function resolveMarketplaceUniqueIdentifier(packageId: string) {
+ const pluginId = parseMarketplacePluginId(packageId)
+ if (!pluginId) return packageId
+
+ const { data } = await fetchPluginInfoFromMarketPlace(pluginId)
+ return data.plugin.latest_package_identifier
+}
+
export type UseInstallFromMarketplaceQueryOptions = {
canInstallPlugin: boolean
isPermissionLoading?: boolean
@@ -42,7 +56,8 @@ export const useInstallFromMarketplaceQuery = ({
const loadMarketplaceInstall = async () => {
if (packageId) {
try {
- const { data } = await fetchManifestFromMarketPlace(encodeURIComponent(packageId))
+ const uniqueIdentifier = await resolveMarketplaceUniqueIdentifier(packageId)
+ const { data } = await fetchManifestFromMarketPlace(encodeURIComponent(uniqueIdentifier))
if (ignore) return
const { plugin, version } = data
@@ -50,7 +65,8 @@ export const useInstallFromMarketplaceQuery = ({
if (redirected) return
setMarketplaceInstall({
- uniqueIdentifier: packageId,
+ packageId,
+ uniqueIdentifier,
manifest: {
...plugin,
version: version.version,
@@ -103,7 +119,6 @@ export const useInstallFromMarketplaceQuery = ({
dependencies,
hideInstallFromMarketplace,
isShowInstallFromMarketplace,
- marketplaceInstall:
- marketplaceInstall?.uniqueIdentifier === packageId ? marketplaceInstall : null,
+ marketplaceInstall: marketplaceInstall?.packageId === packageId ? marketplaceInstall : null,
}
}
diff --git a/web/app/components/plugins/plugin-routes.ts b/web/app/components/plugins/plugin-routes.ts
index dcfd6614b2b..fa36fbba3ab 100644
--- a/web/app/components/plugins/plugin-routes.ts
+++ b/web/app/components/plugins/plugin-routes.ts
@@ -37,6 +37,13 @@ const getFirstSearchParamValue = (value: string | string[] | undefined) => {
return value
}
+export const parseMarketplacePluginId = (packageId: string) => {
+ const [org, name, ...extraParts] = packageId.split('/')
+ if (!org || !name || extraParts.length || name.includes(':')) return null
+
+ return { name, org }
+}
+
const hasInstallSearchParams = (searchParams: LegacyPluginsSearchParams) => {
return Object.keys(searchParams).some((key) => installSearchParamKeys.has(key))
}