@lobehub/chat 1.97.0 → 1.97.2

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 (37) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +90 -32
  3. package/README.zh-CN.md +90 -34
  4. package/changelog/v1.json +18 -0
  5. package/docs/usage/features/desktop.mdx +53 -0
  6. package/docs/usage/features/desktop.zh-CN.mdx +49 -0
  7. package/docs/usage/features/mcp-market.mdx +26 -0
  8. package/docs/usage/features/mcp-market.zh-CN.mdx +22 -0
  9. package/docs/usage/features/mcp.mdx +58 -0
  10. package/docs/usage/features/mcp.zh-CN.mdx +54 -0
  11. package/docs/usage/features/search.mdx +56 -0
  12. package/docs/usage/features/search.zh-CN.mdx +52 -0
  13. package/package.json +3 -2
  14. package/src/app/(backend)/trpc/async/[trpc]/route.ts +4 -2
  15. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/index.tsx +2 -0
  16. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Mobile/index.tsx +2 -0
  17. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/index.tsx +35 -2
  18. package/src/components/Analytics/LobeAnalyticsProvider.tsx +68 -0
  19. package/src/components/Analytics/LobeAnalyticsProviderWrapper.tsx +23 -0
  20. package/src/components/Analytics/MainInterfaceTracker.tsx +52 -0
  21. package/src/components/Analytics/index.tsx +0 -8
  22. package/src/const/analytics.ts +3 -0
  23. package/src/database/schemas/relations.ts +44 -0
  24. package/src/features/AgentSetting/store/action.ts +26 -1
  25. package/src/features/ChatInput/useSend.ts +29 -1
  26. package/src/features/User/UserLoginOrSignup/Community.tsx +14 -1
  27. package/src/layout/GlobalProvider/Locale.tsx +2 -1
  28. package/src/layout/GlobalProvider/index.tsx +4 -1
  29. package/src/libs/analytics/index.ts +25 -0
  30. package/src/locales/create.ts +8 -3
  31. package/src/server/routers/async/caller.ts +2 -2
  32. package/src/server/services/user/index.test.ts +8 -0
  33. package/src/server/services/user/index.ts +18 -0
  34. package/src/store/session/slices/session/action.ts +28 -2
  35. package/src/store/user/slices/common/action.ts +9 -2
  36. package/src/utils/locale.ts +16 -1
  37. package/tests/setup.ts +11 -0
@@ -8,7 +8,6 @@ import Google from './Google';
8
8
  import Vercel from './Vercel';
9
9
 
10
10
  const Plausible = dynamic(() => import('./Plausible'));
11
- const Posthog = dynamic(() => import('./Posthog'));
12
11
  const Umami = dynamic(() => import('./Umami'));
13
12
  const Clarity = dynamic(() => import('./Clarity'));
14
13
  const ReactScan = dynamic(() => import('./ReactScan'));
@@ -24,13 +23,6 @@ const Analytics = () => {
24
23
  scriptBaseUrl={analyticsEnv.PLAUSIBLE_SCRIPT_BASE_URL}
25
24
  />
26
25
  )}
27
- {analyticsEnv.ENABLED_POSTHOG_ANALYTICS && (
28
- <Posthog
29
- debug={analyticsEnv.DEBUG_POSTHOG_ANALYTICS}
30
- host={analyticsEnv.POSTHOG_HOST!}
31
- token={analyticsEnv.POSTHOG_KEY}
32
- />
33
- )}
34
26
  {analyticsEnv.ENABLED_UMAMI_ANALYTICS && (
35
27
  <Umami
36
28
  scriptUrl={analyticsEnv.UMAMI_SCRIPT_URL}
@@ -0,0 +1,3 @@
1
+ import { isDesktop } from '@/const/version';
2
+
3
+ export const BUSINESS_LINE = isDesktop ? 'lobe-chat-desktop' : 'lobe-chat';
@@ -122,6 +122,17 @@ export const agentsToSessionsRelations = relations(agentsToSessions, ({ one }) =
122
122
  }),
123
123
  }));
124
124
 
125
+ export const filesToSessionsRelations = relations(filesToSessions, ({ one }) => ({
126
+ file: one(files, {
127
+ fields: [filesToSessions.fileId],
128
+ references: [files.id],
129
+ }),
130
+ session: one(sessions, {
131
+ fields: [filesToSessions.sessionId],
132
+ references: [sessions.id],
133
+ }),
134
+ }));
135
+
125
136
  export const agentsKnowledgeBasesRelations = relations(agentsKnowledgeBases, ({ one }) => ({
126
137
  knowledgeBase: one(knowledgeBases, {
127
138
  fields: [agentsKnowledgeBases.knowledgeBaseId],
@@ -133,6 +144,39 @@ export const agentsKnowledgeBasesRelations = relations(agentsKnowledgeBases, ({
133
144
  }),
134
145
  }));
135
146
 
147
+ export const agentsFilesRelations = relations(agentsFiles, ({ one }) => ({
148
+ file: one(files, {
149
+ fields: [agentsFiles.fileId],
150
+ references: [files.id],
151
+ }),
152
+ agent: one(agents, {
153
+ fields: [agentsFiles.agentId],
154
+ references: [agents.id],
155
+ }),
156
+ }));
157
+
158
+ export const messagesFilesRelations = relations(messagesFiles, ({ one }) => ({
159
+ file: one(files, {
160
+ fields: [messagesFiles.fileId],
161
+ references: [files.id],
162
+ }),
163
+ message: one(messages, {
164
+ fields: [messagesFiles.messageId],
165
+ references: [messages.id],
166
+ }),
167
+ }));
168
+
169
+ export const fileChunksRelations = relations(fileChunks, ({ one }) => ({
170
+ file: one(files, {
171
+ fields: [fileChunks.fileId],
172
+ references: [files.id],
173
+ }),
174
+ chunk: one(chunks, {
175
+ fields: [fileChunks.chunkId],
176
+ references: [chunks.id],
177
+ }),
178
+ }));
179
+
136
180
  export const sessionsRelations = relations(sessions, ({ many, one }) => ({
137
181
  filesToSessions: many(filesToSessions),
138
182
  agentsToSessions: many(agentsToSessions),
@@ -1,3 +1,4 @@
1
+ import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
1
2
  import { DeepPartial } from 'utility-types';
2
3
  import { StateCreator } from 'zustand/vanilla';
3
4
 
@@ -260,7 +261,31 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
260
261
  await get().dispatchConfig({ config, type: 'update' });
261
262
  },
262
263
  setAgentMeta: async (meta) => {
263
- await get().dispatchMeta({ type: 'update', value: meta });
264
+ const { dispatchMeta, id, meta: currentMeta } = get();
265
+ const mergedMeta = merge(currentMeta, meta);
266
+
267
+ try {
268
+ const analytics = getSingletonAnalyticsOptional();
269
+ if (analytics) {
270
+ analytics.track({
271
+ name: 'agent_meta_updated',
272
+ properties: {
273
+ assistant_avatar: mergedMeta.avatar,
274
+ assistant_background_color: mergedMeta.backgroundColor,
275
+ assistant_description: mergedMeta.description,
276
+ assistant_name: mergedMeta.title,
277
+ assistant_tags: mergedMeta.tags,
278
+ is_inbox: id === 'inbox',
279
+ session_id: id || 'unknown',
280
+ timestamp: Date.now(),
281
+ user_id: useUserStore.getState().user?.id || 'anonymous',
282
+ },
283
+ });
284
+ }
285
+ } catch (error) {
286
+ console.warn('Failed to track agent meta update:', error);
287
+ }
288
+ await dispatchMeta({ type: 'update', value: meta });
264
289
  },
265
290
 
266
291
  setChatConfig: async (config) => {
@@ -1,8 +1,12 @@
1
+ import { useAnalytics } from '@lobehub/analytics/react';
1
2
  import { useCallback, useMemo } from 'react';
2
3
 
4
+ import { getAgentStoreState } from '@/store/agent';
5
+ import { agentSelectors } from '@/store/agent/selectors';
3
6
  import { useChatStore } from '@/store/chat';
4
- import { chatSelectors } from '@/store/chat/selectors';
7
+ import { chatSelectors, topicSelectors } from '@/store/chat/selectors';
5
8
  import { fileChatSelectors, useFileStore } from '@/store/file';
9
+ import { getUserStoreState } from '@/store/user';
6
10
  import { SendMessageParams } from '@/types/message';
7
11
 
8
12
  export type UseSendMessageParams = Pick<
@@ -15,6 +19,7 @@ export const useSendMessage = () => {
15
19
  s.sendMessage,
16
20
  s.updateInputMessage,
17
21
  ]);
22
+ const { analytics } = useAnalytics();
18
23
 
19
24
  const clearChatUploadFileList = useFileStore((s) => s.clearChatUploadFileList);
20
25
 
@@ -49,6 +54,29 @@ export const useSendMessage = () => {
49
54
  updateInputMessage('');
50
55
  clearChatUploadFileList();
51
56
 
57
+ // 获取分析数据
58
+ const userStore = getUserStoreState();
59
+ const agentStore = getAgentStoreState();
60
+
61
+ // 直接使用现有数据结构判断消息类型
62
+ const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
63
+ const messageType = fileList.length === 0 ? 'text' : hasImages ? 'image' : 'file';
64
+
65
+ analytics?.track({
66
+ name: 'send_message',
67
+ properties: {
68
+ chat_id: store.activeId || 'unknown',
69
+ current_topic: topicSelectors.currentActiveTopic(store)?.title || null,
70
+ has_attachments: fileList.length > 0,
71
+ history_message_count: chatSelectors.activeBaseChats(store).length,
72
+ message: store.inputMessage,
73
+ message_length: store.inputMessage.length,
74
+ message_type: messageType,
75
+ selected_model: agentSelectors.currentAgentModel(agentStore),
76
+ session_id: store.activeId || 'inbox', // 当前活跃的会话ID
77
+ user_id: userStore.user?.id || 'anonymous',
78
+ },
79
+ });
52
80
  // const hasSystemRole = agentSelectors.hasSystemRole(useAgentStore.getState());
53
81
  // const agentSetting = useAgentStore.getState().agentSettingInstance;
54
82
 
@@ -1,3 +1,4 @@
1
+ import { useAnalytics } from '@lobehub/analytics/react';
1
2
  import { Button } from '@lobehub/ui';
2
3
  import { memo } from 'react';
3
4
  import { useTranslation } from 'react-i18next';
@@ -7,12 +8,24 @@ import UserInfo from '../UserInfo';
7
8
 
8
9
  const UserLoginOrSignup = memo<{ onClick: () => void }>(({ onClick }) => {
9
10
  const { t } = useTranslation('auth');
11
+ const { analytics } = useAnalytics();
12
+
13
+ const handleClick = () => {
14
+ analytics?.track({
15
+ name: 'login_or_signup_clicked',
16
+ properties: {
17
+ spm: 'homepage.login_or_signup.click',
18
+ },
19
+ });
20
+
21
+ onClick();
22
+ };
10
23
 
11
24
  return (
12
25
  <>
13
26
  <UserInfo />
14
27
  <Flexbox paddingBlock={12} paddingInline={16} width={'100%'}>
15
- <Button block onClick={onClick} type={'primary'}>
28
+ <Button block onClick={handleClick} type={'primary'}>
16
29
  {t('loginOrSignup')}
17
30
  </Button>
18
31
  </Flexbox>
@@ -40,7 +40,8 @@ const Locale = memo<LocaleLayoutProps>(({ children, defaultLang, antdLocale }) =
40
40
 
41
41
  // if run on server side, init i18n instance everytime
42
42
  if (isOnServerSide) {
43
- i18n.init();
43
+ // use sync mode to init instantly
44
+ i18n.init({ initAsync: false });
44
45
 
45
46
  // load the dayjs locale
46
47
  // if (lang) {
@@ -1,5 +1,6 @@
1
1
  import { ReactNode, Suspense } from 'react';
2
2
 
3
+ import { LobeAnalyticsProviderWrapper } from '@/components/Analytics/LobeAnalyticsProviderWrapper';
3
4
  import { getServerFeatureFlagsValue } from '@/config/featureFlags';
4
5
  import { appEnv } from '@/envs/app';
5
6
  import DevPanel from '@/features/DevPanel';
@@ -54,7 +55,9 @@ const GlobalLayout = async ({
54
55
  isMobile={isMobile}
55
56
  serverConfig={serverConfig}
56
57
  >
57
- <QueryProvider>{children}</QueryProvider>
58
+ <QueryProvider>
59
+ <LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
60
+ </QueryProvider>
58
61
  <StoreInitialization />
59
62
  <Suspense>
60
63
  <ImportSettings />
@@ -0,0 +1,25 @@
1
+ import { createServerAnalytics } from '@lobehub/analytics/server';
2
+
3
+ import { analyticsEnv } from '@/config/analytics';
4
+ import { BUSINESS_LINE } from '@/const/analytics';
5
+ import { isDev } from '@/utils/env';
6
+
7
+ export const serverAnalytics = createServerAnalytics({
8
+ business: BUSINESS_LINE,
9
+ debug: isDev,
10
+ providers: {
11
+ posthogNode: {
12
+ debug: analyticsEnv.DEBUG_POSTHOG_ANALYTICS,
13
+ enabled: analyticsEnv.ENABLED_POSTHOG_ANALYTICS,
14
+ host: analyticsEnv.POSTHOG_HOST,
15
+ key: analyticsEnv.POSTHOG_KEY ?? '',
16
+ },
17
+ },
18
+ });
19
+
20
+ export const initializeServerAnalytics = async () => {
21
+ await serverAnalytics.initialize();
22
+ return serverAnalytics;
23
+ };
24
+
25
+ export default serverAnalytics;
@@ -31,8 +31,10 @@ export const createI18nNext = (lang?: string) => {
31
31
  }
32
32
  });
33
33
  return {
34
- init: () =>
35
- instance.init({
34
+ init: (params: { initAsync?: boolean } = {}) => {
35
+ const { initAsync = true } = params;
36
+
37
+ return instance.init({
36
38
  debug: debugMode,
37
39
  defaultNS: ['error', 'common', 'chat'],
38
40
  // detection: {
@@ -50,11 +52,14 @@ export const createI18nNext = (lang?: string) => {
50
52
  // lookupCookie: LOBE_LOCALE_COOKIE,
51
53
  // },
52
54
  fallbackLng: DEFAULT_LANG,
55
+
56
+ initAsync,
53
57
  interpolation: {
54
58
  escapeValue: false,
55
59
  },
56
60
  lng: lang,
57
- }),
61
+ });
62
+ },
58
63
  instance,
59
64
  };
60
65
  };
@@ -1,4 +1,4 @@
1
- import { createTRPCClient, httpBatchLink } from '@trpc/client';
1
+ import { createTRPCClient, httpLink } from '@trpc/client';
2
2
  import superjson from 'superjson';
3
3
  import urlJoin from 'url-join';
4
4
 
@@ -26,7 +26,7 @@ export const createAsyncServerClient = async (userId: string, payload: JWTPayloa
26
26
 
27
27
  return createTRPCClient<AsyncRouter>({
28
28
  links: [
29
- httpBatchLink({
29
+ httpLink({
30
30
  headers,
31
31
  transformer: superjson,
32
32
  url: urlJoin(appEnv.APP_URL!, '/trpc/async'),
@@ -8,6 +8,14 @@ import { AgentService } from '@/server/services/agent';
8
8
 
9
9
  import { UserService } from './index';
10
10
 
11
+ // Mock @/libs/analytics to avoid server-side environment variable access in client test environment
12
+ vi.mock('@/libs/analytics', () => ({
13
+ initializeServerAnalytics: vi.fn().mockResolvedValue({
14
+ identify: vi.fn(),
15
+ track: vi.fn(),
16
+ }),
17
+ }));
18
+
11
19
  // Mock dependencies
12
20
  vi.mock('@/database/models/user', () => {
13
21
  const MockUserModel = vi.fn();
@@ -2,6 +2,7 @@ import { UserJSON } from '@clerk/backend';
2
2
 
3
3
  import { UserModel } from '@/database/models/user';
4
4
  import { serverDB } from '@/database/server';
5
+ import { initializeServerAnalytics } from '@/libs/analytics';
5
6
  import { pino } from '@/libs/logger';
6
7
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
7
8
  import { S3 } from '@/server/modules/S3';
@@ -51,6 +52,23 @@ export class UserService {
51
52
 
52
53
  /* ↑ cloud slot ↑ */
53
54
 
55
+ //analytics
56
+ const analytics = await initializeServerAnalytics();
57
+ analytics?.identify(id, {
58
+ email: email?.email_address,
59
+ firstName: params.first_name,
60
+ lastName: params.last_name,
61
+ phone: phone?.phone_number,
62
+ username: params.username,
63
+ });
64
+ analytics?.track({
65
+ name: 'user_register_completed',
66
+ properties: {
67
+ spm: 'user_service.create_user.user_created',
68
+ },
69
+ userId: id,
70
+ });
71
+
54
72
  return { message: 'user created', success: true };
55
73
  };
56
74
 
@@ -1,3 +1,4 @@
1
+ import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
1
2
  import isEqual from 'fast-deep-equal';
2
3
  import { t } from 'i18next';
3
4
  import useSWR, { SWRResponse, mutate } from 'swr';
@@ -10,8 +11,8 @@ import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
10
11
  import { useClientDataSWR } from '@/libs/swr';
11
12
  import { sessionService } from '@/services/session';
12
13
  import { SessionStore } from '@/store/session';
13
- import { useUserStore } from '@/store/user';
14
- import { settingsSelectors } from '@/store/user/selectors';
14
+ import { getUserStoreState, useUserStore } from '@/store/user';
15
+ import { settingsSelectors, userProfileSelectors } from '@/store/user/selectors';
15
16
  import { MetaData } from '@/types/meta';
16
17
  import {
17
18
  ChatSessionList,
@@ -24,6 +25,7 @@ import {
24
25
  import { merge } from '@/utils/merge';
25
26
  import { setNamespace } from '@/utils/storeDebug';
26
27
 
28
+ import { sessionGroupSelectors } from '../sessionGroup/selectors';
27
29
  import { SessionDispatch, sessionsReducer } from './reducers';
28
30
  import { sessionSelectors } from './selectors';
29
31
  import { sessionMetaSelectors } from './selectors/meta';
@@ -114,6 +116,30 @@ export const createSessionSlice: StateCreator<
114
116
  const id = await sessionService.createSession(LobeSessionType.Agent, newSession);
115
117
  await refreshSessions();
116
118
 
119
+ // Track new agent creation analytics
120
+ const analytics = getSingletonAnalyticsOptional();
121
+ if (analytics) {
122
+ const userStore = getUserStoreState();
123
+ const userId = userProfileSelectors.userId(userStore);
124
+
125
+ // Get group information
126
+ const groupId = newSession.group || 'default';
127
+ const group = sessionGroupSelectors.getGroupById(groupId)(get());
128
+ const groupName = group?.name || (groupId === 'default' ? 'Default' : 'Unknown');
129
+
130
+ analytics.track({
131
+ name: 'new_agent_created',
132
+ properties: {
133
+ assistant_name: newSession.meta?.title || 'Untitled Agent',
134
+ assistant_tags: newSession.meta?.tags || [],
135
+ group_id: groupId,
136
+ group_name: groupName,
137
+ session_id: id,
138
+ user_id: userId || 'anonymous',
139
+ },
140
+ });
141
+ }
142
+
117
143
  // Whether to goto to the new session after creation, the default is to switch to
118
144
  if (isSwitchSession) switchSession(id);
119
145
 
@@ -1,3 +1,4 @@
1
+ import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
1
2
  import useSWR, { SWRResponse, mutate } from 'swr';
2
3
  import { DeepPartial } from 'utility-types';
3
4
  import type { StateCreator } from 'zustand/vanilla';
@@ -22,7 +23,6 @@ const GET_USER_STATE_KEY = 'initUserState';
22
23
  */
23
24
  export interface CommonAction {
24
25
  refreshUserState: () => Promise<void>;
25
-
26
26
  updateAvatar: (avatar: string) => Promise<void>;
27
27
  useCheckTrace: (shouldFetch: boolean) => SWRResponse;
28
28
  useInitUserState: (
@@ -118,7 +118,14 @@ export const createCommonSlice: StateCreator<
118
118
  false,
119
119
  n('initUserState'),
120
120
  );
121
-
121
+ //analytics
122
+ const analytics = getSingletonAnalyticsOptional();
123
+ analytics?.identify(data.userId || '', {
124
+ email: data.email,
125
+ firstName: data.firstName,
126
+ lastName: data.lastName,
127
+ username: data.username,
128
+ });
122
129
  get().refreshDefaultModelProviderList({ trigger: 'fetchUserState' });
123
130
  }
124
131
  },
@@ -1,7 +1,8 @@
1
1
  import { resolveAcceptLanguage } from 'resolve-accept-language';
2
2
 
3
3
  import { DEFAULT_LANG } from '@/const/locale';
4
- import { locales, normalizeLocale } from '@/locales/resources';
4
+ import { Locales, locales, normalizeLocale } from '@/locales/resources';
5
+ import { RouteVariants } from '@/utils/server/routeVariants';
5
6
 
6
7
  export const getAntdLocale = async (lang?: string) => {
7
8
  let normalLang: any = normalizeLocale(lang);
@@ -44,3 +45,17 @@ export const parseBrowserLanguage = (headers: Headers, defaultLang: string = DEF
44
45
 
45
46
  return browserLang;
46
47
  };
48
+
49
+ /**
50
+ * Parse the page locale from the URL and search
51
+ * used in cloud
52
+ */
53
+ export const parsePageLocale = async (props: {
54
+ params: Promise<{ variants: string }>;
55
+ searchParams: Promise<any>;
56
+ }) => {
57
+ const searchParams = await props.searchParams;
58
+
59
+ const browserLocale = await RouteVariants.getLocale(props);
60
+ return normalizeLocale(searchParams?.hl || browserLocale) as Locales;
61
+ };
package/tests/setup.ts CHANGED
@@ -5,6 +5,17 @@ import { theme } from 'antd';
5
5
  // refs: https://github.com/dumbmatter/fakeIndexedDB#dexie-and-other-indexeddb-api-wrappers
6
6
  import 'fake-indexeddb/auto';
7
7
  import React from 'react';
8
+ import { vi } from 'vitest';
9
+
10
+ // Global mock for @lobehub/analytics/react to avoid AnalyticsProvider dependency
11
+ // This prevents tests from failing when components use useAnalytics hook
12
+ vi.mock('@lobehub/analytics/react', () => ({
13
+ useAnalytics: () => ({
14
+ analytics: {
15
+ track: vi.fn(),
16
+ },
17
+ }),
18
+ }));
8
19
 
9
20
  // only inject in the dom environment
10
21
  if (