@lobehub/chat 1.19.17 → 1.19.18
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.
- package/CHANGELOG.md +17 -0
- package/next.config.mjs +46 -1
- package/package.json +1 -1
- package/src/app/(main)/discover/(detail)/_layout/Desktop.tsx +6 -2
- package/src/app/(main)/discover/(detail)/assistant/[slug]/page.tsx +1 -1
- package/src/app/(main)/discover/(detail)/model/[...slugs]/page.tsx +1 -1
- package/src/app/(main)/discover/(detail)/plugin/[slug]/page.tsx +1 -1
- package/src/app/(main)/discover/(detail)/provider/[slug]/page.tsx +1 -1
- package/src/app/manifest.ts +86 -0
- package/src/features/PWAInstall/index.tsx +5 -1
- package/src/server/manifest.test.ts +164 -0
- package/src/server/manifest.ts +101 -0
- package/src/server/metadata.ts +3 -1
- package/src/server/services/discover/index.test.ts +306 -0
- package/src/server/services/discover/index.ts +7 -0
- package/vitest.config.ts +1 -1
- package/public/manifest.json +0 -166
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,23 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.19.18](https://github.com/lobehub/lobe-chat/compare/v1.19.17...v1.19.18)
|
6
|
+
|
7
|
+
<sup>Released on **2024-09-21**</sup>
|
8
|
+
|
9
|
+
<br/>
|
10
|
+
|
11
|
+
<details>
|
12
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
13
|
+
|
14
|
+
</details>
|
15
|
+
|
16
|
+
<div align="right">
|
17
|
+
|
18
|
+
[](#readme-top)
|
19
|
+
|
20
|
+
</div>
|
21
|
+
|
5
22
|
### [Version 1.19.17](https://github.com/lobehub/lobe-chat/compare/v1.19.16...v1.19.17)
|
6
23
|
|
7
24
|
<sup>Released on **2024-09-21**</sup>
|
package/next.config.mjs
CHANGED
@@ -101,6 +101,19 @@ const nextConfig = {
|
|
101
101
|
],
|
102
102
|
source: '/apple-touch-icon.png',
|
103
103
|
},
|
104
|
+
{
|
105
|
+
headers: [
|
106
|
+
{
|
107
|
+
key: 'Content-Type',
|
108
|
+
value: 'application/javascript; charset=utf-8',
|
109
|
+
},
|
110
|
+
{
|
111
|
+
key: 'Content-Security-Policy',
|
112
|
+
value: "default-src 'self'; script-src 'self'",
|
113
|
+
},
|
114
|
+
],
|
115
|
+
source: '/sw.js',
|
116
|
+
},
|
104
117
|
];
|
105
118
|
},
|
106
119
|
|
@@ -113,10 +126,42 @@ const nextConfig = {
|
|
113
126
|
source: '/sitemap.xml',
|
114
127
|
},
|
115
128
|
{
|
116
|
-
destination: '/
|
129
|
+
destination: '/manifest.webmanifest',
|
130
|
+
permanent: true,
|
131
|
+
source: '/manifest.json',
|
132
|
+
},
|
133
|
+
{
|
134
|
+
destination: '/discover/assistant/:slug',
|
135
|
+
has: [
|
136
|
+
{
|
137
|
+
key: 'agent',
|
138
|
+
type: 'query',
|
139
|
+
value: '(?<slug>.*)',
|
140
|
+
},
|
141
|
+
],
|
117
142
|
permanent: true,
|
118
143
|
source: '/market',
|
119
144
|
},
|
145
|
+
{
|
146
|
+
destination: '/discover/assistants',
|
147
|
+
permanent: true,
|
148
|
+
source: '/discover/assistant',
|
149
|
+
},
|
150
|
+
{
|
151
|
+
destination: '/discover/models',
|
152
|
+
permanent: true,
|
153
|
+
source: '/discover/model',
|
154
|
+
},
|
155
|
+
{
|
156
|
+
destination: '/discover/plugins',
|
157
|
+
permanent: true,
|
158
|
+
source: '/discover/plugin',
|
159
|
+
},
|
160
|
+
{
|
161
|
+
destination: '/discover/providers',
|
162
|
+
permanent: true,
|
163
|
+
source: '/discover/provider',
|
164
|
+
},
|
120
165
|
{
|
121
166
|
destination: '/settings/common',
|
122
167
|
permanent: true,
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.19.
|
3
|
+
"version": "1.19.18",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -9,10 +9,14 @@ const Layout = ({ children }: PropsWithChildren) => {
|
|
9
9
|
align={'center'}
|
10
10
|
flex={1}
|
11
11
|
padding={24}
|
12
|
-
style={{ overflowX: 'hidden', overflowY: '
|
12
|
+
style={{ overflowX: 'hidden', overflowY: 'auto', position: 'relative' }}
|
13
13
|
width={'100%'}
|
14
14
|
>
|
15
|
-
<Flexbox
|
15
|
+
<Flexbox
|
16
|
+
gap={24}
|
17
|
+
style={{ maxWidth: MAX_WIDTH, minHeight: '100%', position: 'relative' }}
|
18
|
+
width={'100%'}
|
19
|
+
>
|
16
20
|
{children}
|
17
21
|
</Flexbox>
|
18
22
|
</Flexbox>
|
@@ -27,7 +27,7 @@ export const generateMetadata = async ({ params, searchParams }: Props) => {
|
|
27
27
|
|
28
28
|
const discoverService = new DiscoverService();
|
29
29
|
const data = await discoverService.getAssistantById(locale, identifier);
|
30
|
-
if (!data) return
|
30
|
+
if (!data) return;
|
31
31
|
|
32
32
|
const { meta, createdAt, homepage, author } = data;
|
33
33
|
|
@@ -27,7 +27,7 @@ export const generateMetadata = async ({ params, searchParams }: Props) => {
|
|
27
27
|
|
28
28
|
const discoverService = new DiscoverService();
|
29
29
|
const data = await discoverService.getModelById(locale, identifier);
|
30
|
-
if (!data) return
|
30
|
+
if (!data) return;
|
31
31
|
|
32
32
|
const { meta, createdAt, providers } = data;
|
33
33
|
|
@@ -24,7 +24,7 @@ export const generateMetadata = async ({ params, searchParams }: Props) => {
|
|
24
24
|
|
25
25
|
const discoverService = new DiscoverService();
|
26
26
|
const data = await discoverService.getPluginById(locale, identifier);
|
27
|
-
if (!data) return
|
27
|
+
if (!data) return;
|
28
28
|
|
29
29
|
const { meta, createdAt, homepage, author } = data;
|
30
30
|
|
@@ -27,7 +27,7 @@ export const generateMetadata = async ({ params, searchParams }: Props) => {
|
|
27
27
|
|
28
28
|
const discoverService = new DiscoverService();
|
29
29
|
const data = await discoverService.getProviderById(locale, identifier);
|
30
|
-
if (!data) return
|
30
|
+
if (!data) return;
|
31
31
|
|
32
32
|
const { meta, createdAt, models } = data;
|
33
33
|
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import { kebabCase } from 'lodash-es';
|
2
|
+
import type { MetadataRoute } from 'next';
|
3
|
+
|
4
|
+
import { BRANDING_LOGO_URL, BRANDING_NAME } from '@/const/branding';
|
5
|
+
import { manifestModule } from '@/server/manifest';
|
6
|
+
import { translation } from '@/server/translation';
|
7
|
+
|
8
|
+
const manifest = async (): Promise<MetadataRoute.Manifest | any> => {
|
9
|
+
const { t } = await translation('metadata');
|
10
|
+
|
11
|
+
return manifestModule.generate({
|
12
|
+
description: t('chat.description', { appName: BRANDING_NAME }),
|
13
|
+
icons: [
|
14
|
+
{
|
15
|
+
purpose: 'any',
|
16
|
+
sizes: '192x192',
|
17
|
+
url: '/icons/icon-192x192.png',
|
18
|
+
},
|
19
|
+
{
|
20
|
+
purpose: 'maskable',
|
21
|
+
sizes: '192x192',
|
22
|
+
url: '/icons/icon-192x192.maskable.png',
|
23
|
+
},
|
24
|
+
{
|
25
|
+
purpose: 'any',
|
26
|
+
sizes: '512x512',
|
27
|
+
url: '/icons/icon-512x512.png',
|
28
|
+
},
|
29
|
+
{
|
30
|
+
purpose: 'maskable',
|
31
|
+
sizes: '512x512',
|
32
|
+
url: '/icons/icon-512x512.maskable.png',
|
33
|
+
},
|
34
|
+
],
|
35
|
+
id: kebabCase(BRANDING_NAME),
|
36
|
+
name: BRANDING_NAME,
|
37
|
+
screenshots: BRANDING_LOGO_URL
|
38
|
+
? []
|
39
|
+
: [
|
40
|
+
{
|
41
|
+
form_factor: 'narrow',
|
42
|
+
url: '/screenshots/shot-1.mobile.png',
|
43
|
+
},
|
44
|
+
{
|
45
|
+
form_factor: 'narrow',
|
46
|
+
url: '/screenshots/shot-2.mobile.png',
|
47
|
+
},
|
48
|
+
{
|
49
|
+
form_factor: 'narrow',
|
50
|
+
sizes: '640x1138',
|
51
|
+
|
52
|
+
url: '/screenshots/shot-3.mobile.png',
|
53
|
+
},
|
54
|
+
{
|
55
|
+
form_factor: 'narrow',
|
56
|
+
url: '/screenshots/shot-4.mobile.png',
|
57
|
+
},
|
58
|
+
{
|
59
|
+
form_factor: 'narrow',
|
60
|
+
url: '/screenshots/shot-5.mobile.png',
|
61
|
+
},
|
62
|
+
{
|
63
|
+
form_factor: 'wide',
|
64
|
+
url: '/screenshots/shot-1.desktop.png',
|
65
|
+
},
|
66
|
+
{
|
67
|
+
form_factor: 'wide',
|
68
|
+
url: '/screenshots/shot-2.desktop.png',
|
69
|
+
},
|
70
|
+
{
|
71
|
+
form_factor: 'wide',
|
72
|
+
url: '/screenshots/shot-3.desktop.png',
|
73
|
+
},
|
74
|
+
{
|
75
|
+
form_factor: 'wide',
|
76
|
+
url: '/screenshots/shot-4.desktop.png',
|
77
|
+
},
|
78
|
+
{
|
79
|
+
form_factor: 'wide',
|
80
|
+
url: '/screenshots/shot-5.desktop.png',
|
81
|
+
},
|
82
|
+
],
|
83
|
+
});
|
84
|
+
};
|
85
|
+
|
86
|
+
export default manifest;
|
@@ -68,7 +68,11 @@ const PWAInstall = memo(() => {
|
|
68
68
|
|
69
69
|
if (isPWA) return null;
|
70
70
|
return (
|
71
|
-
<PWA
|
71
|
+
<PWA
|
72
|
+
description={t('chat.description', { appName: BRANDING_NAME })}
|
73
|
+
id={PWA_INSTALL_ID}
|
74
|
+
manifest-url={'/manifest.webmanifest'}
|
75
|
+
/>
|
72
76
|
);
|
73
77
|
});
|
74
78
|
|
@@ -0,0 +1,164 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import qs from 'query-string';
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
4
|
+
|
5
|
+
import { BRANDING_LOGO_URL } from '@/const/branding';
|
6
|
+
import { getCanonicalUrl } from '@/server/utils/url';
|
7
|
+
|
8
|
+
import { Manifest, manifestModule } from './manifest';
|
9
|
+
|
10
|
+
// Mock external dependencies
|
11
|
+
vi.mock('@/const/branding', () => ({
|
12
|
+
BRANDING_LOGO_URL: 'https://example.com/logo.png',
|
13
|
+
}));
|
14
|
+
|
15
|
+
vi.mock('@/server/utils/url', () => ({
|
16
|
+
getCanonicalUrl: vi.fn().mockReturnValue('https://example.com/manifest.webmanifest'),
|
17
|
+
}));
|
18
|
+
|
19
|
+
describe('Manifest', () => {
|
20
|
+
const manifest = new Manifest();
|
21
|
+
|
22
|
+
describe('generate', () => {
|
23
|
+
it('should generate a valid manifest object', () => {
|
24
|
+
const input = {
|
25
|
+
color: '#FF0000',
|
26
|
+
description: 'Test description',
|
27
|
+
name: 'Test App',
|
28
|
+
id: 'test-app',
|
29
|
+
icons: [{ purpose: 'any' as const, sizes: '192x192', url: 'icon.png' }],
|
30
|
+
screenshots: [{ form_factor: 'wide' as const, url: 'screenshot.png' }],
|
31
|
+
};
|
32
|
+
|
33
|
+
const result = manifest.generate(input);
|
34
|
+
|
35
|
+
expect(result).toMatchObject({
|
36
|
+
background_color: input.color,
|
37
|
+
description: input.description,
|
38
|
+
name: input.name,
|
39
|
+
id: input.id,
|
40
|
+
icons: expect.arrayContaining([
|
41
|
+
expect.objectContaining({
|
42
|
+
purpose: 'any',
|
43
|
+
sizes: '192x192',
|
44
|
+
}),
|
45
|
+
]),
|
46
|
+
screenshots: expect.arrayContaining([
|
47
|
+
expect.objectContaining({
|
48
|
+
form_factor: 'wide',
|
49
|
+
sizes: '1280x676',
|
50
|
+
}),
|
51
|
+
]),
|
52
|
+
});
|
53
|
+
});
|
54
|
+
|
55
|
+
it('should use default color if not provided', () => {
|
56
|
+
const input = {
|
57
|
+
description: 'Test description',
|
58
|
+
name: 'Test App',
|
59
|
+
id: 'test-app',
|
60
|
+
icons: [],
|
61
|
+
screenshots: [],
|
62
|
+
};
|
63
|
+
|
64
|
+
const result = manifest.generate(input);
|
65
|
+
|
66
|
+
expect(result.background_color).toBe('#000000');
|
67
|
+
expect(result.theme_color).toBe('#000000');
|
68
|
+
});
|
69
|
+
});
|
70
|
+
|
71
|
+
describe('_getImage', () => {
|
72
|
+
it('should return correct image object', () => {
|
73
|
+
const url = 'https://example.com/image.png';
|
74
|
+
const version = 2;
|
75
|
+
|
76
|
+
// @ts-ignore - Accessing private method for testing
|
77
|
+
const result = manifest._getImage(url, version);
|
78
|
+
|
79
|
+
expect(result).toEqual({
|
80
|
+
cache_busting_mode: 'query',
|
81
|
+
immutable: 'true',
|
82
|
+
max_age: 31536000,
|
83
|
+
src: qs.stringifyUrl({ query: { v: version }, url: BRANDING_LOGO_URL || url }),
|
84
|
+
});
|
85
|
+
});
|
86
|
+
|
87
|
+
it('should use default version if not provided', () => {
|
88
|
+
const url = 'https://example.com/image.png';
|
89
|
+
|
90
|
+
// @ts-ignore - Accessing private method for testing
|
91
|
+
const result = manifest._getImage(url);
|
92
|
+
|
93
|
+
expect(result.src).toContain('v=1');
|
94
|
+
});
|
95
|
+
});
|
96
|
+
|
97
|
+
describe('_getIcon', () => {
|
98
|
+
it('should return correct icon object', () => {
|
99
|
+
const icon = {
|
100
|
+
url: 'https://example.com/icon.png',
|
101
|
+
version: 3,
|
102
|
+
sizes: '64x64',
|
103
|
+
purpose: 'maskable' as const,
|
104
|
+
};
|
105
|
+
|
106
|
+
// @ts-ignore - Accessing private method for testing
|
107
|
+
const result = manifest._getIcon(icon);
|
108
|
+
|
109
|
+
expect(result).toMatchObject({
|
110
|
+
purpose: 'maskable',
|
111
|
+
sizes: '64x64',
|
112
|
+
type: 'image/png',
|
113
|
+
});
|
114
|
+
expect(result.src).toContain('v=3');
|
115
|
+
});
|
116
|
+
});
|
117
|
+
|
118
|
+
describe('_getScreenshot', () => {
|
119
|
+
it('should return correct screenshot object for wide form factor', () => {
|
120
|
+
const screenshot = {
|
121
|
+
form_factor: 'wide' as const,
|
122
|
+
url: 'https://example.com/screenshot.png',
|
123
|
+
version: 4,
|
124
|
+
};
|
125
|
+
|
126
|
+
// @ts-ignore - Accessing private method for testing
|
127
|
+
const result = manifest._getScreenshot(screenshot);
|
128
|
+
|
129
|
+
expect(result).toMatchObject({
|
130
|
+
form_factor: 'wide',
|
131
|
+
sizes: '1280x676',
|
132
|
+
type: 'image/png',
|
133
|
+
});
|
134
|
+
expect(result.src).toContain('v=4');
|
135
|
+
});
|
136
|
+
|
137
|
+
it('should return correct screenshot object for narrow form factor', () => {
|
138
|
+
const screenshot = {
|
139
|
+
form_factor: 'narrow' as const,
|
140
|
+
url: 'https://example.com/screenshot.png',
|
141
|
+
sizes: '320x569',
|
142
|
+
};
|
143
|
+
|
144
|
+
// @ts-ignore - Accessing private method for testing
|
145
|
+
const result = manifest._getScreenshot(screenshot);
|
146
|
+
|
147
|
+
expect(result).toMatchObject({
|
148
|
+
cache_busting_mode: 'query',
|
149
|
+
form_factor: 'narrow',
|
150
|
+
immutable: 'true',
|
151
|
+
max_age: 31536000,
|
152
|
+
sizes: '1280x676',
|
153
|
+
src: 'https://example.com/logo.png?v=1',
|
154
|
+
type: 'image/png',
|
155
|
+
});
|
156
|
+
});
|
157
|
+
});
|
158
|
+
});
|
159
|
+
|
160
|
+
describe('manifestModule', () => {
|
161
|
+
it('should be an instance of Manifest', () => {
|
162
|
+
expect(manifestModule).toBeInstanceOf(Manifest);
|
163
|
+
});
|
164
|
+
});
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import qs from 'query-string';
|
2
|
+
|
3
|
+
import { BRANDING_LOGO_URL } from '@/const/branding';
|
4
|
+
import { getCanonicalUrl } from '@/server/utils/url';
|
5
|
+
|
6
|
+
const MAX_AGE = 31_536_000;
|
7
|
+
const COLOR = '#000000';
|
8
|
+
|
9
|
+
interface IconItem {
|
10
|
+
purpose: 'any' | 'maskable';
|
11
|
+
sizes: string;
|
12
|
+
url: string;
|
13
|
+
version?: number;
|
14
|
+
}
|
15
|
+
|
16
|
+
interface ScreenshotItem {
|
17
|
+
form_factor: 'wide' | 'narrow';
|
18
|
+
sizes?: string;
|
19
|
+
url: string;
|
20
|
+
version?: number;
|
21
|
+
}
|
22
|
+
|
23
|
+
export class Manifest {
|
24
|
+
public generate({
|
25
|
+
color = COLOR,
|
26
|
+
description,
|
27
|
+
name,
|
28
|
+
id,
|
29
|
+
icons,
|
30
|
+
screenshots,
|
31
|
+
}: {
|
32
|
+
color?: string;
|
33
|
+
description: string;
|
34
|
+
icons: IconItem[];
|
35
|
+
id: string;
|
36
|
+
name: string;
|
37
|
+
screenshots: ScreenshotItem[];
|
38
|
+
}) {
|
39
|
+
return {
|
40
|
+
background_color: color,
|
41
|
+
cache_busting_mode: 'all',
|
42
|
+
categories: ['productivity', 'design', 'development', 'education'],
|
43
|
+
description: description,
|
44
|
+
display: 'standalone',
|
45
|
+
display_override: ['tabbed'],
|
46
|
+
edge_side_panel: {
|
47
|
+
preferred_width: 480,
|
48
|
+
},
|
49
|
+
handle_links: 'auto',
|
50
|
+
icons: icons.map((item) => this._getIcon(item)),
|
51
|
+
id: id,
|
52
|
+
immutable: 'true',
|
53
|
+
launch_handler: {
|
54
|
+
client_mode: ['navigate-existing', 'auto'],
|
55
|
+
},
|
56
|
+
max_age: MAX_AGE,
|
57
|
+
name: name,
|
58
|
+
orientation: 'portrait',
|
59
|
+
related_applications: [
|
60
|
+
{
|
61
|
+
platform: 'webapp',
|
62
|
+
url: getCanonicalUrl('manifest.webmanifest'),
|
63
|
+
},
|
64
|
+
],
|
65
|
+
scope: '/',
|
66
|
+
screenshots: screenshots.map((item) => this._getScreenshot(item)),
|
67
|
+
short_name: name,
|
68
|
+
splash_pages: null,
|
69
|
+
start_url: '.',
|
70
|
+
tab_strip: {
|
71
|
+
new_tab_button: {
|
72
|
+
url: '/',
|
73
|
+
},
|
74
|
+
},
|
75
|
+
theme_color: color,
|
76
|
+
};
|
77
|
+
}
|
78
|
+
|
79
|
+
private _getImage = (url: string, version: number = 1) => ({
|
80
|
+
cache_busting_mode: 'query',
|
81
|
+
immutable: 'true',
|
82
|
+
max_age: MAX_AGE,
|
83
|
+
src: qs.stringifyUrl({ query: { v: version }, url: BRANDING_LOGO_URL || url }),
|
84
|
+
});
|
85
|
+
|
86
|
+
private _getIcon = ({ url, version, sizes, purpose }: IconItem) => ({
|
87
|
+
...this._getImage(url, version),
|
88
|
+
purpose,
|
89
|
+
sizes,
|
90
|
+
type: 'image/png',
|
91
|
+
});
|
92
|
+
|
93
|
+
private _getScreenshot = ({ form_factor, url, version, sizes }: ScreenshotItem) => ({
|
94
|
+
...this._getImage(url, version),
|
95
|
+
form_factor,
|
96
|
+
sizes: sizes || form_factor === 'wide' ? '1280x676' : '640x1138',
|
97
|
+
type: 'image/png',
|
98
|
+
});
|
99
|
+
}
|
100
|
+
|
101
|
+
export const manifestModule = new Manifest();
|
package/src/server/metadata.ts
CHANGED
@@ -35,7 +35,9 @@ export class Meta {
|
|
35
35
|
const siteTitle = title.includes(BRANDING_NAME) ? title : title + ` · ${BRANDING_NAME}`;
|
36
36
|
return {
|
37
37
|
alternates: {
|
38
|
-
canonical: getCanonicalUrl(
|
38
|
+
canonical: getCanonicalUrl(
|
39
|
+
alternate ? qs.stringifyUrl({ query: { hl: locale }, url }) : url,
|
40
|
+
),
|
39
41
|
languages: alternate ? this.genAlternateLocales(locale, url) : undefined,
|
40
42
|
},
|
41
43
|
description: formatedDescription,
|
@@ -0,0 +1,306 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { DEFAULT_LANG } from '@/const/locale';
|
5
|
+
import { AssistantCategory, PluginCategory } from '@/types/discover';
|
6
|
+
|
7
|
+
import { DiscoverService } from './index';
|
8
|
+
|
9
|
+
// 模拟 fetch 函数
|
10
|
+
global.fetch = vi.fn();
|
11
|
+
|
12
|
+
describe('DiscoverService', () => {
|
13
|
+
let service: DiscoverService;
|
14
|
+
|
15
|
+
beforeEach(() => {
|
16
|
+
service = new DiscoverService();
|
17
|
+
vi.resetAllMocks();
|
18
|
+
});
|
19
|
+
|
20
|
+
describe('Assistants', () => {
|
21
|
+
it('should search assistants', async () => {
|
22
|
+
const mockAssistants = [
|
23
|
+
{
|
24
|
+
author: 'John',
|
25
|
+
meta: { title: 'Test Assistant', description: 'A test assistant', tags: ['test'] },
|
26
|
+
},
|
27
|
+
{
|
28
|
+
author: 'Jane',
|
29
|
+
meta: {
|
30
|
+
title: 'Another Assistant',
|
31
|
+
description: 'Another test assistant',
|
32
|
+
tags: ['demo'],
|
33
|
+
},
|
34
|
+
},
|
35
|
+
];
|
36
|
+
|
37
|
+
vi.spyOn(service, 'getAssistantList').mockResolvedValue(mockAssistants as any);
|
38
|
+
|
39
|
+
const result = await service.searchAssistant('en-US', 'A test assistant');
|
40
|
+
expect(result).toHaveLength(1);
|
41
|
+
expect(result[0].author).toBe('John');
|
42
|
+
});
|
43
|
+
|
44
|
+
it('should get assistant category', async () => {
|
45
|
+
const mockAssistants = [
|
46
|
+
{ meta: { category: AssistantCategory.General } },
|
47
|
+
{ meta: { category: AssistantCategory.Academic } },
|
48
|
+
];
|
49
|
+
|
50
|
+
vi.spyOn(service, 'getAssistantList').mockResolvedValue(mockAssistants as any);
|
51
|
+
|
52
|
+
const result = await service.getAssistantCategory('en-US', AssistantCategory.General);
|
53
|
+
expect(result).toHaveLength(1);
|
54
|
+
expect(result[0].meta.category).toBe(AssistantCategory.General);
|
55
|
+
});
|
56
|
+
|
57
|
+
it('should get assistant list', async () => {
|
58
|
+
const mockResponse = { agents: [{ id: 'test-assistant' }] };
|
59
|
+
vi.spyOn(global, 'fetch').mockResolvedValueOnce({
|
60
|
+
ok: true,
|
61
|
+
json: vi.fn().mockResolvedValue(mockResponse),
|
62
|
+
} as any);
|
63
|
+
|
64
|
+
const result = await service.getAssistantList('en-US');
|
65
|
+
expect(result).toEqual(mockResponse.agents);
|
66
|
+
});
|
67
|
+
|
68
|
+
it('should get assistant by id', async () => {
|
69
|
+
const mockAssistant = {
|
70
|
+
identifier: 'test-assistant',
|
71
|
+
meta: { category: AssistantCategory.General },
|
72
|
+
};
|
73
|
+
|
74
|
+
vi.spyOn(global, 'fetch').mockResolvedValueOnce({
|
75
|
+
ok: true,
|
76
|
+
json: vi.fn().mockResolvedValue(mockAssistant),
|
77
|
+
} as any);
|
78
|
+
|
79
|
+
vi.spyOn(service, 'getAssistantCategory').mockResolvedValue([]);
|
80
|
+
|
81
|
+
const result = await service.getAssistantById('en-US', 'test-assistant');
|
82
|
+
|
83
|
+
expect(result).toBeDefined();
|
84
|
+
expect(result?.identifier).toBe('test-assistant');
|
85
|
+
});
|
86
|
+
|
87
|
+
it('should get assistants by ids', async () => {
|
88
|
+
const mockAssistants = [{ identifier: 'assistant1' }, { identifier: 'assistant2' }];
|
89
|
+
|
90
|
+
vi.spyOn(service, 'getAssistantById').mockImplementation(
|
91
|
+
async (_, id) => mockAssistants.find((a) => a.identifier === id) as any,
|
92
|
+
);
|
93
|
+
|
94
|
+
const result = await service.getAssistantByIds('en-US', [
|
95
|
+
'assistant1',
|
96
|
+
'assistant2',
|
97
|
+
'nonexistent',
|
98
|
+
]);
|
99
|
+
expect(result).toHaveLength(2);
|
100
|
+
expect(result[0].identifier).toBe('assistant1');
|
101
|
+
expect(result[1].identifier).toBe('assistant2');
|
102
|
+
});
|
103
|
+
});
|
104
|
+
|
105
|
+
describe('Plugins', () => {
|
106
|
+
it('should search plugins', async () => {
|
107
|
+
const mockPlugins = [
|
108
|
+
{
|
109
|
+
author: 'John',
|
110
|
+
meta: { title: 'Test Plugin', description: 'A test plugin', tags: ['test'] },
|
111
|
+
},
|
112
|
+
{
|
113
|
+
author: 'Jane',
|
114
|
+
meta: { title: 'Another Plugin', description: 'Another test plugin', tags: ['demo'] },
|
115
|
+
},
|
116
|
+
];
|
117
|
+
|
118
|
+
vi.spyOn(service, 'getPluginList').mockResolvedValue(mockPlugins as any);
|
119
|
+
|
120
|
+
const result = await service.searchPlugin('en-US', 'A test plugin');
|
121
|
+
expect(result).toHaveLength(1);
|
122
|
+
expect(result[0].author).toBe('John');
|
123
|
+
});
|
124
|
+
|
125
|
+
it('should get plugin category', async () => {
|
126
|
+
const mockPlugins = [
|
127
|
+
{ meta: { category: PluginCategory.Tools } },
|
128
|
+
{ meta: { category: PluginCategory.Social } },
|
129
|
+
];
|
130
|
+
|
131
|
+
vi.spyOn(service, 'getPluginList').mockResolvedValue(mockPlugins as any);
|
132
|
+
|
133
|
+
const result = await service.getPluginCategory('en-US', PluginCategory.Tools);
|
134
|
+
expect(result).toHaveLength(1);
|
135
|
+
expect(result[0].meta.category).toBe(PluginCategory.Tools);
|
136
|
+
});
|
137
|
+
|
138
|
+
it('should get plugin list', async () => {
|
139
|
+
const mockResponse = { plugins: [{ id: 'test-plugin' }] };
|
140
|
+
vi.spyOn(global, 'fetch').mockResolvedValueOnce({
|
141
|
+
ok: true,
|
142
|
+
json: vi.fn().mockResolvedValue(mockResponse),
|
143
|
+
} as any);
|
144
|
+
|
145
|
+
const result = await service.getPluginList('en-US');
|
146
|
+
expect(result).toEqual(mockResponse.plugins);
|
147
|
+
});
|
148
|
+
|
149
|
+
it('should get plugin by id', async () => {
|
150
|
+
const mockPlugin = {
|
151
|
+
identifier: 'test-plugin',
|
152
|
+
meta: { category: PluginCategory.Tools },
|
153
|
+
};
|
154
|
+
|
155
|
+
vi.spyOn(service, 'getPluginList').mockResolvedValue([mockPlugin] as any);
|
156
|
+
vi.spyOn(service, 'getPluginCategory').mockResolvedValue([]);
|
157
|
+
|
158
|
+
const result = await service.getPluginById('en-US', 'test-plugin');
|
159
|
+
|
160
|
+
expect(result).toBeDefined();
|
161
|
+
expect(result?.identifier).toBe('test-plugin');
|
162
|
+
});
|
163
|
+
|
164
|
+
it('should get plugins by ids', async () => {
|
165
|
+
const mockPlugins = [{ identifier: 'plugin1' }, { identifier: 'plugin2' }];
|
166
|
+
|
167
|
+
vi.spyOn(service, 'getPluginById').mockImplementation(
|
168
|
+
async (_, id) => mockPlugins.find((p) => p.identifier === id) as any,
|
169
|
+
);
|
170
|
+
|
171
|
+
const result = await service.getPluginByIds('en-US', ['plugin1', 'plugin2', 'nonexistent']);
|
172
|
+
expect(result).toHaveLength(2);
|
173
|
+
expect(result[0].identifier).toBe('plugin1');
|
174
|
+
expect(result[1].identifier).toBe('plugin2');
|
175
|
+
});
|
176
|
+
});
|
177
|
+
|
178
|
+
describe('Providers', () => {
|
179
|
+
it('should get provider list', async () => {
|
180
|
+
const result = await service.getProviderList('en-US');
|
181
|
+
expect(result).toBeDefined();
|
182
|
+
expect(Array.isArray(result)).toBe(true);
|
183
|
+
expect(result.length).toBeGreaterThan(0);
|
184
|
+
expect(result[0]).toHaveProperty('identifier');
|
185
|
+
expect(result[0]).toHaveProperty('meta');
|
186
|
+
expect(result[0]).toHaveProperty('models');
|
187
|
+
});
|
188
|
+
|
189
|
+
it('should search providers', async () => {
|
190
|
+
const mockProviders = [
|
191
|
+
{ identifier: 'provider1', meta: { title: 'Test Provider' } },
|
192
|
+
{ identifier: 'provider2', meta: { title: 'Another Provider' } },
|
193
|
+
];
|
194
|
+
|
195
|
+
vi.spyOn(service, 'getProviderList').mockResolvedValue(mockProviders as any);
|
196
|
+
|
197
|
+
const result = await service.searchProvider('en-US', 'test');
|
198
|
+
expect(result).toHaveLength(1);
|
199
|
+
expect(result[0].identifier).toBe('provider1');
|
200
|
+
});
|
201
|
+
|
202
|
+
it('should get provider by id', async () => {
|
203
|
+
const mockProvider = {
|
204
|
+
identifier: 'test-provider',
|
205
|
+
meta: { title: 'Test Provider' },
|
206
|
+
models: ['model1', 'model2'],
|
207
|
+
};
|
208
|
+
|
209
|
+
vi.spyOn(service, 'getProviderList').mockResolvedValue([mockProvider] as any);
|
210
|
+
|
211
|
+
const result = await service.getProviderById('en-US', 'test-provider');
|
212
|
+
|
213
|
+
expect(result).toBeDefined();
|
214
|
+
expect(result?.identifier).toBe('test-provider');
|
215
|
+
});
|
216
|
+
|
217
|
+
it('should get providers by ids', async () => {
|
218
|
+
const mockProviders = [{ identifier: 'provider1' }, { identifier: 'provider2' }];
|
219
|
+
|
220
|
+
vi.spyOn(service, 'getProviderById').mockImplementation(
|
221
|
+
async (_, id) => mockProviders.find((p) => p.identifier === id) as any,
|
222
|
+
);
|
223
|
+
|
224
|
+
const result = await service.getProviderByIds('en-US', [
|
225
|
+
'provider1',
|
226
|
+
'provider2',
|
227
|
+
'nonexistent',
|
228
|
+
]);
|
229
|
+
expect(result).toHaveLength(2);
|
230
|
+
expect(result[0].identifier).toBe('provider1');
|
231
|
+
expect(result[1].identifier).toBe('provider2');
|
232
|
+
});
|
233
|
+
});
|
234
|
+
|
235
|
+
describe('Models', () => {
|
236
|
+
it('should get model list', async () => {
|
237
|
+
const result = await service.getModelList('en-US');
|
238
|
+
expect(result).toBeDefined();
|
239
|
+
expect(Array.isArray(result)).toBe(true);
|
240
|
+
expect(result.length).toBeGreaterThan(0);
|
241
|
+
expect(result[0]).toHaveProperty('identifier');
|
242
|
+
expect(result[0]).toHaveProperty('meta');
|
243
|
+
expect(result[0]).toHaveProperty('providers');
|
244
|
+
});
|
245
|
+
|
246
|
+
it('should search models', async () => {
|
247
|
+
const mockModels = [
|
248
|
+
{
|
249
|
+
identifier: 'model1',
|
250
|
+
meta: { title: 'Test Model', description: 'A test model' },
|
251
|
+
providers: ['provider1'],
|
252
|
+
},
|
253
|
+
{
|
254
|
+
identifier: 'model2',
|
255
|
+
meta: { title: 'Another Model', description: 'Another test model' },
|
256
|
+
providers: ['provider2'],
|
257
|
+
},
|
258
|
+
];
|
259
|
+
|
260
|
+
vi.spyOn(service as any, '_getModelList').mockResolvedValue(mockModels);
|
261
|
+
|
262
|
+
const result = await service.searchModel('en-US', 'A test model');
|
263
|
+
expect(result).toHaveLength(1);
|
264
|
+
expect(result[0].identifier).toBe('model1');
|
265
|
+
});
|
266
|
+
|
267
|
+
it('should get model category', async () => {
|
268
|
+
const mockModels = [{ meta: { category: 'category1' } }, { meta: { category: 'category2' } }];
|
269
|
+
|
270
|
+
vi.spyOn(service as any, '_getModelList').mockResolvedValue(mockModels);
|
271
|
+
|
272
|
+
const result = await service.getModelCategory('en-US', 'category1');
|
273
|
+
expect(result).toHaveLength(1);
|
274
|
+
expect(result[0].meta.category).toBe('category1');
|
275
|
+
});
|
276
|
+
|
277
|
+
it('should get model by id', async () => {
|
278
|
+
const mockModel = {
|
279
|
+
identifier: 'test-model',
|
280
|
+
meta: { category: 'test-category' },
|
281
|
+
providers: ['provider1'],
|
282
|
+
};
|
283
|
+
|
284
|
+
vi.spyOn(service, 'getModelList').mockResolvedValue([mockModel] as any);
|
285
|
+
vi.spyOn(service, 'getModelCategory').mockResolvedValue([]);
|
286
|
+
|
287
|
+
const result = await service.getModelById('en-US', 'test-model');
|
288
|
+
|
289
|
+
expect(result).toBeDefined();
|
290
|
+
expect(result?.identifier).toBe('test-model');
|
291
|
+
});
|
292
|
+
|
293
|
+
it('should get models by ids', async () => {
|
294
|
+
const mockModels = [{ identifier: 'model1' }, { identifier: 'model2' }];
|
295
|
+
|
296
|
+
vi.spyOn(service, 'getModelById').mockImplementation(
|
297
|
+
async (_, id) => mockModels.find((m) => m.identifier === id) as any,
|
298
|
+
);
|
299
|
+
|
300
|
+
const result = await service.getModelByIds('en-US', ['model1', 'model2', 'nonexistent']);
|
301
|
+
expect(result).toHaveLength(2);
|
302
|
+
expect(result[0].identifier).toBe('model1');
|
303
|
+
expect(result[1].identifier).toBe('model2');
|
304
|
+
});
|
305
|
+
});
|
306
|
+
});
|
@@ -60,6 +60,8 @@ export class DiscoverService {
|
|
60
60
|
});
|
61
61
|
}
|
62
62
|
|
63
|
+
if (!res.ok) return [];
|
64
|
+
|
63
65
|
const json = await res.json();
|
64
66
|
|
65
67
|
return json.agents;
|
@@ -79,6 +81,8 @@ export class DiscoverService {
|
|
79
81
|
});
|
80
82
|
}
|
81
83
|
|
84
|
+
if (!res.ok) return;
|
85
|
+
|
82
86
|
let assistant = await res.json();
|
83
87
|
|
84
88
|
if (!assistant) return;
|
@@ -148,7 +152,10 @@ export class DiscoverService {
|
|
148
152
|
});
|
149
153
|
}
|
150
154
|
|
155
|
+
if (!res.ok) return [];
|
156
|
+
|
151
157
|
const json = await res.json();
|
158
|
+
|
152
159
|
return json.plugins;
|
153
160
|
};
|
154
161
|
|
package/vitest.config.ts
CHANGED
package/public/manifest.json
DELETED
@@ -1,166 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"background_color": "#000000",
|
3
|
-
"cache_busting_mode": "all",
|
4
|
-
"categories": ["productivity", "design", "development", "education"],
|
5
|
-
"description": "Personal LLM productivity tool, brings you the best user experience of ChatGPT, OLLaMA, Gemini, Claude WebUI. Customize AI assistant features flexibly according to personalized needs to solve problems, enhance productivity, and explore future workflow in LobeChat.",
|
6
|
-
"display": "standalone",
|
7
|
-
"display_override": ["tabbed"],
|
8
|
-
"edge_side_panel": {
|
9
|
-
"preferred_width": 480
|
10
|
-
},
|
11
|
-
"handle_links": "auto",
|
12
|
-
"icons": [
|
13
|
-
{
|
14
|
-
"src": "/icons/icon-192x192.png?v=1",
|
15
|
-
"sizes": "192x192",
|
16
|
-
"type": "image/png",
|
17
|
-
"purpose": "any",
|
18
|
-
"cache_busting_mode": "query",
|
19
|
-
"max_age": 31536000,
|
20
|
-
"immutable": "true"
|
21
|
-
},
|
22
|
-
{
|
23
|
-
"src": "/icons/icon-192x192.maskable.png?v=1",
|
24
|
-
"sizes": "192x192",
|
25
|
-
"type": "image/png",
|
26
|
-
"purpose": "maskable",
|
27
|
-
"cache_busting_mode": "query",
|
28
|
-
"max_age": 31536000,
|
29
|
-
"immutable": "true"
|
30
|
-
},
|
31
|
-
{
|
32
|
-
"src": "/icons/icon-512x512.png?v=1",
|
33
|
-
"sizes": "512x512",
|
34
|
-
"type": "image/png",
|
35
|
-
"purpose": "any",
|
36
|
-
"cache_busting_mode": "query",
|
37
|
-
"max_age": 31536000,
|
38
|
-
"immutable": "true"
|
39
|
-
},
|
40
|
-
{
|
41
|
-
"src": "/icons/icon-512x512.maskable.png?v=1",
|
42
|
-
"sizes": "512x512",
|
43
|
-
"type": "image/png",
|
44
|
-
"purpose": "maskable",
|
45
|
-
"cache_busting_mode": "query",
|
46
|
-
"max_age": 31536000,
|
47
|
-
"immutable": "true"
|
48
|
-
}
|
49
|
-
],
|
50
|
-
"id": "lobe-chat",
|
51
|
-
"immutable": "true",
|
52
|
-
"launch_handler": {
|
53
|
-
"client_mode": ["navigate-existing", "auto"]
|
54
|
-
},
|
55
|
-
"max_age": 31536000,
|
56
|
-
"name": "LobeChat",
|
57
|
-
"orientation": "portrait",
|
58
|
-
"related_applications": [
|
59
|
-
{
|
60
|
-
"platform": "webapp",
|
61
|
-
"url": "https://chat-preview.lobehub.com/manifest.json"
|
62
|
-
}
|
63
|
-
],
|
64
|
-
"scope": "/",
|
65
|
-
"screenshots": [
|
66
|
-
{
|
67
|
-
"src": "/screenshots/shot-1.mobile.png?v=1",
|
68
|
-
"sizes": "640x1138",
|
69
|
-
"type": "image/png",
|
70
|
-
"form_factor": "narrow",
|
71
|
-
"cache_busting_mode": "query",
|
72
|
-
"max_age": 31536000,
|
73
|
-
"immutable": "true"
|
74
|
-
},
|
75
|
-
{
|
76
|
-
"src": "/screenshots/shot-2.mobile.png?v=1",
|
77
|
-
"sizes": "640x1138",
|
78
|
-
"type": "image/png",
|
79
|
-
"form_factor": "narrow",
|
80
|
-
"cache_busting_mode": "query",
|
81
|
-
"max_age": 31536000,
|
82
|
-
"immutable": "true"
|
83
|
-
},
|
84
|
-
{
|
85
|
-
"src": "/screenshots/shot-3.mobile.png?v=1",
|
86
|
-
"sizes": "640x1138",
|
87
|
-
"type": "image/png",
|
88
|
-
"form_factor": "narrow",
|
89
|
-
"cache_busting_mode": "query",
|
90
|
-
"max_age": 31536000,
|
91
|
-
"immutable": "true"
|
92
|
-
},
|
93
|
-
{
|
94
|
-
"src": "/screenshots/shot-4.mobile.png?v=1",
|
95
|
-
"sizes": "640x1138",
|
96
|
-
"type": "image/png",
|
97
|
-
"form_factor": "narrow",
|
98
|
-
"cache_busting_mode": "query",
|
99
|
-
"max_age": 31536000,
|
100
|
-
"immutable": "true"
|
101
|
-
},
|
102
|
-
{
|
103
|
-
"src": "/screenshots/shot-5.mobile.png?v=1",
|
104
|
-
"sizes": "640x1138",
|
105
|
-
"type": "image/png",
|
106
|
-
"form_factor": "narrow",
|
107
|
-
"cache_busting_mode": "query",
|
108
|
-
"max_age": 31536000,
|
109
|
-
"immutable": "true"
|
110
|
-
},
|
111
|
-
{
|
112
|
-
"src": "/screenshots/shot-1.desktop.png?v=1",
|
113
|
-
"sizes": "1280x676",
|
114
|
-
"type": "image/png",
|
115
|
-
"form_factor": "wide",
|
116
|
-
"cache_busting_mode": "query",
|
117
|
-
"max_age": 31536000,
|
118
|
-
"immutable": "true"
|
119
|
-
},
|
120
|
-
{
|
121
|
-
"src": "/screenshots/shot-2.desktop.png?v=1",
|
122
|
-
"sizes": "1280x676",
|
123
|
-
"type": "image/png",
|
124
|
-
"form_factor": "wide",
|
125
|
-
"cache_busting_mode": "query",
|
126
|
-
"max_age": 31536000,
|
127
|
-
"immutable": "true"
|
128
|
-
},
|
129
|
-
{
|
130
|
-
"src": "/screenshots/shot-3.desktop.png?v=1",
|
131
|
-
"sizes": "1280x676",
|
132
|
-
"type": "image/png",
|
133
|
-
"form_factor": "wide",
|
134
|
-
"cache_busting_mode": "query",
|
135
|
-
"max_age": 31536000,
|
136
|
-
"immutable": "true"
|
137
|
-
},
|
138
|
-
{
|
139
|
-
"src": "/screenshots/shot-4.desktop.png?v=1",
|
140
|
-
"sizes": "1280x676",
|
141
|
-
"type": "image/png",
|
142
|
-
"form_factor": "wide",
|
143
|
-
"cache_busting_mode": "query",
|
144
|
-
"max_age": 31536000,
|
145
|
-
"immutable": "true"
|
146
|
-
},
|
147
|
-
{
|
148
|
-
"src": "/screenshots/shot-5.desktop.png?v=1",
|
149
|
-
"sizes": "1280x676",
|
150
|
-
"type": "image/png",
|
151
|
-
"form_factor": "wide",
|
152
|
-
"cache_busting_mode": "query",
|
153
|
-
"max_age": 31536000,
|
154
|
-
"immutable": "true"
|
155
|
-
}
|
156
|
-
],
|
157
|
-
"short_name": "LobeChat",
|
158
|
-
"splash_pages": null,
|
159
|
-
"start_url": ".",
|
160
|
-
"tab_strip": {
|
161
|
-
"new_tab_button": {
|
162
|
-
"url": "/"
|
163
|
-
}
|
164
|
-
},
|
165
|
-
"theme_color": "#000000"
|
166
|
-
}
|