@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.
- package/.env.desktop +0 -1
- package/.env.example +16 -20
- package/.env.example.development +1 -4
- package/.github/workflows/e2e.yml +10 -11
- package/CHANGELOG.md +60 -0
- package/Dockerfile +28 -4
- package/changelog/v1.json +18 -0
- package/docker-compose/local/docker-compose.yml +2 -2
- package/docker-compose/local/grafana/docker-compose.yml +2 -2
- package/docker-compose/local/logto/docker-compose.yml +2 -2
- package/docker-compose/local/zitadel/.env.example +2 -2
- package/docker-compose/local/zitadel/.env.zh-CN.example +2 -2
- package/docker-compose/production/grafana/docker-compose.yml +2 -2
- package/docker-compose/production/logto/.env.example +2 -2
- package/docker-compose/production/logto/.env.zh-CN.example +2 -2
- package/docker-compose/production/zitadel/.env.example +2 -2
- package/docker-compose/production/zitadel/.env.zh-CN.example +2 -2
- package/docs/development/basic/add-new-authentication-providers.mdx +144 -136
- package/docs/development/basic/add-new-authentication-providers.zh-CN.mdx +146 -136
- package/docs/self-hosting/advanced/auth/legacy.mdx +4 -0
- package/docs/self-hosting/advanced/auth/legacy.zh-CN.mdx +4 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +326 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +323 -0
- package/docs/self-hosting/advanced/auth.mdx +43 -16
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +44 -16
- package/docs/self-hosting/advanced/redis/upstash.mdx +69 -0
- package/docs/self-hosting/advanced/redis/upstash.zh-CN.mdx +69 -0
- package/docs/self-hosting/advanced/redis.mdx +128 -0
- package/docs/self-hosting/advanced/redis.zh-CN.mdx +126 -0
- package/docs/self-hosting/environment-variables/auth.mdx +15 -1
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +15 -1
- package/docs/self-hosting/environment-variables/basic.mdx +13 -0
- package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +13 -0
- package/docs/self-hosting/environment-variables/redis.mdx +68 -0
- package/docs/self-hosting/environment-variables/redis.zh-CN.mdx +67 -0
- package/docs/self-hosting/migration/v2/breaking-changes.mdx +23 -23
- package/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx +23 -23
- package/docs/self-hosting/server-database/docker-compose.mdx +4 -4
- package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +4 -4
- package/e2e/CLAUDE.md +5 -6
- package/e2e/docs/local-setup.md +9 -12
- package/e2e/scripts/setup.ts +9 -15
- package/e2e/src/support/webServer.ts +6 -5
- package/package.json +4 -6
- package/packages/database/src/schemas/nextauth.ts +7 -2
- package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +370 -0
- package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +18 -5
- package/packages/utils/src/server/__tests__/auth.test.ts +1 -63
- package/packages/utils/src/server/auth.ts +8 -24
- package/scripts/_shared/checkDeprecatedAuth.js +99 -0
- package/scripts/clerk-to-betterauth/index.ts +8 -3
- package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
- package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
- package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
- package/scripts/nextauth-to-betterauth/index.ts +226 -0
- package/scripts/nextauth-to-betterauth/verify.ts +188 -0
- package/scripts/prebuild.mts +66 -13
- package/scripts/serverLauncher/startServer.js +5 -5
- package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
- package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
- package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
- package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
- package/src/app/(backend)/middleware/auth/index.ts +6 -15
- package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
- package/src/app/(backend)/middleware/auth/utils.ts +3 -8
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
- package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
- package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
- package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
- package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
- package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
- package/src/envs/auth.ts +12 -51
- package/src/envs/email.ts +3 -0
- package/src/envs/redis.ts +12 -54
- package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
- package/src/features/ChatInput/InputEditor/index.tsx +14 -3
- package/src/features/ChatInput/store/initialState.ts +2 -0
- package/src/features/EditorCanvas/DiffAllToolbar.tsx +4 -5
- package/src/features/EditorCanvas/DocumentIdMode.tsx +21 -1
- package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
- package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
- package/src/layout/AuthProvider/index.tsx +1 -6
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
- package/src/libs/better-auth/define-config.ts +2 -0
- package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
- package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
- package/src/libs/next/config/define-config.ts +13 -1
- package/src/libs/next/proxy/define-config.ts +2 -75
- package/src/libs/oidc-provider/provider.test.ts +0 -4
- package/src/libs/redis/index.ts +0 -1
- package/src/libs/redis/manager.test.ts +9 -45
- package/src/libs/redis/manager.ts +2 -16
- package/src/libs/redis/redis.test.ts +2 -4
- package/src/libs/redis/redis.ts +2 -4
- package/src/libs/redis/types.ts +2 -24
- package/src/libs/redis/utils.test.ts +0 -10
- package/src/libs/redis/utils.ts +0 -19
- package/src/libs/trpc/lambda/context.test.ts +0 -13
- package/src/libs/trpc/lambda/context.ts +21 -59
- package/src/libs/trpc/middleware/userAuth.ts +1 -7
- package/src/libs/trusted-client/getSessionUser.ts +15 -35
- package/src/server/globalConfig/index.ts +1 -3
- package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
- package/src/server/routers/lambda/user.ts +1 -12
- package/src/server/services/email/impls/nodemailer/index.ts +2 -2
- package/src/server/services/webhookUser/index.ts +88 -0
- package/src/services/user/index.test.ts +0 -14
- package/src/services/user/index.ts +0 -4
- package/src/store/document/slices/document/action.ts +1 -0
- package/src/store/user/slices/auth/action.test.ts +22 -126
- package/src/store/user/slices/auth/action.ts +32 -65
- package/src/store/user/slices/auth/initialState.ts +0 -3
- package/src/store/user/slices/auth/selectors.ts +0 -3
- package/tests/setup.ts +10 -0
- package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
- package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
- package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
- package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
- package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
- package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
- package/src/envs/auth.test.ts +0 -47
- package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
- package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
- package/src/libs/next-auth/adapter/index.ts +0 -177
- package/src/libs/next-auth/auth.config.ts +0 -64
- package/src/libs/next-auth/index.ts +0 -20
- package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
- package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
- package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
- package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
- package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
- package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
- package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
- package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
- package/src/libs/next-auth/sso-providers/github.ts +0 -23
- package/src/libs/next-auth/sso-providers/google.ts +0 -18
- package/src/libs/next-auth/sso-providers/index.ts +0 -35
- package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
- package/src/libs/next-auth/sso-providers/logto.ts +0 -48
- package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
- package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
- package/src/libs/next-auth/sso-providers/okta.ts +0 -22
- package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
- package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
- package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
- package/src/libs/redis/upstash.test.ts +0 -158
- package/src/libs/redis/upstash.ts +0 -136
- package/src/server/services/nextAuthUser/index.ts +0 -318
- package/src/server/services/nextAuthUser/utils.ts +0 -62
- 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:
|
|
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:
|
|
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:
|
|
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
|
|
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 {
|
|
3
|
+
import { auth } from '@/auth';
|
|
4
4
|
|
|
5
5
|
export const getUserAuth = async () => {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const currentHeaders = await headers();
|
|
7
|
+
const requestHeaders = Object.fromEntries(currentHeaders.entries());
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const session = await auth.api.getSession({
|
|
10
|
+
headers: requestHeaders,
|
|
11
|
+
});
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
headers: requestHeaders,
|
|
14
|
-
});
|
|
13
|
+
const userId = session?.user?.id;
|
|
15
14
|
|
|
16
|
-
|
|
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(
|
|
309
|
+
console.log(`${GREEN_BOLD}✅ Migration success!${RESET} (${formatDuration(Date.now() - startedAt)})`);
|
|
305
310
|
} catch (error) {
|
|
306
311
|
console.log('');
|
|
307
|
-
console.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();
|