@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.
Files changed (90) hide show
  1. package/.github/workflows/test.yml +1 -0
  2. package/CHANGELOG.md +58 -0
  3. package/apps/desktop/package.json +1 -1
  4. package/changelog/v1.json +21 -0
  5. package/docker-compose/local/.env.example +3 -0
  6. package/docs/self-hosting/server-database/docker-compose.mdx +29 -0
  7. package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +29 -0
  8. package/package.json +1 -1
  9. package/packages/const/src/hotkeys.ts +3 -3
  10. package/packages/const/src/models.ts +2 -2
  11. package/packages/const/src/utils/merge.ts +3 -3
  12. package/packages/conversation-flow/package.json +13 -0
  13. package/packages/conversation-flow/src/__tests__/fixtures/index.ts +48 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-chain-with-followup.json +56 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-with-tools.json +144 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/active-index-1.json +131 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-branch.json +96 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-user-branch.json +123 -0
  19. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/conversation.json +128 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +14 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/nested.json +179 -0
  22. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/index.ts +8 -0
  23. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/simple.json +85 -0
  24. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/with-tools.json +169 -0
  25. package/packages/conversation-flow/src/__tests__/fixtures/inputs/complex-scenario.json +107 -0
  26. package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +14 -0
  27. package/packages/conversation-flow/src/__tests__/fixtures/inputs/linear-conversation.json +59 -0
  28. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-chain-with-followup.json +135 -0
  29. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-with-tools.json +340 -0
  30. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +242 -0
  31. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-branch.json +208 -0
  32. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-user-branch.json +254 -0
  33. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +260 -0
  34. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +14 -0
  35. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +389 -0
  36. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/index.ts +8 -0
  37. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/simple.json +224 -0
  38. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/with-tools.json +418 -0
  39. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +239 -0
  40. package/packages/conversation-flow/src/__tests__/fixtures/outputs/linear-conversation.json +138 -0
  41. package/packages/conversation-flow/src/__tests__/parse.test.ts +97 -0
  42. package/packages/conversation-flow/src/index.ts +17 -0
  43. package/packages/conversation-flow/src/indexing.ts +58 -0
  44. package/packages/conversation-flow/src/parse.ts +53 -0
  45. package/packages/conversation-flow/src/structuring.ts +38 -0
  46. package/packages/conversation-flow/src/transformation/BranchResolver.ts +66 -0
  47. package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +292 -0
  48. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +421 -0
  49. package/packages/conversation-flow/src/transformation/MessageCollector.ts +166 -0
  50. package/packages/conversation-flow/src/transformation/MessageTransformer.ts +177 -0
  51. package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +151 -0
  52. package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +385 -0
  53. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +511 -0
  54. package/packages/conversation-flow/src/transformation/__tests__/MessageCollector.test.ts +220 -0
  55. package/packages/conversation-flow/src/transformation/__tests__/MessageTransformer.test.ts +287 -0
  56. package/packages/conversation-flow/src/transformation/index.ts +78 -0
  57. package/packages/conversation-flow/src/types/contextTree.ts +65 -0
  58. package/packages/conversation-flow/src/types/flatMessageList.ts +66 -0
  59. package/packages/conversation-flow/src/types/shared.ts +63 -0
  60. package/packages/conversation-flow/src/types.ts +36 -0
  61. package/packages/conversation-flow/vitest.config.mts +10 -0
  62. package/packages/model-bank/src/aiModels/google.ts +1 -1
  63. package/packages/types/src/message/common/metadata.ts +5 -1
  64. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +3 -4
  65. package/src/app/[variants]/(main)/settings/provider/ProviderMenu/List.tsx +97 -7
  66. package/src/app/[variants]/(main)/settings/provider/features/ModelList/DisabledModels.tsx +144 -8
  67. package/src/envs/__tests__/app.test.ts +47 -13
  68. package/src/envs/app.ts +6 -0
  69. package/src/locales/default/modelProvider.ts +15 -1
  70. package/src/server/routers/async/__tests__/caller.test.ts +333 -0
  71. package/src/server/routers/async/caller.ts +2 -1
  72. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +57 -57
  73. package/src/server/routers/lambda/message.ts +2 -2
  74. package/src/server/services/mcp/deps/checkers/ManualInstallationChecker.test.ts +162 -0
  75. package/src/server/services/mcp/deps/checkers/NpmInstallationChecker.test.ts +374 -0
  76. package/src/server/services/mcp/deps/checkers/PythonInstallationChecker.test.ts +368 -0
  77. package/src/server/services/message/__tests__/index.test.ts +4 -4
  78. package/src/server/services/message/index.ts +1 -1
  79. package/src/services/message/index.ts +2 -3
  80. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +8 -8
  81. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +8 -8
  82. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +1 -1
  83. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +1 -1
  84. package/src/store/chat/slices/message/action.test.ts +7 -7
  85. package/src/store/chat/slices/message/action.ts +2 -2
  86. package/src/store/chat/slices/plugin/action.test.ts +7 -7
  87. package/src/store/chat/slices/plugin/action.ts +1 -1
  88. package/src/store/global/initialState.ts +4 -0
  89. package/src/store/global/selectors/systemStatus.ts +6 -0
  90. 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';
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ coverage: {
6
+ reporter: ['text', 'json', 'lcov', 'text-summary'],
7
+ },
8
+ environment: 'happy-dom',
9
+ },
10
+ });
@@ -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: '2024-06-06',
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
+ }
@@ -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
- const showModel = sessionType === 'agent' && model && model !== defaultModel;
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
- <Text style={{ fontSize: 12, marginTop: 8 }} type={'secondary'}>
67
- {t('menu.list.disabled')}
68
- </Text>
69
- {disabledModelProviderList.map((item) => (
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
- const displayModels = showMore ? filteredDisabledModels : filteredDisabledModels.slice(0, 10);
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
- <Text style={{ fontSize: 12, marginTop: 8 }} type={'secondary'}>
35
- {t('providerModels.list.disabled')}
36
- </Text>
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 && filteredDisabledModels.length > 10 && (
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 environment variables before each test case
15
- vi.restoreAllMocks();
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: { showMore: '显示全部' },
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: '暂无可用模型',