@lobehub/chat 1.39.3 → 1.40.0

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.
Files changed (172) hide show
  1. package/.env.example +19 -8
  2. package/.eslintignore +1 -1
  3. package/CHANGELOG.md +33 -0
  4. package/changelog/v1.json +12 -0
  5. package/docs/.cdn.cache.json +25 -0
  6. package/docs/changelog/2023-09-09-plugin-system.mdx +1 -1
  7. package/docs/changelog/2023-09-09-plugin-system.zh-CN.mdx +1 -1
  8. package/docs/changelog/2024-09-20-artifacts.mdx +1 -1
  9. package/docs/changelog/2024-09-20-artifacts.zh-CN.mdx +1 -1
  10. package/docs/changelog/2024-10-27-pin-assistant.mdx +2 -2
  11. package/docs/changelog/2024-10-27-pin-assistant.zh-CN.mdx +2 -2
  12. package/docs/changelog/2024-11-06-share-text-json.mdx +2 -2
  13. package/docs/changelog/2024-11-06-share-text-json.zh-CN.mdx +2 -2
  14. package/docs/changelog/index.json +16 -16
  15. package/locales/ar/changelog.json +18 -0
  16. package/locales/ar/common.json +1 -0
  17. package/locales/ar/metadata.json +4 -0
  18. package/locales/bg-BG/changelog.json +18 -0
  19. package/locales/bg-BG/common.json +1 -0
  20. package/locales/bg-BG/metadata.json +4 -0
  21. package/locales/de-DE/changelog.json +18 -0
  22. package/locales/de-DE/common.json +1 -0
  23. package/locales/de-DE/metadata.json +4 -0
  24. package/locales/en-US/changelog.json +18 -0
  25. package/locales/en-US/common.json +1 -0
  26. package/locales/en-US/metadata.json +4 -0
  27. package/locales/es-ES/changelog.json +18 -0
  28. package/locales/es-ES/common.json +1 -0
  29. package/locales/es-ES/metadata.json +4 -0
  30. package/locales/fa-IR/changelog.json +18 -0
  31. package/locales/fa-IR/common.json +1 -0
  32. package/locales/fa-IR/metadata.json +4 -0
  33. package/locales/fr-FR/changelog.json +18 -0
  34. package/locales/fr-FR/common.json +1 -0
  35. package/locales/fr-FR/metadata.json +4 -0
  36. package/locales/it-IT/changelog.json +18 -0
  37. package/locales/it-IT/common.json +1 -0
  38. package/locales/it-IT/metadata.json +4 -0
  39. package/locales/ja-JP/changelog.json +18 -0
  40. package/locales/ja-JP/common.json +1 -0
  41. package/locales/ja-JP/metadata.json +4 -0
  42. package/locales/ko-KR/changelog.json +18 -0
  43. package/locales/ko-KR/common.json +1 -0
  44. package/locales/ko-KR/metadata.json +4 -0
  45. package/locales/nl-NL/changelog.json +18 -0
  46. package/locales/nl-NL/common.json +1 -0
  47. package/locales/nl-NL/metadata.json +4 -0
  48. package/locales/pl-PL/changelog.json +18 -0
  49. package/locales/pl-PL/common.json +1 -0
  50. package/locales/pl-PL/metadata.json +4 -0
  51. package/locales/pt-BR/changelog.json +18 -0
  52. package/locales/pt-BR/common.json +1 -0
  53. package/locales/pt-BR/metadata.json +4 -0
  54. package/locales/ru-RU/changelog.json +18 -0
  55. package/locales/ru-RU/common.json +1 -0
  56. package/locales/ru-RU/metadata.json +4 -0
  57. package/locales/tr-TR/changelog.json +18 -0
  58. package/locales/tr-TR/common.json +1 -0
  59. package/locales/tr-TR/metadata.json +4 -0
  60. package/locales/vi-VN/changelog.json +18 -0
  61. package/locales/vi-VN/common.json +1 -0
  62. package/locales/vi-VN/metadata.json +4 -0
  63. package/locales/zh-CN/changelog.json +18 -0
  64. package/locales/zh-CN/common.json +1 -0
  65. package/locales/zh-CN/metadata.json +4 -0
  66. package/locales/zh-TW/changelog.json +18 -0
  67. package/locales/zh-TW/common.json +1 -0
  68. package/locales/zh-TW/metadata.json +4 -0
  69. package/package.json +6 -1
  70. package/scripts/cdnWorkflow/index.ts +217 -0
  71. package/scripts/cdnWorkflow/optimized.ts +21 -0
  72. package/scripts/cdnWorkflow/s3/index.ts +120 -0
  73. package/scripts/cdnWorkflow/s3/types.ts +25 -0
  74. package/scripts/cdnWorkflow/s3/utils.ts +106 -0
  75. package/scripts/cdnWorkflow/uploader.ts +73 -0
  76. package/scripts/cdnWorkflow/utils.ts +93 -0
  77. package/src/app/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +25 -12
  78. package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +19 -9
  79. package/src/app/(main)/_layout/Desktop.tsx +4 -1
  80. package/src/app/(main)/_layout/Mobile.tsx +2 -1
  81. package/src/app/(main)/changelog/_layout/Desktop.tsx +25 -0
  82. package/src/app/(main)/changelog/_layout/Mobile/Header.tsx +33 -0
  83. package/src/app/(main)/changelog/_layout/Mobile/index.tsx +21 -0
  84. package/src/app/(main)/changelog/error.tsx +5 -0
  85. package/src/app/(main)/changelog/features/GridLayout.tsx +22 -0
  86. package/src/app/(main)/changelog/features/Hero.tsx +40 -0
  87. package/src/app/(main)/changelog/features/Post.tsx +56 -0
  88. package/src/app/(main)/changelog/features/PublishedTime.tsx +50 -0
  89. package/src/app/(main)/changelog/features/VersionTag.tsx +27 -0
  90. package/src/app/(main)/changelog/layout.tsx +10 -0
  91. package/src/app/(main)/changelog/loading.tsx +3 -0
  92. package/src/app/(main)/changelog/modal/page.tsx +23 -0
  93. package/src/app/(main)/changelog/not-found.tsx +3 -0
  94. package/src/app/(main)/changelog/page.tsx +73 -0
  95. package/src/app/(main)/chat/(workspace)/page.tsx +9 -2
  96. package/src/app/(main)/settings/about/features/Version.tsx +2 -2
  97. package/src/app/@modal/(.)changelog/modal/features/Cover.tsx +48 -0
  98. package/src/app/@modal/(.)changelog/modal/features/Hero.tsx +29 -0
  99. package/src/app/@modal/(.)changelog/modal/features/Pagination.tsx +54 -0
  100. package/src/app/@modal/(.)changelog/modal/features/Post.tsx +57 -0
  101. package/src/app/@modal/(.)changelog/modal/features/PublishedTime.tsx +50 -0
  102. package/src/app/@modal/(.)changelog/modal/features/ReadDetail.tsx +94 -0
  103. package/src/app/@modal/(.)changelog/modal/features/UpdateChangelogStatus.tsx +21 -0
  104. package/src/app/@modal/(.)changelog/modal/features/VersionTag.tsx +27 -0
  105. package/src/app/@modal/(.)changelog/modal/layout.tsx +39 -0
  106. package/src/app/@modal/(.)changelog/modal/loading.tsx +10 -0
  107. package/src/app/@modal/(.)changelog/modal/page.tsx +37 -0
  108. package/src/app/@modal/(.)settings/modal/layout.tsx +19 -16
  109. package/src/app/@modal/_layout/ModalLayout.tsx +63 -0
  110. package/src/app/@modal/chat/(.)settings/modal/layout.tsx +20 -17
  111. package/src/app/@modal/layout.tsx +5 -69
  112. package/src/components/mdx/Image.tsx +50 -0
  113. package/src/components/mdx/index.tsx +2 -0
  114. package/src/const/url.ts +1 -0
  115. package/src/features/ChangelogModal/index.tsx +22 -0
  116. package/src/features/User/UserPanel/useMenu.tsx +50 -46
  117. package/src/features/User/__tests__/useMenu.test.tsx +7 -6
  118. package/src/hooks/useInterceptingRoutes.ts +1 -6
  119. package/src/hooks/useShare.tsx +1 -0
  120. package/src/locales/default/changelog.ts +18 -0
  121. package/src/locales/default/common.ts +1 -0
  122. package/src/locales/default/index.ts +2 -0
  123. package/src/locales/default/metadata.ts +4 -0
  124. package/src/server/metadata.ts +5 -3
  125. package/src/server/routers/edge/appStatus.ts +3 -0
  126. package/src/server/routers/edge/index.ts +2 -0
  127. package/src/server/routers/lambda/agent.ts +1 -1
  128. package/src/server/services/changelog/index.test.ts +310 -0
  129. package/src/server/services/changelog/index.ts +196 -0
  130. package/src/server/services/discover/index.test.ts +0 -1
  131. package/src/server/sitemap.ts +4 -1
  132. package/src/services/__tests__/chat.test.ts +1 -1
  133. package/src/services/__tests__/global.test.ts +5 -2
  134. package/src/services/_auth.ts +1 -1
  135. package/src/services/agent.ts +25 -21
  136. package/src/services/chat.ts +2 -2
  137. package/src/services/file/ClientS3/index.ts +6 -6
  138. package/src/services/file/client.ts +14 -15
  139. package/src/services/file/server.ts +20 -25
  140. package/src/services/global.ts +2 -2
  141. package/src/services/import/client.ts +6 -5
  142. package/src/services/import/server.ts +6 -5
  143. package/src/services/import/type.ts +7 -0
  144. package/src/services/knowledgeBase.ts +19 -19
  145. package/src/services/message/_deprecated.ts +5 -0
  146. package/src/services/message/client.ts +52 -48
  147. package/src/services/message/server.ts +50 -53
  148. package/src/services/message/type.ts +2 -2
  149. package/src/services/plugin/client.ts +16 -22
  150. package/src/services/plugin/server.ts +15 -19
  151. package/src/services/rag.ts +18 -18
  152. package/src/services/ragEval.ts +29 -26
  153. package/src/services/session/_deprecated.ts +2 -2
  154. package/src/services/session/client.ts +55 -81
  155. package/src/services/session/server.ts +50 -74
  156. package/src/services/session/type.ts +4 -6
  157. package/src/services/share.ts +4 -4
  158. package/src/services/textToImage.ts +5 -2
  159. package/src/services/thread/client.ts +9 -15
  160. package/src/services/thread/server.ts +10 -15
  161. package/src/services/topic/client.ts +25 -25
  162. package/src/services/topic/server.ts +25 -42
  163. package/src/services/trace.ts +4 -4
  164. package/src/services/user/client.ts +13 -17
  165. package/src/services/user/server.ts +9 -13
  166. package/src/services/user/type.ts +1 -1
  167. package/src/store/chat/slices/message/reducer.ts +3 -2
  168. package/src/store/global/action.ts +27 -22
  169. package/src/store/global/initialState.ts +1 -0
  170. package/src/types/changelog.ts +6 -0
  171. package/src/types/message/index.ts +10 -8
  172. 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
+ }
@@ -1,7 +1,6 @@
1
1
  // @vitest-environment node
2
2
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
- import { DEFAULT_LANG } from '@/const/locale';
5
4
  import { AssistantCategory, PluginCategory } from '@/types/discover';
6
5
 
7
6
  import { DiscoverService } from './index';
@@ -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 await importOriginal();
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({ 'dist-tags': { latest: mockVersion } }),
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
 
@@ -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 await createJWT<JWTPayload>({ accessCode, userId, ...payload });
81
+ return createJWT<JWTPayload>({ accessCode, userId, ...payload });
82
82
  };
83
83
 
84
84
  interface AuthParams {
@@ -1,45 +1,49 @@
1
1
  import { lambdaClient } from '@/libs/trpc/client';
2
2
 
3
3
  class AgentService {
4
- async createAgentKnowledgeBase(agentId: string, knowledgeBaseId: string, enabled?: boolean) {
5
- return await lambdaClient.agent.createAgentKnowledgeBase.mutate({
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 deleteAgentKnowledgeBase(agentId: string, knowledgeBaseId: string) {
13
- return await lambdaClient.agent.deleteAgentKnowledgeBase.mutate({ agentId, knowledgeBaseId });
14
- }
16
+ deleteAgentKnowledgeBase = async (agentId: string, knowledgeBaseId: string) => {
17
+ return lambdaClient.agent.deleteAgentKnowledgeBase.mutate({ agentId, knowledgeBaseId });
18
+ };
15
19
 
16
- async toggleKnowledgeBase(agentId: string, knowledgeBaseId: string, enabled?: boolean) {
17
- return await lambdaClient.agent.toggleKnowledgeBase.mutate({
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 createAgentFiles(agentId: string, fileIds: string[], enabled?: boolean) {
25
- return await lambdaClient.agent.createAgentFiles.mutate({ agentId, enabled, fileIds });
26
- }
28
+ createAgentFiles = async (agentId: string, fileIds: string[], enabled?: boolean) => {
29
+ return lambdaClient.agent.createAgentFiles.mutate({ agentId, enabled, fileIds });
30
+ };
27
31
 
28
- async deleteAgentFile(agentId: string, fileId: string) {
29
- return await lambdaClient.agent.deleteAgentFile.mutate({ agentId, fileId });
30
- }
32
+ deleteAgentFile = async (agentId: string, fileId: string) => {
33
+ return lambdaClient.agent.deleteAgentFile.mutate({ agentId, fileId });
34
+ };
31
35
 
32
- async toggleFile(agentId: string, fileId: string, enabled?: boolean) {
33
- return await lambdaClient.agent.toggleFile.mutate({
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 getFilesAndKnowledgeBases(agentId: string) {
41
- return await lambdaClient.agent.getKnowledgeBasesAndFiles.query({ agentId });
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();
@@ -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.