♻️ refactor: change the /community/assistant to /agent routes (#11606)

* refactor: change the /community/assistant to /agent routes

* fix: slove the group detail page go back error

* fix: update the e2e test
This commit is contained in:
Shinji-Li
2026-01-19 15:41:20 +08:00
committed by GitHub
parent 37e59245d0
commit 7f004c5baf
61 changed files with 92 additions and 74 deletions

View File

@@ -90,7 +90,7 @@ Feature: Community Smoke Tests
@COMMUNITY-SMOKE-001 @P0
Scenario: Load community assistant list page
Given I navigate to "/community/assistant"
Given I navigate to "/community/agent"
Then the page should load without errors
And I should see the page body
And I should see the search bar

View File

@@ -11,7 +11,7 @@ Feature: Discover Detail Pages
@COMMUNITY-DETAIL-001 @P1
Scenario: Load assistant detail page and verify content
Given I navigate to "/community/assistant"
Given I navigate to "/community/agent"
And I wait for the page to fully load
When I click on the first assistant card
Then I should be on an assistant detail page
@@ -22,7 +22,7 @@ Feature: Discover Detail Pages
@COMMUNITY-DETAIL-002 @P1
Scenario: Navigate back from assistant detail page
Given I navigate to "/community/assistant"
Given I navigate to "/community/agent"
And I wait for the page to fully load
And I click on the first assistant card
When I click the back button

View File

@@ -11,14 +11,14 @@ Feature: Discover Interactions
@COMMUNITY-INTERACT-001 @P1
Scenario: Search for assistants
Given I navigate to "/community/assistant"
Given I navigate to "/community/agent"
When I type "developer" in the search bar
And I wait for the search results to load
Then I should see filtered assistant cards
@COMMUNITY-INTERACT-002 @P1
Scenario: Filter assistants by category
Given I navigate to "/community/assistant"
Given I navigate to "/community/agent"
When I click on a category in the category menu
And I wait for the filtered results to load
Then I should see assistant cards filtered by the selected category
@@ -26,7 +26,7 @@ Feature: Discover Interactions
@COMMUNITY-INTERACT-003 @P1
Scenario: Navigate to next page of assistants
Given I navigate to "/community/assistant"
Given I navigate to "/community/agent"
When I click the next page button
And I wait for the next page to load
Then I should see different assistant cards
@@ -34,7 +34,7 @@ Feature: Discover Interactions
@COMMUNITY-INTERACT-004 @P1
Scenario: Navigate to assistant detail page
Given I navigate to "/community/assistant"
Given I navigate to "/community/agent"
When I click on the first assistant card
Then I should be navigated to the assistant detail page
And I should see the assistant detail content
@@ -95,7 +95,7 @@ Feature: Discover Interactions
Scenario: Navigate from home to assistant list
Given I navigate to "/community"
When I click on the "more" link in the featured assistants section
Then I should be navigated to "/community/assistant"
Then I should be navigated to "/community/agent"
And I should see the page body
@COMMUNITY-INTERACT-011 @P1

View File

@@ -12,7 +12,7 @@ Feature: Community Smoke Tests
@COMMUNITY-SMOKE-002 @P0
Scenario: Load Assistant List Page
Given I navigate to "/community/assistant"
Given I navigate to "/community/agent"
Then the page should load without errors
And I should see the page body
And I should see the search bar

View File

@@ -8,7 +8,7 @@ import { CustomWorld } from '../../support/world';
// ============================================
Given('I wait for the page to fully load', async function (this: CustomWorld) {
// Use domcontentloaded instead of networkidle to avoid hanging on persistent connections
// Use domcontentloaded instead of networkidle to avoid hanging on persistent connections
await this.page.waitForLoadState('domcontentloaded', { timeout: 10_000 });
// Short wait for React hydration
await this.page.waitForTimeout(1000);
@@ -135,9 +135,9 @@ Then('I should be on the assistant list page', async function (this: CustomWorld
const currentUrl = this.page.url();
// Check if URL is assistant list (not detail page) or community home
// After back navigation, URL should be /community/assistant or /community
// After back navigation, URL should be /community/agent or /community
const isListPage =
(currentUrl.includes('/community/assistant') &&
(currentUrl.includes('/community/agent') &&
!/\/community\/assistant\/[\dA-Za-z-]+$/.test(currentUrl)) ||
currentUrl.endsWith('/community') ||
currentUrl.includes('/community#');
@@ -176,7 +176,9 @@ Then('I should see the model description', async function (this: CustomWorld) {
// Model detail page shows description below the title, it might be a placeholder like "model.description"
// or actual content. Just verify the page structure is correct.
const descriptionArea = this.page.locator('main, article, [class*="detail"], [class*="content"]').first();
const descriptionArea = this.page
.locator('main, article, [class*="detail"], [class*="content"]')
.first();
const isVisible = await descriptionArea.isVisible().catch(() => false);
// Pass if any content area is visible - the description might be a placeholder

View File

@@ -376,11 +376,11 @@ Then('the URL should contain the category parameter', async function (this: Cust
console.log(` 📍 Selected category: ${this.testContext.selectedCategory}`);
// Check if URL contains a category-related parameter
// The URL format is: /community/assistant?category=xxx
// The URL format is: /community/agent?category=xxx
const hasCategory =
currentUrl.includes('category=') ||
currentUrl.includes('tag=') ||
// For path-based routing like /community/assistant/category-name
// For path-based routing like /community/agent/category-name
/\/community\/assistant\/[^/?]+/.test(currentUrl);
expect(
@@ -418,7 +418,7 @@ Then('the URL should contain the page parameter', async function (this: CustomWo
if (this.testContext.usedInfiniteScroll) {
console.log(' 📍 Used infinite scroll, page parameter not expected');
// Just verify we're still on the assistant page
expect(currentUrl.includes('/community/assistant')).toBeTruthy();
expect(currentUrl.includes('/community/agent')).toBeTruthy();
return;
}

View File

@@ -19,7 +19,7 @@ const PublishResultModal = memo<PublishResultModalProps>(({ identifier, onCancel
const handleGoToMarket = () => {
if (identifier) {
navigate(`/community/assistant/${identifier}`);
navigate(`/community/agent/${identifier}`);
}
onCancel();
};

View File

@@ -18,11 +18,23 @@ const Header = memo(() => {
const navigate = useNavigate();
const handleGoBack = () => {
// Extract the path segment (assistant, model, provider, mcp)
// Extract the path segment (agent, model, provider, mcp, group_agent, user)
const path = location.pathname.split('/').filter(Boolean);
if (path[1] && path[1] !== 'user') {
navigate(urlJoin('/community', path[1]));
const detailType = path[1];
// group_agent goes back to agent list page
if (detailType === 'group_agent') {
navigate('/community/agent');
return;
}
// Types that have their own list pages
const typesWithListPage = ['agent', 'model', 'provider', 'mcp'];
if (detailType && typesWithListPage.includes(detailType)) {
navigate(urlJoin('/community', detailType));
} else {
// For user or any other type without a list page
navigate('/community');
}
};

View File

@@ -38,7 +38,7 @@ const TagList = memo<{ tags: string[] }>(({ tags }) => {
q: tag,
source: marketSource,
},
url: '/community/assistant',
url: '/community/agent',
},
{ skipNull: true },
)}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
import { useQuery } from '@/hooks/useQuery';
import { type AssistantMarketSource } from '@/types/discover';
import McpList from '../../../../../(list)/assistant/features/List';
import McpList from '../../../../../(list)/agent/features/List';
import Title from '../../../../../features/Title';
import { useDetailContext } from '../../DetailProvider';
@@ -25,7 +25,7 @@ const Related = memo(() => {
category,
source: marketSource,
},
url: '/community/assistant',
url: '/community/agent',
},
{ skipNull: true },
)}

View File

@@ -38,7 +38,7 @@ const TagList = memo<{ tags: string[] }>(({ tags }) => {
q: tag,
source: marketSource,
},
url: '/community/assistant',
url: '/community/agent',
},
{ skipNull: true },
)}

View File

@@ -4,7 +4,7 @@ import { MessageCircleHeartIcon, MessageCircleQuestionIcon } from 'lucide-react'
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import TokenTag from '../../../../../(list)/assistant/features/List/TokenTag';
import TokenTag from '../../../../../(list)/agent/features/List/TokenTag';
import Title from '../../../../../features/Title';
import MarkdownRender from '../../../../features/MakedownRender';
import { useDetailContext } from '../../DetailProvider';

View File

@@ -32,7 +32,7 @@ import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
import { socialService } from '@/services/social';
import { formatIntergerNumber } from '@/utils/format';
import { useCategory } from '../../../(list)/assistant/features/Category/useCategory';
import { useCategory } from '../../../(list)/agent/features/Category/useCategory';
import PublishedTime from '../../../../../../../components/PublishedTime';
import { useDetailContext } from './DetailProvider';
@@ -142,7 +142,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
<Link
to={qs.stringifyUrl({
query: { category: cate?.key },
url: '/community/assistant',
url: '/community/agent',
})}
>
<Button icon={cate?.icon} size={'middle'} variant={'outlined'}>

View File

@@ -87,7 +87,7 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
event: 'add',
identifier,
source: location.pathname,
})
});
}
if (shouldNavigate) {

View File

@@ -21,7 +21,7 @@ const ActionButton = memo<{ mobile?: boolean }>(({ mobile }) => {
desc: description,
hashtags: tags,
title: title,
url: urlJoin(OFFICIAL_URL, '/community/assistant', identifier as string),
url: urlJoin(OFFICIAL_URL, '/community/agent', identifier as string),
}}
/>
</Flexbox>

View File

@@ -28,7 +28,7 @@ const Related = memo(() => {
category,
source: marketSource,
},
url: '/community/assistant',
url: '/community/agent',
},
{ skipNull: true },
)}
@@ -40,7 +40,7 @@ const Related = memo(() => {
const link = qs.stringifyUrl(
{
query: marketSource ? { source: marketSource } : undefined,
url: urlJoin('/community/assistant', item.identifier),
url: urlJoin('/community/agent', item.identifier),
},
{ skipNull: true },
);

View File

@@ -1,7 +1,7 @@
'use client';
import { ExclamationCircleOutlined, FolderOpenOutlined } from '@ant-design/icons';
import { FluentEmoji, Text , Button } from '@lobehub/ui';
import { Button, FluentEmoji, Text } from '@lobehub/ui';
import { Result } from 'antd';
import { memo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
@@ -16,7 +16,7 @@ const StatusPage = memo<StatusPageProps>(({ status }) => {
const { t } = useTranslation('discover');
const handleBackToMarket = () => {
navigate('/community/assistant');
navigate('/community/agent');
};
// 审核中状态

View File

@@ -142,7 +142,7 @@ const UserAgentCard = memo<UserAgentCardProps>(
const link = qs.stringifyUrl(
{
query: { source: 'new' },
url: urlJoin('/community/assistant', identifier),
url: urlJoin('/community/agent', identifier),
},
{ skipNull: true },
);
@@ -150,7 +150,7 @@ const UserAgentCard = memo<UserAgentCardProps>(
const isPublished = status === 'published';
const handleViewDetail = useCallback(() => {
window.open(urlJoin('/community/assistant', identifier), '_blank');
window.open(urlJoin('/community/agent', identifier), '_blank');
}, [identifier]);
const handleEdit = useCallback(async () => {

View File

@@ -96,7 +96,7 @@ const FavoriteAgentCard = memo<FavoriteAgentCardProps>(
const link = qs.stringifyUrl(
{
query: { source: 'new' },
url: urlJoin('/community/assistant', identifier),
url: urlJoin('/community/agent', identifier),
},
{ skipNull: true },
);

View File

@@ -7,7 +7,7 @@ import { useDiscoverStore } from '@/store/discover';
import { AssistantSorts, McpSorts } from '@/types/discover';
import Title from '../../components/Title';
import AssistantList from '../assistant/features/List';
import AssistantList from '../agent/features/List';
import McpList from '../mcp/features/List';
import Loading from './loading';
@@ -32,7 +32,7 @@ const HomePage = memo(() => {
return (
<>
<Title more={t('home.more')} moreLink={'/community/assistant'}>
<Title more={t('home.more')} moreLink={'/community/agent'}>
{t('home.featuredAssistants')}
</Title>
<AssistantList data={assistantList.items} rows={4} />

View File

@@ -9,7 +9,7 @@ const Loading = memo(() => {
return (
<>
<Title more={t('home.more')} moreLink={'/community/assistant'}>
<Title more={t('home.more')} moreLink={'/community/agent'}>
{t('home.featuredAssistants')}
</Title>
<ListLoading length={8} rows={4} />

View File

@@ -6,7 +6,11 @@ import { memo } from 'react';
import { withSuspense } from '@/components/withSuspense';
import { useQuery } from '@/hooks/useQuery';
import { useDiscoverStore } from '@/store/discover';
import { type AssistantMarketSource, type AssistantQueryParams, DiscoverTab } from '@/types/discover';
import {
type AssistantMarketSource,
type AssistantQueryParams,
DiscoverTab,
} from '@/types/discover';
import Pagination from '../features/Pagination';
import List from './features/List';

View File

@@ -29,7 +29,7 @@ const Category = memo(() => {
qs.stringifyUrl(
{
query: { category: key === AssistantCategory.All ? null : key, q, source },
url: '/community/assistant',
url: '/community/agent',
},
{ skipNull: true },
);

View File

@@ -73,7 +73,7 @@ const AssistantItem = memo<DiscoverAssistantItem>(
const navigate = useNavigate();
const { source } = useQuery() as { source?: AssistantMarketSource };
const isGroupAgent = type === 'agent-group';
const basePath = isGroupAgent ? '/community/group_agent' : '/community/assistant';
const basePath = isGroupAgent ? '/community/group_agent' : '/community/agent';
const link = qs.stringifyUrl(
{
query: { source },

View File

@@ -41,7 +41,7 @@ const MarketSourceSwitch = memo(() => {
);
const handleChange = (value: AssistantMarketSource) => {
router.push('/community/assistant', {
router.push('/community/agent', {
query: {
page: null,
source: value === 'new' ? null : value,

View File

@@ -3,12 +3,12 @@
import { Flexbox } from '@lobehub/ui';
import { McpIcon, ProviderIcon } from '@lobehub/ui/icons';
import { Bot, Brain, ShapesIcon } from 'lucide-react';
import { usePathname } from '@/libs/router/navigation';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom';
import NavItem, { type NavItemProps } from '@/features/NavPanel/components/NavItem';
import { usePathname } from '@/libs/router/navigation';
import { DiscoverTab } from '@/types/discover';
interface Item {
@@ -42,7 +42,7 @@ const Nav = memo(() => {
icon: Bot,
key: DiscoverTab.Assistants,
title: t('tab.assistant'),
url: '/community/assistant',
url: '/community/agent',
},
{
icon: McpIcon,

View File

@@ -41,7 +41,7 @@ const CommunityAgentsList = memo(() => {
color: 'inherit',
textDecoration: 'none',
}}
to={urlJoin('/community/assistant', item.identifier)}
to={urlJoin('/community/agent', item.identifier)}
>
<CommunityAgentItem {...item} />
</Link>

View File

@@ -24,7 +24,7 @@ const CommunityAgents = memo(() => {
key: 'all-assistants',
label: t('home.more'),
onClick: () => {
navigate('/community/assistant');
navigate('/community/agent');
},
},
]}

View File

@@ -15,7 +15,7 @@ const CloudBanner = dynamic(() => import('@/features/AlertBanner/CloudBanner'));
const MOBILE_NAV_ROUTES = new Set([
'/',
'/community',
'/community/assistant',
'/community/agent',
'/community/mcp',
'/community/plugin',
'/community/model',

View File

@@ -65,10 +65,10 @@ export const mobileRoutes: RouteConfig[] = [
children: [
{
element: dynamicElement(
() => import('../../(main)/community/(list)/assistant'),
'Mobile > Discover > List > Assistant',
() => import('../../(main)/community/(list)/agent'),
'Mobile > Discover > List > Agent',
),
path: 'assistant',
path: 'agent',
},
],
},
@@ -113,12 +113,12 @@ export const mobileRoutes: RouteConfig[] = [
{
element: dynamicElement(
() =>
import('../../(main)/community/(detail)/assistant').then(
import('../../(main)/community/(detail)/agent').then(
(m) => m.MobileDiscoverAssistantDetailPage,
),
'Mobile > Discover > Detail > Assistant',
'Mobile > Discover > Detail > Agent',
),
path: 'assistant/:slug',
path: 'agent/:slug',
},
{
element: dynamicElement(

View File

@@ -94,17 +94,17 @@ export const desktopRoutes: RouteConfig[] = [
children: [
{
element: dynamicElement(
() => import('../(main)/community/(list)/assistant'),
'Desktop > Discover > List > Assistant',
() => import('../(main)/community/(list)/agent'),
'Desktop > Discover > List > Agent',
),
index: true,
},
],
element: dynamicElement(
() => import('../(main)/community/(list)/assistant/_layout'),
'Desktop > Discover > List > Assistant > Layout',
() => import('../(main)/community/(list)/agent/_layout'),
'Desktop > Discover > List > Agent > Layout',
),
path: 'assistant',
path: 'agent',
},
{
children: [
@@ -163,10 +163,10 @@ export const desktopRoutes: RouteConfig[] = [
children: [
{
element: dynamicElement(
() => import('../(main)/community/(detail)/assistant'),
'Desktop > Discover > Detail > Assistant',
() => import('../(main)/community/(detail)/agent'),
'Desktop > Discover > Detail > Agent',
),
path: 'assistant/:slug',
path: 'agent/:slug',
},
{
element: dynamicElement(

View File

@@ -116,7 +116,7 @@ const ShareTopicLayout = memo<PropsWithChildren>(({ children }) => {
// If agent has marketIdentifier, render as link to assistant page
if (agentMarketIdentifier && !data?.groupMeta?.title) {
return (
<a href={`/community/assistant/${agentMarketIdentifier}`} rel="noreferrer" target="_blank">
<a href={`/community/agent/${agentMarketIdentifier}`} rel="noreferrer" target="_blank">
<Typography.Text ellipsis strong>
{agentOrGroupTitle}
</Typography.Text>

View File

@@ -101,7 +101,7 @@ const SearchResults = memo<SearchResultsProps>(
break;
}
case 'communityAgent': {
navigate(`/community/assistant/${result.identifier}`);
navigate(`/community/agent/${result.identifier}`);
break;
}
}

View File

@@ -79,7 +79,7 @@ const routePatterns: RoutePattern[] = [
// Community/Discover routes
{
icon: Compass,
test: (p) => p.startsWith('/community/assistant'),
test: (p) => p.startsWith('/community/agent'),
titleKey: 'navigation.discoverAssistants',
},
{

View File

@@ -267,7 +267,7 @@ export function defineConfig(config: CustomNextConfig) {
source: '/manifest.json',
},
{
destination: '/community/assistant',
destination: '/community/agent',
permanent: true,
source: '/community/assistants',
},

View File

@@ -59,7 +59,7 @@ describe('Sitemap', () => {
);
expect(pageSitemap).toContainEqual(
expect.objectContaining({
url: getCanonicalUrl('/community/assistant'),
url: getCanonicalUrl('/community/agent'),
changeFrequency: 'daily',
priority: 0.7,
}),
@@ -85,13 +85,13 @@ describe('Sitemap', () => {
expect(assistantsSitemap.length).toBe(LOCALE_COUNT);
expect(assistantsSitemap).toContainEqual(
expect.objectContaining({
url: getCanonicalUrl('/community/assistant/test-assistant'),
url: getCanonicalUrl('/community/agent/test-assistant'),
lastModified: '2023-01-01T00:00:00.000Z',
}),
);
expect(assistantsSitemap).toContainEqual(
expect.objectContaining({
url: getCanonicalUrl('/community/assistant/test-assistant?hl=zh-CN'),
url: getCanonicalUrl('/community/agent/test-assistant?hl=zh-CN'),
lastModified: '2023-01-01T00:00:00.000Z',
}),
);
@@ -113,7 +113,7 @@ describe('Sitemap', () => {
expect(firstPageSitemap.length).toBe(100 * LOCALE_COUNT); // 100 items * LOCALE_COUNT locales
expect(firstPageSitemap).toContainEqual(
expect.objectContaining({
url: getCanonicalUrl('/community/assistant/test-assistant-0'),
url: getCanonicalUrl('/community/agent/test-assistant-0'),
lastModified: '2023-01-01T00:00:00.000Z',
}),
);
@@ -123,7 +123,7 @@ describe('Sitemap', () => {
expect(secondPageSitemap.length).toBe(50 * LOCALE_COUNT); // 50 items * LOCALE_COUNT locales
expect(secondPageSitemap).toContainEqual(
expect.objectContaining({
url: getCanonicalUrl('/community/assistant/test-assistant-100'),
url: getCanonicalUrl('/community/agent/test-assistant-100'),
lastModified: '2023-01-01T00:00:00.000Z',
}),
);

View File

@@ -213,7 +213,7 @@ export class Sitemap {
const sitmap = pageAssistants
.filter((item) => item.identifier) // Filter out items with empty identifiers
.map((item) =>
this._genSitemap(urlJoin('/community/assistant', item.identifier), {
this._genSitemap(urlJoin('/community/agent', item.identifier), {
lastModified: item?.lastModified || LAST_MODIFIED,
}),
);
@@ -224,7 +224,7 @@ export class Sitemap {
const sitmap = list
.filter((item) => item.identifier) // 过滤掉 identifier 为空的项目
.map((item) =>
this._genSitemap(urlJoin('/community/assistant', item.identifier), {
this._genSitemap(urlJoin('/community/agent', item.identifier), {
lastModified: item?.lastModified || LAST_MODIFIED,
}),
);
@@ -311,7 +311,7 @@ export class Sitemap {
/* ↑ cloud slot ↑ */
...this._genSitemap('/community', { changeFrequency: 'daily', priority: 0.7 }),
...this._genSitemap('/community/assistant', { changeFrequency: 'daily', priority: 0.7 }),
...this._genSitemap('/community/agent', { changeFrequency: 'daily', priority: 0.7 }),
...this._genSitemap('/community/mcp', { changeFrequency: 'daily', priority: 0.7 }),
...this._genSitemap('/community/plugin', { changeFrequency: 'daily', priority: 0.7 }),
...this._genSitemap('/community/model', { changeFrequency: 'daily', priority: 0.7 }),