@lobehub/lobehub 2.0.0-next.355 → 2.0.0-next.357

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 (156) hide show
  1. package/.env.desktop +0 -1
  2. package/.env.example +16 -20
  3. package/.env.example.development +1 -4
  4. package/.github/workflows/e2e.yml +10 -11
  5. package/CHANGELOG.md +60 -0
  6. package/Dockerfile +28 -4
  7. package/changelog/v1.json +18 -0
  8. package/docker-compose/local/docker-compose.yml +2 -2
  9. package/docker-compose/local/grafana/docker-compose.yml +2 -2
  10. package/docker-compose/local/logto/docker-compose.yml +2 -2
  11. package/docker-compose/local/zitadel/.env.example +2 -2
  12. package/docker-compose/local/zitadel/.env.zh-CN.example +2 -2
  13. package/docker-compose/production/grafana/docker-compose.yml +2 -2
  14. package/docker-compose/production/logto/.env.example +2 -2
  15. package/docker-compose/production/logto/.env.zh-CN.example +2 -2
  16. package/docker-compose/production/zitadel/.env.example +2 -2
  17. package/docker-compose/production/zitadel/.env.zh-CN.example +2 -2
  18. package/docs/development/basic/add-new-authentication-providers.mdx +144 -136
  19. package/docs/development/basic/add-new-authentication-providers.zh-CN.mdx +146 -136
  20. package/docs/self-hosting/advanced/auth/legacy.mdx +4 -0
  21. package/docs/self-hosting/advanced/auth/legacy.zh-CN.mdx +4 -0
  22. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +326 -0
  23. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +323 -0
  24. package/docs/self-hosting/advanced/auth.mdx +43 -16
  25. package/docs/self-hosting/advanced/auth.zh-CN.mdx +44 -16
  26. package/docs/self-hosting/advanced/redis/upstash.mdx +69 -0
  27. package/docs/self-hosting/advanced/redis/upstash.zh-CN.mdx +69 -0
  28. package/docs/self-hosting/advanced/redis.mdx +128 -0
  29. package/docs/self-hosting/advanced/redis.zh-CN.mdx +126 -0
  30. package/docs/self-hosting/environment-variables/auth.mdx +15 -1
  31. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +15 -1
  32. package/docs/self-hosting/environment-variables/basic.mdx +13 -0
  33. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +13 -0
  34. package/docs/self-hosting/environment-variables/redis.mdx +68 -0
  35. package/docs/self-hosting/environment-variables/redis.zh-CN.mdx +67 -0
  36. package/docs/self-hosting/migration/v2/breaking-changes.mdx +23 -23
  37. package/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx +23 -23
  38. package/docs/self-hosting/server-database/docker-compose.mdx +4 -4
  39. package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +4 -4
  40. package/e2e/CLAUDE.md +5 -6
  41. package/e2e/docs/local-setup.md +9 -12
  42. package/e2e/scripts/setup.ts +9 -15
  43. package/e2e/src/support/webServer.ts +6 -5
  44. package/package.json +4 -6
  45. package/packages/database/src/schemas/nextauth.ts +7 -2
  46. package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +370 -0
  47. package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +18 -5
  48. package/packages/utils/src/server/__tests__/auth.test.ts +1 -63
  49. package/packages/utils/src/server/auth.ts +8 -24
  50. package/scripts/_shared/checkDeprecatedAuth.js +99 -0
  51. package/scripts/clerk-to-betterauth/index.ts +8 -3
  52. package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
  53. package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
  54. package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
  55. package/scripts/nextauth-to-betterauth/index.ts +226 -0
  56. package/scripts/nextauth-to-betterauth/verify.ts +188 -0
  57. package/scripts/prebuild.mts +66 -13
  58. package/scripts/serverLauncher/startServer.js +5 -5
  59. package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
  60. package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
  61. package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
  62. package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
  63. package/src/app/(backend)/middleware/auth/index.ts +6 -15
  64. package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
  65. package/src/app/(backend)/middleware/auth/utils.ts +3 -8
  66. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
  67. package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
  68. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
  69. package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
  70. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
  71. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
  72. package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
  73. package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
  74. package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
  75. package/src/envs/auth.ts +12 -51
  76. package/src/envs/email.ts +3 -0
  77. package/src/envs/redis.ts +12 -54
  78. package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
  79. package/src/features/ChatInput/InputEditor/index.tsx +14 -3
  80. package/src/features/ChatInput/store/initialState.ts +2 -0
  81. package/src/features/EditorCanvas/DiffAllToolbar.tsx +4 -5
  82. package/src/features/EditorCanvas/DocumentIdMode.tsx +21 -1
  83. package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
  84. package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
  85. package/src/layout/AuthProvider/index.tsx +1 -6
  86. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
  87. package/src/libs/better-auth/define-config.ts +2 -0
  88. package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
  89. package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
  90. package/src/libs/next/config/define-config.ts +13 -1
  91. package/src/libs/next/proxy/define-config.ts +2 -75
  92. package/src/libs/oidc-provider/provider.test.ts +0 -4
  93. package/src/libs/redis/index.ts +0 -1
  94. package/src/libs/redis/manager.test.ts +9 -45
  95. package/src/libs/redis/manager.ts +2 -16
  96. package/src/libs/redis/redis.test.ts +2 -4
  97. package/src/libs/redis/redis.ts +2 -4
  98. package/src/libs/redis/types.ts +2 -24
  99. package/src/libs/redis/utils.test.ts +0 -10
  100. package/src/libs/redis/utils.ts +0 -19
  101. package/src/libs/trpc/lambda/context.test.ts +0 -13
  102. package/src/libs/trpc/lambda/context.ts +21 -59
  103. package/src/libs/trpc/middleware/userAuth.ts +1 -7
  104. package/src/libs/trusted-client/getSessionUser.ts +15 -35
  105. package/src/server/globalConfig/index.ts +1 -3
  106. package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
  107. package/src/server/routers/lambda/user.ts +1 -12
  108. package/src/server/services/email/impls/nodemailer/index.ts +2 -2
  109. package/src/server/services/webhookUser/index.ts +88 -0
  110. package/src/services/user/index.test.ts +0 -14
  111. package/src/services/user/index.ts +0 -4
  112. package/src/store/document/slices/document/action.ts +1 -0
  113. package/src/store/user/slices/auth/action.test.ts +22 -126
  114. package/src/store/user/slices/auth/action.ts +32 -65
  115. package/src/store/user/slices/auth/initialState.ts +0 -3
  116. package/src/store/user/slices/auth/selectors.ts +0 -3
  117. package/tests/setup.ts +10 -0
  118. package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
  119. package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
  120. package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
  121. package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
  122. package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
  123. package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
  124. package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
  125. package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
  126. package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
  127. package/src/envs/auth.test.ts +0 -47
  128. package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
  129. package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
  130. package/src/libs/next-auth/adapter/index.ts +0 -177
  131. package/src/libs/next-auth/auth.config.ts +0 -64
  132. package/src/libs/next-auth/index.ts +0 -20
  133. package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
  134. package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
  135. package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
  136. package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
  137. package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
  138. package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
  139. package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
  140. package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
  141. package/src/libs/next-auth/sso-providers/github.ts +0 -23
  142. package/src/libs/next-auth/sso-providers/google.ts +0 -18
  143. package/src/libs/next-auth/sso-providers/index.ts +0 -35
  144. package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
  145. package/src/libs/next-auth/sso-providers/logto.ts +0 -48
  146. package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
  147. package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
  148. package/src/libs/next-auth/sso-providers/okta.ts +0 -22
  149. package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
  150. package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
  151. package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
  152. package/src/libs/redis/upstash.test.ts +0 -158
  153. package/src/libs/redis/upstash.ts +0 -136
  154. package/src/server/services/nextAuthUser/index.ts +0 -318
  155. package/src/server/services/nextAuthUser/utils.ts +0 -62
  156. package/src/types/next-auth.d.ts +0 -26
@@ -281,6 +281,62 @@ describe('anthropicHelpers', () => {
281
281
  const result = await buildAnthropicMessage(message);
282
282
  expect(result).toBeUndefined();
283
283
  });
284
+
285
+ it('should handle assistant message with tool_calls but null content', async () => {
286
+ const message: OpenAIChatMessage = {
287
+ content: null as any,
288
+ role: 'assistant',
289
+ tool_calls: [
290
+ {
291
+ id: 'call1',
292
+ type: 'function',
293
+ function: {
294
+ name: 'search_people',
295
+ arguments: '{"location":"Singapore"}',
296
+ },
297
+ },
298
+ ],
299
+ };
300
+ const result = await buildAnthropicMessage(message);
301
+ expect(result!.role).toBe('assistant');
302
+ // null content should be filtered out, only tool_use remains
303
+ expect(result!.content).toEqual([
304
+ {
305
+ id: 'call1',
306
+ input: { location: 'Singapore' },
307
+ name: 'search_people',
308
+ type: 'tool_use',
309
+ },
310
+ ]);
311
+ });
312
+
313
+ it('should handle assistant message with tool_calls but empty string content', async () => {
314
+ const message: OpenAIChatMessage = {
315
+ content: '',
316
+ role: 'assistant',
317
+ tool_calls: [
318
+ {
319
+ id: 'call1',
320
+ type: 'function',
321
+ function: {
322
+ name: 'search_people',
323
+ arguments: '{"location":"Singapore"}',
324
+ },
325
+ },
326
+ ],
327
+ };
328
+ const result = await buildAnthropicMessage(message);
329
+ expect(result!.role).toBe('assistant');
330
+ // empty string content should be filtered out, only tool_use remains
331
+ expect(result!.content).toEqual([
332
+ {
333
+ id: 'call1',
334
+ input: { location: 'Singapore' },
335
+ name: 'search_people',
336
+ type: 'tool_use',
337
+ },
338
+ ]);
339
+ });
284
340
  });
285
341
 
286
342
  describe('buildAnthropicMessages', () => {
@@ -526,6 +582,320 @@ describe('anthropicHelpers', () => {
526
582
  ]);
527
583
  });
528
584
 
585
+ it('should handle tool message with null content', async () => {
586
+ const messages: OpenAIChatMessage[] = [
587
+ {
588
+ content: '搜索人员',
589
+ role: 'user',
590
+ },
591
+ {
592
+ content: '正在搜索...',
593
+ role: 'assistant',
594
+ tool_calls: [
595
+ {
596
+ function: {
597
+ arguments: '{"location": "Singapore"}',
598
+ name: 'search_people',
599
+ },
600
+ id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
601
+ type: 'function',
602
+ },
603
+ ],
604
+ },
605
+ {
606
+ content: null as any,
607
+ name: 'search_people',
608
+ role: 'tool',
609
+ tool_call_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
610
+ },
611
+ ];
612
+
613
+ const contents = await buildAnthropicMessages(messages);
614
+
615
+ expect(contents).toEqual([
616
+ { content: '搜索人员', role: 'user' },
617
+ {
618
+ content: [
619
+ { text: '正在搜索...', type: 'text' },
620
+ {
621
+ id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
622
+ input: { location: 'Singapore' },
623
+ name: 'search_people',
624
+ type: 'tool_use',
625
+ },
626
+ ],
627
+ role: 'assistant',
628
+ },
629
+ {
630
+ content: [
631
+ {
632
+ content: [{ text: '<empty_content>', type: 'text' }],
633
+ tool_use_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
634
+ type: 'tool_result',
635
+ },
636
+ ],
637
+ role: 'user',
638
+ },
639
+ ]);
640
+ });
641
+
642
+ it('should handle tool message with empty string content', async () => {
643
+ const messages: OpenAIChatMessage[] = [
644
+ {
645
+ content: '搜索人员',
646
+ role: 'user',
647
+ },
648
+ {
649
+ content: '正在搜索...',
650
+ role: 'assistant',
651
+ tool_calls: [
652
+ {
653
+ function: {
654
+ arguments: '{"location": "Singapore"}',
655
+ name: 'search_people',
656
+ },
657
+ id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
658
+ type: 'function',
659
+ },
660
+ ],
661
+ },
662
+ {
663
+ content: '',
664
+ name: 'search_people',
665
+ role: 'tool',
666
+ tool_call_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
667
+ },
668
+ ];
669
+
670
+ const contents = await buildAnthropicMessages(messages);
671
+
672
+ expect(contents).toEqual([
673
+ { content: '搜索人员', role: 'user' },
674
+ {
675
+ content: [
676
+ { text: '正在搜索...', type: 'text' },
677
+ {
678
+ id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
679
+ input: { location: 'Singapore' },
680
+ name: 'search_people',
681
+ type: 'tool_use',
682
+ },
683
+ ],
684
+ role: 'assistant',
685
+ },
686
+ {
687
+ content: [
688
+ {
689
+ content: [{ text: '<empty_content>', type: 'text' }],
690
+ tool_use_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
691
+ type: 'tool_result',
692
+ },
693
+ ],
694
+ role: 'user',
695
+ },
696
+ ]);
697
+ });
698
+
699
+ it('should handle tool message with array content', async () => {
700
+ const messages: OpenAIChatMessage[] = [
701
+ {
702
+ content: '搜索人员',
703
+ role: 'user',
704
+ },
705
+ {
706
+ content: '正在搜索...',
707
+ role: 'assistant',
708
+ tool_calls: [
709
+ {
710
+ function: {
711
+ arguments: '{"location": "Singapore"}',
712
+ name: 'search_people',
713
+ },
714
+ id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
715
+ type: 'function',
716
+ },
717
+ ],
718
+ },
719
+ {
720
+ content: [
721
+ { type: 'text', text: 'Found 5 candidates' },
722
+ { type: 'text', text: 'Result details here' },
723
+ ] as any,
724
+ name: 'search_people',
725
+ role: 'tool',
726
+ tool_call_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
727
+ },
728
+ ];
729
+
730
+ const contents = await buildAnthropicMessages(messages);
731
+
732
+ expect(contents).toEqual([
733
+ { content: '搜索人员', role: 'user' },
734
+ {
735
+ content: [
736
+ { text: '正在搜索...', type: 'text' },
737
+ {
738
+ id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
739
+ input: { location: 'Singapore' },
740
+ name: 'search_people',
741
+ type: 'tool_use',
742
+ },
743
+ ],
744
+ role: 'assistant',
745
+ },
746
+ {
747
+ content: [
748
+ {
749
+ content: [
750
+ { type: 'text', text: 'Found 5 candidates' },
751
+ { type: 'text', text: 'Result details here' },
752
+ ],
753
+ tool_use_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
754
+ type: 'tool_result',
755
+ },
756
+ ],
757
+ role: 'user',
758
+ },
759
+ ]);
760
+ });
761
+
762
+ it('should handle tool message with array content containing image', async () => {
763
+ vi.mocked(parseDataUri).mockReturnValueOnce({
764
+ mimeType: 'image/png',
765
+ base64: 'screenshotBase64Data',
766
+ type: 'base64',
767
+ });
768
+
769
+ const messages: OpenAIChatMessage[] = [
770
+ {
771
+ content: '截图分析',
772
+ role: 'user',
773
+ },
774
+ {
775
+ content: '正在截图...',
776
+ role: 'assistant',
777
+ tool_calls: [
778
+ {
779
+ function: {
780
+ arguments: '{"url": "https://example.com"}',
781
+ name: 'screenshot',
782
+ },
783
+ id: 'toolu_screenshot_123',
784
+ type: 'function',
785
+ },
786
+ ],
787
+ },
788
+ {
789
+ content: [
790
+ { type: 'text', text: 'Screenshot captured' },
791
+ {
792
+ type: 'image_url',
793
+ image_url: { url: 'data:image/png;base64,screenshotBase64Data' },
794
+ },
795
+ ] as any,
796
+ name: 'screenshot',
797
+ role: 'tool',
798
+ tool_call_id: 'toolu_screenshot_123',
799
+ },
800
+ ];
801
+
802
+ const contents = await buildAnthropicMessages(messages);
803
+
804
+ expect(contents).toEqual([
805
+ { content: '截图分析', role: 'user' },
806
+ {
807
+ content: [
808
+ { text: '正在截图...', type: 'text' },
809
+ {
810
+ id: 'toolu_screenshot_123',
811
+ input: { url: 'https://example.com' },
812
+ name: 'screenshot',
813
+ type: 'tool_use',
814
+ },
815
+ ],
816
+ role: 'assistant',
817
+ },
818
+ {
819
+ content: [
820
+ {
821
+ content: [
822
+ { type: 'text', text: 'Screenshot captured' },
823
+ {
824
+ type: 'image',
825
+ source: {
826
+ type: 'base64',
827
+ media_type: 'image/png',
828
+ data: 'screenshotBase64Data',
829
+ },
830
+ },
831
+ ],
832
+ tool_use_id: 'toolu_screenshot_123',
833
+ type: 'tool_result',
834
+ },
835
+ ],
836
+ role: 'user',
837
+ },
838
+ ]);
839
+ });
840
+
841
+ it('should handle orphan tool message with null content', async () => {
842
+ // Tool message without corresponding assistant tool_call
843
+ const messages: OpenAIChatMessage[] = [
844
+ {
845
+ content: null as any,
846
+ name: 'some_tool',
847
+ role: 'tool',
848
+ tool_call_id: 'orphan_tool_call_id',
849
+ },
850
+ {
851
+ content: 'Continue',
852
+ role: 'user',
853
+ },
854
+ ];
855
+
856
+ const contents = await buildAnthropicMessages(messages);
857
+
858
+ expect(contents).toEqual([
859
+ {
860
+ content: '<empty_content>',
861
+ role: 'user',
862
+ },
863
+ {
864
+ content: 'Continue',
865
+ role: 'user',
866
+ },
867
+ ]);
868
+ });
869
+
870
+ it('should handle orphan tool message with empty string content', async () => {
871
+ // Tool message without corresponding assistant tool_call
872
+ const messages: OpenAIChatMessage[] = [
873
+ {
874
+ content: '',
875
+ name: 'some_tool',
876
+ role: 'tool',
877
+ tool_call_id: 'orphan_tool_call_id',
878
+ },
879
+ {
880
+ content: 'Continue',
881
+ role: 'user',
882
+ },
883
+ ];
884
+
885
+ const contents = await buildAnthropicMessages(messages);
886
+
887
+ expect(contents).toEqual([
888
+ {
889
+ content: '<empty_content>',
890
+ role: 'user',
891
+ },
892
+ {
893
+ content: 'Continue',
894
+ role: 'user',
895
+ },
896
+ ]);
897
+ });
898
+
529
899
  it('should work well starting with tool message', async () => {
530
900
  const messages: OpenAIChatMessage[] = [
531
901
  {
@@ -114,10 +114,13 @@ export const buildAnthropicMessage = async (
114
114
  // if there is tool_calls , we need to covert the tool_calls to tool_use content block
115
115
  // refs: https://docs.anthropic.com/claude/docs/tool-use#tool-use-and-tool-result-content-blocks
116
116
  if (message.tool_calls && message.tool_calls.length > 0) {
117
+ // Handle content: string with text, array, null/undefined/empty -> filter out
117
118
  const rawContent =
118
- typeof content === 'string'
119
- ? ([{ text: message.content, type: 'text' }] as UserMessageContentPart[])
120
- : content;
119
+ typeof content === 'string' && content.trim()
120
+ ? ([{ text: content, type: 'text' }] as UserMessageContentPart[])
121
+ : Array.isArray(content)
122
+ ? content
123
+ : []; // null/undefined/empty string -> empty array (will be filtered)
121
124
 
122
125
  const messageContent = await buildArrayContent(rawContent);
123
126
 
@@ -180,10 +183,17 @@ export const buildAnthropicMessages = async (
180
183
 
181
184
  // refs: https://docs.anthropic.com/claude/docs/tool-use#tool-use-and-tool-result-content-blocks
182
185
  if (message.role === 'tool') {
186
+ // Handle different content types in tool messages
187
+ const toolResultContent = Array.isArray(message.content)
188
+ ? await buildArrayContent(message.content)
189
+ : !message.content
190
+ ? [{ text: '<empty_content>', type: 'text' as const }]
191
+ : [{ text: message.content, type: 'text' as const }];
192
+
183
193
  // 检查这个工具消息是否有对应的 assistant 工具调用
184
194
  if (message.tool_call_id && validToolCallIds.has(message.tool_call_id)) {
185
195
  pendingToolResults.push({
186
- content: [{ text: message.content as string, type: 'text' }],
196
+ content: toolResultContent as Anthropic.ToolResultBlockParam['content'],
187
197
  tool_use_id: message.tool_call_id,
188
198
  type: 'tool_result',
189
199
  });
@@ -198,8 +208,11 @@ export const buildAnthropicMessages = async (
198
208
  }
199
209
  } else {
200
210
  // 如果工具消息没有对应的 assistant 工具调用,则作为普通文本处理
211
+ const fallbackContent = Array.isArray(message.content)
212
+ ? JSON.stringify(message.content)
213
+ : message.content || '<empty_content>';
201
214
  messages.push({
202
- content: message.content as string,
215
+ content: fallbackContent,
203
216
  role: 'user',
204
217
  });
205
218
  }
@@ -2,29 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { extractBearerToken, getUserAuth } from '../auth';
4
4
 
5
- // Mock auth constants
6
- let mockEnableBetterAuth = false;
7
- let mockEnableNextAuth = false;
8
-
9
- vi.mock('@/envs/auth', () => ({
10
- get enableBetterAuth() {
11
- return mockEnableBetterAuth;
12
- },
13
- get enableNextAuth() {
14
- return mockEnableNextAuth;
15
- },
16
- }));
17
-
18
- vi.mock('@/libs/next-auth', () => ({
19
- default: {
20
- auth: vi.fn().mockResolvedValue({
21
- user: {
22
- id: 'next-auth-user-id',
23
- },
24
- }),
25
- },
26
- }));
27
-
28
5
  vi.mock('next/headers', () => ({
29
6
  headers: vi.fn(() => new Headers()),
30
7
  }));
@@ -44,48 +21,9 @@ vi.mock('@/auth', () => ({
44
21
  describe('getUserAuth', () => {
45
22
  beforeEach(() => {
46
23
  vi.clearAllMocks();
47
- mockEnableBetterAuth = false;
48
- mockEnableNextAuth = false;
49
24
  });
50
25
 
51
- it('should throw error when no auth method is enabled', async () => {
52
- await expect(getUserAuth()).rejects.toThrow('Auth method is not enabled');
53
- });
54
-
55
- it('should return next auth when next auth is enabled', async () => {
56
- mockEnableNextAuth = true;
57
-
58
- const auth = await getUserAuth();
59
-
60
- expect(auth).toEqual({
61
- nextAuth: {
62
- user: {
63
- id: 'next-auth-user-id',
64
- },
65
- },
66
- userId: 'next-auth-user-id',
67
- });
68
- });
69
-
70
- it('should return better auth when better auth is enabled', async () => {
71
- mockEnableBetterAuth = true;
72
-
73
- const auth = await getUserAuth();
74
-
75
- expect(auth).toEqual({
76
- betterAuth: {
77
- user: {
78
- id: 'better-auth-user-id',
79
- },
80
- },
81
- userId: 'better-auth-user-id',
82
- });
83
- });
84
-
85
- it('should prioritize better auth over next auth when both are enabled', async () => {
86
- mockEnableBetterAuth = true;
87
- mockEnableNextAuth = true;
88
-
26
+ it('should return better auth session', async () => {
89
27
  const auth = await getUserAuth();
90
28
 
91
29
  expect(auth).toEqual({
@@ -1,34 +1,18 @@
1
1
  import { headers } from 'next/headers';
2
2
 
3
- import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
3
+ import { auth } from '@/auth';
4
4
 
5
5
  export const getUserAuth = async () => {
6
- if (enableBetterAuth) {
7
- const { auth: betterAuth } = await import('@/auth');
6
+ const currentHeaders = await headers();
7
+ const requestHeaders = Object.fromEntries(currentHeaders.entries());
8
8
 
9
- const currentHeaders = await headers();
10
- const requestHeaders = Object.fromEntries(currentHeaders.entries());
9
+ const session = await auth.api.getSession({
10
+ headers: requestHeaders,
11
+ });
11
12
 
12
- const session = await betterAuth.api.getSession({
13
- headers: requestHeaders,
14
- });
13
+ const userId = session?.user?.id;
15
14
 
16
- const userId = session?.user?.id;
17
-
18
- return { betterAuth: session, userId };
19
- }
20
-
21
- if (enableNextAuth) {
22
- const { default: NextAuth } = await import('@/libs/next-auth');
23
-
24
- const session = await NextAuth.auth();
25
-
26
- const userId = session?.user.id;
27
-
28
- return { nextAuth: session, userId };
29
- }
30
-
31
- throw new Error('Auth method is not enabled');
15
+ return { betterAuth: session, userId };
32
16
  };
33
17
 
34
18
  /**
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Shared utility to check for deprecated authentication environment variables.
3
+ * Used by both prebuild.mts (build time) and startServer.js (Docker runtime).
4
+ *
5
+ * IMPORTANT: Keep this file as CommonJS (.js) for compatibility with startServer.js
6
+ */
7
+
8
+ const MIGRATION_DOC_BASE = 'https://lobehub.com/docs/self-hosting/advanced/auth';
9
+
10
+ /**
11
+ * Deprecated environment variable checks configuration
12
+ * @type {Array<{
13
+ * name: string;
14
+ * getVars: () => string[];
15
+ * message: string;
16
+ * docUrl?: string;
17
+ * formatVar?: (envVar: string) => string;
18
+ * }>}
19
+ */
20
+ const DEPRECATED_CHECKS = [
21
+ {
22
+ docUrl: `${MIGRATION_DOC_BASE}/nextauth-to-betterauth`,
23
+ getVars: () =>
24
+ Object.keys(process.env).filter(
25
+ (key) => key.startsWith('NEXT_AUTH') || key.startsWith('NEXTAUTH'),
26
+ ),
27
+ message: 'NextAuth has been removed from LobeChat. Please migrate to Better Auth.',
28
+ name: 'NextAuth',
29
+ },
30
+ {
31
+ docUrl: `${MIGRATION_DOC_BASE}/clerk-to-betterauth`,
32
+ getVars: () =>
33
+ ['NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY', 'CLERK_SECRET_KEY', 'CLERK_WEBHOOK_SECRET'].filter(
34
+ (key) => process.env[key],
35
+ ),
36
+ message: 'Clerk has been removed from LobeChat. Please migrate to Better Auth.',
37
+ name: 'Clerk',
38
+ },
39
+ {
40
+ formatVar: (envVar) => {
41
+ const mapping = {
42
+ NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION: 'AUTH_EMAIL_VERIFICATION',
43
+ NEXT_PUBLIC_ENABLE_MAGIC_LINK: 'ENABLE_MAGIC_LINK',
44
+ };
45
+ return `${envVar} → Please use ${mapping[envVar]} instead`;
46
+ },
47
+ getVars: () =>
48
+ ['NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION', 'NEXT_PUBLIC_ENABLE_MAGIC_LINK'].filter(
49
+ (key) => process.env[key],
50
+ ),
51
+ message: 'Please update to the new environment variable names.',
52
+ name: 'Deprecated Auth',
53
+ },
54
+ ];
55
+
56
+ /**
57
+ * Print error message and exit
58
+ */
59
+ function printErrorAndExit(name, vars, message, action, docUrl, formatVar) {
60
+ console.error('\n' + '═'.repeat(70));
61
+ console.error(`❌ ERROR: ${name} environment variables are deprecated!`);
62
+ console.error('═'.repeat(70));
63
+ console.error('\nDetected deprecated environment variables:');
64
+ for (const envVar of vars) {
65
+ console.error(` • ${formatVar ? formatVar(envVar) : envVar}`);
66
+ }
67
+ console.error(`\n${message}`);
68
+ if (docUrl) {
69
+ console.error(`\n📖 Migration guide: ${docUrl}`);
70
+ }
71
+ console.error(`\nPlease update your environment variables and ${action}.`);
72
+ console.error('═'.repeat(70) + '\n');
73
+ process.exit(1);
74
+ }
75
+
76
+ /**
77
+ * Check for deprecated authentication environment variables and exit if found
78
+ * @param {object} options
79
+ * @param {string} [options.action='redeploy'] - Action hint in error message ('redeploy' or 'restart')
80
+ */
81
+ function checkDeprecatedAuth(options = {}) {
82
+ const { action = 'redeploy' } = options;
83
+
84
+ for (const check of DEPRECATED_CHECKS) {
85
+ const foundVars = check.getVars();
86
+ if (foundVars.length > 0) {
87
+ printErrorAndExit(
88
+ check.name,
89
+ foundVars,
90
+ check.message,
91
+ action,
92
+ check.docUrl,
93
+ check.formatVar,
94
+ );
95
+ }
96
+ }
97
+ }
98
+
99
+ module.exports = { checkDeprecatedAuth };
@@ -13,6 +13,11 @@ const IS_DRY_RUN =
13
13
  process.argv.includes('--dry-run') || process.env.CLERK_TO_BETTERAUTH_DRY_RUN === '1';
14
14
  const formatDuration = (ms: number) => `${(ms / 1000).toFixed(1)}s`;
15
15
 
16
+ // ANSI color codes
17
+ const GREEN_BOLD = '\u001B[1;32m';
18
+ const RED_BOLD = '\u001B[1;31m';
19
+ const RESET = '\u001B[0m';
20
+
16
21
  function chunk<T>(items: T[], size: number): T[][] {
17
22
  if (!Number.isFinite(size) || size <= 0) return [items];
18
23
  const result: T[][] = [];
@@ -241,7 +246,7 @@ async function migrateFromClerk() {
241
246
  }
242
247
 
243
248
  console.log(
244
- `[clerk-to-betterauth] completed users=${processed}, skipped=${skipped}, accounts attempted=${accountAttempts}, 2fa attempted=${twoFactorAttempts}, dryRun=${IS_DRY_RUN}, elapsed=${formatDuration(Date.now() - startedAt)}`,
249
+ `[clerk-to-betterauth] completed users=${GREEN_BOLD}${processed}${RESET}, skipped=${skipped}, accounts attempted=${accountAttempts}, 2fa attempted=${twoFactorAttempts}, dryRun=${IS_DRY_RUN}, elapsed=${formatDuration(Date.now() - startedAt)}`,
245
250
  );
246
251
 
247
252
  const accountCountsText = Object.entries(accountCounts)
@@ -301,10 +306,10 @@ async function main() {
301
306
  try {
302
307
  await migrateFromClerk();
303
308
  console.log('');
304
- console.log(`✅ Migration success! (${formatDuration(Date.now() - startedAt)})`);
309
+ console.log(`${GREEN_BOLD}✅ Migration success!${RESET} (${formatDuration(Date.now() - startedAt)})`);
305
310
  } catch (error) {
306
311
  console.log('');
307
- console.error(`❌ Migration failed (${formatDuration(Date.now() - startedAt)}):`, error);
312
+ console.error(`${RED_BOLD}❌ Migration failed${RESET} (${formatDuration(Date.now() - startedAt)}):`, error);
308
313
  process.exitCode = 1;
309
314
  } finally {
310
315
  await pool.end();