@lobehub/chat 1.19.3 → 1.19.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @lobehub/chat might be problematic. Click here for more details.

Files changed (30) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/next.config.mjs +10 -0
  3. package/package.json +6 -7
  4. package/scripts/buildSitemapIndex/index.ts +12 -0
  5. package/src/app/(main)/discover/(detail)/assistant/[slug]/page.tsx +1 -0
  6. package/src/app/(main)/discover/(detail)/model/[...slugs]/features/ProviderList/ProviderItem.tsx +1 -0
  7. package/src/app/(main)/discover/(detail)/model/[...slugs]/page.tsx +1 -0
  8. package/src/app/(main)/discover/(detail)/plugin/[slug]/page.tsx +1 -0
  9. package/src/app/(main)/discover/(detail)/provider/[slug]/features/ModelList/ModelItem.tsx +1 -0
  10. package/src/app/(main)/discover/(detail)/provider/[slug]/page.tsx +1 -0
  11. package/src/app/(main)/discover/(list)/_layout/Desktop/Nav.tsx +1 -0
  12. package/src/app/(main)/discover/_layout/Desktop/index.tsx +1 -0
  13. package/src/app/page.tsx +1 -1
  14. package/src/app/robots.tsx +16 -0
  15. package/src/app/sitemap.tsx +30 -0
  16. package/src/config/modelProviders/qwen.ts +112 -52
  17. package/src/config/modelProviders/stepfun.ts +8 -0
  18. package/src/const/url.ts +2 -2
  19. package/src/server/ld.test.ts +102 -0
  20. package/src/server/ld.ts +3 -9
  21. package/src/server/metadata.test.ts +138 -0
  22. package/src/server/metadata.ts +3 -3
  23. package/src/server/modules/AssistantStore/index.test.ts +1 -1
  24. package/src/server/modules/AssistantStore/index.ts +2 -6
  25. package/src/server/sitemap.test.ts +179 -0
  26. package/src/server/sitemap.ts +243 -0
  27. package/src/server/translation.test.ts +137 -0
  28. package/src/server/utils/url.test.ts +61 -0
  29. package/src/server/utils/url.ts +9 -0
  30. package/next-sitemap.config.mjs +0 -53
@@ -0,0 +1,102 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { DEFAULT_LANG } from '@/const/locale';
5
+
6
+ import { AUTHOR_LIST, Ld } from './ld';
7
+
8
+ describe('Ld', () => {
9
+ const ld = new Ld();
10
+
11
+ describe('generate', () => {
12
+ it('should generate correct LD+JSON structure', () => {
13
+ const result = ld.generate({
14
+ title: 'Test Title',
15
+ description: 'Test Description',
16
+ url: 'https://example.com/test',
17
+ locale: DEFAULT_LANG,
18
+ });
19
+
20
+ expect(result['@context']).toBe('https://schema.org');
21
+ expect(Array.isArray(result['@graph'])).toBe(true);
22
+ expect(result['@graph'].length).toBeGreaterThan(0);
23
+ });
24
+ });
25
+
26
+ describe('genOrganization', () => {
27
+ it('should generate correct organization structure', () => {
28
+ const org = ld.genOrganization();
29
+
30
+ expect(org['@type']).toBe('Organization');
31
+ expect(org.name).toBe('LobeHub');
32
+ expect(org.url).toBe('https://lobehub.com/');
33
+ });
34
+ });
35
+
36
+ describe('getAuthors', () => {
37
+ it('should return default author when no ids provided', () => {
38
+ const author = ld.getAuthors();
39
+ expect(author['@type']).toBe('Organization');
40
+ });
41
+
42
+ it('should return person when valid id provided', () => {
43
+ const author = ld.getAuthors(['arvinxx']);
44
+ expect(author['@type']).toBe('Person');
45
+ // @ts-ignore
46
+ expect(author.name).toBe(AUTHOR_LIST.arvinxx.name);
47
+ });
48
+ });
49
+
50
+ describe('genWebPage', () => {
51
+ it('should generate correct webpage structure', () => {
52
+ const webpage = ld.genWebPage({
53
+ title: 'Test Page',
54
+ description: 'Test Description',
55
+ url: 'https://example.com/test',
56
+ locale: DEFAULT_LANG,
57
+ });
58
+
59
+ expect(webpage['@type']).toBe('WebPage');
60
+ expect(webpage.name).toBe('Test Page · LobeChat');
61
+ expect(webpage.description).toBe('Test Description');
62
+ });
63
+ });
64
+
65
+ describe('genImageObject', () => {
66
+ it('should generate correct image object', () => {
67
+ const image = ld.genImageObject({
68
+ image: 'https://example.com/image.jpg',
69
+ url: 'https://example.com/test',
70
+ });
71
+
72
+ expect(image['@type']).toBe('ImageObject');
73
+ expect(image.url).toBe('https://example.com/image.jpg');
74
+ });
75
+ });
76
+
77
+ describe('genWebSite', () => {
78
+ it('should generate correct website structure', () => {
79
+ const website = ld.genWebSite();
80
+
81
+ expect(website['@type']).toBe('WebSite');
82
+ expect(website.name).toBe('LobeChat');
83
+ });
84
+ });
85
+
86
+ describe('genArticle', () => {
87
+ it('should generate correct article structure', () => {
88
+ const article = ld.genArticle({
89
+ title: 'Test Article',
90
+ description: 'Test Description',
91
+ url: 'https://example.com/test',
92
+ author: ['arvinxx'],
93
+ identifier: 'test-id',
94
+ locale: DEFAULT_LANG,
95
+ });
96
+
97
+ expect(article['@type']).toBe('Article');
98
+ expect(article.headline).toBe('Test Article · LobeChat');
99
+ expect(article.author['@type']).toBe('Person');
100
+ });
101
+ });
102
+ });
package/src/server/ld.ts CHANGED
@@ -3,15 +3,9 @@ import urlJoin from 'url-join';
3
3
 
4
4
  import { BRANDING_NAME } from '@/const/branding';
5
5
  import { DEFAULT_LANG } from '@/const/locale';
6
- import {
7
- EMAIL_BUSINESS,
8
- EMAIL_SUPPORT,
9
- OFFICIAL_SITE,
10
- OFFICIAL_URL,
11
- X,
12
- getCanonicalUrl,
13
- } from '@/const/url';
6
+ import { EMAIL_BUSINESS, EMAIL_SUPPORT, OFFICIAL_SITE, OFFICIAL_URL, X } from '@/const/url';
14
7
  import { Locales } from '@/locales/resources';
8
+ import { getCanonicalUrl } from '@/server/utils/url';
15
9
 
16
10
  import pkg from '../../package.json';
17
11
 
@@ -37,7 +31,7 @@ export const AUTHOR_LIST = {
37
31
  },
38
32
  };
39
33
 
40
- class Ld {
34
+ export class Ld {
41
35
  generate({
42
36
  image = '/og/cover.png',
43
37
  article,
@@ -0,0 +1,138 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { BRANDING_NAME } from '@/const/branding';
5
+ import { OG_URL } from '@/const/url';
6
+
7
+ import { Meta } from './metadata';
8
+
9
+ describe('Metadata', () => {
10
+ const meta = new Meta();
11
+
12
+ describe('generate', () => {
13
+ it('should generate metadata with default values', () => {
14
+ const result = meta.generate({
15
+ title: 'Test Title',
16
+ url: 'https://example.com',
17
+ });
18
+
19
+ expect(result).toMatchObject({
20
+ title: 'Test Title',
21
+ description: expect.any(String),
22
+ openGraph: expect.objectContaining({
23
+ title: `Test Title · ${BRANDING_NAME}`,
24
+ description: expect.any(String),
25
+ images: [{ url: OG_URL, alt: `Test Title · ${BRANDING_NAME}` }],
26
+ }),
27
+ twitter: expect.objectContaining({
28
+ title: `Test Title · ${BRANDING_NAME}`,
29
+ description: expect.any(String),
30
+ images: [OG_URL],
31
+ }),
32
+ });
33
+ });
34
+
35
+ it('should generate metadata with custom values', () => {
36
+ const result = meta.generate({
37
+ title: 'Custom Title',
38
+ description: 'Custom description',
39
+ image: 'https://custom-image.com',
40
+ url: 'https://example.com/custom',
41
+ type: 'article',
42
+ tags: ['tag1', 'tag2'],
43
+ locale: 'fr-FR',
44
+ alternate: true,
45
+ });
46
+
47
+ expect(result).toMatchObject({
48
+ title: 'Custom Title',
49
+ description: expect.stringContaining('Custom description'),
50
+ openGraph: expect.objectContaining({
51
+ title: `Custom Title · ${BRANDING_NAME}`,
52
+ description: 'Custom description',
53
+ images: [{ url: 'https://custom-image.com', alt: `Custom Title · ${BRANDING_NAME}` }],
54
+ type: 'article',
55
+ locale: 'fr-FR',
56
+ }),
57
+ twitter: expect.objectContaining({
58
+ title: `Custom Title · ${BRANDING_NAME}`,
59
+ description: 'Custom description',
60
+ images: ['https://custom-image.com'],
61
+ }),
62
+ alternates: expect.objectContaining({
63
+ languages: expect.any(Object),
64
+ }),
65
+ });
66
+ });
67
+ });
68
+
69
+ describe('genAlternateLocales', () => {
70
+ it('should generate alternate locales correctly', () => {
71
+ const result = (meta as any).genAlternateLocales('en', '/test');
72
+
73
+ expect(result).toHaveProperty('x-default', expect.stringContaining('/test'));
74
+ expect(result).toHaveProperty('zh-CN', expect.stringContaining('hl=zh-CN'));
75
+ expect(result).not.toHaveProperty('en');
76
+ });
77
+ });
78
+
79
+ describe('genTwitter', () => {
80
+ it('should generate Twitter metadata correctly', () => {
81
+ const result = (meta as any).genTwitter({
82
+ title: 'Twitter Title',
83
+ description: 'Twitter description',
84
+ image: 'https://twitter-image.com',
85
+ url: 'https://example.com/twitter',
86
+ });
87
+
88
+ expect(result).toEqual({
89
+ card: 'summary_large_image',
90
+ title: 'Twitter Title',
91
+ description: 'Twitter description',
92
+ images: ['https://twitter-image.com'],
93
+ site: '@lobehub',
94
+ url: 'https://example.com/twitter',
95
+ });
96
+ });
97
+ });
98
+
99
+ describe('genOpenGraph', () => {
100
+ it('should generate OpenGraph metadata correctly', () => {
101
+ const result = (meta as any).genOpenGraph({
102
+ title: 'OG Title',
103
+ description: 'OG description',
104
+ image: 'https://og-image.com',
105
+ url: 'https://example.com/og',
106
+ locale: 'es-ES',
107
+ type: 'article',
108
+ alternate: true,
109
+ });
110
+
111
+ expect(result).toMatchObject({
112
+ title: 'OG Title',
113
+ description: 'OG description',
114
+ images: [{ url: 'https://og-image.com', alt: 'OG Title' }],
115
+ locale: 'es-ES',
116
+ type: 'article',
117
+ url: 'https://example.com/og',
118
+ siteName: 'LobeChat',
119
+ alternateLocale: expect.arrayContaining([
120
+ 'ar',
121
+ 'bg-BG',
122
+ 'de-DE',
123
+ 'en-US',
124
+ 'es-ES',
125
+ 'fr-FR',
126
+ 'ja-JP',
127
+ 'ko-KR',
128
+ 'pt-BR',
129
+ 'ru-RU',
130
+ 'tr-TR',
131
+ 'zh-CN',
132
+ 'zh-TW',
133
+ 'vi-VN',
134
+ ]),
135
+ });
136
+ });
137
+ });
138
+ });
@@ -3,8 +3,9 @@ import qs from 'query-string';
3
3
 
4
4
  import { BRANDING_NAME } from '@/const/branding';
5
5
  import { DEFAULT_LANG } from '@/const/locale';
6
- import { OG_URL, getCanonicalUrl } from '@/const/url';
6
+ import { OG_URL } from '@/const/url';
7
7
  import { Locales, locales } from '@/locales/resources';
8
+ import { getCanonicalUrl } from '@/server/utils/url';
8
9
  import { formatDescLength, formatTitleLength } from '@/utils/genOG';
9
10
 
10
11
  export class Meta {
@@ -59,7 +60,6 @@ export class Meta {
59
60
  let links: any = {};
60
61
  const defaultLink = getCanonicalUrl(path);
61
62
  for (const alterLocales of locales) {
62
- if (locale === alterLocales) continue;
63
63
  links[alterLocales] = qs.stringifyUrl({
64
64
  query: { hl: alterLocales },
65
65
  url: defaultLink,
@@ -125,7 +125,7 @@ export class Meta {
125
125
  };
126
126
 
127
127
  if (alternate) {
128
- data['alternateLocale'] = locales.filter((l) => l !== locale);
128
+ data['alternateLocale'] = locales;
129
129
  }
130
130
 
131
131
  return data;
@@ -13,7 +13,7 @@ describe('AssistantStore', () => {
13
13
 
14
14
  it('should return the index URL for a not supported language', () => {
15
15
  const agentMarket = new AssistantStore();
16
- const url = agentMarket.getAgentIndexUrl('ko-KR');
16
+ const url = agentMarket.getAgentIndexUrl('xxx' as any);
17
17
  expect(url).toBe('https://chat-agents.lobehub.com');
18
18
  });
19
19
 
@@ -4,10 +4,6 @@ import { appEnv } from '@/config/app';
4
4
  import { DEFAULT_LANG, isLocaleNotSupport } from '@/const/locale';
5
5
  import { Locales, normalizeLocale } from '@/locales/resources';
6
6
 
7
- const checkSupportLocale = (lang: Locales) => {
8
- return isLocaleNotSupport(lang) || normalizeLocale(lang) !== 'zh-CN';
9
- };
10
-
11
7
  export class AssistantStore {
12
8
  private readonly baseUrl: string;
13
9
 
@@ -16,13 +12,13 @@ export class AssistantStore {
16
12
  }
17
13
 
18
14
  getAgentIndexUrl = (lang: Locales = DEFAULT_LANG) => {
19
- if (checkSupportLocale(lang)) return this.baseUrl;
15
+ if (isLocaleNotSupport(lang)) return this.baseUrl;
20
16
 
21
17
  return urlJoin(this.baseUrl, `index.${normalizeLocale(lang)}.json`);
22
18
  };
23
19
 
24
20
  getAgentUrl = (identifier: string, lang: Locales = DEFAULT_LANG) => {
25
- if (checkSupportLocale(lang)) return urlJoin(this.baseUrl, `${identifier}.json`);
21
+ if (isLocaleNotSupport(lang)) return urlJoin(this.baseUrl, `${identifier}.json`);
26
22
 
27
23
  return urlJoin(this.baseUrl, `${identifier}.${normalizeLocale(lang)}.json`);
28
24
  };
@@ -0,0 +1,179 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { getCanonicalUrl } from '@/server/utils/url';
5
+ import { AssistantCategory, PluginCategory } from '@/types/discover';
6
+
7
+ import { LAST_MODIFIED, Sitemap, SitemapType } from './sitemap';
8
+
9
+ describe('Sitemap', () => {
10
+ const sitemap = new Sitemap();
11
+
12
+ describe('getIndex', () => {
13
+ it('should return a valid sitemap index', () => {
14
+ const index = sitemap.getIndex();
15
+ expect(index).toContain('<?xml version="1.0" encoding="UTF-8"?>');
16
+ expect(index).toContain('<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
17
+ [
18
+ SitemapType.Pages,
19
+ SitemapType.Assistants,
20
+ SitemapType.Plugins,
21
+ SitemapType.Models,
22
+ SitemapType.Providers,
23
+ ].forEach((type) => {
24
+ expect(index).toContain(`<loc>${getCanonicalUrl(`/sitemap/${type}.xml`)}</loc>`);
25
+ });
26
+ expect(index).toContain(`<lastmod>${LAST_MODIFIED}</lastmod>`);
27
+ });
28
+ });
29
+
30
+ describe('getPage', () => {
31
+ it('should return a valid page sitemap', async () => {
32
+ const pageSitemap = await sitemap.getPage();
33
+ expect(pageSitemap).toContainEqual(
34
+ expect.objectContaining({
35
+ url: getCanonicalUrl('/'),
36
+ changeFrequency: 'monthly',
37
+ priority: 0.4,
38
+ }),
39
+ );
40
+ expect(pageSitemap).toContainEqual(
41
+ expect.objectContaining({
42
+ url: getCanonicalUrl('/discover'),
43
+ changeFrequency: 'daily',
44
+ priority: 0.7,
45
+ }),
46
+ );
47
+ Object.values(AssistantCategory).forEach((category) => {
48
+ expect(pageSitemap).toContainEqual(
49
+ expect.objectContaining({
50
+ url: getCanonicalUrl(`/discover/assistants/${category}`),
51
+ changeFrequency: 'daily',
52
+ priority: 0.7,
53
+ }),
54
+ );
55
+ });
56
+ Object.values(PluginCategory).forEach((category) => {
57
+ expect(pageSitemap).toContainEqual(
58
+ expect.objectContaining({
59
+ url: getCanonicalUrl(`/discover/plugins/${category}`),
60
+ changeFrequency: 'daily',
61
+ priority: 0.7,
62
+ }),
63
+ );
64
+ });
65
+ });
66
+ });
67
+
68
+ describe('getAssistants', () => {
69
+ it('should return a valid assistants sitemap', async () => {
70
+ vi.spyOn(sitemap['discoverService'], 'getAssistantList').mockResolvedValue([
71
+ // @ts-ignore
72
+ { identifier: 'test-assistant', createdAt: '2023-01-01' },
73
+ ]);
74
+
75
+ const assistantsSitemap = await sitemap.getAssistants();
76
+ expect(assistantsSitemap.length).toBe(14);
77
+ expect(assistantsSitemap).toContainEqual(
78
+ expect.objectContaining({
79
+ url: getCanonicalUrl('/discover/assistant/test-assistant'),
80
+ lastModified: '2023-01-01T00:00:00.000Z',
81
+ }),
82
+ );
83
+ expect(assistantsSitemap).toContainEqual(
84
+ expect.objectContaining({
85
+ url: getCanonicalUrl('/discover/assistant/test-assistant?hl=zh-CN'),
86
+ lastModified: '2023-01-01T00:00:00.000Z',
87
+ }),
88
+ );
89
+ });
90
+ });
91
+
92
+ describe('getPlugins', () => {
93
+ it('should return a valid plugins sitemap', async () => {
94
+ vi.spyOn(sitemap['discoverService'], 'getPluginList').mockResolvedValue([
95
+ // @ts-ignore
96
+ { identifier: 'test-plugin', createdAt: '2023-01-01' },
97
+ ]);
98
+
99
+ const pluginsSitemap = await sitemap.getPlugins();
100
+ expect(pluginsSitemap.length).toBe(14);
101
+ expect(pluginsSitemap).toContainEqual(
102
+ expect.objectContaining({
103
+ url: getCanonicalUrl('/discover/plugin/test-plugin'),
104
+ lastModified: '2023-01-01T00:00:00.000Z',
105
+ }),
106
+ );
107
+ expect(pluginsSitemap).toContainEqual(
108
+ expect.objectContaining({
109
+ url: getCanonicalUrl('/discover/plugin/test-plugin?hl=ja-JP'),
110
+ lastModified: '2023-01-01T00:00:00.000Z',
111
+ }),
112
+ );
113
+ });
114
+ });
115
+
116
+ describe('getModels', () => {
117
+ it('should return a valid models sitemap', async () => {
118
+ vi.spyOn(sitemap['discoverService'], 'getModelList').mockResolvedValue([
119
+ // @ts-ignore
120
+ { identifier: 'test:model', createdAt: '2023-01-01' },
121
+ ]);
122
+
123
+ const modelsSitemap = await sitemap.getModels();
124
+ expect(modelsSitemap.length).toBe(14);
125
+ expect(modelsSitemap).toContainEqual(
126
+ expect.objectContaining({
127
+ url: getCanonicalUrl('/discover/model/test:model'),
128
+ lastModified: '2023-01-01T00:00:00.000Z',
129
+ }),
130
+ );
131
+ expect(modelsSitemap).toContainEqual(
132
+ expect.objectContaining({
133
+ url: getCanonicalUrl('/discover/model/test:model?hl=ko-KR'),
134
+ lastModified: '2023-01-01T00:00:00.000Z',
135
+ }),
136
+ );
137
+ });
138
+ });
139
+
140
+ describe('getProviders', () => {
141
+ it('should return a valid providers sitemap', async () => {
142
+ vi.spyOn(sitemap['discoverService'], 'getProviderList').mockResolvedValue([
143
+ // @ts-ignore
144
+ { identifier: 'test-provider', createdAt: '2023-01-01' },
145
+ ]);
146
+
147
+ const providersSitemap = await sitemap.getProviders();
148
+ expect(providersSitemap.length).toBe(14);
149
+ expect(providersSitemap).toContainEqual(
150
+ expect.objectContaining({
151
+ url: getCanonicalUrl('/discover/provider/test-provider'),
152
+ lastModified: '2023-01-01T00:00:00.000Z',
153
+ }),
154
+ );
155
+ expect(providersSitemap).toContainEqual(
156
+ expect.objectContaining({
157
+ url: getCanonicalUrl('/discover/provider/test-provider?hl=ar'),
158
+ lastModified: '2023-01-01T00:00:00.000Z',
159
+ }),
160
+ );
161
+ });
162
+ });
163
+
164
+ describe('getRobots', () => {
165
+ it('should return correct robots.txt entries', () => {
166
+ const robots = sitemap.getRobots();
167
+ expect(robots).toContain(getCanonicalUrl('/sitemap-index.xml'));
168
+ [
169
+ SitemapType.Pages,
170
+ SitemapType.Assistants,
171
+ SitemapType.Plugins,
172
+ SitemapType.Models,
173
+ SitemapType.Providers,
174
+ ].forEach((type) => {
175
+ expect(robots).toContain(getCanonicalUrl(`/sitemap/${type}.xml`));
176
+ });
177
+ });
178
+ });
179
+ });