@lobehub/chat 1.42.6 → 1.43.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/CHANGELOG.md +58 -0
- package/changelog/v1.json +21 -0
- package/docs/.cdn.cache.json +1 -0
- package/docs/changelog/2025-01-03-user-profile.mdx +27 -0
- package/docs/changelog/2025-01-03-user-profile.zh-CN.mdx +26 -0
- package/docs/self-hosting/advanced/auth/next-auth/wechat.mdx +3 -1
- package/docs/self-hosting/advanced/auth/next-auth/wechat.zh-CN.mdx +2 -2
- package/locales/ar/auth.json +83 -4
- package/locales/bg-BG/auth.json +82 -3
- package/locales/de-DE/auth.json +85 -6
- package/locales/en-US/auth.json +85 -6
- package/locales/es-ES/auth.json +82 -3
- package/locales/fa-IR/auth.json +84 -5
- package/locales/fr-FR/auth.json +85 -6
- package/locales/it-IT/auth.json +83 -4
- package/locales/ja-JP/auth.json +83 -4
- package/locales/ko-KR/auth.json +82 -3
- package/locales/nl-NL/auth.json +83 -4
- package/locales/pl-PL/auth.json +83 -4
- package/locales/pt-BR/auth.json +83 -4
- package/locales/ru-RU/auth.json +82 -3
- package/locales/tr-TR/auth.json +82 -3
- package/locales/vi-VN/auth.json +82 -3
- package/locales/zh-CN/auth.json +82 -3
- package/locales/zh-TW/auth.json +82 -3
- package/package.json +4 -3
- package/src/app/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +4 -0
- package/src/app/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +0 -46
- package/src/app/(main)/(mobile)/me/(home)/features/UserBanner.tsx +11 -14
- package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +6 -21
- package/src/app/(main)/(mobile)/me/profile/features/Category.tsx +38 -21
- package/src/app/(main)/(mobile)/me/profile/layout.tsx +0 -3
- package/src/app/(main)/(mobile)/me/profile/page.tsx +3 -3
- package/src/app/(main)/chat/loading.tsx +2 -2
- package/src/app/(main)/discover/loading.tsx +2 -8
- package/src/app/(main)/files/loading.tsx +2 -2
- package/src/app/(main)/profile/(home)/Client.tsx +53 -0
- package/src/app/(main)/profile/(home)/[[...slugs]]/page.tsx +38 -0
- package/src/app/(main)/profile/@category/default.tsx +9 -0
- package/src/app/(main)/profile/@category/features/CategoryContent.tsx +38 -0
- package/src/app/(main)/profile/_layout/Desktop/Header.tsx +85 -0
- package/src/app/(main)/profile/_layout/Desktop/SideBar.tsx +42 -0
- package/src/app/(main)/profile/_layout/Desktop/index.tsx +48 -0
- package/src/app/(main)/profile/_layout/Mobile/Header.tsx +23 -5
- package/src/app/(main)/profile/_layout/Mobile/index.tsx +12 -5
- package/src/app/(main)/profile/_layout/type.ts +6 -0
- package/src/app/(main)/profile/error.tsx +5 -0
- package/src/app/(main)/profile/features/ClerkProfile.tsx +72 -0
- package/src/app/(main)/profile/hooks/useCategory.tsx +51 -0
- package/src/app/(main)/profile/layout.tsx +7 -17
- package/src/app/(main)/profile/loading.tsx +2 -22
- package/src/app/(main)/profile/not-found.tsx +3 -0
- package/src/app/(main)/profile/security/page.tsx +34 -0
- package/src/app/(main)/profile/stats/Client.tsx +52 -0
- package/src/app/(main)/profile/stats/features/AiHeatmaps.tsx +130 -0
- package/src/app/(main)/profile/stats/features/AssistantsRank.tsx +115 -0
- package/src/app/(main)/profile/stats/features/ModelsRank.tsx +84 -0
- package/src/app/(main)/profile/stats/features/ShareButton/Preview.tsx +159 -0
- package/src/app/(main)/profile/stats/features/ShareButton/ShareModal.tsx +87 -0
- package/src/app/(main)/profile/stats/features/ShareButton/TotalCard.tsx +39 -0
- package/src/app/(main)/profile/stats/features/ShareButton/index.tsx +26 -0
- package/src/app/(main)/profile/stats/features/TimeLabel.tsx +30 -0
- package/src/app/(main)/profile/stats/features/TopicsRank.tsx +104 -0
- package/src/app/(main)/profile/stats/features/TotalAssistants.tsx +56 -0
- package/src/app/(main)/profile/stats/features/TotalMessages.tsx +56 -0
- package/src/app/(main)/profile/stats/features/TotalTopics.tsx +53 -0
- package/src/app/(main)/profile/stats/features/TotalWords.tsx +54 -0
- package/src/app/(main)/profile/stats/features/Welcome.tsx +86 -0
- package/src/app/(main)/profile/{[[...slugs]] → stats}/page.tsx +4 -5
- package/src/app/(main)/repos/[id]/evals/dataset/page.tsx +2 -2
- package/src/app/(main)/repos/[id]/evals/evaluation/page.tsx +2 -2
- package/src/app/(main)/settings/@category/features/CategoryContent.tsx +1 -1
- package/src/app/(main)/settings/_layout/Desktop/index.tsx +1 -1
- package/src/app/(main)/settings/_layout/Mobile/Header.tsx +1 -1
- package/src/app/(main)/settings/_layout/Mobile/index.tsx +2 -0
- package/src/app/(main)/settings/common/features/Theme/index.tsx +2 -17
- package/src/app/(main)/settings/loading.tsx +2 -2
- package/src/components/Loading/BrandTextLoading/index.tsx +2 -2
- package/src/components/Statistic/index.tsx +15 -0
- package/src/components/StatisticCard/TitleWithPercentage.tsx +80 -0
- package/src/components/StatisticCard/growthPercentage.tsx +8 -0
- package/src/components/StatisticCard/index.tsx +209 -0
- package/src/const/url.ts +3 -3
- package/src/database/server/models/__tests__/message.test.ts +346 -35
- package/src/database/server/models/__tests__/session.test.ts +185 -2
- package/src/database/server/models/__tests__/topic.test.ts +136 -0
- package/src/database/server/models/__tests__/user.test.ts +140 -1
- package/src/database/server/models/message.ts +109 -14
- package/src/database/server/models/session.ts +76 -4
- package/src/database/server/models/topic.ts +44 -3
- package/src/database/server/models/user.ts +22 -0
- package/src/database/utils/genWhere.ts +39 -0
- package/src/features/ShareModal/ShareImage/index.tsx +11 -24
- package/src/features/ShareModal/ShareImage/type.ts +1 -6
- package/src/features/User/DataStatistics.tsx +21 -14
- package/src/features/User/UserPanel/PanelContent.tsx +12 -16
- package/src/features/User/UserPanel/useMenu.tsx +4 -6
- package/src/features/User/__tests__/PanelContent.test.tsx +4 -0
- package/src/features/User/__tests__/useMenu.test.tsx +1 -21
- package/src/hooks/useActiveTabKey.ts +34 -1
- package/src/{features/ShareModal/ShareImage → hooks}/useScreenshot.ts +51 -6
- package/src/locales/default/auth.ts +81 -2
- package/src/server/ld.test.ts +1 -1
- package/src/server/modules/AssistantStore/index.ts +3 -2
- package/src/server/routers/lambda/message.ts +35 -6
- package/src/server/routers/lambda/session.ts +17 -3
- package/src/server/routers/lambda/topic.ts +17 -3
- package/src/server/routers/lambda/user.ts +4 -0
- package/src/server/services/changelog/index.ts +1 -1
- package/src/services/message/_deprecated.ts +16 -0
- package/src/services/message/client.test.ts +0 -18
- package/src/services/message/client.ts +12 -9
- package/src/services/message/server.ts +12 -4
- package/src/services/message/type.ts +15 -3
- package/src/services/session/_deprecated.ts +5 -0
- package/src/services/session/client.ts +6 -2
- package/src/services/session/server.ts +6 -2
- package/src/services/session/type.ts +7 -1
- package/src/services/topic/_deprecated.ts +5 -0
- package/src/services/topic/client.ts +6 -2
- package/src/services/topic/server.ts +7 -1
- package/src/services/topic/type.ts +7 -2
- package/src/services/user/_deprecated.ts +4 -0
- package/src/services/user/client.ts +4 -0
- package/src/services/user/server.ts +4 -0
- package/src/services/user/type.ts +5 -0
- package/src/store/global/initialState.ts +6 -0
- package/src/store/user/slices/auth/action.test.ts +1 -33
- package/src/store/user/slices/auth/action.ts +0 -9
- package/src/store/user/slices/common/action.test.ts +2 -2
- package/src/types/message/index.ts +5 -0
- package/src/types/session/index.ts +8 -0
- package/src/types/topic/topic.ts +7 -0
- package/src/utils/format.ts +1 -1
- package/src/utils/time.ts +23 -0
- package/src/app/(main)/profile/[[...slugs]]/Client.tsx +0 -76
- package/src/components/Loading/BrandTextLoading/LobeChatText/SVG.tsx +0 -44
- package/src/components/Loading/BrandTextLoading/LobeChatText/index.tsx +0 -6
- package/src/components/Loading/BrandTextLoading/LobeChatText/style.css +0 -32
- package/src/hooks/useActiveSettingsKey.ts +0 -20
@@ -620,4 +620,140 @@ describe('TopicModel', () => {
|
|
620
620
|
);
|
621
621
|
});
|
622
622
|
});
|
623
|
+
|
624
|
+
describe('rank', () => {
|
625
|
+
it('should return ranked topics based on message count', async () => {
|
626
|
+
// 创建测试数据
|
627
|
+
await serverDB.transaction(async (tx) => {
|
628
|
+
await tx.insert(topics).values([
|
629
|
+
{ id: 'topic1', title: 'Topic 1', sessionId, userId },
|
630
|
+
{ id: 'topic2', title: 'Topic 2', sessionId, userId },
|
631
|
+
{ id: 'topic3', title: 'Topic 3', sessionId, userId },
|
632
|
+
]);
|
633
|
+
|
634
|
+
// topic1 有 3 条消息
|
635
|
+
await tx.insert(messages).values([
|
636
|
+
{ id: 'msg1', role: 'user', topicId: 'topic1', userId },
|
637
|
+
{ id: 'msg2', role: 'assistant', topicId: 'topic1', userId },
|
638
|
+
{ id: 'msg3', role: 'user', topicId: 'topic1', userId },
|
639
|
+
]);
|
640
|
+
|
641
|
+
// topic2 有 2 条消息
|
642
|
+
await tx.insert(messages).values([
|
643
|
+
{ id: 'msg4', role: 'user', topicId: 'topic2', userId },
|
644
|
+
{ id: 'msg5', role: 'assistant', topicId: 'topic2', userId },
|
645
|
+
]);
|
646
|
+
|
647
|
+
// topic3 有 1 条消息
|
648
|
+
await tx.insert(messages).values([{ id: 'msg6', role: 'user', topicId: 'topic3', userId }]);
|
649
|
+
});
|
650
|
+
|
651
|
+
// 调用 rank 方法
|
652
|
+
const result = await topicModel.rank(2);
|
653
|
+
|
654
|
+
// 断言返回结果符合预期
|
655
|
+
expect(result).toHaveLength(2);
|
656
|
+
expect(result[0]).toMatchObject({
|
657
|
+
id: 'topic1',
|
658
|
+
title: 'Topic 1',
|
659
|
+
count: 3,
|
660
|
+
sessionId,
|
661
|
+
});
|
662
|
+
expect(result[1]).toMatchObject({
|
663
|
+
id: 'topic2',
|
664
|
+
title: 'Topic 2',
|
665
|
+
count: 2,
|
666
|
+
sessionId,
|
667
|
+
});
|
668
|
+
});
|
669
|
+
|
670
|
+
it('should return empty array if no topics exist', async () => {
|
671
|
+
const result = await topicModel.rank();
|
672
|
+
expect(result).toHaveLength(0);
|
673
|
+
});
|
674
|
+
|
675
|
+
it('should respect the limit parameter', async () => {
|
676
|
+
// 创建测试数据
|
677
|
+
await serverDB.transaction(async (tx) => {
|
678
|
+
await tx.insert(topics).values([
|
679
|
+
{ id: 'topic1', title: 'Topic 1', sessionId, userId },
|
680
|
+
{ id: 'topic2', title: 'Topic 2', sessionId, userId },
|
681
|
+
]);
|
682
|
+
|
683
|
+
await tx.insert(messages).values([
|
684
|
+
{ id: 'msg1', role: 'user', topicId: 'topic1', userId },
|
685
|
+
{ id: 'msg2', role: 'user', topicId: 'topic2', userId },
|
686
|
+
]);
|
687
|
+
});
|
688
|
+
|
689
|
+
// 使用限制为 1 调用 rank 方法
|
690
|
+
const result = await topicModel.rank(1);
|
691
|
+
|
692
|
+
// 断言只返回一个结果
|
693
|
+
expect(result).toHaveLength(1);
|
694
|
+
});
|
695
|
+
});
|
696
|
+
|
697
|
+
describe('count with date filters', () => {
|
698
|
+
beforeEach(async () => {
|
699
|
+
// 创建测试数据
|
700
|
+
await serverDB.insert(topics).values([
|
701
|
+
{
|
702
|
+
id: 'topic1',
|
703
|
+
userId,
|
704
|
+
createdAt: new Date('2023-01-01'),
|
705
|
+
},
|
706
|
+
{
|
707
|
+
id: 'topic2',
|
708
|
+
userId,
|
709
|
+
createdAt: new Date('2023-02-01'),
|
710
|
+
},
|
711
|
+
{
|
712
|
+
id: 'topic3',
|
713
|
+
userId,
|
714
|
+
createdAt: new Date('2023-03-01'),
|
715
|
+
},
|
716
|
+
]);
|
717
|
+
});
|
718
|
+
|
719
|
+
it('should count topics with start date filter', async () => {
|
720
|
+
const result = await topicModel.count({
|
721
|
+
startDate: '2023-02-01',
|
722
|
+
});
|
723
|
+
|
724
|
+
expect(result).toBe(2); // should count topics from Feb 1st onwards
|
725
|
+
});
|
726
|
+
|
727
|
+
it('should count topics with end date filter', async () => {
|
728
|
+
const result = await topicModel.count({
|
729
|
+
endDate: '2023-02-01',
|
730
|
+
});
|
731
|
+
|
732
|
+
expect(result).toBe(2); // should count topics up to Feb 1st
|
733
|
+
});
|
734
|
+
|
735
|
+
it('should count topics within date range', async () => {
|
736
|
+
const result = await topicModel.count({
|
737
|
+
range: ['2023-01-15', '2023-02-15'],
|
738
|
+
});
|
739
|
+
|
740
|
+
expect(result).toBe(1); // should only count topic2
|
741
|
+
});
|
742
|
+
|
743
|
+
it('should return 0 if no topics match date filters', async () => {
|
744
|
+
const result = await topicModel.count({
|
745
|
+
range: ['2024-01-01', '2024-12-31'],
|
746
|
+
});
|
747
|
+
|
748
|
+
expect(result).toBe(0);
|
749
|
+
});
|
750
|
+
|
751
|
+
it('should handle invalid date filters gracefully', async () => {
|
752
|
+
const result = await topicModel.count({
|
753
|
+
startDate: 'invalid-date',
|
754
|
+
});
|
755
|
+
|
756
|
+
expect(result).toBe(3); // should return all topics if date is invalid
|
757
|
+
});
|
758
|
+
});
|
623
759
|
});
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import dayjs from 'dayjs';
|
1
2
|
import { eq } from 'drizzle-orm/expressions';
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
3
4
|
|
@@ -5,7 +6,6 @@ import { INBOX_SESSION_ID } from '@/const/session';
|
|
5
6
|
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
6
7
|
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
7
8
|
import { UserGuide, UserPreference } from '@/types/user';
|
8
|
-
import { UserSettings } from '@/types/user/settings';
|
9
9
|
|
10
10
|
import { UserSettingsItem, userSettings, users } from '../../../schemas';
|
11
11
|
import { SessionModel } from '../session';
|
@@ -268,4 +268,143 @@ describe('UserModel', () => {
|
|
268
268
|
expect(result).toEqual({});
|
269
269
|
});
|
270
270
|
});
|
271
|
+
|
272
|
+
describe('getUserRegistrationDuration', () => {
|
273
|
+
it('should return default values when user not found', async () => {
|
274
|
+
const duration = await userModel.getUserRegistrationDuration();
|
275
|
+
const today = dayjs().format('YYYY-MM-DD');
|
276
|
+
|
277
|
+
expect(duration).toEqual({
|
278
|
+
createdAt: today,
|
279
|
+
duration: 1,
|
280
|
+
updatedAt: today,
|
281
|
+
});
|
282
|
+
});
|
283
|
+
|
284
|
+
it('should calculate correct duration for existing user', async () => {
|
285
|
+
// Mock the current date
|
286
|
+
const now = new Date('2024-01-15');
|
287
|
+
vi.setSystemTime(now);
|
288
|
+
|
289
|
+
const createdAt = new Date('2024-01-10'); // 5 days ago
|
290
|
+
await serverDB.insert(users).values({
|
291
|
+
id: userId,
|
292
|
+
createdAt,
|
293
|
+
});
|
294
|
+
|
295
|
+
const duration = await userModel.getUserRegistrationDuration();
|
296
|
+
|
297
|
+
expect(duration).toEqual({
|
298
|
+
createdAt: '2024-01-10',
|
299
|
+
duration: 6, // 5 days difference + 1
|
300
|
+
updatedAt: '2024-01-15',
|
301
|
+
});
|
302
|
+
|
303
|
+
vi.useRealTimers();
|
304
|
+
});
|
305
|
+
});
|
306
|
+
|
307
|
+
// 补充一些边界情况的测试
|
308
|
+
describe('edge cases', () => {
|
309
|
+
describe('updatePreference', () => {
|
310
|
+
it('should handle undefined preference', async () => {
|
311
|
+
await serverDB.insert(users).values({ id: userId });
|
312
|
+
|
313
|
+
const newPreference: Partial<UserPreference> = {
|
314
|
+
guide: { topic: true },
|
315
|
+
};
|
316
|
+
|
317
|
+
await userModel.updatePreference(newPreference);
|
318
|
+
|
319
|
+
const updatedUser = await serverDB.query.users.findFirst({
|
320
|
+
where: eq(users.id, userId),
|
321
|
+
});
|
322
|
+
|
323
|
+
expect(updatedUser?.preference).toMatchObject(newPreference);
|
324
|
+
});
|
325
|
+
|
326
|
+
it('should do nothing if user not found', async () => {
|
327
|
+
const nonExistentUserModel = new UserModel(serverDB, 'non-existent-id');
|
328
|
+
const result = await nonExistentUserModel.updatePreference({ guide: { topic: true } });
|
329
|
+
expect(result).toBeUndefined();
|
330
|
+
});
|
331
|
+
});
|
332
|
+
|
333
|
+
describe('updateGuide', () => {
|
334
|
+
it('should handle undefined guide', async () => {
|
335
|
+
await serverDB.insert(users).values({
|
336
|
+
id: userId,
|
337
|
+
preference: {} as UserPreference,
|
338
|
+
});
|
339
|
+
|
340
|
+
const newGuide: Partial<UserGuide> = {
|
341
|
+
topic: true,
|
342
|
+
};
|
343
|
+
|
344
|
+
await userModel.updateGuide(newGuide);
|
345
|
+
|
346
|
+
const updatedUser = await serverDB.query.users.findFirst({
|
347
|
+
where: eq(users.id, userId),
|
348
|
+
});
|
349
|
+
expect(updatedUser?.preference).toEqual({ guide: newGuide });
|
350
|
+
});
|
351
|
+
|
352
|
+
it('should do nothing if user not found', async () => {
|
353
|
+
const nonExistentUserModel = new UserModel(serverDB, 'non-existent-id');
|
354
|
+
const result = await nonExistentUserModel.updateGuide({ topic: true });
|
355
|
+
expect(result).toBeUndefined();
|
356
|
+
});
|
357
|
+
});
|
358
|
+
|
359
|
+
describe('createUser', () => {
|
360
|
+
it('should not create duplicate user with same id', async () => {
|
361
|
+
const params = {
|
362
|
+
id: userId,
|
363
|
+
username: 'existinguser',
|
364
|
+
email: 'existing@example.com',
|
365
|
+
};
|
366
|
+
|
367
|
+
// First creation
|
368
|
+
await UserModel.createUser(serverDB, params);
|
369
|
+
|
370
|
+
// Attempt to create with same ID but different details
|
371
|
+
await UserModel.createUser(serverDB, {
|
372
|
+
...params,
|
373
|
+
username: 'newuser',
|
374
|
+
email: 'new@example.com',
|
375
|
+
});
|
376
|
+
|
377
|
+
const user = await UserModel.findById(serverDB, userId);
|
378
|
+
expect(user?.username).toBe('existinguser');
|
379
|
+
expect(user?.email).toBe('existing@example.com');
|
380
|
+
});
|
381
|
+
});
|
382
|
+
|
383
|
+
describe('getUserState', () => {
|
384
|
+
it('should handle empty settings', async () => {
|
385
|
+
await serverDB.insert(users).values({
|
386
|
+
id: userId,
|
387
|
+
preference: {} as UserPreference,
|
388
|
+
isOnboarded: true,
|
389
|
+
});
|
390
|
+
|
391
|
+
const state = await userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults);
|
392
|
+
|
393
|
+
expect(state).toMatchObject({
|
394
|
+
userId,
|
395
|
+
isOnboarded: true,
|
396
|
+
preference: {},
|
397
|
+
settings: {
|
398
|
+
defaultAgent: {},
|
399
|
+
general: {},
|
400
|
+
keyVaults: {},
|
401
|
+
languageModel: {},
|
402
|
+
systemAgent: {},
|
403
|
+
tool: {},
|
404
|
+
tts: {},
|
405
|
+
},
|
406
|
+
});
|
407
|
+
});
|
408
|
+
});
|
409
|
+
});
|
271
410
|
});
|
@@ -1,7 +1,15 @@
|
|
1
|
-
import {
|
2
|
-
import
|
1
|
+
import type { HeatmapsProps } from '@lobehub/charts';
|
2
|
+
import dayjs from 'dayjs';
|
3
|
+
import { count, sql } from 'drizzle-orm';
|
4
|
+
import { and, asc, desc, eq, gt, inArray, isNotNull, isNull, like } from 'drizzle-orm/expressions';
|
3
5
|
|
4
6
|
import { LobeChatDatabase } from '@/database/type';
|
7
|
+
import {
|
8
|
+
genEndDateWhere,
|
9
|
+
genRangeWhere,
|
10
|
+
genStartDateWhere,
|
11
|
+
genWhere,
|
12
|
+
} from '@/database/utils/genWhere';
|
5
13
|
import { idGenerator } from '@/database/utils/idGenerator';
|
6
14
|
import {
|
7
15
|
ChatFileItem,
|
@@ -9,8 +17,10 @@ import {
|
|
9
17
|
ChatTTS,
|
10
18
|
ChatToolPayload,
|
11
19
|
CreateMessageParams,
|
20
|
+
ModelRankItem,
|
12
21
|
} from '@/types/message';
|
13
22
|
import { merge } from '@/utils/merge';
|
23
|
+
import { today } from '@/utils/time';
|
14
24
|
|
15
25
|
import {
|
16
26
|
MessageItem,
|
@@ -265,37 +275,122 @@ export class MessageModel {
|
|
265
275
|
});
|
266
276
|
};
|
267
277
|
|
268
|
-
count = async (
|
278
|
+
count = async (params?: {
|
279
|
+
endDate?: string;
|
280
|
+
range?: [string, string];
|
281
|
+
startDate?: string;
|
282
|
+
}): Promise<number> => {
|
269
283
|
const result = await this.db
|
270
284
|
.select({
|
271
285
|
count: count(messages.id),
|
272
286
|
})
|
273
287
|
.from(messages)
|
274
|
-
.where(
|
288
|
+
.where(
|
289
|
+
genWhere([
|
290
|
+
eq(messages.userId, this.userId),
|
291
|
+
params?.range
|
292
|
+
? genRangeWhere(params.range, messages.createdAt, (date) => date.toDate())
|
293
|
+
: undefined,
|
294
|
+
params?.endDate
|
295
|
+
? genEndDateWhere(params.endDate, messages.createdAt, (date) => date.toDate())
|
296
|
+
: undefined,
|
297
|
+
params?.startDate
|
298
|
+
? genStartDateWhere(params.startDate, messages.createdAt, (date) => date.toDate())
|
299
|
+
: undefined,
|
300
|
+
]),
|
301
|
+
);
|
275
302
|
|
276
303
|
return result[0].count;
|
277
304
|
};
|
278
305
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
306
|
+
countWords = async (params?: {
|
307
|
+
endDate?: string;
|
308
|
+
range?: [string, string];
|
309
|
+
startDate?: string;
|
310
|
+
}): Promise<number> => {
|
311
|
+
const result = await this.db
|
312
|
+
.select({
|
313
|
+
count: sql<string>`sum(length(${messages.content}))`.as('total_length'),
|
314
|
+
})
|
315
|
+
.from(messages)
|
316
|
+
.where(
|
317
|
+
genWhere([
|
318
|
+
eq(messages.userId, this.userId),
|
319
|
+
params?.range
|
320
|
+
? genRangeWhere(params.range, messages.createdAt, (date) => date.toDate())
|
321
|
+
: undefined,
|
322
|
+
params?.endDate
|
323
|
+
? genEndDateWhere(params.endDate, messages.createdAt, (date) => date.toDate())
|
324
|
+
: undefined,
|
325
|
+
params?.startDate
|
326
|
+
? genStartDateWhere(params.startDate, messages.createdAt, (date) => date.toDate())
|
327
|
+
: undefined,
|
328
|
+
]),
|
329
|
+
);
|
330
|
+
|
331
|
+
return Number(result[0].count);
|
332
|
+
};
|
333
|
+
|
334
|
+
rankModels = async (limit: number = 10): Promise<ModelRankItem[]> => {
|
335
|
+
return this.db
|
336
|
+
.select({
|
337
|
+
count: count(messages.id).as('count'),
|
338
|
+
id: messages.model,
|
339
|
+
})
|
340
|
+
.from(messages)
|
341
|
+
.where(and(eq(messages.userId, this.userId), isNotNull(messages.model)))
|
342
|
+
.having(({ count }) => gt(count, 0))
|
343
|
+
.groupBy(messages.model)
|
344
|
+
.orderBy(desc(sql`count`), asc(messages.model))
|
345
|
+
.limit(limit);
|
346
|
+
};
|
347
|
+
|
348
|
+
getHeatmaps = async (): Promise<HeatmapsProps['data']> => {
|
349
|
+
const startDate = today().subtract(1, 'year').startOf('day');
|
350
|
+
const endDate = today().endOf('day');
|
284
351
|
|
285
352
|
const result = await this.db
|
286
353
|
.select({
|
287
354
|
count: count(messages.id),
|
355
|
+
date: sql`DATE(${messages.createdAt})`.as('heatmaps_date'),
|
288
356
|
})
|
289
357
|
.from(messages)
|
290
358
|
.where(
|
291
|
-
|
359
|
+
genWhere([
|
292
360
|
eq(messages.userId, this.userId),
|
293
|
-
|
294
|
-
|
295
|
-
|
361
|
+
genRangeWhere(
|
362
|
+
[startDate.format('YYYY-MM-DD'), endDate.add(1, 'day').format('YYYY-MM-DD')],
|
363
|
+
messages.createdAt,
|
364
|
+
(date) => date.toDate(),
|
365
|
+
),
|
366
|
+
]),
|
367
|
+
)
|
368
|
+
.groupBy(sql`heatmaps_date`)
|
369
|
+
.orderBy(desc(sql`heatmaps_date`));
|
370
|
+
|
371
|
+
const heatmapData: HeatmapsProps['data'] = [];
|
372
|
+
let currentDate = startDate;
|
373
|
+
|
374
|
+
while (currentDate.isBefore(endDate) || currentDate.isSame(endDate, 'day')) {
|
375
|
+
const formattedDate = currentDate.format('YYYY-MM-DD');
|
376
|
+
const matchingResult = result.find(
|
377
|
+
(r) => r?.date && dayjs(r.date as string).format('YYYY-MM-DD') === formattedDate,
|
296
378
|
);
|
297
379
|
|
298
|
-
|
380
|
+
const count = matchingResult ? matchingResult.count : 0;
|
381
|
+
const levelCount = count > 0 ? Math.floor(count / 5) : 0;
|
382
|
+
const level = levelCount > 4 ? 4 : levelCount;
|
383
|
+
|
384
|
+
heatmapData.push({
|
385
|
+
count,
|
386
|
+
date: formattedDate,
|
387
|
+
level,
|
388
|
+
});
|
389
|
+
|
390
|
+
currentDate = currentDate.add(1, 'day');
|
391
|
+
}
|
392
|
+
|
393
|
+
return heatmapData;
|
299
394
|
};
|
300
395
|
|
301
396
|
hasMoreThanN = async (n: number): Promise<boolean> => {
|
@@ -1,13 +1,20 @@
|
|
1
1
|
import { Column, count, sql } from 'drizzle-orm';
|
2
|
-
import { and, asc, desc, eq, inArray, like, not, or } from 'drizzle-orm/expressions';
|
2
|
+
import { and, asc, desc, eq, gt, inArray, isNull, like, not, or } from 'drizzle-orm/expressions';
|
3
3
|
|
4
4
|
import { appEnv } from '@/config/app';
|
5
|
+
import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
|
5
6
|
import { INBOX_SESSION_ID } from '@/const/session';
|
6
7
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
7
8
|
import { LobeChatDatabase } from '@/database/type';
|
9
|
+
import {
|
10
|
+
genEndDateWhere,
|
11
|
+
genRangeWhere,
|
12
|
+
genStartDateWhere,
|
13
|
+
genWhere,
|
14
|
+
} from '@/database/utils/genWhere';
|
8
15
|
import { idGenerator } from '@/database/utils/idGenerator';
|
9
16
|
import { parseAgentConfig } from '@/server/globalConfig/parseDefaultAgent';
|
10
|
-
import { ChatSessionList, LobeAgentSession } from '@/types/session';
|
17
|
+
import { ChatSessionList, LobeAgentSession, SessionRankItem } from '@/types/session';
|
11
18
|
import { merge } from '@/utils/merge';
|
12
19
|
|
13
20
|
import {
|
@@ -19,6 +26,7 @@ import {
|
|
19
26
|
agentsToSessions,
|
20
27
|
sessionGroups,
|
21
28
|
sessions,
|
29
|
+
topics,
|
22
30
|
} from '../../schemas';
|
23
31
|
|
24
32
|
export class SessionModel {
|
@@ -84,17 +92,81 @@ export class SessionModel {
|
|
84
92
|
return { ...result, agent: (result?.agentsToSessions?.[0] as any)?.agent } as any;
|
85
93
|
};
|
86
94
|
|
87
|
-
count = async (
|
95
|
+
count = async (params?: {
|
96
|
+
endDate?: string;
|
97
|
+
range?: [string, string];
|
98
|
+
startDate?: string;
|
99
|
+
}): Promise<number> => {
|
88
100
|
const result = await this.db
|
89
101
|
.select({
|
90
102
|
count: count(sessions.id),
|
91
103
|
})
|
92
104
|
.from(sessions)
|
93
|
-
.where(
|
105
|
+
.where(
|
106
|
+
genWhere([
|
107
|
+
eq(sessions.userId, this.userId),
|
108
|
+
params?.range
|
109
|
+
? genRangeWhere(params.range, sessions.createdAt, (date) => date.toDate())
|
110
|
+
: undefined,
|
111
|
+
params?.endDate
|
112
|
+
? genEndDateWhere(params.endDate, sessions.createdAt, (date) => date.toDate())
|
113
|
+
: undefined,
|
114
|
+
params?.startDate
|
115
|
+
? genStartDateWhere(params.startDate, sessions.createdAt, (date) => date.toDate())
|
116
|
+
: undefined,
|
117
|
+
]),
|
118
|
+
);
|
94
119
|
|
95
120
|
return result[0].count;
|
96
121
|
};
|
97
122
|
|
123
|
+
_rank = async (limit: number = 10): Promise<SessionRankItem[]> => {
|
124
|
+
return this.db
|
125
|
+
.select({
|
126
|
+
avatar: agents.avatar,
|
127
|
+
backgroundColor: agents.backgroundColor,
|
128
|
+
count: count(topics.id).as('count'),
|
129
|
+
id: sessions.id,
|
130
|
+
title: agents.title,
|
131
|
+
})
|
132
|
+
.from(sessions)
|
133
|
+
.where(and(eq(sessions.userId, this.userId)))
|
134
|
+
.leftJoin(topics, eq(sessions.id, topics.sessionId))
|
135
|
+
.leftJoin(agentsToSessions, eq(sessions.id, agentsToSessions.sessionId))
|
136
|
+
.leftJoin(agents, eq(agentsToSessions.agentId, agents.id))
|
137
|
+
.groupBy(sessions.id, agentsToSessions.agentId, agents.id)
|
138
|
+
.having(({ count }) => gt(count, 0))
|
139
|
+
.orderBy(desc(sql`count`))
|
140
|
+
.limit(limit);
|
141
|
+
};
|
142
|
+
|
143
|
+
// TODO: 未来将 Inbox id 入库后可以直接使用 _rank 方法
|
144
|
+
rank = async (limit: number = 10): Promise<SessionRankItem[]> => {
|
145
|
+
const inboxResult = await this.db
|
146
|
+
.select({
|
147
|
+
count: count(topics.id).as('count'),
|
148
|
+
})
|
149
|
+
.from(topics)
|
150
|
+
.where(and(eq(topics.userId, this.userId), isNull(topics.sessionId)));
|
151
|
+
|
152
|
+
const inboxCount = inboxResult[0].count;
|
153
|
+
|
154
|
+
if (!inboxCount || inboxCount === 0) return this._rank(limit);
|
155
|
+
|
156
|
+
const result = await this._rank(limit ? limit - 1 : undefined);
|
157
|
+
|
158
|
+
return [
|
159
|
+
{
|
160
|
+
avatar: DEFAULT_INBOX_AVATAR,
|
161
|
+
backgroundColor: null,
|
162
|
+
count: inboxCount,
|
163
|
+
id: INBOX_SESSION_ID,
|
164
|
+
title: 'inbox.title',
|
165
|
+
},
|
166
|
+
...result,
|
167
|
+
].sort((a, b) => b.count - a.count);
|
168
|
+
};
|
169
|
+
|
98
170
|
hasMoreThanN = async (n: number): Promise<boolean> => {
|
99
171
|
const result = await this.db
|
100
172
|
.select({ id: sessions.id })
|
@@ -1,8 +1,15 @@
|
|
1
1
|
import { Column, count, sql } from 'drizzle-orm';
|
2
|
-
import { and, desc, eq, exists, inArray, isNull, like, or } from 'drizzle-orm/expressions';
|
2
|
+
import { and, desc, eq, exists, gt, inArray, isNull, like, or } from 'drizzle-orm/expressions';
|
3
3
|
|
4
4
|
import { LobeChatDatabase } from '@/database/type';
|
5
|
+
import {
|
6
|
+
genEndDateWhere,
|
7
|
+
genRangeWhere,
|
8
|
+
genStartDateWhere,
|
9
|
+
genWhere,
|
10
|
+
} from '@/database/utils/genWhere';
|
5
11
|
import { idGenerator } from '@/database/utils/idGenerator';
|
12
|
+
import { TopicRankItem } from '@/types/topic';
|
6
13
|
|
7
14
|
import { NewMessage, TopicItem, messages, topics } from '../../schemas';
|
8
15
|
|
@@ -92,17 +99,51 @@ export class TopicModel {
|
|
92
99
|
});
|
93
100
|
};
|
94
101
|
|
95
|
-
count = async (
|
102
|
+
count = async (params?: {
|
103
|
+
endDate?: string;
|
104
|
+
range?: [string, string];
|
105
|
+
startDate?: string;
|
106
|
+
}): Promise<number> => {
|
96
107
|
const result = await this.db
|
97
108
|
.select({
|
98
109
|
count: count(topics.id),
|
99
110
|
})
|
100
111
|
.from(topics)
|
101
|
-
.where(
|
112
|
+
.where(
|
113
|
+
genWhere([
|
114
|
+
eq(topics.userId, this.userId),
|
115
|
+
params?.range
|
116
|
+
? genRangeWhere(params.range, topics.createdAt, (date) => date.toDate())
|
117
|
+
: undefined,
|
118
|
+
params?.endDate
|
119
|
+
? genEndDateWhere(params.endDate, topics.createdAt, (date) => date.toDate())
|
120
|
+
: undefined,
|
121
|
+
params?.startDate
|
122
|
+
? genStartDateWhere(params.startDate, topics.createdAt, (date) => date.toDate())
|
123
|
+
: undefined,
|
124
|
+
]),
|
125
|
+
);
|
102
126
|
|
103
127
|
return result[0].count;
|
104
128
|
};
|
105
129
|
|
130
|
+
rank = async (limit: number = 10): Promise<TopicRankItem[]> => {
|
131
|
+
return this.db
|
132
|
+
.select({
|
133
|
+
count: count(messages.id).as('count'),
|
134
|
+
id: topics.id,
|
135
|
+
sessionId: topics.sessionId,
|
136
|
+
title: topics.title,
|
137
|
+
})
|
138
|
+
.from(topics)
|
139
|
+
.where(and(eq(topics.userId, this.userId)))
|
140
|
+
.leftJoin(messages, eq(topics.id, messages.topicId))
|
141
|
+
.groupBy(topics.id)
|
142
|
+
.orderBy(desc(sql`count`))
|
143
|
+
.having(({ count }) => gt(count, 0))
|
144
|
+
.limit(limit);
|
145
|
+
};
|
146
|
+
|
106
147
|
// **************** Create *************** //
|
107
148
|
|
108
149
|
create = async (
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { TRPCError } from '@trpc/server';
|
2
|
+
import dayjs from 'dayjs';
|
2
3
|
import { eq } from 'drizzle-orm/expressions';
|
3
4
|
import { DeepPartial } from 'utility-types';
|
4
5
|
|
@@ -6,6 +7,7 @@ import { LobeChatDatabase } from '@/database/type';
|
|
6
7
|
import { UserGuide, UserPreference } from '@/types/user';
|
7
8
|
import { UserKeyVaults, UserSettings } from '@/types/user/settings';
|
8
9
|
import { merge } from '@/utils/merge';
|
10
|
+
import { today } from '@/utils/time';
|
9
11
|
|
10
12
|
import { NewUser, UserItem, UserSettingsItem, userSettings, users } from '../../schemas';
|
11
13
|
import { SessionModel } from './session';
|
@@ -30,6 +32,26 @@ export class UserModel {
|
|
30
32
|
this.db = db;
|
31
33
|
}
|
32
34
|
|
35
|
+
getUserRegistrationDuration = async (): Promise<{
|
36
|
+
createdAt: string;
|
37
|
+
duration: number;
|
38
|
+
updatedAt: string;
|
39
|
+
}> => {
|
40
|
+
const user = await this.db.query.users.findFirst({ where: eq(users.id, this.userId) });
|
41
|
+
if (!user)
|
42
|
+
return {
|
43
|
+
createdAt: today().format('YYYY-MM-DD'),
|
44
|
+
duration: 1,
|
45
|
+
updatedAt: today().format('YYYY-MM-DD'),
|
46
|
+
};
|
47
|
+
|
48
|
+
return {
|
49
|
+
createdAt: dayjs(user.createdAt).format('YYYY-MM-DD'),
|
50
|
+
duration: dayjs().diff(dayjs(user.createdAt), 'day') + 1,
|
51
|
+
updatedAt: today().format('YYYY-MM-DD'),
|
52
|
+
};
|
53
|
+
};
|
54
|
+
|
33
55
|
getUserState = async (decryptor: DecryptUserKeyVaults) => {
|
34
56
|
const result = await this.db
|
35
57
|
.select({
|