@lobehub/lobehub 2.0.0-next.243 → 2.0.0-next.245

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.
@@ -61,26 +61,41 @@ jobs:
61
61
 
62
62
  - name: Upload coverage to Codecov
63
63
  if: always()
64
+ env:
65
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
64
66
  run: |
65
67
  curl -Os https://cli.codecov.io/latest/linux/codecov
66
68
  chmod +x codecov
67
- TOKEN="${{ secrets.CODECOV_TOKEN }}"
69
+
70
+ # Build common args
71
+ COMMON_ARGS="--git-service github"
72
+
73
+ # PR args setup
74
+ if [ "${{ github.event_name }}" == "pull_request" ]; then
75
+ COMMON_ARGS="$COMMON_ARGS --pr ${{ github.event.pull_request.number }}"
76
+ COMMON_ARGS="$COMMON_ARGS --sha ${{ github.event.pull_request.head.sha }}"
77
+ # Fork PR needs username:branch format for tokenless upload
78
+ if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
79
+ COMMON_ARGS="$COMMON_ARGS --branch ${{ github.event.pull_request.head.label }}"
80
+ else
81
+ COMMON_ARGS="$COMMON_ARGS --branch ${{ github.event.pull_request.head.ref }}"
82
+ fi
83
+ fi
84
+
85
+ # Token (if available)
86
+ if [ -n "$CODECOV_TOKEN" ]; then
87
+ COMMON_ARGS="$COMMON_ARGS -t $CODECOV_TOKEN"
88
+ fi
89
+
68
90
  for package in $PACKAGES; do
69
91
  dir="${package#@lobechat/}"
70
92
  if [ -f "./packages/$dir/coverage/lcov.info" ]; then
71
93
  echo "Uploading coverage for $dir..."
72
- if [ -n "$TOKEN" ]; then
73
- ./codecov upload-process \
74
- -t "$TOKEN" \
75
- -f ./packages/$dir/coverage/lcov.info \
76
- -F packages/$dir \
77
- --disable-search
78
- else
79
- ./codecov upload-process \
80
- -f ./packages/$dir/coverage/lcov.info \
81
- -F packages/$dir \
82
- --disable-search
83
- fi
94
+ ./codecov upload-coverage \
95
+ $COMMON_ARGS \
96
+ --file ./packages/$dir/coverage/lcov.info \
97
+ --flag packages/$dir \
98
+ --disable-search
84
99
  fi
85
100
  done
86
101
 
@@ -111,7 +126,7 @@ jobs:
111
126
  run: bun i
112
127
 
113
128
  - name: Run tests
114
- run: bunx vitest --coverage --silent='passed-only' --shard=${{ matrix.shard }}/2
129
+ run: bunx vitest --coverage --silent='passed-only' --reporter=default --reporter=blob --shard=${{ matrix.shard }}/2
115
130
 
116
131
  - name: Upload blob report
117
132
  if: ${{ !cancelled() }}
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.245](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.244...v2.0.0-next.245)
6
+
7
+ <sup>Released on **2026-01-09**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Improve Tools popover component structure and fix UI consistency.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Improve Tools popover component structure and fix UI consistency, closes [#11356](https://github.com/lobehub/lobe-chat/issues/11356) ([f46837a](https://github.com/lobehub/lobe-chat/commit/f46837a))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 2.0.0-next.244](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.243...v2.0.0-next.244)
31
+
32
+ <sup>Released on **2026-01-08**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **api**: Fix the issue where custom AI Providers cannot use custom APIs.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **api**: Fix the issue where custom AI Providers cannot use custom APIs, closes [#11335](https://github.com/lobehub/lobe-chat/issues/11335) ([2c666b8](https://github.com/lobehub/lobe-chat/commit/2c666b8))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## [Version 2.0.0-next.243](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.242...v2.0.0-next.243)
6
56
 
7
57
  <sup>Released on **2026-01-08**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Improve Tools popover component structure and fix UI consistency."
6
+ ]
7
+ },
8
+ "date": "2026-01-09",
9
+ "version": "2.0.0-next.245"
10
+ },
11
+ {
12
+ "children": {},
13
+ "date": "2026-01-08",
14
+ "version": "2.0.0-next.244"
15
+ },
2
16
  {
3
17
  "children": {},
4
18
  "date": "2026-01-08",
@@ -245,7 +245,7 @@
245
245
  "qa.list.embeddings.q": "How is vector storage calculated?",
246
246
  "qa.list.free.a": "{{name}} has always adhered to open source principles. For professional developers, you can use all open source capabilities through self-deployment of the community version. In {{cloud}}, we provide all registered users with {{credit}} free computing credits per month, ready to use without complex configuration. If you need more usage, you can subscribe to {{starter}}, {{premium}} or {{ultimate}}.",
247
247
  "qa.list.free.q": "Can {{name}} be used for free?",
248
- "qa.list.limit.a": "{{cloud}} subscription plans are divided into {{starter}}, {{premium}} and {{ultimate}}, each providing different computing credits. If your current plan credits are insufficient, we recommend upgrading. Alternatively, you can set up a custom model API key to use API credits purchased from other sources.",
248
+ "qa.list.limit.a": "{{cloud}} subscription plans are divided into {{starter}}, {{premium}} and {{ultimate}}, each providing different computing credits. If your current plan credits are insufficient, we recommend upgrading. You can also purchase credit packages on the <fundsLink>\"{{funds}}\"</fundsLink> page for pay-as-you-go usage. Alternatively, you can set up a custom model API key to use API credits purchased from other sources.",
249
249
  "qa.list.limit.q": "What if I run out of computing credits?",
250
250
  "qa.list.management.a": "On the {{subscribe}} page, you can \"Upgrade / Downgrade\" your current subscription plan, or switch between yearly and monthly billing. Through \"{{usage}}-{{management}}\" you can go to Stripe for subscription management, and you can cancel your subscription at any time. After cancellation, you will be automatically downgraded to the free version when your current plan expires.",
251
251
  "qa.list.management.q": "How do I change or cancel my subscription?",
@@ -245,7 +245,7 @@
245
245
  "qa.list.embeddings.q": "向量存储是如何计算的?",
246
246
  "qa.list.free.a": "{{name}} 始终坚持开源原则。专业开发者可通过部署社区版使用全部开源功能。在 {{cloud}} 中,我们为所有注册用户每月提供 {{credit}} 点免费算力,无需复杂配置即可使用。如需更多使用量,可订阅 {{starter}}、{{premium}} 或 {{ultimate}}。",
247
247
  "qa.list.free.q": "{{name}} 可以免费使用吗?",
248
- "qa.list.limit.a": "{{cloud}} 的订阅计划分为 {{starter}}、{{premium}} 和 {{ultimate}},每个计划提供不同的算力点数。如当前计划点数不足,建议升级计划。您也可以配置自定义模型 API 密钥,使用其他来源购买的 API 点数。",
248
+ "qa.list.limit.a": "{{cloud}} 的订阅计划分为 {{starter}}、{{premium}} 和 {{ultimate}},每个计划提供不同的算力点数。如当前计划点数不足,建议升级计划。您也可以在<fundsLink>「{{funds}}」</fundsLink>页面购买积分包进行按量付费充值。此外,您也可以配置自定义模型 API 密钥,使用其他来源购买的 API 点数。",
249
249
  "qa.list.limit.q": "算力点数用完怎么办?",
250
250
  "qa.list.management.a": "在 {{subscribe}} 页面,您可以“升级 / 降级”当前订阅计划,或切换年付与月付。在“{{usage}}-{{management}}”中可跳转至 Stripe 进行订阅管理,您可随时取消订阅。取消后,当前计划到期时将自动降级为免费版。",
251
251
  "qa.list.management.q": "如何更改或取消订阅?",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.243",
3
+ "version": "2.0.0-next.245",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -42,6 +42,7 @@ const SwitchPanel = memo<PropsWithChildren>(({ children }) => {
42
42
  <Popover
43
43
  classNames={{ trigger: styles.trigger }}
44
44
  content={content}
45
+ nativeButton={false}
45
46
  placement="bottomLeft"
46
47
  styles={{
47
48
  content: {
@@ -12,7 +12,7 @@ const HeaderActions = memo(() => {
12
12
  const { menuItems } = useMenu();
13
13
 
14
14
  return (
15
- <DropdownMenu items={menuItems}>
15
+ <DropdownMenu items={menuItems} nativeButton={false}>
16
16
  <ActionIcon icon={MoreHorizontal} size={DESKTOP_HEADER_ICON_SIZE} />
17
17
  </DropdownMenu>
18
18
  );
@@ -9,7 +9,7 @@ import { useAgentStore } from '@/store/agent';
9
9
  import { agentByIdSelectors } from '@/store/agent/selectors';
10
10
 
11
11
  import { useAgentId } from '../../hooks/useAgentId';
12
- import CheckboxItem from '../components/CheckbokWithLoading';
12
+ import CheckboxItem from '../components/CheckboxWithLoading';
13
13
 
14
14
  export const useControls = ({
15
15
  setModalOpen,
@@ -0,0 +1,92 @@
1
+ import { Flexbox, Icon, type ItemType, Segmented, usePopoverContext } from '@lobehub/ui';
2
+ import { createStaticStyles, cssVar } from 'antd-style';
3
+ import { ChevronRight, Store } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import ToolsList, { toolsListStyles } from './ToolsList';
8
+
9
+ const styles = createStaticStyles(({ css }) => ({
10
+ footer: css`
11
+ padding: 4px;
12
+ border-block-start: 1px solid ${cssVar.colorBorderSecondary};
13
+ `,
14
+ header: css`
15
+ padding: 8px;
16
+ border-block-end: 1px solid ${cssVar.colorBorderSecondary};
17
+ `,
18
+ trailingIcon: css`
19
+ opacity: 0.5;
20
+ `,
21
+ }));
22
+
23
+ type TabType = 'all' | 'installed';
24
+
25
+ interface PopoverContentProps {
26
+ activeTab: TabType;
27
+ currentItems: ItemType[];
28
+ enableKlavis: boolean;
29
+ onOpenStore: () => void;
30
+ onTabChange: (tab: TabType) => void;
31
+ }
32
+
33
+ const PopoverContent = memo<PopoverContentProps>(
34
+ ({ activeTab, currentItems, enableKlavis, onTabChange, onOpenStore }) => {
35
+ const { t } = useTranslation('setting');
36
+
37
+ const { close: closePopover } = usePopoverContext();
38
+
39
+ return (
40
+ <Flexbox gap={0}>
41
+ <div className={styles.header}>
42
+ <Segmented
43
+ block
44
+ onChange={(v) => onTabChange(v as TabType)}
45
+ options={[
46
+ {
47
+ label: t('tools.tabs.all', { defaultValue: 'all' }),
48
+ value: 'all',
49
+ },
50
+ {
51
+ label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
52
+ value: 'installed',
53
+ },
54
+ ]}
55
+ size="small"
56
+ value={activeTab}
57
+ />
58
+ </div>
59
+ <div
60
+ style={{
61
+ maxHeight: 500,
62
+ minHeight: enableKlavis ? 500 : undefined,
63
+ overflowY: 'auto',
64
+ }}
65
+ >
66
+ <ToolsList items={currentItems} />
67
+ </div>
68
+ <div className={styles.footer}>
69
+ <div
70
+ className={toolsListStyles.item}
71
+ onClick={() => {
72
+ closePopover();
73
+ onOpenStore();
74
+ }}
75
+ role="button"
76
+ tabIndex={0}
77
+ >
78
+ <div className={toolsListStyles.itemIcon}>
79
+ <Icon icon={Store} size={20} />
80
+ </div>
81
+ <div className={toolsListStyles.itemContent}>{t('tools.plugins.store')}</div>
82
+ <Icon className={styles.trailingIcon} icon={ChevronRight} size={16} />
83
+ </div>
84
+ </div>
85
+ </Flexbox>
86
+ );
87
+ },
88
+ );
89
+
90
+ PopoverContent.displayName = 'PopoverContent';
91
+
92
+ export default PopoverContent;
@@ -5,7 +5,7 @@ import PluginTag from '@/components/Plugins/PluginTag';
5
5
  import { useToolStore } from '@/store/tool';
6
6
  import { customPluginSelectors } from '@/store/tool/selectors';
7
7
 
8
- import CheckboxItem, { type CheckboxItemProps } from '../components/CheckbokWithLoading';
8
+ import CheckboxItem, { type CheckboxItemProps } from '../components/CheckboxWithLoading';
9
9
 
10
10
  const ToolItem = memo<CheckboxItemProps>(({ id, onUpdate, label, checked }) => {
11
11
  const isCustom = useToolStore((s) => customPluginSelectors.isCustomPlugin(id)(s));
@@ -13,6 +13,7 @@ const ToolItem = memo<CheckboxItemProps>(({ id, onUpdate, label, checked }) => {
13
13
  return (
14
14
  <CheckboxItem
15
15
  checked={checked}
16
+ hasPadding={false}
16
17
  id={id}
17
18
  label={
18
19
  <Flexbox align={'center'} gap={8} horizontal>
@@ -0,0 +1,107 @@
1
+ import { Flexbox, Icon, type ItemType, Text } from '@lobehub/ui';
2
+ import { Divider } from 'antd';
3
+ import { createStaticStyles, cssVar } from 'antd-style';
4
+ import type { ReactNode } from 'react';
5
+ import { Fragment, isValidElement, memo } from 'react';
6
+
7
+ export const toolsListStyles = createStaticStyles(({ css }) => ({
8
+ groupLabel: css`
9
+ padding-block: 4px;
10
+ padding-inline: 12px;
11
+ `,
12
+ item: css`
13
+ cursor: pointer;
14
+
15
+ display: flex;
16
+ gap: 12px;
17
+ align-items: center;
18
+
19
+ padding-block: 8px;
20
+ padding-inline: 12px;
21
+ border-radius: 6px;
22
+
23
+ transition: background-color 0.2s;
24
+
25
+ &:hover {
26
+ background: ${cssVar.colorFillTertiary};
27
+ }
28
+ `,
29
+ itemContent: css`
30
+ flex: 1;
31
+ min-width: 0;
32
+ `,
33
+ itemIcon: css`
34
+ display: flex;
35
+ flex-shrink: 0;
36
+ align-items: center;
37
+ justify-content: center;
38
+
39
+ width: 24px;
40
+ height: 24px;
41
+ `,
42
+ }));
43
+
44
+ interface ToolItemData {
45
+ children?: ToolItemData[];
46
+ extra?: ReactNode;
47
+ icon?: ReactNode;
48
+ key?: string;
49
+ label?: ReactNode;
50
+ onClick?: () => void;
51
+ type?: 'group' | 'divider';
52
+ }
53
+
54
+ interface ToolsListProps {
55
+ items: ItemType[];
56
+ }
57
+
58
+ const ToolsList = memo<ToolsListProps>(({ items }) => {
59
+ const renderItem = (item: ToolItemData, index: number) => {
60
+ if (item.type === 'divider') {
61
+ return <Divider key={`divider-${index}`} style={{ margin: '4px 0' }} />;
62
+ }
63
+
64
+ if (item.type === 'group') {
65
+ return (
66
+ <Fragment key={item.key || `group-${index}`}>
67
+ <Text className={toolsListStyles.groupLabel} fontSize={12} type="secondary">
68
+ {item.label}
69
+ </Text>
70
+ {item.children?.map((child, childIndex) => renderItem(child, childIndex))}
71
+ </Fragment>
72
+ );
73
+ }
74
+
75
+ // Regular item
76
+ // icon can be: ReactNode (already rendered), LucideIcon/ForwardRef (needs Icon wrapper), or undefined
77
+ const iconNode = item.icon ? (
78
+ isValidElement(item.icon) ? (
79
+ item.icon
80
+ ) : (
81
+ <Icon icon={item.icon as any} size={20} />
82
+ )
83
+ ) : null;
84
+
85
+ return (
86
+ <div
87
+ className={toolsListStyles.item}
88
+ key={item.key || `item-${index}`}
89
+ onClick={item.onClick}
90
+ role="button"
91
+ tabIndex={0}
92
+ >
93
+ {iconNode && <div className={toolsListStyles.itemIcon}>{iconNode}</div>}
94
+ <div className={toolsListStyles.itemContent}>{item.label}</div>
95
+ {item.extra}
96
+ </div>
97
+ );
98
+ };
99
+
100
+ return (
101
+ <Flexbox gap={0} padding={4}>
102
+ {items.map((item, index) => renderItem(item as ToolItemData, index))}
103
+ </Flexbox>
104
+ );
105
+ });
106
+
107
+ export default ToolsList;
@@ -1,5 +1,3 @@
1
- import { Segmented } from '@lobehub/ui';
2
- import { createStaticStyles, cssVar } from 'antd-style';
3
1
  import { Blocks } from 'lucide-react';
4
2
  import { Suspense, memo, useEffect, useRef, useState } from 'react';
5
3
  import { useTranslation } from 'react-i18next';
@@ -12,46 +10,17 @@ import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfi
12
10
 
13
11
  import { useAgentId } from '../../hooks/useAgentId';
14
12
  import Action from '../components/Action';
13
+ import PopoverContent from './PopoverContent';
15
14
  import { useControls } from './useControls';
16
15
 
17
16
  type TabType = 'all' | 'installed';
18
17
 
19
- const prefixCls = 'ant';
20
-
21
- const styles = createStaticStyles(({ css }) => ({
22
- dropdown: css`
23
- overflow: hidden;
24
-
25
- width: 100%;
26
- border: 1px solid ${cssVar.colorBorderSecondary};
27
- border-radius: ${cssVar.borderRadiusLG};
28
-
29
- background: ${cssVar.colorBgElevated};
30
- box-shadow: ${cssVar.boxShadowSecondary};
31
-
32
- .${prefixCls}-dropdown-menu {
33
- border-radius: 0 !important;
34
- background: transparent !important;
35
- box-shadow: none !important;
36
- }
37
- `,
38
- header: css`
39
- padding: ${cssVar.paddingXS};
40
- border-block-end: 1px solid ${cssVar.colorBorderSecondary};
41
- background: transparent;
42
- `,
43
- scroller: css`
44
- overflow: hidden auto;
45
- `,
46
- }));
47
-
48
18
  const Tools = memo(() => {
49
19
  const { t } = useTranslation('setting');
50
20
  const [modalOpen, setModalOpen] = useState(false);
51
21
  const [updating, setUpdating] = useState(false);
52
22
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
53
23
  const { marketItems, installedPluginItems } = useControls({
54
- setModalOpen,
55
24
  setUpdating,
56
25
  });
57
26
 
@@ -82,52 +51,26 @@ const Tools = memo(() => {
82
51
  return (
83
52
  <Suspense fallback={<Action disabled icon={Blocks} title={t('tools.title')} />}>
84
53
  <Action
85
- dropdown={{
54
+ icon={Blocks}
55
+ loading={updating}
56
+ popover={{
57
+ content: (
58
+ <PopoverContent
59
+ activeTab={effectiveTab}
60
+ currentItems={currentItems}
61
+ enableKlavis={enableKlavis}
62
+ onOpenStore={() => setModalOpen(true)}
63
+ onTabChange={setActiveTab}
64
+ />
65
+ ),
86
66
  maxWidth: 320,
87
- menu: {
88
- items: [...currentItems],
89
- style: {
90
- // let only the custom scroller scroll
91
- maxHeight: 'unset',
92
- overflowY: 'visible',
67
+ minWidth: 320,
68
+ styles: {
69
+ content: {
70
+ padding: 0,
93
71
  },
94
72
  },
95
- minHeight: enableKlavis ? 500 : undefined,
96
- minWidth: 320,
97
- popupRender: (menu) => (
98
- <div className={styles.dropdown}>
99
- <div className={styles.header}>
100
- <Segmented
101
- block
102
- onChange={(v) => setActiveTab(v as TabType)}
103
- options={[
104
- {
105
- label: t('tools.tabs.all', { defaultValue: 'all' }),
106
- value: 'all',
107
- },
108
- {
109
- label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
110
- value: 'installed',
111
- },
112
- ]}
113
- size="small"
114
- value={effectiveTab}
115
- />
116
- </div>
117
- <div
118
- className={styles.scroller}
119
- style={{
120
- maxHeight: 500,
121
- minHeight: enableKlavis ? 500 : undefined,
122
- }}
123
- >
124
- {menu}
125
- </div>
126
- </div>
127
- ),
128
73
  }}
129
- icon={Blocks}
130
- loading={updating}
131
74
  showTooltip={false}
132
75
  title={t('tools.title')}
133
76
  />
@@ -7,7 +7,7 @@ import {
7
7
  import { Avatar, Flexbox, Icon, Image, type ItemType } from '@lobehub/ui';
8
8
  import { cssVar } from 'antd-style';
9
9
  import isEqual from 'fast-deep-equal';
10
- import { ArrowRight, Store, ToyBrick } from 'lucide-react';
10
+ import { ToyBrick } from 'lucide-react';
11
11
  import { memo, useMemo } from 'react';
12
12
  import { useTranslation } from 'react-i18next';
13
13
 
@@ -61,13 +61,7 @@ const LobehubSkillIcon = memo<Pick<LobehubSkillProviderType, 'icon' | 'label'>>(
61
61
 
62
62
  LobehubSkillIcon.displayName = 'LobehubSkillIcon';
63
63
 
64
- export const useControls = ({
65
- setModalOpen,
66
- setUpdating,
67
- }: {
68
- setModalOpen: (open: boolean) => void;
69
- setUpdating: (updating: boolean) => void;
70
- }) => {
64
+ export const useControls = ({ setUpdating }: { setUpdating: (updating: boolean) => void }) => {
71
65
  const { t } = useTranslation('setting');
72
66
  const agentId = useAgentId();
73
67
  const list = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
@@ -234,18 +228,6 @@ export const useControls = ({
234
228
  ),
235
229
  type: 'group',
236
230
  },
237
- {
238
- type: 'divider',
239
- },
240
- {
241
- extra: <Icon icon={ArrowRight} />,
242
- icon: Store,
243
- key: 'plugin-store',
244
- label: t('tools.plugins.store'),
245
- onClick: () => {
246
- setModalOpen(true);
247
- },
248
- },
249
231
  ];
250
232
 
251
233
  // 已安装 tab 的 items - 只显示已安装的插件
@@ -21,7 +21,7 @@ import { preferenceSelectors } from '@/store/user/selectors';
21
21
 
22
22
  import { useAgentId } from '../../hooks/useAgentId';
23
23
  import Action from '../components/Action';
24
- import CheckboxItem from '../components/CheckbokWithLoading';
24
+ import CheckboxItem from '../components/CheckboxWithLoading';
25
25
 
26
26
  const hotArea = css`
27
27
  &::before {
@@ -84,6 +84,7 @@ const ActionPopover = memo<ActionPopoverProps>(
84
84
  content: contentClassName,
85
85
  }}
86
86
  content={popoverContent}
87
+ nativeButton={false}
87
88
  placement={isMobile ? 'top' : placement}
88
89
  styles={{
89
90
  ...(typeof resolvedStyles === 'object' ? resolvedStyles : {}),
@@ -0,0 +1,60 @@
1
+ import { Center, Checkbox, Flexbox, Icon } from '@lobehub/ui';
2
+ import { Loader2 } from 'lucide-react';
3
+ import { type ReactNode, memo, useState } from 'react';
4
+
5
+ export interface CheckboxItemProps {
6
+ checked?: boolean;
7
+ hasPadding?: boolean;
8
+ id: string;
9
+ label?: ReactNode;
10
+ onUpdate: (id: string, enabled: boolean) => Promise<void>;
11
+ }
12
+
13
+ const CheckboxItem = memo<CheckboxItemProps>(
14
+ ({ id, onUpdate, label, checked, hasPadding = true }) => {
15
+ const [loading, setLoading] = useState(false);
16
+
17
+ const updateState = async () => {
18
+ setLoading(true);
19
+ await onUpdate(id, !checked);
20
+ setLoading(false);
21
+ };
22
+
23
+ return (
24
+ <Flexbox
25
+ align={'center'}
26
+ gap={24}
27
+ horizontal
28
+ justify={'space-between'}
29
+ onClick={async (e) => {
30
+ e.stopPropagation();
31
+ updateState();
32
+ }}
33
+ style={
34
+ hasPadding
35
+ ? {
36
+ paddingLeft: 8,
37
+ }
38
+ : void 0
39
+ }
40
+ >
41
+ {label || id}
42
+ {loading ? (
43
+ <Center width={18}>
44
+ <Icon icon={Loader2} spin />
45
+ </Center>
46
+ ) : (
47
+ <Checkbox
48
+ checked={checked}
49
+ onClick={async (e) => {
50
+ e.stopPropagation();
51
+ await updateState();
52
+ }}
53
+ />
54
+ )}
55
+ </Flexbox>
56
+ );
57
+ },
58
+ );
59
+
60
+ export default CheckboxItem;
@@ -43,6 +43,7 @@ const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
43
43
  provider={providerProp}
44
44
  />
45
45
  }
46
+ nativeButton={false}
46
47
  onOpenChange={handleOpenChange}
47
48
  open={isOpen}
48
49
  placement={placement}
@@ -278,7 +278,7 @@ export default {
278
278
  '{{name}} has always adhered to open source principles. For professional developers, you can use all open source capabilities through self-deployment of the community version. In {{cloud}}, we provide all registered users with {{credit}} free computing credits per month, ready to use without complex configuration. If you need more usage, you can subscribe to {{starter}}, {{premium}} or {{ultimate}}.',
279
279
  'qa.list.free.q': 'Can {{name}} be used for free?',
280
280
  'qa.list.limit.a':
281
- '{{cloud}} subscription plans are divided into {{starter}}, {{premium}} and {{ultimate}}, each providing different computing credits. If your current plan credits are insufficient, we recommend upgrading. Alternatively, you can set up a custom model API key to use API credits purchased from other sources.',
281
+ '{{cloud}} subscription plans are divided into {{starter}}, {{premium}} and {{ultimate}}, each providing different computing credits. If your current plan credits are insufficient, we recommend upgrading. You can also purchase credit packages on the <fundsLink>"{{funds}}"</fundsLink> page for pay-as-you-go usage. Alternatively, you can set up a custom model API key to use API credits purchased from other sources.',
282
282
  'qa.list.limit.q': 'What if I run out of computing credits?',
283
283
  'qa.list.management.a':
284
284
  'On the {{subscribe}} page, you can "Upgrade / Downgrade" your current subscription plan, or switch between yearly and monthly billing. Through "{{usage}}-{{management}}" you can go to Stripe for subscription management, and you can cancel your subscription at any time. After cancellation, you will be automatically downgraded to the free version when your current plan expires.',
@@ -82,7 +82,8 @@ describe('ModelsService', () => {
82
82
  await modelsService.getModels('custom-provider');
83
83
 
84
84
  expect(mockedResolveRuntimeProvider).toHaveBeenCalledWith('custom-provider');
85
- expect(fetch).toHaveBeenCalledWith('/webapi/models/openai', { headers: {} });
85
+ // API endpoint uses original provider, allowing server to query correct config
86
+ expect(fetch).toHaveBeenCalledWith('/webapi/models/custom-provider', { headers: {} });
86
87
  expect(mockedInitializeWithClientStore).not.toHaveBeenCalled();
87
88
  });
88
89
 
@@ -402,7 +402,7 @@ class ChatService {
402
402
  responseAnimation,
403
403
  ].reduce((acc, cur) => merge(acc, standardizeAnimationStyle(cur)), {});
404
404
 
405
- return fetchSSE(API_ENDPOINTS.chat(sdkType), {
405
+ return fetchSSE(API_ENDPOINTS.chat(provider), {
406
406
  body: JSON.stringify(payload),
407
407
  fetcher: fetcher,
408
408
  headers,
@@ -49,7 +49,7 @@ export class ModelsService {
49
49
  return agentRuntime.models();
50
50
  }
51
51
 
52
- const res = await fetch(API_ENDPOINTS.models(runtimeProvider), { headers });
52
+ const res = await fetch(API_ENDPOINTS.models(provider), { headers });
53
53
  if (!res.ok) return;
54
54
 
55
55
  return res.json();
@@ -87,7 +87,7 @@ export class ModelsService {
87
87
  });
88
88
  res = (await agentRuntime.pullModel({ model }, { signal }))!;
89
89
  } else {
90
- res = await fetch(API_ENDPOINTS.modelPull(runtimeProvider), {
90
+ res = await fetch(API_ENDPOINTS.modelPull(provider), {
91
91
  body: JSON.stringify({ model }),
92
92
  headers,
93
93
  method: 'POST',
package/vitest.config.mts CHANGED
@@ -92,7 +92,6 @@ export default defineConfig({
92
92
  '**/e2e/**',
93
93
  ],
94
94
  globals: true,
95
- reporters: ['default', 'blob'],
96
95
  server: {
97
96
  deps: {
98
97
  inline: ['vitest-canvas-mock', '@lobehub/ui', '@lobehub/fluent-emoji'],
@@ -1,53 +0,0 @@
1
- import { Center, Flexbox, Icon , Checkbox } from '@lobehub/ui';
2
- import { Loader2 } from 'lucide-react';
3
- import { type ReactNode, memo, useState } from 'react';
4
-
5
- export interface CheckboxItemProps {
6
- checked?: boolean;
7
- id: string;
8
- label?: ReactNode;
9
- onUpdate: (id: string, enabled: boolean) => Promise<void>;
10
- }
11
-
12
- const CheckboxItem = memo<CheckboxItemProps>(({ id, onUpdate, label, checked }) => {
13
- const [loading, setLoading] = useState(false);
14
-
15
- const updateState = async () => {
16
- setLoading(true);
17
- await onUpdate(id, !checked);
18
- setLoading(false);
19
- };
20
-
21
- return (
22
- <Flexbox
23
- align={'center'}
24
- gap={24}
25
- horizontal
26
- justify={'space-between'}
27
- onClick={async (e) => {
28
- e.stopPropagation();
29
- updateState();
30
- }}
31
- style={{
32
- paddingLeft: 8,
33
- }}
34
- >
35
- {label || id}
36
- {loading ? (
37
- <Center width={18}>
38
- <Icon icon={Loader2} spin />
39
- </Center>
40
- ) : (
41
- <Checkbox
42
- checked={checked}
43
- onClick={async (e) => {
44
- e.stopPropagation();
45
- await updateState();
46
- }}
47
- />
48
- )}
49
- </Flexbox>
50
- );
51
- });
52
-
53
- export default CheckboxItem;