@lobehub/chat 1.39.2 → 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 (203) hide show
  1. package/.env.example +19 -8
  2. package/.eslintignore +1 -1
  3. package/CHANGELOG.md +58 -0
  4. package/changelog/v1.json +21 -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)/(mobile)/me/(home)/loading.tsx +1 -1
  80. package/src/app/(main)/(mobile)/me/data/loading.tsx +1 -1
  81. package/src/app/(main)/(mobile)/me/profile/loading.tsx +1 -1
  82. package/src/app/(main)/(mobile)/me/settings/loading.tsx +1 -1
  83. package/src/app/(main)/_layout/Desktop.tsx +4 -1
  84. package/src/app/(main)/_layout/Mobile.tsx +2 -1
  85. package/src/app/(main)/changelog/_layout/Desktop.tsx +25 -0
  86. package/src/app/(main)/changelog/_layout/Mobile/Header.tsx +33 -0
  87. package/src/app/(main)/changelog/_layout/Mobile/index.tsx +21 -0
  88. package/src/app/(main)/changelog/error.tsx +5 -0
  89. package/src/app/(main)/changelog/features/GridLayout.tsx +22 -0
  90. package/src/app/(main)/changelog/features/Hero.tsx +40 -0
  91. package/src/app/(main)/changelog/features/Post.tsx +56 -0
  92. package/src/app/(main)/changelog/features/PublishedTime.tsx +50 -0
  93. package/src/app/(main)/changelog/features/VersionTag.tsx +27 -0
  94. package/src/app/(main)/changelog/layout.tsx +10 -0
  95. package/src/app/(main)/changelog/loading.tsx +3 -0
  96. package/src/app/(main)/changelog/modal/page.tsx +23 -0
  97. package/src/app/(main)/changelog/not-found.tsx +3 -0
  98. package/src/app/(main)/changelog/page.tsx +73 -0
  99. package/src/app/(main)/chat/(workspace)/@portal/default.tsx +1 -1
  100. package/src/app/(main)/chat/(workspace)/@portal/loading.tsx +1 -1
  101. package/src/app/(main)/chat/(workspace)/page.tsx +9 -2
  102. package/src/app/(main)/chat/@session/default.tsx +3 -2
  103. package/src/app/(main)/chat/loading.tsx +1 -1
  104. package/src/app/(main)/chat/settings/loading.tsx +1 -1
  105. package/src/app/(main)/discover/loading.tsx +1 -1
  106. package/src/app/(main)/files/loading.tsx +2 -22
  107. package/src/app/(main)/profile/loading.tsx +1 -1
  108. package/src/app/(main)/repos/[id]/evals/dataset/page.tsx +1 -1
  109. package/src/app/(main)/repos/[id]/evals/evaluation/page.tsx +1 -1
  110. package/src/app/(main)/settings/@category/default.tsx +6 -2
  111. package/src/app/(main)/settings/_layout/Desktop/SideBar.tsx +1 -1
  112. package/src/app/(main)/settings/about/features/Version.tsx +2 -2
  113. package/src/app/(main)/settings/loading.tsx +2 -8
  114. package/src/app/@modal/(.)changelog/modal/features/Cover.tsx +48 -0
  115. package/src/app/@modal/(.)changelog/modal/features/Hero.tsx +29 -0
  116. package/src/app/@modal/(.)changelog/modal/features/Pagination.tsx +54 -0
  117. package/src/app/@modal/(.)changelog/modal/features/Post.tsx +57 -0
  118. package/src/app/@modal/(.)changelog/modal/features/PublishedTime.tsx +50 -0
  119. package/src/app/@modal/(.)changelog/modal/features/ReadDetail.tsx +94 -0
  120. package/src/app/@modal/(.)changelog/modal/features/UpdateChangelogStatus.tsx +21 -0
  121. package/src/app/@modal/(.)changelog/modal/features/VersionTag.tsx +27 -0
  122. package/src/app/@modal/(.)changelog/modal/layout.tsx +39 -0
  123. package/src/app/@modal/(.)changelog/modal/loading.tsx +10 -0
  124. package/src/app/@modal/(.)changelog/modal/page.tsx +37 -0
  125. package/src/app/@modal/(.)settings/modal/layout.tsx +19 -16
  126. package/src/app/@modal/_layout/ModalLayout.tsx +63 -0
  127. package/src/app/@modal/chat/(.)settings/modal/layout.tsx +20 -17
  128. package/src/app/@modal/layout.tsx +5 -69
  129. package/src/app/loading/Client/Content.tsx +1 -1
  130. package/src/app/loading/Server/Content.tsx +1 -1
  131. package/src/components/Loading/BrandTextLoading/LobeChatText/SVG.tsx +44 -0
  132. package/src/components/Loading/BrandTextLoading/LobeChatText/index.tsx +6 -0
  133. package/src/components/Loading/BrandTextLoading/LobeChatText/style.css +32 -0
  134. package/src/components/Loading/BrandTextLoading/index.tsx +11 -0
  135. package/src/components/{SkeletonLoading → Loading/SkeletonLoading}/index.tsx +1 -1
  136. package/src/components/mdx/Image.tsx +50 -0
  137. package/src/components/mdx/index.tsx +2 -0
  138. package/src/const/url.ts +1 -0
  139. package/src/features/ChangelogModal/index.tsx +22 -0
  140. package/src/features/FileViewer/Renderer/TXT/index.tsx +1 -1
  141. package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
  142. package/src/features/Portal/Home/Body/Files/FileList/index.tsx +1 -1
  143. package/src/features/Setting/Footer.tsx +3 -1
  144. package/src/features/Setting/SettingContainer.tsx +1 -0
  145. package/src/features/User/UserPanel/useMenu.tsx +50 -46
  146. package/src/features/User/__tests__/useMenu.test.tsx +7 -6
  147. package/src/hooks/useInterceptingRoutes.ts +1 -6
  148. package/src/hooks/useShare.tsx +1 -0
  149. package/src/locales/default/changelog.ts +18 -0
  150. package/src/locales/default/common.ts +1 -0
  151. package/src/locales/default/index.ts +2 -0
  152. package/src/locales/default/metadata.ts +4 -0
  153. package/src/server/metadata.ts +5 -3
  154. package/src/server/routers/edge/appStatus.ts +3 -0
  155. package/src/server/routers/edge/index.ts +2 -0
  156. package/src/server/routers/lambda/agent.ts +1 -1
  157. package/src/server/services/changelog/index.test.ts +310 -0
  158. package/src/server/services/changelog/index.ts +196 -0
  159. package/src/server/services/discover/index.test.ts +0 -1
  160. package/src/server/sitemap.ts +4 -1
  161. package/src/services/__tests__/chat.test.ts +1 -1
  162. package/src/services/__tests__/global.test.ts +5 -2
  163. package/src/services/_auth.ts +1 -1
  164. package/src/services/agent.ts +25 -21
  165. package/src/services/chat.ts +2 -2
  166. package/src/services/file/ClientS3/index.ts +6 -6
  167. package/src/services/file/client.ts +14 -15
  168. package/src/services/file/server.ts +20 -25
  169. package/src/services/global.ts +2 -2
  170. package/src/services/import/client.ts +6 -5
  171. package/src/services/import/server.ts +6 -5
  172. package/src/services/import/type.ts +7 -0
  173. package/src/services/knowledgeBase.ts +19 -19
  174. package/src/services/message/_deprecated.ts +5 -0
  175. package/src/services/message/client.ts +52 -48
  176. package/src/services/message/server.ts +50 -53
  177. package/src/services/message/type.ts +2 -2
  178. package/src/services/plugin/client.ts +16 -22
  179. package/src/services/plugin/server.ts +15 -19
  180. package/src/services/rag.ts +18 -18
  181. package/src/services/ragEval.ts +29 -26
  182. package/src/services/session/_deprecated.ts +2 -2
  183. package/src/services/session/client.ts +55 -81
  184. package/src/services/session/server.ts +50 -74
  185. package/src/services/session/type.ts +4 -6
  186. package/src/services/share.ts +4 -4
  187. package/src/services/textToImage.ts +5 -2
  188. package/src/services/thread/client.ts +9 -15
  189. package/src/services/thread/server.ts +10 -15
  190. package/src/services/topic/client.ts +25 -25
  191. package/src/services/topic/server.ts +25 -42
  192. package/src/services/trace.ts +4 -4
  193. package/src/services/user/client.ts +13 -17
  194. package/src/services/user/server.ts +9 -13
  195. package/src/services/user/type.ts +1 -1
  196. package/src/store/chat/slices/message/reducer.ts +3 -2
  197. package/src/store/global/action.ts +27 -22
  198. package/src/store/global/initialState.ts +1 -0
  199. package/src/types/changelog.ts +6 -0
  200. package/src/types/message/index.ts +10 -8
  201. package/src/app/@modal/features/InterceptingContext.tsx +0 -9
  202. /package/src/components/{CircleLoading → Loading/CircleLoading}/index.tsx +0 -0
  203. /package/src/components/{FullscreenLoading → Loading/FullscreenLoading}/index.tsx +0 -0
@@ -7,6 +7,7 @@ import {
7
7
  Cloudy,
8
8
  Download,
9
9
  Feather,
10
+ FileClockIcon,
10
11
  HardDriveDownload,
11
12
  HardDriveUpload,
12
13
  LifeBuoy,
@@ -174,15 +175,38 @@ export const useMenu = () => {
174
175
  },
175
176
  ].filter(Boolean) as ItemType[]);
176
177
 
177
- const helps: MenuProps['items'] = hideDocs
178
- ? []
179
- : ([
180
- showCloudPromotion && {
181
- icon: <Icon icon={Cloudy} />,
182
- key: 'cloud',
178
+ const helps: MenuProps['items'] = [
179
+ showCloudPromotion && {
180
+ icon: <Icon icon={Cloudy} />,
181
+ key: 'cloud',
182
+ label: (
183
+ <Link href={`${OFFICIAL_URL}?utm_source=${UTM_SOURCE}`} target={'_blank'}>
184
+ {t('userPanel.cloud', { name: LOBE_CHAT_CLOUD })}
185
+ </Link>
186
+ ),
187
+ },
188
+ {
189
+ icon: <Icon icon={FileClockIcon} />,
190
+ key: 'changelog',
191
+ label: <Link href={'/changelog'}>{t('changelog')}</Link>,
192
+ },
193
+ {
194
+ children: [
195
+ {
196
+ icon: <Icon icon={Book} />,
197
+ key: 'docs',
183
198
  label: (
184
- <Link href={`${OFFICIAL_URL}?utm_source=${UTM_SOURCE}`} target={'_blank'}>
185
- {t('userPanel.cloud', { name: LOBE_CHAT_CLOUD })}
199
+ <Link href={DOCUMENTS_REFER_URL} target={'_blank'}>
200
+ {t('userPanel.docs')}
201
+ </Link>
202
+ ),
203
+ },
204
+ {
205
+ icon: <Icon icon={Feather} />,
206
+ key: 'feedback',
207
+ label: (
208
+ <Link href={GITHUB_ISSUES} target={'_blank'}>
209
+ {t('userPanel.feedback')}
186
210
  </Link>
187
211
  ),
188
212
  },
@@ -196,56 +220,36 @@ export const useMenu = () => {
196
220
  ),
197
221
  },
198
222
  {
199
- children: [
200
- {
201
- icon: <Icon icon={Book} />,
202
- key: 'docs',
203
- label: (
204
- <Link href={DOCUMENTS_REFER_URL} target={'_blank'}>
205
- {t('userPanel.docs')}
206
- </Link>
207
- ),
208
- },
209
- {
210
- icon: <Icon icon={Feather} />,
211
- key: 'feedback',
212
- label: (
213
- <Link href={GITHUB_ISSUES} target={'_blank'}>
214
- {t('userPanel.feedback')}
215
- </Link>
216
- ),
217
- },
218
- {
219
- icon: <Icon icon={Mail} />,
220
- key: 'email',
221
- label: (
222
- <Link href={mailTo(EMAIL_SUPPORT)} target={'_blank'}>
223
- {t('userPanel.email')}
224
- </Link>
225
- ),
226
- },
227
- ],
228
- icon: <Icon icon={LifeBuoy} />,
229
- key: 'help',
230
- label: t('userPanel.help'),
231
- },
232
- {
233
- type: 'divider',
223
+ icon: <Icon icon={Mail} />,
224
+ key: 'email',
225
+ label: (
226
+ <Link href={mailTo(EMAIL_SUPPORT)} target={'_blank'}>
227
+ {t('userPanel.email')}
228
+ </Link>
229
+ ),
234
230
  },
235
- ].filter(Boolean) as ItemType[]);
231
+ ],
232
+ icon: <Icon icon={LifeBuoy} />,
233
+ key: 'help',
234
+ label: t('userPanel.help'),
235
+ },
236
+ {
237
+ type: 'divider',
238
+ },
239
+ ].filter(Boolean) as ItemType[];
236
240
 
237
241
  const mainItems = [
238
242
  {
239
243
  type: 'divider',
240
244
  },
241
- ...(isLogin ? settings : []),
242
245
  ...(isLoginWithClerk ? profile : []),
246
+ ...(isLogin ? settings : []),
243
247
  /* ↓ cloud slot ↓ */
244
248
 
245
249
  /* ↑ cloud slot ↑ */
246
250
  ...(canInstall ? pwa : []),
247
251
  ...data,
248
- ...helps,
252
+ ...(!hideDocs ? helps : []),
249
253
  ].filter(Boolean) as MenuProps['items'];
250
254
 
251
255
  const logoutItems: MenuProps['items'] = isLoginWithAuth
@@ -1,13 +1,14 @@
1
1
  import { act, renderHook } from '@testing-library/react';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
3
 
4
- import { useUserStore } from '@/store/user';
5
4
  import { ServerConfigStoreProvider } from '@/store/serverConfig';
5
+ import { useUserStore } from '@/store/user';
6
6
 
7
7
  import { useMenu } from '../UserPanel/useMenu';
8
8
 
9
- const wrapper: React.JSXElementConstructor<{ children: React.ReactNode }> = ({ children }) =>
9
+ const wrapper: React.JSXElementConstructor<{ children: React.ReactNode }> = ({ children }) => (
10
10
  <ServerConfigStoreProvider>{children}</ServerConfigStoreProvider>
11
+ );
11
12
 
12
13
  // Mock dependencies
13
14
  vi.mock('next/link', () => ({
@@ -81,7 +82,7 @@ describe('useMenu', () => {
81
82
  expect(mainItems?.some((item) => item?.key === 'setting')).toBe(true);
82
83
  expect(mainItems?.some((item) => item?.key === 'import')).toBe(true);
83
84
  expect(mainItems?.some((item) => item?.key === 'export')).toBe(true);
84
- expect(mainItems?.some((item) => item?.key === 'discord')).toBe(true);
85
+ expect(mainItems?.some((item) => item?.key === 'changelog')).toBe(true);
85
86
  expect(logoutItems.some((item) => item?.key === 'logout')).toBe(true);
86
87
  });
87
88
  });
@@ -101,7 +102,7 @@ describe('useMenu', () => {
101
102
  expect(mainItems?.some((item) => item?.key === 'setting')).toBe(true);
102
103
  expect(mainItems?.some((item) => item?.key === 'import')).toBe(true);
103
104
  expect(mainItems?.some((item) => item?.key === 'export')).toBe(true);
104
- expect(mainItems?.some((item) => item?.key === 'discord')).toBe(true);
105
+ expect(mainItems?.some((item) => item?.key === 'changelog')).toBe(true);
105
106
  expect(logoutItems.some((item) => item?.key === 'logout')).toBe(true);
106
107
  });
107
108
  });
@@ -120,7 +121,7 @@ describe('useMenu', () => {
120
121
  expect(mainItems?.some((item) => item?.key === 'setting')).toBe(true);
121
122
  expect(mainItems?.some((item) => item?.key === 'import')).toBe(true);
122
123
  expect(mainItems?.some((item) => item?.key === 'export')).toBe(true);
123
- expect(mainItems?.some((item) => item?.key === 'discord')).toBe(true);
124
+ expect(mainItems?.some((item) => item?.key === 'changelog')).toBe(true);
124
125
  expect(logoutItems.some((item) => item?.key === 'logout')).toBe(false);
125
126
  });
126
127
  });
@@ -139,7 +140,7 @@ describe('useMenu', () => {
139
140
  expect(mainItems?.some((item) => item?.key === 'setting')).toBe(false);
140
141
  expect(mainItems?.some((item) => item?.key === 'import')).toBe(false);
141
142
  expect(mainItems?.some((item) => item?.key === 'export')).toBe(false);
142
- expect(mainItems?.some((item) => item?.key === 'discord')).toBe(true);
143
+ expect(mainItems?.some((item) => item?.key === 'changelog')).toBe(true);
143
144
  expect(logoutItems.some((item) => item?.key === 'logout')).toBe(false);
144
145
  });
145
146
  });
@@ -1,7 +1,6 @@
1
- import { useContext, useMemo } from 'react';
1
+ import { useMemo } from 'react';
2
2
  import urlJoin from 'url-join';
3
3
 
4
- import { InterceptContext } from '@/app/@modal/features/InterceptingContext';
5
4
  import { INBOX_SESSION_ID } from '@/const/session';
6
5
  import { useIsMobile } from '@/hooks/useIsMobile';
7
6
  import { useQueryRoute } from '@/hooks/useQueryRoute';
@@ -45,7 +44,3 @@ export const useOpenChatSettings = (tab: ChatSettingsTabs = ChatSettingsTabs.Met
45
44
  }
46
45
  }, [openSettings, mobile, activeId, router, tab]);
47
46
  };
48
-
49
- export const useInterceptingRoutes = () => {
50
- return useContext(InterceptContext);
51
- };
@@ -15,6 +15,7 @@ const stringifyHashtags = (hashtags: string[], joinfix: string = ',', prefix?: s
15
15
  if (prefix) hashtags = hashtags.map((tag) => prefix + camelCase(tag));
16
16
  return hashtags.filter(Boolean).join(joinfix);
17
17
  };
18
+
18
19
  export const useShare = ({
19
20
  url,
20
21
  title,
@@ -0,0 +1,18 @@
1
+ export default {
2
+ actions: {
3
+ followOnX: '在 X 上关注我们',
4
+ subscribeToUpdates: '订阅更新',
5
+ versions: '版本详情',
6
+ },
7
+ addedWhileAway: '在您离开期间,我们带来了新的特性。',
8
+ allChangelog: '查看所有更新日志',
9
+ description: '持续追踪 {{appName}} 的新功能和改进',
10
+ pagination: {
11
+ older: '查看历史变更',
12
+ prev: '上一页',
13
+ },
14
+ readDetails: '阅读详情',
15
+ title: '更新日志',
16
+ versionDetails: '版本详情',
17
+ welcomeBack: '欢迎回来!',
18
+ };
@@ -220,6 +220,7 @@ export default {
220
220
  pinOff: '取消置顶',
221
221
  privacy: '隐私政策',
222
222
  regenerate: '重新生成',
223
+ releaseNotes: '版本详情',
223
224
  rename: '重命名',
224
225
  reset: '重置',
225
226
  retry: '重试',
@@ -1,4 +1,5 @@
1
1
  import auth from './auth';
2
+ import changelog from './changelog';
2
3
  import chat from './chat';
3
4
  import clerk from './clerk';
4
5
  import common from './common';
@@ -23,6 +24,7 @@ import welcome from './welcome';
23
24
 
24
25
  const resources = {
25
26
  auth,
27
+ changelog,
26
28
  chat,
27
29
  clerk,
28
30
  common,
@@ -1,4 +1,8 @@
1
1
  export default {
2
+ changelog: {
3
+ description: '持续追踪 {{appName}} 的新功能和改进',
4
+ title: '更新日志',
5
+ },
2
6
  chat: {
3
7
  description: '{{appName}} 带给你最好的 ChatGPT, Claude , Gemini, OLLaMA WebUI 使用体验',
4
8
  title: '{{appName}}:个人 AI 效能工具,给自己一个更聪明的大脑',
@@ -18,8 +18,10 @@ export class Meta {
18
18
  tags,
19
19
  alternate,
20
20
  locale = DEFAULT_LANG,
21
+ canonical,
21
22
  }: {
22
23
  alternate?: boolean;
24
+ canonical?: string;
23
25
  description?: string;
24
26
  image?: string;
25
27
  locale?: Locales;
@@ -35,9 +37,9 @@ export class Meta {
35
37
  const siteTitle = title.includes(BRANDING_NAME) ? title : title + ` · ${BRANDING_NAME}`;
36
38
  return {
37
39
  alternates: {
38
- canonical: getCanonicalUrl(
39
- alternate ? qs.stringifyUrl({ query: { hl: locale }, url }) : url,
40
- ),
40
+ canonical:
41
+ canonical ||
42
+ getCanonicalUrl(alternate ? qs.stringifyUrl({ query: { hl: locale }, url }) : url),
41
43
  languages: alternate ? this.genAlternateLocales(locale, url) : undefined,
42
44
  },
43
45
  description: formatedDescription,
@@ -0,0 +1,3 @@
1
+ import { router } from '@/libs/trpc';
2
+
3
+ export const appStatusRouter = router({});
@@ -3,10 +3,12 @@
3
3
  */
4
4
  import { publicProcedure, router } from '@/libs/trpc';
5
5
 
6
+ import { appStatusRouter } from './appStatus';
6
7
  import { configRouter } from './config';
7
8
  import { uploadRouter } from './upload';
8
9
 
9
10
  export const edgeRouter = router({
11
+ appStatus: appStatusRouter,
10
12
  config: configRouter,
11
13
  healthcheck: publicProcedure.query(() => "i'm live!"),
12
14
  upload: uploadRouter,
@@ -101,7 +101,7 @@ export const agentRouter = router({
101
101
  if (!session) throw new Error('Session not found');
102
102
  const sessionId = session.id;
103
103
 
104
- return await ctx.agentModel.findBySessionId(sessionId);
104
+ return ctx.agentModel.findBySessionId(sessionId);
105
105
  }),
106
106
 
107
107
  getKnowledgeBasesAndFiles: agentProcedure
@@ -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
+ });