@lobehub/chat 1.39.3 → 1.40.1
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/.env.example +19 -8
- package/.eslintignore +1 -1
- package/CHANGELOG.md +58 -0
- package/changelog/v1.json +21 -0
- package/docs/.cdn.cache.json +25 -0
- package/docs/changelog/2023-09-09-plugin-system.mdx +1 -1
- package/docs/changelog/2023-09-09-plugin-system.zh-CN.mdx +1 -1
- package/docs/changelog/2024-09-20-artifacts.mdx +1 -1
- package/docs/changelog/2024-09-20-artifacts.zh-CN.mdx +1 -1
- package/docs/changelog/2024-10-27-pin-assistant.mdx +2 -2
- package/docs/changelog/2024-10-27-pin-assistant.zh-CN.mdx +2 -2
- package/docs/changelog/2024-11-06-share-text-json.mdx +2 -2
- package/docs/changelog/2024-11-06-share-text-json.zh-CN.mdx +2 -2
- package/docs/changelog/index.json +16 -16
- package/locales/ar/changelog.json +18 -0
- package/locales/ar/common.json +1 -0
- package/locales/ar/metadata.json +4 -0
- package/locales/bg-BG/changelog.json +18 -0
- package/locales/bg-BG/common.json +1 -0
- package/locales/bg-BG/metadata.json +4 -0
- package/locales/de-DE/changelog.json +18 -0
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/metadata.json +4 -0
- package/locales/en-US/changelog.json +18 -0
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/metadata.json +4 -0
- package/locales/es-ES/changelog.json +18 -0
- package/locales/es-ES/common.json +1 -0
- package/locales/es-ES/metadata.json +4 -0
- package/locales/fa-IR/changelog.json +18 -0
- package/locales/fa-IR/common.json +1 -0
- package/locales/fa-IR/metadata.json +4 -0
- package/locales/fr-FR/changelog.json +18 -0
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/metadata.json +4 -0
- package/locales/it-IT/changelog.json +18 -0
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/metadata.json +4 -0
- package/locales/ja-JP/changelog.json +18 -0
- package/locales/ja-JP/common.json +1 -0
- package/locales/ja-JP/metadata.json +4 -0
- package/locales/ko-KR/changelog.json +18 -0
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/metadata.json +4 -0
- package/locales/nl-NL/changelog.json +18 -0
- package/locales/nl-NL/common.json +1 -0
- package/locales/nl-NL/metadata.json +4 -0
- package/locales/pl-PL/changelog.json +18 -0
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/metadata.json +4 -0
- package/locales/pt-BR/changelog.json +18 -0
- package/locales/pt-BR/common.json +1 -0
- package/locales/pt-BR/metadata.json +4 -0
- package/locales/ru-RU/changelog.json +18 -0
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/metadata.json +4 -0
- package/locales/tr-TR/changelog.json +18 -0
- package/locales/tr-TR/common.json +1 -0
- package/locales/tr-TR/metadata.json +4 -0
- package/locales/vi-VN/changelog.json +18 -0
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/metadata.json +4 -0
- package/locales/zh-CN/changelog.json +18 -0
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-CN/metadata.json +4 -0
- package/locales/zh-TW/changelog.json +18 -0
- package/locales/zh-TW/common.json +1 -0
- package/locales/zh-TW/metadata.json +4 -0
- package/package.json +6 -1
- package/scripts/cdnWorkflow/index.ts +217 -0
- package/scripts/cdnWorkflow/optimized.ts +21 -0
- package/scripts/cdnWorkflow/s3/index.ts +120 -0
- package/scripts/cdnWorkflow/s3/types.ts +25 -0
- package/scripts/cdnWorkflow/s3/utils.ts +106 -0
- package/scripts/cdnWorkflow/uploader.ts +73 -0
- package/scripts/cdnWorkflow/utils.ts +93 -0
- package/src/app/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +25 -12
- package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +19 -9
- package/src/app/(main)/_layout/Desktop.tsx +4 -1
- package/src/app/(main)/_layout/Mobile.tsx +2 -1
- package/src/app/(main)/changelog/_layout/Desktop.tsx +25 -0
- package/src/app/(main)/changelog/_layout/Mobile/Header.tsx +33 -0
- package/src/app/(main)/changelog/_layout/Mobile/index.tsx +21 -0
- package/src/app/(main)/changelog/error.tsx +5 -0
- package/src/app/(main)/changelog/features/GridLayout.tsx +22 -0
- package/src/app/(main)/changelog/features/Hero.tsx +40 -0
- package/src/app/(main)/changelog/features/Post.tsx +56 -0
- package/src/app/(main)/changelog/features/PublishedTime.tsx +50 -0
- package/src/app/(main)/changelog/features/VersionTag.tsx +27 -0
- package/src/app/(main)/changelog/layout.tsx +10 -0
- package/src/app/(main)/changelog/loading.tsx +3 -0
- package/src/app/(main)/changelog/modal/page.tsx +23 -0
- package/src/app/(main)/changelog/not-found.tsx +3 -0
- package/src/app/(main)/changelog/page.tsx +73 -0
- package/src/app/(main)/chat/(workspace)/_layout/Desktop/HotKeys.tsx +7 -0
- package/src/app/(main)/chat/(workspace)/page.tsx +9 -2
- package/src/app/(main)/settings/about/features/Version.tsx +2 -2
- package/src/app/@modal/(.)changelog/modal/features/Cover.tsx +48 -0
- package/src/app/@modal/(.)changelog/modal/features/Hero.tsx +29 -0
- package/src/app/@modal/(.)changelog/modal/features/Pagination.tsx +54 -0
- package/src/app/@modal/(.)changelog/modal/features/Post.tsx +57 -0
- package/src/app/@modal/(.)changelog/modal/features/PublishedTime.tsx +50 -0
- package/src/app/@modal/(.)changelog/modal/features/ReadDetail.tsx +94 -0
- package/src/app/@modal/(.)changelog/modal/features/UpdateChangelogStatus.tsx +21 -0
- package/src/app/@modal/(.)changelog/modal/features/VersionTag.tsx +27 -0
- package/src/app/@modal/(.)changelog/modal/layout.tsx +39 -0
- package/src/app/@modal/(.)changelog/modal/loading.tsx +10 -0
- package/src/app/@modal/(.)changelog/modal/page.tsx +37 -0
- package/src/app/@modal/(.)settings/modal/layout.tsx +19 -16
- package/src/app/@modal/_layout/ModalLayout.tsx +63 -0
- package/src/app/@modal/chat/(.)settings/modal/layout.tsx +20 -17
- package/src/app/@modal/layout.tsx +5 -69
- package/src/components/mdx/Image.tsx +50 -0
- package/src/components/mdx/index.tsx +2 -0
- package/src/const/hotkeys.ts +1 -0
- package/src/const/url.ts +1 -0
- package/src/features/ChangelogModal/index.tsx +22 -0
- package/src/features/User/UserPanel/useMenu.tsx +50 -46
- package/src/features/User/__tests__/useMenu.test.tsx +7 -6
- package/src/hooks/useInterceptingRoutes.ts +1 -6
- package/src/hooks/useShare.tsx +1 -0
- package/src/libs/agent-runtime/openai/index.ts +2 -0
- package/src/locales/default/changelog.ts +18 -0
- package/src/locales/default/common.ts +1 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/metadata.ts +4 -0
- package/src/server/metadata.ts +5 -3
- package/src/server/routers/edge/appStatus.ts +3 -0
- package/src/server/routers/edge/index.ts +2 -0
- package/src/server/routers/lambda/agent.ts +1 -1
- package/src/server/services/changelog/index.test.ts +310 -0
- package/src/server/services/changelog/index.ts +196 -0
- package/src/server/services/discover/index.test.ts +0 -1
- package/src/server/sitemap.ts +4 -1
- package/src/services/__tests__/chat.test.ts +1 -1
- package/src/services/__tests__/global.test.ts +5 -2
- package/src/services/_auth.ts +1 -1
- package/src/services/agent.ts +25 -21
- package/src/services/chat.ts +2 -2
- package/src/services/file/ClientS3/index.ts +6 -6
- package/src/services/file/client.ts +14 -15
- package/src/services/file/server.ts +20 -25
- package/src/services/global.ts +2 -2
- package/src/services/import/client.ts +6 -5
- package/src/services/import/server.ts +6 -5
- package/src/services/import/type.ts +7 -0
- package/src/services/knowledgeBase.ts +19 -19
- package/src/services/message/_deprecated.ts +5 -0
- package/src/services/message/client.ts +52 -48
- package/src/services/message/server.ts +50 -53
- package/src/services/message/type.ts +2 -2
- package/src/services/plugin/client.ts +16 -22
- package/src/services/plugin/server.ts +15 -19
- package/src/services/rag.ts +18 -18
- package/src/services/ragEval.ts +29 -26
- package/src/services/session/_deprecated.ts +2 -2
- package/src/services/session/client.ts +55 -81
- package/src/services/session/server.ts +50 -74
- package/src/services/session/type.ts +4 -6
- package/src/services/share.ts +4 -4
- package/src/services/textToImage.ts +5 -2
- package/src/services/thread/client.ts +9 -15
- package/src/services/thread/server.ts +10 -15
- package/src/services/topic/client.ts +25 -25
- package/src/services/topic/server.ts +25 -42
- package/src/services/trace.ts +4 -4
- package/src/services/user/client.ts +13 -17
- package/src/services/user/server.ts +9 -13
- package/src/services/user/type.ts +1 -1
- package/src/store/chat/slices/message/reducer.ts +3 -2
- package/src/store/global/action.ts +27 -22
- package/src/store/global/initialState.ts +1 -0
- package/src/types/changelog.ts +6 -0
- package/src/types/message/index.ts +10 -8
- package/src/app/@modal/features/InterceptingContext.tsx +0 -9
@@ -0,0 +1,310 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { ChangelogIndexItem } from '@/types/changelog';
|
5
|
+
|
6
|
+
import { ChangelogService } from './index';
|
7
|
+
|
8
|
+
// Mock external dependencies
|
9
|
+
vi.mock('dayjs', () => ({
|
10
|
+
default: (date: string) => ({
|
11
|
+
format: vi.fn().mockReturnValue(date),
|
12
|
+
}),
|
13
|
+
}));
|
14
|
+
|
15
|
+
vi.mock('gray-matter', () => ({
|
16
|
+
default: vi.fn().mockImplementation((text) => ({
|
17
|
+
data: { date: '2023-01-01' },
|
18
|
+
content: text,
|
19
|
+
})),
|
20
|
+
}));
|
21
|
+
|
22
|
+
vi.mock('markdown-to-txt', () => ({
|
23
|
+
markdownToTxt: vi.fn().mockImplementation((text) => text),
|
24
|
+
}));
|
25
|
+
|
26
|
+
vi.mock('semver', async (importOriginal) => {
|
27
|
+
const actual: any = await importOriginal();
|
28
|
+
return {
|
29
|
+
...actual,
|
30
|
+
rcompare: vi.fn().mockImplementation((a, b) => b.localeCompare(a)),
|
31
|
+
lt: vi.fn().mockImplementation((a, b) => a < b),
|
32
|
+
gt: vi.fn().mockImplementation((a, b) => a > b),
|
33
|
+
parse: vi.fn().mockImplementation((v) => ({ toString: () => v })),
|
34
|
+
};
|
35
|
+
});
|
36
|
+
|
37
|
+
vi.mock('url-join', () => ({
|
38
|
+
default: vi.fn((...args) => args.join('/')),
|
39
|
+
}));
|
40
|
+
|
41
|
+
// 模拟 process.env
|
42
|
+
const originalEnv = process.env;
|
43
|
+
|
44
|
+
beforeEach(() => {
|
45
|
+
vi.resetModules();
|
46
|
+
process.env = { ...originalEnv };
|
47
|
+
});
|
48
|
+
|
49
|
+
afterEach(() => {
|
50
|
+
process.env = originalEnv;
|
51
|
+
});
|
52
|
+
|
53
|
+
describe('ChangelogService', () => {
|
54
|
+
let service: ChangelogService;
|
55
|
+
|
56
|
+
beforeEach(() => {
|
57
|
+
service = new ChangelogService();
|
58
|
+
// Mock fetch globally
|
59
|
+
global.fetch = vi.fn();
|
60
|
+
});
|
61
|
+
|
62
|
+
describe('getLatestChangelogId', () => {
|
63
|
+
it('should return the id of the first changelog item', async () => {
|
64
|
+
const mockIndex = [{ id: 'latest' }, { id: 'older' }];
|
65
|
+
vi.spyOn(service, 'getChangelogIndex').mockResolvedValue(mockIndex as ChangelogIndexItem[]);
|
66
|
+
|
67
|
+
const result = await service.getLatestChangelogId();
|
68
|
+
expect(result).toBe('latest');
|
69
|
+
});
|
70
|
+
|
71
|
+
it('should return undefined if the index is empty', async () => {
|
72
|
+
vi.spyOn(service, 'getChangelogIndex').mockResolvedValue([]);
|
73
|
+
|
74
|
+
const result = await service.getLatestChangelogId();
|
75
|
+
expect(result).toBeUndefined();
|
76
|
+
});
|
77
|
+
});
|
78
|
+
|
79
|
+
describe('getChangelogIndex', () => {
|
80
|
+
it('should fetch and merge changelog data', async () => {
|
81
|
+
const mockResponse = {
|
82
|
+
json: vi.fn().mockResolvedValue({
|
83
|
+
cloud: [{ id: 'cloud1', date: '2023-01-01', versionRange: ['1.0.0'] }],
|
84
|
+
community: [{ id: 'community1', date: '2023-01-02', versionRange: ['1.1.0'] }],
|
85
|
+
}),
|
86
|
+
};
|
87
|
+
(global.fetch as any).mockResolvedValue(mockResponse);
|
88
|
+
|
89
|
+
const result = await service.getChangelogIndex();
|
90
|
+
expect(result).toHaveLength(2);
|
91
|
+
expect(result[0].id).toBe('community1');
|
92
|
+
expect(result[1].id).toBe('cloud1');
|
93
|
+
});
|
94
|
+
|
95
|
+
it('should handle fetch errors', async () => {
|
96
|
+
(global.fetch as any).mockRejectedValue(new Error('Fetch failed'));
|
97
|
+
|
98
|
+
const result = await service.getChangelogIndex();
|
99
|
+
expect(result).toBe(false);
|
100
|
+
});
|
101
|
+
|
102
|
+
it('should return only community items when config type is community', async () => {
|
103
|
+
service.config.type = 'community';
|
104
|
+
const mockResponse = {
|
105
|
+
json: vi.fn().mockResolvedValue({
|
106
|
+
cloud: [{ id: 'cloud1', date: '2023-01-01', versionRange: ['1.0.0'] }],
|
107
|
+
community: [{ id: 'community1', date: '2023-01-02', versionRange: ['1.1.0'] }],
|
108
|
+
}),
|
109
|
+
};
|
110
|
+
(global.fetch as any).mockResolvedValue(mockResponse);
|
111
|
+
|
112
|
+
const result = await service.getChangelogIndex();
|
113
|
+
expect(result).toHaveLength(1);
|
114
|
+
expect(result[0].id).toBe('community1');
|
115
|
+
});
|
116
|
+
});
|
117
|
+
|
118
|
+
describe('getIndexItemById', () => {
|
119
|
+
it('should return the correct item by id', async () => {
|
120
|
+
const mockIndex = [
|
121
|
+
{ id: 'item1', date: '2023-01-01', versionRange: ['1.0.0'] },
|
122
|
+
{ id: 'item2', date: '2023-01-02', versionRange: ['1.1.0'] },
|
123
|
+
];
|
124
|
+
vi.spyOn(service, 'getChangelogIndex').mockResolvedValue(mockIndex as ChangelogIndexItem[]);
|
125
|
+
|
126
|
+
const result = await service.getIndexItemById('item2');
|
127
|
+
expect(result).toEqual({ id: 'item2', date: '2023-01-02', versionRange: ['1.1.0'] });
|
128
|
+
});
|
129
|
+
|
130
|
+
it('should return undefined for non-existent id', async () => {
|
131
|
+
vi.spyOn(service, 'getChangelogIndex').mockResolvedValue([]);
|
132
|
+
|
133
|
+
const result = await service.getIndexItemById('nonexistent');
|
134
|
+
expect(result).toBeUndefined();
|
135
|
+
});
|
136
|
+
});
|
137
|
+
|
138
|
+
describe('getPostById', () => {
|
139
|
+
it('should fetch and parse post content', async () => {
|
140
|
+
vi.spyOn(service, 'getIndexItemById').mockResolvedValue({
|
141
|
+
id: 'post1',
|
142
|
+
date: '2023-01-01',
|
143
|
+
versionRange: ['1.0.0'],
|
144
|
+
} as ChangelogIndexItem);
|
145
|
+
|
146
|
+
const mockResponse = {
|
147
|
+
text: vi.fn().mockResolvedValue('# Post Title\nPost content'),
|
148
|
+
};
|
149
|
+
(global.fetch as any).mockResolvedValue(mockResponse);
|
150
|
+
|
151
|
+
const result = await service.getPostById('post1');
|
152
|
+
expect(result).toMatchObject({
|
153
|
+
content: 'Post content',
|
154
|
+
date: expect.any(String), // 改为期望字符串而不是 Date 对象
|
155
|
+
description: 'Post content',
|
156
|
+
image: undefined,
|
157
|
+
rawTitle: 'Post Title',
|
158
|
+
tags: ['changelog'],
|
159
|
+
title: 'Post Title',
|
160
|
+
});
|
161
|
+
|
162
|
+
// 额外检查日期格式
|
163
|
+
expect(result.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
164
|
+
});
|
165
|
+
|
166
|
+
it('should handle fetch errors', async () => {
|
167
|
+
vi.spyOn(service, 'getIndexItemById').mockResolvedValue({} as ChangelogIndexItem);
|
168
|
+
(global.fetch as any).mockRejectedValue(new Error('Fetch failed'));
|
169
|
+
|
170
|
+
const result = await service.getPostById('error');
|
171
|
+
expect(result).toBe(false);
|
172
|
+
});
|
173
|
+
|
174
|
+
it('should use the correct locale for fetching content', async () => {
|
175
|
+
vi.spyOn(service, 'getIndexItemById').mockResolvedValue({
|
176
|
+
id: 'post1',
|
177
|
+
date: '2023-01-01',
|
178
|
+
versionRange: ['1.0.0'],
|
179
|
+
} as ChangelogIndexItem);
|
180
|
+
|
181
|
+
const mockResponse = {
|
182
|
+
text: vi.fn().mockResolvedValue('# Chinese Title\n中文内容'),
|
183
|
+
};
|
184
|
+
(global.fetch as any).mockResolvedValue(mockResponse);
|
185
|
+
|
186
|
+
const result = await service.getPostById('post1', { locale: 'zh-CN' });
|
187
|
+
expect(result).toEqual({
|
188
|
+
content: '中文内容',
|
189
|
+
date: '2023-01-01',
|
190
|
+
description: '中文内容',
|
191
|
+
image: undefined,
|
192
|
+
rawTitle: 'Chinese Title',
|
193
|
+
tags: ['changelog'],
|
194
|
+
title: 'Chinese Title',
|
195
|
+
});
|
196
|
+
});
|
197
|
+
});
|
198
|
+
|
199
|
+
describe('private methods', () => {
|
200
|
+
describe('mergeChangelogs', () => {
|
201
|
+
it('should merge and sort changelogs correctly', () => {
|
202
|
+
const cloud = [{ id: 'cloud1', date: '2023-01-01', versionRange: ['1.0.0'] }];
|
203
|
+
const community = [{ id: 'community1', date: '2023-01-02', versionRange: ['1.1.0'] }];
|
204
|
+
|
205
|
+
// @ts-ignore - accessing private method for testing
|
206
|
+
const result = service.mergeChangelogs(cloud, community);
|
207
|
+
expect(result).toHaveLength(2);
|
208
|
+
expect(result[0].id).toBe('community1');
|
209
|
+
expect(result[1].id).toBe('cloud1');
|
210
|
+
});
|
211
|
+
|
212
|
+
it('should override community items with cloud items when ids match', () => {
|
213
|
+
const cloud = [{ id: 'item1', date: '2023-01-01', versionRange: ['1.0.0'], type: 'cloud' }];
|
214
|
+
const community = [
|
215
|
+
{ id: 'item1', date: '2023-01-01', versionRange: ['1.0.0'], type: 'community' },
|
216
|
+
];
|
217
|
+
|
218
|
+
// @ts-ignore - accessing private method for testing
|
219
|
+
const result = service.mergeChangelogs(cloud, community);
|
220
|
+
expect(result).toHaveLength(1);
|
221
|
+
// @ts-ignore
|
222
|
+
expect(result[0].type).toBe('cloud');
|
223
|
+
});
|
224
|
+
});
|
225
|
+
|
226
|
+
describe('formatVersionRange', () => {
|
227
|
+
it('should format version range correctly', () => {
|
228
|
+
// @ts-ignore - accessing private method for testing
|
229
|
+
const result = service.formatVersionRange(['1.0.0', '1.1.0']);
|
230
|
+
expect(result).toEqual(['1.1.0', '1.0.0']);
|
231
|
+
});
|
232
|
+
|
233
|
+
it('should return single version as is', () => {
|
234
|
+
// @ts-ignore - accessing private method for testing
|
235
|
+
const result = service.formatVersionRange(['1.0.0']);
|
236
|
+
expect(result).toEqual(['1.0.0']);
|
237
|
+
});
|
238
|
+
});
|
239
|
+
|
240
|
+
describe('genUrl', () => {
|
241
|
+
it('should generate correct URL', () => {
|
242
|
+
// @ts-ignore - accessing private method for testing
|
243
|
+
const result = service.genUrl('test/path');
|
244
|
+
expect(result).toBe('https://raw.githubusercontent.com/lobehub/lobe-chat/main/test/path');
|
245
|
+
});
|
246
|
+
});
|
247
|
+
|
248
|
+
describe('extractHttpsLinks', () => {
|
249
|
+
it('should extract HTTPS links from text', () => {
|
250
|
+
const text = 'Text with https://example.com and https://test.com/image.jpg links';
|
251
|
+
// @ts-ignore - accessing private method for testing
|
252
|
+
const result = service.extractHttpsLinks(text);
|
253
|
+
expect(result).toEqual(['https://example.com', 'https://test.com/image.jpg']);
|
254
|
+
});
|
255
|
+
});
|
256
|
+
|
257
|
+
describe('cdnInit', () => {
|
258
|
+
it('should initialize CDN URLs if docCdnPrefix is set', async () => {
|
259
|
+
// 设置环境变量
|
260
|
+
process.env.DOC_S3_PUBLIC_DOMAIN = 'https://cdn.example.com';
|
261
|
+
|
262
|
+
// 重新导入模块以确保环境变量生效
|
263
|
+
const { ChangelogService } = await import('./index');
|
264
|
+
const service = new ChangelogService();
|
265
|
+
|
266
|
+
const mockData = { 'https://example.com/image.jpg': 'image-hash.jpg' };
|
267
|
+
const mockResponse = {
|
268
|
+
json: vi.fn().mockResolvedValue(mockData),
|
269
|
+
};
|
270
|
+
global.fetch = vi.fn().mockResolvedValue(mockResponse);
|
271
|
+
|
272
|
+
// @ts-ignore - accessing private method for testing
|
273
|
+
await service.cdnInit();
|
274
|
+
|
275
|
+
expect(service.cdnUrls).toEqual(mockData);
|
276
|
+
});
|
277
|
+
});
|
278
|
+
|
279
|
+
describe('replaceCdnUrl', () => {
|
280
|
+
it('should replace URL with CDN URL if available', async () => {
|
281
|
+
// 设置环境变量
|
282
|
+
process.env.DOC_S3_PUBLIC_DOMAIN = 'https://cdn.example.com';
|
283
|
+
|
284
|
+
// 重新导入模块以确保环境变量生效
|
285
|
+
const { ChangelogService } = await import('./index');
|
286
|
+
const service = new ChangelogService();
|
287
|
+
|
288
|
+
service.cdnUrls = { 'https://example.com/image.jpg': 'image-hash.jpg' };
|
289
|
+
|
290
|
+
// @ts-ignore - accessing private method for testing
|
291
|
+
const result = service.replaceCdnUrl('https://example.com/image.jpg');
|
292
|
+
|
293
|
+
expect(result).toBe('https://cdn.example.com/image-hash.jpg');
|
294
|
+
});
|
295
|
+
|
296
|
+
it('should return original URL if CDN URL is not available', () => {
|
297
|
+
const originalDocCdnPrefix = process.env.DOC_S3_PUBLIC_DOMAIN;
|
298
|
+
process.env.DOC_S3_PUBLIC_DOMAIN = 'https://cdn.example.com';
|
299
|
+
service.cdnUrls = {};
|
300
|
+
|
301
|
+
// @ts-ignore - accessing private method for testing
|
302
|
+
const result = service.replaceCdnUrl('https://example.com/image.jpg');
|
303
|
+
expect(result).toBe('https://example.com/image.jpg');
|
304
|
+
|
305
|
+
// Restore original value
|
306
|
+
process.env.DOC_S3_PUBLIC_DOMAIN = originalDocCdnPrefix;
|
307
|
+
});
|
308
|
+
});
|
309
|
+
});
|
310
|
+
});
|
@@ -0,0 +1,196 @@
|
|
1
|
+
import dayjs from 'dayjs';
|
2
|
+
import matter from 'gray-matter';
|
3
|
+
import { markdownToTxt } from 'markdown-to-txt';
|
4
|
+
import semver from 'semver';
|
5
|
+
import urlJoin from 'url-join';
|
6
|
+
|
7
|
+
import { Locales } from '@/locales/resources';
|
8
|
+
import { ChangelogIndexItem } from '@/types/changelog';
|
9
|
+
|
10
|
+
const BASE_URL = 'https://raw.githubusercontent.com';
|
11
|
+
const LAST_MODIFIED = new Date().toISOString();
|
12
|
+
|
13
|
+
const docCdnPrefix = process.env.DOC_S3_PUBLIC_DOMAIN || '';
|
14
|
+
|
15
|
+
export interface ChangelogConfig {
|
16
|
+
branch: string;
|
17
|
+
cdnPath: string;
|
18
|
+
changelogPath: string;
|
19
|
+
docsPath: string;
|
20
|
+
majorVersion: number;
|
21
|
+
repo: string;
|
22
|
+
type: 'cloud' | 'community';
|
23
|
+
user: string;
|
24
|
+
}
|
25
|
+
|
26
|
+
export class ChangelogService {
|
27
|
+
cdnUrls: {
|
28
|
+
[key: string]: string;
|
29
|
+
} = {};
|
30
|
+
config: ChangelogConfig = {
|
31
|
+
branch: process.env.DOCS_BRANCH || 'main',
|
32
|
+
cdnPath: 'docs/.cdn.cache.json',
|
33
|
+
changelogPath: 'changelog',
|
34
|
+
docsPath: 'docs/changelog',
|
35
|
+
majorVersion: 1,
|
36
|
+
repo: 'lobe-chat',
|
37
|
+
type: 'cloud',
|
38
|
+
user: 'lobehub',
|
39
|
+
};
|
40
|
+
|
41
|
+
async getLatestChangelogId() {
|
42
|
+
const index = await this.getChangelogIndex();
|
43
|
+
return index[0]?.id;
|
44
|
+
}
|
45
|
+
|
46
|
+
async getChangelogIndex(): Promise<ChangelogIndexItem[]> {
|
47
|
+
try {
|
48
|
+
const url = this.genUrl(urlJoin(this.config.docsPath, 'index.json'));
|
49
|
+
|
50
|
+
const res = await fetch(url);
|
51
|
+
|
52
|
+
const data = await res.json();
|
53
|
+
|
54
|
+
return this.mergeChangelogs(data.cloud, data.community).slice(0, 5);
|
55
|
+
} catch (e) {
|
56
|
+
console.error('Error getting changelog lists:', e);
|
57
|
+
return false as any;
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
async getIndexItemById(id: string) {
|
62
|
+
const index = await this.getChangelogIndex();
|
63
|
+
return index.find((item) => item.id === id);
|
64
|
+
}
|
65
|
+
|
66
|
+
async getPostById(id: string, options?: { locale?: Locales }) {
|
67
|
+
await this.cdnInit();
|
68
|
+
try {
|
69
|
+
const post = await this.getIndexItemById(id);
|
70
|
+
|
71
|
+
const filename = options?.locale === 'en-US' ? `${id}.mdx` : `${id}.zh-CN.mdx`;
|
72
|
+
const url = this.genUrl(urlJoin(this.config.docsPath, filename));
|
73
|
+
|
74
|
+
const response = await fetch(url);
|
75
|
+
const text = await response.text();
|
76
|
+
const { data, content } = matter(text);
|
77
|
+
|
78
|
+
const regex = /^#\s(.+)/;
|
79
|
+
const match = regex.exec(content.trim());
|
80
|
+
const matches = content.trim().split(regex);
|
81
|
+
|
82
|
+
let description: string;
|
83
|
+
|
84
|
+
if (matches[2]) {
|
85
|
+
description = matches[2] ? matches[2].trim() : '';
|
86
|
+
} else {
|
87
|
+
description = matches[1] ? matches[1].trim() : '';
|
88
|
+
}
|
89
|
+
|
90
|
+
if (docCdnPrefix) {
|
91
|
+
const images = this.extractHttpsLinks(content);
|
92
|
+
for (const url of images) {
|
93
|
+
const cdnUrl = this.replaceCdnUrl(url);
|
94
|
+
if (cdnUrl && url !== cdnUrl) {
|
95
|
+
description = description.replaceAll(url, cdnUrl);
|
96
|
+
}
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
return {
|
101
|
+
date: post?.date
|
102
|
+
? new Date(post.date)
|
103
|
+
: data?.date
|
104
|
+
? new Date(data.date)
|
105
|
+
: new Date(LAST_MODIFIED),
|
106
|
+
description: markdownToTxt(description.replaceAll('\n', '').replaceAll(' ', ' ')).slice(
|
107
|
+
0,
|
108
|
+
160,
|
109
|
+
),
|
110
|
+
image: post?.image ? this.replaceCdnUrl(post.image) : undefined,
|
111
|
+
tags: ['changelog'],
|
112
|
+
title: match ? match[1] : '',
|
113
|
+
...data,
|
114
|
+
content: description,
|
115
|
+
rawTitle: match ? match[1] : '',
|
116
|
+
};
|
117
|
+
} catch {
|
118
|
+
console.error('Error getting changlog post by id', id);
|
119
|
+
return false as any;
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
private mergeChangelogs(
|
124
|
+
cloud: ChangelogIndexItem[],
|
125
|
+
community: ChangelogIndexItem[],
|
126
|
+
): ChangelogIndexItem[] {
|
127
|
+
if (this.config.type === 'community') {
|
128
|
+
return community;
|
129
|
+
}
|
130
|
+
|
131
|
+
const merged = [...community];
|
132
|
+
|
133
|
+
for (const cloudItem of cloud) {
|
134
|
+
const index = merged.findIndex((item) => item.id === cloudItem.id);
|
135
|
+
if (index !== -1) {
|
136
|
+
merged[index] = cloudItem;
|
137
|
+
} else {
|
138
|
+
merged.push(cloudItem);
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
return merged
|
143
|
+
.map((item) => ({
|
144
|
+
...item,
|
145
|
+
date: dayjs(item.date).format('YYYY-MM-DD'),
|
146
|
+
versionRange: this.formatVersionRange(item.versionRange),
|
147
|
+
}))
|
148
|
+
.sort((a, b) => semver.rcompare(a.versionRange[0], b.versionRange[0]));
|
149
|
+
}
|
150
|
+
|
151
|
+
private formatVersionRange(range: string[]): string[] {
|
152
|
+
if (range.length === 1) {
|
153
|
+
return range;
|
154
|
+
}
|
155
|
+
|
156
|
+
const [v1, v2]: any = range.map((v) => semver.parse(v)?.toString());
|
157
|
+
|
158
|
+
const minVersion = semver.lt(v1, v2) ? v1 : v2;
|
159
|
+
const maxVersion = semver.gt(v1, v2) ? v1 : v2;
|
160
|
+
|
161
|
+
return [maxVersion, minVersion];
|
162
|
+
}
|
163
|
+
|
164
|
+
private genUrl(path: string) {
|
165
|
+
return urlJoin(BASE_URL, this.config.user, this.config.repo, this.config.branch, path);
|
166
|
+
}
|
167
|
+
|
168
|
+
private extractHttpsLinks(text: string) {
|
169
|
+
const regex = /https:\/\/[^\s"')>]+/g;
|
170
|
+
const links = text.match(regex);
|
171
|
+
return links || [];
|
172
|
+
}
|
173
|
+
|
174
|
+
private async cdnInit() {
|
175
|
+
if (!docCdnPrefix) return;
|
176
|
+
if (Object.keys(this.cdnUrls).length === 0) {
|
177
|
+
try {
|
178
|
+
const url = this.genUrl(this.config.cdnPath);
|
179
|
+
const res = await fetch(url);
|
180
|
+
const data = await res.json();
|
181
|
+
if (data) {
|
182
|
+
this.cdnUrls = data;
|
183
|
+
}
|
184
|
+
} catch (error) {
|
185
|
+
console.error('Error getting changelog cdn cache:', error);
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
private replaceCdnUrl(url: string) {
|
191
|
+
if (!docCdnPrefix || !this.cdnUrls?.[url]) {
|
192
|
+
return url;
|
193
|
+
}
|
194
|
+
return urlJoin(docCdnPrefix, this.cdnUrls[url]);
|
195
|
+
}
|
196
|
+
}
|
package/src/server/sitemap.ts
CHANGED
@@ -3,6 +3,7 @@ import { MetadataRoute } from 'next';
|
|
3
3
|
import qs from 'query-string';
|
4
4
|
import urlJoin from 'url-join';
|
5
5
|
|
6
|
+
import { serverFeatureFlags } from '@/config/featureFlags';
|
6
7
|
import { DEFAULT_LANG } from '@/const/locale';
|
7
8
|
import { SITEMAP_BASE_URL } from '@/const/url';
|
8
9
|
import { Locales, locales as allLocales } from '@/locales/resources';
|
@@ -195,12 +196,14 @@ export class Sitemap {
|
|
195
196
|
}
|
196
197
|
|
197
198
|
async getPage(): Promise<MetadataRoute.Sitemap> {
|
199
|
+
const hideDocs = serverFeatureFlags().hideDocs;
|
198
200
|
const assistantsCategory = Object.values(AssistantCategory);
|
199
201
|
const pluginCategory = Object.values(PluginCategory);
|
200
202
|
const modelCategory = await this.discoverService.getProviderList(DEFAULT_LANG);
|
201
203
|
return [
|
202
204
|
...this._genSitemap('/', { noLocales: true }),
|
203
205
|
...this._genSitemap('/chat', { noLocales: true }),
|
206
|
+
...(!hideDocs ? this._genSitemap('/changelog', { noLocales: true }) : []),
|
204
207
|
/* ↓ cloud slot ↓ */
|
205
208
|
|
206
209
|
/* ↑ cloud slot ↑ */
|
@@ -227,7 +230,7 @@ export class Sitemap {
|
|
227
230
|
}),
|
228
231
|
),
|
229
232
|
...this._genSitemap('/discover/providers', { changeFrequency: 'daily', priority: 0.7 }),
|
230
|
-
];
|
233
|
+
].filter(Boolean);
|
231
234
|
}
|
232
235
|
getRobots() {
|
233
236
|
return [
|
@@ -904,7 +904,7 @@ describe('ChatService', () => {
|
|
904
904
|
* initialization of AgentRuntime with different providers
|
905
905
|
*/
|
906
906
|
vi.mock('../_auth', async (importOriginal) => {
|
907
|
-
return
|
907
|
+
return importOriginal();
|
908
908
|
});
|
909
909
|
describe('AgentRuntimeOnClient', () => {
|
910
910
|
describe('initializeWithClientStore', () => {
|
@@ -18,6 +18,9 @@ vi.mock('@/libs/trpc/client', () => {
|
|
18
18
|
getGlobalConfig: { query: vi.fn() },
|
19
19
|
getDefaultAgentConfig: { query: vi.fn() },
|
20
20
|
},
|
21
|
+
appStatus: {
|
22
|
+
getLatestChangelogId: { query: vi.fn() },
|
23
|
+
},
|
21
24
|
},
|
22
25
|
};
|
23
26
|
});
|
@@ -28,14 +31,14 @@ describe('GlobalService', () => {
|
|
28
31
|
// Arrange
|
29
32
|
const mockVersion = '1.0.0';
|
30
33
|
(fetch as Mock).mockResolvedValue({
|
31
|
-
json: () => Promise.resolve({
|
34
|
+
json: () => Promise.resolve({ version: mockVersion }),
|
32
35
|
});
|
33
36
|
|
34
37
|
// Act
|
35
38
|
const version = await globalService.getLatestVersion();
|
36
39
|
|
37
40
|
// Assert
|
38
|
-
expect(fetch).toHaveBeenCalledWith('https://registry.npmmirror.com/@lobehub/chat');
|
41
|
+
expect(fetch).toHaveBeenCalledWith('https://registry.npmmirror.com/@lobehub/chat/latest');
|
39
42
|
expect(version).toBe(mockVersion);
|
40
43
|
});
|
41
44
|
|
package/src/services/_auth.ts
CHANGED
@@ -78,7 +78,7 @@ const createAuthTokenWithPayload = async (payload = {}) => {
|
|
78
78
|
const accessCode = keyVaultsConfigSelectors.password(useUserStore.getState());
|
79
79
|
const userId = userProfileSelectors.userId(useUserStore.getState());
|
80
80
|
|
81
|
-
return
|
81
|
+
return createJWT<JWTPayload>({ accessCode, userId, ...payload });
|
82
82
|
};
|
83
83
|
|
84
84
|
interface AuthParams {
|
package/src/services/agent.ts
CHANGED
@@ -1,45 +1,49 @@
|
|
1
1
|
import { lambdaClient } from '@/libs/trpc/client';
|
2
2
|
|
3
3
|
class AgentService {
|
4
|
-
|
5
|
-
|
4
|
+
createAgentKnowledgeBase = async (
|
5
|
+
agentId: string,
|
6
|
+
knowledgeBaseId: string,
|
7
|
+
enabled?: boolean,
|
8
|
+
) => {
|
9
|
+
return lambdaClient.agent.createAgentKnowledgeBase.mutate({
|
6
10
|
agentId,
|
7
11
|
enabled,
|
8
12
|
knowledgeBaseId,
|
9
13
|
});
|
10
|
-
}
|
14
|
+
};
|
11
15
|
|
12
|
-
async
|
13
|
-
return
|
14
|
-
}
|
16
|
+
deleteAgentKnowledgeBase = async (agentId: string, knowledgeBaseId: string) => {
|
17
|
+
return lambdaClient.agent.deleteAgentKnowledgeBase.mutate({ agentId, knowledgeBaseId });
|
18
|
+
};
|
15
19
|
|
16
|
-
async
|
17
|
-
return
|
20
|
+
toggleKnowledgeBase = async (agentId: string, knowledgeBaseId: string, enabled?: boolean) => {
|
21
|
+
return lambdaClient.agent.toggleKnowledgeBase.mutate({
|
18
22
|
agentId,
|
19
23
|
enabled,
|
20
24
|
knowledgeBaseId,
|
21
25
|
});
|
22
|
-
}
|
26
|
+
};
|
23
27
|
|
24
|
-
async
|
25
|
-
return
|
26
|
-
}
|
28
|
+
createAgentFiles = async (agentId: string, fileIds: string[], enabled?: boolean) => {
|
29
|
+
return lambdaClient.agent.createAgentFiles.mutate({ agentId, enabled, fileIds });
|
30
|
+
};
|
27
31
|
|
28
|
-
async
|
29
|
-
return
|
30
|
-
}
|
32
|
+
deleteAgentFile = async (agentId: string, fileId: string) => {
|
33
|
+
return lambdaClient.agent.deleteAgentFile.mutate({ agentId, fileId });
|
34
|
+
};
|
31
35
|
|
32
|
-
async
|
33
|
-
return
|
36
|
+
toggleFile = async (agentId: string, fileId: string, enabled?: boolean) => {
|
37
|
+
return lambdaClient.agent.toggleFile.mutate({
|
34
38
|
agentId,
|
35
39
|
enabled,
|
36
40
|
fileId,
|
37
41
|
});
|
38
|
-
}
|
42
|
+
};
|
39
43
|
|
40
|
-
async
|
41
|
-
return
|
42
|
-
}
|
44
|
+
getFilesAndKnowledgeBases = async (agentId: string) => {
|
45
|
+
return lambdaClient.agent.getKnowledgeBasesAndFiles.query({ agentId });
|
46
|
+
};
|
43
47
|
}
|
44
48
|
|
45
49
|
export const agentService = new AgentService();
|
package/src/services/chat.ts
CHANGED
@@ -499,7 +499,7 @@ class ChatService {
|
|
499
499
|
return this.reorderToolMessages(postMessages);
|
500
500
|
};
|
501
501
|
|
502
|
-
private mapTrace(trace?: TracePayload, tag?: TraceTagMap): TracePayload {
|
502
|
+
private mapTrace = (trace?: TracePayload, tag?: TraceTagMap): TracePayload => {
|
503
503
|
const tags = sessionMetaSelectors.currentAgentMeta(useSessionStore.getState()).tags || [];
|
504
504
|
|
505
505
|
const enabled = preferenceSelectors.userAllowTrace(useUserStore.getState());
|
@@ -512,7 +512,7 @@ class ChatService {
|
|
512
512
|
tags: [tag, ...(trace?.tags || []), ...tags].filter(Boolean) as string[],
|
513
513
|
userId: userProfileSelectors.userId(useUserStore.getState()),
|
514
514
|
};
|
515
|
-
}
|
515
|
+
};
|
516
516
|
|
517
517
|
/**
|
518
518
|
* Fetch chat completion on the client side.
|