fix(web): resolve marketplace plugin ids before install

This commit is contained in:
Stephen Zhou 2026-09-01 18:25:08 +08:00
parent e9180d38e5
commit a7a16c1335
No known key found for this signature in database
5 changed files with 116 additions and 9 deletions

View File

@ -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 () => {

View File

@ -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

View File

@ -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(<PluginCategoryPage category={PluginCategoryEnum.datasource} />)
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(<PluginCategoryPage canInstall={false} category={PluginCategoryEnum.agent} />)

View File

@ -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,
}
}

View File

@ -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))
}