@lobehub/lobehub 2.0.0-next.32 → 2.0.0-next.34
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/test.yml +1 -0
- package/CHANGELOG.md +58 -0
- package/apps/desktop/package.json +1 -1
- package/changelog/v1.json +21 -0
- package/docker-compose/local/.env.example +3 -0
- package/docs/self-hosting/server-database/docker-compose.mdx +29 -0
- package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +29 -0
- package/package.json +1 -1
- package/packages/const/src/hotkeys.ts +3 -3
- package/packages/const/src/models.ts +2 -2
- package/packages/const/src/utils/merge.ts +3 -3
- package/packages/conversation-flow/package.json +13 -0
- package/packages/conversation-flow/src/__tests__/fixtures/index.ts +48 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-chain-with-followup.json +56 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-with-tools.json +144 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/active-index-1.json +131 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-branch.json +96 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-user-branch.json +123 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/conversation.json +128 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +14 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/nested.json +179 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/index.ts +8 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/simple.json +85 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/with-tools.json +169 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/complex-scenario.json +107 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +14 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/linear-conversation.json +59 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-chain-with-followup.json +135 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-with-tools.json +340 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +242 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-branch.json +208 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-user-branch.json +254 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +260 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +14 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +389 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/index.ts +8 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/simple.json +224 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/with-tools.json +418 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +239 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/linear-conversation.json +138 -0
- package/packages/conversation-flow/src/__tests__/parse.test.ts +97 -0
- package/packages/conversation-flow/src/index.ts +17 -0
- package/packages/conversation-flow/src/indexing.ts +58 -0
- package/packages/conversation-flow/src/parse.ts +53 -0
- package/packages/conversation-flow/src/structuring.ts +38 -0
- package/packages/conversation-flow/src/transformation/BranchResolver.ts +66 -0
- package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +292 -0
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +421 -0
- package/packages/conversation-flow/src/transformation/MessageCollector.ts +166 -0
- package/packages/conversation-flow/src/transformation/MessageTransformer.ts +177 -0
- package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +151 -0
- package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +385 -0
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +511 -0
- package/packages/conversation-flow/src/transformation/__tests__/MessageCollector.test.ts +220 -0
- package/packages/conversation-flow/src/transformation/__tests__/MessageTransformer.test.ts +287 -0
- package/packages/conversation-flow/src/transformation/index.ts +78 -0
- package/packages/conversation-flow/src/types/contextTree.ts +65 -0
- package/packages/conversation-flow/src/types/flatMessageList.ts +66 -0
- package/packages/conversation-flow/src/types/shared.ts +63 -0
- package/packages/conversation-flow/src/types.ts +36 -0
- package/packages/conversation-flow/vitest.config.mts +10 -0
- package/packages/model-bank/src/aiModels/google.ts +1 -1
- package/packages/types/src/message/common/metadata.ts +5 -1
- package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +3 -4
- package/src/app/[variants]/(main)/settings/provider/ProviderMenu/List.tsx +97 -7
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/DisabledModels.tsx +144 -8
- package/src/envs/__tests__/app.test.ts +47 -13
- package/src/envs/app.ts +6 -0
- package/src/locales/default/modelProvider.ts +15 -1
- package/src/server/routers/async/__tests__/caller.test.ts +333 -0
- package/src/server/routers/async/caller.ts +2 -1
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +57 -57
- package/src/server/routers/lambda/message.ts +2 -2
- package/src/server/services/mcp/deps/checkers/ManualInstallationChecker.test.ts +162 -0
- package/src/server/services/mcp/deps/checkers/NpmInstallationChecker.test.ts +374 -0
- package/src/server/services/mcp/deps/checkers/PythonInstallationChecker.test.ts +368 -0
- package/src/server/services/message/__tests__/index.test.ts +4 -4
- package/src/server/services/message/index.ts +1 -1
- package/src/services/message/index.ts +2 -3
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +8 -8
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +8 -8
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +1 -1
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +1 -1
- package/src/store/chat/slices/message/action.test.ts +7 -7
- package/src/store/chat/slices/message/action.ts +2 -2
- package/src/store/chat/slices/plugin/action.test.ts +7 -7
- package/src/store/chat/slices/plugin/action.ts +1 -1
- package/src/store/global/initialState.ts +4 -0
- package/src/store/global/selectors/systemStatus.ts +6 -0
- package/packages/context-engine/ARCHITECTURE.md +0 -425
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { UIChatMessage } from '@lobechat/types';
|
|
2
|
+
|
|
3
|
+
import type { ContextNode } from './contextTree';
|
|
4
|
+
import type { FlatMessage } from './flatMessageList';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared Types
|
|
8
|
+
*
|
|
9
|
+
* Common types used across the conversation flow engine.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Re-export UIChatMessage as Message for convenience
|
|
14
|
+
*/
|
|
15
|
+
export type Message = UIChatMessage;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Message group metadata from database
|
|
19
|
+
* Used for multi-model parallel conversations and manual grouping
|
|
20
|
+
*/
|
|
21
|
+
export interface MessageGroupMetadata {
|
|
22
|
+
description?: string;
|
|
23
|
+
id: string;
|
|
24
|
+
/** Presentation mode: compare, summary, or manual */
|
|
25
|
+
mode?: 'compare' | 'summary' | 'manual';
|
|
26
|
+
/** Parent message that triggered this group */
|
|
27
|
+
parentMessageId?: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Internal structure node used during tree building
|
|
33
|
+
*/
|
|
34
|
+
export interface IdNode {
|
|
35
|
+
children: IdNode[];
|
|
36
|
+
id: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Result of the parse function
|
|
41
|
+
*/
|
|
42
|
+
export interface ParseResult {
|
|
43
|
+
/** Semantic tree structure for navigation and context understanding */
|
|
44
|
+
contextTree: ContextNode[];
|
|
45
|
+
/** Flattened list optimized for virtual list rendering */
|
|
46
|
+
flatList: FlatMessage[];
|
|
47
|
+
/** Map for O(1) message access */
|
|
48
|
+
messageMap: Record<string, Message>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Internal helper maps used during parsing
|
|
53
|
+
*/
|
|
54
|
+
export interface HelperMaps {
|
|
55
|
+
/** Maps parent ID to array of child IDs */
|
|
56
|
+
childrenMap: Map<string | null, string[]>;
|
|
57
|
+
/** Maps message group ID to its metadata */
|
|
58
|
+
messageGroupMap: Map<string, MessageGroupMetadata>;
|
|
59
|
+
/** Maps message ID to message */
|
|
60
|
+
messageMap: Map<string, Message>;
|
|
61
|
+
/** Maps thread ID to all messages in that thread */
|
|
62
|
+
threadMap: Map<string, Message[]>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Index
|
|
3
|
+
*
|
|
4
|
+
* Centralized exports for all conversation flow types.
|
|
5
|
+
* Types are organized into three categories:
|
|
6
|
+
*
|
|
7
|
+
* 1. Context Tree (types/contextTree.ts) - Tree structure for navigation
|
|
8
|
+
* 2. Flat Message List (types/flatMessageList.ts) - Optimized for rendering
|
|
9
|
+
* 3. Shared (types/shared.ts) - Common types used across modules
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Context Tree Types
|
|
13
|
+
export type {
|
|
14
|
+
AssistantGroupNode,
|
|
15
|
+
BranchNode,
|
|
16
|
+
CompareNode,
|
|
17
|
+
ContextNode,
|
|
18
|
+
MessageNode,
|
|
19
|
+
} from './types/contextTree';
|
|
20
|
+
|
|
21
|
+
// Flat Message List Types
|
|
22
|
+
export type {
|
|
23
|
+
BranchMetadata,
|
|
24
|
+
FlatMessage,
|
|
25
|
+
FlatMessageExtra,
|
|
26
|
+
FlatMessageRole,
|
|
27
|
+
} from './types/flatMessageList';
|
|
28
|
+
|
|
29
|
+
// Shared Types
|
|
30
|
+
export type {
|
|
31
|
+
HelperMaps,
|
|
32
|
+
IdNode,
|
|
33
|
+
Message,
|
|
34
|
+
MessageGroupMetadata,
|
|
35
|
+
ParseResult,
|
|
36
|
+
} from './types/shared';
|
|
@@ -932,7 +932,7 @@ const googleImageModels: AIImageModelCard[] = [
|
|
|
932
932
|
type: 'image',
|
|
933
933
|
description: 'Imagen 4th generation text-to-image model series',
|
|
934
934
|
organization: 'Deepmind',
|
|
935
|
-
releasedAt: '
|
|
935
|
+
releasedAt: '2025-06-06',
|
|
936
936
|
parameters: imagenGenParameters,
|
|
937
937
|
pricing: {
|
|
938
938
|
units: [{ name: 'imageGeneration', rate: 0.04, strategy: 'fixed', unit: 'image' }],
|
|
@@ -103,4 +103,8 @@ export interface ModelPerformance {
|
|
|
103
103
|
latency?: number;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
export interface MessageMetadata extends ModelUsage, ModelPerformance {
|
|
106
|
+
export interface MessageMetadata extends ModelUsage, ModelPerformance {
|
|
107
|
+
activeBranchIndex?: number;
|
|
108
|
+
activeColumn?: boolean;
|
|
109
|
+
compare?: boolean;
|
|
110
|
+
}
|
package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx
CHANGED
|
@@ -4,9 +4,8 @@ import { Flexbox } from 'react-layout-kit';
|
|
|
4
4
|
import { shallow } from 'zustand/shallow';
|
|
5
5
|
|
|
6
6
|
import { DEFAULT_AVATAR } from '@/const/meta';
|
|
7
|
+
import { INBOX_SESSION_ID } from '@/const/session';
|
|
7
8
|
import { isDesktop } from '@/const/version';
|
|
8
|
-
import { useAgentStore } from '@/store/agent';
|
|
9
|
-
import { agentSelectors } from '@/store/agent/selectors';
|
|
10
9
|
import { useChatStore } from '@/store/chat';
|
|
11
10
|
import { messageStateSelectors } from '@/store/chat/selectors';
|
|
12
11
|
import { useGlobalStore } from '@/store/global';
|
|
@@ -28,7 +27,6 @@ interface SessionItemProps {
|
|
|
28
27
|
const SessionItem = memo<SessionItemProps>(({ id }) => {
|
|
29
28
|
const [open, setOpen] = useState(false);
|
|
30
29
|
const [createGroupModalOpen, setCreateGroupModalOpen] = useState(false);
|
|
31
|
-
const [defaultModel] = useAgentStore((s) => [agentSelectors.inboxAgentModel(s)]);
|
|
32
30
|
|
|
33
31
|
const openSessionInNewWindow = useGlobalStore((s) => s.openSessionInNewWindow);
|
|
34
32
|
|
|
@@ -55,7 +53,8 @@ const SessionItem = memo<SessionItemProps>(({ id }) => {
|
|
|
55
53
|
];
|
|
56
54
|
});
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
// Only hide the model tag for the inbox session itself (随便聊聊)
|
|
57
|
+
const showModel = sessionType === 'agent' && model && id !== INBOX_SESSION_ID;
|
|
59
58
|
|
|
60
59
|
const handleDoubleClick = () => {
|
|
61
60
|
if (isDesktop) {
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { ActionIcon, ScrollShadow, Text } from '@lobehub/ui';
|
|
3
|
+
import { ActionIcon, Dropdown, Icon, ScrollShadow, Text } from '@lobehub/ui';
|
|
4
|
+
import type { ItemType } from 'antd/es/menu/interface';
|
|
4
5
|
import isEqual from 'fast-deep-equal';
|
|
5
|
-
import { ArrowDownUpIcon } from 'lucide-react';
|
|
6
|
-
import { useState } from 'react';
|
|
6
|
+
import { ArrowDownUpIcon, LucideCheck } from 'lucide-react';
|
|
7
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
7
8
|
import { useTranslation } from 'react-i18next';
|
|
8
9
|
import { Flexbox } from 'react-layout-kit';
|
|
9
10
|
|
|
10
11
|
import { aiProviderSelectors } from '@/store/aiInfra';
|
|
11
12
|
import { useAiInfraStore } from '@/store/aiInfra/store';
|
|
13
|
+
import { useGlobalStore } from '@/store/global';
|
|
14
|
+
import { systemStatusSelectors } from '@/store/global/selectors';
|
|
12
15
|
|
|
13
16
|
import All from './All';
|
|
14
17
|
import ProviderItem from './Item';
|
|
15
18
|
import SortProviderModal from './SortProviderModal';
|
|
16
19
|
|
|
20
|
+
// Sort type enumeration
|
|
21
|
+
enum SortType {
|
|
22
|
+
Alphabetical = 'alphabetical',
|
|
23
|
+
AlphabeticalDesc = 'alphabeticalDesc',
|
|
24
|
+
Default = 'default',
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
const ProviderList = (props: {
|
|
18
28
|
mobile?: boolean;
|
|
19
29
|
onProviderSelect: (providerKey: string) => void;
|
|
@@ -21,6 +31,19 @@ const ProviderList = (props: {
|
|
|
21
31
|
const { onProviderSelect, mobile } = props;
|
|
22
32
|
const { t } = useTranslation('modelProvider');
|
|
23
33
|
const [open, setOpen] = useState(false);
|
|
34
|
+
|
|
35
|
+
const [sortType, updateSystemStatus] = useGlobalStore((s) => [
|
|
36
|
+
systemStatusSelectors.disabledModelProvidersSortType(s),
|
|
37
|
+
s.updateSystemStatus,
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const updateSortType = useCallback(
|
|
41
|
+
(newSortType: SortType) => {
|
|
42
|
+
updateSystemStatus({ disabledModelProvidersSortType: newSortType });
|
|
43
|
+
},
|
|
44
|
+
[updateSystemStatus],
|
|
45
|
+
);
|
|
46
|
+
|
|
24
47
|
const enabledModelProviderList = useAiInfraStore(
|
|
25
48
|
aiProviderSelectors.enabledAiProviderList,
|
|
26
49
|
isEqual,
|
|
@@ -30,6 +53,34 @@ const ProviderList = (props: {
|
|
|
30
53
|
aiProviderSelectors.disabledAiProviderList,
|
|
31
54
|
isEqual,
|
|
32
55
|
);
|
|
56
|
+
|
|
57
|
+
// Sort model providers based on sort type
|
|
58
|
+
const sortedDisabledProviders = useMemo(() => {
|
|
59
|
+
const providers = [...disabledModelProviderList];
|
|
60
|
+
switch (sortType) {
|
|
61
|
+
case SortType.Alphabetical: {
|
|
62
|
+
return providers.sort((a, b) => {
|
|
63
|
+
const cmpDisplay = (a.name || a.id).localeCompare(b.name || b.id);
|
|
64
|
+
if (cmpDisplay !== 0) return cmpDisplay;
|
|
65
|
+
return a.id.localeCompare(b.id);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
case SortType.AlphabeticalDesc: {
|
|
69
|
+
return providers.sort((a, b) => {
|
|
70
|
+
const cmpDisplay = (b.name || a.id).localeCompare(a.name || b.id);
|
|
71
|
+
if (cmpDisplay !== 0) return cmpDisplay;
|
|
72
|
+
return b.id.localeCompare(a.id);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
case SortType.Default: {
|
|
76
|
+
return providers;
|
|
77
|
+
}
|
|
78
|
+
default: {
|
|
79
|
+
return providers;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}, [disabledModelProviderList, sortType]);
|
|
83
|
+
|
|
33
84
|
return (
|
|
34
85
|
<ScrollShadow gap={4} height={'100%'} paddingInline={12} size={4} style={{ paddingBottom: 32 }}>
|
|
35
86
|
{!mobile && <All onClick={onProviderSelect} />}
|
|
@@ -63,10 +114,49 @@ const ProviderList = (props: {
|
|
|
63
114
|
{enabledModelProviderList.map((item) => (
|
|
64
115
|
<ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
|
|
65
116
|
))}
|
|
66
|
-
<
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
117
|
+
<Flexbox align={'center'} horizontal justify={'space-between'}>
|
|
118
|
+
<Text style={{ fontSize: 12, marginTop: 8 }} type={'secondary'}>
|
|
119
|
+
{t('menu.list.disabled')}
|
|
120
|
+
</Text>
|
|
121
|
+
{disabledModelProviderList.length > 1 && (
|
|
122
|
+
<Dropdown
|
|
123
|
+
menu={{
|
|
124
|
+
items: [
|
|
125
|
+
{
|
|
126
|
+
icon: sortType === SortType.Default ? <Icon icon={LucideCheck} /> : <div />,
|
|
127
|
+
key: 'default',
|
|
128
|
+
label: t('menu.list.disabledActions.sortDefault'),
|
|
129
|
+
onClick: () => updateSortType(SortType.Default),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: 'divider',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
icon: sortType === SortType.Alphabetical ? <Icon icon={LucideCheck} /> : <div />,
|
|
136
|
+
key: 'alphabetical',
|
|
137
|
+
label: t('menu.list.disabledActions.sortAlphabetical'),
|
|
138
|
+
onClick: () => updateSortType(SortType.Alphabetical),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
icon:
|
|
142
|
+
sortType === SortType.AlphabeticalDesc ? <Icon icon={LucideCheck} /> : <div />,
|
|
143
|
+
key: 'alphabeticalDesc',
|
|
144
|
+
label: t('menu.list.disabledActions.sortAlphabeticalDesc'),
|
|
145
|
+
onClick: () => updateSortType(SortType.AlphabeticalDesc),
|
|
146
|
+
},
|
|
147
|
+
] as ItemType[],
|
|
148
|
+
}}
|
|
149
|
+
trigger={['click']}
|
|
150
|
+
>
|
|
151
|
+
<ActionIcon
|
|
152
|
+
icon={ArrowDownUpIcon}
|
|
153
|
+
size={'small'}
|
|
154
|
+
title={t('menu.list.disabledActions.sort')}
|
|
155
|
+
/>
|
|
156
|
+
</Dropdown>
|
|
157
|
+
)}
|
|
158
|
+
</Flexbox>
|
|
159
|
+
{sortedDisabledProviders.map((item) => (
|
|
70
160
|
<ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
|
|
71
161
|
))}
|
|
72
162
|
</ScrollShadow>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { Button, Text } from '@lobehub/ui';
|
|
1
|
+
import { ActionIcon, Button, Dropdown, Icon, Text } from '@lobehub/ui';
|
|
2
|
+
import type { ItemType } from 'antd/es/menu/interface';
|
|
2
3
|
import isEqual from 'fast-deep-equal';
|
|
3
|
-
import { ChevronDown } from 'lucide-react';
|
|
4
|
-
import { memo, useMemo, useState } from 'react';
|
|
4
|
+
import { ArrowDownUpIcon, ChevronDown, LucideCheck } from 'lucide-react';
|
|
5
|
+
import { memo, useCallback, useMemo, useState } from 'react';
|
|
5
6
|
import { useTranslation } from 'react-i18next';
|
|
6
7
|
import { Flexbox } from 'react-layout-kit';
|
|
7
8
|
|
|
8
9
|
import { useAiInfraStore } from '@/store/aiInfra';
|
|
9
10
|
import { aiModelSelectors } from '@/store/aiInfra/selectors';
|
|
11
|
+
import { useGlobalStore } from '@/store/global';
|
|
12
|
+
import { systemStatusSelectors } from '@/store/global/selectors';
|
|
10
13
|
|
|
11
14
|
import ModelItem from './ModelItem';
|
|
12
15
|
|
|
@@ -14,10 +17,32 @@ interface DisabledModelsProps {
|
|
|
14
17
|
activeTab: string;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
// Sort type enumeration
|
|
21
|
+
enum SortType {
|
|
22
|
+
Alphabetical = 'alphabetical',
|
|
23
|
+
AlphabeticalDesc = 'alphabeticalDesc',
|
|
24
|
+
Default = 'default',
|
|
25
|
+
ReleasedAt = 'releasedAt',
|
|
26
|
+
ReleasedAtDesc = 'releasedAtDesc',
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
const DisabledModels = memo<DisabledModelsProps>(({ activeTab }) => {
|
|
18
30
|
const { t } = useTranslation('modelProvider');
|
|
19
31
|
|
|
20
32
|
const [showMore, setShowMore] = useState(false);
|
|
33
|
+
|
|
34
|
+
const [sortType, updateSystemStatus] = useGlobalStore((s) => [
|
|
35
|
+
systemStatusSelectors.disabledModelsSortType(s),
|
|
36
|
+
s.updateSystemStatus,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const updateSortType = useCallback(
|
|
40
|
+
(newSortType: SortType) => {
|
|
41
|
+
updateSystemStatus({ disabledModelsSortType: newSortType });
|
|
42
|
+
},
|
|
43
|
+
[updateSystemStatus],
|
|
44
|
+
);
|
|
45
|
+
|
|
21
46
|
const disabledModels = useAiInfraStore(aiModelSelectors.disabledAiProviderModelList, isEqual);
|
|
22
47
|
|
|
23
48
|
// Filter models based on active tab
|
|
@@ -26,18 +51,129 @@ const DisabledModels = memo<DisabledModelsProps>(({ activeTab }) => {
|
|
|
26
51
|
return disabledModels.filter((model) => model.type === activeTab);
|
|
27
52
|
}, [disabledModels, activeTab]);
|
|
28
53
|
|
|
29
|
-
|
|
54
|
+
// Sort models based on sort type
|
|
55
|
+
const sortedDisabledModels = useMemo(() => {
|
|
56
|
+
const models = [...filteredDisabledModels];
|
|
57
|
+
switch (sortType) {
|
|
58
|
+
case SortType.Alphabetical: {
|
|
59
|
+
return models.sort((a, b) => {
|
|
60
|
+
const cmpDisplay = (a.displayName || a.id).localeCompare(b.displayName || b.id);
|
|
61
|
+
if (cmpDisplay !== 0) return cmpDisplay;
|
|
62
|
+
return a.id.localeCompare(b.id);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
case SortType.AlphabeticalDesc: {
|
|
66
|
+
return models.sort((a, b) => {
|
|
67
|
+
const cmpDisplay = (b.displayName || b.id).localeCompare(a.displayName || a.id);
|
|
68
|
+
if (cmpDisplay !== 0) return cmpDisplay;
|
|
69
|
+
return b.id.localeCompare(a.id);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
case SortType.ReleasedAt: {
|
|
73
|
+
return models.sort((a, b) => {
|
|
74
|
+
const aHasDate = !!a.releasedAt;
|
|
75
|
+
const bHasDate = !!b.releasedAt;
|
|
76
|
+
|
|
77
|
+
if (aHasDate && !bHasDate) return -1;
|
|
78
|
+
if (!aHasDate && bHasDate) return 1;
|
|
79
|
+
if (!aHasDate && !bHasDate) return 0;
|
|
80
|
+
|
|
81
|
+
return a.releasedAt!.localeCompare(b.releasedAt!);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
case SortType.ReleasedAtDesc: {
|
|
85
|
+
return models.sort((a, b) => {
|
|
86
|
+
const aHasDate = !!a.releasedAt;
|
|
87
|
+
const bHasDate = !!b.releasedAt;
|
|
88
|
+
|
|
89
|
+
if (aHasDate && !bHasDate) return -1;
|
|
90
|
+
if (!aHasDate && bHasDate) return 1;
|
|
91
|
+
if (!aHasDate && !bHasDate) return 0;
|
|
92
|
+
|
|
93
|
+
return b.releasedAt!.localeCompare(a.releasedAt!);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
case SortType.Default: {
|
|
97
|
+
return models;
|
|
98
|
+
}
|
|
99
|
+
default: {
|
|
100
|
+
return models;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, [filteredDisabledModels, sortType]);
|
|
104
|
+
|
|
105
|
+
const displayModels = showMore ? sortedDisabledModels : sortedDisabledModels.slice(0, 10);
|
|
30
106
|
|
|
31
107
|
return (
|
|
32
108
|
filteredDisabledModels.length > 0 && (
|
|
33
109
|
<Flexbox>
|
|
34
|
-
<
|
|
35
|
-
{
|
|
36
|
-
|
|
110
|
+
<Flexbox align="center" horizontal justify="space-between">
|
|
111
|
+
<Text style={{ fontSize: 12, marginTop: 8 }} type={'secondary'}>
|
|
112
|
+
{t('providerModels.list.disabled')}
|
|
113
|
+
</Text>
|
|
114
|
+
{filteredDisabledModels.length > 1 && (
|
|
115
|
+
<Dropdown
|
|
116
|
+
menu={{
|
|
117
|
+
items: [
|
|
118
|
+
{
|
|
119
|
+
icon: sortType === SortType.Default ? <Icon icon={LucideCheck} /> : <div />,
|
|
120
|
+
key: 'default',
|
|
121
|
+
label: t('providerModels.list.disabledActions.sortDefault'),
|
|
122
|
+
onClick: () => updateSortType(SortType.Default),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'divider',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
icon:
|
|
129
|
+
sortType === SortType.Alphabetical ? <Icon icon={LucideCheck} /> : <div />,
|
|
130
|
+
key: 'alphabetical',
|
|
131
|
+
label: t('providerModels.list.disabledActions.sortAlphabetical'),
|
|
132
|
+
onClick: () => updateSortType(SortType.Alphabetical),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
icon:
|
|
136
|
+
sortType === SortType.AlphabeticalDesc ? (
|
|
137
|
+
<Icon icon={LucideCheck} />
|
|
138
|
+
) : (
|
|
139
|
+
<div />
|
|
140
|
+
),
|
|
141
|
+
key: 'alphabeticalDesc',
|
|
142
|
+
label: t('providerModels.list.disabledActions.sortAlphabeticalDesc'),
|
|
143
|
+
onClick: () => updateSortType(SortType.AlphabeticalDesc),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: 'divider',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
icon: sortType === SortType.ReleasedAt ? <Icon icon={LucideCheck} /> : <div />,
|
|
150
|
+
key: 'releasedAt',
|
|
151
|
+
label: t('providerModels.list.disabledActions.sortReleasedAt'),
|
|
152
|
+
onClick: () => updateSortType(SortType.ReleasedAt),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
icon:
|
|
156
|
+
sortType === SortType.ReleasedAtDesc ? <Icon icon={LucideCheck} /> : <div />,
|
|
157
|
+
key: 'releasedAtDesc',
|
|
158
|
+
label: t('providerModels.list.disabledActions.sortReleasedAtDesc'),
|
|
159
|
+
onClick: () => updateSortType(SortType.ReleasedAtDesc),
|
|
160
|
+
},
|
|
161
|
+
] as ItemType[],
|
|
162
|
+
}}
|
|
163
|
+
trigger={['click']}
|
|
164
|
+
>
|
|
165
|
+
<ActionIcon
|
|
166
|
+
icon={ArrowDownUpIcon}
|
|
167
|
+
size={'small'}
|
|
168
|
+
title={t('providerModels.list.disabledActions.sort')}
|
|
169
|
+
/>
|
|
170
|
+
</Dropdown>
|
|
171
|
+
)}
|
|
172
|
+
</Flexbox>
|
|
37
173
|
{displayModels.map((item) => (
|
|
38
174
|
<ModelItem {...item} key={item.id} />
|
|
39
175
|
))}
|
|
40
|
-
{!showMore &&
|
|
176
|
+
{!showMore && sortedDisabledModels.length > 10 && (
|
|
41
177
|
<Button
|
|
42
178
|
block
|
|
43
179
|
icon={ChevronDown}
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import { getAppConfig } from '../app';
|
|
5
|
-
|
|
6
|
-
// Stub the global process object to safely mock environment variables
|
|
7
|
-
vi.stubGlobal('process', {
|
|
8
|
-
...process, // Preserve the original process object
|
|
9
|
-
env: { ...process.env }, // Clone the environment variables object for modification
|
|
10
|
-
});
|
|
11
|
-
|
|
12
4
|
describe('getServerConfig', () => {
|
|
13
5
|
beforeEach(() => {
|
|
14
|
-
// Reset
|
|
15
|
-
vi.
|
|
6
|
+
// Reset modules to clear the cached config
|
|
7
|
+
vi.resetModules();
|
|
16
8
|
});
|
|
17
9
|
|
|
18
10
|
// it('correctly handles values for OPENAI_FUNCTION_REGIONS', () => {
|
|
@@ -22,7 +14,8 @@ describe('getServerConfig', () => {
|
|
|
22
14
|
// });
|
|
23
15
|
|
|
24
16
|
describe('index url', () => {
|
|
25
|
-
it('should return default URLs when no environment variables are set', () => {
|
|
17
|
+
it('should return default URLs when no environment variables are set', async () => {
|
|
18
|
+
const { getAppConfig } = await import('../app');
|
|
26
19
|
const config = getAppConfig();
|
|
27
20
|
expect(config.AGENTS_INDEX_URL).toBe(
|
|
28
21
|
'https://registry.npmmirror.com/@lobehub/agents-index/v1/files/public',
|
|
@@ -32,18 +25,20 @@ describe('getServerConfig', () => {
|
|
|
32
25
|
);
|
|
33
26
|
});
|
|
34
27
|
|
|
35
|
-
it('should return custom URLs when environment variables are set', () => {
|
|
28
|
+
it('should return custom URLs when environment variables are set', async () => {
|
|
36
29
|
process.env.AGENTS_INDEX_URL = 'https://custom-agents-url.com';
|
|
37
30
|
process.env.PLUGINS_INDEX_URL = 'https://custom-plugins-url.com';
|
|
31
|
+
const { getAppConfig } = await import('../app');
|
|
38
32
|
const config = getAppConfig();
|
|
39
33
|
expect(config.AGENTS_INDEX_URL).toBe('https://custom-agents-url.com');
|
|
40
34
|
expect(config.PLUGINS_INDEX_URL).toBe('https://custom-plugins-url.com');
|
|
41
35
|
});
|
|
42
36
|
|
|
43
|
-
it('should return default URLs when environment variables are empty string', () => {
|
|
37
|
+
it('should return default URLs when environment variables are empty string', async () => {
|
|
44
38
|
process.env.AGENTS_INDEX_URL = '';
|
|
45
39
|
process.env.PLUGINS_INDEX_URL = '';
|
|
46
40
|
|
|
41
|
+
const { getAppConfig } = await import('../app');
|
|
47
42
|
const config = getAppConfig();
|
|
48
43
|
expect(config.AGENTS_INDEX_URL).toBe(
|
|
49
44
|
'https://registry.npmmirror.com/@lobehub/agents-index/v1/files/public',
|
|
@@ -53,4 +48,43 @@ describe('getServerConfig', () => {
|
|
|
53
48
|
);
|
|
54
49
|
});
|
|
55
50
|
});
|
|
51
|
+
|
|
52
|
+
describe('INTERNAL_APP_URL', () => {
|
|
53
|
+
it('should default to APP_URL when INTERNAL_APP_URL is not set', async () => {
|
|
54
|
+
process.env.APP_URL = 'https://example.com';
|
|
55
|
+
delete process.env.INTERNAL_APP_URL;
|
|
56
|
+
|
|
57
|
+
const { getAppConfig } = await import('../app');
|
|
58
|
+
const config = getAppConfig();
|
|
59
|
+
expect(config.INTERNAL_APP_URL).toBe('https://example.com');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should use INTERNAL_APP_URL when explicitly set', async () => {
|
|
63
|
+
process.env.APP_URL = 'https://public.example.com';
|
|
64
|
+
process.env.INTERNAL_APP_URL = 'http://localhost:3210';
|
|
65
|
+
|
|
66
|
+
const { getAppConfig } = await import('../app');
|
|
67
|
+
const config = getAppConfig();
|
|
68
|
+
expect(config.INTERNAL_APP_URL).toBe('http://localhost:3210');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should use INTERNAL_APP_URL over APP_URL when both are set', async () => {
|
|
72
|
+
process.env.APP_URL = 'https://public.example.com';
|
|
73
|
+
process.env.INTERNAL_APP_URL = 'http://internal-service:3210';
|
|
74
|
+
|
|
75
|
+
const { getAppConfig } = await import('../app');
|
|
76
|
+
const config = getAppConfig();
|
|
77
|
+
expect(config.APP_URL).toBe('https://public.example.com');
|
|
78
|
+
expect(config.INTERNAL_APP_URL).toBe('http://internal-service:3210');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle localhost INTERNAL_APP_URL for bypassing CDN', async () => {
|
|
82
|
+
process.env.APP_URL = 'https://cloudflare-proxied.com';
|
|
83
|
+
process.env.INTERNAL_APP_URL = 'http://127.0.0.1:3210';
|
|
84
|
+
|
|
85
|
+
const { getAppConfig } = await import('../app');
|
|
86
|
+
const config = getAppConfig();
|
|
87
|
+
expect(config.INTERNAL_APP_URL).toBe('http://127.0.0.1:3210');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
56
90
|
});
|
package/src/envs/app.ts
CHANGED
|
@@ -20,6 +20,10 @@ const APP_URL = process.env.APP_URL
|
|
|
20
20
|
? vercelUrl
|
|
21
21
|
: 'http://localhost:3010';
|
|
22
22
|
|
|
23
|
+
// INTERNAL_APP_URL is used for server-to-server calls to bypass CDN/proxy
|
|
24
|
+
// Falls back to APP_URL if not set
|
|
25
|
+
const INTERNAL_APP_URL = process.env.INTERNAL_APP_URL || APP_URL;
|
|
26
|
+
|
|
23
27
|
const ASSISTANT_INDEX_URL = 'https://registry.npmmirror.com/@lobehub/agents-index/v1/files/public';
|
|
24
28
|
|
|
25
29
|
const PLUGINS_INDEX_URL = 'https://registry.npmmirror.com/@lobehub/plugins-index/v1/files/public';
|
|
@@ -43,6 +47,7 @@ export const getAppConfig = () => {
|
|
|
43
47
|
PLUGIN_SETTINGS: z.string().optional(),
|
|
44
48
|
|
|
45
49
|
APP_URL: z.string().optional(),
|
|
50
|
+
INTERNAL_APP_URL: z.string().optional(),
|
|
46
51
|
VERCEL_EDGE_CONFIG: z.string().optional(),
|
|
47
52
|
MIDDLEWARE_REWRITE_THROUGH_LOCAL: z.boolean().optional(),
|
|
48
53
|
ENABLE_AUTH_PROTECTION: z.boolean().optional(),
|
|
@@ -77,6 +82,7 @@ export const getAppConfig = () => {
|
|
|
77
82
|
VERCEL_EDGE_CONFIG: process.env.VERCEL_EDGE_CONFIG,
|
|
78
83
|
|
|
79
84
|
APP_URL,
|
|
85
|
+
INTERNAL_APP_URL,
|
|
80
86
|
MIDDLEWARE_REWRITE_THROUGH_LOCAL: process.env.MIDDLEWARE_REWRITE_THROUGH_LOCAL === '1',
|
|
81
87
|
ENABLE_AUTH_PROTECTION: process.env.ENABLE_AUTH_PROTECTION === '1',
|
|
82
88
|
|
|
@@ -202,6 +202,12 @@ export default {
|
|
|
202
202
|
all: '全部',
|
|
203
203
|
list: {
|
|
204
204
|
disabled: '未启用',
|
|
205
|
+
disabledActions: {
|
|
206
|
+
sort: '排序方式',
|
|
207
|
+
sortAlphabetical: '按字母排序',
|
|
208
|
+
sortAlphabeticalDesc: '按字母倒序排序',
|
|
209
|
+
sortDefault: '默认排序',
|
|
210
|
+
},
|
|
205
211
|
enabled: '已启用',
|
|
206
212
|
},
|
|
207
213
|
notFound: '未找到搜索结果',
|
|
@@ -399,7 +405,15 @@ export default {
|
|
|
399
405
|
list: {
|
|
400
406
|
addNew: '添加模型',
|
|
401
407
|
disabled: '未启用',
|
|
402
|
-
disabledActions: {
|
|
408
|
+
disabledActions: {
|
|
409
|
+
showMore: '显示全部',
|
|
410
|
+
sort: '排序方式',
|
|
411
|
+
sortAlphabetical: '按字母排序',
|
|
412
|
+
sortAlphabeticalDesc: '按字母倒序排序',
|
|
413
|
+
sortDefault: '默认排序',
|
|
414
|
+
sortReleasedAt: '按最早发布时间排序',
|
|
415
|
+
sortReleasedAtDesc: '按最新发布时间排序',
|
|
416
|
+
},
|
|
403
417
|
empty: {
|
|
404
418
|
desc: '请创建自定义模型或拉取模型后开始使用吧',
|
|
405
419
|
title: '暂无可用模型',
|