@lobehub/lobehub 2.0.0-next.134 → 2.0.0-next.136

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.
@@ -364,6 +364,13 @@
364
364
  "when": 1764335703306,
365
365
  "tag": "0051_add_market_into_user_settings",
366
366
  "breakpoints": true
367
+ },
368
+ {
369
+ "idx": 52,
370
+ "version": "7",
371
+ "when": 1764500630663,
372
+ "tag": "0052_topic_and_messages",
373
+ "breakpoints": true
367
374
  }
368
375
  ],
369
376
  "version": "6"
@@ -844,5 +844,20 @@
844
844
  "bps": true,
845
845
  "folderMillis": 1764335703306,
846
846
  "hash": "28c0d738c0b1fdf5fd871363be1a1477b4accbabdc140fe8dc6e9b339aae2c89"
847
+ },
848
+ {
849
+ "sql": [
850
+ "ALTER TABLE \"messages\" DROP CONSTRAINT \"messages_agent_id_agents_id_fk\";\n",
851
+ "\nALTER TABLE \"messages\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;",
852
+ "\nALTER TABLE \"topics\" ADD COLUMN IF NOT EXISTS \"content\" text;",
853
+ "\nALTER TABLE \"topics\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;",
854
+ "\nALTER TABLE \"topics\" ADD COLUMN IF NOT EXISTS \"agent_id\" text;",
855
+ "\nALTER TABLE \"messages\" ADD CONSTRAINT \"messages_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;",
856
+ "\nALTER TABLE \"topics\" ADD CONSTRAINT \"topics_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;",
857
+ "\nCREATE INDEX IF NOT EXISTS \"topics_agent_id_idx\" ON \"topics\" USING btree (\"agent_id\");\n"
858
+ ],
859
+ "bps": true,
860
+ "folderMillis": 1764500630663,
861
+ "hash": "94721bc06910a456a4756c9b0c27ef5d7ff55b7ea8c772acf58052c0155c693b"
847
862
  }
848
863
  ]
@@ -366,9 +366,9 @@ describe('TopicModel', () => {
366
366
  await topicModel.batchDeleteByGroupId('group1');
367
367
 
368
368
  // 断言属于 group1 的 topics 都被删除了
369
- expect(
370
- await serverDB.select().from(topics).where(eq(topics.groupId, 'group1')),
371
- ).toHaveLength(0);
369
+ expect(await serverDB.select().from(topics).where(eq(topics.groupId, 'group1'))).toHaveLength(
370
+ 0,
371
+ );
372
372
  expect(await serverDB.select().from(topics)).toHaveLength(2);
373
373
  });
374
374
 
@@ -385,9 +385,9 @@ describe('TopicModel', () => {
385
385
  await topicModel.batchDeleteByGroupId();
386
386
 
387
387
  // 断言属于 group1 的 topics 都被删除了
388
- expect(
389
- await serverDB.select().from(topics).where(eq(topics.groupId, 'group1')),
390
- ).toHaveLength(2);
388
+ expect(await serverDB.select().from(topics).where(eq(topics.groupId, 'group1'))).toHaveLength(
389
+ 2,
390
+ );
391
391
  expect(await serverDB.select().from(topics)).toHaveLength(2);
392
392
  });
393
393
  });
@@ -506,6 +506,9 @@ describe('TopicModel', () => {
506
506
  metadata: null,
507
507
  groupId: null,
508
508
  clientId: null,
509
+ agentId: null,
510
+ content: null,
511
+ editorData: null,
509
512
  createdAt: expect.any(Date),
510
513
  updatedAt: expect.any(Date),
511
514
  accessedAt: expect.any(Date),
@@ -551,6 +554,9 @@ describe('TopicModel', () => {
551
554
  title: 'New Topic',
552
555
  favorite: false,
553
556
  clientId: null,
557
+ agentId: null,
558
+ content: null,
559
+ editorData: null,
554
560
  groupId: null,
555
561
  historySummary: null,
556
562
  metadata: null,
@@ -83,6 +83,7 @@ export const messages = pgTable(
83
83
 
84
84
  role: varchar255('role').notNull(),
85
85
  content: text('content'),
86
+ editorData: jsonb('editor_data'),
86
87
  reasoning: jsonb('reasoning').$type<ModelReasoning>(),
87
88
  search: jsonb('search').$type<GroundingSearch>(),
88
89
  metadata: jsonb('metadata'),
@@ -104,6 +105,9 @@ export const messages = pgTable(
104
105
  userId: text('user_id')
105
106
  .references(() => users.id, { onDelete: 'cascade' })
106
107
  .notNull(),
108
+ /**
109
+ * we might deprecate sessionId in the future
110
+ */
107
111
  sessionId: text('session_id').references(() => sessions.id, { onDelete: 'cascade' }),
108
112
  topicId: text('topic_id').references(() => topics.id, { onDelete: 'cascade' }),
109
113
  threadId: text('thread_id').references(() => threads.id, { onDelete: 'cascade' }),
@@ -111,8 +115,7 @@ export const messages = pgTable(
111
115
  parentId: text('parent_id').references(() => messages.id, { onDelete: 'set null' }),
112
116
  quotaId: text('quota_id').references(() => messages.id, { onDelete: 'set null' }),
113
117
 
114
- // used for group chat
115
- agentId: text('agent_id').references(() => agents.id, { onDelete: 'set null' }),
118
+ agentId: text('agent_id').references(() => agents.id, { onDelete: 'cascade' }),
116
119
  groupId: text('group_id').references(() => chatGroups.id, { onDelete: 'set null' }),
117
120
  // targetId can be an agent ID, "user", or null - no FK constraint
118
121
  targetId: text('target_id'),
@@ -5,6 +5,7 @@ import { createInsertSchema } from 'drizzle-zod';
5
5
 
6
6
  import { idGenerator } from '../utils/idGenerator';
7
7
  import { createdAt, timestamps, timestamptz } from './_helpers';
8
+ import { agents } from './agent';
8
9
  import { chatGroups } from './chatGroup';
9
10
  import { documents } from './file';
10
11
  import { sessions } from './session';
@@ -19,6 +20,9 @@ export const topics = pgTable(
19
20
  title: text('title'),
20
21
  favorite: boolean('favorite').default(false),
21
22
  sessionId: text('session_id').references(() => sessions.id, { onDelete: 'cascade' }),
23
+ content: text('content'),
24
+ editorData: jsonb('editor_data'),
25
+ agentId: text('agent_id').references(() => agents.id, { onDelete: 'cascade' }),
22
26
  groupId: text('group_id').references(() => chatGroups.id, { onDelete: 'cascade' }),
23
27
  userId: text('user_id')
24
28
  .references(() => users.id, { onDelete: 'cascade' })
@@ -34,6 +38,7 @@ export const topics = pgTable(
34
38
  index('topics_id_user_id_idx').on(t.id, t.userId),
35
39
  index('topics_session_id_idx').on(t.sessionId),
36
40
  index('topics_group_id_idx').on(t.groupId),
41
+ index('topics_agent_id_idx').on(t.agentId),
37
42
  ],
38
43
  );
39
44
 
@@ -12,8 +12,8 @@
12
12
  "test:update": "vitest -u"
13
13
  },
14
14
  "dependencies": {
15
- "@aws-sdk/client-bedrock-runtime": "^3.932.0",
16
- "@huggingface/inference": "^4.13.3",
15
+ "@aws-sdk/client-bedrock-runtime": "^3.941.0",
16
+ "@huggingface/inference": "^4.13.4",
17
17
  "@lobechat/const": "workspace:*",
18
18
  "@lobechat/types": "workspace:*",
19
19
  "@lobechat/utils": "workspace:*",
@@ -32,7 +32,7 @@ def patch_matplotlib():
32
32
 
33
33
  patch_matplotlib()`;
34
34
 
35
- // Pyodide 对象不能在 Worker 之间传递,因此定义为全局变量
35
+ // Pyodide object cannot be transferred between Workers, so it's defined as a global variable
36
36
  let pyodide: PyodideAPI | undefined;
37
37
 
38
38
  class PythonWorker {
@@ -56,7 +56,7 @@ class PythonWorker {
56
56
  }
57
57
 
58
58
  /**
59
- * 初始化 Python 解释器
59
+ * Initialize Python interpreter
60
60
  */
61
61
  async init() {
62
62
  pyodide = await globalThis.loadPyodide({
@@ -67,13 +67,13 @@ class PythonWorker {
67
67
  }
68
68
 
69
69
  /**
70
- * 上传文件到解释器环境中
71
- * @param files 文件列表
70
+ * Upload files to the interpreter environment
71
+ * @param files File list
72
72
  */
73
73
  async uploadFiles(files: File[]) {
74
74
  for (const file of files) {
75
75
  const content = new Uint8Array(await file.arrayBuffer());
76
- // TODO: 此处可以考虑使用 WORKERFS 减少一次拷贝
76
+ // TODO: Consider using WORKERFS here to reduce one copy operation
77
77
  if (file.name.startsWith('/')) {
78
78
  this.pyodide.FS.writeFile(file.name, content);
79
79
  } else {
@@ -84,15 +84,15 @@ class PythonWorker {
84
84
  }
85
85
 
86
86
  /**
87
- * 从解释器环境中下载变动的文件
88
- * @param files 文件列表
87
+ * Download modified files from the interpreter environment
88
+ * @param files File list
89
89
  */
90
90
  async downloadFiles() {
91
91
  const result: File[] = [];
92
92
  for (const entry of this.pyodide.FS.readdir('/mnt/data')) {
93
93
  if (entry === '.' || entry === '..') continue;
94
94
  const filePath = `/mnt/data/${entry}`;
95
- // pyodide FS 类型定义有问题,只能采用 any
95
+ // pyodide's FS type definition has issues, have to use any
96
96
  const content = (this.pyodide.FS as any).readFile(filePath, { encoding: 'binary' });
97
97
  const blob = new Blob([content]);
98
98
  const file = new File([blob], filePath);
@@ -104,8 +104,8 @@ class PythonWorker {
104
104
  }
105
105
 
106
106
  /**
107
- * 安装 Python
108
- * @param packages 包名列表
107
+ * Install Python packages
108
+ * @param packages Package name list
109
109
  */
110
110
  async installPackages(packages: string[]) {
111
111
  await this.pyodide.loadPackage('micropip');
@@ -115,16 +115,16 @@ class PythonWorker {
115
115
  }
116
116
 
117
117
  /**
118
- * 执行 Python 代码
119
- * @param code 代码
118
+ * Execute Python code
119
+ * @param code Code
120
120
  */
121
121
  async runPython(code: string): Promise<PythonResult> {
122
122
  await this.patchFonts();
123
- // NOTE: loadPackagesFromImports 只会处理 pyodide 官方包
123
+ // NOTE: loadPackagesFromImports only processes official pyodide packages
124
124
  await this.pyodide.loadPackagesFromImports(code);
125
125
  await this.patchPackages();
126
126
 
127
- // 安装依赖后再捕获标准输出,避免记录安装日志
127
+ // Capture standard output after installing dependencies to avoid logging installation messages
128
128
  const output: PythonOutput[] = [];
129
129
  this.pyodide.setStdout({
130
130
  batched: (o: string) => {
@@ -137,7 +137,7 @@ class PythonWorker {
137
137
  },
138
138
  });
139
139
 
140
- // 执行代码
140
+ // Execute code
141
141
  let result;
142
142
  let success = false;
143
143
  try {
@@ -172,15 +172,15 @@ class PythonWorker {
172
172
  };
173
173
  for (const [filename, url] of Object.entries(fontFiles)) {
174
174
  const buffer = await fetch(url, { cache: 'force-cache' }).then((res) => res.arrayBuffer());
175
- // NOTE: 此处理论上使用 createLazyFile 更好,但 pyodide 中使用会导致报错
175
+ // NOTE: In theory, createLazyFile would be better here, but it causes errors in pyodide
176
176
  this.pyodide.FS.writeFile(`/usr/share/fonts/truetype/${filename}`, new Uint8Array(buffer));
177
177
  }
178
178
  }
179
179
 
180
180
  private async isNewFile(file: File) {
181
181
  const isSameFile = async (a: File, b: File) => {
182
- // a 是传入的文件,可能使用了绝对路径或相对路径
183
- // b 是解释器环境中的文件,使用绝对路径
182
+ // a is the passed-in file, may use absolute or relative path
183
+ // b is the file in the interpreter environment, uses absolute path
184
184
  if (a.name.startsWith('/')) {
185
185
  if (a.name !== b.name) return false;
186
186
  } else {
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@mozilla/readability": "^0.6.0",
13
- "happy-dom": "^20.0.10",
13
+ "happy-dom": "^20.0.11",
14
14
  "node-html-markdown": "^1.3.0",
15
15
  "ssrf-safe-fetch": "workspace:*",
16
16
  "query-string": "^9.3.1",
@@ -23,6 +23,10 @@ const List = memo((props: ListProps) => {
23
23
  const { t } = useTranslation('modelProvider');
24
24
  const enabledList = useAiInfraStore(aiProviderSelectors.enabledAiProviderList, isEqual);
25
25
  const disabledList = useAiInfraStore(aiProviderSelectors.disabledAiProviderList, isEqual);
26
+ const disabledCustomList = useAiInfraStore(
27
+ aiProviderSelectors.disabledCustomAiProviderList,
28
+ isEqual,
29
+ );
26
30
  const [initAiProviderList] = useAiInfraStore((s) => [s.initAiProviderList]);
27
31
 
28
32
  if (!initAiProviderList)
@@ -63,6 +67,21 @@ const List = memo((props: ListProps) => {
63
67
  ))}
64
68
  </Grid>
65
69
  </Flexbox>
70
+ {disabledCustomList.length > 0 && (
71
+ <Flexbox gap={24}>
72
+ <Flexbox align={'center'} gap={8} horizontal>
73
+ <Text strong style={{ fontSize: 18 }}>
74
+ {t('list.title.custom')}
75
+ </Text>
76
+ <Tag>{disabledCustomList.length}</Tag>
77
+ </Flexbox>
78
+ <Grid gap={16} rows={3}>
79
+ {disabledCustomList.map((item) => (
80
+ <Card {...item} key={item.id} onProviderSelect={onProviderSelect} />
81
+ ))}
82
+ </Grid>
83
+ </Flexbox>
84
+ )}
66
85
  <Flexbox gap={24}>
67
86
  <Flexbox align={'center'} gap={8} horizontal>
68
87
  <Text strong style={{ fontSize: 18 }}>
@@ -1,10 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { ActionIcon, Dropdown, Icon, ScrollShadow, Text } from '@lobehub/ui';
3
+ import { ActionIcon, Dropdown, Icon, ScrollShadow } from '@lobehub/ui';
4
+ import { Collapse } from 'antd';
5
+ import { createStyles } from 'antd-style';
4
6
  import type { ItemType } from 'antd/es/menu/interface';
5
7
  import isEqual from 'fast-deep-equal';
6
- import { ArrowDownUpIcon, LucideCheck } from 'lucide-react';
7
- import { useCallback, useMemo, useState } from 'react';
8
+ import { ArrowDownUpIcon, ChevronDownIcon, LucideCheck } from 'lucide-react';
9
+ import { type ReactNode, useCallback, useMemo, useState } from 'react';
8
10
  import { useTranslation } from 'react-i18next';
9
11
  import { Flexbox } from 'react-layout-kit';
10
12
 
@@ -24,14 +26,68 @@ enum SortType {
24
26
  Default = 'default',
25
27
  }
26
28
 
29
+ const useStyles = createStyles(({ css, token }) => ({
30
+ collapse: css`
31
+ &.ant-collapse {
32
+ border: none;
33
+ border-radius: 0;
34
+ background: transparent;
35
+ }
36
+
37
+ .ant-collapse-item {
38
+ border: none !important;
39
+ }
40
+
41
+ .ant-collapse-header {
42
+ padding: 0 !important;
43
+ padding-block: 8px !important;
44
+
45
+ font-size: 12px !important;
46
+ color: ${token.colorTextSecondary} !important;
47
+
48
+ background: transparent !important;
49
+ }
50
+
51
+ .ant-collapse-content {
52
+ border: none !important;
53
+ background: transparent !important;
54
+ }
55
+
56
+ .ant-collapse-content-box {
57
+ padding: 0 !important;
58
+ }
59
+
60
+ .ant-collapse-expand-icon {
61
+ padding-inline-end: 4px !important;
62
+ }
63
+ `,
64
+ sectionHeader: css`
65
+ cursor: pointer;
66
+
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: space-between;
70
+
71
+ margin-block-start: 8px;
72
+ padding-block: 4px;
73
+
74
+ font-size: 12px;
75
+ color: ${token.colorTextSecondary};
76
+ `,
77
+ }));
78
+
27
79
  const ProviderList = (props: {
28
80
  mobile?: boolean;
29
81
  onProviderSelect: (providerKey: string) => void;
30
82
  }) => {
31
83
  const { onProviderSelect, mobile } = props;
32
84
  const { t } = useTranslation('modelProvider');
85
+ const { styles } = useStyles();
33
86
  const [open, setOpen] = useState(false);
34
87
 
88
+ // Collapse states - using array of active keys
89
+ const [activeKeys, setActiveKeys] = useState<string[]>(['enabled', 'custom', 'disabled']);
90
+
35
91
  const [sortType, updateSystemStatus] = useGlobalStore((s) => [
36
92
  systemStatusSelectors.disabledModelProvidersSortType(s),
37
93
  s.updateSystemStatus,
@@ -54,6 +110,11 @@ const ProviderList = (props: {
54
110
  isEqual,
55
111
  );
56
112
 
113
+ const disabledCustomProviderList = useAiInfraStore(
114
+ aiProviderSelectors.disabledCustomAiProviderList,
115
+ isEqual,
116
+ );
117
+
57
118
  // Sort model providers based on sort type
58
119
  const sortedDisabledProviders = useMemo(() => {
59
120
  const providers = [...disabledModelProviderList];
@@ -81,84 +142,150 @@ const ProviderList = (props: {
81
142
  }
82
143
  }, [disabledModelProviderList, sortType]);
83
144
 
145
+ const collapseItems = useMemo(() => {
146
+ const items: {
147
+ children: ReactNode;
148
+ extra?: ReactNode;
149
+ key: string;
150
+ label: string;
151
+ }[] = [
152
+ {
153
+ children: (
154
+ <Flexbox gap={0}>
155
+ {enabledModelProviderList.map((item) => (
156
+ <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
157
+ ))}
158
+ </Flexbox>
159
+ ),
160
+ extra: (
161
+ <div onClick={(e) => e.stopPropagation()}>
162
+ <ActionIcon
163
+ icon={ArrowDownUpIcon}
164
+ onClick={() => setOpen(true)}
165
+ size={'small'}
166
+ title={t('menu.sort')}
167
+ />
168
+ </div>
169
+ ),
170
+ key: 'enabled',
171
+ label: t('menu.list.enabled'),
172
+ },
173
+ ];
174
+
175
+ // Add custom providers section if there are any
176
+ if (disabledCustomProviderList.length > 0) {
177
+ items.push({
178
+ children: (
179
+ <Flexbox gap={0}>
180
+ {disabledCustomProviderList.map((item) => (
181
+ <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
182
+ ))}
183
+ </Flexbox>
184
+ ),
185
+ key: 'custom',
186
+ label: t('menu.list.custom'),
187
+ });
188
+ }
189
+
190
+ // Add disabled providers section
191
+ items.push({
192
+ children: (
193
+ <Flexbox gap={0}>
194
+ {sortedDisabledProviders.map((item) => (
195
+ <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
196
+ ))}
197
+ </Flexbox>
198
+ ),
199
+ extra:
200
+ disabledModelProviderList.length > 1 ? (
201
+ <div onClick={(e) => e.stopPropagation()}>
202
+ <Dropdown
203
+ menu={{
204
+ items: [
205
+ {
206
+ icon: sortType === SortType.Default ? <Icon icon={LucideCheck} /> : <div />,
207
+ key: 'default',
208
+ label: t('menu.list.disabledActions.sortDefault'),
209
+ onClick: () => updateSortType(SortType.Default),
210
+ },
211
+ {
212
+ type: 'divider',
213
+ },
214
+ {
215
+ icon:
216
+ sortType === SortType.Alphabetical ? <Icon icon={LucideCheck} /> : <div />,
217
+ key: 'alphabetical',
218
+ label: t('menu.list.disabledActions.sortAlphabetical'),
219
+ onClick: () => updateSortType(SortType.Alphabetical),
220
+ },
221
+ {
222
+ icon:
223
+ sortType === SortType.AlphabeticalDesc ? (
224
+ <Icon icon={LucideCheck} />
225
+ ) : (
226
+ <div />
227
+ ),
228
+ key: 'alphabeticalDesc',
229
+ label: t('menu.list.disabledActions.sortAlphabeticalDesc'),
230
+ onClick: () => updateSortType(SortType.AlphabeticalDesc),
231
+ },
232
+ ] as ItemType[],
233
+ }}
234
+ trigger={['click']}
235
+ >
236
+ <ActionIcon
237
+ icon={ArrowDownUpIcon}
238
+ size={'small'}
239
+ title={t('menu.list.disabledActions.sort')}
240
+ />
241
+ </Dropdown>
242
+ </div>
243
+ ) : undefined,
244
+ key: 'disabled',
245
+ label: t('menu.list.disabled'),
246
+ });
247
+
248
+ return items;
249
+ }, [
250
+ enabledModelProviderList,
251
+ disabledCustomProviderList,
252
+ sortedDisabledProviders,
253
+ disabledModelProviderList.length,
254
+ sortType,
255
+ t,
256
+ onProviderSelect,
257
+ updateSortType,
258
+ ]);
259
+
84
260
  return (
85
261
  <ScrollShadow gap={4} height={'100%'} paddingInline={12} size={4} style={{ paddingBottom: 32 }}>
86
262
  {!mobile && <All onClick={onProviderSelect} />}
87
- <Flexbox
88
- align={'center'}
89
- horizontal
90
- justify={'space-between'}
91
- style={{ fontSize: 12, marginTop: 8 }}
92
- >
93
- <Text style={{ fontSize: 12 }} type={'secondary'}>
94
- {t('menu.list.enabled')}
95
- </Text>
96
- <ActionIcon
97
- icon={ArrowDownUpIcon}
98
- onClick={() => {
99
- setOpen(true);
263
+ {open && (
264
+ <SortProviderModal
265
+ defaultItems={enabledModelProviderList}
266
+ onCancel={() => {
267
+ setOpen(false);
100
268
  }}
101
- size={'small'}
102
- title={t('menu.sort')}
269
+ open={open}
103
270
  />
104
- {open && (
105
- <SortProviderModal
106
- defaultItems={enabledModelProviderList}
107
- onCancel={() => {
108
- setOpen(false);
271
+ )}
272
+ <Collapse
273
+ activeKey={activeKeys}
274
+ className={styles.collapse}
275
+ expandIcon={({ isActive }) => (
276
+ <Icon
277
+ icon={ChevronDownIcon}
278
+ size={'small'}
279
+ style={{
280
+ transform: isActive ? 'rotate(0deg)' : 'rotate(-90deg)',
281
+ transition: 'transform 0.2s ease',
109
282
  }}
110
- open={open}
111
283
  />
112
284
  )}
113
- </Flexbox>
114
- {enabledModelProviderList.map((item) => (
115
- <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
116
- ))}
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) => (
160
- <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
161
- ))}
285
+ ghost
286
+ items={collapseItems}
287
+ onChange={(keys) => setActiveKeys(keys as string[])}
288
+ />
162
289
  </ScrollShadow>
163
290
  );
164
291
  };
@@ -15,7 +15,7 @@ const ClientMode = memo<{ id: string }>(({ id }) => {
15
15
  const useFetchAiProviderItem = useAiInfraStore((s) => s.useFetchAiProviderItem);
16
16
  useFetchAiProviderItem(id);
17
17
 
18
- const { data, isLoading } = useClientDataSWR('get-client-provider', () =>
18
+ const { data, isLoading } = useClientDataSWR(`get-client-provider-${id}`, () =>
19
19
  aiProviderService.getAiProviderById(id),
20
20
  );
21
21
 
package/src/auth.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  getVerificationEmailTemplate,
12
12
  } from '@/libs/better-auth/email-templates';
13
13
  import { initBetterAuthSSOProviders } from '@/libs/better-auth/sso';
14
+ import { parseSSOProviders } from '@/libs/better-auth/utils/server';
14
15
  import { EmailService } from '@/server/services/email';
15
16
 
16
17
  // Email verification link expiration time (in seconds)
@@ -62,6 +63,7 @@ export const auth = betterAuth({
62
63
  accountLinking: {
63
64
  allowDifferentEmails: true,
64
65
  enabled: true,
66
+ trustedProviders: parseSSOProviders(authEnv.AUTH_SSO_PROVIDERS),
65
67
  },
66
68
  },
67
69
 
@@ -193,6 +193,7 @@ export default {
193
193
  },
194
194
  list: {
195
195
  title: {
196
+ custom: '未启用自定义服务商',
196
197
  disabled: '未启用服务商',
197
198
  enabled: '已启用服务商',
198
199
  },
@@ -201,6 +202,7 @@ export default {
201
202
  addCustomProvider: '添加自定义服务商',
202
203
  all: '全部',
203
204
  list: {
205
+ custom: '自定义未启用',
204
206
  disabled: '未启用',
205
207
  disabledActions: {
206
208
  sort: '排序方式',