Compare commits

...

16 Commits

Author SHA1 Message Date
semantic-release-bot d718de7e1a 🔖 chore(release): v1.143.3 [skip ci]
### [Version 1.143.3](https://github.com/lobehub/lobe-chat/compare/v1.143.2...v1.143.3)
<sup>Released on **2026-01-25**</sup>

#### 🐛 Bug Fixes

- **misc**: Correct claude_args quoting to prevent shell-quote parsing errors.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: Correct claude_args quoting to prevent shell-quote parsing errors, closes [#10790](https://github.com/lobehub/lobe-chat/issues/10790) [#10789](https://github.com/lobehub/lobe-chat/issues/10789) ([cf197e6](https://github.com/lobehub/lobe-chat/commit/cf197e6))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2026-01-25 03:14:07 +00:00
René Wang 71de434b35 feat: Add notification card to v1 (#11736)
* feat: add ph card

* feat: add ph card

* feat: add ph card

* feat: add translation
2026-01-25 11:02:34 +08:00
YuTengjing cf197e638d 🐛 fix: correct claude_args quoting to prevent shell-quote parsing errors (#10790)
Closes #10789
2025-12-15 16:05:12 +08:00
lobehubbot fa55cd2897 📝 docs(bot): Auto sync agents & plugin to readme 2025-12-04 02:46:55 +00:00
semantic-release-bot 1b5afffe1e 🔖 chore(release): v1.143.2 [skip ci]
### [Version&nbsp;1.143.2](https://github.com/lobehub/lobe-chat/compare/v1.143.1...v1.143.2)
<sup>Released on **2025-12-04**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix React CVE.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: Fix React CVE, closes [#10592](https://github.com/lobehub/lobe-chat/issues/10592) ([20809b5](https://github.com/lobehub/lobe-chat/commit/20809b5))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-12-04 02:45:42 +00:00
Arvin Xu 20809b5245 🐛 fix: fix React CVE (#10592)
* fix

* pin

* pin
2025-12-04 10:34:00 +08:00
lobehubbot 52b4c1271e 📝 docs(bot): Auto sync agents & plugin to readme 2025-12-02 07:32:16 +00:00
semantic-release-bot f3a48ea4db 🔖 chore(release): v1.143.1 [skip ci]
### [Version&nbsp;1.143.1](https://github.com/lobehub/lobe-chat/compare/v1.143.0...v1.143.1)
<sup>Released on **2025-12-02**</sup>

#### 🐛 Bug Fixes

- **misc**: Deepseek  interleaved thinking.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: Deepseek  interleaved thinking, closes [#10550](https://github.com/lobehub/lobe-chat/issues/10550) ([73f3066](https://github.com/lobehub/lobe-chat/commit/73f3066))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-12-02 07:31:05 +00:00
Arvin Xu 73f3066b11 🐛 fix: deepseek interleaved thinking (#10550)
fix
2025-12-02 15:19:31 +08:00
lobehubbot 96784b68b5 📝 docs(bot): Auto sync agents & plugin to readme 2025-12-01 03:01:34 +00:00
semantic-release-bot eb0b8a56c7 🔖 chore(release): v1.143.0 [skip ci]
## [Version&nbsp;1.143.0](https://github.com/lobehub/lobe-chat/compare/v1.142.9...v1.143.0)
<sup>Released on **2025-12-01**</sup>

####  Features

- **misc**: Support DeepSeek Interleaved thinking.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's improved

* **misc**: Support DeepSeek Interleaved thinking, closes [#10478](https://github.com/lobehub/lobe-chat/issues/10478) [#10219](https://github.com/lobehub/lobe-chat/issues/10219) [#10152](https://github.com/lobehub/lobe-chat/issues/10152) ([aee5d71](https://github.com/lobehub/lobe-chat/commit/aee5d71))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-12-01 03:00:20 +00:00
Arvin Xu aee5d7144f feat: support DeepSeek Interleaved thinking (#10478)
*  feat: support DeepSeek Interleaved thinking (#10219)

* 🐛 fix: add SSRF protection  (#10152)

* fix snap

---------

Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2025-12-01 09:57:10 +08:00
lobehubbot 60924d7742 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-02 13:49:18 +00:00
semantic-release-bot a2a097fbec 🔖 chore(release): v1.142.9 [skip ci]
### [Version&nbsp;1.142.9](https://github.com/lobehub/lobe-chat/compare/v1.142.8...v1.142.9)
<sup>Released on **2025-11-02**</sup>

#### 🐛 Bug Fixes

- **misc**: OIDC error when connecting to self-host instance.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: OIDC error when connecting to self-host instance, closes [#9916](https://github.com/lobehub/lobe-chat/issues/9916) ([2e2b9c4](https://github.com/lobehub/lobe-chat/commit/2e2b9c4))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-11-02 13:47:55 +00:00
YuTengjing 197118347c 📝 docs: update ComfyUI documentation cover image URL (#9997) 2025-11-02 21:35:01 +08:00
Aloxaf 2e2b9c4c88 🐛 fix: OIDC error when connecting to self-host instance (#9916)
fix: oidc/consent redirect header
2025-10-31 00:25:21 +08:00
115 changed files with 1060 additions and 378 deletions
+11
View File
@@ -13,6 +13,17 @@
# Default is '0' (enabled)
# ENABLED_CSP=1
# SSRF Protection Settings
# Set to '1' to allow connections to private IP addresses (disable SSRF protection)
# WARNING: Only enable this in trusted environments
# Default is '0' (SSRF protection enabled)
# SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
# Whitelist of allowed private IP addresses (comma-separated)
# Only takes effect when SSRF_ALLOW_PRIVATE_IP_ADDRESS is '0'
# Example: Allow specific internal servers while keeping SSRF protection
# SSRF_ALLOW_IP_ADDRESS_LIST=192.168.1.100,10.0.0.50
########################################
########## AI Provider Service #########
########################################
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
github_token: ${{ secrets.GH_TOKEN }}
allowed_non_write_users: "*"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--allowed-tools Bash(gh *),Read"
claude_args: '--allowed-tools "Bash(gh *),Read"'
prompt: |
You're an issue triage assistant for GitHub issues. Your task is to analyze issues, apply appropriate labels, and mention the responsible team member.
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
github_token: ${{ secrets.GH_TOKEN }}
allowed_non_write_users: "*"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)"
claude_args: '--allowed-tools "Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)"'
prompt: |
You are a multilingual translation assistant. You need to respond to the following four types of GitHub Webhook events:
+125
View File
@@ -2,6 +2,131 @@
# Changelog
### [Version 1.143.3](https://github.com/lobehub/lobe-chat/compare/v1.143.2...v1.143.3)
<sup>Released on **2026-01-25**</sup>
#### 🐛 Bug Fixes
- **misc**: Correct claude_args quoting to prevent shell-quote parsing errors.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Correct claude_args quoting to prevent shell-quote parsing errors, closes [#10790](https://github.com/lobehub/lobe-chat/issues/10790) [#10789](https://github.com/lobehub/lobe-chat/issues/10789) ([cf197e6](https://github.com/lobehub/lobe-chat/commit/cf197e6))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.143.2](https://github.com/lobehub/lobe-chat/compare/v1.143.1...v1.143.2)
<sup>Released on **2025-12-04**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix React CVE.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix React CVE, closes [#10592](https://github.com/lobehub/lobe-chat/issues/10592) ([20809b5](https://github.com/lobehub/lobe-chat/commit/20809b5))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.143.1](https://github.com/lobehub/lobe-chat/compare/v1.143.0...v1.143.1)
<sup>Released on **2025-12-02**</sup>
#### 🐛 Bug Fixes
- **misc**: Deepseek interleaved thinking.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Deepseek interleaved thinking, closes [#10550](https://github.com/lobehub/lobe-chat/issues/10550) ([73f3066](https://github.com/lobehub/lobe-chat/commit/73f3066))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.143.0](https://github.com/lobehub/lobe-chat/compare/v1.142.9...v1.143.0)
<sup>Released on **2025-12-01**</sup>
#### ✨ Features
- **misc**: Support DeepSeek Interleaved thinking.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Support DeepSeek Interleaved thinking, closes [#10478](https://github.com/lobehub/lobe-chat/issues/10478) [#10219](https://github.com/lobehub/lobe-chat/issues/10219) [#10152](https://github.com/lobehub/lobe-chat/issues/10152) ([aee5d71](https://github.com/lobehub/lobe-chat/commit/aee5d71))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.142.9](https://github.com/lobehub/lobe-chat/compare/v1.142.8...v1.142.9)
<sup>Released on **2025-11-02**</sup>
#### 🐛 Bug Fixes
- **misc**: OIDC error when connecting to self-host instance.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: OIDC error when connecting to self-host instance, closes [#9916](https://github.com/lobehub/lobe-chat/issues/9916) ([2e2b9c4](https://github.com/lobehub/lobe-chat/commit/2e2b9c4))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.142.8](https://github.com/lobehub/lobe-chat/compare/v1.142.7...v1.142.8)
<sup>Released on **2025-10-30**</sup>
+7 -7
View File
@@ -381,14 +381,14 @@ In addition, these plugins are not limited to news aggregation, but can also ext
<!-- PLUGIN LIST -->
| Recent Submits | Description |
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
| [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` |
| Recent Submits | Description |
| -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | Enter any URL and keyword and get an On-Page SEO analysis & insights!<br/>`seo` |
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
> 📊 Total plugins: [<kbd>**41**</kbd>](https://lobechat.com/discover/plugins)
<!-- PLUGIN LIST -->
+7 -7
View File
@@ -374,14 +374,14 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
<!-- PLUGIN LIST -->
| 最近新增 | 描述 |
| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
| [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
| 最近新增 | 描述 |
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-11-28**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | 输入任何 URL 和关键词,获取页面 SEO 分析和见解!<br/>`seo` |
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
> 📊 Total plugins: [<kbd>**41**</kbd>](https://lobechat.com/discover/plugins)
<!-- PLUGIN LIST -->
+28
View File
@@ -1,4 +1,32 @@
[
{
"children": {
"fixes": ["Fix React CVE."]
},
"date": "2025-12-04",
"version": "1.143.2"
},
{
"children": {
"fixes": ["Deepseek interleaved thinking."]
},
"date": "2025-12-02",
"version": "1.143.1"
},
{
"children": {
"features": ["Support DeepSeek Interleaved thinking."]
},
"date": "2025-12-01",
"version": "1.143.0"
},
{
"children": {
"fixes": ["OIDC error when connecting to self-host instance."]
},
"date": "2025-11-02",
"version": "1.142.9"
},
{
"children": {},
"date": "2025-10-30",
@@ -127,16 +127,62 @@ For specific content, please refer to the [Feature Flags](/docs/self-hosting/adv
### `SSRF_ALLOW_PRIVATE_IP_ADDRESS`
- Type: Optional
- Description: Allow to connect private IP address. In a trusted environment, it can be set to true to turn off SSRF protection.
- Description: Controls whether to allow connections to private IP addresses. Set to `1` to disable SSRF protection and allow all private IP addresses. In a trusted environment (e.g., internal network), this can be enabled to allow access to internal resources.
- Default: `0`
- Example: `1` or `0`
<Callout type="warning">
**Security Notice**: Enabling this option will disable SSRF protection and allow connections to private
IP addresses (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.). Only enable this in
trusted environments where you need to access internal network resources.
</Callout>
**Use Cases**:
LobeChat performs SSRF security checks in the following scenarios:
1. **Image/Video URL to Base64 Conversion**: When processing media messages (e.g., vision models, multimodal models), LobeChat converts image and video URLs to base64 format. This check prevents malicious users from accessing internal network resources.
Examples:
- Image: A user sends an image message with URL `http://192.168.1.100/admin/secrets.png`
- Video: A user sends a video message with URL `http://10.0.0.50/internal/meeting.mp4`
Without SSRF protection, these requests could expose internal network resources.
2. **Web Crawler**: When using web crawling features to fetch external content.
3. **Proxy Requests**: When proxying external API requests.
**Configuration Examples**:
```bash
# Scenario 1: Public deployment (recommended)
# Block all private IP addresses for security
SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
# Scenario 2: Internal deployment
# Allow all private IP addresses to access internal image servers
SSRF_ALLOW_PRIVATE_IP_ADDRESS=1
# Scenario 3: Hybrid deployment (most common)
# Block private IPs by default, but allow specific trusted internal servers
SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
SSRF_ALLOW_IP_ADDRESS_LIST=192.168.1.100,10.0.0.50
```
### `SSRF_ALLOW_IP_ADDRESS_LIST`
- Type: Optional
- Description: Allow private IP address list, multiple IP addresses are separated by commas. Only when `SSRF_ALLOW_PRIVATE_IP_ADDRESS` is `0`, it takes effect.
- Description: Whitelist of allowed IP addresses, separated by commas. Only takes effect when `SSRF_ALLOW_PRIVATE_IP_ADDRESS` is `0`. Use this to allow specific internal IP addresses while keeping SSRF protection enabled for other private IPs.
- Default: -
- Example: `198.18.1.62,224.0.0.3`
- Example: `192.168.1.100,10.0.0.50,172.16.0.10`
**Common Use Cases**:
- Allow access to internal image storage server: `192.168.1.100`
- Allow access to internal API gateway: `10.0.0.50`
- Allow access to internal documentation server: `172.16.0.10`
### `ENABLE_AUTH_PROTECTION`
@@ -123,16 +123,61 @@ LobeChat 在部署时提供了一些额外的配置项,你可以使用环境
### `SSRF_ALLOW_PRIVATE_IP_ADDRESS`
- 类型:可选
- 描述:是否允许连接私有 IP 地址。在可信环境中可以设置为 true 来关闭 SSRF 防护
- 描述:控制是否允许连接私有 IP 地址。设置为 `1` 时将关闭 SSRF 防护并允许所有私有 IP 地址。在可信环境(如内网部署)中,可以启用此选项以访问内部资源
- 默认值:`0`
- 示例:`1` or `0`
- 示例:`1` `0`
<Callout type="warning">
**安全提示**:启用此选项将关闭 SSRF 防护,允许连接私有 IP 地址段(127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16
等)。仅在需要访问内网资源的可信环境中启用。
</Callout>
**应用场景**
LobeChat 会在以下场景执行 SSRF 安全检查:
1. **图片 / 视频 URL 转 Base64**:在处理媒体消息时(例如视觉模型、多模态模型),LobeChat 会将图片和视频 URL 转换为 base64 格式。此检查可防止恶意用户通过媒体 URL 访问内网资源。
举例:
- 图片:用户发送图片消息,URL 为 `http://192.168.1.100/admin/secrets.png`
- 视频:用户发送视频消息,URL 为 `http://10.0.0.50/internal/meeting.mp4`
若无 SSRF 防护,这些请求可能导致内网资源泄露。
2. **网页爬取**:使用网页爬取功能获取外部内容时。
3. **代理请求**:代理外部 API 请求时。
**配置示例**
```bash
# 场景 1:公网部署(推荐)
# 阻止所有私有 IP 访问,保证安全
SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
# 场景 2:内网部署
# 允许所有私有 IP,可访问内网图片服务器等资源
SSRF_ALLOW_PRIVATE_IP_ADDRESS=1
# 场景 3:混合部署(最常见)
# 默认阻止私有 IP,但允许特定可信的内网服务器
SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
SSRF_ALLOW_IP_ADDRESS_LIST=192.168.1.100,10.0.0.50
```
### `SSRF_ALLOW_IP_ADDRESS_LIST`
- 类型:可选
- 说明:允许的私有 IP 地址列表,多个 IP 地址用逗号分隔。仅在 `SSRF_ALLOW_PRIVATE_IP_ADDRESS` 为 `0` 时生效。
- 描述:允许访问的 IP 地址白名单,多个 IP 地址用逗号分隔。仅在 `SSRF_ALLOW_PRIVATE_IP_ADDRESS` 为 `0` 时生效。使用此选项可以在保持 SSRF 防护的同时,允许访问特定的内网 IP 地址。
- 默认值:-
- 示例:`198.18.1.62,224.0.0.3`
- 示例:`192.168.1.100,10.0.0.50,172.16.0.10`
**常见使用场景**
- 允许访问内网图片存储服务器:`192.168.1.100`
- 允许访问内网 API 网关:`10.0.0.50`
- 允许访问内网文档服务器:`172.16.0.10`
### `ENABLE_AUTH_PROTECTION`
+1 -1
View File
@@ -11,7 +11,7 @@ tags:
# Using ComfyUI in LobeChat
<Image alt={'Using ComfyUI in LobeChat'} cover src={'https://github.com/lobehub/lobe-chat/assets/17870709/c9e5eafc-ca22-496b-a88d-cc0ae53bf720'} />
<Image alt={'Using ComfyUI in LobeChat'} cover src={'https://hub-apac-1.lobeobjects.space/docs/e9b811f248a1db2bd1be1af888cf9b9d.png'} />
This documentation will guide you on how to use [ComfyUI](https://github.com/comfyanonymous/ComfyUI) in LobeChat for high-quality AI image generation and editing.
+1 -1
View File
@@ -11,7 +11,7 @@ tags:
# 在 LobeChat 中使用 ComfyUI
<Image alt={'在 LobeChat 中使用 ComfyUI'} cover src={'https://github.com/lobehub/lobe-chat/assets/17870709/c9e5eafc-ca22-496b-a88d-cc0ae53bf720'} />
<Image alt={'在 LobeChat 中使用 ComfyUI'} cover src={'https://hub-apac-1.lobeobjects.space/docs/e9b811f248a1db2bd1be1af888cf9b9d.png'} />
本文档将指导你如何在 LobeChat 中使用 [ComfyUI](https://github.com/comfyanonymous/ComfyUI) 进行高质量的 AI 图像生成和编辑。
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "الموقع الرسمي",
"ok": "موافق",
"password": "كلمة المرور",
"phLaunch": {
"action": "اذهب إلى الدعم",
"body": "نحن نُطلق LobeChat على Product Hunt! إذا أعجبك LobeChat، صوّت لنا وادعمنا!",
"title": "LobeChat يتم إطلاقه على Product Hunt!"
},
"pin": "تثبيت في الأعلى",
"pinOff": "إلغاء التثبيت",
"privacy": "سياسة الخصوصية",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Официален сайт",
"ok": "Добре",
"password": "Парола",
"phLaunch": {
"action": "Отидете на страницата за подкрепа",
"body": "Стартираме в Product Hunt! Ако харесвате LobeChat, подкрепете ни с вашия глас!",
"title": "LobeChat стартира в Product Hunt!"
},
"pin": "Закачи",
"pinOff": "Откачи",
"privacy": "Политика за поверителност",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Offizielle Website",
"ok": "OK",
"password": "Passwort",
"phLaunch": {
"action": "Zur Unterstützung",
"body": "Wir starten gerade auf Product Hunt! Wenn dir LobeChat gefällt, gib uns bitte deine Stimme!",
"title": "LobeChat startet jetzt auf Product Hunt!"
},
"pin": "Anheften",
"pinOff": "Anheften aufheben",
"privacy": "Datenschutzrichtlinie",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Official Website",
"ok": "OK",
"password": "Password",
"phLaunch": {
"action": "Support Us",
"body": "Your support means a lot to us!",
"title": "We are on Product Hunt!"
},
"pin": "Pin",
"pinOff": "Unpin",
"privacy": "Privacy Policy",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Sitio oficial",
"ok": "Aceptar",
"password": "Contraseña",
"phLaunch": {
"action": "Ir a apoyar",
"body": "¡Estamos lanzando en Product Hunt! Si te gusta LobeChat, ¡vota por nosotros!",
"title": "¡LobeChat se lanza en Product Hunt!"
},
"pin": "Fijar",
"pinOff": "Quitar fijación",
"privacy": "Política de privacidad",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "وب‌سایت رسمی",
"ok": "تأیید",
"password": "رمز عبور",
"phLaunch": {
"action": "رفتن به صفحه پشتیبانی",
"body": "ما در Product Hunt راه‌اندازی شده‌ایم! اگر LobeChat را دوست دارید، لطفاً به ما رأی دهید و حمایت کنید!",
"title": "LobeChat در Product Hunt راه‌اندازی شد!"
},
"pin": "سنجاق کردن",
"pinOff": "لغو سنجاق کردن",
"privacy": "سیاست حفظ حریم خصوصی",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Site officiel",
"ok": "OK",
"password": "Mot de passe",
"phLaunch": {
"action": "Aller au soutien",
"body": "Nous lançons LobeChat sur Product Hunt ! Si vous aimez LobeChat, votez pour nous soutenir !",
"title": "LobeChat est lancé sur Product Hunt !"
},
"pin": "Épingler",
"pinOff": "Désactiver l'épinglage",
"privacy": "Politique de confidentialité",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Sito ufficiale",
"ok": "OK",
"password": "Password",
"phLaunch": {
"action": "Vai al supporto",
"body": "Stiamo lanciando su Product Hunt! Se ti piace LobeChat, votaci e supportaci!",
"title": "LobeChat è ora su Product Hunt!"
},
"pin": "Fissa in alto",
"pinOff": "Annulla fissaggio",
"privacy": "Informativa sulla privacy",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "公式サイト",
"ok": "OK",
"password": "パスワード",
"phLaunch": {
"action": "サポートする",
"body": "私たちは現在 Product Hunt で公開中です!LobeChat を気に入っていただけたら、ぜひ投票をお願いします!",
"title": "LobeChat が Product Hunt に登場!"
},
"pin": "ピン留め",
"pinOff": "ピン留め解除",
"privacy": "プライバシーポリシー",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "공식 웹사이트",
"ok": "확인",
"password": "비밀번호",
"phLaunch": {
"action": "지원하러 가기",
"body": "우리는 지금 Product Hunt에 출시되었습니다! LobeChat이 마음에 드신다면 투표로 응원해 주세요!",
"title": "LobeChat이 Product Hunt에 출시되었습니다!"
},
"pin": "상단 고정",
"pinOff": "고정 해제",
"privacy": "개인정보 보호정책",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Officiële website",
"ok": "Oké",
"password": "Wachtwoord",
"phLaunch": {
"action": "Ga naar ondersteuning",
"body": "We lanceren op Product Hunt! Als je LobeChat leuk vindt, stem dan op ons!",
"title": "LobeChat lanceert op Product Hunt!"
},
"pin": "Vastzetten",
"pinOff": "Vastzetten uitschakelen",
"privacy": "Privacybeleid",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Oficjalna strona internetowa",
"ok": "OK",
"password": "Hasło",
"phLaunch": {
"action": "Przejdź do wsparcia",
"body": "Właśnie debiutujemy na Product Hunt! Jeśli podoba Ci się LobeChat, zagłosuj na nas!",
"title": "LobeChat debiutuje na Product Hunt!"
},
"pin": "Przypnij",
"pinOff": "Odepnij",
"privacy": "Polityka prywatności",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Site Oficial",
"ok": "OK",
"password": "Senha",
"phLaunch": {
"action": "Apoie-nos",
"body": "Estamos lançando no Product Hunt! Se você gosta do LobeChat, vote em nós!",
"title": "LobeChat está sendo lançado no Product Hunt!"
},
"pin": "Fixar",
"pinOff": "Desafixar",
"privacy": "Política de Privacidade",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Официальный сайт",
"ok": "ОК",
"password": "Пароль",
"phLaunch": {
"action": "Перейти к поддержке",
"body": "Мы запускаемся на Product Hunt! Если вам нравится LobeChat, проголосуйте за нас!",
"title": "LobeChat запускается на Product Hunt!"
},
"pin": "Закрепить",
"pinOff": "Открепить",
"privacy": "Политика конфиденциальности",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Resmi Site",
"ok": "Tamam",
"password": "Password",
"phLaunch": {
"action": "Destekle",
"body": "Product Hunt'ta yayındayız! LobeChat'i beğendiyseniz, bize oy vererek destek olun!",
"title": "LobeChat, Product Hunt'ta yayında!"
},
"pin": "Pin",
"pinOff": "Unpin",
"privacy": "Gizlilik Politikası",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "Trang web chính thức",
"ok": "Đồng ý",
"password": "Mật khẩu",
"phLaunch": {
"action": "Truy cập để ủng hộ",
"body": "Chúng tôi đang ra mắt trên Product Hunt! Nếu bạn yêu thích LobeChat, hãy bình chọn ủng hộ chúng tôi!",
"title": "LobeChat đang ra mắt trên Product Hunt!"
},
"pin": "Ghim",
"pinOff": "Bỏ ghim",
"privacy": "Chính sách bảo mật",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "官方网站",
"ok": "确定",
"password": "密码",
"phLaunch": {
"action": "前往支持",
"body": "我们正在 Product Hunt 上发布!如果你喜欢 LobeChat,请给我们投票支持!",
"title": "LobeChat 正在 Product Hunt 上发布!"
},
"pin": "置顶",
"pinOff": "取消置顶",
"privacy": "隐私政策",
+5
View File
@@ -286,6 +286,11 @@
"officialSite": "官方網站",
"ok": "確定",
"password": "密碼",
"phLaunch": {
"action": "前往支持",
"body": "我們正在 Product Hunt 上發布!如果你喜歡 LobeChat,請為我們投票支持!",
"title": "LobeChat 正在 Product Hunt 上發布!"
},
"pin": "置頂",
"pinOff": "取消置頂",
"privacy": "隱私政策",
+10 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@lobehub/chat",
"version": "1.142.8",
"version": "1.143.3",
"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.",
"keywords": [
"framework",
@@ -33,12 +33,12 @@
"prebuild": "tsx scripts/prebuild.mts && npm run lint",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=6144 next build",
"postbuild": "npm run build-sitemap && npm run build-migrate-db",
"build-migrate-db": "bun run db:migrate",
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
"build:analyze": "NODE_OPTIONS=--max-old-space-size=6144 ANALYZE=true next build",
"build:docker": "npm run prebuild && NODE_OPTIONS=--max-old-space-size=6144 DOCKER=true next build && npm run build-sitemap",
"prebuild:electron": "cross-env NEXT_PUBLIC_IS_DESKTOP_APP=1 tsx scripts/prebuild.mts",
"build:electron": "cross-env NODE_OPTIONS=--max-old-space-size=6144 NEXT_PUBLIC_IS_DESKTOP_APP=1 NEXT_PUBLIC_SERVICE_MODE=server next build",
"build-migrate-db": "bun run db:migrate",
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
"clean:node_modules": "bash -lc 'set -e; echo \"Removing all node_modules...\"; rm -rf node_modules; pnpm -r exec rm -rf node_modules; rm -rf apps/desktop/node_modules; echo \"All node_modules removed.\"'",
"db:generate": "drizzle-kit generate && npm run db:generate-client && npm run workflow:dbml",
"db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
@@ -79,11 +79,11 @@
"start": "next start -p 3210",
"stylelint": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
"test": "npm run test-app && npm run test-server",
"test-app": "vitest run",
"test-app:coverage": "vitest --coverage --silent='passed-only'",
"test:e2e": "pnpm --filter @lobechat/e2e-tests test",
"test:e2e:smoke": "pnpm --filter @lobechat/e2e-tests test:smoke",
"test:update": "vitest -u",
"test-app": "vitest run",
"test-app:coverage": "vitest --coverage --silent='passed-only'",
"type-check": "tsgo --noEmit",
"webhook:ngrok": "ngrok http http://localhost:3011",
"workflow:cdn": "tsx ./scripts/cdnWorkflow/index.ts",
@@ -156,6 +156,7 @@
"@lobechat/database": "workspace:*",
"@lobechat/electron-client-ipc": "workspace:*",
"@lobechat/electron-server-ipc": "workspace:*",
"@lobechat/fetch-sse": "workspace:*",
"@lobechat/file-loaders": "workspace:*",
"@lobechat/model-runtime": "workspace:*",
"@lobechat/observability-otel": "workspace:*",
@@ -172,7 +173,7 @@
"@lobehub/market-sdk": "^0.22.7",
"@lobehub/tts": "^2.0.1",
"@lobehub/ui": "^2.13.2",
"@modelcontextprotocol/sdk": "^1.20.0",
"@modelcontextprotocol/sdk": "1.23.0",
"@neondatabase/serverless": "^1.0.2",
"@next/third-parties": "^15.5.4",
"@opentelemetry/exporter-jaeger": "^2.1.0",
@@ -235,7 +236,7 @@
"model-bank": "workspace:*",
"modern-screenshot": "^4.6.6",
"nanoid": "^5.1.6",
"next": "~15.3.5",
"next": "~15.3.6",
"next-auth": "5.0.0-beta.30",
"next-mdx-remote": "^5.0.0",
"nextjs-toploader": "^3.9.17",
@@ -261,9 +262,9 @@
"pwa-install-handler": "^2.6.3",
"query-string": "^9.3.1",
"random-words": "^2.0.1",
"react": "^19.2.0",
"react": "^19.2.1",
"react-confetti": "^6.4.0",
"react-dom": "^19.2.0",
"react-dom": "^19.2.1",
"react-fast-marquee": "^1.6.5",
"react-hotkeys-hook": "^5.1.0",
"react-i18next": "^15.7.4",
@@ -66,6 +66,7 @@ export class MessageCleanupProcessor extends BaseProcessor {
content: message.content,
role: message.role,
...(message.tool_calls && { tool_calls: message.tool_calls }),
...(message.reasoning && { reasoning: message.reasoning }),
};
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "@lobechat/fetch-sse",
"version": "1.0.0",
"private": true,
"description": "SSE fetch utilities with streaming support",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./parseError": {
"types": "./src/parseError.ts",
"default": "./src/parseError.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {
"@lobechat/const": "workspace:*",
"@lobechat/model-runtime": "workspace:*",
"@lobechat/types": "workspace:*",
"@lobechat/utils": "workspace:*",
"i18next": "^24.2.1"
}
}
@@ -1,10 +1,10 @@
import { MESSAGE_CANCEL_FLAT } from '@lobechat/const';
import { ChatMessageError } from '@lobechat/types';
import { FetchEventSourceInit } from '@lobechat/utils/client/fetchEventSource/index';
import { fetchEventSource } from '@lobechat/utils/client/fetchEventSource/index';
import { sleep } from '@lobechat/utils/sleep';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { FetchEventSourceInit } from '../../client/fetchEventSource';
import { fetchEventSource } from '../../client/fetchEventSource';
import { sleep } from '../../sleep';
import { fetchSSE } from '../fetchSSE';
// 模拟 i18next
@@ -12,7 +12,7 @@ vi.mock('i18next', () => ({
t: vi.fn((key) => `translated_${key}`),
}));
vi.mock('../../client/fetchEventSource', () => ({
vi.mock('@lobechat/utils/client/fetchEventSource/index', () => ({
fetchEventSource: vi.fn(),
}));
@@ -1,14 +1,14 @@
import { ErrorResponse } from '@lobechat/types';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { getMessageError } from '../parseError';
// 模拟 i18next
// Mock i18next
vi.mock('i18next', () => ({
t: vi.fn((key) => `translated_${key}`),
}));
// 模拟 Response
// Mock Response
const createMockResponse = (body: any, ok: boolean, status: number = 200) => ({
ok,
status,
@@ -38,11 +38,14 @@ const createMockResponse = (body: any, ok: boolean, status: number = 200) => ({
},
});
// 在每次测试后清理所有模拟
afterEach(() => {
vi.restoreAllMocks();
});
beforeEach(() => {
vi.clearAllMocks();
});
describe('getMessageError', () => {
it('should handle business error correctly', async () => {
const mockErrorResponse: ErrorResponse = {
@@ -12,9 +12,9 @@ import {
ResponseAnimation,
ResponseAnimationStyle,
} from '@lobechat/types';
import { fetchEventSource } from '@lobechat/utils/client/fetchEventSource/index';
import { nanoid } from '@lobechat/utils/uuid';
import { fetchEventSource } from '../client/fetchEventSource';
import { nanoid } from '../uuid';
import { getMessageError } from './parseError';
type SSEFinishType = 'done' | 'error' | 'abort';
@@ -1,7 +1,7 @@
import { ChatMessageError, ErrorResponse, ErrorType } from '@lobechat/types';
import { t } from 'i18next';
export const getMessageError = async (response: Response) => {
export const getMessageError = async (response: Response): Promise<ChatMessageError> => {
let chatMessageError: ChatMessageError;
// try to get the biz error
@@ -9,13 +9,13 @@ export const getMessageError = async (response: Response) => {
const data = (await response.json()) as ErrorResponse;
chatMessageError = {
body: data.body,
message: t(`response.${data.errorType}` as any, { ns: 'error' }),
message: t(`response.${data.errorType}`, { ns: 'error' }),
type: data.errorType,
};
} catch {
// if not return, then it's a common error
chatMessageError = {
message: t(`response.${response.status}` as any, { ns: 'error' }),
message: t(`response.${response.status}`, { ns: 'error' }),
type: response.status as ErrorType,
};
}
@@ -1,8 +1,8 @@
import { imageUrlToBase64 } from '@lobechat/utils';
import { OpenAI } from 'openai';
import { describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { OpenAIChatMessage, UserMessageContentPart } from '../../types/chat';
import { imageUrlToBase64 } from '../../utils/imageToBase64';
import { parseDataUri } from '../../utils/uriParser';
import {
buildAnthropicBlock,
@@ -12,16 +12,22 @@ import {
} from './anthropic';
// Mock the parseDataUri function since it's an implementation detail
vi.mock('../../utils/uriParser', () => ({
parseDataUri: vi.fn().mockReturnValue({
mimeType: 'image/jpeg',
base64: 'base64EncodedString',
type: 'base64',
}),
vi.mock('../../utils/uriParser');
vi.mock('@lobechat/utils', () => ({
imageUrlToBase64: vi.fn(),
}));
vi.mock('../../utils/imageToBase64');
describe('anthropicHelpers', () => {
beforeEach(() => {
vi.resetAllMocks();
// Set default mock implementation for parseDataUri
vi.mocked(parseDataUri).mockReturnValue({
mimeType: 'image/jpeg',
base64: 'base64EncodedString',
type: 'base64',
});
});
describe('buildAnthropicBlock', () => {
it('should return the content as is for text type', async () => {
const content: UserMessageContentPart = { type: 'text', text: 'Hello!' };
@@ -52,7 +58,7 @@ describe('anthropicHelpers', () => {
base64: null,
type: 'url',
});
vi.mocked(imageUrlToBase64).mockResolvedValue({
vi.mocked(imageUrlToBase64).mockResolvedValueOnce({
base64: 'convertedBase64String',
mimeType: 'image/jpg',
});
@@ -82,7 +88,7 @@ describe('anthropicHelpers', () => {
base64: null,
type: 'url',
});
vi.mocked(imageUrlToBase64).mockResolvedValue({
vi.mocked(imageUrlToBase64).mockResolvedValueOnce({
base64: 'convertedBase64String',
mimeType: 'image/png',
});
@@ -1,8 +1,8 @@
import Anthropic from '@anthropic-ai/sdk';
import { imageUrlToBase64 } from '@lobechat/utils';
import OpenAI from 'openai';
import { OpenAIChatMessage, UserMessageContentPart } from '../../types';
import { imageUrlToBase64 } from '../../utils/imageToBase64';
import { parseDataUri } from '../../utils/uriParser';
export const buildAnthropicBlock = async (
@@ -1,9 +1,9 @@
// @vitest-environment node
import { Type as SchemaType } from '@google/genai';
import * as imageToBase64Module from '@lobechat/utils';
import { describe, expect, it, vi } from 'vitest';
import { ChatCompletionTool, OpenAIChatMessage, UserMessageContentPart } from '../../types';
import * as imageToBase64Module from '../../utils/imageToBase64';
import { parseDataUri } from '../../utils/uriParser';
import {
buildGoogleMessage,
@@ -5,9 +5,9 @@ import {
Part,
Type as SchemaType,
} from '@google/genai';
import { imageUrlToBase64 } from '@lobechat/utils';
import { ChatCompletionTool, OpenAIChatMessage, UserMessageContentPart } from '../../types';
import { imageUrlToBase64 } from '../../utils/imageToBase64';
import { safeParseJSON } from '../../utils/safeParseJSON';
import { parseDataUri } from '../../utils/uriParser';
@@ -64,12 +64,9 @@ export const buildGooglePart = async (
}
if (type === 'url') {
// For video URLs, we need to fetch and convert to base64
// Use imageUrlToBase64 for SSRF protection (works for any binary data including videos)
// Note: This might need size/duration limits for practical use
const response = await fetch(content.video_url.url);
const arrayBuffer = await response.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString('base64');
const mimeType = response.headers.get('content-type') || 'video/mp4';
const { base64, mimeType } = await imageUrlToBase64(content.video_url.url);
return {
inlineData: { data: base64, mimeType },
@@ -1,7 +1,8 @@
import { imageUrlToBase64 } from '@lobechat/utils';
import OpenAI from 'openai';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { imageUrlToBase64 } from '../../utils/imageToBase64';
import { OpenAIChatMessage } from '../../types';
import { parseDataUri } from '../../utils/uriParser';
import {
convertImageUrlToFile,
@@ -11,7 +12,9 @@ import {
} from './openai';
// 模拟依赖
vi.mock('../../utils/imageToBase64');
vi.mock('@lobechat/utils', () => ({
imageUrlToBase64: vi.fn(),
}));
vi.mock('../../utils/uriParser');
describe('convertMessageContent', () => {
@@ -147,11 +150,71 @@ describe('convertOpenAIMessages', () => {
expect(Promise.all).toHaveBeenCalledTimes(2); // 一次用于消息数组,一次用于内容数组
});
it('should filter out reasoning field from messages', async () => {
const messages = [
{
role: 'assistant',
content: 'Hello',
reasoning: { content: 'some reasoning', duration: 100 },
},
{ role: 'user', content: 'Hi' },
] as any;
const result = await convertOpenAIMessages(messages);
expect(result).toEqual([
{ role: 'assistant', content: 'Hello' },
{ role: 'user', content: 'Hi' },
]);
// Ensure reasoning field is removed
expect((result[0] as any).reasoning).toBeUndefined();
});
it('should preserve reasoning_content field from messages (for DeepSeek compatibility)', async () => {
const messages = [
{
role: 'assistant',
content: 'Hello',
reasoning_content: 'some reasoning content',
},
{ role: 'user', content: 'Hi' },
] as any;
const result = await convertOpenAIMessages(messages);
expect(result).toEqual([
{ role: 'assistant', content: 'Hello', reasoning_content: 'some reasoning content' },
{ role: 'user', content: 'Hi' },
]);
// Ensure reasoning_content field is preserved
expect((result[0] as any).reasoning_content).toBe('some reasoning content');
});
it('should filter out reasoning but preserve reasoning_content field', async () => {
const messages = [
{
role: 'assistant',
content: 'Hello',
reasoning: { content: 'some reasoning', duration: 100 },
reasoning_content: 'some reasoning content',
},
] as any;
const result = await convertOpenAIMessages(messages);
expect(result).toEqual([
{ role: 'assistant', content: 'Hello', reasoning_content: 'some reasoning content' },
]);
// Ensure reasoning object is removed but reasoning_content is preserved
expect((result[0] as any).reasoning).toBeUndefined();
expect((result[0] as any).reasoning_content).toBe('some reasoning content');
});
});
describe('convertOpenAIResponseInputs', () => {
it('应该正确转换普通文本消息', async () => {
const messages: OpenAI.ChatCompletionMessageParam[] = [
const messages: OpenAIChatMessage[] = [
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi there!' },
];
@@ -165,7 +228,7 @@ describe('convertOpenAIResponseInputs', () => {
});
it('应该正确转换带有工具调用的消息', async () => {
const messages: OpenAI.ChatCompletionMessageParam[] = [
const messages: OpenAIChatMessage[] = [
{
role: 'assistant',
content: '',
@@ -195,7 +258,7 @@ describe('convertOpenAIResponseInputs', () => {
});
it('应该正确转换工具响应消息', async () => {
const messages: OpenAI.ChatCompletionMessageParam[] = [
const messages: OpenAIChatMessage[] = [
{
role: 'tool',
content: 'Function result',
@@ -215,7 +278,7 @@ describe('convertOpenAIResponseInputs', () => {
});
it('应该正确转换包含图片的消息', async () => {
const messages: OpenAI.ChatCompletionMessageParam[] = [
const messages: OpenAIChatMessage[] = [
{
role: 'user',
content: [
@@ -247,7 +310,7 @@ describe('convertOpenAIResponseInputs', () => {
});
it('应该正确处理混合类型的消息序列', async () => {
const messages: OpenAI.ChatCompletionMessageParam[] = [
const messages: OpenAIChatMessage[] = [
{ role: 'user', content: 'I need help with a function' },
{
role: 'assistant',
@@ -287,6 +350,29 @@ describe('convertOpenAIResponseInputs', () => {
},
]);
});
it('should extract reasoning.content into a separate reasoning item', async () => {
const messages: OpenAIChatMessage[] = [
{ content: 'system prompts', role: 'system' },
{ content: '你好', role: 'user' },
{
content: 'hello',
role: 'assistant',
reasoning: { content: 'reasoning content', duration: 2706 },
},
{ content: '杭州天气如何', role: 'user' },
];
const result = await convertOpenAIResponseInputs(messages);
expect(result).toEqual([
{ content: 'system prompts', role: 'developer' },
{ content: '你好', role: 'user' },
{ summary: [{ text: 'reasoning content', type: 'summary_text' }], type: 'reasoning' },
{ content: 'hello', role: 'assistant' },
{ content: '杭州天气如何', role: 'user' },
]);
});
});
describe('convertImageUrlToFile', () => {
@@ -1,8 +1,8 @@
import { imageUrlToBase64 } from '@lobechat/utils';
import OpenAI, { toFile } from 'openai';
import { disableStreamModels, systemToUserModels } from '../../const/models';
import { ChatStreamPayload, OpenAIChatMessage } from '../../types';
import { imageUrlToBase64 } from '../../utils/imageToBase64';
import { parseDataUri } from '../../utils/uriParser';
export const convertMessageContent = async (
@@ -26,26 +26,49 @@ export const convertMessageContent = async (
export const convertOpenAIMessages = async (messages: OpenAI.ChatCompletionMessageParam[]) => {
return (await Promise.all(
messages.map(async (message) => ({
...message,
content:
typeof message.content === 'string'
? message.content
: await Promise.all(
(message.content || []).map((c) =>
convertMessageContent(c as OpenAI.ChatCompletionContentPart),
messages.map(async (message) => {
const msg = message as any;
// Explicitly map only valid ChatCompletionMessageParam fields
// Exclude reasoning and reasoning_content fields as they should not be sent in requests
const result: any = {
content:
typeof message.content === 'string'
? message.content
: await Promise.all(
(message.content || []).map((c) =>
convertMessageContent(c as OpenAI.ChatCompletionContentPart),
),
),
),
})),
role: msg.role,
};
// Add optional fields if they exist
if (msg.name !== undefined) result.name = msg.name;
if (msg.tool_calls !== undefined) result.tool_calls = msg.tool_calls;
if (msg.tool_call_id !== undefined) result.tool_call_id = msg.tool_call_id;
if (msg.function_call !== undefined) result.function_call = msg.function_call;
// it's compatible for DeepSeek
if (msg.reasoning_content !== undefined) result.reasoning_content = msg.reasoning_content;
return result;
}),
)) as OpenAI.ChatCompletionMessageParam[];
};
export const convertOpenAIResponseInputs = async (
messages: OpenAI.ChatCompletionMessageParam[],
) => {
export const convertOpenAIResponseInputs = async (messages: OpenAIChatMessage[]) => {
let input: OpenAI.Responses.ResponseInputItem[] = [];
await Promise.all(
messages.map(async (message) => {
// if message has reasoning, add it as a separate reasoning item
if (message.reasoning?.content) {
input.push({
summary: [{ text: message.reasoning.content, type: 'summary_text' }],
type: 'reasoning',
} as OpenAI.Responses.ResponseReasoningItem);
}
// if message is assistant messages with tool calls , transform it to function type item
if (message.role === 'assistant' && message.tool_calls && message.tool_calls?.length > 0) {
message.tool_calls?.forEach((tool) => {
@@ -70,6 +93,11 @@ export const convertOpenAIResponseInputs = async (
return;
}
if (message.role === 'system') {
input.push({ ...message, role: 'developer' } as OpenAI.Responses.ResponseInputItem);
return;
}
// default item
// also need handle image
const item = {
@@ -92,6 +120,9 @@ export const convertOpenAIResponseInputs = async (
),
} as OpenAI.Responses.ResponseInputItem;
// remove reasoning field from the message item
delete (item as any).reasoning;
input.push(item);
}),
);
@@ -1,9 +1,9 @@
// @vitest-environment node
import * as imageToBase64Module from '@lobechat/utils';
import OpenAI from 'openai';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CreateImagePayload } from '../../types/image';
import * as imageToBase64Module from '../../utils/imageToBase64';
import * as uriParserModule from '../../utils/uriParser';
import { createOpenAICompatibleImage } from './createImage';
@@ -1,3 +1,4 @@
import { imageUrlToBase64 } from '@lobechat/utils';
import { cleanObject } from '@lobechat/utils/object';
import createDebug from 'debug';
import { RuntimeImageGenParamsValue } from 'model-bank';
@@ -5,7 +6,6 @@ import OpenAI from 'openai';
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
import { getModelPricing } from '../../utils/getModelPricing';
import { imageUrlToBase64 } from '../../utils/imageToBase64';
import { parseDataUri } from '../../utils/uriParser';
import { convertImageUrlToFile } from '../contextBuilders/openai';
import { convertOpenAIImageUsage } from '../usageConverters/openai';
@@ -1,15 +1,12 @@
// @vitest-environment node
import {
AgentRuntimeErrorType,
ChatStreamCallbacks,
ChatStreamPayload,
LobeOpenAICompatibleRuntime,
} from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import OpenAI from 'openai';
import type { Stream } from 'openai/streaming';
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { ChatStreamCallbacks, ChatStreamPayload } from '../../types/chat';
import { AgentRuntimeErrorType } from '../../types/error';
import * as debugStreamModule from '../../utils/debugStream';
import * as openaiHelpers from '../contextBuilders/openai';
import { createOpenAICompatibleRuntime } from './index';
@@ -1,6 +1,6 @@
import { AgentRuntimeErrorType } from '@lobechat/model-runtime';
import { describe, expect, it, vi } from 'vitest';
import { AgentRuntimeErrorType } from '../../../types/error';
import { FIRST_CHUNK_ERROR_KEY } from '../protocol';
import { createReadableStream, readStreamChunk } from '../utils';
import { OpenAIResponsesStream } from './responsesStream';
@@ -1,6 +1,7 @@
import { ChatMethodOptions } from '@lobechat/model-runtime';
import debug from 'debug';
import { ChatMethodOptions } from '../types/chat';
const log = debug('model-runtime:helpers:mergeChatMethodOptions');
export const mergeMultipleChatMethodOptions = (options: ChatMethodOptions[]): ChatMethodOptions => {
@@ -1,4 +1,4 @@
// @vitest-environment edge-runtime
// @vitest-environment node
import { ModelProvider } from 'model-bank';
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -1,4 +1,4 @@
// @vitest-environment edge-runtime
// @vitest-environment node
import { describe, expect, it, vi } from 'vitest';
import { createAnthropicGenerateObject } from './generateObject';
@@ -1,8 +1,8 @@
// @vitest-environment node
import { ChatCompletionTool, ChatStreamPayload } from '@lobechat/model-runtime';
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as anthropicHelpers from '../../core/contextBuilders/anthropic';
import { ChatCompletionTool, ChatStreamPayload } from '../../types/chat';
import * as debugStreamModule from '../../utils/debugStream';
import { LobeAnthropicAI } from './index';
@@ -1,8 +1,8 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobeBaichuanAI, params } from './index';
@@ -3,10 +3,10 @@ import {
InvokeModelCommand,
InvokeModelWithResponseStreamCommand,
} from '@aws-sdk/client-bedrock-runtime';
import { AgentRuntimeErrorType } from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AgentRuntimeErrorType } from '../../types/error';
import * as debugStreamModule from '../../utils/debugStream';
import { LobeBedrockAI, experimental_buildLlama2Prompt } from './index';
@@ -6,7 +6,7 @@ import { createBflImage } from './createImage';
import { BflStatusResponse } from './types';
// Mock external dependencies
vi.mock('../../utils/imageToBase64', () => ({
vi.mock('@lobechat/utils', () => ({
imageUrlToBase64: vi.fn(),
}));
@@ -187,7 +187,7 @@ describe('createBflImage', () => {
it('should convert single imageUrl to image_prompt base64', async () => {
// Arrange
const { parseDataUri } = await import('../../utils/uriParser');
const { imageUrlToBase64 } = await import('../../utils/imageToBase64');
const { imageUrlToBase64 } = await import('@lobechat/utils');
const { asyncifyPolling } = await import('../../utils/asyncifyPolling');
const mockParseDataUri = vi.mocked(parseDataUri);
@@ -290,7 +290,7 @@ describe('createBflImage', () => {
it('should convert multiple imageUrls for Kontext models', async () => {
// Arrange
const { parseDataUri } = await import('../../utils/uriParser');
const { imageUrlToBase64 } = await import('../../utils/imageToBase64');
const { imageUrlToBase64 } = await import('@lobechat/utils');
const { asyncifyPolling } = await import('../../utils/asyncifyPolling');
const mockParseDataUri = vi.mocked(parseDataUri);
@@ -350,7 +350,7 @@ describe('createBflImage', () => {
it('should limit imageUrls to maximum 4 images', async () => {
// Arrange
const { parseDataUri } = await import('../../utils/uriParser');
const { imageUrlToBase64 } = await import('../../utils/imageToBase64');
const { imageUrlToBase64 } = await import('@lobechat/utils');
const { asyncifyPolling } = await import('../../utils/asyncifyPolling');
const mockParseDataUri = vi.mocked(parseDataUri);
@@ -1,3 +1,4 @@
import { imageUrlToBase64 } from '@lobechat/utils';
import createDebug from 'debug';
import { RuntimeImageGenParamsValue } from 'model-bank';
@@ -5,7 +6,6 @@ import { AgentRuntimeErrorType } from '../../types/error';
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
import { type TaskResult, asyncifyPolling } from '../../utils/asyncifyPolling';
import { AgentRuntimeError } from '../../utils/createError';
import { imageUrlToBase64 } from '../../utils/imageToBase64';
import { parseDataUri } from '../../utils/uriParser';
import {
BFL_ENDPOINTS,
@@ -1,7 +1,7 @@
// @vitest-environment node
import { ChatCompletionTool } from '@lobechat/model-runtime';
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ChatCompletionTool } from '../../types/chat';
import * as debugStreamModule from '../../utils/debugStream';
import { LobeCloudflareAI } from './index';
@@ -1,8 +1,8 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobeCohereAI, params } from './index';
@@ -20,6 +20,92 @@ testProvider({
});
describe('LobeDeepSeekAI - custom features', () => {
describe('chatCompletion.handlePayload', () => {
it('should transform reasoning object to reasoning_content string', () => {
const payload = {
messages: [
{ role: 'user', content: 'Hello' },
{
role: 'assistant',
content: 'Hi there',
reasoning: { content: 'Let me think...', duration: 1000 },
},
{ role: 'user', content: 'How are you?' },
],
model: 'deepseek-r1',
};
const result = params.chatCompletion!.handlePayload!(payload as any);
expect(result.messages).toEqual([
{ role: 'user', content: 'Hello' },
{
role: 'assistant',
content: 'Hi there',
reasoning_content: 'Let me think...',
},
{ role: 'user', content: 'How are you?' },
]);
});
it('should not modify messages without reasoning field', () => {
const payload = {
messages: [
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi there' },
],
model: 'deepseek-chat',
};
const result = params.chatCompletion!.handlePayload!(payload as any);
expect(result.messages).toEqual(payload.messages);
});
it('should handle empty reasoning content', () => {
const payload = {
messages: [
{
role: 'assistant',
content: 'Response',
reasoning: { duration: 1000 },
},
],
model: 'deepseek-r1',
};
const result = params.chatCompletion!.handlePayload!(payload as any);
expect(result.messages[0]).toEqual({
role: 'assistant',
content: 'Response',
});
});
it('should set stream to true by default', () => {
const payload = {
messages: [{ role: 'user', content: 'Hello' }],
model: 'deepseek-chat',
};
const result = params.chatCompletion!.handlePayload!(payload as any);
expect(result.stream).toBe(true);
});
it('should preserve existing stream value', () => {
const payload = {
messages: [{ role: 'user', content: 'Hello' }],
model: 'deepseek-chat',
stream: false,
};
const result = params.chatCompletion!.handlePayload!(payload as any);
expect(result.stream).toBe(false);
});
});
describe('Debug Configuration', () => {
it('should disable debug by default', () => {
delete process.env.DEBUG_DEEPSEEK_CHAT_COMPLETION;
@@ -12,6 +12,30 @@ export interface DeepSeekModelCard {
export const params = {
baseURL: 'https://api.deepseek.com/v1',
chatCompletion: {
handlePayload: (payload) => {
// Transform reasoning object to reasoning_content string for multi-turn conversations
const messages = payload.messages.map((message: any) => {
// Only transform if message has reasoning.content
if (message.reasoning?.content) {
const { reasoning, ...rest } = message;
return {
...rest,
reasoning_content: reasoning.content,
};
}
// If message has reasoning but no content, remove reasoning field entirely
delete message.reasoning;
return message;
});
return {
...payload,
messages,
stream: payload.stream ?? true,
} as any;
},
},
debug: {
chatCompletion: () => process.env.DEBUG_DEEPSEEK_CHAT_COMPLETION === '1',
},
@@ -1,9 +1,9 @@
// @vitest-environment edge-runtime
// @vitest-environment node
import { GoogleGenAI } from '@google/genai';
import * as imageToBase64Module from '@lobechat/utils';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CreateImagePayload } from '../../types/image';
import * as imageToBase64Module from '../../utils/imageToBase64';
import { createGoogleImage } from './createImage';
const provider = 'google';
@@ -1,11 +1,11 @@
import { Content, GenerateContentConfig, GoogleGenAI, Part } from '@google/genai';
import { imageUrlToBase64 } from '@lobechat/utils';
import { convertGoogleAIUsage } from '../../core/usageConverters/google-ai';
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
import { AgentRuntimeError } from '../../utils/createError';
import { getModelPricing } from '../../utils/getModelPricing';
import { parseGoogleErrorMessage } from '../../utils/googleErrorParser';
import { imageUrlToBase64 } from '../../utils/imageToBase64';
import { parseDataUri } from '../../utils/uriParser';
// Maximum number of images allowed for processing
@@ -1,4 +1,4 @@
// @vitest-environment edge-runtime
// @vitest-environment node
import { Type as SchemaType } from '@google/genai';
import { describe, expect, it, vi } from 'vitest';
@@ -1,14 +1,11 @@
// @vitest-environment edge-runtime
// @vitest-environment node
import { GenerateContentResponse, Tool } from '@google/genai';
import { OpenAIChatMessage } from '@lobechat/model-runtime';
import { ChatStreamPayload } from '@lobechat/types';
import OpenAI from 'openai';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LOBE_ERROR_KEY } from '../../core/streams';
import { AgentRuntimeErrorType } from '../../types/error';
import * as debugStreamModule from '../../utils/debugStream';
import * as imageToBase64Module from '../../utils/imageToBase64';
import { LobeGoogleAI, resolveModelThinkingBudget } from './index';
const provider = 'google';
@@ -1,7 +1,7 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { AgentRuntimeErrorType } from '../../types/error';
import { LobeGroq, params } from './index';
@@ -1,8 +1,8 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobeHunyuanAI, params } from './index';
@@ -1,4 +1,4 @@
// @vitest-environment edge-runtime
// @vitest-environment node
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { CreateImageOptions } from '../../core/openaiCompatibleFactory';
@@ -1,7 +1,7 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobeMistralAI, params } from './index';
@@ -1,7 +1,7 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobeMoonshotAI, params } from './index';
@@ -1,8 +1,8 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import models from './fixtures/models.json';
import { LobeNovitaAI } from './index';
@@ -1,4 +1,5 @@
// @vitest-environment node
import { imageUrlToBase64 } from '@lobechat/utils';
import { ModelProvider } from 'model-bank';
import { Ollama } from 'ollama/browser';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -9,6 +10,13 @@ import * as debugStreamModule from '../../utils/debugStream';
import { LobeOllamaAI, params } from './index';
vi.mock('ollama/browser');
vi.mock('@lobechat/utils', async () => {
const actual = await vi.importActual('@lobechat/utils');
return {
...actual,
imageUrlToBase64: vi.fn(),
};
});
// Mock the console.error to avoid polluting test output
vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -462,13 +470,13 @@ describe('LobeOllamaAI', () => {
});
describe('buildOllamaMessages', () => {
it('should convert OpenAIChatMessage array to OllamaMessage array', () => {
it('should convert OpenAIChatMessage array to OllamaMessage array', async () => {
const messages = [
{ content: 'Hello', role: 'user' },
{ content: 'Hi there!', role: 'assistant' },
];
const ollamaMessages = ollamaAI['buildOllamaMessages'](messages as any);
const ollamaMessages = await ollamaAI['buildOllamaMessages'](messages as any);
expect(ollamaMessages).toEqual([
{ content: 'Hello', role: 'user' },
@@ -476,15 +484,15 @@ describe('LobeOllamaAI', () => {
]);
});
it('should handle empty message array', () => {
it('should handle empty message array', async () => {
const messages: any[] = [];
const ollamaMessages = ollamaAI['buildOllamaMessages'](messages);
const ollamaMessages = await ollamaAI['buildOllamaMessages'](messages);
expect(ollamaMessages).toEqual([]);
});
it('should handle multiple messages with different roles', () => {
it('should handle multiple messages with different roles', async () => {
const messages = [
{ content: 'Hello', role: 'system' },
{ content: 'Hi', role: 'user' },
@@ -492,7 +500,7 @@ describe('LobeOllamaAI', () => {
{ content: 'How are you?', role: 'user' },
];
const ollamaMessages = ollamaAI['buildOllamaMessages'](messages as any);
const ollamaMessages = await ollamaAI['buildOllamaMessages'](messages as any);
expect(ollamaMessages).toHaveLength(4);
expect(ollamaMessages[0].role).toBe('system');
@@ -503,26 +511,26 @@ describe('LobeOllamaAI', () => {
});
describe('convertContentToOllamaMessage', () => {
it('should convert string content to OllamaMessage', () => {
it('should convert string content to OllamaMessage', async () => {
const message = { content: 'Hello', role: 'user' };
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({ content: 'Hello', role: 'user' });
});
it('should convert text content to OllamaMessage', () => {
it('should convert text content to OllamaMessage', async () => {
const message = {
content: [{ type: 'text', text: 'Hello' }],
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({ content: 'Hello', role: 'user' });
});
it('should convert image_url content to OllamaMessage with images', () => {
it('should convert image_url content to OllamaMessage with images', async () => {
const message = {
content: [
{
@@ -533,7 +541,7 @@ describe('LobeOllamaAI', () => {
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: '',
@@ -542,7 +550,7 @@ describe('LobeOllamaAI', () => {
});
});
it('should ignore invalid image_url content', () => {
it('should ignore invalid image_url content', async () => {
const message = {
content: [
{
@@ -553,7 +561,7 @@ describe('LobeOllamaAI', () => {
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: '',
@@ -561,7 +569,7 @@ describe('LobeOllamaAI', () => {
});
});
it('should handle mixed text and image content', () => {
it('should handle mixed text and image content', async () => {
const message = {
content: [
{ type: 'text', text: 'First text' },
@@ -578,7 +586,7 @@ describe('LobeOllamaAI', () => {
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: 'Second text', // Should keep latest text
@@ -587,13 +595,13 @@ describe('LobeOllamaAI', () => {
});
});
it('should handle content with empty text', () => {
it('should handle content with empty text', async () => {
const message = {
content: [{ type: 'text', text: '' }],
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: '',
@@ -601,7 +609,7 @@ describe('LobeOllamaAI', () => {
});
});
it('should handle content with only images (no text)', () => {
it('should handle content with only images (no text)', async () => {
const message = {
content: [
{
@@ -612,7 +620,7 @@ describe('LobeOllamaAI', () => {
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: '',
@@ -621,7 +629,7 @@ describe('LobeOllamaAI', () => {
});
});
it('should handle multiple images without text', () => {
it('should handle multiple images without text', async () => {
const message = {
content: [
{
@@ -640,7 +648,7 @@ describe('LobeOllamaAI', () => {
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: '',
@@ -649,7 +657,10 @@ describe('LobeOllamaAI', () => {
});
});
it('should ignore images with invalid data URIs', () => {
it('should ignore images with invalid data URIs', async () => {
// Mock imageUrlToBase64 to simulate conversion failure for external URLs
vi.mocked(imageUrlToBase64).mockRejectedValue(new Error('Network error'));
const message = {
content: [
{ type: 'text', text: 'Hello' },
@@ -665,7 +676,7 @@ describe('LobeOllamaAI', () => {
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: 'Hello',
@@ -674,7 +685,7 @@ describe('LobeOllamaAI', () => {
});
});
it('should handle complex interleaved content', () => {
it('should handle complex interleaved content', async () => {
const message = {
content: [
{ type: 'text', text: 'Text 1' },
@@ -692,7 +703,7 @@ describe('LobeOllamaAI', () => {
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: 'Text 3', // Should keep latest text
@@ -701,7 +712,7 @@ describe('LobeOllamaAI', () => {
});
});
it('should handle assistant role with images', () => {
it('should handle assistant role with images', async () => {
const message = {
content: [
{ type: 'text', text: 'Here is the image' },
@@ -713,7 +724,7 @@ describe('LobeOllamaAI', () => {
role: 'assistant',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: 'Here is the image',
@@ -722,13 +733,13 @@ describe('LobeOllamaAI', () => {
});
});
it('should handle system role with text', () => {
it('should handle system role with text', async () => {
const message = {
content: [{ type: 'text', text: 'You are a helpful assistant' }],
role: 'system',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: 'You are a helpful assistant',
@@ -736,13 +747,13 @@ describe('LobeOllamaAI', () => {
});
});
it('should handle empty content array', () => {
it('should handle empty content array', async () => {
const message = {
content: [],
role: 'user',
};
const ollamaMessage = ollamaAI['convertContentToOllamaMessage'](message as any);
const ollamaMessage = await ollamaAI['convertContentToOllamaMessage'](message as any);
expect(ollamaMessage).toEqual({
content: '',
@@ -1,4 +1,5 @@
import type { ChatModelCard } from '@lobechat/types';
import { imageUrlToBase64 } from '@lobechat/utils';
import { ModelProvider } from 'model-bank';
import { Ollama, Tool } from 'ollama/browser';
import { ClientOptions } from 'openai';
@@ -61,7 +62,7 @@ export class LobeOllamaAI implements LobeRuntimeAI {
options?.signal?.addEventListener('abort', abort);
const response = await this.client.chat({
messages: this.buildOllamaMessages(payload.messages),
messages: await this.buildOllamaMessages(payload.messages),
model: payload.model,
options: {
frequency_penalty: payload.frequency_penalty,
@@ -169,11 +170,13 @@ export class LobeOllamaAI implements LobeRuntimeAI {
}
};
private buildOllamaMessages(messages: OpenAIChatMessage[]) {
return messages.map((message) => this.convertContentToOllamaMessage(message));
private async buildOllamaMessages(messages: OpenAIChatMessage[]) {
return Promise.all(messages.map((message) => this.convertContentToOllamaMessage(message)));
}
private convertContentToOllamaMessage = (message: OpenAIChatMessage): OllamaMessage => {
private convertContentToOllamaMessage = async (
message: OpenAIChatMessage,
): Promise<OllamaMessage> => {
if (typeof message.content === 'string') {
return { content: message.content, role: message.role };
}
@@ -183,6 +186,9 @@ export class LobeOllamaAI implements LobeRuntimeAI {
role: message.role,
};
// Collect image processing tasks for parallel execution
const imagePromises: Array<Promise<string | null> | string> = [];
for (const content of message.content) {
switch (content.type) {
case 'text': {
@@ -191,16 +197,34 @@ export class LobeOllamaAI implements LobeRuntimeAI {
break;
}
case 'image_url': {
const { base64 } = parseDataUri(content.image_url.url);
const { base64, type } = parseDataUri(content.image_url.url);
// If already base64 format, use it directly
if (base64) {
ollamaMessage.images ??= [];
ollamaMessage.images.push(base64);
imagePromises.push(base64);
}
// If it's a URL, add async conversion task with error handling
else if (type === 'url') {
imagePromises.push(
imageUrlToBase64(content.image_url.url)
.then((result) => result.base64)
.catch(() => null), // Silently ignore failed conversions
);
}
break;
}
}
}
// Process all images in parallel and filter out failed conversions
if (imagePromises.length > 0) {
const results = await Promise.all(imagePromises);
const validImages = results.filter((img): img is string => img !== null);
if (validImages.length > 0) {
ollamaMessage.images = validImages;
}
}
return ollamaMessage;
};
@@ -1,7 +1,7 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobeOpenRouterAI, params } from './index';
@@ -1,8 +1,8 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobePerplexityAI } from './index';
@@ -1,8 +1,8 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import models from './fixtures/models.json';
import { LobePPIOAI } from './index';
@@ -1,4 +1,4 @@
// @vitest-environment edge-runtime
// @vitest-environment node
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { CreateImageOptions } from '../../core/openaiCompatibleFactory';
@@ -1,7 +1,7 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobeSearch1API, params } from './index';
@@ -1,3 +1,4 @@
import { imageUrlToBase64 } from '@lobechat/utils';
import createDebug from 'debug';
import { RuntimeImageGenParamsValue } from 'model-bank';
@@ -5,7 +6,6 @@ import { CreateImageOptions } from '../../core/openaiCompatibleFactory';
import { CreateImagePayload, CreateImageResponse } from '../../types';
import { AgentRuntimeErrorType } from '../../types/error';
import { AgentRuntimeError } from '../../utils/createError';
import { imageUrlToBase64 } from '../../utils/imageToBase64';
import { parseDataUri } from '../../utils/uriParser';
const log = createDebug('lobe-image:siliconcloud');
@@ -1,9 +1,9 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import OpenAI from 'openai';
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import * as debugStreamModule from '../../utils/debugStream';
import { LobeTaichuAI } from './index';
@@ -1,8 +1,8 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { ModelProvider } from 'model-bank';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobeWenxinAI, params } from './index';
@@ -1,7 +1,7 @@
// @vitest-environment node
import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeOpenAICompatibleRuntime } from '../../core/BaseAI';
import { testProvider } from '../../providerTestUtils';
import { LobeZhipuAI, params } from './index';
+4
View File
@@ -47,6 +47,10 @@ export type UserMessageContentPart =
export interface OpenAIChatMessage {
content: string | UserMessageContentPart[];
name?: string;
reasoning?: {
content?: string;
duration?: number;
};
role: LLMRoleType;
tool_call_id?: string;
tool_calls?: MessageToolCall[];
@@ -1,7 +1,7 @@
import { AgentRuntimeErrorType } from '@lobechat/model-runtime';
import { ChatErrorType } from '@lobechat/types';
import { describe, expect, it, vi } from 'vitest';
import { AgentRuntimeErrorType } from '../types/error';
import { createErrorResponse } from './errorResponse';
describe('createErrorResponse', () => {
@@ -1,91 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { imageToBase64, imageUrlToBase64 } from './imageToBase64';
describe('imageToBase64', () => {
let mockImage: HTMLImageElement;
let mockCanvas: HTMLCanvasElement;
let mockContext: CanvasRenderingContext2D;
beforeEach(() => {
mockImage = {
width: 200,
height: 100,
} as HTMLImageElement;
mockContext = {
drawImage: vi.fn(),
} as unknown as CanvasRenderingContext2D;
mockCanvas = {
width: 0,
height: 0,
getContext: vi.fn().mockReturnValue(mockContext),
toDataURL: vi.fn().mockReturnValue('data:image/webp;base64,mockBase64Data'),
} as unknown as HTMLCanvasElement;
vi.spyOn(document, 'createElement').mockReturnValue(mockCanvas);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should convert image to base64 with correct size and type', () => {
const result = imageToBase64({ img: mockImage, size: 100, type: 'image/jpeg' });
expect(document.createElement).toHaveBeenCalledWith('canvas');
expect(mockCanvas.width).toBe(100);
expect(mockCanvas.height).toBe(100);
expect(mockCanvas.getContext).toHaveBeenCalledWith('2d');
expect(mockContext.drawImage).toHaveBeenCalledWith(mockImage, 50, 0, 100, 100, 0, 0, 100, 100);
expect(mockCanvas.toDataURL).toHaveBeenCalledWith('image/jpeg');
expect(result).toBe('data:image/webp;base64,mockBase64Data');
});
it('should use default type when not specified', () => {
imageToBase64({ img: mockImage, size: 100 });
expect(mockCanvas.toDataURL).toHaveBeenCalledWith('image/webp');
});
it('should handle taller images correctly', () => {
mockImage.width = 100;
mockImage.height = 200;
imageToBase64({ img: mockImage, size: 100 });
expect(mockContext.drawImage).toHaveBeenCalledWith(mockImage, 0, 50, 100, 100, 0, 0, 100, 100);
});
});
describe('imageUrlToBase64', () => {
const mockFetch = vi.fn();
const mockArrayBuffer = new ArrayBuffer(8);
beforeEach(() => {
global.fetch = mockFetch;
global.btoa = vi.fn().mockReturnValue('mockBase64String');
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should convert image URL to base64 string', async () => {
mockFetch.mockResolvedValue({
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
blob: () => Promise.resolve(new Blob([mockArrayBuffer], { type: 'image/jpg' })),
});
const result = await imageUrlToBase64('https://example.com/image.jpg');
expect(mockFetch).toHaveBeenCalledWith('https://example.com/image.jpg');
expect(global.btoa).toHaveBeenCalled();
expect(result).toEqual({ base64: 'mockBase64String', mimeType: 'image/jpg' });
});
it('should throw an error when fetch fails', async () => {
const mockError = new Error('Fetch failed');
mockFetch.mockRejectedValue(mockError);
await expect(imageUrlToBase64('https://example.com/image.jpg')).rejects.toThrow('Fetch failed');
});
});
@@ -1,62 +0,0 @@
export const imageToBase64 = ({
size,
img,
type = 'image/webp',
}: {
img: HTMLImageElement;
size: number;
type?: string;
}) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
let startX = 0;
let startY = 0;
if (img.width > img.height) {
startX = (img.width - img.height) / 2;
} else {
startY = (img.height - img.width) / 2;
}
canvas.width = size;
canvas.height = size;
ctx.drawImage(
img,
startX,
startY,
Math.min(img.width, img.height),
Math.min(img.width, img.height),
0,
0,
size,
size,
);
return canvas.toDataURL(type);
};
export const imageUrlToBase64 = async (
imageUrl: string,
): Promise<{ base64: string; mimeType: string }> => {
try {
const res = await fetch(imageUrl);
const blob = await res.blob();
const arrayBuffer = await blob.arrayBuffer();
const base64 =
typeof btoa === 'function'
? btoa(
new Uint8Array(arrayBuffer).reduce(
(data, byte) => data + String.fromCharCode(byte),
'',
),
)
: Buffer.from(arrayBuffer).toString('base64');
return { base64, mimeType: blob.type };
} catch (error) {
console.error('Error converting image to base64:', error);
throw error;
}
};
+14
View File
@@ -0,0 +1,14 @@
/**
* Browser version of SSRF-safe fetch
* In browser environments, we simply use the native fetch API
* as SSRF attacks are not applicable in client-side code
*/
/**
* Browser-safe fetch implementation
* Uses native fetch API in browser environments
*/
// eslint-disable-next-line no-undef
export const ssrfSafeFetch = async (url: string, options?: RequestInit): Promise<Response> => {
return fetch(url, options);
};
+8 -1
View File
@@ -2,7 +2,14 @@
"name": "ssrf-safe-fetch",
"version": "1.0.0",
"private": true,
"description": "",
"description": "SSRF-safe fetch implementation with browser/node conditional exports",
"exports": {
".": {
"browser": "./index.browser.ts",
"node": "./index.ts",
"default": "./index.ts"
}
},
"main": "index.ts",
"scripts": {
"test": "vitest run"
+17 -10
View File
@@ -36,23 +36,30 @@ export const imageToBase64 = ({
return canvas.toDataURL(type);
};
/**
* Convert image URL to base64
* Uses SSRF-safe fetch on server-side to prevent SSRF attacks
*/
export const imageUrlToBase64 = async (
imageUrl: string,
): Promise<{ base64: string; mimeType: string }> => {
try {
const res = await fetch(imageUrl);
const isServer = typeof window === 'undefined';
// Use SSRF-safe fetch on server-side to prevent SSRF attacks
const res = isServer
? await import('ssrf-safe-fetch').then((m) => m.ssrfSafeFetch(imageUrl))
: await fetch(imageUrl);
const blob = await res.blob();
const arrayBuffer = await blob.arrayBuffer();
const base64 =
typeof btoa === 'function'
? btoa(
new Uint8Array(arrayBuffer).reduce(
(data, byte) => data + String.fromCharCode(byte),
'',
),
)
: Buffer.from(arrayBuffer).toString('base64');
// Client-side uses btoa, server-side uses Buffer
const base64 = isServer
? Buffer.from(arrayBuffer).toString('base64')
: btoa(
new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), ''),
);
return { base64, mimeType: blob.type };
} catch (error) {
+1 -1
View File
@@ -4,9 +4,9 @@ export * from './detectChinese';
export * from './format';
export * from './imageToBase64';
export * from './keyboard';
export * from './merge';
export * from './number';
export * from './object';
export * from './parseModels';
export * from './pricing';
export * from './safeParseJSON';
export * from './sleep';
-1
View File
@@ -122,7 +122,6 @@ export async function POST(request: NextRequest) {
}
return NextResponse.redirect(finalRedirectUrl, {
headers: request.headers,
status: 303,
});
} catch (error) {
@@ -8,6 +8,8 @@ import { Flexbox } from 'react-layout-kit';
import { GITHUB } from '@/const/url';
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
import PHLaunch from './PHLaunch';
const ICON_SIZE: ActionIconProps['size'] = {
blockSize: 36,
size: 20,
@@ -39,6 +41,7 @@ const BottomActions = memo(() => {
tooltipProps={{ placement: 'right' }}
/>
</Link>
<PHLaunch />
</Flexbox>
);
});
@@ -0,0 +1,148 @@
'use client';
import { ActionIcon, ActionIconProps } from '@lobehub/ui';
import { Button, Popover } from 'antd';
import { createStyles } from 'antd-style';
import { Rocket, X } from 'lucide-react';
import { memo, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { useGlobalStore } from '@/store/global';
const PH_LAUNCH_URL = 'https://lobe.li/ph';
const PH_IMAGE_URL = 'https://hub-apac-1.lobeobjects.space/og/lobehub-ph.png';
// Configure the date range for showing the PH launch card
const PH_START_DATE = new Date('2026-01-27T08:00:00Z');
const PH_END_DATE = new Date('2026-02-01T00:00:00Z');
const useStyles = createStyles(({ css, token }) => ({
action: css`
margin-top: 12px;
`,
body: css`
font-size: 14px;
color: ${token.colorTextSecondary};
`,
card: css`
overflow: hidden;
position: relative;
width: 280px;
border-radius: 8px;
`,
closeButton: css`
position: absolute;
top: 8px;
right: 8px;
`,
content: css`
padding: 12px;
`,
image: css`
overflow: hidden;
width: 100%;
height: auto;
border-bottom: 1px solid ${token.colorBorderSecondary};
`,
title: css`
font-size: 16px;
font-weight: 600;
color: ${token.colorText};
`,
}));
const ICON_SIZE: ActionIconProps['size'] = {
blockSize: 36,
size: 20,
strokeWidth: 1.5,
};
const PHLaunch = memo(() => {
const { t } = useTranslation('common');
const { styles } = useStyles();
const [open, setOpen] = useState(false);
const [hidePHLaunch, updateSystemStatus] = useGlobalStore((s) => [
s.status.hidePHLaunch,
s.updateSystemStatus,
]);
const isWithinDateRange = useMemo(() => {
const now = new Date();
return now >= PH_START_DATE && now <= PH_END_DATE;
}, []);
// Auto open the popover if user hasn't seen it yet
useEffect(() => {
if (!hidePHLaunch && isWithinDateRange) {
// Small delay to ensure the component is mounted
const timer = setTimeout(() => {
setOpen(true);
}, 1000);
return () => clearTimeout(timer);
}
}, [hidePHLaunch, isWithinDateRange]);
const handleClose = () => {
setOpen(false);
updateSystemStatus({ hidePHLaunch: true });
};
const handleAction = () => {
window.open(PH_LAUNCH_URL, '_blank');
handleClose();
};
// Don't render if outside the date range
if (!isWithinDateRange) return null;
const content = (
<Flexbox className={styles.card}>
<ActionIcon
className={styles.closeButton}
icon={X}
onClick={handleClose}
size={{ blockSize: 24, size: 14 }}
/>
<div className={styles.image}>
<img
alt="LobeChat Product Hunt Launch"
height="100%"
src={PH_IMAGE_URL}
style={{ objectFit: 'cover' }}
width="100%"
/>
</div>
<Flexbox className={styles.content} gap={4}>
<div className={styles.title}>{t('phLaunch.title')}</div>
<div className={styles.body}>{t('phLaunch.body')}</div>
<Button block className={styles.action} onClick={handleAction} size="small" type="primary">
{t('phLaunch.action')}
</Button>
</Flexbox>
</Flexbox>
);
return (
<Popover
arrow={false}
content={content}
onOpenChange={setOpen}
open={open}
placement="rightBottom"
styles={{ body: { padding: 0 } }}
trigger="click"
>
<ActionIcon
icon={Rocket}
onClick={() => setOpen(!open)}
size={ICON_SIZE}
title="Product Hunt"
tooltipProps={{ placement: 'right' }}
/>
</Popover>
);
});
export default PHLaunch;
@@ -1,3 +1,4 @@
import { getMessageError } from '@lobechat/fetch-sse';
import { ChatMessageError } from '@lobechat/types';
import { AudioPlayer } from '@lobehub/tts/react';
import { Alert, Button, Highlighter, Select, SelectProps } from '@lobehub/ui';
@@ -9,7 +10,6 @@ import { Flexbox } from 'react-layout-kit';
import { useTTS } from '@/hooks/useTTS';
import { TTSServer } from '@/types/agent';
import { getMessageError } from '@/utils/fetch';
interface SelectWithTTSPreviewProps extends SelectProps {
server: TTSServer;
+1 -1
View File
@@ -1,3 +1,4 @@
import { MessageTextChunk } from '@lobechat/fetch-sse';
import {
chainPickEmoji,
chainSummaryAgentName,
@@ -16,7 +17,6 @@ import { systemAgentSelectors } from '@/store/user/slices/settings/selectors';
import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent';
import { MetaData } from '@/types/meta';
import { SystemAgentItem } from '@/types/user/settings';
import { MessageTextChunk } from '@/utils/fetch';
import { merge } from '@/utils/merge';
import { setNamespace } from '@/utils/storeDebug';
@@ -1,3 +1,4 @@
import { getMessageError } from '@lobechat/fetch-sse';
import { ChatMessageError } from '@lobechat/types';
import { SpeechRecognitionOptions, useSpeechRecognition } from '@lobehub/tts/react';
import isEqual from 'fast-deep-equal';
@@ -13,7 +14,6 @@ import { useGlobalStore } from '@/store/global';
import { globalGeneralSelectors } from '@/store/global/selectors';
import { useUserStore } from '@/store/user';
import { settingsSelectors } from '@/store/user/selectors';
import { getMessageError } from '@/utils/fetch';
import CommonSTT from './common';
@@ -1,3 +1,4 @@
import { getMessageError } from '@lobechat/fetch-sse';
import { ChatMessageError } from '@lobechat/types';
import { getRecordMineType } from '@lobehub/tts';
import { OpenAISTTOptions, useOpenAISTT } from '@lobehub/tts/react';
@@ -16,7 +17,6 @@ import { useGlobalStore } from '@/store/global';
import { globalGeneralSelectors } from '@/store/global/selectors';
import { useUserStore } from '@/store/user';
import { settingsSelectors } from '@/store/user/selectors';
import { getMessageError } from '@/utils/fetch';
import CommonSTT from './common';
@@ -1,3 +1,4 @@
import { getMessageError } from '@lobechat/fetch-sse';
import { ChatMessageError, ChatTTS } from '@lobechat/types';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -5,7 +6,6 @@ import { useTranslation } from 'react-i18next';
import { useTTS } from '@/hooks/useTTS';
import { useChatStore } from '@/store/chat';
import { useFileStore } from '@/store/file';
import { getMessageError } from '@/utils/fetch';
import Player from './Player';
@@ -24,7 +24,6 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
"description": "Lists all available tools and methods",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {},
"type": "object",
},
+5
View File
@@ -287,6 +287,11 @@ export default {
},
oauth: 'SSO 登录',
officialSite: '官方网站',
phLaunch: {
action: '前往支持',
body: '我们正在 Product Hunt 上发布!如果你喜欢 LobeChat,请给我们投票支持!',
title: 'LobeChat 正在 Product Hunt 上发布!',
},
ok: '确定',
password: '密码',
pin: '置顶',

Some files were not shown because too many files have changed in this diff Show More