@lobehub/lobehub 2.0.0-next.344 → 2.0.0-next.346
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/.cursor/rules/i18n.mdc +1 -1
- package/.cursor/rules/modal-imperative.mdc +162 -0
- package/.cursor/rules/rules-index.mdc +1 -0
- package/.env.example +0 -14
- package/.eslintrc.js +8 -1
- package/CHANGELOG.md +66 -0
- package/CLAUDE.md +4 -2
- package/Dockerfile +3 -13
- package/README.md +3 -5
- package/README.zh-CN.md +3 -5
- package/changelog/v1.json +20 -0
- package/docs/self-hosting/advanced/auth/clerk-to-betterauth.mdx +11 -42
- package/docs/self-hosting/advanced/auth/clerk-to-betterauth.zh-CN.mdx +10 -41
- package/e2e/src/support/webServer.ts +2 -0
- package/locales/ar/error.json +0 -4
- package/locales/bg-BG/error.json +0 -4
- package/locales/de-DE/error.json +0 -4
- package/locales/en-US/error.json +0 -4
- package/locales/es-ES/error.json +0 -4
- package/locales/fa-IR/error.json +0 -4
- package/locales/fr-FR/error.json +0 -4
- package/locales/it-IT/error.json +0 -4
- package/locales/ja-JP/error.json +0 -4
- package/locales/ko-KR/error.json +0 -4
- package/locales/nl-NL/error.json +0 -4
- package/locales/pl-PL/error.json +0 -4
- package/locales/pt-BR/error.json +0 -4
- package/locales/ru-RU/error.json +0 -4
- package/locales/tr-TR/error.json +0 -4
- package/locales/vi-VN/error.json +0 -4
- package/locales/zh-CN/error.json +0 -4
- package/locales/zh-TW/error.json +0 -4
- package/package.json +12 -12
- package/packages/builtin-agents/package.json +2 -0
- package/packages/builtin-agents/src/agents/agent-builder/index.ts +4 -2
- package/packages/builtin-agents/src/agents/group-agent-builder/index.ts +4 -2
- package/packages/builtin-agents/src/agents/page-agent/index.ts +5 -2
- package/packages/context-engine/src/engine/messages/MessagesEngine.ts +9 -9
- package/packages/context-engine/src/providers/GroupContextInjector.ts +19 -33
- package/packages/context-engine/src/providers/__tests__/GroupContextInjector.test.ts +79 -43
- package/packages/context-engine/src/providers/__tests__/__snapshots__/GroupContextInjector.test.ts.snap +5 -15
- package/packages/database/src/repositories/userMemory/__tests__/UserMemoryTopicRepository.test.ts +24 -3
- package/packages/file-loaders/package.json +1 -1
- package/packages/file-loaders/src/loadFile.ts +10 -15
- package/packages/file-loaders/src/loaders/index.ts +68 -19
- package/packages/file-loaders/src/loaders/pdf/__snapshots__/index.test.ts.snap +1 -1
- package/packages/file-loaders/test/__snapshots__/loaders.test.ts.snap +1 -1
- package/packages/model-bank/src/modelProviders/comfyui.ts +0 -1
- package/packages/model-bank/src/modelProviders/fal.ts +0 -1
- package/packages/types/src/fetch.ts +1 -2
- package/packages/utils/src/server/__tests__/auth.test.ts +0 -47
- package/packages/utils/src/server/auth.ts +1 -9
- package/pnpm-workspace.yaml +1 -0
- package/scripts/_shared/checkDeprecatedClerkEnv.js +42 -0
- package/scripts/changelogWorkflow/buildStaticChangelog.ts +2 -1
- package/scripts/clerk-to-betterauth/_internal/types.ts +53 -20
- package/scripts/clerk-to-betterauth/export-clerk-users-with-api.ts +43 -36
- package/scripts/countEnWord.ts +1 -1
- package/scripts/electronWorkflow/modifiers/appCode.mts +2 -131
- package/scripts/i18nWorkflow/protectedPatterns.ts +1 -2
- package/scripts/prebuild.mts +10 -8
- package/scripts/serverLauncher/startServer.js +23 -5
- package/src/app/(backend)/middleware/auth/index.test.ts +8 -4
- package/src/app/(backend)/middleware/auth/index.ts +0 -15
- package/src/app/(backend)/middleware/auth/utils.test.ts +0 -28
- package/src/app/(backend)/middleware/auth/utils.ts +2 -17
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +3 -51
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -4
- package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +7 -6
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -16
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/index.tsx +1 -1
- package/src/app/[variants]/(main)/home/features/InputArea/SkillInstallBanner.tsx +13 -13
- package/src/app/[variants]/(main)/home/features/RecentPage/Item.tsx +2 -2
- package/src/app/[variants]/(main)/resource/features/store/action.ts +2 -2
- package/src/app/[variants]/(main)/resource/features/store/initialState.ts +2 -2
- package/src/app/[variants]/(main)/resource/store/action.ts +2 -2
- package/src/app/[variants]/(main)/resource/store/initialState.ts +2 -2
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +3 -21
- package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -2
- package/src/app/[variants]/(main)/settings/security/index.tsx +1 -22
- package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +12 -14
- package/src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx +8 -14
- package/src/app/[variants]/(main)/settings/skill/index.tsx +7 -5
- package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +2 -35
- package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +0 -20
- package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -2
- package/src/app/[variants]/(mobile)/me/profile/features/Category.tsx +3 -13
- package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +2 -3
- package/src/app/[variants]/share/t/[id]/_layout/index.tsx +1 -1
- package/src/app/[variants]/share/t/[id]/index.tsx +1 -1
- package/src/app/robots.tsx +1 -1
- package/src/envs/auth.ts +2 -27
- package/src/envs/llm.ts +2 -2
- package/src/features/AgentSetting/AgentPlugin/index.tsx +9 -12
- package/src/features/ChatInput/ActionBar/Tools/index.tsx +7 -5
- package/src/features/ChatMiniMap/utils.ts +1 -1
- package/src/features/CommandMenu/SearchResults.tsx +1 -1
- package/src/features/Conversation/ChatList/components/AutoScroll/DebugInspector.tsx +166 -0
- package/src/features/Conversation/ChatList/components/AutoScroll/index.tsx +86 -0
- package/src/features/Conversation/ChatList/components/VirtualizedList.tsx +11 -17
- package/src/features/Conversation/Messages/AgentCouncil/components/AutoScrollShadow.tsx +25 -14
- package/src/features/Conversation/Messages/AgentCouncil/components/CouncilMember.tsx +1 -1
- package/src/features/FileViewer/Renderer/PDF/index.tsx +5 -8
- package/src/features/IntegrationDetailModal/IntegrationDetailContent.tsx +305 -0
- package/src/features/IntegrationDetailModal/index.tsx +21 -283
- package/src/features/MCPPluginDetail/Deployment/index.tsx +1 -1
- package/src/features/MCPPluginDetail/Schema/Prompts.tsx +1 -1
- package/src/features/MCPPluginDetail/Schema/Tools.tsx +1 -1
- package/src/features/ProfileEditor/AgentTool.tsx +14 -20
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +0 -8
- package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/NoteFileItem.tsx +1 -1
- package/src/features/ResourceManager/index.tsx +1 -1
- package/src/features/ShareModal/SharePdf/PdfPreview.tsx +4 -4
- package/src/features/SkillStore/LobeHubList/index.tsx +50 -87
- package/src/features/SkillStore/Search/index.tsx +1 -1
- package/src/features/SkillStore/{Content.tsx → SkillStoreContent.tsx} +3 -8
- package/src/features/SkillStore/index.tsx +15 -33
- package/src/features/User/UserPanel/PanelContent.tsx +0 -8
- package/src/features/User/__tests__/PanelContent.test.tsx +1 -35
- package/src/features/User/__tests__/UserAvatar.test.tsx +30 -57
- package/src/features/User/__tests__/useMenu.test.tsx +2 -43
- package/src/layout/AuthProvider/index.tsx +0 -5
- package/src/libs/next/config/define-config.ts +20 -15
- package/src/libs/next/proxy/createRouteMatcher.test.ts +121 -0
- package/src/libs/next/proxy/createRouteMatcher.ts +18 -0
- package/src/libs/next/proxy/define-config.ts +4 -53
- package/src/libs/next-auth/adapter/index.ts +1 -2
- package/src/libs/oidc-provider/provider.test.ts +5 -316
- package/src/libs/pdfjs/pdf.worker.ts +1 -0
- package/src/libs/pdfjs/worker.ts +12 -0
- package/src/libs/trpc/lambda/context.test.ts +0 -13
- package/src/libs/trpc/lambda/context.ts +3 -22
- package/src/libs/trpc/middleware/userAuth.ts +2 -4
- package/src/libs/trusted-client/getSessionUser.ts +2 -17
- package/src/locales/default/error.ts +0 -6
- package/src/locales/default/index.ts +0 -2
- package/src/proxy.ts +0 -1
- package/src/server/routers/lambda/__tests__/user.test.ts +0 -71
- package/src/server/routers/lambda/user.ts +6 -63
- package/src/server/services/changelog/index.test.ts +3 -2
- package/src/server/services/changelog/index.ts +1 -1
- package/src/server/services/user/index.ts +0 -83
- package/src/services/chat/index.ts +1 -2
- package/src/services/chat/mecha/agentConfigResolver.test.ts +43 -0
- package/src/services/chat/mecha/agentConfigResolver.ts +3 -1
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +58 -14
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +10 -2
- package/src/store/user/slices/auth/action.test.ts +1 -81
- package/src/store/user/slices/auth/action.ts +3 -28
- package/src/store/user/slices/auth/initialState.ts +1 -18
- package/src/store/user/slices/auth/selectors.test.ts +2 -127
- package/src/store/user/slices/auth/selectors.ts +1 -21
- package/src/utils/errorResponse.ts +1 -4
- package/src/utils/markdownToTxt.ts +20 -0
- package/locales/ar/clerk.json +0 -545
- package/locales/bg-BG/clerk.json +0 -545
- package/locales/de-DE/clerk.json +0 -545
- package/locales/en-US/clerk.json +0 -545
- package/locales/es-ES/clerk.json +0 -545
- package/locales/fa-IR/clerk.json +0 -545
- package/locales/fr-FR/clerk.json +0 -545
- package/locales/it-IT/clerk.json +0 -545
- package/locales/ja-JP/clerk.json +0 -545
- package/locales/ko-KR/clerk.json +0 -545
- package/locales/nl-NL/clerk.json +0 -545
- package/locales/pl-PL/clerk.json +0 -545
- package/locales/pt-BR/clerk.json +0 -545
- package/locales/ru-RU/clerk.json +0 -545
- package/locales/tr-TR/clerk.json +0 -545
- package/locales/vi-VN/clerk.json +0 -545
- package/locales/zh-CN/clerk.json +0 -545
- package/locales/zh-TW/clerk.json +0 -545
- package/src/app/(backend)/api/webhooks/clerk/__tests__/fixtures/createUser.json +0 -73
- package/src/app/(backend)/api/webhooks/clerk/route.ts +0 -95
- package/src/app/(backend)/api/webhooks/clerk/validateRequest.ts +0 -22
- package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +0 -27
- package/src/app/[variants]/(main)/settings/security/features/ClerkProfile.tsx +0 -67
- package/src/features/Conversation/ChatList/components/AutoScroll.tsx +0 -25
- package/src/layout/AuthProvider/Clerk/UserUpdater.tsx +0 -40
- package/src/layout/AuthProvider/Clerk/index.tsx +0 -54
- package/src/layout/AuthProvider/Clerk/useAppearance.ts +0 -133
- package/src/libs/clerk-auth/index.test.ts +0 -216
- package/src/libs/clerk-auth/index.ts +0 -80
- package/src/locales/default/clerk.ts +0 -677
- package/src/server/services/user/index.test.ts +0 -220
|
@@ -446,18 +446,20 @@ describe('StreamingExecutor actions', () => {
|
|
|
446
446
|
});
|
|
447
447
|
|
|
448
448
|
describe('effectiveAgentId for group orchestration', () => {
|
|
449
|
-
it('should
|
|
449
|
+
it('should use subAgentId as agentId when groupId is present (group orchestration)', async () => {
|
|
450
450
|
const { result } = renderHook(() => useChatStore());
|
|
451
451
|
const messages = [createMockMessage({ role: 'user' })];
|
|
452
452
|
const supervisorAgentId = 'supervisor-agent-id';
|
|
453
453
|
const subAgentId = 'sub-agent-id';
|
|
454
|
+
const groupId = 'test-group-id';
|
|
454
455
|
|
|
455
|
-
// Create operation with
|
|
456
|
+
// Create operation with groupId and subAgentId (group orchestration scenario)
|
|
456
457
|
const { operationId } = result.current.startOperation({
|
|
457
458
|
type: 'execAgentRuntime',
|
|
458
459
|
context: {
|
|
459
460
|
agentId: supervisorAgentId,
|
|
460
461
|
subAgentId: subAgentId,
|
|
462
|
+
groupId: groupId, // groupId present = group orchestration
|
|
461
463
|
topicId: TEST_IDS.TOPIC_ID,
|
|
462
464
|
messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
|
|
463
465
|
},
|
|
@@ -484,14 +486,12 @@ describe('StreamingExecutor actions', () => {
|
|
|
484
486
|
});
|
|
485
487
|
});
|
|
486
488
|
|
|
487
|
-
//
|
|
488
|
-
// - agentId param is for context/tracing (supervisor ID)
|
|
489
|
-
// - resolvedAgentConfig contains the sub-agent's config (passed in by caller)
|
|
489
|
+
// In group orchestration (groupId present), subAgentId should be used as agentId
|
|
490
490
|
expect(streamSpy).toHaveBeenCalledWith(
|
|
491
491
|
expect.objectContaining({
|
|
492
492
|
params: expect.objectContaining({
|
|
493
|
-
agentId:
|
|
494
|
-
resolvedAgentConfig: subAgentConfig,
|
|
493
|
+
agentId: subAgentId, // subAgentId used for context injection in group orchestration
|
|
494
|
+
resolvedAgentConfig: subAgentConfig,
|
|
495
495
|
}),
|
|
496
496
|
}),
|
|
497
497
|
);
|
|
@@ -499,6 +499,52 @@ describe('StreamingExecutor actions', () => {
|
|
|
499
499
|
streamSpy.mockRestore();
|
|
500
500
|
});
|
|
501
501
|
|
|
502
|
+
it('should use agentId when subAgentId is present but groupId is not (non-group scenario)', async () => {
|
|
503
|
+
const { result } = renderHook(() => useChatStore());
|
|
504
|
+
const messages = [createMockMessage({ role: 'user' })];
|
|
505
|
+
const agentId = 'normal-agent-id';
|
|
506
|
+
const subAgentId = 'sub-agent-id';
|
|
507
|
+
|
|
508
|
+
// Create operation with subAgentId but NO groupId (not a group orchestration scenario)
|
|
509
|
+
const { operationId } = result.current.startOperation({
|
|
510
|
+
type: 'execAgentRuntime',
|
|
511
|
+
context: {
|
|
512
|
+
agentId: agentId,
|
|
513
|
+
subAgentId: subAgentId, // subAgentId present but no groupId
|
|
514
|
+
topicId: TEST_IDS.TOPIC_ID,
|
|
515
|
+
messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
|
|
516
|
+
},
|
|
517
|
+
label: 'Test Non-Group with SubAgentId',
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const streamSpy = vi
|
|
521
|
+
.spyOn(chatService, 'createAssistantMessageStream')
|
|
522
|
+
.mockImplementation(async ({ onFinish }) => {
|
|
523
|
+
await onFinish?.(TEST_CONTENT.AI_RESPONSE, {});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await act(async () => {
|
|
527
|
+
await result.current.internal_fetchAIChatMessage({
|
|
528
|
+
messages,
|
|
529
|
+
messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
|
|
530
|
+
model: 'gpt-4o-mini',
|
|
531
|
+
provider: 'openai',
|
|
532
|
+
operationId,
|
|
533
|
+
agentConfig: createMockResolvedAgentConfig(),
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Without groupId, should use agentId even if subAgentId is present
|
|
538
|
+
expect(streamSpy).toHaveBeenCalledWith(
|
|
539
|
+
expect.objectContaining({
|
|
540
|
+
// agentId used since no groupId
|
|
541
|
+
params: expect.objectContaining({ agentId }),
|
|
542
|
+
}),
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
streamSpy.mockRestore();
|
|
546
|
+
});
|
|
547
|
+
|
|
502
548
|
it('should pass agentId to chatService when no subAgentId is set (normal chat)', async () => {
|
|
503
549
|
const { result } = renderHook(() => useChatStore());
|
|
504
550
|
const messages = [createMockMessage({ role: 'user' })];
|
|
@@ -545,7 +591,7 @@ describe('StreamingExecutor actions', () => {
|
|
|
545
591
|
streamSpy.mockRestore();
|
|
546
592
|
});
|
|
547
593
|
|
|
548
|
-
it('should pass resolvedAgentConfig through chatService
|
|
594
|
+
it('should pass resolvedAgentConfig through chatService in group orchestration speak scenario', async () => {
|
|
549
595
|
const { result } = renderHook(() => useChatStore());
|
|
550
596
|
const messages = [createMockMessage({ role: 'user' })];
|
|
551
597
|
const supervisorAgentId = 'supervisor-agent-id';
|
|
@@ -588,15 +634,13 @@ describe('StreamingExecutor actions', () => {
|
|
|
588
634
|
});
|
|
589
635
|
});
|
|
590
636
|
|
|
591
|
-
//
|
|
592
|
-
// The
|
|
593
|
-
// The speaking agent's config is ensured by the caller (internal_createAgentState)
|
|
594
|
-
// resolving config with subAgentId and passing it as agentConfig param.
|
|
637
|
+
// In group orchestration (groupId present), subAgentId is used as agentId for context injection
|
|
638
|
+
// The speaking agent's config is passed via resolvedAgentConfig
|
|
595
639
|
expect(streamSpy).toHaveBeenCalledWith(
|
|
596
640
|
expect.objectContaining({
|
|
597
641
|
params: expect.objectContaining({
|
|
598
|
-
//
|
|
599
|
-
agentId:
|
|
642
|
+
// subAgentId used as agentId in group orchestration
|
|
643
|
+
agentId: subAgentId,
|
|
600
644
|
// resolvedAgentConfig contains the speaking agent's config
|
|
601
645
|
resolvedAgentConfig: speakingAgentConfig,
|
|
602
646
|
}),
|
|
@@ -344,13 +344,21 @@ export const streamingExecutor: StateCreator<
|
|
|
344
344
|
log('[internal_fetchAIChatMessage] ERROR: Operation not found: %s', operationId);
|
|
345
345
|
throw new Error(`Operation not found: ${operationId}`);
|
|
346
346
|
}
|
|
347
|
-
agentId = operation.context.agentId!;
|
|
348
|
-
subAgentId = operation.context.subAgentId;
|
|
349
347
|
topicId = operation.context.topicId;
|
|
350
348
|
threadId = operation.context.threadId ?? undefined;
|
|
351
349
|
groupId = operation.context.groupId;
|
|
352
350
|
scope = operation.context.scope;
|
|
351
|
+
subAgentId = operation.context.subAgentId;
|
|
353
352
|
abortController = operation.abortController; // 👈 Use operation's abortController
|
|
353
|
+
|
|
354
|
+
// In group orchestration scenarios (has groupId), subAgentId is the actual responding agent
|
|
355
|
+
// Use it for context injection instead of the session agentId
|
|
356
|
+
if (groupId && subAgentId) {
|
|
357
|
+
agentId = subAgentId;
|
|
358
|
+
} else {
|
|
359
|
+
agentId = operation.context.agentId!;
|
|
360
|
+
}
|
|
361
|
+
|
|
354
362
|
log(
|
|
355
363
|
'[internal_fetchAIChatMessage] get context from operation %s: agentId=%s, subAgentId=%s, topicId=%s, groupId=%s, aborted=%s',
|
|
356
364
|
operationId,
|
|
@@ -16,26 +16,18 @@ vi.mock('@/libs/swr', async () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
// Use vi.hoisted to ensure variables exist before vi.mock factory executes
|
|
19
|
-
const {
|
|
20
|
-
enableClerk: { value: false },
|
|
19
|
+
const { enableNextAuth, enableBetterAuth } = vi.hoisted(() => ({
|
|
21
20
|
enableNextAuth: { value: false },
|
|
22
21
|
enableBetterAuth: { value: false },
|
|
23
|
-
enableAuth: { value: true },
|
|
24
22
|
}));
|
|
25
23
|
|
|
26
24
|
vi.mock('@/envs/auth', () => ({
|
|
27
|
-
get enableClerk() {
|
|
28
|
-
return enableClerk.value;
|
|
29
|
-
},
|
|
30
25
|
get enableNextAuth() {
|
|
31
26
|
return enableNextAuth.value;
|
|
32
27
|
},
|
|
33
28
|
get enableBetterAuth() {
|
|
34
29
|
return enableBetterAuth.value;
|
|
35
30
|
},
|
|
36
|
-
get enableAuth() {
|
|
37
|
-
return enableAuth.value;
|
|
38
|
-
},
|
|
39
31
|
}));
|
|
40
32
|
|
|
41
33
|
const mockUserService = vi.hoisted(() => ({
|
|
@@ -59,9 +51,7 @@ afterEach(() => {
|
|
|
59
51
|
vi.clearAllMocks();
|
|
60
52
|
|
|
61
53
|
enableNextAuth.value = false;
|
|
62
|
-
enableClerk.value = false;
|
|
63
54
|
enableBetterAuth.value = false;
|
|
64
|
-
enableAuth.value = true;
|
|
65
55
|
|
|
66
56
|
// Reset store state
|
|
67
57
|
useUserStore.setState({
|
|
@@ -95,34 +85,6 @@ describe('createAuthSlice', () => {
|
|
|
95
85
|
});
|
|
96
86
|
|
|
97
87
|
describe('logout', () => {
|
|
98
|
-
it('should call clerkSignOut when Clerk is enabled', async () => {
|
|
99
|
-
enableClerk.value = true;
|
|
100
|
-
|
|
101
|
-
const clerkSignOutMock = vi.fn();
|
|
102
|
-
useUserStore.setState({ clerkSignOut: clerkSignOutMock });
|
|
103
|
-
|
|
104
|
-
const { result } = renderHook(() => useUserStore());
|
|
105
|
-
|
|
106
|
-
await act(async () => {
|
|
107
|
-
await result.current.logout();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
expect(clerkSignOutMock).toHaveBeenCalled();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should not call clerkSignOut when Clerk is disabled', async () => {
|
|
114
|
-
const clerkSignOutMock = vi.fn();
|
|
115
|
-
useUserStore.setState({ clerkSignOut: clerkSignOutMock });
|
|
116
|
-
|
|
117
|
-
const { result } = renderHook(() => useUserStore());
|
|
118
|
-
|
|
119
|
-
await act(async () => {
|
|
120
|
-
await result.current.logout();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
expect(clerkSignOutMock).not.toHaveBeenCalled();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
88
|
it('should call next-auth signOut when NextAuth is enabled', async () => {
|
|
127
89
|
enableNextAuth.value = true;
|
|
128
90
|
|
|
@@ -152,32 +114,6 @@ describe('createAuthSlice', () => {
|
|
|
152
114
|
});
|
|
153
115
|
|
|
154
116
|
describe('openLogin', () => {
|
|
155
|
-
it('should call clerkSignIn when Clerk is enabled', async () => {
|
|
156
|
-
enableClerk.value = true;
|
|
157
|
-
const clerkSignInMock = vi.fn();
|
|
158
|
-
useUserStore.setState({ clerkSignIn: clerkSignInMock });
|
|
159
|
-
|
|
160
|
-
const { result } = renderHook(() => useUserStore());
|
|
161
|
-
|
|
162
|
-
await act(async () => {
|
|
163
|
-
await result.current.openLogin();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
expect(clerkSignInMock).toHaveBeenCalled();
|
|
167
|
-
});
|
|
168
|
-
it('should not call clerkSignIn when Clerk is disabled', async () => {
|
|
169
|
-
const clerkSignInMock = vi.fn();
|
|
170
|
-
useUserStore.setState({ clerkSignIn: clerkSignInMock });
|
|
171
|
-
|
|
172
|
-
const { result } = renderHook(() => useUserStore());
|
|
173
|
-
|
|
174
|
-
await act(async () => {
|
|
175
|
-
await result.current.openLogin();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
expect(clerkSignInMock).not.toHaveBeenCalled();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
117
|
it('should call next-auth signIn when NextAuth is enabled', async () => {
|
|
182
118
|
enableNextAuth.value = true;
|
|
183
119
|
|
|
@@ -269,22 +205,6 @@ describe('createAuthSlice', () => {
|
|
|
269
205
|
});
|
|
270
206
|
});
|
|
271
207
|
|
|
272
|
-
describe('enableAuth', () => {
|
|
273
|
-
it('should return true when auth is enabled', () => {
|
|
274
|
-
enableAuth.value = true;
|
|
275
|
-
const { result } = renderHook(() => useUserStore());
|
|
276
|
-
|
|
277
|
-
expect(result.current.enableAuth()).toBe(true);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('should return false when auth is disabled', () => {
|
|
281
|
-
enableAuth.value = false;
|
|
282
|
-
const { result } = renderHook(() => useUserStore());
|
|
283
|
-
|
|
284
|
-
expect(result.current.enableAuth()).toBe(false);
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
208
|
describe('fetchAuthProviders', () => {
|
|
289
209
|
it('should skip fetching if already loaded', async () => {
|
|
290
210
|
useUserStore.setState({ isLoadedAuthProviders: true });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type SSOProvider } from '@lobechat/types';
|
|
2
2
|
import { type StateCreator } from 'zustand/vanilla';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
|
|
5
5
|
import { userService } from '@/services/user';
|
|
6
6
|
|
|
7
7
|
import type { UserStore } from '../../store';
|
|
@@ -12,7 +12,6 @@ interface AuthProvidersData {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface UserAuthAction {
|
|
15
|
-
enableAuth: () => boolean;
|
|
16
15
|
/**
|
|
17
16
|
* Fetch auth providers (SSO accounts) for the current user
|
|
18
17
|
*/
|
|
@@ -66,9 +65,6 @@ export const createAuthSlice: StateCreator<
|
|
|
66
65
|
[],
|
|
67
66
|
UserAuthAction
|
|
68
67
|
> = (set, get) => ({
|
|
69
|
-
enableAuth: () => {
|
|
70
|
-
return enableAuth;
|
|
71
|
-
},
|
|
72
68
|
fetchAuthProviders: async () => {
|
|
73
69
|
// Skip if already loaded
|
|
74
70
|
if (get().isLoadedAuthProviders) return;
|
|
@@ -82,12 +78,6 @@ export const createAuthSlice: StateCreator<
|
|
|
82
78
|
}
|
|
83
79
|
},
|
|
84
80
|
logout: async () => {
|
|
85
|
-
if (enableClerk) {
|
|
86
|
-
get().clerkSignOut?.({ redirectUrl: location.toString() });
|
|
87
|
-
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
81
|
if (enableBetterAuth) {
|
|
92
82
|
const { signOut } = await import('@/libs/better-auth/auth-client');
|
|
93
83
|
await signOut({
|
|
@@ -109,24 +99,9 @@ export const createAuthSlice: StateCreator<
|
|
|
109
99
|
}
|
|
110
100
|
},
|
|
111
101
|
openLogin: async () => {
|
|
112
|
-
// Skip if already on a login page
|
|
102
|
+
// Skip if already on a Better Auth login page (/signin, /signup)
|
|
113
103
|
const pathname = location.pathname;
|
|
114
|
-
if (
|
|
115
|
-
pathname.startsWith('/signin') ||
|
|
116
|
-
pathname.startsWith('/signup') ||
|
|
117
|
-
pathname.startsWith('/login')
|
|
118
|
-
) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (enableClerk) {
|
|
123
|
-
const redirectUrl = location.toString();
|
|
124
|
-
get().clerkSignIn?.({
|
|
125
|
-
fallbackRedirectUrl: redirectUrl,
|
|
126
|
-
signUpForceRedirectUrl: redirectUrl,
|
|
127
|
-
signUpUrl: '/signup',
|
|
128
|
-
});
|
|
129
|
-
|
|
104
|
+
if (pathname.startsWith('/signin') || pathname.startsWith('/signup')) {
|
|
130
105
|
return;
|
|
131
106
|
}
|
|
132
107
|
|
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
import { type Session, type User } from '@auth/core/types';
|
|
2
|
-
import {
|
|
3
|
-
type SignInProps,
|
|
4
|
-
type SignOut,
|
|
5
|
-
type SignedInSessionResource,
|
|
6
|
-
type UserProfileProps,
|
|
7
|
-
type UserResource,
|
|
8
|
-
} from '@clerk/types';
|
|
9
2
|
import { type SSOProvider } from '@lobechat/types';
|
|
10
3
|
|
|
11
|
-
import { enableClerk } from '@/envs/auth';
|
|
12
4
|
import { type LobeUser } from '@/types/user';
|
|
13
5
|
|
|
14
6
|
export interface UserAuthState {
|
|
15
7
|
authProviders?: SSOProvider[];
|
|
16
|
-
clerkOpenUserProfile?: (props?: UserProfileProps) => void;
|
|
17
|
-
clerkSession?: SignedInSessionResource;
|
|
18
|
-
|
|
19
|
-
clerkSignIn?: (props?: SignInProps) => void;
|
|
20
|
-
clerkSignOut?: SignOut;
|
|
21
|
-
clerkUser?: UserResource;
|
|
22
8
|
/**
|
|
23
9
|
* Whether user registered with email/password (credential login)
|
|
24
10
|
*/
|
|
@@ -33,7 +19,4 @@ export interface UserAuthState {
|
|
|
33
19
|
user?: LobeUser;
|
|
34
20
|
}
|
|
35
21
|
|
|
36
|
-
export const initialAuthState: UserAuthState = {
|
|
37
|
-
// Clerk doesn't need to fetch auth providers
|
|
38
|
-
isLoadedAuthProviders: enableClerk ? true : undefined,
|
|
39
|
-
};
|
|
22
|
+
export const initialAuthState: UserAuthState = {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { BRANDING_NAME } from '@lobechat/business-const';
|
|
2
1
|
import { t } from 'i18next';
|
|
3
2
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
3
|
|
|
@@ -10,62 +9,16 @@ vi.mock('i18next', () => ({
|
|
|
10
9
|
t: vi.fn((key) => key),
|
|
11
10
|
}));
|
|
12
11
|
|
|
13
|
-
// 定义一个变量来存储 enableAuth 的值
|
|
14
|
-
let enableAuth = true;
|
|
15
|
-
let isDesktop = false;
|
|
16
|
-
|
|
17
|
-
// 模拟 @/const/auth 模块
|
|
18
|
-
vi.mock('@/envs/auth', () => ({
|
|
19
|
-
get enableAuth() {
|
|
20
|
-
return enableAuth;
|
|
21
|
-
},
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
// 模拟 @/const/version 模块
|
|
25
|
-
vi.mock('@/const/version', () => ({
|
|
26
|
-
get isDesktop() {
|
|
27
|
-
return isDesktop;
|
|
28
|
-
},
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
12
|
afterEach(() => {
|
|
32
|
-
|
|
33
|
-
isDesktop = false;
|
|
13
|
+
vi.clearAllMocks();
|
|
34
14
|
});
|
|
35
15
|
|
|
36
16
|
describe('userProfileSelectors', () => {
|
|
37
17
|
describe('displayUserName', () => {
|
|
38
|
-
it('should return default username when auth is disabled and not desktop', () => {
|
|
39
|
-
enableAuth = false;
|
|
40
|
-
isDesktop = false;
|
|
41
|
-
|
|
42
|
-
const store: UserStore = {
|
|
43
|
-
isSignedIn: false,
|
|
44
|
-
user: null,
|
|
45
|
-
enableAuth: () => false,
|
|
46
|
-
} as unknown as UserStore;
|
|
47
|
-
|
|
48
|
-
expect(userProfileSelectors.displayUserName(store)).toBe(BRANDING_NAME);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should return user username when auth is disabled and is desktop', () => {
|
|
52
|
-
enableAuth = false;
|
|
53
|
-
isDesktop = true;
|
|
54
|
-
|
|
55
|
-
const store: UserStore = {
|
|
56
|
-
isSignedIn: false,
|
|
57
|
-
user: { username: 'johndoe' },
|
|
58
|
-
enableAuth: () => false,
|
|
59
|
-
} as unknown as UserStore;
|
|
60
|
-
|
|
61
|
-
expect(userProfileSelectors.displayUserName(store)).toBe('johndoe');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
18
|
it('should return user username when signed in', () => {
|
|
65
19
|
const store: UserStore = {
|
|
66
20
|
isSignedIn: true,
|
|
67
21
|
user: { username: 'johndoe' },
|
|
68
|
-
enableAuth: () => true,
|
|
69
22
|
} as UserStore;
|
|
70
23
|
|
|
71
24
|
expect(userProfileSelectors.displayUserName(store)).toBe('johndoe');
|
|
@@ -75,7 +28,6 @@ describe('userProfileSelectors', () => {
|
|
|
75
28
|
const store: UserStore = {
|
|
76
29
|
isSignedIn: true,
|
|
77
30
|
user: { email: 'demo@lobehub.com' },
|
|
78
|
-
enableAuth: () => true,
|
|
79
31
|
} as UserStore;
|
|
80
32
|
|
|
81
33
|
expect(userProfileSelectors.displayUserName(store)).toBe('demo@lobehub.com');
|
|
@@ -83,7 +35,6 @@ describe('userProfileSelectors', () => {
|
|
|
83
35
|
|
|
84
36
|
it('should return "anonymous" when not signed in', () => {
|
|
85
37
|
const store: UserStore = {
|
|
86
|
-
enableAuth: () => true,
|
|
87
38
|
isSignedIn: false,
|
|
88
39
|
user: null,
|
|
89
40
|
} as unknown as UserStore;
|
|
@@ -129,40 +80,10 @@ describe('userProfileSelectors', () => {
|
|
|
129
80
|
});
|
|
130
81
|
|
|
131
82
|
describe('nickName', () => {
|
|
132
|
-
it('should return default nickname when auth is disabled and not desktop', () => {
|
|
133
|
-
enableAuth = false;
|
|
134
|
-
isDesktop = false;
|
|
135
|
-
|
|
136
|
-
const store: UserStore = {
|
|
137
|
-
isSignedIn: false,
|
|
138
|
-
user: null,
|
|
139
|
-
enableAuth: () => false,
|
|
140
|
-
} as unknown as UserStore;
|
|
141
|
-
|
|
142
|
-
expect(userProfileSelectors.nickName(store)).toBe('userPanel.defaultNickname');
|
|
143
|
-
expect(t).toHaveBeenCalledWith('userPanel.defaultNickname', { ns: 'common' });
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should return user fullName when auth is disabled and is desktop', () => {
|
|
147
|
-
enableAuth = false;
|
|
148
|
-
isDesktop = true;
|
|
149
|
-
|
|
150
|
-
const store: UserStore = {
|
|
151
|
-
isSignedIn: false,
|
|
152
|
-
user: { fullName: 'John Doe' },
|
|
153
|
-
enableAuth: () => false,
|
|
154
|
-
} as unknown as UserStore;
|
|
155
|
-
|
|
156
|
-
expect(userProfileSelectors.nickName(store)).toBe('John Doe');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
83
|
it('should return user fullName when signed in', () => {
|
|
160
|
-
enableAuth = true;
|
|
161
|
-
|
|
162
84
|
const store: UserStore = {
|
|
163
85
|
isSignedIn: true,
|
|
164
86
|
user: { fullName: 'John Doe' },
|
|
165
|
-
enableAuth: () => true,
|
|
166
87
|
} as UserStore;
|
|
167
88
|
|
|
168
89
|
expect(userProfileSelectors.nickName(store)).toBe('John Doe');
|
|
@@ -172,17 +93,13 @@ describe('userProfileSelectors', () => {
|
|
|
172
93
|
const store: UserStore = {
|
|
173
94
|
isSignedIn: true,
|
|
174
95
|
user: { username: 'johndoe' },
|
|
175
|
-
enableAuth: () => true,
|
|
176
96
|
} as UserStore;
|
|
177
97
|
|
|
178
98
|
expect(userProfileSelectors.nickName(store)).toBe('johndoe');
|
|
179
99
|
});
|
|
180
100
|
|
|
181
101
|
it('should return anonymous nickname when not signed in', () => {
|
|
182
|
-
enableAuth = true;
|
|
183
|
-
|
|
184
102
|
const store: UserStore = {
|
|
185
|
-
enableAuth: () => true,
|
|
186
103
|
isSignedIn: false,
|
|
187
104
|
user: null,
|
|
188
105
|
} as unknown as UserStore;
|
|
@@ -193,37 +110,10 @@ describe('userProfileSelectors', () => {
|
|
|
193
110
|
});
|
|
194
111
|
|
|
195
112
|
describe('username', () => {
|
|
196
|
-
it('should return default username when auth is disabled and not desktop', () => {
|
|
197
|
-
enableAuth = false;
|
|
198
|
-
isDesktop = false;
|
|
199
|
-
|
|
200
|
-
const store: UserStore = {
|
|
201
|
-
isSignedIn: false,
|
|
202
|
-
user: null,
|
|
203
|
-
enableAuth: () => false,
|
|
204
|
-
} as unknown as UserStore;
|
|
205
|
-
|
|
206
|
-
expect(userProfileSelectors.username(store)).toBe(BRANDING_NAME);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('should return user username when auth is disabled and is desktop', () => {
|
|
210
|
-
enableAuth = false;
|
|
211
|
-
isDesktop = true;
|
|
212
|
-
|
|
213
|
-
const store: UserStore = {
|
|
214
|
-
isSignedIn: false,
|
|
215
|
-
user: { username: 'johndoe' },
|
|
216
|
-
enableAuth: () => false,
|
|
217
|
-
} as unknown as UserStore;
|
|
218
|
-
|
|
219
|
-
expect(userProfileSelectors.username(store)).toBe('johndoe');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
113
|
it('should return user username when signed in', () => {
|
|
223
114
|
const store: UserStore = {
|
|
224
115
|
isSignedIn: true,
|
|
225
116
|
user: { username: 'johndoe' },
|
|
226
|
-
enableAuth: () => true,
|
|
227
117
|
} as UserStore;
|
|
228
118
|
|
|
229
119
|
expect(userProfileSelectors.username(store)).toBe('johndoe');
|
|
@@ -231,7 +121,6 @@ describe('userProfileSelectors', () => {
|
|
|
231
121
|
|
|
232
122
|
it('should return "anonymous" when not signed in', () => {
|
|
233
123
|
const store: UserStore = {
|
|
234
|
-
enableAuth: () => true,
|
|
235
124
|
isSignedIn: false,
|
|
236
125
|
user: null,
|
|
237
126
|
} as unknown as UserStore;
|
|
@@ -243,31 +132,17 @@ describe('userProfileSelectors', () => {
|
|
|
243
132
|
|
|
244
133
|
describe('authSelectors', () => {
|
|
245
134
|
describe('isLogin', () => {
|
|
246
|
-
it('should return false when not signed in (regardless of auth enabled state)', () => {
|
|
247
|
-
enableAuth = false;
|
|
248
|
-
|
|
249
|
-
const store: UserStore = {
|
|
250
|
-
isSignedIn: false,
|
|
251
|
-
enableAuth: () => false,
|
|
252
|
-
} as UserStore;
|
|
253
|
-
|
|
254
|
-
// isLogin now only checks isSignedIn, not enableAuth
|
|
255
|
-
expect(authSelectors.isLogin(store)).toBe(false);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
135
|
it('should return true when signed in', () => {
|
|
259
136
|
const store: UserStore = {
|
|
260
137
|
isSignedIn: true,
|
|
261
|
-
enableAuth: () => true,
|
|
262
138
|
} as UserStore;
|
|
263
139
|
|
|
264
140
|
expect(authSelectors.isLogin(store)).toBe(true);
|
|
265
141
|
});
|
|
266
142
|
|
|
267
|
-
it('should return false when not signed in
|
|
143
|
+
it('should return false when not signed in', () => {
|
|
268
144
|
const store: UserStore = {
|
|
269
145
|
isSignedIn: false,
|
|
270
|
-
enableAuth: () => true,
|
|
271
146
|
} as UserStore;
|
|
272
147
|
|
|
273
148
|
expect(authSelectors.isLogin(store)).toBe(false);
|
|
@@ -1,36 +1,17 @@
|
|
|
1
|
-
import { BRANDING_NAME } from '@lobechat/business-const';
|
|
2
|
-
import { isDesktop } from '@lobechat/const';
|
|
3
1
|
import { type LobeUser, type SSOProvider } from '@lobechat/types';
|
|
4
2
|
import { t } from 'i18next';
|
|
5
3
|
|
|
6
|
-
import {
|
|
4
|
+
import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
|
|
7
5
|
import type { UserStore } from '@/store/user';
|
|
8
6
|
|
|
9
|
-
const DEFAULT_USERNAME = BRANDING_NAME;
|
|
10
|
-
|
|
11
7
|
const nickName = (s: UserStore) => {
|
|
12
8
|
const defaultNickName = s.user?.fullName || s.user?.username;
|
|
13
|
-
if (!enableAuth) {
|
|
14
|
-
if (isDesktop) {
|
|
15
|
-
return defaultNickName;
|
|
16
|
-
}
|
|
17
|
-
return t('userPanel.defaultNickname', { ns: 'common' });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
9
|
if (s.isSignedIn) return defaultNickName;
|
|
21
10
|
|
|
22
11
|
return t('userPanel.anonymousNickName', { ns: 'common' });
|
|
23
12
|
};
|
|
24
13
|
|
|
25
14
|
const username = (s: UserStore) => {
|
|
26
|
-
if (!enableAuth) {
|
|
27
|
-
if (isDesktop) {
|
|
28
|
-
return s.user?.username;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return DEFAULT_USERNAME;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
15
|
if (s.isSignedIn) return s.user?.username;
|
|
35
16
|
|
|
36
17
|
return 'anonymous';
|
|
@@ -57,6 +38,5 @@ export const authSelectors = {
|
|
|
57
38
|
isLogin: (s: UserStore) => s.isSignedIn,
|
|
58
39
|
isLoginWithAuth: (s: UserStore) => s.isSignedIn,
|
|
59
40
|
isLoginWithBetterAuth: (s: UserStore): boolean => (s.isSignedIn && enableBetterAuth) || false,
|
|
60
|
-
isLoginWithClerk: (s: UserStore): boolean => (s.isSignedIn && enableClerk) || false,
|
|
61
41
|
isLoginWithNextAuth: (s: UserStore): boolean => (s.isSignedIn && !!enableNextAuth) || false,
|
|
62
42
|
};
|
|
@@ -6,10 +6,7 @@ import { ChatErrorType, type ErrorResponse, type ErrorType } from '@lobechat/typ
|
|
|
6
6
|
* When these errors occur, the response will include X-Auth-Required header
|
|
7
7
|
* to signal the client that re-authentication is needed.
|
|
8
8
|
*/
|
|
9
|
-
const AUTH_REQUIRED_ERROR_TYPES = new Set<ErrorType>([
|
|
10
|
-
ChatErrorType.Unauthorized,
|
|
11
|
-
ChatErrorType.InvalidClerkUser,
|
|
12
|
-
]);
|
|
9
|
+
const AUTH_REQUIRED_ERROR_TYPES = new Set<ErrorType>([ChatErrorType.Unauthorized]);
|
|
13
10
|
|
|
14
11
|
const getStatus = (errorType: ILobeAgentRuntimeErrorType | ErrorType) => {
|
|
15
12
|
// InvalidAccessCode / InvalidAzureAPIKey / InvalidOpenAIAPIKey / InvalidZhipuAPIKey ....
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import removeMarkdown from 'remove-markdown';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert markdown into plain text.
|
|
5
|
+
*
|
|
6
|
+
* This is a local wrapper to avoid importing third-party markdown-to-txt directly.
|
|
7
|
+
* It uses `remark` + `strip-markdown` under the hood.
|
|
8
|
+
*/
|
|
9
|
+
export const markdownToTxt = (markdown: string): string => {
|
|
10
|
+
if (!markdown) return '';
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
return removeMarkdown(markdown).trimEnd();
|
|
14
|
+
} catch {
|
|
15
|
+
// Best-effort: fall back to raw input when parsing fails.
|
|
16
|
+
return markdown;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default markdownToTxt;
|