@lobehub/chat 1.70.2 → 1.70.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.70.4](https://github.com/lobehub/lobe-chat/compare/v1.70.3...v1.70.4)
6
+
7
+ <sup>Released on **2025-03-11**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Support OpenRouter custom BaseURL.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Support OpenRouter custom BaseURL ([a8089ed](https://github.com/lobehub/lobe-chat/commit/a8089ed))
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 1.70.3](https://github.com/lobehub/lobe-chat/compare/v1.70.2...v1.70.3)
31
+
32
+ <sup>Released on **2025-03-11**</sup>
33
+
34
+ #### 💄 Styles
35
+
36
+ - **spelling**: Correct "broswer" to "browser" across codebase.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### Styles
44
+
45
+ - **spelling**: Correct "broswer" to "browser" across codebase, closes [#6876](https://github.com/lobehub/lobe-chat/issues/6876) ([8d677a2](https://github.com/lobehub/lobe-chat/commit/8d677a2))
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 1.70.2](https://github.com/lobehub/lobe-chat/compare/v1.70.1...v1.70.2)
6
56
 
7
57
  <sup>Released on **2025-03-10**</sup>
package/README.md CHANGED
@@ -330,7 +330,7 @@ In addition, these plugins are not limited to news aggregation, but can also ext
330
330
  | [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
331
331
  | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2024-12-22**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
332
332
 
333
- > 📊 Total plugins: [<kbd>**47**</kbd>](https://lobechat.com/discover/plugins)
333
+ > 📊 Total plugins: [<kbd>**46**</kbd>](https://lobechat.com/discover/plugins)
334
334
 
335
335
  <!-- PLUGIN LIST -->
336
336
 
package/README.zh-CN.md CHANGED
@@ -323,7 +323,7 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
323
323
  | [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
324
324
  | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2024-12-22**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
325
325
 
326
- > 📊 Total plugins: [<kbd>**47**</kbd>](https://lobechat.com/discover/plugins)
326
+ > 📊 Total plugins: [<kbd>**46**</kbd>](https://lobechat.com/discover/plugins)
327
327
 
328
328
  <!-- PLUGIN LIST -->
329
329
 
package/changelog/v1.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Support OpenRouter custom BaseURL."
6
+ ]
7
+ },
8
+ "date": "2025-03-11",
9
+ "version": "1.70.4"
10
+ },
11
+ {
12
+ "children": {},
13
+ "date": "2025-03-11",
14
+ "version": "1.70.3"
15
+ },
2
16
  {
3
17
  "children": {
4
18
  "fixes": [
@@ -14,10 +14,10 @@ tags:
14
14
 
15
15
  LobeChat supports customizing the model list during deployment. This configuration is done in the environment for each [model provider](/docs/self-hosting/environment-variables/model-provider).
16
16
 
17
- You can use `+` to add a model, `-` to hide a model, and use `model name=display name<extension configuration>` to customize the display name of a model, separated by English commas. The basic syntax is as follows:
17
+ You can use `+` to add a model, `-` to hide a model, and use `model name->deploymentName=display name<extension configuration>` to customize the display name of a model, separated by English commas. The basic syntax is as follows:
18
18
 
19
19
  ```text
20
- id=displayName<maxToken:vision:reasoning:search:fc:file>,model2,model3
20
+ id->deploymentName=displayName<maxToken:vision:reasoning:search:fc:file>,model2,model3
21
21
  ```
22
22
 
23
23
  For example: `+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-0125-preview=gpt-4-turbo`
@@ -29,7 +29,7 @@ In the above example, it adds `qwen-7b-chat` and `glm-6b` to the model list, rem
29
29
  Considering the diversity of model capabilities, we started to add extension configuration in version `0.147.8`, with the following rules:
30
30
 
31
31
  ```shell
32
- id=displayName<maxToken:vision:reasoning:search:fc:file>
32
+ id->deploymentName=displayName<maxToken:vision:reasoning:search:fc:file>
33
33
  ```
34
34
 
35
35
  The first value in angle brackets is designated as the `maxToken` for this model. The second value and beyond are the model's extension capabilities, separated by colons `:`, and the order is not important.
@@ -13,10 +13,10 @@ tags:
13
13
 
14
14
  LobeChat 支持在部署时自定义模型列表,详情请参考 [模型提供商](/zh/docs/self-hosting/environment-variables/model-provider) 。
15
15
 
16
- 你可以使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名<扩展配置>` 来自定义模型的展示名,用英文逗号隔开。通过 `<>` 来添加扩展配置。基本语法如下:
16
+ 你可以使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名->部署名=展示名<扩展配置>` 来自定义模型的展示名,用英文逗号隔开。通过 `<>` 来添加扩展配置。基本语法如下:
17
17
 
18
18
  ```text
19
- id=displayName<maxToken:vision:reasoning:search:fc:file>,model2,model3
19
+ id->deploymentName=displayName<maxToken:vision:reasoning:search:fc:file>,model2,model3
20
20
  ```
21
21
 
22
22
  例如: `+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-0125-preview=gpt-4-turbo`
@@ -28,7 +28,7 @@ id=displayName<maxToken:vision:reasoning:search:fc:file>,model2,model3
28
28
  考虑到模型的能力多样性,我们在 `0.147.8` 版本开始增加扩展性配置,它的规则如下:
29
29
 
30
30
  ```shell
31
- id=displayName<maxToken:vision:reasoning:search:fc:file>
31
+ id->deploymentName=displayName<maxToken:vision:reasoning:search:fc:file>
32
32
  ```
33
33
 
34
34
  尖括号第一个值约定为这个模型的 `maxToken` 。第二个及以后作为模型的扩展能力,能力与能力之间用冒号 `:` 作为分隔符,顺序不重要。
@@ -94,7 +94,7 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
94
94
  ### `AZURE_MODEL_LIST`
95
95
 
96
96
  - Type: Optional
97
- - Description: Used to control the model list, use `+` to add a model, use `-` to hide a model, use `id->deplymentName=displayName` to customize the display name of a model, separated by commas. Definition syntax rules see [model-list][model-list]
97
+ - Description: Used to control the model list, use `+` to add a model, use `-` to hide a model, use `id->deploymentName=displayName` to customize the display name of a model, separated by commas. Definition syntax rules see [model-list][model-list]
98
98
  - Default: `-`
99
99
  - Example: `gpt-35-turbo->my-deploy=GPT 3.5 Turbo` 或 `gpt-4-turbo->my-gpt4=GPT 4 Turbo<128000:vision:fc>`
100
100
 
@@ -183,6 +183,13 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
183
183
  - Default: -
184
184
  - Example: `sk-xxxxxx...xxxxxx`
185
185
 
186
+ ### `DEEPSEEK_MODEL_LIST`
187
+
188
+ - Type: Optional
189
+ - Description: Used to control the model list, use `+` to add a model, use `-` to hide a model, use `model_name=displayName` to customize the display name of a model, separated by commas. Definition syntax rules see [model-list][model-list]
190
+ - Default: `-`
191
+ - Example: `-all,+deepseek-reasoner`
192
+
186
193
  ## XAI
187
194
 
188
195
  ### `XAI_API_KEY`
@@ -425,6 +432,13 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
425
432
  - Default: `-`
426
433
  - Example: `-all,+qwen-turbo-latest,+qwen-plus-latest`
427
434
 
435
+ ### `QWEN_PROXY_URL`
436
+
437
+ - Type: Optional
438
+ - Description: If you manually configure the Qwen API proxy, you can use this configuration item to override the default Qwen API request base URL
439
+ - Default: `https://dashscope.aliyuncs.com/compatible-mode/v1`
440
+ - Example: `https://my-qwen-proxy.com/v1`
441
+
428
442
  ## Stepfun AI
429
443
 
430
444
  ### `STEPFUN_API_KEY`
@@ -555,9 +569,9 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
555
569
  ### `VOLCENGINE_MODEL_LIST`
556
570
 
557
571
  - Type: Optional
558
- - Description: Used to control the model list, use `+` to add a model, use `-` to hide a model, use `model_name=display_name` to customize the display name of a model, separated by commas. Definition syntax rules see [model-list][model-list]
572
+ - Description: Used to control the model list, use `+` to add a model, use `-` to hide a model, use `model_name->deploymentName=display_name` to customize the display name of a model, separated by commas. Definition syntax rules see [model-list][model-list]
559
573
  - Default: `-`
560
- - Example: `-all,+deepseek-r1-250120,+deepseek-v3-241226,+doubao-1-5-pro-256k-250115,+doubao-1-5-pro-32k-250115,+doubao-1-5-lite-32k-250115`
574
+ - Example: `-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-241226,+doubao-1.5-pro-256k->doubao-1-5-pro-256k-250115,+doubao-1.5-pro-32k->doubao-1-5-pro-32k-250115,+doubao-1.5-lite-32k->doubao-1-5-lite-32k-250115`
561
575
 
562
576
 
563
577
  [model-list]: /docs/self-hosting/advanced/model-list
@@ -181,6 +181,13 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
181
181
  - 默认值:-
182
182
  - 示例:`sk-xxxxxx...xxxxxx`
183
183
 
184
+ ### `DEEPSEEK_MODEL_LIST`
185
+
186
+ - 类型:可选
187
+ - 描述:用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名<扩展配置>` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则见 [模型列表][model-list]
188
+ - 默认值:`-`
189
+ - 示例:`-all,+deepseek-reasoner`
190
+
184
191
  ## XAI
185
192
 
186
193
  ### `XAI_API_KEY`
@@ -423,6 +430,13 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
423
430
  - 默认值:`-`
424
431
  - 示例:`-all,+qwen-turbo-latest,+qwen-plus-latest`
425
432
 
433
+ ### `QWEN_PROXY_URL`
434
+
435
+ - 类型:可选
436
+ - 描述:如果你手动配置了 Qwen 接口代理,可以使用此配置项来覆盖默认的 Qwen API 请求基础 URL
437
+ - 默认值:`https://dashscope.aliyuncs.com/compatible-mode/v1`
438
+ - 示例:`https://my-qwen-proxy.com/v1`
439
+
426
440
  ## Stepfun AI
427
441
 
428
442
  ### `STEPFUN_API_KEY`
@@ -553,8 +567,8 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
553
567
  ### `VOLCENGINE_MODEL_LIST`
554
568
 
555
569
  - 类型:可选
556
- - 描述:用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名<扩展配置>` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则见 [模型列表][model-list]
570
+ - 描述:用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名->部署名=展示名<扩展配置>` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则见 [模型列表][model-list]
557
571
  - 默认值:`-`
558
- - 示例:`-all,+deepseek-r1-250120,+deepseek-v3-241226,+doubao-1-5-pro-256k-250115,+doubao-1-5-pro-32k-250115,+doubao-1-5-lite-32k-250115`
572
+ - 示例:`-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-241226,+doubao-1.5-pro-256k->doubao-1-5-pro-256k-250115,+doubao-1.5-pro-32k->doubao-1-5-pro-32k-250115,+doubao-1.5-lite-32k->doubao-1-5-lite-32k-250115`
559
573
 
560
574
  [model-list]: /zh/docs/self-hosting/advanced/model-list
@@ -289,7 +289,7 @@
289
289
  "title": "زبان تشخیص گفتار"
290
290
  },
291
291
  "sttService": {
292
- "desc": "در این میان، broswer به سرویس تشخیص گفتار بومی مرورگر اشاره دارد",
292
+ "desc": "در این میان، browser به سرویس تشخیص گفتار بومی مرورگر اشاره دارد",
293
293
  "title": "سرویس تشخیص گفتار"
294
294
  },
295
295
  "title": "سرویس‌های گفتاری",
@@ -289,7 +289,7 @@
289
289
  "title": "语音识别语种"
290
290
  },
291
291
  "sttService": {
292
- "desc": "其中 broswer 为浏览器原生的语音识别服务",
292
+ "desc": "其中 browser 为浏览器原生的语音识别服务",
293
293
  "title": "语音识别服务"
294
294
  },
295
295
  "title": "语音服务",
@@ -289,7 +289,7 @@
289
289
  "title": "語音識別語種"
290
290
  },
291
291
  "sttService": {
292
- "desc": "其中 broswer 為瀏覽器原生的語音識別服務",
292
+ "desc": "其中 browser 為瀏覽器原生的語音識別服務",
293
293
  "title": "語音識別服務"
294
294
  },
295
295
  "title": "語音服務",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.70.2",
3
+ "version": "1.70.4",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot 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",
@@ -75,11 +75,19 @@ export const naive: CrawlImpl = async (url, { filterOptions }) => {
75
75
  const type = res.headers.get('content-type');
76
76
 
77
77
  if (type?.includes('application/json')) {
78
- const json = await res.json();
78
+ let content: string;
79
+
80
+ try {
81
+ const json = await res.clone().json();
82
+ content = JSON.stringify(json, null, 2);
83
+ } catch {
84
+ content = await res.text();
85
+ }
86
+
79
87
  return {
80
- content: JSON.stringify(json, null, 2),
88
+ content: content,
81
89
  contentType: 'json',
82
- length: json.length,
90
+ length: content.length,
83
91
  url,
84
92
  } satisfies CrawlSuccessResult;
85
93
  }
@@ -72,4 +72,10 @@ export const crawUrlRules: CrawlUrlRule[] = [
72
72
  impls: ['jina'],
73
73
  urlPattern: 'https://cvpr.thecvf.com(.*)',
74
74
  },
75
+ // 飞书用 jina
76
+ // https://github.com/lobehub/lobe-chat/issues/6879
77
+ {
78
+ impls: ['jina'],
79
+ urlPattern: 'https://(.*).feishu.cn/(.*)',
80
+ },
75
81
  ];
@@ -104,6 +104,10 @@ const CustomLogo = memo<LobeChatProps>(({ extra, size = 32, className, style, ty
104
104
 
105
105
  break;
106
106
  }
107
+ default: {
108
+ logoComponent = <CustomImageLogo size={size} style={style} {...rest} />;
109
+ break;
110
+ }
107
111
  }
108
112
 
109
113
  if (!extra) return logoComponent;
@@ -151,7 +151,7 @@ export const filterEnabledModels = (provider: ModelProviderCard) => {
151
151
  return provider.chatModels.filter((v) => v.enabled).map((m) => m.id);
152
152
  };
153
153
 
154
- export const isProviderDisableBroswerRequest = (id: string) => {
154
+ export const isProviderDisableBrowserRequest = (id: string) => {
155
155
  const provider = DEFAULT_MODEL_PROVIDER_LIST.find((v) => v.id === id && v.disableBrowserRequest);
156
156
  return !!provider;
157
157
  };
@@ -326,10 +326,17 @@ const OpenRouter: ModelProviderCard = {
326
326
  modelList: { showModelFetcher: true },
327
327
  modelsUrl: 'https://openrouter.ai/models',
328
328
  name: 'OpenRouter',
329
+ proxyUrl: {
330
+ placeholder: 'https://openrouter.ai/api/v1',
331
+ },
329
332
  settings: {
330
333
  // OpenRouter don't support browser request
331
334
  // https://github.com/lobehub/lobe-chat/issues/5900
332
335
  disableBrowserRequest: true,
336
+
337
+ proxyUrl: {
338
+ placeholder: 'https://openrouter.ai/api/v1',
339
+ },
333
340
  sdkType: 'openai',
334
341
  searchMode: 'params',
335
342
  showModelFetcher: true,
@@ -51,7 +51,7 @@ export const userSettings = pgTable('user_settings', {
51
51
  });
52
52
  export type UserSettingsItem = typeof userSettings.$inferSelect;
53
53
 
54
- export const installedPlugins = pgTable(
54
+ export const userInstalledPlugins = pgTable(
55
55
  'user_installed_plugins',
56
56
  {
57
57
  userId: text('user_id')
@@ -71,5 +71,5 @@ export const installedPlugins = pgTable(
71
71
  }),
72
72
  );
73
73
 
74
- export type NewInstalledPlugin = typeof installedPlugins.$inferInsert;
75
- export type InstalledPluginItem = typeof installedPlugins.$inferSelect;
74
+ export type NewInstalledPlugin = typeof userInstalledPlugins.$inferInsert;
75
+ export type InstalledPluginItem = typeof userInstalledPlugins.$inferSelect;
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
4
  import { getTestDBInstance } from '@/database/server/core/dbForTest';
5
5
 
6
- import { NewInstalledPlugin, installedPlugins, users } from '../../../schemas';
6
+ import { NewInstalledPlugin, userInstalledPlugins, users } from '../../../schemas';
7
7
  import { PluginModel } from '../plugin';
8
8
 
9
9
  let serverDB = await getTestDBInstance();
@@ -44,7 +44,7 @@ describe('PluginModel', () => {
44
44
 
45
45
  describe('delete', () => {
46
46
  it('should delete an installed plugin by identifier', async () => {
47
- await serverDB.insert(installedPlugins).values({
47
+ await serverDB.insert(userInstalledPlugins).values({
48
48
  userId,
49
49
  type: 'plugin',
50
50
  identifier: 'test-plugin',
@@ -53,14 +53,14 @@ describe('PluginModel', () => {
53
53
 
54
54
  await pluginModel.delete('test-plugin');
55
55
 
56
- const result = await serverDB.select().from(installedPlugins);
56
+ const result = await serverDB.select().from(userInstalledPlugins);
57
57
  expect(result).toHaveLength(0);
58
58
  });
59
59
  });
60
60
 
61
61
  describe('deleteAll', () => {
62
62
  it('should delete all installed plugins for the user', async () => {
63
- await serverDB.insert(installedPlugins).values([
63
+ await serverDB.insert(userInstalledPlugins).values([
64
64
  {
65
65
  userId,
66
66
  type: 'plugin',
@@ -83,7 +83,7 @@ describe('PluginModel', () => {
83
83
 
84
84
  await pluginModel.deleteAll();
85
85
 
86
- const result = await serverDB.select().from(installedPlugins);
86
+ const result = await serverDB.select().from(userInstalledPlugins);
87
87
  expect(result).toHaveLength(1);
88
88
  expect(result[0].userId).toBe('456');
89
89
  });
@@ -91,7 +91,7 @@ describe('PluginModel', () => {
91
91
 
92
92
  describe('query', () => {
93
93
  it('should query installed plugins for the user', async () => {
94
- await serverDB.insert(installedPlugins).values([
94
+ await serverDB.insert(userInstalledPlugins).values([
95
95
  {
96
96
  userId,
97
97
  type: 'plugin',
@@ -125,7 +125,7 @@ describe('PluginModel', () => {
125
125
 
126
126
  describe('findById', () => {
127
127
  it('should find an installed plugin by identifier', async () => {
128
- await serverDB.insert(installedPlugins).values([
128
+ await serverDB.insert(userInstalledPlugins).values([
129
129
  {
130
130
  userId,
131
131
  type: 'plugin',
@@ -149,7 +149,7 @@ describe('PluginModel', () => {
149
149
 
150
150
  describe('update', () => {
151
151
  it('should update an installed plugin', async () => {
152
- await serverDB.insert(installedPlugins).values({
152
+ await serverDB.insert(userInstalledPlugins).values({
153
153
  userId,
154
154
  type: 'plugin',
155
155
  identifier: 'test-plugin',
@@ -2,7 +2,7 @@ import { and, desc, eq } from 'drizzle-orm/expressions';
2
2
 
3
3
  import { LobeChatDatabase } from '@/database/type';
4
4
 
5
- import { InstalledPluginItem, NewInstalledPlugin, installedPlugins } from '../../schemas';
5
+ import { InstalledPluginItem, NewInstalledPlugin, userInstalledPlugins } from '../../schemas';
6
6
 
7
7
  export class PluginModel {
8
8
  private userId: string;
@@ -17,11 +17,11 @@ export class PluginModel {
17
17
  params: Pick<NewInstalledPlugin, 'type' | 'identifier' | 'manifest' | 'customParams'>,
18
18
  ) => {
19
19
  const [result] = await this.db
20
- .insert(installedPlugins)
20
+ .insert(userInstalledPlugins)
21
21
  .values({ ...params, createdAt: new Date(), updatedAt: new Date(), userId: this.userId })
22
22
  .onConflictDoUpdate({
23
23
  set: { ...params, updatedAt: new Date() },
24
- target: [installedPlugins.identifier, installedPlugins.userId],
24
+ target: [userInstalledPlugins.identifier, userInstalledPlugins.userId],
25
25
  })
26
26
  .returning();
27
27
 
@@ -30,40 +30,40 @@ export class PluginModel {
30
30
 
31
31
  delete = async (id: string) => {
32
32
  return this.db
33
- .delete(installedPlugins)
34
- .where(and(eq(installedPlugins.identifier, id), eq(installedPlugins.userId, this.userId)));
33
+ .delete(userInstalledPlugins)
34
+ .where(and(eq(userInstalledPlugins.identifier, id), eq(userInstalledPlugins.userId, this.userId)));
35
35
  };
36
36
 
37
37
  deleteAll = async () => {
38
- return this.db.delete(installedPlugins).where(eq(installedPlugins.userId, this.userId));
38
+ return this.db.delete(userInstalledPlugins).where(eq(userInstalledPlugins.userId, this.userId));
39
39
  };
40
40
 
41
41
  query = async () => {
42
42
  return this.db
43
43
  .select({
44
- createdAt: installedPlugins.createdAt,
45
- customParams: installedPlugins.customParams,
46
- identifier: installedPlugins.identifier,
47
- manifest: installedPlugins.manifest,
48
- settings: installedPlugins.settings,
49
- type: installedPlugins.type,
50
- updatedAt: installedPlugins.updatedAt,
44
+ createdAt: userInstalledPlugins.createdAt,
45
+ customParams: userInstalledPlugins.customParams,
46
+ identifier: userInstalledPlugins.identifier,
47
+ manifest: userInstalledPlugins.manifest,
48
+ settings: userInstalledPlugins.settings,
49
+ type: userInstalledPlugins.type,
50
+ updatedAt: userInstalledPlugins.updatedAt,
51
51
  })
52
- .from(installedPlugins)
53
- .where(eq(installedPlugins.userId, this.userId))
54
- .orderBy(desc(installedPlugins.createdAt));
52
+ .from(userInstalledPlugins)
53
+ .where(eq(userInstalledPlugins.userId, this.userId))
54
+ .orderBy(desc(userInstalledPlugins.createdAt));
55
55
  };
56
56
 
57
57
  findById = async (id: string) => {
58
- return this.db.query.installedPlugins.findFirst({
59
- where: and(eq(installedPlugins.identifier, id), eq(installedPlugins.userId, this.userId)),
58
+ return this.db.query.userInstalledPlugins.findFirst({
59
+ where: and(eq(userInstalledPlugins.identifier, id), eq(userInstalledPlugins.userId, this.userId)),
60
60
  });
61
61
  };
62
62
 
63
63
  update = async (id: string, value: Partial<InstalledPluginItem>) => {
64
64
  return this.db
65
- .update(installedPlugins)
65
+ .update(userInstalledPlugins)
66
66
  .set({ ...value, updatedAt: new Date() })
67
- .where(and(eq(installedPlugins.identifier, id), eq(installedPlugins.userId, this.userId)));
67
+ .where(and(eq(userInstalledPlugins.identifier, id), eq(userInstalledPlugins.userId, this.userId)));
68
68
  };
69
69
  }
@@ -30,7 +30,7 @@ beforeEach(() => {
30
30
  });
31
31
 
32
32
  afterEach(() => {
33
- vi.clearAllMocks();
33
+ vi.restoreAllMocks();
34
34
  });
35
35
 
36
36
  describe('LobeOpenRouterAI', () => {
@@ -40,6 +40,15 @@ describe('LobeOpenRouterAI', () => {
40
40
  expect(instance).toBeInstanceOf(LobeOpenRouterAI);
41
41
  expect(instance.baseURL).toEqual(defaultBaseURL);
42
42
  });
43
+
44
+ it('should correctly initialize with a custom base URL', async () => {
45
+ const instance = new LobeOpenRouterAI({
46
+ apiKey: 'test_api_key',
47
+ baseURL: 'https://api.abc.com/v1',
48
+ });
49
+ expect(instance).toBeInstanceOf(LobeOpenRouterAI);
50
+ expect(instance.baseURL).toEqual('https://api.abc.com/v1');
51
+ });
43
52
  });
44
53
 
45
54
  describe('chat', () => {
@@ -291,7 +291,7 @@ export default {
291
291
  title: '语音识别语种',
292
292
  },
293
293
  sttService: {
294
- desc: '其中 broswer 为浏览器原生的语音识别服务',
294
+ desc: '其中 browser 为浏览器原生的语音识别服务',
295
295
  title: '语音识别服务',
296
296
  },
297
297
  title: '语音服务',
@@ -3,7 +3,7 @@ import { eq } from 'drizzle-orm';
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { clientDB, initializeDB } from '@/database/client/db';
6
- import { installedPlugins, users } from '@/database/schemas';
6
+ import { userInstalledPlugins, users } from '@/database/schemas';
7
7
  import { LobeTool } from '@/types/tool';
8
8
  import { LobeToolCustomPlugin } from '@/types/tool/plugin';
9
9
 
@@ -40,8 +40,8 @@ describe('PluginService', () => {
40
40
  await pluginService.installPlugin(fakePlugin);
41
41
 
42
42
  // Assert
43
- const result = await clientDB.query.installedPlugins.findFirst({
44
- where: eq(installedPlugins.identifier, fakePlugin.identifier),
43
+ const result = await clientDB.query.userInstalledPlugins.findFirst({
44
+ where: eq(userInstalledPlugins.identifier, fakePlugin.identifier),
45
45
  });
46
46
  expect(result).toMatchObject(fakePlugin);
47
47
  });
@@ -52,7 +52,7 @@ describe('PluginService', () => {
52
52
  // Arrange
53
53
  const fakePlugins = [{ identifier: 'test-plugin', type: 'plugin' }] as LobeTool[];
54
54
  await clientDB
55
- .insert(installedPlugins)
55
+ .insert(userInstalledPlugins)
56
56
  .values([{ identifier: 'test-plugin', type: 'plugin', userId }]);
57
57
  // Act
58
58
  const data = await pluginService.getInstalledPlugins();
@@ -66,14 +66,14 @@ describe('PluginService', () => {
66
66
  it('should uninstall a plugin', async () => {
67
67
  // Arrange
68
68
  const identifier = 'test-plugin';
69
- await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]);
69
+ await clientDB.insert(userInstalledPlugins).values([{ identifier, type: 'plugin', userId }]);
70
70
 
71
71
  // Act
72
72
  await pluginService.uninstallPlugin(identifier);
73
73
 
74
74
  // Assert
75
- const result = await clientDB.query.installedPlugins.findFirst({
76
- where: eq(installedPlugins.identifier, identifier),
75
+ const result = await clientDB.query.userInstalledPlugins.findFirst({
76
+ where: eq(userInstalledPlugins.identifier, identifier),
77
77
  });
78
78
  expect(result).toBe(undefined);
79
79
  });
@@ -92,8 +92,8 @@ describe('PluginService', () => {
92
92
  await pluginService.createCustomPlugin(customPlugin);
93
93
 
94
94
  // Assert
95
- const result = await clientDB.query.installedPlugins.findFirst({
96
- where: eq(installedPlugins.identifier, customPlugin.identifier),
95
+ const result = await clientDB.query.userInstalledPlugins.findFirst({
96
+ where: eq(userInstalledPlugins.identifier, customPlugin.identifier),
97
97
  });
98
98
  expect(result).toMatchObject(customPlugin);
99
99
  });
@@ -104,14 +104,14 @@ describe('PluginService', () => {
104
104
  // Arrange
105
105
  const identifier = 'plugin-id';
106
106
  const value = { customParams: { ab: '1' } } as unknown as LobeToolCustomPlugin;
107
- await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]);
107
+ await clientDB.insert(userInstalledPlugins).values([{ identifier, type: 'plugin', userId }]);
108
108
 
109
109
  // Act
110
110
  await pluginService.updatePlugin(identifier, value);
111
111
 
112
112
  // Assert
113
- const result = await clientDB.query.installedPlugins.findFirst({
114
- where: eq(installedPlugins.identifier, identifier),
113
+ const result = await clientDB.query.userInstalledPlugins.findFirst({
114
+ where: eq(userInstalledPlugins.identifier, identifier),
115
115
  });
116
116
  expect(result).toMatchObject(value);
117
117
  });
@@ -122,14 +122,14 @@ describe('PluginService', () => {
122
122
  // Arrange
123
123
  const identifier = 'plugin-id';
124
124
  const manifest = { name: 'NewPluginManifest' } as unknown as LobeChatPluginManifest;
125
- await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]);
125
+ await clientDB.insert(userInstalledPlugins).values([{ identifier, type: 'plugin', userId }]);
126
126
 
127
127
  // Act
128
128
  await pluginService.updatePluginManifest(identifier, manifest);
129
129
 
130
130
  // Assert
131
- const result = await clientDB.query.installedPlugins.findFirst({
132
- where: eq(installedPlugins.identifier, identifier),
131
+ const result = await clientDB.query.userInstalledPlugins.findFirst({
132
+ where: eq(userInstalledPlugins.identifier, identifier),
133
133
  });
134
134
  expect(result).toMatchObject({ manifest });
135
135
  });
@@ -138,7 +138,7 @@ describe('PluginService', () => {
138
138
  describe('removeAllPlugins', () => {
139
139
  it('should remove all plugins', async () => {
140
140
  // Arrange
141
- await clientDB.insert(installedPlugins).values([
141
+ await clientDB.insert(userInstalledPlugins).values([
142
142
  { identifier: '123', type: 'plugin', userId },
143
143
  { identifier: '234', type: 'plugin', userId },
144
144
  ]);
@@ -147,8 +147,8 @@ describe('PluginService', () => {
147
147
  await pluginService.removeAllPlugins();
148
148
 
149
149
  // Assert
150
- const result = await clientDB.query.installedPlugins.findMany({
151
- where: eq(installedPlugins.userId, userId),
150
+ const result = await clientDB.query.userInstalledPlugins.findMany({
151
+ where: eq(userInstalledPlugins.userId, userId),
152
152
  });
153
153
  expect(result.length).toEqual(0);
154
154
  });
@@ -159,14 +159,14 @@ describe('PluginService', () => {
159
159
  // Arrange
160
160
  const id = 'plugin-id';
161
161
  const settings = { color: 'blue' };
162
- await clientDB.insert(installedPlugins).values([{ identifier: id, type: 'plugin', userId }]);
162
+ await clientDB.insert(userInstalledPlugins).values([{ identifier: id, type: 'plugin', userId }]);
163
163
 
164
164
  // Act
165
165
  await pluginService.updatePluginSettings(id, settings);
166
166
 
167
167
  // Assert
168
- const result = await clientDB.query.installedPlugins.findFirst({
169
- where: eq(installedPlugins.identifier, id),
168
+ const result = await clientDB.query.userInstalledPlugins.findFirst({
169
+ where: eq(userInstalledPlugins.identifier, id),
170
170
  });
171
171
 
172
172
  expect(result).toMatchObject({ settings });
@@ -0,0 +1,249 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { aiProviderSelectors } from '../selectors';
4
+
5
+ describe('aiProviderSelectors', () => {
6
+ const mockState: any = {
7
+ aiProviderList: [
8
+ { id: 'provider1', enabled: true, sort: 1 },
9
+ { id: 'provider2', enabled: false, sort: 2 },
10
+ { id: 'provider3', enabled: true, sort: 0 },
11
+ ],
12
+ aiProviderDetail: {
13
+ id: 'provider1',
14
+ keyVaults: {
15
+ baseURL: 'https://api.example.com',
16
+ apiKey: 'test-key',
17
+ },
18
+ },
19
+ aiProviderLoadingIds: ['loading-provider'],
20
+ aiProviderConfigUpdatingIds: ['updating-provider'],
21
+ activeAiProvider: 'provider1',
22
+ aiProviderRuntimeConfig: {
23
+ provider1: {
24
+ keyVaults: {
25
+ baseURL: 'https://api.example.com',
26
+ apiKey: 'test-key',
27
+ },
28
+ settings: {
29
+ searchMode: 'internal',
30
+ },
31
+ fetchOnClient: true,
32
+ },
33
+ provider2: {
34
+ keyVaults: {
35
+ baseURL: 'https://api2.example.com',
36
+ },
37
+ settings: {},
38
+ },
39
+ ollama: {
40
+ keyVaults: {},
41
+ settings: {},
42
+ fetchOnClient: true,
43
+ },
44
+ },
45
+ // Required by AIProviderStoreState
46
+ activeProviderModelList: [],
47
+ initAiProviderList: [],
48
+ providerSearchKeyword: '',
49
+ aiModelLoadingIds: [],
50
+ modelFetchingStatus: {},
51
+ modelRuntimeConfig: {},
52
+ modelSearchKeyword: '',
53
+ };
54
+
55
+ describe('enabledAiProviderList', () => {
56
+ it('should return enabled providers sorted by sort', () => {
57
+ const result = aiProviderSelectors.enabledAiProviderList(mockState);
58
+ expect(result).toEqual([
59
+ { id: 'provider3', enabled: true, sort: 0 },
60
+ { id: 'provider1', enabled: true, sort: 1 },
61
+ ]);
62
+ });
63
+ });
64
+
65
+ describe('disabledAiProviderList', () => {
66
+ it('should return disabled providers', () => {
67
+ const result = aiProviderSelectors.disabledAiProviderList(mockState);
68
+ expect(result).toEqual([{ id: 'provider2', enabled: false, sort: 2 }]);
69
+ });
70
+ });
71
+
72
+ describe('isProviderEnabled', () => {
73
+ it('should return true for enabled provider', () => {
74
+ expect(aiProviderSelectors.isProviderEnabled('provider1')(mockState)).toBe(true);
75
+ });
76
+
77
+ it('should return false for disabled provider', () => {
78
+ expect(aiProviderSelectors.isProviderEnabled('provider2')(mockState)).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('isProviderLoading', () => {
83
+ it('should return true for loading provider', () => {
84
+ expect(aiProviderSelectors.isProviderLoading('loading-provider')(mockState)).toBe(true);
85
+ });
86
+
87
+ it('should return false for non-loading provider', () => {
88
+ expect(aiProviderSelectors.isProviderLoading('provider1')(mockState)).toBe(false);
89
+ });
90
+ });
91
+
92
+ describe('activeProviderConfig', () => {
93
+ it('should return active provider config', () => {
94
+ expect(aiProviderSelectors.activeProviderConfig(mockState)).toEqual(
95
+ mockState.aiProviderDetail,
96
+ );
97
+ });
98
+ });
99
+
100
+ describe('isAiProviderConfigLoading', () => {
101
+ it('should return true if provider id does not match active provider', () => {
102
+ expect(aiProviderSelectors.isAiProviderConfigLoading('provider2')(mockState)).toBe(true);
103
+ });
104
+
105
+ it('should return false if provider id matches active provider', () => {
106
+ expect(aiProviderSelectors.isAiProviderConfigLoading('provider1')(mockState)).toBe(false);
107
+ });
108
+ });
109
+
110
+ describe('isActiveProviderEndpointNotEmpty', () => {
111
+ it('should return true when baseURL exists', () => {
112
+ expect(aiProviderSelectors.isActiveProviderEndpointNotEmpty(mockState)).toBe(true);
113
+ });
114
+
115
+ it('should return false when no endpoint info exists', () => {
116
+ const stateWithoutEndpoint = {
117
+ ...mockState,
118
+ aiProviderDetail: { keyVaults: {} },
119
+ };
120
+ expect(aiProviderSelectors.isActiveProviderEndpointNotEmpty(stateWithoutEndpoint)).toBe(
121
+ false,
122
+ );
123
+ });
124
+ });
125
+
126
+ describe('isActiveProviderApiKeyNotEmpty', () => {
127
+ it('should return true when apiKey exists', () => {
128
+ expect(aiProviderSelectors.isActiveProviderApiKeyNotEmpty(mockState)).toBe(true);
129
+ });
130
+
131
+ it('should return false when no api key exists', () => {
132
+ const stateWithoutApiKey = {
133
+ ...mockState,
134
+ aiProviderDetail: { keyVaults: {} },
135
+ };
136
+ expect(aiProviderSelectors.isActiveProviderApiKeyNotEmpty(stateWithoutApiKey)).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe('providerConfigById', () => {
141
+ it('should return config for existing provider', () => {
142
+ expect(aiProviderSelectors.providerConfigById('provider1')(mockState)).toEqual(
143
+ mockState.aiProviderRuntimeConfig.provider1,
144
+ );
145
+ });
146
+
147
+ it('should return undefined for non-existing provider', () => {
148
+ expect(aiProviderSelectors.providerConfigById('non-existing')(mockState)).toBeUndefined();
149
+ });
150
+
151
+ it('should return undefined for empty id', () => {
152
+ expect(aiProviderSelectors.providerConfigById('')(mockState)).toBeUndefined();
153
+ });
154
+ });
155
+
156
+ describe('isProviderConfigUpdating', () => {
157
+ it('should return true for updating provider', () => {
158
+ expect(aiProviderSelectors.isProviderConfigUpdating('updating-provider')(mockState)).toBe(
159
+ true,
160
+ );
161
+ });
162
+
163
+ it('should return false for non-updating provider', () => {
164
+ expect(aiProviderSelectors.isProviderConfigUpdating('provider1')(mockState)).toBe(false);
165
+ });
166
+ });
167
+
168
+ describe('isProviderFetchOnClient', () => {
169
+ it('should return false if provider is in disable browser request list', () => {
170
+ expect(
171
+ aiProviderSelectors.isProviderFetchOnClient('provider-with-disabled-browser')(mockState),
172
+ ).toBe(false);
173
+ });
174
+
175
+ it('should follow user settings for whitelisted providers', () => {
176
+ expect(aiProviderSelectors.isProviderFetchOnClient('ollama')(mockState)).toBe(true);
177
+ });
178
+
179
+ it('should return false if no endpoint and api key', () => {
180
+ const state = {
181
+ ...mockState,
182
+ aiProviderRuntimeConfig: {
183
+ test: {
184
+ keyVaults: {},
185
+ settings: {},
186
+ },
187
+ },
188
+ };
189
+ expect(aiProviderSelectors.isProviderFetchOnClient('test')(state)).toBe(false);
190
+ });
191
+
192
+ it('should return true if only baseURL exists', () => {
193
+ const state = {
194
+ ...mockState,
195
+ aiProviderRuntimeConfig: {
196
+ test: {
197
+ keyVaults: { baseURL: 'http://test.com' },
198
+ settings: {},
199
+ },
200
+ },
201
+ };
202
+ expect(aiProviderSelectors.isProviderFetchOnClient('test')(state)).toBe(true);
203
+ });
204
+
205
+ it('should follow user settings if both endpoint and api key exist', () => {
206
+ expect(aiProviderSelectors.isProviderFetchOnClient('provider1')(mockState)).toBe(true);
207
+ });
208
+ });
209
+
210
+ describe('providerKeyVaults', () => {
211
+ it('should return key vaults for existing provider', () => {
212
+ expect(aiProviderSelectors.providerKeyVaults('provider1')(mockState)).toEqual(
213
+ mockState.aiProviderRuntimeConfig.provider1.keyVaults,
214
+ );
215
+ });
216
+
217
+ it('should return undefined for undefined provider', () => {
218
+ expect(aiProviderSelectors.providerKeyVaults(undefined)(mockState)).toBeUndefined();
219
+ });
220
+
221
+ it('should return undefined for non-existing provider', () => {
222
+ expect(aiProviderSelectors.providerKeyVaults('non-existing')(mockState)).toBeUndefined();
223
+ });
224
+ });
225
+
226
+ describe('isProviderHasBuiltinSearch', () => {
227
+ it('should return true if provider has search mode', () => {
228
+ expect(aiProviderSelectors.isProviderHasBuiltinSearch('provider1')(mockState)).toBe(true);
229
+ });
230
+
231
+ it('should return false if provider has no search mode', () => {
232
+ expect(aiProviderSelectors.isProviderHasBuiltinSearch('provider2')(mockState)).toBe(false);
233
+ });
234
+ });
235
+
236
+ describe('isProviderHasBuiltinSearchConfig', () => {
237
+ it('should return false if search mode is internal', () => {
238
+ expect(aiProviderSelectors.isProviderHasBuiltinSearchConfig('provider1')(mockState)).toBe(
239
+ false,
240
+ );
241
+ });
242
+
243
+ it('should return false if no search mode exists', () => {
244
+ expect(aiProviderSelectors.isProviderHasBuiltinSearchConfig('provider2')(mockState)).toBe(
245
+ false,
246
+ );
247
+ });
248
+ });
249
+ });
@@ -1,4 +1,4 @@
1
- import { isProviderDisableBroswerRequest } from '@/config/modelProviders';
1
+ import { isProviderDisableBrowserRequest } from '@/config/modelProviders';
2
2
  import { AIProviderStoreState } from '@/store/aiInfra/initialState';
3
3
  import { AiProviderRuntimeConfig } from '@/types/aiProvider';
4
4
  import { GlobalLLMProviderKey } from '@/types/user/settings';
@@ -59,8 +59,8 @@ const isProviderFetchOnClient =
59
59
  (provider: GlobalLLMProviderKey | string) => (s: AIProviderStoreState) => {
60
60
  const config = providerConfigById(provider)(s);
61
61
 
62
- // If the provider already disable broswer request in model config, force on Server.
63
- if (isProviderDisableBroswerRequest(provider)) return false;
62
+ // If the provider already disable browser request in model config, force on Server.
63
+ if (isProviderDisableBrowserRequest(provider)) return false;
64
64
 
65
65
  // If the provider in the whitelist, follow the user settings
66
66
  if (providerWhitelist.has(provider) && typeof config?.fetchOnClient !== 'undefined')
@@ -110,10 +110,10 @@ describe('modelConfigSelectors', () => {
110
110
  expect(modelConfigSelectors.isProviderFetchOnClient('azure')(s)).toBe(true);
111
111
  });
112
112
 
113
- // Qwen provider not work in broswer request. Please skip this case if it work in future.
113
+ // Qwen provider not work in browser request. Please skip this case if it work in future.
114
114
  // Issue: https://github.com/lobehub/lobe-chat/issues/3108
115
115
  // PR: https://github.com/lobehub/lobe-chat/pull/3133
116
- it('client fecth should be disabled if provider is disable broswer request', () => {
116
+ it('client fecth should be disabled if provider is disable browser request', () => {
117
117
  const s = merge(initialSettingsState, {
118
118
  settings: {
119
119
  languageModel: {
@@ -1,4 +1,4 @@
1
- import { isProviderDisableBroswerRequest } from '@/config/modelProviders';
1
+ import { isProviderDisableBrowserRequest } from '@/config/modelProviders';
2
2
  import { UserStore } from '@/store/user';
3
3
  import { GlobalLLMProviderKey } from '@/types/user/settings';
4
4
 
@@ -19,8 +19,8 @@ const providerWhitelist = new Set(['ollama']);
19
19
  const isProviderFetchOnClient = (provider: GlobalLLMProviderKey | string) => (s: UserStore) => {
20
20
  const config = getProviderConfigById(provider)(s);
21
21
 
22
- // If the provider already disable broswer request in model config, force on Server.
23
- if (isProviderDisableBroswerRequest(provider)) return false;
22
+ // If the provider already disable browser request in model config, force on Server.
23
+ if (isProviderDisableBrowserRequest(provider)) return false;
24
24
 
25
25
  // If the provider in the whitelist, follow the user settings
26
26
  if (providerWhitelist.has(provider) && typeof config?.fetchOnClient !== 'undefined')