@lobehub/chat 1.42.6 → 1.43.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.
- package/CHANGELOG.md +33 -0
- package/changelog/v1.json +12 -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 +76 -4
- package/locales/bg-BG/auth.json +75 -3
- package/locales/de-DE/auth.json +78 -6
- package/locales/en-US/auth.json +78 -6
- package/locales/es-ES/auth.json +75 -3
- package/locales/fa-IR/auth.json +77 -5
- package/locales/fr-FR/auth.json +78 -6
- package/locales/it-IT/auth.json +76 -4
- package/locales/ja-JP/auth.json +76 -4
- package/locales/ko-KR/auth.json +75 -3
- package/locales/nl-NL/auth.json +76 -4
- package/locales/pl-PL/auth.json +76 -4
- package/locales/pt-BR/auth.json +76 -4
- package/locales/ru-RU/auth.json +75 -3
- package/locales/tr-TR/auth.json +74 -3
- package/locales/vi-VN/auth.json +75 -3
- package/locales/zh-CN/auth.json +75 -3
- package/locales/zh-TW/auth.json +75 -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 +103 -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 +75 -4
- package/src/database/server/models/topic.ts +43 -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 +74 -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
@@ -626,8 +626,6 @@ describe('SessionModel', () => {
|
|
626
626
|
});
|
627
627
|
});
|
628
628
|
|
629
|
-
// 在原有的 describe('SessionModel') 中添加以下测试套件
|
630
|
-
|
631
629
|
describe('createInbox', () => {
|
632
630
|
it('should create inbox session if not exists', async () => {
|
633
631
|
const inbox = await sessionModel.createInbox();
|
@@ -782,4 +780,189 @@ describe('SessionModel', () => {
|
|
782
780
|
});
|
783
781
|
});
|
784
782
|
});
|
783
|
+
|
784
|
+
describe('rank', () => {
|
785
|
+
it('should return ranked sessions based on topic count', async () => {
|
786
|
+
// Create test data
|
787
|
+
await serverDB.transaction(async (trx) => {
|
788
|
+
// Create sessions
|
789
|
+
await trx.insert(sessions).values([
|
790
|
+
{ id: '1', userId },
|
791
|
+
{ id: '2', userId },
|
792
|
+
{ id: '3', userId },
|
793
|
+
]);
|
794
|
+
|
795
|
+
// Create agents
|
796
|
+
await trx.insert(agents).values([
|
797
|
+
{ id: 'a1', userId, title: 'Agent 1', avatar: 'avatar1', backgroundColor: 'bg1' },
|
798
|
+
{ id: 'a2', userId, title: 'Agent 2', avatar: 'avatar2', backgroundColor: 'bg2' },
|
799
|
+
{ id: 'a3', userId, title: 'Agent 3', avatar: 'avatar3', backgroundColor: 'bg3' },
|
800
|
+
]);
|
801
|
+
|
802
|
+
// Link agents to sessions
|
803
|
+
await trx.insert(agentsToSessions).values([
|
804
|
+
{ sessionId: '1', agentId: 'a1' },
|
805
|
+
{ sessionId: '2', agentId: 'a2' },
|
806
|
+
{ sessionId: '3', agentId: 'a3' },
|
807
|
+
]);
|
808
|
+
|
809
|
+
// Create topics (different counts for ranking)
|
810
|
+
await trx.insert(topics).values([
|
811
|
+
{ id: 't1', sessionId: '1', userId },
|
812
|
+
{ id: 't2', sessionId: '1', userId },
|
813
|
+
{ id: 't3', sessionId: '1', userId }, // Session 1 has 3 topics
|
814
|
+
{ id: 't4', sessionId: '2', userId },
|
815
|
+
{ id: 't5', sessionId: '2', userId }, // Session 2 has 2 topics
|
816
|
+
{ id: 't6', sessionId: '3', userId }, // Session 3 has 1 topic
|
817
|
+
]);
|
818
|
+
});
|
819
|
+
|
820
|
+
// Get ranked sessions with default limit
|
821
|
+
const result = await sessionModel.rank();
|
822
|
+
|
823
|
+
// Verify results
|
824
|
+
expect(result).toHaveLength(3);
|
825
|
+
// Should be ordered by topic count (descending)
|
826
|
+
expect(result[0]).toMatchObject({
|
827
|
+
id: '1',
|
828
|
+
count: 3,
|
829
|
+
title: 'Agent 1',
|
830
|
+
avatar: 'avatar1',
|
831
|
+
backgroundColor: 'bg1',
|
832
|
+
});
|
833
|
+
expect(result[1]).toMatchObject({
|
834
|
+
id: '2',
|
835
|
+
count: 2,
|
836
|
+
title: 'Agent 2',
|
837
|
+
avatar: 'avatar2',
|
838
|
+
backgroundColor: 'bg2',
|
839
|
+
});
|
840
|
+
expect(result[2]).toMatchObject({
|
841
|
+
id: '3',
|
842
|
+
count: 1,
|
843
|
+
title: 'Agent 3',
|
844
|
+
avatar: 'avatar3',
|
845
|
+
backgroundColor: 'bg3',
|
846
|
+
});
|
847
|
+
});
|
848
|
+
|
849
|
+
it('should respect the limit parameter', async () => {
|
850
|
+
// Create test data
|
851
|
+
await serverDB.transaction(async (trx) => {
|
852
|
+
// Create sessions and related data
|
853
|
+
await trx.insert(sessions).values([
|
854
|
+
{ id: '1', userId },
|
855
|
+
{ id: '2', userId },
|
856
|
+
{ id: '3', userId },
|
857
|
+
]);
|
858
|
+
|
859
|
+
await trx.insert(agents).values([
|
860
|
+
{ id: 'a1', userId, title: 'Agent 1' },
|
861
|
+
{ id: 'a2', userId, title: 'Agent 2' },
|
862
|
+
{ id: 'a3', userId, title: 'Agent 3' },
|
863
|
+
]);
|
864
|
+
|
865
|
+
await trx.insert(agentsToSessions).values([
|
866
|
+
{ sessionId: '1', agentId: 'a1' },
|
867
|
+
{ sessionId: '2', agentId: 'a2' },
|
868
|
+
{ sessionId: '3', agentId: 'a3' },
|
869
|
+
]);
|
870
|
+
|
871
|
+
await trx.insert(topics).values([
|
872
|
+
{ id: 't1', sessionId: '1', userId },
|
873
|
+
{ id: 't2', sessionId: '1', userId },
|
874
|
+
{ id: 't3', sessionId: '2', userId },
|
875
|
+
{ id: 't4', sessionId: '3', userId },
|
876
|
+
]);
|
877
|
+
});
|
878
|
+
|
879
|
+
// Get ranked sessions with limit of 2
|
880
|
+
const result = await sessionModel.rank(2);
|
881
|
+
|
882
|
+
// Verify results
|
883
|
+
expect(result).toHaveLength(2);
|
884
|
+
expect(result[0].id).toBe('1'); // Most topics (2)
|
885
|
+
expect(result[1].id).toBe('2'); // Second most topics (1)
|
886
|
+
});
|
887
|
+
|
888
|
+
it('should handle sessions with no topics', async () => {
|
889
|
+
// Create test data
|
890
|
+
await serverDB.transaction(async (trx) => {
|
891
|
+
await trx.insert(sessions).values([
|
892
|
+
{ id: '1', userId },
|
893
|
+
{ id: '2', userId },
|
894
|
+
]);
|
895
|
+
|
896
|
+
await trx.insert(agents).values([
|
897
|
+
{ id: 'a1', userId, title: 'Agent 1' },
|
898
|
+
{ id: 'a2', userId, title: 'Agent 2' },
|
899
|
+
]);
|
900
|
+
|
901
|
+
await trx.insert(agentsToSessions).values([
|
902
|
+
{ sessionId: '1', agentId: 'a1' },
|
903
|
+
{ sessionId: '2', agentId: 'a2' },
|
904
|
+
]);
|
905
|
+
|
906
|
+
// No topics created
|
907
|
+
});
|
908
|
+
|
909
|
+
const result = await sessionModel.rank();
|
910
|
+
|
911
|
+
expect(result).toHaveLength(0);
|
912
|
+
});
|
913
|
+
});
|
914
|
+
|
915
|
+
describe('hasMoreThanN', () => {
|
916
|
+
it('should return true when session count is more than N', async () => {
|
917
|
+
// Create test data
|
918
|
+
await serverDB.insert(sessions).values([
|
919
|
+
{ id: '1', userId },
|
920
|
+
{ id: '2', userId },
|
921
|
+
{ id: '3', userId },
|
922
|
+
]);
|
923
|
+
|
924
|
+
const result = await sessionModel.hasMoreThanN(2);
|
925
|
+
expect(result).toBe(true);
|
926
|
+
});
|
927
|
+
|
928
|
+
it('should return false when session count is equal to N', async () => {
|
929
|
+
// Create test data
|
930
|
+
await serverDB.insert(sessions).values([
|
931
|
+
{ id: '1', userId },
|
932
|
+
{ id: '2', userId },
|
933
|
+
]);
|
934
|
+
|
935
|
+
const result = await sessionModel.hasMoreThanN(2);
|
936
|
+
expect(result).toBe(false);
|
937
|
+
});
|
938
|
+
|
939
|
+
it('should return false when session count is less than N', async () => {
|
940
|
+
// Create test data
|
941
|
+
await serverDB.insert(sessions).values([{ id: '1', userId }]);
|
942
|
+
|
943
|
+
const result = await sessionModel.hasMoreThanN(2);
|
944
|
+
expect(result).toBe(false);
|
945
|
+
});
|
946
|
+
|
947
|
+
it('should only count sessions for the current user', async () => {
|
948
|
+
// Create sessions for current user and another user
|
949
|
+
await serverDB.transaction(async (trx) => {
|
950
|
+
await trx.insert(users).values([{ id: 'other-user' }]);
|
951
|
+
await trx.insert(sessions).values([
|
952
|
+
{ id: '1', userId }, // Current user
|
953
|
+
{ id: '2', userId: 'other-user' }, // Other user
|
954
|
+
{ id: '3', userId: 'other-user' }, // Other user
|
955
|
+
]);
|
956
|
+
});
|
957
|
+
|
958
|
+
const result = await sessionModel.hasMoreThanN(1);
|
959
|
+
// Should return false as current user only has 1 session
|
960
|
+
expect(result).toBe(false);
|
961
|
+
});
|
962
|
+
|
963
|
+
it('should return false when no sessions exist', async () => {
|
964
|
+
const result = await sessionModel.hasMoreThanN(0);
|
965
|
+
expect(result).toBe(false);
|
966
|
+
});
|
967
|
+
});
|
785
968
|
});
|
@@ -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> => {
|