@lobehub/lobehub 2.0.0-next.304 → 2.0.0-next.306
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/.github/workflows/manual-build-desktop.yml +11 -1
- package/CHANGELOG.md +50 -0
- package/apps/desktop/.i18nrc.js +3 -3
- package/apps/desktop/electron.vite.config.ts +0 -2
- package/apps/desktop/resources/locales/ar/dialog.json +5 -1
- package/apps/desktop/resources/locales/ar/menu.json +16 -0
- package/apps/desktop/resources/locales/bg-BG/dialog.json +5 -1
- package/apps/desktop/resources/locales/bg-BG/menu.json +16 -0
- package/apps/desktop/resources/locales/de-DE/dialog.json +5 -1
- package/apps/desktop/resources/locales/de-DE/menu.json +16 -0
- package/apps/desktop/resources/locales/en/common.json +26 -0
- package/apps/desktop/resources/locales/en/dialog.json +27 -0
- package/apps/desktop/resources/locales/en/menu.json +73 -0
- package/apps/desktop/resources/locales/es-ES/dialog.json +5 -1
- package/apps/desktop/resources/locales/es-ES/menu.json +16 -0
- package/apps/desktop/resources/locales/fa-IR/dialog.json +5 -1
- package/apps/desktop/resources/locales/fa-IR/menu.json +16 -0
- package/apps/desktop/resources/locales/fr-FR/dialog.json +5 -1
- package/apps/desktop/resources/locales/fr-FR/menu.json +16 -0
- package/apps/desktop/resources/locales/it-IT/dialog.json +5 -1
- package/apps/desktop/resources/locales/it-IT/menu.json +16 -0
- package/apps/desktop/resources/locales/ja-JP/dialog.json +5 -1
- package/apps/desktop/resources/locales/ja-JP/menu.json +16 -0
- package/apps/desktop/resources/locales/ko-KR/dialog.json +5 -1
- package/apps/desktop/resources/locales/ko-KR/menu.json +16 -0
- package/apps/desktop/resources/locales/nl-NL/dialog.json +5 -1
- package/apps/desktop/resources/locales/nl-NL/menu.json +16 -0
- package/apps/desktop/resources/locales/pl-PL/dialog.json +5 -1
- package/apps/desktop/resources/locales/pl-PL/menu.json +16 -0
- package/apps/desktop/resources/locales/pt-BR/dialog.json +5 -1
- package/apps/desktop/resources/locales/pt-BR/menu.json +16 -0
- package/apps/desktop/resources/locales/ru-RU/dialog.json +5 -1
- package/apps/desktop/resources/locales/ru-RU/menu.json +16 -0
- package/apps/desktop/resources/locales/tr-TR/dialog.json +5 -1
- package/apps/desktop/resources/locales/tr-TR/menu.json +16 -0
- package/apps/desktop/resources/locales/vi-VN/dialog.json +5 -1
- package/apps/desktop/resources/locales/vi-VN/menu.json +16 -0
- package/apps/desktop/resources/locales/zh-TW/dialog.json +5 -1
- package/apps/desktop/resources/locales/zh-TW/menu.json +16 -0
- package/apps/desktop/scripts/update-test/README.md +15 -0
- package/apps/desktop/src/main/core/infrastructure/BackendProxyProtocolManager.ts +7 -6
- package/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts +38 -5
- package/apps/desktop/src/main/utils/logger.ts +2 -2
- package/changelog/v1.json +14 -0
- package/e2e/src/steps/community/detail-pages.steps.ts +3 -1
- package/e2e/src/steps/community/interactions.steps.ts +4 -4
- package/locales/en-US/auth.json +5 -0
- package/locales/zh-CN/auth.json +5 -0
- package/package.json +6 -5
- package/packages/builtin-tool-agent-builder/src/ExecutionRuntime/index.ts +362 -30
- package/packages/builtin-tool-agent-builder/src/client/Intervention/InstallPlugin.tsx +28 -4
- package/packages/context-engine/src/processors/GroupMessageFlatten.ts +9 -6
- package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +103 -0
- package/packages/context-engine/src/providers/GroupAgentBuilderContextInjector.ts +18 -31
- package/packages/context-engine/src/providers/__tests__/GroupAgentBuilderContextInjector.test.ts +307 -0
- package/packages/prompts/src/prompts/userMemory/__snapshots__/index.test.ts.snap +14 -38
- package/packages/prompts/src/prompts/userMemory/index.ts +5 -24
- package/scripts/electronWorkflow/buildDesktopChannel.ts +135 -0
- package/src/app/[variants]/(main)/_layout/index.tsx +2 -0
- package/src/app/[variants]/(main)/community/(detail)/assistant/index.tsx +1 -1
- package/src/app/[variants]/(main)/community/(detail)/mcp/index.tsx +1 -1
- package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/index.tsx +2 -2
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -2
- package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -1
- package/src/features/Conversation/Messages/components/ContentLoading.tsx +8 -2
- package/src/features/DesktopNavigationBridge/index.tsx +0 -9
- package/src/features/Electron/AuthRequiredModal/index.tsx +151 -0
- package/src/locales/default/auth.ts +6 -0
- package/src/services/chat/mecha/agentConfigResolver.ts +65 -0
- package/src/services/chat/mecha/modelParamsResolver.test.ts +211 -0
- package/src/store/agentGroup/action.ts +30 -0
- package/src/store/agentGroup/slices/lifecycle.test.ts +77 -18
- package/src/store/agentGroup/slices/lifecycle.ts +7 -9
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +124 -0
- package/src/store/chat/slices/operation/selectors.ts +22 -0
- package/src/utils/errorResponse.ts +21 -1
|
@@ -800,4 +800,215 @@ describe('resolveModelExtendParams', () => {
|
|
|
800
800
|
expect(modelExtendParamsSpy).toHaveBeenCalledWith('test-model', 'test-provider');
|
|
801
801
|
});
|
|
802
802
|
});
|
|
803
|
+
|
|
804
|
+
describe('parameter precedence and conflicts', () => {
|
|
805
|
+
beforeEach(() => {
|
|
806
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'isModelHasExtendParams').mockReturnValue(
|
|
807
|
+
() => true,
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
describe('reasoning effort variants precedence', () => {
|
|
812
|
+
it('should give precedence to later reasoning effort variants when multiple are configured', () => {
|
|
813
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
814
|
+
'reasoningEffort',
|
|
815
|
+
'gpt5ReasoningEffort',
|
|
816
|
+
'gpt5_1ReasoningEffort',
|
|
817
|
+
]);
|
|
818
|
+
|
|
819
|
+
const result = resolveModelExtendParams({
|
|
820
|
+
chatConfig: {
|
|
821
|
+
gpt5_1ReasoningEffort: 'high',
|
|
822
|
+
gpt5ReasoningEffort: 'medium',
|
|
823
|
+
reasoningEffort: 'low',
|
|
824
|
+
} as any,
|
|
825
|
+
model: 'gpt-5.1',
|
|
826
|
+
provider: 'openai',
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// gpt5_1ReasoningEffort should win as it's processed last
|
|
830
|
+
expect(result.reasoning_effort).toBe('high');
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('should handle mixed reasoning effort variants with only some configured', () => {
|
|
834
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
835
|
+
'reasoningEffort',
|
|
836
|
+
'gpt5ReasoningEffort',
|
|
837
|
+
'gpt5_2ReasoningEffort',
|
|
838
|
+
'gpt5_2ProReasoningEffort',
|
|
839
|
+
]);
|
|
840
|
+
|
|
841
|
+
const result = resolveModelExtendParams({
|
|
842
|
+
chatConfig: {
|
|
843
|
+
gpt5_2ProReasoningEffort: undefined,
|
|
844
|
+
gpt5_2ReasoningEffort: 'medium',
|
|
845
|
+
gpt5ReasoningEffort: undefined,
|
|
846
|
+
reasoningEffort: 'low',
|
|
847
|
+
} as any,
|
|
848
|
+
model: 'gpt-5.2',
|
|
849
|
+
provider: 'openai',
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// gpt5_2ReasoningEffort should be set, others are undefined
|
|
853
|
+
expect(result.reasoning_effort).toBe('medium');
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('should use the last supported variant in processing order', () => {
|
|
857
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
858
|
+
'reasoningEffort',
|
|
859
|
+
'gpt5_2ProReasoningEffort',
|
|
860
|
+
]);
|
|
861
|
+
|
|
862
|
+
const result = resolveModelExtendParams({
|
|
863
|
+
chatConfig: {
|
|
864
|
+
gpt5_2ProReasoningEffort: 'high',
|
|
865
|
+
reasoningEffort: 'low',
|
|
866
|
+
} as any,
|
|
867
|
+
model: 'gpt-5.2-pro',
|
|
868
|
+
provider: 'openai',
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// gpt5_2ProReasoningEffort is processed after reasoningEffort
|
|
872
|
+
expect(result.reasoning_effort).toBe('high');
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
describe('thinking configuration conflicts', () => {
|
|
877
|
+
it('should allow thinking type param to overwrite enableReasoning thinking config', () => {
|
|
878
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
879
|
+
'enableReasoning',
|
|
880
|
+
'thinking',
|
|
881
|
+
]);
|
|
882
|
+
|
|
883
|
+
const result = resolveModelExtendParams({
|
|
884
|
+
chatConfig: {
|
|
885
|
+
enableReasoning: true,
|
|
886
|
+
reasoningBudgetToken: 2048,
|
|
887
|
+
thinking: 'extended',
|
|
888
|
+
} as any,
|
|
889
|
+
model: 'model',
|
|
890
|
+
provider: 'provider',
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// thinking param overwrites enableReasoning's thinking config
|
|
894
|
+
expect(result.thinking).toEqual({
|
|
895
|
+
type: 'extended',
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it('should handle reasoningBudgetToken with thinking type param', () => {
|
|
900
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
901
|
+
'reasoningBudgetToken',
|
|
902
|
+
'thinking',
|
|
903
|
+
]);
|
|
904
|
+
|
|
905
|
+
const result = resolveModelExtendParams({
|
|
906
|
+
chatConfig: {
|
|
907
|
+
reasoningBudgetToken: 4096,
|
|
908
|
+
thinking: 'basic',
|
|
909
|
+
} as any,
|
|
910
|
+
model: 'model',
|
|
911
|
+
provider: 'provider',
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// thinking param should overwrite the entire thinking config
|
|
915
|
+
expect(result.thinking).toEqual({
|
|
916
|
+
type: 'basic',
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it('should combine independent thinking params without conflict', () => {
|
|
921
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
922
|
+
'thinking',
|
|
923
|
+
'thinkingBudget',
|
|
924
|
+
'thinkingLevel',
|
|
925
|
+
]);
|
|
926
|
+
|
|
927
|
+
const result = resolveModelExtendParams({
|
|
928
|
+
chatConfig: {
|
|
929
|
+
thinking: 'enabled',
|
|
930
|
+
thinkingBudget: 5000,
|
|
931
|
+
thinkingLevel: 'advanced',
|
|
932
|
+
} as any,
|
|
933
|
+
model: 'model',
|
|
934
|
+
provider: 'provider',
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// These are independent params and should all be set
|
|
938
|
+
expect(result.thinking).toEqual({ type: 'enabled' });
|
|
939
|
+
expect(result.thinkingBudget).toBe(5000);
|
|
940
|
+
expect(result.thinkingLevel).toBe('advanced');
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
describe('complex multi-parameter scenarios', () => {
|
|
945
|
+
it('should handle all reasoning variants with context caching and verbosity', () => {
|
|
946
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
947
|
+
'enableReasoning',
|
|
948
|
+
'reasoningEffort',
|
|
949
|
+
'gpt5ReasoningEffort',
|
|
950
|
+
'disableContextCaching',
|
|
951
|
+
'textVerbosity',
|
|
952
|
+
]);
|
|
953
|
+
|
|
954
|
+
const result = resolveModelExtendParams({
|
|
955
|
+
chatConfig: {
|
|
956
|
+
disableContextCaching: true,
|
|
957
|
+
enableReasoning: true,
|
|
958
|
+
gpt5ReasoningEffort: 'high',
|
|
959
|
+
reasoningBudgetToken: 3000,
|
|
960
|
+
reasoningEffort: 'medium',
|
|
961
|
+
textVerbosity: 'verbose',
|
|
962
|
+
} as any,
|
|
963
|
+
model: 'gpt-5',
|
|
964
|
+
provider: 'openai',
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
expect(result).toEqual({
|
|
968
|
+
enabledContextCaching: false,
|
|
969
|
+
reasoning_effort: 'high',
|
|
970
|
+
thinking: {
|
|
971
|
+
budget_tokens: 3000,
|
|
972
|
+
type: 'enabled',
|
|
973
|
+
},
|
|
974
|
+
verbosity: 'verbose',
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it('should handle all params when none are configured', () => {
|
|
979
|
+
vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
|
|
980
|
+
'enableReasoning',
|
|
981
|
+
'reasoningEffort',
|
|
982
|
+
'textVerbosity',
|
|
983
|
+
'thinking',
|
|
984
|
+
'thinkingBudget',
|
|
985
|
+
'thinkingLevel',
|
|
986
|
+
'urlContext',
|
|
987
|
+
'imageAspectRatio',
|
|
988
|
+
'imageResolution',
|
|
989
|
+
'disableContextCaching',
|
|
990
|
+
]);
|
|
991
|
+
|
|
992
|
+
const result = resolveModelExtendParams({
|
|
993
|
+
chatConfig: {} as any,
|
|
994
|
+
model: 'model',
|
|
995
|
+
provider: 'provider',
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
// Only enableReasoning should set thinking to disabled, others should be undefined
|
|
999
|
+
expect(result.thinking).toEqual({
|
|
1000
|
+
budget_tokens: 0,
|
|
1001
|
+
type: 'disabled',
|
|
1002
|
+
});
|
|
1003
|
+
expect(result.reasoning_effort).toBeUndefined();
|
|
1004
|
+
expect(result.verbosity).toBeUndefined();
|
|
1005
|
+
expect(result.thinkingBudget).toBeUndefined();
|
|
1006
|
+
expect(result.thinkingLevel).toBeUndefined();
|
|
1007
|
+
expect(result.urlContext).toBeUndefined();
|
|
1008
|
+
expect(result.imageAspectRatio).toBeUndefined();
|
|
1009
|
+
expect(result.imageResolution).toBeUndefined();
|
|
1010
|
+
expect(result.enabledContextCaching).toBeUndefined();
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
803
1014
|
});
|
|
@@ -48,6 +48,12 @@ export interface ChatGroupInternalAction {
|
|
|
48
48
|
type: string;
|
|
49
49
|
},
|
|
50
50
|
) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Fetch group detail directly and update store.
|
|
53
|
+
* Unlike refreshGroupDetail which uses SWR mutate, this method fetches immediately
|
|
54
|
+
* and is useful when SWR hook is not yet mounted (e.g., after createGroup).
|
|
55
|
+
*/
|
|
56
|
+
internal_fetchGroupDetail: (groupId: string) => Promise<void>;
|
|
51
57
|
internal_updateGroupMaps: (groups: ChatGroupItem[]) => void;
|
|
52
58
|
loadGroups: () => Promise<void>;
|
|
53
59
|
refreshGroupDetail: (groupId: string) => Promise<void>;
|
|
@@ -91,6 +97,30 @@ const chatGroupInternalSlice: StateCreator<
|
|
|
91
97
|
return {
|
|
92
98
|
internal_dispatchChatGroup: dispatch,
|
|
93
99
|
|
|
100
|
+
internal_fetchGroupDetail: async (groupId: string) => {
|
|
101
|
+
const groupDetail = await chatGroupService.getGroupDetail(groupId);
|
|
102
|
+
if (!groupDetail) return;
|
|
103
|
+
|
|
104
|
+
// Update groupMap with full group detail including supervisorAgentId and agents
|
|
105
|
+
dispatch({ payload: { id: groupDetail.id, value: groupDetail }, type: 'updateGroup' });
|
|
106
|
+
|
|
107
|
+
// Sync group agents to agentStore for builtin agent resolution
|
|
108
|
+
const agentStore = getAgentStoreState();
|
|
109
|
+
for (const agent of groupDetail.agents) {
|
|
110
|
+
agentStore.internal_dispatchAgentMap(agent.id, agent as any);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Set activeAgentId to supervisor for correct model resolution
|
|
114
|
+
if (groupDetail.supervisorAgentId) {
|
|
115
|
+
agentStore.setActiveAgentId(groupDetail.supervisorAgentId);
|
|
116
|
+
useChatStore.setState(
|
|
117
|
+
{ activeAgentId: groupDetail.supervisorAgentId },
|
|
118
|
+
false,
|
|
119
|
+
'syncActiveAgentIdFromAgentGroup',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
94
124
|
internal_updateGroupMaps: (groups) => {
|
|
95
125
|
// Build a candidate map from incoming groups
|
|
96
126
|
const incomingMap = groups.reduce(
|
|
@@ -10,19 +10,31 @@ vi.mock('@/services/chatGroup', () => ({
|
|
|
10
10
|
chatGroupService: {
|
|
11
11
|
addAgentsToGroup: vi.fn(),
|
|
12
12
|
createGroup: vi.fn(),
|
|
13
|
+
getGroupDetail: vi.fn(),
|
|
13
14
|
getGroups: vi.fn(),
|
|
14
15
|
},
|
|
15
16
|
}));
|
|
16
17
|
|
|
17
|
-
vi.mock('@/store/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
sessions: [],
|
|
22
|
-
switchSession: vi.fn(),
|
|
18
|
+
vi.mock('@/store/home', () => ({
|
|
19
|
+
getHomeStoreState: vi.fn(() => ({
|
|
20
|
+
refreshAgentList: vi.fn(),
|
|
21
|
+
switchToGroup: vi.fn(),
|
|
23
22
|
})),
|
|
24
23
|
}));
|
|
25
24
|
|
|
25
|
+
vi.mock('@/store/agent', () => ({
|
|
26
|
+
getAgentStoreState: vi.fn(() => ({
|
|
27
|
+
internal_dispatchAgentMap: vi.fn(),
|
|
28
|
+
setActiveAgentId: vi.fn(),
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('@/store/chat', () => ({
|
|
33
|
+
useChatStore: {
|
|
34
|
+
setState: vi.fn(),
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
26
38
|
describe('ChatGroupLifecycleSlice', () => {
|
|
27
39
|
beforeEach(() => {
|
|
28
40
|
vi.clearAllMocks();
|
|
@@ -47,12 +59,17 @@ describe('ChatGroupLifecycleSlice', () => {
|
|
|
47
59
|
title: 'Test Group',
|
|
48
60
|
userId: 'user-1',
|
|
49
61
|
};
|
|
62
|
+
const mockGroupDetail = {
|
|
63
|
+
...mockGroup,
|
|
64
|
+
agents: [],
|
|
65
|
+
supervisorAgentId: 'supervisor-1',
|
|
66
|
+
};
|
|
50
67
|
|
|
51
68
|
vi.mocked(chatGroupService.createGroup).mockResolvedValue({
|
|
52
69
|
group: mockGroup as any,
|
|
53
70
|
supervisorAgentId: 'supervisor-1',
|
|
54
71
|
});
|
|
55
|
-
vi.mocked(chatGroupService.
|
|
72
|
+
vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
|
|
56
73
|
|
|
57
74
|
const { result } = renderHook(() => useAgentGroupStore());
|
|
58
75
|
|
|
@@ -71,13 +88,18 @@ describe('ChatGroupLifecycleSlice', () => {
|
|
|
71
88
|
title: 'Test Group',
|
|
72
89
|
userId: 'user-1',
|
|
73
90
|
};
|
|
91
|
+
const mockGroupDetail = {
|
|
92
|
+
...mockGroup,
|
|
93
|
+
agents: [],
|
|
94
|
+
supervisorAgentId: 'supervisor-1',
|
|
95
|
+
};
|
|
74
96
|
|
|
75
97
|
vi.mocked(chatGroupService.createGroup).mockResolvedValue({
|
|
76
98
|
group: mockGroup as any,
|
|
77
99
|
supervisorAgentId: 'supervisor-1',
|
|
78
100
|
});
|
|
79
101
|
vi.mocked(chatGroupService.addAgentsToGroup).mockResolvedValue({ added: [], existing: [] });
|
|
80
|
-
vi.mocked(chatGroupService.
|
|
102
|
+
vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
|
|
81
103
|
|
|
82
104
|
const { result } = renderHook(() => useAgentGroupStore());
|
|
83
105
|
|
|
@@ -91,14 +113,46 @@ describe('ChatGroupLifecycleSlice', () => {
|
|
|
91
113
|
]);
|
|
92
114
|
});
|
|
93
115
|
|
|
94
|
-
it('should
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
it('should fetch group detail and store supervisorAgentId for tools injection', async () => {
|
|
117
|
+
const mockGroup = {
|
|
118
|
+
id: 'new-group-id',
|
|
119
|
+
title: 'Test Group',
|
|
120
|
+
userId: 'user-1',
|
|
121
|
+
};
|
|
122
|
+
const mockSupervisorAgentId = 'supervisor-agent-123';
|
|
123
|
+
const mockGroupDetail = {
|
|
124
|
+
...mockGroup,
|
|
125
|
+
agents: [],
|
|
126
|
+
supervisorAgentId: mockSupervisorAgentId,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
vi.mocked(chatGroupService.createGroup).mockResolvedValue({
|
|
130
|
+
group: mockGroup as any,
|
|
131
|
+
supervisorAgentId: mockSupervisorAgentId,
|
|
132
|
+
});
|
|
133
|
+
vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
|
|
134
|
+
|
|
135
|
+
const { result } = renderHook(() => useAgentGroupStore());
|
|
136
|
+
|
|
137
|
+
await act(async () => {
|
|
138
|
+
await result.current.createGroup({ title: 'Test Group' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Verify getGroupDetail was called to fetch full group info
|
|
142
|
+
expect(chatGroupService.getGroupDetail).toHaveBeenCalledWith('new-group-id');
|
|
143
|
+
|
|
144
|
+
// Verify supervisorAgentId is stored in groupMap for tools injection
|
|
145
|
+
const groupDetail = result.current.groupMap['new-group-id'];
|
|
146
|
+
expect(groupDetail).toBeDefined();
|
|
147
|
+
expect(groupDetail.supervisorAgentId).toBe(mockSupervisorAgentId);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not switch to group when silent is true', async () => {
|
|
151
|
+
const mockSwitchToGroup = vi.fn();
|
|
152
|
+
const { getHomeStoreState } = await import('@/store/home');
|
|
153
|
+
vi.mocked(getHomeStoreState).mockReturnValue({
|
|
154
|
+
refreshAgentList: vi.fn(),
|
|
155
|
+
switchToGroup: mockSwitchToGroup,
|
|
102
156
|
} as any);
|
|
103
157
|
|
|
104
158
|
const mockGroup = {
|
|
@@ -106,12 +160,17 @@ describe('ChatGroupLifecycleSlice', () => {
|
|
|
106
160
|
title: 'Test Group',
|
|
107
161
|
userId: 'user-1',
|
|
108
162
|
};
|
|
163
|
+
const mockGroupDetail = {
|
|
164
|
+
...mockGroup,
|
|
165
|
+
agents: [],
|
|
166
|
+
supervisorAgentId: 'supervisor-1',
|
|
167
|
+
};
|
|
109
168
|
|
|
110
169
|
vi.mocked(chatGroupService.createGroup).mockResolvedValue({
|
|
111
170
|
group: mockGroup as any,
|
|
112
171
|
supervisorAgentId: 'supervisor-1',
|
|
113
172
|
});
|
|
114
|
-
vi.mocked(chatGroupService.
|
|
173
|
+
vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
|
|
115
174
|
|
|
116
175
|
const { result } = renderHook(() => useAgentGroupStore());
|
|
117
176
|
|
|
@@ -119,7 +178,7 @@ describe('ChatGroupLifecycleSlice', () => {
|
|
|
119
178
|
await result.current.createGroup({ title: 'Test Group' }, [], true);
|
|
120
179
|
});
|
|
121
180
|
|
|
122
|
-
expect(
|
|
181
|
+
expect(mockSwitchToGroup).not.toHaveBeenCalled();
|
|
123
182
|
});
|
|
124
183
|
});
|
|
125
184
|
});
|
|
@@ -5,7 +5,7 @@ import { type StateCreator } from 'zustand/vanilla';
|
|
|
5
5
|
import { chatGroupService } from '@/services/chatGroup';
|
|
6
6
|
import { type ChatGroupStore } from '@/store/agentGroup/store';
|
|
7
7
|
import { useChatStore } from '@/store/chat';
|
|
8
|
-
import {
|
|
8
|
+
import { getHomeStoreState } from '@/store/home';
|
|
9
9
|
|
|
10
10
|
export interface ChatGroupLifecycleAction {
|
|
11
11
|
createGroup: (
|
|
@@ -14,7 +14,6 @@ export interface ChatGroupLifecycleAction {
|
|
|
14
14
|
silent?: boolean,
|
|
15
15
|
) => Promise<string>;
|
|
16
16
|
/**
|
|
17
|
-
* @deprecated Use switchTopic(undefined) instead
|
|
18
17
|
* Switch to a new topic in the group
|
|
19
18
|
* Clears activeTopicId and navigates to group root
|
|
20
19
|
*/
|
|
@@ -32,11 +31,8 @@ export const chatGroupLifecycleSlice: StateCreator<
|
|
|
32
31
|
[],
|
|
33
32
|
ChatGroupLifecycleAction
|
|
34
33
|
> = (_, get) => ({
|
|
35
|
-
/**
|
|
36
|
-
* @param silent - if true, do not switch to the new group session
|
|
37
|
-
*/
|
|
38
34
|
createGroup: async (newGroup, agentIds, silent = false) => {
|
|
39
|
-
const {
|
|
35
|
+
const { switchToGroup, refreshAgentList } = getHomeStoreState();
|
|
40
36
|
|
|
41
37
|
const { group } = await chatGroupService.createGroup(newGroup);
|
|
42
38
|
|
|
@@ -52,11 +48,13 @@ export const chatGroupLifecycleSlice: StateCreator<
|
|
|
52
48
|
|
|
53
49
|
get().internal_dispatchChatGroup({ payload: group, type: 'addGroup' });
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
await
|
|
51
|
+
// Fetch full group detail to get supervisorAgentId and agents for tools injection
|
|
52
|
+
await get().internal_fetchGroupDetail(group.id);
|
|
53
|
+
|
|
54
|
+
refreshAgentList();
|
|
57
55
|
|
|
58
56
|
if (!silent) {
|
|
59
|
-
|
|
57
|
+
switchToGroup(group.id);
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
return group.id;
|
|
@@ -509,6 +509,130 @@ describe('Operation Selectors', () => {
|
|
|
509
509
|
});
|
|
510
510
|
});
|
|
511
511
|
|
|
512
|
+
describe('isAgentRunning', () => {
|
|
513
|
+
it('should return false when no operations exist', () => {
|
|
514
|
+
const { result } = renderHook(() => useChatStore());
|
|
515
|
+
|
|
516
|
+
expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('should return true only for the agent with running operations', () => {
|
|
520
|
+
const { result } = renderHook(() => useChatStore());
|
|
521
|
+
|
|
522
|
+
act(() => {
|
|
523
|
+
result.current.startOperation({
|
|
524
|
+
type: 'execAgentRuntime',
|
|
525
|
+
context: { agentId: 'agent1' },
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
|
|
530
|
+
expect(operationSelectors.isAgentRunning('agent2')(result.current)).toBe(false);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should return false when operation completes', () => {
|
|
534
|
+
const { result } = renderHook(() => useChatStore());
|
|
535
|
+
|
|
536
|
+
let opId: string;
|
|
537
|
+
|
|
538
|
+
act(() => {
|
|
539
|
+
opId = result.current.startOperation({
|
|
540
|
+
type: 'execAgentRuntime',
|
|
541
|
+
context: { agentId: 'agent1' },
|
|
542
|
+
}).operationId;
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
|
|
546
|
+
|
|
547
|
+
act(() => {
|
|
548
|
+
result.current.completeOperation(opId!);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('should exclude aborting operations', () => {
|
|
555
|
+
const { result } = renderHook(() => useChatStore());
|
|
556
|
+
|
|
557
|
+
let opId: string;
|
|
558
|
+
|
|
559
|
+
act(() => {
|
|
560
|
+
opId = result.current.startOperation({
|
|
561
|
+
type: 'execAgentRuntime',
|
|
562
|
+
context: { agentId: 'agent1' },
|
|
563
|
+
}).operationId;
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
|
|
567
|
+
|
|
568
|
+
act(() => {
|
|
569
|
+
result.current.updateOperationMetadata(opId!, { isAborting: true });
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('should detect any topic with running operations for the agent', () => {
|
|
576
|
+
const { result } = renderHook(() => useChatStore());
|
|
577
|
+
|
|
578
|
+
act(() => {
|
|
579
|
+
// Agent 1, topic 1
|
|
580
|
+
result.current.startOperation({
|
|
581
|
+
type: 'execAgentRuntime',
|
|
582
|
+
context: { agentId: 'agent1', topicId: 'topic1' },
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// Agent 1, topic 2
|
|
586
|
+
result.current.startOperation({
|
|
587
|
+
type: 'execAgentRuntime',
|
|
588
|
+
context: { agentId: 'agent1', topicId: 'topic2' },
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Agent 2, topic 3
|
|
592
|
+
result.current.startOperation({
|
|
593
|
+
type: 'execAgentRuntime',
|
|
594
|
+
context: { agentId: 'agent2', topicId: 'topic3' },
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Agent 1 should be running (has 2 topics with operations)
|
|
599
|
+
expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
|
|
600
|
+
// Agent 2 should also be running
|
|
601
|
+
expect(operationSelectors.isAgentRunning('agent2')(result.current)).toBe(true);
|
|
602
|
+
// Agent 3 should not be running
|
|
603
|
+
expect(operationSelectors.isAgentRunning('agent3')(result.current)).toBe(false);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('should detect server agent runtime operations', () => {
|
|
607
|
+
const { result } = renderHook(() => useChatStore());
|
|
608
|
+
|
|
609
|
+
act(() => {
|
|
610
|
+
result.current.startOperation({
|
|
611
|
+
type: 'execServerAgentRuntime',
|
|
612
|
+
context: { agentId: 'agent1', groupId: 'group1' },
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
|
|
617
|
+
expect(operationSelectors.isAgentRunning('agent2')(result.current)).toBe(false);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should not detect non-AI-runtime operations', () => {
|
|
621
|
+
const { result } = renderHook(() => useChatStore());
|
|
622
|
+
|
|
623
|
+
act(() => {
|
|
624
|
+
// sendMessage is not an AI runtime operation type
|
|
625
|
+
result.current.startOperation({
|
|
626
|
+
type: 'sendMessage',
|
|
627
|
+
context: { agentId: 'agent1' },
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// sendMessage is not in AI_RUNTIME_OPERATION_TYPES, so should return false
|
|
632
|
+
expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
512
636
|
describe('backward compatibility selectors', () => {
|
|
513
637
|
it('isAgentRuntimeRunning should work', () => {
|
|
514
638
|
const { result } = renderHook(() => useChatStore());
|
|
@@ -234,6 +234,27 @@ const isAgentRuntimeRunningByContext =
|
|
|
234
234
|
};
|
|
235
235
|
|
|
236
236
|
// === Backward Compatibility ===
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if a specific agent has running AI runtime operations
|
|
240
|
+
* Used for agent list item loading states where we need per-agent granularity
|
|
241
|
+
*/
|
|
242
|
+
const isAgentRunning =
|
|
243
|
+
(agentId: string) =>
|
|
244
|
+
(s: ChatStoreState): boolean => {
|
|
245
|
+
for (const type of AI_RUNTIME_OPERATION_TYPES) {
|
|
246
|
+
const operationIds = s.operationsByType[type] || [];
|
|
247
|
+
const hasRunning = operationIds.some((id) => {
|
|
248
|
+
const op = s.operations[id];
|
|
249
|
+
return (
|
|
250
|
+
op && op.status === 'running' && !op.metadata.isAborting && op.context.agentId === agentId
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
if (hasRunning) return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
};
|
|
257
|
+
|
|
237
258
|
/**
|
|
238
259
|
* Check if agent runtime is running (including both main window and thread)
|
|
239
260
|
* Checks both client-side (execAgentRuntime) and server-side (execServerAgentRuntime) operations
|
|
@@ -477,6 +498,7 @@ export const operationSelectors = {
|
|
|
477
498
|
|
|
478
499
|
isAborting,
|
|
479
500
|
|
|
501
|
+
isAgentRunning,
|
|
480
502
|
isAgentRuntimeRunning,
|
|
481
503
|
isAgentRuntimeRunningByContext,
|
|
482
504
|
isAnyMessageLoading,
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { AgentRuntimeErrorType, type ILobeAgentRuntimeErrorType } from '@lobechat/model-runtime';
|
|
2
2
|
import { ChatErrorType, type ErrorResponse, type ErrorType } from '@lobechat/types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Error types that indicate a real authentication failure.
|
|
6
|
+
* When these errors occur, the response will include X-Auth-Required header
|
|
7
|
+
* to signal the client that re-authentication is needed.
|
|
8
|
+
*/
|
|
9
|
+
const AUTH_REQUIRED_ERROR_TYPES = new Set<ErrorType>([
|
|
10
|
+
ChatErrorType.Unauthorized,
|
|
11
|
+
ChatErrorType.InvalidClerkUser,
|
|
12
|
+
]);
|
|
13
|
+
|
|
4
14
|
const getStatus = (errorType: ILobeAgentRuntimeErrorType | ErrorType) => {
|
|
5
15
|
// InvalidAccessCode / InvalidAzureAPIKey / InvalidOpenAIAPIKey / InvalidZhipuAPIKey ....
|
|
6
16
|
if (errorType.toString().includes('Invalid')) return 401;
|
|
@@ -71,5 +81,15 @@ export const createErrorResponse = (
|
|
|
71
81
|
);
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
const headers: Record<string, string> = {
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Add X-Auth-Required header for real authentication failures
|
|
89
|
+
// This allows the client to distinguish between auth failures and other 401 errors (e.g., invalid API keys)
|
|
90
|
+
if (AUTH_REQUIRED_ERROR_TYPES.has(errorType as ErrorType)) {
|
|
91
|
+
headers['X-Auth-Required'] = 'true';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new Response(JSON.stringify(data), { headers, status: statusCode });
|
|
75
95
|
};
|