Compare commits

...

96 Commits

Author SHA1 Message Date
semantic-release-bot 9f2eea3d2f 🔖 chore(release): v1.102.2 [skip ci]
### [Version 1.102.2](https://github.com/lobehub/lobe-chat/compare/v1.102.1...v1.102.2)
<sup>Released on **2025-07-22**</sup>

#### 💄 Styles

- **misc**: Add notification for desktop.

<br/>

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

#### Styles

* **misc**: Add notification for desktop, closes [#8523](https://github.com/lobehub/lobe-chat/issues/8523) ([4917d17](https://github.com/lobehub/lobe-chat/commit/4917d17))

</details>

<div align="right">

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

</div>
2025-07-22 03:11:41 +00:00
Arvin Xu 4917d175bb 💄 style: add notification for desktop (#8523)
* add notification for desktop

* update i18n

* fix tests
2025-07-22 10:56:17 +08:00
lobehubbot 5b53773dc6 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-21 16:01:39 +00:00
semantic-release-bot 46bdf21f37 🔖 chore(release): v1.102.1 [skip ci]
### [Version&nbsp;1.102.1](https://github.com/lobehub/lobe-chat/compare/v1.102.0...v1.102.1)
<sup>Released on **2025-07-21**</sup>

#### 🐛 Bug Fixes

- **groq**: Enable streaming for tool calls and add Kimi K2 model.

#### 💄 Styles

- **misc**: Modal list header sticky style.

<br/>

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

#### What's fixed

* **groq**: Enable streaming for tool calls and add Kimi K2 model, closes [#8510](https://github.com/lobehub/lobe-chat/issues/8510) ([60739bc](https://github.com/lobehub/lobe-chat/commit/60739bc))

#### Styles

* **misc**: Modal list header sticky style, closes [#8514](https://github.com/lobehub/lobe-chat/issues/8514) ([75273d5](https://github.com/lobehub/lobe-chat/commit/75273d5))

</details>

<div align="right">

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

</div>
2025-07-21 16:00:42 +00:00
afon 60739bc903 🐛 fix(groq): Enable streaming for tool calls and add Kimi K2 model (#8510) 2025-07-21 23:45:30 +08:00
Innei 75273d5497 💄 style: modal list header sticky style (#8514) 2025-07-21 23:43:25 +08:00
Innei 22e2de2b00 🔨 chore(typo): fix redirectUrl typo (#8513) 2025-07-21 23:42:53 +08:00
lobehubbot cbddf3ed25 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-21 15:28:30 +00:00
semantic-release-bot 14fd4bb2e7 🔖 chore(release): v1.102.0 [skip ci]
## [Version&nbsp;1.102.0](https://github.com/lobehub/lobe-chat/compare/v1.101.2...v1.102.0)
<sup>Released on **2025-07-21**</sup>

####  Features

- **misc**: Add image generation capabilities using Google AI Imagen API.

<br/>

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

#### What's improved

* **misc**: Add image generation capabilities using Google AI Imagen API, closes [#8503](https://github.com/lobehub/lobe-chat/issues/8503) ([cef8208](https://github.com/lobehub/lobe-chat/commit/cef8208))

</details>

<div align="right">

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

</div>
2025-07-21 15:27:38 +00:00
YuTengjing cef8208457 feat: add image generation capabilities using Google AI Imagen API (#8503) 2025-07-21 23:11:31 +08:00
lobehubbot 7d85a772db 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-21 06:00:46 +00:00
semantic-release-bot 83dd2865f0 🔖 chore(release): v1.101.2 [skip ci]
### [Version&nbsp;1.101.2](https://github.com/lobehub/lobe-chat/compare/v1.101.1...v1.101.2)
<sup>Released on **2025-07-21**</sup>

#### 💄 Styles

- **misc**: Fix lobehub provider `/chat` in desktop.

<br/>

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

#### Styles

* **misc**: Fix lobehub provider `/chat` in desktop, closes [#8508](https://github.com/lobehub/lobe-chat/issues/8508) ([c801f9c](https://github.com/lobehub/lobe-chat/commit/c801f9c))

</details>

<div align="right">

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

</div>
2025-07-21 05:59:47 +00:00
Arvin Xu c04507e34f 🔨 chore: fix model runtime test issue (#8511) 2025-07-21 13:44:54 +08:00
Arvin Xu c801f9ce58 💄 style: fix lobehub provider /chat in desktop (#8508) 2025-07-21 11:43:20 +08:00
vual ac2a83a3ce 👷 build: add default APP_URL for docker image to avoid building error (#8507) 2025-07-21 11:39:59 +08:00
lobehubbot 7bdda9bab4 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-19 17:19:58 +00:00
semantic-release-bot e8321355f8 🔖 chore(release): v1.101.1 [skip ci]
### [Version&nbsp;1.101.1](https://github.com/lobehub/lobe-chat/compare/v1.101.0...v1.101.1)
<sup>Released on **2025-07-19**</sup>

#### 🐛 Bug Fixes

- **misc**: Try fix authorization code exchange & pin next-auto to `beta.29`.

<br/>

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

#### What's fixed

* **misc**: Try fix authorization code exchange & pin next-auto to `beta.29`, closes [#8496](https://github.com/lobehub/lobe-chat/issues/8496) ([27c4881](https://github.com/lobehub/lobe-chat/commit/27c4881))

</details>

<div align="right">

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

</div>
2025-07-19 17:19:05 +00:00
Rylan Cai 27c4881205 🐛 fix: Try fix authorization code exchange & pin next-auto to beta.29 (#8496)
* 📌 pin: next-auth@beta.29

* 🐛 fix: infinite redirection
2025-07-20 01:03:35 +08:00
lobehubbot 472c40e969 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-19 07:32:42 +00:00
semantic-release-bot c98e7f8032 🔖 chore(release): v1.101.0 [skip ci]
## [Version&nbsp;1.101.0](https://github.com/lobehub/lobe-chat/compare/v1.100.2...v1.101.0)
<sup>Released on **2025-07-19**</sup>

####  Features

- **misc**: Add zhipu cogview4.

#### 🐛 Bug Fixes

- **misc**: Some ai image bugs.

<br/>

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

#### What's improved

* **misc**: Add zhipu cogview4, closes [#8486](https://github.com/lobehub/lobe-chat/issues/8486) ([0b1557d](https://github.com/lobehub/lobe-chat/commit/0b1557d))

#### What's fixed

* **misc**: Some ai image bugs, closes [#8490](https://github.com/lobehub/lobe-chat/issues/8490) ([5d852be](https://github.com/lobehub/lobe-chat/commit/5d852be))

</details>

<div align="right">

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

</div>
2025-07-19 07:31:47 +00:00
YuTengjing 0b1557d6ad feat: add zhipu cogview4 (#8486) 2025-07-18 23:01:12 +08:00
YuTengjing 5d852be8a2 🐛 fix: some ai image bugs (#8490) 2025-07-18 23:00:56 +08:00
lobehubbot 993b0fa81f 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-18 05:13:35 +00:00
semantic-release-bot 83783b4650 🔖 chore(release): v1.100.2 [skip ci]
### [Version&nbsp;1.100.2](https://github.com/lobehub/lobe-chat/compare/v1.100.1...v1.100.2)
<sup>Released on **2025-07-18**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix webapi proxy with clerk.

<br/>

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

#### What's fixed

* **misc**: Fix webapi proxy with clerk, closes [#8479](https://github.com/lobehub/lobe-chat/issues/8479) ([7dd65f0](https://github.com/lobehub/lobe-chat/commit/7dd65f0))

</details>

<div align="right">

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

</div>
2025-07-18 05:12:40 +00:00
Arvin Xu 7dd65f0cb4 🐛 fix: fix webapi proxy with clerk (#8479)
* fix webapi proxy with clerk

* Update jwt.ts
2025-07-18 12:56:55 +08:00
lobehubbot ecf1fdc2f7 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-17 18:57:38 +00:00
semantic-release-bot 71c33300cf 🔖 chore(release): v1.100.1 [skip ci]
### [Version&nbsp;1.100.1](https://github.com/lobehub/lobe-chat/compare/v1.100.0...v1.100.1)
<sup>Released on **2025-07-17**</sup>

#### 🐛 Bug Fixes

- **misc**: Use server env config image models.

<br/>

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

#### What's fixed

* **misc**: Use server env config image models, closes [#8478](https://github.com/lobehub/lobe-chat/issues/8478) ([768ee2b](https://github.com/lobehub/lobe-chat/commit/768ee2b))

</details>

<div align="right">

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

</div>
2025-07-17 18:56:46 +00:00
YuTengjing 768ee2bf23 🐛 fix: use server env config image models (#8478)
* docs: update fal provider invalid image links

* docs: add FAL model provider environment variables documentation

* 🐛 fix: update model type assignment in parseModels.ts to use dynamic lookup

* 📝 docs: add FAQ for resolving AI image generation timeout issues on Vercel

*  feat: implement getModelPropertyWithFallback utility for dynamic model property retrieval

* 📝 docs: expand testing guide with best practices for mock data strategies, error handling, and module pollution prevention

* 🐛 fix: update model type in LobeOpenAICompatibleFactory tests to 'chat'
2025-07-18 02:41:27 +08:00
lobehubbot d23d8f1c29 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-17 05:25:51 +00:00
semantic-release-bot 7eadecd340 🔖 chore(release): v1.100.0 [skip ci]
## [Version&nbsp;1.100.0](https://github.com/lobehub/lobe-chat/compare/v1.99.6...v1.100.0)
<sup>Released on **2025-07-17**</sup>

####  Features

- **misc**: Refactor desktop oauth and use JWTs token to support remote chat.

<br/>

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

#### What's improved

* **misc**: Refactor desktop oauth and use JWTs token to support remote chat, closes [#8446](https://github.com/lobehub/lobe-chat/issues/8446) ([054ca5f](https://github.com/lobehub/lobe-chat/commit/054ca5f))

</details>

<div align="right">

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

</div>
2025-07-17 05:24:42 +00:00
Arvin Xu 785406be9a 🔨 chore: improve code (#8469) 2025-07-17 13:09:41 +08:00
Arvin Xu 054ca5fd97 feat: refactor desktop oauth and use JWTs token to support remote chat (#8446)
* refactor the oauth

* refactor the oauth

* refactor the oauth

* improve oauth status

* fix desktop auth

* fix tests

* improve clean handoff

* try to fix handoff public issue

* fix route protection

* refactor anim

* refactor

* update to access token to jwt

* update to access token to jwt

* improve config

* refactor for JWKs token

* fix auto refresh issue

*  feat: support webapi proxy

* wip: 完成新流式接口

* wip: 跑通流式框架

* fix webhooks

* fix network proxy

* try to fix workflow

* fix proxy in remote sync

* fix tests

* fix tests

* fix oauth bypass route

* fix webapi proxy
2025-07-17 12:54:37 +08:00
lobehubbot 5d3a4ad460 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-16 18:05:16 +00:00
semantic-release-bot 8eb257bb2e 🔖 chore(release): v1.99.6 [skip ci]
### [Version&nbsp;1.99.6](https://github.com/lobehub/lobe-chat/compare/v1.99.5...v1.99.6)
<sup>Released on **2025-07-16**</sup>

#### 🐛 Bug Fixes

- **misc**: Desktop local db can't upload image.

<br/>

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

#### What's fixed

* **misc**: Desktop local db can't upload image, closes [#8459](https://github.com/lobehub/lobe-chat/issues/8459) ([25bfc80](https://github.com/lobehub/lobe-chat/commit/25bfc80))

</details>

<div align="right">

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

</div>
2025-07-16 18:04:24 +00:00
YuTengjing 25bfc802b5 🐛 fix: desktop local db can't upload image (#8459) 2025-07-17 01:49:11 +08:00
lobehubbot 752e576b80 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-16 13:57:35 +00:00
semantic-release-bot 166f3e2400 🔖 chore(release): v1.99.5 [skip ci]
### [Version&nbsp;1.99.5](https://github.com/lobehub/lobe-chat/compare/v1.99.4...v1.99.5)
<sup>Released on **2025-07-16**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix page error when url is not defined in web search plugin.

<br/>

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

#### What's fixed

* **misc**: Fix page error when url is not defined in web search plugin, closes [#8441](https://github.com/lobehub/lobe-chat/issues/8441) ([a55b65b](https://github.com/lobehub/lobe-chat/commit/a55b65b))

</details>

<div align="right">

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

</div>
2025-07-16 13:56:43 +00:00
vual a55b65b0b1 🐛 fix: fix page error when url is not defined in web search plugin (#8441)
Co-authored-by: Arvin Xu <arvinx@foxmail.com>
2025-07-16 21:41:14 +08:00
lobehubbot 3359b3f237 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-16 13:22:45 +00:00
semantic-release-bot 8ba8e8217b 🔖 chore(release): v1.99.4 [skip ci]
### [Version&nbsp;1.99.4](https://github.com/lobehub/lobe-chat/compare/v1.99.3...v1.99.4)
<sup>Released on **2025-07-16**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix apikey issue on server log.

<br/>

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

#### What's fixed

* **misc**: Fix apikey issue on server log, closes [#8457](https://github.com/lobehub/lobe-chat/issues/8457) ([43be2d1](https://github.com/lobehub/lobe-chat/commit/43be2d1))

</details>

<div align="right">

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

</div>
2025-07-16 13:21:54 +00:00
Arvin Xu 43be2d1905 🐛 fix: fix apikey issue on server log (#8457) 2025-07-16 21:06:51 +08:00
lobehubbot 53e0b51cbd 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-16 08:52:28 +00:00
semantic-release-bot c530000334 🔖 chore(release): v1.99.3 [skip ci]
### [Version&nbsp;1.99.3](https://github.com/lobehub/lobe-chat/compare/v1.99.2...v1.99.3)
<sup>Released on **2025-07-16**</sup>

#### 🐛 Bug Fixes

- **misc**: Chat model list should not show image model.

<br/>

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

#### What's fixed

* **misc**: Chat model list should not show image model, closes [#8448](https://github.com/lobehub/lobe-chat/issues/8448) ([2bb1506](https://github.com/lobehub/lobe-chat/commit/2bb1506))

</details>

<div align="right">

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

</div>
2025-07-16 08:51:36 +00:00
YuTengjing 2bb1506ea2 🐛 fix: chat model list should not show image model (#8448) 2025-07-16 16:36:43 +08:00
YuTengjing c8f32c301e ✏️ docs: replace all 'Language Model' with 'Al Service Provider' in provider docs (#8444) 2025-07-15 17:23:09 +08:00
lobehubbot 1261cee35d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-15 08:36:44 +00:00
semantic-release-bot 8c99c5dfd4 🔖 chore(release): v1.99.2 [skip ci]
### [Version&nbsp;1.99.2](https://github.com/lobehub/lobe-chat/compare/v1.99.1...v1.99.2)
<sup>Released on **2025-07-15**</sup>

#### 🐛 Bug Fixes

- **misc**: Some ai image generation feedback issues.

<br/>

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

#### What's fixed

* **misc**: Some ai image generation feedback issues, closes [#8440](https://github.com/lobehub/lobe-chat/issues/8440) ([bc41329](https://github.com/lobehub/lobe-chat/commit/bc41329))

</details>

<div align="right">

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

</div>
2025-07-15 08:35:52 +00:00
YuTengjing bc413299ba 🐛 fix: some ai image generation feedback issues (#8440) 2025-07-15 16:20:41 +08:00
lobehubbot f8369e1e3d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-15 08:00:07 +00:00
semantic-release-bot 8e4f59c25b 🔖 chore(release): v1.99.1 [skip ci]
### [Version&nbsp;1.99.1](https://github.com/lobehub/lobe-chat/compare/v1.99.0...v1.99.1)
<sup>Released on **2025-07-15**</sup>

<br/>

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

</details>

<div align="right">

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

</div>
2025-07-15 07:59:14 +00:00
Arvin Xu 28c1fe060a ️ perf: improve memory usage in desktop (#8431)
* improve usage of memory

* try to improve
2025-07-15 15:44:10 +08:00
YuTengjing 9557d79e33 🐛 fix: some ai image bugs (#8432) 2025-07-14 23:11:02 +08:00
lobehubbot 876df9ca08 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-14 12:51:05 +00:00
semantic-release-bot 025529d94f 🔖 chore(release): v1.99.0 [skip ci]
## [Version&nbsp;1.99.0](https://github.com/lobehub/lobe-chat/compare/v1.98.2...v1.99.0)
<sup>Released on **2025-07-14**</sup>

####  Features

- **plugin**: Support Streamable HTTP MCP Server Auth.
- **misc**:  support AI Image.

<br/>

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

#### What's improved

* **plugin**: Support Streamable HTTP MCP Server Auth, closes [#8425](https://github.com/lobehub/lobe-chat/issues/8425) ([853a09a](https://github.com/lobehub/lobe-chat/commit/853a09a))
* **misc**:  support AI Image, closes [#8312](https://github.com/lobehub/lobe-chat/issues/8312) ([095de57](https://github.com/lobehub/lobe-chat/commit/095de57))

</details>

<div align="right">

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

</div>
2025-07-14 12:50:05 +00:00
Arvin Xu 853a09af1b feat(plugin): support Streamable HTTP MCP Server Auth (#8425)
*  feat: support http streamable auth and headers

*  feat: support http streamable auth and headers

* improve

* improve token

* add i18n

* update i18n
2025-07-14 20:34:39 +08:00
YuTengjing 095de57675 feat: support AI Image (#8312)
Co-authored-by: canisminor1990 <i@canisminor.cc>
2025-07-14 20:29:57 +08:00
lobehubbot c98860e6cf 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-14 05:50:19 +00:00
semantic-release-bot f64f00fd6d 🔖 chore(release): v1.98.2 [skip ci]
### [Version&nbsp;1.98.2](https://github.com/lobehub/lobe-chat/compare/v1.98.1...v1.98.2)
<sup>Released on **2025-07-14**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

* **misc**: Update i18n, closes [#8422](https://github.com/lobehub/lobe-chat/issues/8422) ([5b89ec8](https://github.com/lobehub/lobe-chat/commit/5b89ec8))

</details>

<div align="right">

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

</div>
2025-07-14 05:49:30 +00:00
LobeHub Bot 5b89ec8bd9 💄 style: update i18n (#8422)
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-07-14 13:35:27 +08:00
lobehubbot 3b37094dfb 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-14 03:01:40 +00:00
semantic-release-bot b2a2fe3617 🔖 chore(release): v1.98.1 [skip ci]
### [Version&nbsp;1.98.1](https://github.com/lobehub/lobe-chat/compare/v1.98.0...v1.98.1)
<sup>Released on **2025-07-14**</sup>

#### 💄 Styles

- **misc**: Fix discover translation.

<br/>

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

#### Styles

* **misc**: Fix discover translation, closes [#8423](https://github.com/lobehub/lobe-chat/issues/8423) ([15ae35c](https://github.com/lobehub/lobe-chat/commit/15ae35c))

</details>

<div align="right">

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

</div>
2025-07-14 03:00:53 +00:00
René Wang 15ae35ca2a 💄 style: fix discover translation (#8423) 2025-07-14 10:46:57 +08:00
lobehubbot e9e11cbbed 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-13 05:45:45 +00:00
semantic-release-bot 1c9c8d75ee 🔖 chore(release): v1.98.0 [skip ci]
## [Version&nbsp;1.98.0](https://github.com/lobehub/lobe-chat/compare/v1.97.17...v1.98.0)
<sup>Released on **2025-07-13**</sup>

####  Features

- **misc**: Add network proxy for desktop.

<br/>

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

#### What's improved

* **misc**: Add network proxy for desktop, closes [#7848](https://github.com/lobehub/lobe-chat/issues/7848) ([46d2509](https://github.com/lobehub/lobe-chat/commit/46d2509))

</details>

<div align="right">

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

</div>
2025-07-13 05:44:37 +00:00
renovate[bot] 95ac795573 Update dependency @anthropic-ai/sdk to ^0.56.0 (#8412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-13 13:30:10 +08:00
renovate[bot] a173f6e401 Update dependency lucide-react to ^0.525.0 (#8413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-13 13:29:05 +08:00
Arvin Xu 37bb8ba2b7 🔨 chore: fix settings in desktop (#8414) 2025-07-13 13:26:22 +08:00
Arvin Xu 46d25092a4 feat: add network proxy for desktop (#7848)
* add network proxy

* update network proxy

* refactor network proxy

* support network proxy

* fix types

* fix lint

* fix lint
2025-07-13 13:19:21 +08:00
lobehubbot 88c86179ba 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-13 05:07:12 +00:00
semantic-release-bot 127d38d702 🔖 chore(release): v1.97.17 [skip ci]
### [Version&nbsp;1.97.17](https://github.com/lobehub/lobe-chat/compare/v1.97.16...v1.97.17)
<sup>Released on **2025-07-13**</sup>

#### 💄 Styles

- **misc**: Support Hunyuan A13B thinking model.

<br/>

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

#### Styles

* **misc**: Support Hunyuan A13B thinking model, closes [#8278](https://github.com/lobehub/lobe-chat/issues/8278) ([09ca978](https://github.com/lobehub/lobe-chat/commit/09ca978))

</details>

<div align="right">

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

</div>
2025-07-13 05:06:26 +00:00
sxjeru 09ca978f9c 💄 style: Support Hunyuan A13B thinking model (#8278)
* Update novita.ts

* feat: 添加新的混元模型并更新处理负载逻辑

* fix

* free Gemini 2.5 Pro

* 添加 ERNIE 4.5 300B A47B 模型到 siliconcloudChatModels

* novita

* 添加 GLM-4.1V-Thinking 系列模型到 zhipuChatModels,并更新现有模型的上下文窗口和最大输出设置

* 更新 zhipuChatModels,添加视觉能力并启用新模型;修改 ZhiPu 的检查模型 ID

* 移除多个 siliconcloud 废弃模型

* fix

* 移除 groq Qwen QwQ 32B 模型配置

* 更新 siliconcloud 模型

* 更新 novita/qwen 模型

* update siliconcloud model

* add Pangu Pro MoE 72B A16B

* Update novita.ts

* Update novita.ts

* update novita

---------

Co-authored-by: Arvin Xu <arvinx@foxmail.com>
2025-07-13 12:52:39 +08:00
lobehubbot 1696bf114b 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-13 03:48:21 +00:00
semantic-release-bot feb42fcc9e 🔖 chore(release): v1.97.16 [skip ci]
### [Version&nbsp;1.97.16](https://github.com/lobehub/lobe-chat/compare/v1.97.15...v1.97.16)
<sup>Released on **2025-07-13**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

* **misc**: Update i18n, closes [#8410](https://github.com/lobehub/lobe-chat/issues/8410) ([2515875](https://github.com/lobehub/lobe-chat/commit/2515875))

</details>

<div align="right">

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

</div>
2025-07-13 03:47:33 +00:00
LobeHub Bot 25158750c0 💄 style: update i18n (#8410)
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-07-13 11:33:28 +08:00
lobehubbot d3d7a158fc 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-12 17:17:24 +00:00
semantic-release-bot 2212ea98ac 🔖 chore(release): v1.97.15 [skip ci]
### [Version&nbsp;1.97.15](https://github.com/lobehub/lobe-chat/compare/v1.97.14...v1.97.15)
<sup>Released on **2025-07-12**</sup>

#### 🐛 Bug Fixes

- **misc**: Add vision support to Grok 4.

<br/>

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

#### What's fixed

* **misc**: Add vision support to Grok 4, closes [#8386](https://github.com/lobehub/lobe-chat/issues/8386) ([8512f5a](https://github.com/lobehub/lobe-chat/commit/8512f5a))

</details>

<div align="right">

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

</div>
2025-07-12 17:16:32 +00:00
bbbugg 8512f5a9ae 🐛 fix: add vision support to Grok 4 (#8386)
*  feat: add vision support to Grok 4

*  feat: disable Grok 2 Vision 1212 model
2025-07-13 01:02:02 +08:00
lobehubbot a2d40643c7 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-12 17:00:04 +00:00
semantic-release-bot 2ef77567f7 🔖 chore(release): v1.97.14 [skip ci]
### [Version&nbsp;1.97.14](https://github.com/lobehub/lobe-chat/compare/v1.97.13...v1.97.14)
<sup>Released on **2025-07-12**</sup>

#### 🐛 Bug Fixes

- **misc**: Revert "💄 style: Open new topic by tap Just Chat again".

#### 💄 Styles

- **misc**: Add Kimi K2 model.

<br/>

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

#### What's fixed

* **misc**: Revert "💄 style: Open new topic by tap Just Chat again", closes [#8402](https://github.com/lobehub/lobe-chat/issues/8402) ([55462b9](https://github.com/lobehub/lobe-chat/commit/55462b9))

#### Styles

* **misc**: Add Kimi K2 model, closes [#8401](https://github.com/lobehub/lobe-chat/issues/8401) ([4cb1a18](https://github.com/lobehub/lobe-chat/commit/4cb1a18))

</details>

<div align="right">

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

</div>
2025-07-12 16:59:10 +00:00
Arvin Xu 755e878a44 🔨 chore: fix lint (#8407) 2025-07-13 00:45:15 +08:00
sxjeru 4cb1a185ff 💄 style: Add Kimi K2 model (#8401) 2025-07-13 00:10:22 +08:00
Arvin Xu 55462b95ff 🐛 fix: Revert "💄 style: Open new topic by tap Just Chat again" (#8402)
This reverts commit 7e2f4ce5dd.
2025-07-12 23:04:10 +08:00
lobehubbot 9a95886fe7 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-12 05:04:50 +00:00
semantic-release-bot fd44b088d6 🔖 chore(release): v1.97.13 [skip ci]
### [Version&nbsp;1.97.13](https://github.com/lobehub/lobe-chat/compare/v1.97.12...v1.97.13)
<sup>Released on **2025-07-12**</sup>

#### 💄 Styles

- **misc**: Support new Doubao thinking models, update i18n.

<br/>

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

#### Styles

* **misc**: Support new Doubao thinking models, closes [#8174](https://github.com/lobehub/lobe-chat/issues/8174) ([637d75c](https://github.com/lobehub/lobe-chat/commit/637d75c))
* **misc**: Update i18n, closes [#8400](https://github.com/lobehub/lobe-chat/issues/8400) ([790eeb8](https://github.com/lobehub/lobe-chat/commit/790eeb8))

</details>

<div align="right">

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

</div>
2025-07-12 05:03:58 +00:00
LobeHub Bot 790eeb86c3 💄 style: update i18n (#8400) 2025-07-12 12:49:42 +08:00
sxjeru 637d75cde0 💄 style: Support new Doubao thinking models (#8174) 2025-07-12 12:49:21 +08:00
lobehubbot 526b93470d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-11 06:18:11 +00:00
semantic-release-bot 4963c599c9 🔖 chore(release): v1.97.12 [skip ci]
### [Version&nbsp;1.97.12](https://github.com/lobehub/lobe-chat/compare/v1.97.11...v1.97.12)
<sup>Released on **2025-07-11**</sup>

#### 🐛 Bug Fixes

- **misc**: Grok-4 reasoning model universal matching.

<br/>

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

#### What's fixed

* **misc**: Grok-4 reasoning model universal matching, closes [#8390](https://github.com/lobehub/lobe-chat/issues/8390) ([d6f17f8](https://github.com/lobehub/lobe-chat/commit/d6f17f8))

</details>

<div align="right">

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

</div>
2025-07-11 06:17:05 +00:00
Arvin Xu 09f921d531 ️ perf: add database autovacuum sql (#8395)
* remove ai sdk

* add autovacuum tuning sql
2025-07-11 14:03:05 +08:00
sxjeru d6f17f8246 🐛 fix: Grok-4 reasoning model universal matching (#8390)
* Update index.ts

* Update xai.ts
2025-07-11 14:02:08 +08:00
lobehubbot efb9311e6f 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-11 03:39:38 +00:00
semantic-release-bot 1353d208e9 🔖 chore(release): v1.97.11 [skip ci]
### [Version&nbsp;1.97.11](https://github.com/lobehub/lobe-chat/compare/v1.97.10...v1.97.11)
<sup>Released on **2025-07-11**</sup>

#### 💄 Styles

- **misc**: Open new topic by tap Just Chat again.

<br/>

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

#### Styles

* **misc**: Open new topic by tap Just Chat again, closes [#8311](https://github.com/lobehub/lobe-chat/issues/8311) ([7e2f4ce](https://github.com/lobehub/lobe-chat/commit/7e2f4ce))

</details>

<div align="right">

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

</div>
2025-07-11 03:38:50 +00:00
René Wang 7e2f4ce5dd 💄 style: Open new topic by tap Just Chat again (#8311)
* feat: Open new topic by tap Just Chat again

* feat: Prevent re-render of message update

* fix: Import loop
2025-07-11 11:24:43 +08:00
lobehubbot ca5432ea6d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-11 03:02:49 +00:00
semantic-release-bot 86ff384587 🔖 chore(release): v1.97.10 [skip ci]
### [Version&nbsp;1.97.10](https://github.com/lobehub/lobe-chat/compare/v1.97.9...v1.97.10)
<sup>Released on **2025-07-11**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

* **misc**: Update i18n, closes [#8387](https://github.com/lobehub/lobe-chat/issues/8387) ([00215c0](https://github.com/lobehub/lobe-chat/commit/00215c0))

</details>

<div align="right">

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

</div>
2025-07-11 03:01:58 +00:00
LobeHub Bot 00215c02eb 💄 style: update i18n (#8387) 2025-07-11 10:47:51 +08:00
741 changed files with 55003 additions and 4130 deletions
+93 -17
View File
@@ -1,8 +1,9 @@
---
description:
description:
globs: src/services/**/*,src/database/**/*,src/server/**/*
alwaysApply: false
---
# LobeChat 后端技术架构指南
本指南旨在阐述 LobeChat 项目的后端分层架构,重点介绍各核心目录的职责以及它们之间的协作方式。
@@ -29,24 +30,21 @@ LobeChat 的后端设计注重模块化、可测试性和灵活性,以适应
其主要分层如下:
1. 客户端服务层 (`src/services`):
- 位于 src/services/。
- 这是客户端业务逻辑的核心层,负责封装各种业务操作和数据处理逻辑。
- 环境适配: 根据不同的运行环境,服务层会选择合适的数据访问方式:
- 本地数据库模式: 直接调用 `Model` 层进行数据操作,适用于浏览器 PGLite 和本地 Electron 应用。
- 远程数据库模式: 通过 `tRPC` 客户端调用服务端 API,适用于需要云同步的场景。
- 本地数据库模式: 直接调用 `Model` 层进行数据操作,适用于浏览器 PGLite 和本地 Electron 应用。
- 远程数据库模式: 通过 `tRPC` 客户端调用服务端 API,适用于需要云同步的场景。
- 类型转换: 对于简单的数据类型转换,直接在此层进行类型断言,如 `this.pluginModel.query() as Promise<LobeTool[]>`
- 每个服务模块通常包含 `client.ts`(本地模式)、`server.ts`(远程模式)和 `type.ts`(接口定义)文件,在实现时应该确保本地模式和远程模式业务逻辑实现一致,只是数据库不同。
2. API 接口层 (`TRPC`):
- 位于 src/server/routers/
- 使用 `tRPC` 构建类型安全的 API。Router 根据运行时环境(如 Edge Functions, Node.js Lambda)进行组织。
- 负责接收客户端请求,并将其路由到相应的 `Service` 层进行处理。
- 新建 lambda 端点时可以参考 src/server/routers/lambda/\_template.ts
3. 仓库层 (`Repositories`):
- 位于 src/database/repositories/。
- 主要处理复杂的跨表查询和数据聚合逻辑,特别是当需要从多个 `Model` 获取数据并进行组合时。
- 与 `Model` 层不同,`Repository` 层专注于复杂的业务查询场景,而不涉及简单的领域模型转换。
@@ -54,7 +52,6 @@ LobeChat 的后端设计注重模块化、可测试性和灵活性,以适应
- 如果数据操作简单(仅涉及单个 `Model`),则通常直接在 `src/services` 层调用 `Model` 并进行简单的类型断言。
4. 模型层 (`Models`):
- 位于 src/database/models/ (例如 src/database/models/plugin.ts 和 src/database/models/document.ts)。
- 提供对数据库中各个表(由 src/database/schemas/ 中的 Drizzle ORM schema 定义)的基本 CRUD (创建、读取、更新、删除) 操作和简单的查询能力。
- `Model` 类专注于单个数据表的直接操作,不涉及复杂的领域模型转换,这些转换通常在上层的 `src/services` 中通过类型断言完成。
@@ -65,11 +62,11 @@ LobeChat 的后端设计注重模块化、可测试性和灵活性,以适应
- 客户端模式 (浏览器/PWA): 使用 PGLite (基于 WASM 的 PostgreSQL),数据存储在用户浏览器本地。
- 服务端模式 (云部署): 使用远程 PostgreSQL 数据库。
- Electron 桌面应用:
- Electron 客户端会启动一个本地 Node.js 服务。
- 本地服务通过 `tRPC` 与 Electron 的渲染进程通信。
- 数据库选择依赖于是否开启云同步功能:
- 云同步开启: 连接到远程 PostgreSQL 数据库。
- 云同步关闭: 使用 PGLite (通过 Node.js 的 WASM 实现) 在本地存储数据。
- Electron 客户端会启动一个本地 Node.js 服务。
- 本地服务通过 `tRPC` 与 Electron 的渲染进程通信。
- 数据库选择依赖于是否开启云同步功能:
- 云同步开启: 连接到远程 PostgreSQL 数据库。
- 云同步关闭: 使用 PGLite (通过 Node.js 的 WASM 实现) 在本地存储数据。
## 数据流向说明
@@ -93,8 +90,87 @@ UI (Electron Renderer) → Zustand action → Client Service -> TRPC Client →
## 服务层 (Server Services)
- 位于 src/server/services/。
- 核心职责是封装独立的、可复用的业务逻辑单元。这些服务应易于测试。
- 平台差异抽象: 一个关键特性是通过其内部的 `impls` 子目录(例如 src/server/services/file/impls 包含 s3.ts 和 local.ts)来抹平不同运行环境带来的差异(例如云端使用 S3 存储,桌面版使用本地文件系统)。这使得上层(如 `tRPC` routers)无需关心底层具体实现。
- 目标是使 `tRPC` router 层的逻辑尽可能纯粹,专注于请求处理和业务流程编排。
- 服务可能会调用 `Repository` 层或直接调用 `Model` 层进行数据持久化和检索,也可能调用其他服务。
- 位于 src/server/services/。
- 核心职责是封装独立的、可复用的业务逻辑单元。这些服务应易于测试。
- 平台差异抽象: 一个关键特性是通过其内部的 `impls` 子目录(例如 src/server/services/file/impls 包含 s3.ts 和 local.ts)来抹平不同运行环境带来的差异(例如云端使用 S3 存储,桌面版使用本地文件系统)。这使得上层(如 `tRPC` routers)无需关心底层具体实现。
- 目标是使 `tRPC` router 层的逻辑尽可能纯粹,专注于请求处理和业务流程编排。
- 服务可能会调用 `Repository` 层或直接调用 `Model` 层进行数据持久化和检索,也可能调用其他服务。
## 最佳实践 (Best Practices)
### 数据库操作封装原则
**连续的数据库操作应该封装到 Model 层**
当业务逻辑涉及多个相关的数据库操作时,建议将这些操作封装到 Model 层中,而不是在上层(Service 或 Router 层)中进行多次数据库调用。
**优势:**
- **代码复用**: Client DB 环境的 service 实现和 Server DB 的 lambda 层实现可以复用相同的 Model 方法
- **事务一致性**: 相关的数据库操作可以在同一个方法中管理,便于维护数据一致性
- **性能优化**: 减少数据库连接次数,提高查询效率
- **职责清晰**: Model 层专注数据访问,上层专注业务协调
**示例:**
```typescript
// ✅ 推荐:在 Model 层封装连续的数据库操作
class GenerationBatchModel {
async delete(id: string): Promise<{ deletedBatch: BatchItem; thumbnailUrls: string[] }> {
// 1. 查询相关数据
const batchWithGenerations = await this.db.query.generationBatches.findFirst({...});
// 2. 收集需要处理的数据
const thumbnailUrls = [...];
// 3. 执行删除操作
const [deletedBatch] = await this.db.delete(generationBatches)...;
return { deletedBatch, thumbnailUrls };
}
}
// ✅ 上层使用简洁
const { thumbnailUrls } = await model.delete(id);
await fileService.deleteFiles(thumbnailUrls);
```
### 文件操作与数据库操作的执行顺序
**删除操作原则:数据库删除在前,文件删除在后**
当业务逻辑同时涉及数据库记录和文件系统操作时,应该遵循"数据库优先"的原则。
**原因:**
- **用户体验优先**: 如果先删除文件再删除数据库记录,可能出现文件已删除但数据库记录仍存在的情况,用户访问时会遇到文件不存在的错误
- **影响程度较小**: 如果先删除数据库记录再删除文件,即使文件删除失败,用户也看不到这个记录,只是造成一些存储空间浪费,对用户体验影响更小
- **数据一致性**: 数据库记录是业务逻辑的核心,应该优先保证其一致性
**示例:**
```typescript
// ✅ 推荐:先删除数据库记录,再删除文件
async deleteGeneration(id: string) {
// 1. 先删除数据库记录
const deletedGeneration = await generationModel.delete(id);
// 2. 再删除相关文件
if (deletedGeneration.asset?.thumbnailUrl) {
await fileService.deleteFile(deletedGeneration.asset.thumbnailUrl);
}
}
// ❌ 不推荐:先删除文件
async deleteGeneration(id: string) {
const generation = await generationModel.findById(id);
// 如果这里删除成功,但后面数据库删除失败,用户会遇到访问错误
await fileService.deleteFile(generation.asset.thumbnailUrl);
await generationModel.delete(id); // 可能失败
}
```
**创建操作原则:数据库创建在前,文件操作在后**
创建操作同样应该优先处理数据库记录,确保数据的一致性和完整性。
+38 -34
View File
@@ -1,13 +1,14 @@
---
description: How to code review
globs:
globs:
alwaysApply: false
---
# Role Description
- You are a senior full-stack engineer skilled in performance optimization, security, and design systems.
- You excel at reviewing code and providing constructive feedback.
- Your task is to review submitted Git diffs **in Chinese** and return a structured review report.
- You are a senior full-stack engineer skilled in performance optimization, security, and design systems.
- You excel at reviewing code and providing constructive feedback.
- Your task is to review submitted Git diffs **in Chinese** and return a structured review report.
- Review style: concise, direct, focused on what matters most, with actionable suggestions.
## Before the Review
@@ -16,54 +17,57 @@ Gather the modified code and context. Please strictly follow the process below:
1. Use `read_file` to read [package.json](mdc:package.json)
2. Use terminal to run command `git diff HEAD | cat` to obtain the diff and list the changed files. If you recieived empty result, run the same command once more.
3. Use `read_file` to open each changed file.
4. Use `read_file` to read [rules-attach.mdc](mdc:.cursor/rules/rules-attach.mdc). Even if you think it's unnecessary, you must read it.
5. combine changed files, step3 and `agent_requestable_workspace_rules`, list the rules which need to read
3. Use `read_file` to open each changed file.
4. Use `read_file` to read [rules-attach.mdc](mdc:.cursor/rules/rules-attach.mdc). Even if you think it's unnecessary, you must read it.
5. combine changed files, step3 and `agent_requestable_workspace_rules`, list the rules which need to read
6. Use `read_file` to read the rules list in step 5
## Review
### Code Style
- Ensure JSDoc comments accurately reflect the implementation; update them when needed.
- Look for opportunities to simplify or modernize code with the latest JavaScript/TypeScript features.
- Prefer `async`/`await` over callbacks or chained `.then` promises.
- Use consistent, descriptive naming—avoid obscure abbreviations.
- Replace magic numbers or strings with well-named constants.
- Ensure JSDoc comments accurately reflect the implementation; update them when needed.
- Look for opportunities to simplify or modernize code with the latest JavaScript/TypeScript features.
- Prefer `async`/`await` over callbacks or chained `.then` promises.
- Use consistent, descriptive naming—avoid obscure abbreviations.
- Replace magic numbers or strings with well-named constants.
- Use semantically meaningful variable, function, and class names.
- Ignore purely formatting issues and other autofixable lint problems.
### Code Optimization
- Prefer `for…of` loops to index-based `for` loops when feasible.
- Decide whether callbacks should be **debounced** or **throttled**.
- Use components from `@lobehub/ui`, Ant Design, or the existing design system instead of raw HTML tags (e.g., `Button` vs. `button`).
- reuse npm packages already installed (e.g., `lodash/omit`) rather than reinventing the wheel.
- Design for dark mode and mobile responsiveness:
- Use the `antd-style` token system instead of hard-coded colors.
- Select the proper component variants.
- Performance considerations:
- Where safe, convert sequential async flows to concurrent ones with `Promise.all`, `Promise.race`, etc.
- Prefer `for…of` loops to index-based `for` loops when feasible.
- Decide whether callbacks should be **debounced** or **throttled**.
- Use components from `@lobehub/ui`, Ant Design, or the existing design system instead of raw HTML tags (e.g., `Button` vs. `button`).
- reuse npm packages already installed (e.g., `lodash/omit`) rather than reinventing the wheel.
- Design for dark mode and mobile responsiveness:
- Use the `antd-style` token system instead of hard-coded colors.
- Select the proper component variants.
- Performance considerations:
- Where safe, convert sequential async flows to concurrent ones with `Promise.all`, `Promise.race`, etc.
- Query only the required columns from a database rather than selecting entire rows.
### Obvious Bugs
- Do not silently swallow errors in `catch` blocks; at minimum, log them.
- Revert temporary code used only for testing (e.g., debug logs, temporary configs).
- Remove empty handlers (e.g., an empty `onClick`).
- Do not silently swallow errors in `catch` blocks; at minimum, log them.
- Revert temporary code used only for testing (e.g., debug logs, temporary configs).
- Remove empty handlers (e.g., an empty `onClick`).
- Confirm the UI degrades gracefully for unauthenticated users.
- Don't leave any debug logs in the code (except when using the `debug` module properly).
- When using the `debug` module, avoid `import { log } from 'debug'` as it logs directly to console. Use proper debug namespaces instead.
- Check logs for sensitive information like api key, etc
## After the Review: output
1. Summary
- Start with a brief explanation of what the change set does.
- Summarize the changes for each modified file (or logical group).
- Start with a brief explanation of what the change set does.
- Summarize the changes for each modified file (or logical group).
2. Comments Issues
- List the most critical issues first.
- Use an ordered list, which will be convenient for me to reference later.
- For each issue:
- Mark severity tag (`❌ Must fix`, `⚠️ Should fix`, `💅 Nitpick`)
- Provode file path to the relevant file.
- Provide recommended fix
- End with a **git commit** command, instruct the author to run it.
- We use gitmoji to label commit messages, format: [emoji] <type>(<scope>): <subject>
- List the most critical issues first.
- Use an ordered list, which will be convenient for me to reference later.
- For each issue:
- Mark severity tag (`❌ Must fix`, `⚠️ Should fix`, `💅 Nitpick`)
- Provode file path to the relevant file.
- Provide recommended fix
- End with a **git commit** command, instruct the author to run it.
- We use gitmoji to label commit messages, format: [emoji] <type>(<scope>): <subject>
+45 -35
View File
@@ -1,21 +1,25 @@
---
description:
globs:
description:
globs:
alwaysApply: true
---
# Guide to Optimize Output(Response) Rendering
## File Path and Code Symbol Rendering
- When rendering file paths, use backtick wrapping instead of markdown links so they can be parsed as clickable links in Cursor IDE.
- Good: `src/components/Button.tsx`
- Bad: [src/components/Button.tsx](src/components/Button.tsx)
- Good: `src/components/Button.tsx`
- Bad: [src/components/Button.tsx](mdc:src/components/Button.tsx)
- Don't use line and column number in file path, this will make file path not clickable in Cursor IDE.
- Good: `src/components/Button.tsx` `10:20` (add a space between the file path and the line and column number)
- Bad: `src/components/Button.tsx:10:20`
- When rendering functions, variables, or other code symbols, use backtick wrapping so they can be parsed as navigable links in Cursor IDE
- Good: The `useState` hook in `MyComponent`
- Bad: The useState hook in MyComponent
- Good: The `useState` hook in `MyComponent`
- Bad: The useState hook in MyComponent
## Markdown Render
- don't use br tag to wrap in table cell
@@ -23,9 +27,9 @@ alwaysApply: true
## Terminal Command Output
- If terminal commands don't produce output, it's likely due to paging issues. Try piping the command to `cat` to ensure full output is displayed.
- Good: `git show commit_hash -- file.txt | cat`
- Good: `git log --oneline | cat`
- Reason: Some git commands use pagers by default, which may prevent output from being captured properly
- Good: `git show commit_hash -- file.txt | cat`
- Good: `git log --oneline | cat`
- Reason: Some git commands use pagers by default, which may prevent output from being captured properly
## Mermaid Diagram Generation: Strict Syntax Validation Checklist
@@ -44,50 +48,56 @@ Before producing any Mermaid diagram, you **must** compare your final code line-
### Checklist Details
#### Rule 1: Edge Labels Must Be Plain Text Only
#### Rule 1: Edge Labels Must Be Plain Text Only
> **Essence:** Anything inside `|...|` must contain pure, unformatted text. Absolutely NO Markdown, list markers, or parentheses/brackets allowed—these often cause rendering failures.
- **✅ Do:** `A -->|Process plain text data| B`
- **❌ Don't:** `A -->|1. Ordered list item| B` (No numbered lists)
- **❌ Don't:** `CC --"1. fetch('/api/...')"--> API` (No square brackets)
- **❌ Don't:** `A -->|- Unordered list item| B` (No hyphen lists)
- **❌ Don't:** `A -->|Transform (important)| B` (No parentheses)
- **❌ Don't:** `A -->|Transform [important]| B` (No square brackets)
- **✅ Do:** `A -->|Process plain text data| B`
- **❌ Don't:** `A -->|1. Ordered list item| B` (No numbered lists)
- **❌ Don't:** `CC --"1. fetch('/api/...')"--> API` (No square brackets)
- **❌ Don't:** `A -->|- Unordered list item| B` (No hyphen lists)
- **❌ Don't:** `A -->|Transform (important)| B` (No parentheses)
- **❌ Don't:** `A -->|Transform [important]| B` (No square brackets)
#### Rule 2: Node Definition Handle Special Characters with Care
#### Rule 2: Node Definition Handle Special Characters with Care
> **Essence:** When node text or subgraph titles contain special characters like `()` or `[]`, wrap the text in quotes to avoid conflicts with Mermaid shape syntax.
- **When your node text includes parentheses (e.g., 'React (JSX)'):**
- **✅ Do:** `I_REACT["<b>React component (JSX)</b>"]` (Quotes wrap all text)
- **❌ Don't:** `I_REACT(<b>React component (JSX)</b>)` (Wrong, Mermaid parses this as a shape)
- **❌ Don't:** `subgraph Plugin Features (Plugins)` (Wrong, subgraph titles with parentheses must also be wrapped in quotes)
- **When your node text includes parentheses (e.g., 'React (JSX)'):**
- **✅ Do:** `I_REACT["<b>React component (JSX)</b>"]` (Quotes wrap all text)
- **❌ Don't:** `I_REACT(<b>React component (JSX)</b>)` (Wrong, Mermaid parses this as a shape)
- **❌ Don't:** `subgraph Plugin Features (Plugins)` (Wrong, subgraph titles with parentheses must also be wrapped in quotes)
#### Rule 3: Double Quotes in Text Must Be Escaped
#### Rule 3: Double Quotes in Text Must Be Escaped
> **Essence:** Use `&quot;` for double quotes **inside node text**.
- **✅ Do:** `A[This node contains &quot;quotes&quot;]`
- **❌ Don't:** `A[This node contains "quotes"]`
- **✅ Do:** `A[This node contains &quot;quotes&quot;]`
- **❌ Don't:** `A[This node contains "quotes"]`
#### Rule 4: All Formatting Must Use HTML Tags (NOT Markdown!)
#### Rule 4: All Formatting Must Use HTML Tags (NOT Markdown!)
> **Essence:** For newlines, bold, and other text formatting in nodes, use HTML tags only. Markdown is not supported.
- **✅ Do (robust):** `A["<b>Bold</b> and <code>code</code><br>This is a new line"]`
- **❌ Don't (not rendered):** `C["# This is a heading"]`
- **❌ Don't (not rendered):** ``C["`const` means constant"]``
- **⚠️ Warning (unreliable):** `B["Markdown **bold** might sometimes work but DON'T rely on it"]`
- **✅ Do (robust):** `A["<b>Bold</b> and <code>code</code><br>This is a new line"]`
- **❌ Don't (not rendered):** `C["# This is a heading"]`
- **❌ Don't (not rendered):** ``C["`const` means constant"]``
- **⚠️ Warning (unreliable):** `B["Markdown **bold** might sometimes work but DON'T rely on it"]`
#### Rule 5: No HTML Tags for Participants and Message Labels (Sequence Diagrams)
#### Rule 5: No HTML Tags for Participants and Message Labels (Sequence Diagrams)
> **Important Addition:**
> In Mermaid sequence diagrams, you MUST NOT use any HTML tags (such as `<b>`, `<code>`, etc.) in:
>
> - `participant` display names (`as` part)
> - Message labels (the text after `:` in diagram flows)
>
> These tags are generally not rendered—they may appear as-is or cause compatibility issues.
- **✅ Do:** `participant A as Client`
- **❌ Don't:** `participant A as <b>Client</b>`
- **✅ Do:** `A->>B: 1. Establish connection`
- **❌ Don't:** `A->>B: 1. <code>Establish connection</code>`
- **✅ Do:** `participant A as Client`
- **❌ Don't:** `participant A as <b>Client</b>`
- **✅ Do:** `A->>B: 1. Establish connection`
- **❌ Don't:** `A->>B: 1. <code>Establish connection</code>`
---
@@ -1,6 +0,0 @@
---
description:
globs: src/locales/**/*
alwaysApply: false
---
read [i18n.mdc](mdc:.cursor/rules/i18n/i18n.mdc)
+18 -7
View File
@@ -1,19 +1,16 @@
---
description:
globs:
alwaysApply: true
---
## Project Description
You are developing an open-source, modern-design AI chat framework: lobe chat.
Emoji logo: 🤯
You are developing an open-source, modern-design AI chat framework: lobe chat.
Emoji logo: 🤯
## Project Technologies Stack
read [package.json](mdc:package.json) to know all npm packages you can use.
read [folder-structure.mdx](mdc:docs/development/basic/folder-structure.mdx) to learn project structure.
read [package.json](mdc:package.json) to know all npm packages you can use. read [folder-structure.mdx](mdc:docs/development/basic/folder-structure.mdx) to learn project structure.
The project uses the following technologies:
@@ -45,3 +42,17 @@ The project uses the following technologies:
- Cursor AI for code editing and AI coding assistance
Note: All tools and libraries used are the latest versions. The application only needs to be compatible with the latest browsers;
## Often used npm scripts
```bash
# type check
bun type-check
# install dependencies
pnpm install
# !: don't any build script to check weather code can work after modify
```
check [testing guide](./testing-guide/testing-guide.mdc) to learn test scripts.
+16 -7
View File
@@ -1,8 +1,9 @@
---
description:
globs:
description:
globs:
alwaysApply: true
---
# LobeChat Cursor Rules System Guide
This document explains how the LobeChat project's Cursor rules system works and serves as an index for manually accessible rules.
@@ -14,22 +15,26 @@ This document explains how the LobeChat project's Cursor rules system works and
## 📚 Four Ways to Access Rules
### 1. **Always Applied Rules** - `always_applied_workspace_rules`
- **What**: Core project guidelines that are always active
- **Content**: Project tech stack, basic coding standards, output formatting rules
- **Access**: No tools needed - automatically provided in every conversation
### 2. **Dynamic Context Rules** - `cursor_rules_context`
- **What**: Rules automatically matched based on files referenced in the conversation
- **Trigger**: Only when user **explicitly @ mentions files** or **opens files in Cursor**
- **Content**: May include brief descriptions or full rule content, depending on relevance
- **Access**: No tools needed - automatically updated when files are referenced
### 3. **Agent Requestable Rules** - `agent_requestable_workspace_rules`
- **What**: Detailed operational guides that can be requested on-demand
- **Access**: Use `fetch_rules` tool with rule names
- **Examples**: `debug`, `i18n/i18n`, `code-review`
### 4. **Manual Rules Index** - This file + `read_file`
- **What**: Additional rules not covered by the above mechanisms
- **Why needed**: Cursor's rule system only supports "agent request" or "auto attach" modes
- **Access**: Use `read_file` tool to read specific `.mdc` files
@@ -47,10 +52,13 @@ Use `read_file` to access rules from the index below when:
The following rules are available via `read_file` from the `.cursor/rules/` directory:
- `backend-architecture.mdc` Backend layer architecture and design guidelines
- `zustand-action-patterns.mdc` Recommended patterns for organizing Zustand actions
- `zustand-slice-organization.mdc` Best practices for structuring Zustand slices
- `define-database-model.mdc` Database model definition guidelines
- `drizzle-schema-style-guide.mdc` Style guide for defining Drizzle ORM schemas
- `react-component.mdc` React component style guide and conventions
- `testing-guide.mdc` Comprehensive testing guide for Vitest environment
- `typescript.mdc` TypeScript code style guide
- `zustand-action-patterns.mdc` Recommended patterns for organizing Zustand actions
- `zustand-slice-organization.mdc` Best practices for structuring Zustand slices
## ❌ Common Misunderstandings to Avoid
@@ -62,7 +70,7 @@ The following rules are available via `read_file` from the `.cursor/rules/` dire
```
1. Start with always_applied_workspace_rules (automatic)
2. Check cursor_rules_context for auto-matched rules (automatic)
2. Check cursor_rules_context for auto-matched rules (automatic)
3. If you need specific guides: fetch_rules (manual)
4. If you identify gaps: consult this index → read_file (manual)
```
@@ -70,7 +78,8 @@ The following rules are available via `read_file` from the `.cursor/rules/` dire
## Example Decision Flow
**Scenario**: Working on a new Zustand store slice
1. Follow always_applied_workspace_rules ✅
2. If store files were @ mentioned → use cursor_rules_context rules ✅
2. If store files were @ mentioned → use cursor_rules_context rules ✅
3. Need detailed Zustand guidance → `read_file('.cursor/rules/zustand-slice-organization.mdc')` ✅
4. All rules apply simultaneously - no conflicts ✅
4. All rules apply simultaneously - no conflicts ✅
+8 -3
View File
@@ -1,8 +1,9 @@
---
description:
globs:
description:
globs:
alwaysApply: true
---
## System Role
You are an expert in full-stack Web development, proficient in JavaScript, TypeScript, CSS, React, Node.js, Next.js, Postgresql, all kinds of network protocols.
@@ -11,7 +12,6 @@ You are an expert in LLM and Ai art. In Ai image generation, you are proficient
You are an expert in UI/UX design, proficient in web interaction patterns, responsive design, accessibility, and user behavior optimization. You excel at improving user retention and paid conversion rates through various interaction details.
## Problem Solving
- Before formulating any response, you must first gather context by using tools like codebase_search, grep_search, file_search, web_search, fetch_rules, context7, and read_file to avoid making assumptions.
@@ -36,3 +36,8 @@ You are an expert in UI/UX design, proficient in web interaction patterns, respo
- If you're unable to access or retrieve content from websites, please inform me immediately and request the specific information needed rather than making assumptions
- You can use emojis, npm packages like `chalk`/`chalk-animation`/`terminal-link`/`gradient-string`/`log-symbols`/`boxen`/`consola`/`@clack/prompts` to create beautiful terminal output
- Don't run `tsc --noEmit` to check ts syntax error, because our project is very large and the validate very slow
## Some logging rules
- Never log user private information like api key, etc
- Don't use `import { log } from 'debug'` to log messages, because it will directly log the message to the console.
-881
View File
@@ -1,881 +0,0 @@
---
description:
globs: *.test.ts,*.test.tsx
alwaysApply: false
---
---
type: agent-requested
title: 测试指南 - LobeChat Testing Guide
description: LobeChat 项目的 Vitest 测试环境配置、运行方式、修复原则指南
---
# 测试指南 - LobeChat Testing Guide
## 🧪 测试环境概览
LobeChat 项目使用 Vitest 测试库,配置了两种不同的测试环境:
### 客户端测试环境 (DOM Environment)
- **配置文件**: [vitest.config.ts](mdc:vitest.config.ts)
- **环境**: Happy DOM (浏览器环境模拟)
- **数据库**: PGLite (浏览器环境的 PostgreSQL)
- **用途**: 测试前端组件、客户端逻辑、React 组件等
- **设置文件**: [tests/setup.ts](mdc:tests/setup.ts)
### 服务端测试环境 (Node Environment)
- **配置文件**: [vitest.config.server.ts](mdc:vitest.config.server.ts)
- **环境**: Node.js
- **数据库**: 真实的 PostgreSQL 数据库
- **并发限制**: 单线程运行 (`singleFork: true`)
- **用途**: 测试数据库模型、服务端逻辑、API 端点等
- **设置文件**: [tests/setup-db.ts](mdc:tests/setup-db.ts)
## 🚀 测试运行命令
### package.json 脚本说明
查看 [package.json](mdc:package.json) 中的测试相关脚本:
```json
{
"test": "npm run test-app && npm run test-server",
"test-app": "vitest run --config vitest.config.ts",
"test-app:coverage": "vitest run --config vitest.config.ts --coverage",
"test-server": "vitest run --config vitest.config.server.ts",
"test-server:coverage": "vitest run --config vitest.config.server.ts --coverage"
}
```
### 推荐的测试运行方式
#### ✅ 正确的命令格式
```bash
# 运行所有客户端测试
npx vitest run --config vitest.config.ts
# 运行所有服务端测试
npx vitest run --config vitest.config.server.ts
# 运行特定测试文件 (支持模糊匹配)
npx vitest run --config vitest.config.ts basic
npx vitest run --config vitest.config.ts user.test.ts
# 运行特定文件的特定行号
npx vitest run --config vitest.config.ts src/utils/helper.test.ts:25
npx vitest run --config vitest.config.ts basic/foo.test.ts:10,basic/foo.test.ts:25
# 过滤特定测试用例名称
npx vitest -t "test case name" --config vitest.config.ts
# 组合使用文件和测试名称过滤
npx vitest run --config vitest.config.ts filename.test.ts -t "specific test"
```
#### ❌ 避免的命令格式
```bash
# ❌ 不要使用 pnpm test xxx (这不是有效的 vitest 命令)
pnpm test some-file
# ❌ 不要使用裸 vitest (会进入 watch 模式)
vitest test-file.test.ts
# ❌ 不要混淆测试环境
npx vitest run --config vitest.config.server.ts client-component.test.ts
```
### 关键运行参数说明
- **`vitest run`**: 运行一次测试然后退出 (避免 watch 模式)
- **`vitest`**: 默认进入 watch 模式,持续监听文件变化
- **`--config`**: 指定配置文件,选择正确的测试环境
- **`-t`**: 过滤测试用例名称,支持正则表达式
- **`--coverage`**: 生成测试覆盖率报告
## 🔧 测试修复原则
### 核心原则 ⚠️
1. **充分阅读测试代码**: 在修复测试之前,必须完整理解测试的意图和实现
2. **测试优先修复**: 如果是测试本身写错了,修改测试而不是实现代码
3. **专注单一问题**: 只修复指定的测试,不要添加额外测试或功能
4. **不自作主张**: 不要因为发现其他问题就直接修改,先提出再讨论
### 测试修复流程
```mermaid
flowchart TD
subgraph "阶段一:分析与复现"
A[开始:收到测试失败报告] --> B[定位并运行失败的测试];
B --> C{是否能在本地复现?};
C -->|否| D[检查测试环境/配置/依赖];
C -->|是| E[分析:阅读测试代码、错误日志、Git 历史];
end
subgraph "阶段二:诊断与调试"
E --> F[建立假设:问题出在测试、代码还是环境?];
F --> G["调试:使用 console.log 或 debugger 深入检查"];
G --> H{假设是否被证实?};
H -->|否, 重新假设| F;
end
subgraph "阶段三:修复与验证"
H -->|是| I{确定根本原因};
I -->|测试逻辑错误| J[修复测试代码];
I -->|实现代码 Bug| K[修复实现代码];
I -->|环境/配置问题| L[修复配置或依赖];
J --> M[验证修复:重新运行失败的测试];
K --> M;
L --> M;
M --> N{测试是否通过?};
N -->|否, 修复无效| F;
N -->|是| O[扩大验证:运行当前文件内所有测试];
O --> P{是否全部通过?};
P -->|否, 引入新问题| F;
end
subgraph "阶段四:总结"
P -->|是| Q[完成:撰写修复总结];
end
D --> F;
```
### 修复完成后的总结
测试修复完成后,应该提供简要说明,包括:
1. **错误原因分析**: 说明测试失败的根本原因
- 测试逻辑错误
- 实现代码bug
- 环境配置问题
- 依赖变更导致的问题
2. **修复方法说明**: 简述采用的修复方式
- 修改了哪些文件
- 采用了什么解决方案
- 为什么选择这种修复方式
**示例格式**:
```markdown
## 测试修复总结
**错误原因**: 测试中的 mock 数据格式与实际 API 返回格式不匹配,导致断言失败。
**修复方法**: 更新了测试文件中的 mock 数据结构,使其与最新的 API 响应格式保持一致。具体修改了 `user.test.ts` 中的 `mockUserData` 对象结构。
```
## 📂 测试文件组织
### 文件命名约定
- **客户端测试**: `*.test.ts`, `*.test.tsx` (任意位置)
- **服务端测试**: `src/database/models/**/*.test.ts`, `src/database/server/**/*.test.ts` (限定路径)
### 测试文件组织风格
项目采用 **测试文件与源文件同目录** 的组织风格:
- 测试文件放在对应源文件的同一目录下
- 命名格式:`原文件名.test.ts` 或 `原文件名.test.tsx`
例如:
```
src/components/Button/
├── index.tsx # 源文件
└── index.test.tsx # 测试文件
```
## 🛠️ 测试调试技巧
### 运行失败测试的步骤
1. **确定测试类型**: 查看文件路径确定使用哪个配置
2. **运行单个测试**: 使用 `-t` 参数隔离问题
3. **检查错误日志**: 仔细阅读错误信息和堆栈跟踪
4. **查看最近修改记录**: 检查相关文件的最近变更情况
5. **添加调试日志**: 在测试中添加 `console.log` 了解执行流程
### Electron IPC 接口测试策略 🖥️
对于涉及 Electron IPC 接口的测试,由于提供真实的 Electron 环境比较复杂,采用 **Mock 返回值** 的方式进行测试。
#### 基本 Mock 设置
```typescript
import { vi } from "vitest";
import { electronIpcClient } from "@/server/modules/ElectronIPCClient";
// Mock Electron IPC 客户端
vi.mock("@/server/modules/ElectronIPCClient", () => ({
electronIpcClient: {
getFilePathById: vi.fn(),
deleteFiles: vi.fn(),
// 根据需要添加其他 IPC 方法
},
}));
```
#### 在测试中设置 Mock 行为
```typescript
beforeEach(() => {
// 重置所有 Mock
vi.resetAllMocks();
// 设置默认的 Mock 返回值
vi.mocked(electronIpcClient.getFilePathById).mockResolvedValue(
"/path/to/file.txt"
);
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
success: true,
});
});
```
#### 测试不同场景的示例
```typescript
it("应该处理文件删除成功的情况", async () => {
// 设置成功场景的 Mock
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
success: true,
});
const result = await service.deleteFiles(["desktop://file1.txt"]);
expect(electronIpcClient.deleteFiles).toHaveBeenCalledWith([
"desktop://file1.txt",
]);
expect(result.success).toBe(true);
});
it("应该处理文件删除失败的情况", async () => {
// 设置失败场景的 Mock
vi.mocked(electronIpcClient.deleteFiles).mockRejectedValue(
new Error("删除失败")
);
const result = await service.deleteFiles(["desktop://file1.txt"]);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
});
```
#### Mock 策略的优势
1. **环境简化**: 避免了复杂的 Electron 环境搭建
2. **测试可控**: 可以精确控制 IPC 调用的返回值和行为
3. **场景覆盖**: 容易测试各种成功/失败场景
4. **执行速度**: Mock 调用比真实 IPC 调用更快
#### 注意事项
- **Mock 准确性**: 确保 Mock 的行为与真实 IPC 接口行为一致
- **类型安全**: 使用 `vi.mocked()` 确保类型安全
- **Mock 重置**: 在 `beforeEach` 中重置 Mock 状态,避免测试间干扰
- **调用验证**: 不仅要验证返回值,还要验证 IPC 方法是否被正确调用
### 检查最近修改记录 🔍
为了更好地判断测试失败的根本原因,需要**系统性地检查相关文件的修改历史**。这是问题定位的关键步骤。
#### 第一步:确定需要检查的文件范围
1. **测试文件本身**: `path/to/component.test.ts`
2. **对应的实现文件**: `path/to/component.ts` 或 `path/to/component/index.ts`
3. **相关依赖文件**: 测试或实现中导入的其他模块
#### 第二步:检查当前工作目录状态
```bash
# 查看所有未提交的修改状态
git status
# 重点关注测试文件和实现文件是否有未提交的修改
git status | grep -E "(test|spec)"
```
#### 第三步:检查未提交的修改内容
```bash
# 查看测试文件的未提交修改 (工作区 vs 暂存区)
git diff path/to/component.test.ts | cat
# 查看对应实现文件的未提交修改
git diff path/to/component.ts | cat
# 查看已暂存但未提交的修改
git diff --cached path/to/component.test.ts | cat
git diff --cached path/to/component.ts | cat
```
#### 第四步:检查提交历史和时间相关性
**首先查看提交时间,判断修改的时效性**:
```bash
# 查看测试文件的最近提交历史,包含提交时间
git log --pretty=format:"%h %ad %s" --date=relative -5 path/to/component.test.ts | cat
# 查看实现文件的最近提交历史,包含提交时间
git log --pretty=format:"%h %ad %s" --date=relative -5 path/to/component.ts | cat
# 查看详细的提交时间(ISO格式,便于精确判断)
git log --pretty=format:"%h %ad %an %s" --date=iso -3 path/to/component.ts | cat
git log --pretty=format:"%h %ad %an %s" --date=iso -3 path/to/component.test.ts | cat
```
**判断提交的参考价值**
1. **最近提交(24小时内)**: 🔴 **高度相关** - 很可能是导致测试失败的直接原因
2. **近期提交(1-7天内)**: 🟡 **中等相关** - 可能相关,需要仔细分析修改内容
3. **较早提交(超过1周)**: ⚪ **低相关性** - 除非是重大重构,否则不太可能是直接原因
#### 第五步:基于时间相关性查看具体修改内容
**根据提交时间的远近,优先查看最近的修改**:
```bash
# 如果有24小时内的提交,重点查看这些修改
git show HEAD -- path/to/component.test.ts | cat
git show HEAD -- path/to/component.ts | cat
# 查看次新的提交(如果最新提交时间较远)
git show HEAD~1 -- path/to/component.ts | cat
git show <recent-commit-hash> -- path/to/component.ts | cat
# 对比最近两次提交的差异
git diff HEAD~1 HEAD -- path/to/component.ts | cat
```
#### 第六步:分析修改与测试失败的关系
基于修改记录和时间相关性判断:
1. **最近修改了实现代码**:
```bash
# 重点检查实现逻辑的变化
git diff HEAD~1 path/to/component.ts | cat
```
- 很可能是实现代码的变更导致测试失败
- 检查实现逻辑是否正确
- 确认测试是否需要相应更新
2. **最近修改了测试代码**:
```bash
# 重点检查测试逻辑的变化
git diff HEAD~1 path/to/component.test.ts | cat
```
- 可能是测试本身写错了
- 检查测试逻辑和断言是否正确
- 确认测试是否符合实现的预期行为
3. **两者都有最近修改**:
```bash
# 对比两个文件的修改时间
git log --pretty=format:"%ad %f" --date=iso -1 path/to/component.ts | cat
git log --pretty=format:"%ad %f" --date=iso -1 path/to/component.test.ts | cat
```
- 需要综合分析两者的修改
- 确定哪个修改更可能导致问题
- 优先检查时间更近的修改
4. **都没有最近修改**:
- 可能是依赖变更或环境问题
- 检查 `package.json`、配置文件等的修改
- 查看是否有全局性的代码重构
#### 修改记录检查示例
```bash
# 完整的检查流程示例
echo "=== 检查文件修改状态 ==="
git status | grep component
echo "=== 检查未提交修改 ==="
git diff src/components/Button/index.test.tsx | cat
git diff src/components/Button/index.tsx | cat
echo "=== 检查提交历史和时间 ==="
git log --pretty=format:"%h %ad %s" --date=relative -3 src/components/Button/index.test.tsx | cat
git log --pretty=format:"%h %ad %s" --date=relative -3 src/components/Button/index.tsx | cat
echo "=== 根据时间优先级查看修改内容 ==="
# 如果有24小时内的提交,重点查看
git show HEAD -- src/components/Button/index.tsx | cat
```
## 🗃️ 数据库 Model 测试指南
### 测试环境选择 💡
数据库 Model 层通过环境变量控制数据库类型,在两种测试环境下有不同的数据库后端:客户端环境 (PGLite) 和 服务端环境 (PostgreSQL)
### ⚠️ 双环境验证要求
**对于所有 Model 测试,必须在两个环境下都验证通过**:
#### 完整验证流程
```bash
# 1. 先在客户端环境测试(快速验证)
npx vitest run --config vitest.config.ts src/database/models/__tests__/myModel.test.ts
# 2. 再在服务端环境测试(兼容性验证)
npx vitest run --config vitest.config.server.ts src/database/models/__tests__/myModel.test.ts
```
### 创建新 Model 测试的最佳实践 📋
#### 1. 参考现有实现和测试模板
创建新 Model 测试前,**必须先参考现有的实现模式**:
- **Model 实现参考**:
- **测试模板参考**:
- **复杂示例参考**:
#### 2. 用户权限检查 - 安全第一 🔒
这是**最关键的安全要求**。所有涉及用户数据的操作都必须包含用户权限检查:
**❌ 错误示例 - 存在安全漏洞**:
```typescript
// 危险:缺少用户权限检查,任何用户都能操作任何数据
update = async (id: string, data: Partial<MyModel>) => {
return this.db
.update(myTable)
.set(data)
.where(eq(myTable.id, id)) // ❌ 只检查 ID,没有检查 userId
.returning();
};
```
**✅ 正确示例 - 安全的实现**:
```typescript
// 安全:必须同时匹配 ID 和 userId
update = async (id: string, data: Partial<MyModel>) => {
return this.db
.update(myTable)
.set(data)
.where(
and(
eq(myTable.id, id),
eq(myTable.userId, this.userId) // ✅ 用户权限检查
)
)
.returning();
};
```
**必须进行用户权限检查的方法**
- `update()` - 更新操作
- `delete()` - 删除操作
- `findById()` - 查找特定记录
- 任何涉及特定记录的查询或修改操作
#### 3. 测试文件结构和必测场景
**基本测试结构**:
```typescript
// @vitest-environment node
describe("MyModel", () => {
describe("create", () => {
it("should create a new record");
it("should handle edge cases");
});
describe("queryAll", () => {
it("should return records for current user only");
it("should handle empty results");
});
describe("update", () => {
it("should update own records");
it("should NOT update other users records"); // 🔒 安全测试
});
describe("delete", () => {
it("should delete own records");
it("should NOT delete other users records"); // 🔒 安全测试
});
describe("user isolation", () => {
it("should enforce user data isolation"); // 🔒 核心安全测试
});
});
```
**必须测试的安全场景** 🔒:
```typescript
it("should not update records of other users", async () => {
// 创建其他用户的记录
const [otherUserRecord] = await serverDB
.insert(myTable)
.values({ userId: "other-user", data: "original" })
.returning();
// 尝试更新其他用户的记录
const result = await myModel.update(otherUserRecord.id, { data: "hacked" });
// 应该返回 undefined 或空数组(因为权限检查失败)
expect(result).toBeUndefined();
// 验证原始数据未被修改
const unchanged = await serverDB.query.myTable.findFirst({
where: eq(myTable.id, otherUserRecord.id),
});
expect(unchanged?.data).toBe("original"); // 数据应该保持不变
});
```
#### 4. Mock 外部依赖服务
如果 Model 依赖外部服务(如 FileService),需要正确 Mock
**设置 Mock**:
```typescript
// 在文件顶部设置 Mock
const mockGetFullFileUrl = vi.fn();
vi.mock("@/server/services/file", () => ({
FileService: vi.fn().mockImplementation(() => ({
getFullFileUrl: mockGetFullFileUrl,
})),
}));
// 在 beforeEach 中重置和配置 Mock
beforeEach(async () => {
vi.clearAllMocks();
mockGetFullFileUrl.mockImplementation(
(url: string) => `https://example.com/${url}`
);
});
```
**验证 Mock 调用**:
```typescript
it("should process URLs through FileService", async () => {
// ... 测试逻辑
// 验证 Mock 被正确调用
expect(mockGetFullFileUrl).toHaveBeenCalledWith("expected-url");
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
});
```
#### 5. 数据库状态管理
**正确的数据清理模式**:
```typescript
const userId = "test-user";
const otherUserId = "other-user";
beforeEach(async () => {
// 清理用户表(级联删除相关数据)
await serverDB.delete(users);
// 创建测试用户
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
});
afterEach(async () => {
// 清理测试数据
await serverDB.delete(users);
});
```
#### 6. 测试数据类型和外键约束处理 ⚠️
**必须使用 Schema 导出的类型**:
```typescript
// ✅ 正确:使用 schema 导出的类型
import { NewGenerationBatch, NewGeneration } from '../../schemas';
const testBatch: NewGenerationBatch = {
userId,
generationTopicId: 'test-topic-id',
provider: 'test-provider',
model: 'test-model',
prompt: 'Test prompt for image generation',
width: 1024,
height: 1024,
config: { /* ... */ },
};
const testGeneration: NewGeneration = {
id: 'test-gen-id',
generationBatchId: 'test-batch-id',
asyncTaskId: null, // 处理外键约束
fileId: null, // 处理外键约束
seed: 12345,
userId,
};
```
```typescript
// ❌ 错误:没有类型声明或使用错误类型
const testBatch = { // 缺少类型声明
generationTopicId: 'test-topic-id',
// ...
};
const testGeneration = { // 缺少类型声明
asyncTaskId: 'invalid-uuid', // 外键约束错误
fileId: 'non-existent-file', // 外键约束错误
// ...
};
```
**外键约束处理策略**:
1. **使用 null 值**: 对于可选的外键字段,使用 null 避免约束错误
2. **创建关联记录**: 如果需要测试关联关系,先创建被引用的记录
3. **理解约束关系**: 了解哪些字段有外键约束,避免引用不存在的记录
```typescript
// 外键约束处理示例
beforeEach(async () => {
// 清理数据库
await serverDB.delete(users);
// 创建测试用户
await serverDB.insert(users).values([{ id: userId }]);
// 如果需要测试文件关联,创建文件记录
if (needsFileAssociation) {
await serverDB.insert(files).values({
id: 'test-file-id',
userId,
name: 'test.jpg',
url: 'test-url',
size: 1024,
fileType: 'image/jpeg',
});
}
});
```
**排序测试的可预测性**:
```typescript
// ✅ 正确:使用明确的时间戳确保排序结果可预测
it('should find batches by topic id in correct order', async () => {
const oldDate = new Date('2024-01-01T10:00:00Z');
const newDate = new Date('2024-01-02T10:00:00Z');
const batch1 = { ...testBatch, prompt: 'First batch', userId, createdAt: oldDate };
const batch2 = { ...testBatch, prompt: 'Second batch', userId, createdAt: newDate };
await serverDB.insert(generationBatches).values([batch1, batch2]);
const results = await generationBatchModel.findByTopicId(testTopic.id);
expect(results[0].prompt).toBe('Second batch'); // 最新优先 (desc order)
expect(results[1].prompt).toBe('First batch');
});
```
```typescript
// ❌ 错误:依赖数据库的默认时间戳,结果不可预测
it('should find batches by topic id', async () => {
const batch1 = { ...testBatch, prompt: 'First batch', userId };
const batch2 = { ...testBatch, prompt: 'Second batch', userId };
await serverDB.insert(generationBatches).values([batch1, batch2]);
// 插入顺序和数据库时间戳可能不一致,导致测试不稳定
const results = await generationBatchModel.findByTopicId(testTopic.id);
expect(results[0].prompt).toBe('Second batch'); // 可能失败
});
```
### 常见问题和解决方案 💡
#### 问题 1:权限检查缺失导致安全漏洞
**现象**: 测试失败,用户能修改其他用户的数据
**解决**: 在 Model 的 `update` 和 `delete` 方法中添加 `and(eq(table.id, id), eq(table.userId, this.userId))`
#### 问题 2:Mock 未生效或验证失败
**现象**: `undefined is not a spy` 错误
**解决**: 检查 Mock 设置位置和方式,确保在测试文件顶部设置,在 `beforeEach` 中重置
#### 问题 3:测试数据污染
**现象**: 测试间相互影响,结果不稳定
**解决**: 在 `beforeEach` 和 `afterEach` 中正确清理数据库状态
#### 问题 4:外部依赖导致测试失败
**现象**: 因为真实的外部服务调用导致测试不稳定
**解决**: Mock 所有外部依赖,使测试更可控和快速
#### 问题 5:外键约束违反导致测试失败
**现象**: `insert or update on table "xxx" violates foreign key constraint`
**解决**:
- 将可选外键字段设为 `null` 而不是无效的字符串值
- 或者先创建被引用的记录,再创建当前记录
```typescript
// ❌ 错误:无效的外键值
const testData = {
asyncTaskId: 'invalid-uuid', // 表中不存在此记录
fileId: 'non-existent-file', // 表中不存在此记录
};
// ✅ 正确:使用 null 值
const testData = {
asyncTaskId: null, // 避免外键约束
fileId: null, // 避免外键约束
};
// ✅ 或者:先创建被引用的记录
beforeEach(async () => {
const [asyncTask] = await serverDB.insert(asyncTasks).values({
id: 'valid-task-id',
status: 'pending',
type: 'generation',
}).returning();
const testData = {
asyncTaskId: asyncTask.id, // 使用有效的外键值
};
});
```
#### 问题 6:排序测试结果不一致
**现象**: 相同的测试有时通过,有时失败,特别是涉及排序的测试
**解决**: 使用明确的时间戳,不要依赖数据库的默认时间戳
```typescript
// ❌ 错误:依赖插入顺序和默认时间戳
await serverDB.insert(table).values([data1, data2]); // 时间戳不可预测
// ✅ 正确:明确指定时间戳
const oldDate = new Date('2024-01-01T10:00:00Z');
const newDate = new Date('2024-01-02T10:00:00Z');
await serverDB.insert(table).values([
{ ...data1, createdAt: oldDate },
{ ...data2, createdAt: newDate },
]);
```
#### 问题 7:Mock 验证失败或调用次数不匹配
**现象**: `expect(mockFunction).toHaveBeenCalledWith(...)` 失败
**解决**:
- 检查 Mock 函数的实际调用参数和期望参数是否完全匹配
- 确认 Mock 在正确的时机被重置和配置
- 使用 `toHaveBeenCalledTimes()` 验证调用次数
```typescript
// 在 beforeEach 中正确配置 Mock
beforeEach(() => {
vi.clearAllMocks(); // 重置所有 Mock
mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
mockTransformGeneration.mockResolvedValue({
id: 'test-id',
// ... 其他字段
});
});
// 测试中验证 Mock 调用
it('should call FileService with correct parameters', async () => {
await model.someMethod();
// 验证调用参数
expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
// 验证调用次数
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
});
```
### Model 测试检查清单 ✅
创建 Model 测试时,请确保以下各项都已完成:
#### 🔧 基础配置
- [ ] **双环境验证** - 在客户端环境 (vitest.config.ts) 和服务端环境 (vitest.config.server.ts) 下都测试通过
- [ ] 参考了 `_template.ts` 和现有 Model 的实现模式
- [ ] **使用正确的 Schema 类型** - 测试数据使用 `NewXxx` 类型声明,如 `NewGenerationBatch`、`NewGeneration`
#### 🔒 安全测试
- [ ] **所有涉及用户数据的操作都包含用户权限检查**
- [ ] 包含了用户权限隔离的安全测试
- [ ] 测试了用户无法访问其他用户数据的场景
#### 🗃️ 数据处理
- [ ] **正确处理外键约束** - 使用 `null` 值或先创建被引用记录
- [ ] **排序测试使用明确时间戳** - 不依赖数据库默认时间,确保结果可预测
- [ ] 在 `beforeEach` 和 `afterEach` 中正确管理数据库状态
- [ ] 所有测试都能独立运行且互不干扰
#### 🎭 Mock 和外部依赖
- [ ] 正确 Mock 了外部依赖服务 (如 FileService、GenerationModel)
- [ ] 在 `beforeEach` 中重置和配置 Mock
- [ ] 验证了 Mock 服务的调用参数和次数
- [ ] 测试了外部服务错误场景的处理
#### 📋 测试覆盖
- [ ] 测试覆盖了所有主要方法 (create, query, update, delete)
- [ ] 测试了边界条件和错误场景
- [ ] 包含了空结果处理的测试
- [ ] **确认两个环境下的测试结果一致**
#### 🚨 常见问题检查
- [ ] 没有外键约束违反错误
- [ ] 排序测试结果稳定可预测
- [ ] Mock 验证无失败
- [ ] 无测试数据污染问题
### 安全警告 ⚠️
**数据库 Model 层是安全的第一道防线**。如果 Model 层缺少用户权限检查:
1. **任何用户都能访问和修改其他用户的数据**
2. **即使上层有权限检查,也可能被绕过**
3. **可能导致严重的数据泄露和安全事故**
因此,**每个涉及用户数据的 Model 方法都必须包含用户权限检查,且必须有对应的安全测试来验证这些检查的有效性**。
## 🎯 总结
修复测试时,记住以下关键点:
- **使用正确的命令**: `npx vitest run --config [config-file]`
- **理解测试意图**: 先读懂测试再修复
- **查看最近修改**: 检查相关文件的 git 修改记录,判断问题根源
- **选择正确环境**: 客户端测试用 `vitest.config.ts`,服务端用 `vitest.config.server.ts`
- **专注单一问题**: 只修复当前的测试失败
- **验证修复结果**: 确保修复后测试通过且无副作用
- **提供修复总结**: 说明错误原因和修复方法
- **Model 测试安全第一**: 必须包含用户权限检查和对应的安全测试
- **Model 双环境验证**: 必须在 PGLite 和 PostgreSQL 两个环境下都验证通过
@@ -0,0 +1,453 @@
---
globs: src/database/**/*.test.ts
alwaysApply: false
---
## 🗃️ 数据库 Model 测试指南
### 测试环境选择 💡
数据库 Model 层通过环境变量控制数据库类型,在两种测试环境下有不同的数据库后端:客户端环境 (PGLite) 和 服务端环境 (PostgreSQL)
### ⚠️ 双环境验证要求
**对于所有 Model 测试,必须在两个环境下都验证通过**:
#### 完整验证流程
```bash
# 1. 先在客户端环境测试(快速验证)
npx vitest run --config vitest.config.ts src/database/models/__tests__/myModel.test.ts
# 2. 再在服务端环境测试(兼容性验证)
npx vitest run --config vitest.config.server.ts src/database/models/__tests__/myModel.test.ts
```
### 创建新 Model 测试的最佳实践 📋
#### 1. 参考现有实现和测试模板
创建新 Model 测试前,**必须先参考现有的实现模式**:
- **Model 实现参考**:
- **测试模板参考**:
- **复杂示例参考**:
#### 2. 用户权限检查 - 安全第一 🔒
这是**最关键的安全要求**。所有涉及用户数据的操作都必须包含用户权限检查:
**❌ 错误示例 - 存在安全漏洞**:
```typescript
// 危险:缺少用户权限检查,任何用户都能操作任何数据
update = async (id: string, data: Partial<MyModel>) => {
return this.db
.update(myTable)
.set(data)
.where(eq(myTable.id, id)) // ❌ 只检查 ID,没有检查 userId
.returning();
};
```
**✅ 正确示例 - 安全的实现**:
```typescript
// 安全:必须同时匹配 ID 和 userId
update = async (id: string, data: Partial<MyModel>) => {
return this.db
.update(myTable)
.set(data)
.where(
and(
eq(myTable.id, id),
eq(myTable.userId, this.userId), // ✅ 用户权限检查
),
)
.returning();
};
```
**必须进行用户权限检查的方法**
- `update()` - 更新操作
- `delete()` - 删除操作
- `findById()` - 查找特定记录
- 任何涉及特定记录的查询或修改操作
#### 3. 测试文件结构和必测场景
**基本测试结构**:
```typescript
// @vitest-environment node
describe('MyModel', () => {
describe('create', () => {
it('should create a new record');
it('should handle edge cases');
});
describe('queryAll', () => {
it('should return records for current user only');
it('should handle empty results');
});
describe('update', () => {
it('should update own records');
it('should NOT update other users records'); // 🔒 安全测试
});
describe('delete', () => {
it('should delete own records');
it('should NOT delete other users records'); // 🔒 安全测试
});
describe('user isolation', () => {
it('should enforce user data isolation'); // 🔒 核心安全测试
});
});
```
**必须测试的安全场景** 🔒:
```typescript
it('should not update records of other users', async () => {
// 创建其他用户的记录
const [otherUserRecord] = await serverDB
.insert(myTable)
.values({ userId: 'other-user', data: 'original' })
.returning();
// 尝试更新其他用户的记录
const result = await myModel.update(otherUserRecord.id, { data: 'hacked' });
// 应该返回 undefined 或空数组(因为权限检查失败)
expect(result).toBeUndefined();
// 验证原始数据未被修改
const unchanged = await serverDB.query.myTable.findFirst({
where: eq(myTable.id, otherUserRecord.id),
});
expect(unchanged?.data).toBe('original'); // 数据应该保持不变
});
```
#### 4. Mock 外部依赖服务
如果 Model 依赖外部服务(如 FileService),需要正确 Mock
**设置 Mock**:
```typescript
// 在文件顶部设置 Mock
const mockGetFullFileUrl = vi.fn();
vi.mock('@/server/services/file', () => ({
FileService: vi.fn().mockImplementation(() => ({
getFullFileUrl: mockGetFullFileUrl,
})),
}));
// 在 beforeEach 中重置和配置 Mock
beforeEach(async () => {
vi.clearAllMocks();
mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
});
```
**验证 Mock 调用**:
```typescript
it('should process URLs through FileService', async () => {
// ... 测试逻辑
// 验证 Mock 被正确调用
expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
});
```
#### 5. 数据库状态管理
**正确的数据清理模式**:
```typescript
const userId = 'test-user';
const otherUserId = 'other-user';
beforeEach(async () => {
// 清理用户表(级联删除相关数据)
await serverDB.delete(users);
// 创建测试用户
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
});
afterEach(async () => {
// 清理测试数据
await serverDB.delete(users);
});
```
#### 6. 测试数据类型和外键约束处理 ⚠️
**必须使用 Schema 导出的类型**:
```typescript
// ✅ 正确:使用 schema 导出的类型
import { NewGeneration, NewGenerationBatch } from '../../schemas';
const testBatch: NewGenerationBatch = {
userId,
generationTopicId: 'test-topic-id',
provider: 'test-provider',
model: 'test-model',
prompt: 'Test prompt for image generation',
width: 1024,
height: 1024,
config: {
/* ... */
},
};
const testGeneration: NewGeneration = {
id: 'test-gen-id',
generationBatchId: 'test-batch-id',
asyncTaskId: null, // 处理外键约束
fileId: null, // 处理外键约束
seed: 12345,
userId,
};
```
```typescript
// ❌ 错误:没有类型声明或使用错误类型
const testBatch = {
// 缺少类型声明
generationTopicId: 'test-topic-id',
// ...
};
const testGeneration = {
// 缺少类型声明
asyncTaskId: 'invalid-uuid', // 外键约束错误
fileId: 'non-existent-file', // 外键约束错误
// ...
};
```
**外键约束处理策略**:
1. **使用 null 值**: 对于可选的外键字段,使用 null 避免约束错误
2. **创建关联记录**: 如果需要测试关联关系,先创建被引用的记录
3. **理解约束关系**: 了解哪些字段有外键约束,避免引用不存在的记录
```typescript
// 外键约束处理示例
beforeEach(async () => {
// 清理数据库
await serverDB.delete(users);
// 创建测试用户
await serverDB.insert(users).values([{ id: userId }]);
// 如果需要测试文件关联,创建文件记录
if (needsFileAssociation) {
await serverDB.insert(files).values({
id: 'test-file-id',
userId,
name: 'test.jpg',
url: 'test-url',
size: 1024,
fileType: 'image/jpeg',
});
}
});
```
**排序测试的可预测性**:
```typescript
// ✅ 正确:使用明确的时间戳确保排序结果可预测
it('should find batches by topic id in correct order', async () => {
const oldDate = new Date('2024-01-01T10:00:00Z');
const newDate = new Date('2024-01-02T10:00:00Z');
const batch1 = { ...testBatch, prompt: 'First batch', userId, createdAt: oldDate };
const batch2 = { ...testBatch, prompt: 'Second batch', userId, createdAt: newDate };
await serverDB.insert(generationBatches).values([batch1, batch2]);
const results = await generationBatchModel.findByTopicId(testTopic.id);
expect(results[0].prompt).toBe('Second batch'); // 最新优先 (desc order)
expect(results[1].prompt).toBe('First batch');
});
```
```typescript
// ❌ 错误:依赖数据库的默认时间戳,结果不可预测
it('should find batches by topic id', async () => {
const batch1 = { ...testBatch, prompt: 'First batch', userId };
const batch2 = { ...testBatch, prompt: 'Second batch', userId };
await serverDB.insert(generationBatches).values([batch1, batch2]);
// 插入顺序和数据库时间戳可能不一致,导致测试不稳定
const results = await generationBatchModel.findByTopicId(testTopic.id);
expect(results[0].prompt).toBe('Second batch'); // 可能失败
});
```
### 常见问题和解决方案 💡
#### 问题 1:权限检查缺失导致安全漏洞
**现象**: 测试失败,用户能修改其他用户的数据 **解决**: 在 Model 的 `update` 和 `delete` 方法中添加 `and(eq(table.id, id), eq(table.userId, this.userId))`
#### 问题 2:Mock 未生效或验证失败
**现象**: `undefined is not a spy` 错误 **解决**: 检查 Mock 设置位置和方式,确保在测试文件顶部设置,在 `beforeEach` 中重置
#### 问题 3:测试数据污染
**现象**: 测试间相互影响,结果不稳定 **解决**: 在 `beforeEach` 和 `afterEach` 中正确清理数据库状态
#### 问题 4:外部依赖导致测试失败
**现象**: 因为真实的外部服务调用导致测试不稳定 **解决**: Mock 所有外部依赖,使测试更可控和快速
#### 问题 5:外键约束违反导致测试失败
**现象**: `insert or update on table "xxx" violates foreign key constraint` **解决**:
- 将可选外键字段设为 `null` 而不是无效的字符串值
- 或者先创建被引用的记录,再创建当前记录
```typescript
// ❌ 错误:无效的外键值
const testData = {
asyncTaskId: 'invalid-uuid', // 表中不存在此记录
fileId: 'non-existent-file', // 表中不存在此记录
};
// ✅ 正确:使用 null 值
const testData = {
asyncTaskId: null, // 避免外键约束
fileId: null, // 避免外键约束
};
// ✅ 或者:先创建被引用的记录
beforeEach(async () => {
const [asyncTask] = await serverDB.insert(asyncTasks).values({
id: 'valid-task-id',
status: 'pending',
type: 'generation',
}).returning();
const testData = {
asyncTaskId: asyncTask.id, // 使用有效的外键值
};
});
```
#### 问题 6:排序测试结果不一致
**现象**: 相同的测试有时通过,有时失败,特别是涉及排序的测试 **解决**: 使用明确的时间戳,不要依赖数据库的默认时间戳
```typescript
// ❌ 错误:依赖插入顺序和默认时间戳
await serverDB.insert(table).values([data1, data2]); // 时间戳不可预测
// ✅ 正确:明确指定时间戳
const oldDate = new Date('2024-01-01T10:00:00Z');
const newDate = new Date('2024-01-02T10:00:00Z');
await serverDB.insert(table).values([
{ ...data1, createdAt: oldDate },
{ ...data2, createdAt: newDate },
]);
```
#### 问题 7:Mock 验证失败或调用次数不匹配
**现象**: `expect(mockFunction).toHaveBeenCalledWith(...)` 失败 **解决**:
- 检查 Mock 函数的实际调用参数和期望参数是否完全匹配
- 确认 Mock 在正确的时机被重置和配置
- 使用 `toHaveBeenCalledTimes()` 验证调用次数
```typescript
// 在 beforeEach 中正确配置 Mock
beforeEach(() => {
vi.clearAllMocks(); // 重置所有 Mock
mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
mockTransformGeneration.mockResolvedValue({
id: 'test-id',
// ... 其他字段
});
});
// 测试中验证 Mock 调用
it('should call FileService with correct parameters', async () => {
await model.someMethod();
// 验证调用参数
expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
// 验证调用次数
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
});
```
### Model 测试检查清单 ✅
创建 Model 测试时,请确保以下各项都已完成:
#### 🔧 基础配置
- [ ] **双环境验证** - 在客户端环境 (vitest.config.ts) 和服务端环境 (vitest.config.server.ts) 下都测试通过
- [ ] 参考了 `_template.ts` 和现有 Model 的实现模式
- [ ] **使用正确的 Schema 类型** - 测试数据使用 `NewXxx` 类型声明,如 `NewGenerationBatch`、`NewGeneration`
#### 🔒 安全测试
- [ ] **所有涉及用户数据的操作都包含用户权限检查**
- [ ] 包含了用户权限隔离的安全测试
- [ ] 测试了用户无法访问其他用户数据的场景
#### 🗃️ 数据处理
- [ ] **正确处理外键约束** - 使用 `null` 值或先创建被引用记录
- [ ] **排序测试使用明确时间戳** - 不依赖数据库默认时间,确保结果可预测
- [ ] 在 `beforeEach` 和 `afterEach` 中正确管理数据库状态
- [ ] 所有测试都能独立运行且互不干扰
#### 🎭 Mock 和外部依赖
- [ ] 正确 Mock 了外部依赖服务 (如 FileService、GenerationModel)
- [ ] 在 `beforeEach` 中重置和配置 Mock
- [ ] 验证了 Mock 服务的调用参数和次数
- [ ] 测试了外部服务错误场景的处理
#### 📋 测试覆盖
- [ ] 测试覆盖了所有主要方法 (create, query, update, delete)
- [ ] 测试了边界条件和错误场景
- [ ] 包含了空结果处理的测试
- [ ] **确认两个环境下的测试结果一致**
#### 🚨 常见问题检查
- [ ] 没有外键约束违反错误
- [ ] 排序测试结果稳定可预测
- [ ] Mock 验证无失败
- [ ] 无测试数据污染问题
### 安全警告 ⚠️
**数据库 Model 层是安全的第一道防线**。如果 Model 层缺少用户权限检查:
1. **任何用户都能访问和修改其他用户的数据**
2. **即使上层有权限检查,也可能被绕过**
3. **可能导致严重的数据泄露和安全事故**
因此,**每个涉及用户数据的 Model 方法都必须包含用户权限检查,且必须有对应的安全测试来验证这些检查的有效性**。
@@ -0,0 +1,80 @@
---
description: Electron IPC 接口测试策略
alwaysApply: false
---
### Electron IPC 接口测试策略 🖥️
对于涉及 Electron IPC 接口的测试,由于提供真实的 Electron 环境比较复杂,采用 **Mock 返回值** 的方式进行测试。
#### 基本 Mock 设置
```typescript
import { vi } from 'vitest';
import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
// Mock Electron IPC 客户端
vi.mock('@/server/modules/ElectronIPCClient', () => ({
electronIpcClient: {
getFilePathById: vi.fn(),
deleteFiles: vi.fn(),
// 根据需要添加其他 IPC 方法
},
}));
```
#### 在测试中设置 Mock 行为
```typescript
beforeEach(() => {
// 重置所有 Mock
vi.resetAllMocks();
// 设置默认的 Mock 返回值
vi.mocked(electronIpcClient.getFilePathById).mockResolvedValue('/path/to/file.txt');
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
success: true,
});
});
```
#### 测试不同场景的示例
```typescript
it('应该处理文件删除成功的情况', async () => {
// 设置成功场景的 Mock
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
success: true,
});
const result = await service.deleteFiles(['desktop://file1.txt']);
expect(electronIpcClient.deleteFiles).toHaveBeenCalledWith(['desktop://file1.txt']);
expect(result.success).toBe(true);
});
it('应该处理文件删除失败的情况', async () => {
// 设置失败场景的 Mock
vi.mocked(electronIpcClient.deleteFiles).mockRejectedValue(new Error('删除失败'));
const result = await service.deleteFiles(['desktop://file1.txt']);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
});
```
#### Mock 策略的优势
1. **环境简化**: 避免了复杂的 Electron 环境搭建
2. **测试可控**: 可以精确控制 IPC 调用的返回值和行为
3. **场景覆盖**: 容易测试各种成功/失败场景
4. **执行速度**: Mock 调用比真实 IPC 调用更快
#### 注意事项
- **Mock 准确性**: 确保 Mock 的行为与真实 IPC 接口行为一致
- **类型安全**: 使用 `vi.mocked()` 确保类型安全
- **Mock 重置**: 在 `beforeEach` 中重置 Mock 状态,避免测试间干扰
- **调用验证**: 不仅要验证返回值,还要验证 IPC 方法是否被正确调用
@@ -0,0 +1,496 @@
---
globs: *.test.ts,*.test.tsx
alwaysApply: false
---
# 测试指南 - LobeChat Testing Guide
## 🧪 测试环境概览
LobeChat 项目使用 Vitest 测试库,配置了两种不同的测试环境:
### 客户端测试环境 (DOM Environment)
- **配置文件**: [vitest.config.ts](mdc:vitest.config.ts)
- **环境**: Happy DOM (浏览器环境模拟)
- **数据库**: PGLite (浏览器环境的 PostgreSQL)
- **用途**: 测试前端组件、客户端逻辑、React 组件等
- **设置文件**: [tests/setup.ts](mdc:tests/setup.ts)
### 服务端测试环境 (Node Environment)
- **配置文件**: [vitest.config.server.ts](mdc:vitest.config.server.ts)
- **环境**: Node.js
- **数据库**: 真实的 PostgreSQL 数据库
- **并发限制**: 单线程运行 (`singleFork: true`)
- **用途**: 测试数据库模型、服务端逻辑、API 端点等
- **设置文件**: [tests/setup-db.ts](mdc:tests/setup-db.ts)
## 🚀 测试运行命令
**🚨 性能警告**: 项目包含 3000+ 测试用例,完整运行需要约 10 分钟。务必使用文件过滤或测试名称过滤。
### ✅ 正确的命令格式
```bash
# 运行所有客户端/服务端测试
npx vitest run --config vitest.config.ts # 客户端测试
npx vitest run --config vitest.config.server.ts # 服务端测试
# 运行特定测试文件 (支持模糊匹配)
npx vitest run --config vitest.config.ts user.test.ts
# 运行特定测试用例名称 (使用 -t 参数)
npx vitest run --config vitest.config.ts -t "test case name"
# 组合使用文件和测试名称过滤
npx vitest run --config vitest.config.ts filename.test.ts -t "specific test"
# 生成覆盖率报告 (使用 --coverage 参数)
npx vitest run --config vitest.config.ts --coverage
```
### ❌ 避免的命令格式
```bash
# ❌ 这些命令会运行所有 3000+ 测试用例,耗时约 10 分钟!
npm test
npm test some-file.test.ts
# ❌ 不要使用裸 vitest (会进入 watch 模式)
vitest test-file.test.ts
```
## 🔧 测试修复原则
### 核心原则 ⚠️
1. **充分阅读测试代码**: 在修复测试之前,必须完整理解测试的意图和实现
2. **测试优先修复**: 如果是测试本身写错了,修改测试而不是实现代码
3. **专注单一问题**: 只修复指定的测试,不要添加额外测试或功能
4. **不自作主张**: 不要因为发现其他问题就直接修改,先提出再讨论
### 测试协作最佳实践 🤝
基于实际开发经验总结的重要协作原则:
#### 1. 失败处理策略
**核心原则**: 避免盲目重试,快速识别问题并寻求帮助。
- **失败阈值**: 当连续尝试修复测试 1-2 次都失败后,应立即停止继续尝试
- **问题总结**: 分析失败原因,整理已尝试的解决方案及其失败原因
- **寻求帮助**: 带着清晰的问题摘要和尝试记录向团队寻求帮助
- **避免陷阱**: 不要陷入"不断尝试相同或类似方法"的循环
```typescript
// ❌ 错误做法:连续失败后继续盲目尝试
// 第3次、第4次仍在用相似的方法修复同一个问题
// ✅ 正确做法:失败1-2次后总结问题
/*
问题总结:
1. 尝试过的方法:修改 mock 数据结构
2. 失败原因:仍然提示类型不匹配
3. 具体错误:Expected 'UserData' but received 'UserProfile'
4. 需要帮助:不确定最新的 UserData 接口定义
*/
```
#### 2. 测试用例命名规范
**核心原则**: 测试应该关注"行为",而不是"实现细节"。
- **描述业务场景**: `describe` 和 `it` 的标题应该描述具体的业务场景和预期行为
- **避免实现绑定**: 不要在测试名称中提及具体的代码行号、覆盖率目标或实现细节
- **保持稳定性**: 测试名称应该在代码重构后仍然有意义
```typescript
// ❌ 错误的测试命名
describe('User component coverage', () => {
it('covers line 45-50 in getUserData', () => {
// 为了覆盖第45-50行而写的测试
});
it('tests the else branch', () => {
// 仅为了测试某个分支而存在
});
});
// ✅ 正确的测试命名
describe('<UserAvatar />', () => {
it('should render fallback icon when image url is not provided', () => {
// 测试具体的业务场景,自然会覆盖相关代码分支
});
it('should display user initials when avatar image fails to load', () => {
// 描述用户行为和预期结果
});
});
```
**覆盖率提升的正确思路**:
- ✅ 通过设计各种业务场景(正常流程、边缘情况、错误处理)来自然提升覆盖率
- ❌ 不要为了达到覆盖率数字而写测试,更不要在测试中注释"为了覆盖 xxx 行"
#### 3. 测试组织结构
**核心原则**: 维护清晰的测试层次结构,避免冗余的顶级测试块。
- **复用现有结构**: 添加新测试时,优先在现有的 `describe` 块中寻找合适的位置
- **逻辑分组**: 相关的测试用例应该组织在同一个 `describe` 块内
- **避免碎片化**: 不要为了单个测试用例就创建新的顶级 `describe` 块
```typescript
// ❌ 错误的组织方式:创建过多顶级块
describe('<UserProfile />', () => {
it('should render user name', () => {});
});
describe('UserProfile new prop test', () => {
// 不必要的新块
it('should handle email display', () => {});
});
describe('UserProfile edge cases', () => {
// 不必要的新块
it('should handle missing avatar', () => {});
});
// ✅ 正确的组织方式:合并相关测试
describe('<UserProfile />', () => {
it('should render user name', () => {});
it('should handle email display', () => {});
it('should handle missing avatar', () => {});
describe('when user data is incomplete', () => {
// 只有在有多个相关子场景时才创建子组
it('should show placeholder for missing name', () => {});
it('should hide email section when email is undefined', () => {});
});
});
```
**组织决策流程**:
1. 是否存在逻辑相关的现有 `describe` 块? → 如果有,添加到其中
2. 是否有多个(3个以上)相关的测试用例? → 如果有,可以考虑创建新的子 `describe`
3. 是否是独立的、无关联的功能模块? → 如果是,才考虑创建新的顶级 `describe`
### 测试修复流程
1. **复现问题**: 定位并运行失败的测试,确认能在本地复现
2. **分析原因**: 阅读测试代码、错误日志和相关文件的 Git 修改历史
3. **建立假设**: 判断问题出在测试逻辑、实现代码还是环境配置
4. **修复验证**: 根据假设进行修复,重新运行测试确认通过
5. **扩大验证**: 运行当前文件内所有测试,确保没有引入新问题
6. **撰写总结**: 说明错误原因和修复方法
### 修复完成后的总结
测试修复完成后,应该提供简要说明,包括:
1. **错误原因分析**: 说明测试失败的根本原因
- 测试逻辑错误
- 实现代码bug
- 环境配置问题
- 依赖变更导致的问题
2. **修复方法说明**: 简述采用的修复方式
- 修改了哪些文件
- 采用了什么解决方案
- 为什么选择这种修复方式
**示例格式**:
```markdown
## 测试修复总结
**错误原因**: 测试中的 mock 数据格式与实际 API 返回格式不匹配,导致断言失败。
**修复方法**: 更新了测试文件中的 mock 数据结构,使其与最新的 API 响应格式保持一致。具体修改了 `user.test.ts` 中的 `mockUserData` 对象结构。
```
## 🎯 测试编写最佳实践
### Mock 数据策略:追求"低成本的真实性" 📋
**核心原则**: 测试数据应默认追求真实性,只有在引入"高昂的测试成本"时才进行简化。
#### 什么是"高昂的测试成本"?
"高成本"指的是测试中引入了外部依赖,使测试变慢、不稳定或复杂:
- **文件 I/O 操作**:读写硬盘文件
- **网络请求**:HTTP 调用、数据库连接
- **系统调用**:获取系统时间、环境变量等
#### ✅ 推荐做法:Mock 依赖,保留真实数据
```typescript
// ✅ 好的做法:Mock I/O 操作,但使用真实的文件内容格式
describe('parseContentType', () => {
beforeEach(() => {
// Mock 文件读取操作(避免真实 I/O)
vi.spyOn(fs, 'readFileSync').mockImplementation((path) => {
// 但返回真实的文件内容格式
if (path.includes('.pdf')) return '%PDF-1.4\n%âãÏÓ'; // 真实 PDF 文件头
if (path.includes('.png')) return '\x89PNG\r\n\x1a\n'; // 真实 PNG 文件头
return '';
});
});
it('should detect PDF content type correctly', () => {
const result = parseContentType('/path/to/file.pdf');
expect(result).toBe('application/pdf');
});
});
// ❌ 过度简化:使用不真实的数据
describe('parseContentType', () => {
it('should detect PDF content type correctly', () => {
// 这种简化数据没有测试价值
const result = parseContentType('fake-pdf-content');
expect(result).toBe('application/pdf');
});
});
```
#### 🎯 真实标识符的价值
```typescript
// ✅ 使用真实的提供商标识符
it('should parse OpenAI model list correctly', () => {
const result = parseModelString('openai', '+gpt-4,+gpt-3.5-turbo');
expect(result.add).toHaveLength(2);
expect(result.add[0].id).toBe('gpt-4');
});
// ❌ 使用占位符标识符(价值较低)
it('should parse model list correctly', () => {
const result = parseModelString('test-provider', '+model1,+model2');
expect(result.add).toHaveLength(2);
// 这种测试对理解真实场景帮助不大
});
```
### 错误处理测试:测试"行为"而非"文本" ⚠️
**核心原则**: 测试应该验证程序在错误发生时的行为是可预测的,而不是验证易变的错误信息文本。
#### ✅ 推荐的错误测试方式
```typescript
// ✅ 测试是否抛出错误
it('should throw error when invalid input provided', () => {
expect(() => processInput(null)).toThrow();
});
// ✅ 测试错误类型(最推荐)
it('should throw ValidationError for invalid data', () => {
expect(() => validateUser({})).toThrow(ValidationError);
});
// ✅ 测试错误属性而非消息文本
it('should throw error with correct error code', () => {
expect(() => processPayment({})).toThrow(
expect.objectContaining({
code: 'INVALID_PAYMENT_DATA',
statusCode: 400,
}),
);
});
```
#### ❌ 应避免的做法
```typescript
// ❌ 过度依赖具体错误信息文本
it('should throw specific error message', () => {
expect(() => processUser({})).toThrow('用户数据不能为空,请检查输入参数');
// 这种测试很脆弱,错误文案稍有修改就会失败
});
```
#### 🎯 例外情况:何时可以测试错误信息
```typescript
// ✅ 测试标准 API 错误(这是契约的一部分)
it('should return proper HTTP error for API', () => {
expect(response.statusCode).toBe(400);
expect(response.error).toBe('Bad Request');
});
// ✅ 测试错误信息的关键部分(使用正则)
it('should include field name in validation error', () => {
expect(() => validateField('email', '')).toThrow(/email/i);
});
```
### 疑难解答:警惕模块污染 🚨
**识别信号**: 当你的测试出现以下"灵异"现象时,优先怀疑模块污染:
- 单独运行某个测试通过,但和其他测试一起运行就失败
- 测试的执行顺序影响结果
- Mock 设置看起来正确,但实际使用的是旧的 Mock 版本
#### 典型场景:动态 Mock 同一模块
```typescript
// ❌ 容易出现模块污染的写法
describe('ConfigService', () => {
it('should work in development mode', async () => {
vi.doMock('./config', () => ({ isDev: true }));
const { getSettings } = await import('./configService'); // 第一次加载
expect(getSettings().debugMode).toBe(true);
});
it('should work in production mode', async () => {
vi.doMock('./config', () => ({ isDev: false }));
const { getSettings } = await import('./configService'); // 可能使用缓存的旧版本!
expect(getSettings().debugMode).toBe(false); // ❌ 可能失败
});
});
// ✅ 使用 resetModules 解决模块污染
describe('ConfigService', () => {
beforeEach(() => {
vi.resetModules(); // 清除模块缓存,确保每个测试都是干净的环境
});
it('should work in development mode', async () => {
vi.doMock('./config', () => ({ isDev: true }));
const { getSettings } = await import('./configService');
expect(getSettings().debugMode).toBe(true);
});
it('should work in production mode', async () => {
vi.doMock('./config', () => ({ isDev: false }));
const { getSettings } = await import('./configService');
expect(getSettings().debugMode).toBe(false); // ✅ 测试通过
});
});
```
#### 🔧 排查和解决步骤
1. **识别问题**: 测试失败时,首先问自己:"是否有多个测试在 Mock 同一个模块?"
2. **添加隔离**: 在 `beforeEach` 中添加 `vi.resetModules()`
3. **验证修复**: 重新运行测试,确认问题解决
**记住**: `vi.resetModules()` 是解决测试"灵异"失败的终极武器,当常规调试方法都无效时,它往往能一针见血地解决问题。
## 📂 测试文件组织
### 文件命名约定
- **客户端测试**: `*.test.ts`, `*.test.tsx` (任意位置)
- **服务端测试**: `src/database/models/**/*.test.ts`, `src/database/server/**/*.test.ts` (限定路径)
### 测试文件组织风格
项目采用 **测试文件与源文件同目录** 的组织风格:
- 测试文件放在对应源文件的同一目录下
- 命名格式:`原文件名.test.ts` 或 `原文件名.test.tsx`
例如:
```plaintext
src/components/Button/
├── index.tsx # 源文件
└── index.test.tsx # 测试文件
```
## 🛠️ 测试调试技巧
### 测试调试步骤
1. **确定测试环境**: 根据文件路径选择正确的配置文件
2. **隔离问题**: 使用 `-t` 参数只运行失败的测试用例
3. **分析错误**: 仔细阅读错误信息、堆栈跟踪和最近的文件修改记录
4. **添加调试**: 在测试中添加 `console.log` 了解执行流程
### TypeScript 类型处理 📝
在测试中,为了提高编写效率和可读性,可以适当放宽 TypeScript 类型检测:
#### ✅ 推荐的类型放宽策略
```typescript
// ✅ 使用非空断言访问测试中确定存在的属性
const result = await someFunction();
expect(result!.data).toBeDefined();
expect(result!.status).toBe('success');
// ✅ 使用 any 类型简化复杂的 Mock 设置
const mockStream = new ReadableStream() as any;
mockStream.toReadableStream = () => mockStream;
```
#### 🎯 适用场景
- **Mock 对象**: 对于测试用的 Mock 数据,使用 `as any` 避免复杂的类型定义
- **第三方库**: 处理复杂的第三方库类型时,适当使用 `any` 提高效率
- **测试断言**: 在确定对象存在的测试场景中,使用 `!` 非空断言
- **临时调试**: 快速编写测试时,先用 `any` 保证功能,后续可选择性地优化类型
#### ⚠️ 注意事项
- **适度使用**: 不要过度依赖 `any`,核心业务逻辑的类型仍应保持严格
- **文档说明**: 对于使用 `any` 的复杂场景,添加注释说明原因
- **测试覆盖**: 确保即使使用了 `any`,测试仍能有效验证功能正确性
### 检查最近修改记录 🔍
系统性地检查相关文件的修改历史是问题定位的关键步骤。
#### 三步检查法
**Step 1: 查看当前状态**
```bash
git status # 查看未提交的修改
git diff path/to/component.test.ts | cat # 查看测试文件修改
git diff path/to/component.ts | cat # 查看实现文件修改
```
**Step 2: 查看提交历史**
```bash
git log --pretty=format:"%h %ad %s" --date=relative -3 path/to/component.ts | cat
```
**Step 3: 查看具体修改内容**
```bash
git show HEAD -- path/to/component.ts | cat # 查看最新提交的修改
```
#### 时间相关性判断
- **24小时内的提交**: 🔴 **高度相关** - 很可能是直接原因
- **1-7天内的提交**: 🟡 **中等相关** - 需要仔细分析
- **超过1周的提交**: ⚪ **低相关性** - 除非重大重构
## 特殊场景的测试
针对一些特殊场景的测试,需要阅读相关 rules:
- [Electron IPC 接口测试策略](mdc:./electron-ipc-test.mdc)
- [数据库 Model 测试指南](mdc:./db-model-test.mdc)
## 🎯 核心要点
- **命令格式**: 使用 `npx vitest run --config [config-file]` 并指定文件过滤
- **修复原则**: 失败1-2次后寻求帮助,测试命名关注行为而非实现细节
- **调试流程**: 复现 → 分析 → 假设 → 修复 → 验证 → 总结
- **文件组织**: 优先在现有 `describe` 块中添加测试,避免创建冗余顶级块
- **数据策略**: 默认追求真实性,只有高成本(I/O、网络等)时才简化
- **错误测试**: 测试错误类型和行为,避免依赖具体的错误信息文本
- **模块污染**: 测试"灵异"失败时,优先怀疑模块污染,使用 `vi.resetModules()` 解决
- **安全要求**: Model 测试必须包含权限检查,并在双环境下验证通过
+10
View File
@@ -36,6 +36,16 @@ config.overrides = [
'mdx/code-blocks': false,
},
},
{
files: ['src/store/image/**/*', 'src/types/generation/**/*'],
rules: {
'@typescript-eslint/no-empty-interface': 0,
'sort-keys-fix/sort-keys-fix': 0,
'typescript-sort-keys/interface': 0,
'typescript-sort-keys/string-enum': 0,
},
},
];
module.exports = config;
+3 -3
View File
@@ -155,9 +155,9 @@ jobs:
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }}
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }}
# 将 TEMP 和 TMP 目录设置到 D
TEMP: D:\temp
TMP: D:\temp
# 将 TEMP 和 TMP 目录设置到 C
TEMP: C:\temp
TMP: C:\temp
# Linux 平台构建处理
- name: Build artifact on Linux
+3 -3
View File
@@ -139,9 +139,9 @@ jobs:
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }}
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }}
# 将 TEMP 和 TMP 目录设置到 D
TEMP: D:\temp
TMP: D:\temp
# 将 TEMP 和 TMP 目录设置到 C
TEMP: C:\temp
TMP: C:\temp
# Linux 平台构建处理
- name: Build artifact on Linux
+5
View File
@@ -43,6 +43,7 @@ test-output
# misc
# add other ignore file below
CLAUDE.md
# local env files
.env*.local
@@ -71,3 +72,7 @@ public/swe-worker*
vertex-ai-key.json
.pnpm-store
./packages/lobe-ui
# for local prd docs
docs/prd
+694
View File
@@ -2,6 +2,700 @@
# Changelog
### [Version 1.102.2](https://github.com/lobehub/lobe-chat/compare/v1.102.1...v1.102.2)
<sup>Released on **2025-07-22**</sup>
#### 💄 Styles
- **misc**: Add notification for desktop.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Add notification for desktop, closes [#8523](https://github.com/lobehub/lobe-chat/issues/8523) ([4917d17](https://github.com/lobehub/lobe-chat/commit/4917d17))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.102.1](https://github.com/lobehub/lobe-chat/compare/v1.102.0...v1.102.1)
<sup>Released on **2025-07-21**</sup>
#### 🐛 Bug Fixes
- **groq**: Enable streaming for tool calls and add Kimi K2 model.
#### 💄 Styles
- **misc**: Modal list header sticky style.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **groq**: Enable streaming for tool calls and add Kimi K2 model, closes [#8510](https://github.com/lobehub/lobe-chat/issues/8510) ([60739bc](https://github.com/lobehub/lobe-chat/commit/60739bc))
#### Styles
- **misc**: Modal list header sticky style, closes [#8514](https://github.com/lobehub/lobe-chat/issues/8514) ([75273d5](https://github.com/lobehub/lobe-chat/commit/75273d5))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.102.0](https://github.com/lobehub/lobe-chat/compare/v1.101.2...v1.102.0)
<sup>Released on **2025-07-21**</sup>
#### ✨ Features
- **misc**: Add image generation capabilities using Google AI Imagen API.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Add image generation capabilities using Google AI Imagen API, closes [#8503](https://github.com/lobehub/lobe-chat/issues/8503) ([cef8208](https://github.com/lobehub/lobe-chat/commit/cef8208))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.101.2](https://github.com/lobehub/lobe-chat/compare/v1.101.1...v1.101.2)
<sup>Released on **2025-07-21**</sup>
#### 💄 Styles
- **misc**: Fix lobehub provider `/chat` in desktop.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Fix lobehub provider `/chat` in desktop, closes [#8508](https://github.com/lobehub/lobe-chat/issues/8508) ([c801f9c](https://github.com/lobehub/lobe-chat/commit/c801f9c))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.101.1](https://github.com/lobehub/lobe-chat/compare/v1.101.0...v1.101.1)
<sup>Released on **2025-07-19**</sup>
#### 🐛 Bug Fixes
- **misc**: Try fix authorization code exchange & pin next-auto to `beta.29`.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Try fix authorization code exchange & pin next-auto to `beta.29`, closes [#8496](https://github.com/lobehub/lobe-chat/issues/8496) ([27c4881](https://github.com/lobehub/lobe-chat/commit/27c4881))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.101.0](https://github.com/lobehub/lobe-chat/compare/v1.100.2...v1.101.0)
<sup>Released on **2025-07-19**</sup>
#### ✨ Features
- **misc**: Add zhipu cogview4.
#### 🐛 Bug Fixes
- **misc**: Some ai image bugs.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Add zhipu cogview4, closes [#8486](https://github.com/lobehub/lobe-chat/issues/8486) ([0b1557d](https://github.com/lobehub/lobe-chat/commit/0b1557d))
#### What's fixed
- **misc**: Some ai image bugs, closes [#8490](https://github.com/lobehub/lobe-chat/issues/8490) ([5d852be](https://github.com/lobehub/lobe-chat/commit/5d852be))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.100.2](https://github.com/lobehub/lobe-chat/compare/v1.100.1...v1.100.2)
<sup>Released on **2025-07-18**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix webapi proxy with clerk.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix webapi proxy with clerk, closes [#8479](https://github.com/lobehub/lobe-chat/issues/8479) ([7dd65f0](https://github.com/lobehub/lobe-chat/commit/7dd65f0))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.100.1](https://github.com/lobehub/lobe-chat/compare/v1.100.0...v1.100.1)
<sup>Released on **2025-07-17**</sup>
#### 🐛 Bug Fixes
- **misc**: Use server env config image models.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Use server env config image models, closes [#8478](https://github.com/lobehub/lobe-chat/issues/8478) ([768ee2b](https://github.com/lobehub/lobe-chat/commit/768ee2b))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.100.0](https://github.com/lobehub/lobe-chat/compare/v1.99.6...v1.100.0)
<sup>Released on **2025-07-17**</sup>
#### ✨ Features
- **misc**: Refactor desktop oauth and use JWTs token to support remote chat.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Refactor desktop oauth and use JWTs token to support remote chat, closes [#8446](https://github.com/lobehub/lobe-chat/issues/8446) ([054ca5f](https://github.com/lobehub/lobe-chat/commit/054ca5f))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.99.6](https://github.com/lobehub/lobe-chat/compare/v1.99.5...v1.99.6)
<sup>Released on **2025-07-16**</sup>
#### 🐛 Bug Fixes
- **misc**: Desktop local db can't upload image.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Desktop local db can't upload image, closes [#8459](https://github.com/lobehub/lobe-chat/issues/8459) ([25bfc80](https://github.com/lobehub/lobe-chat/commit/25bfc80))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.99.5](https://github.com/lobehub/lobe-chat/compare/v1.99.4...v1.99.5)
<sup>Released on **2025-07-16**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix page error when url is not defined in web search plugin.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix page error when url is not defined in web search plugin, closes [#8441](https://github.com/lobehub/lobe-chat/issues/8441) ([a55b65b](https://github.com/lobehub/lobe-chat/commit/a55b65b))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.99.4](https://github.com/lobehub/lobe-chat/compare/v1.99.3...v1.99.4)
<sup>Released on **2025-07-16**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix apikey issue on server log.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix apikey issue on server log, closes [#8457](https://github.com/lobehub/lobe-chat/issues/8457) ([43be2d1](https://github.com/lobehub/lobe-chat/commit/43be2d1))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.99.3](https://github.com/lobehub/lobe-chat/compare/v1.99.2...v1.99.3)
<sup>Released on **2025-07-16**</sup>
#### 🐛 Bug Fixes
- **misc**: Chat model list should not show image model.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Chat model list should not show image model, closes [#8448](https://github.com/lobehub/lobe-chat/issues/8448) ([2bb1506](https://github.com/lobehub/lobe-chat/commit/2bb1506))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.99.2](https://github.com/lobehub/lobe-chat/compare/v1.99.1...v1.99.2)
<sup>Released on **2025-07-15**</sup>
#### 🐛 Bug Fixes
- **misc**: Some ai image generation feedback issues.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Some ai image generation feedback issues, closes [#8440](https://github.com/lobehub/lobe-chat/issues/8440) ([bc41329](https://github.com/lobehub/lobe-chat/commit/bc41329))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.99.1](https://github.com/lobehub/lobe-chat/compare/v1.99.0...v1.99.1)
<sup>Released on **2025-07-15**</sup>
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.99.0](https://github.com/lobehub/lobe-chat/compare/v1.98.2...v1.99.0)
<sup>Released on **2025-07-14**</sup>
#### ✨ Features
- **plugin**: Support Streamable HTTP MCP Server Auth.
- **misc**: support AI Image.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **plugin**: Support Streamable HTTP MCP Server Auth, closes [#8425](https://github.com/lobehub/lobe-chat/issues/8425) ([853a09a](https://github.com/lobehub/lobe-chat/commit/853a09a))
- **misc**: support AI Image, closes [#8312](https://github.com/lobehub/lobe-chat/issues/8312) ([095de57](https://github.com/lobehub/lobe-chat/commit/095de57))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.98.2](https://github.com/lobehub/lobe-chat/compare/v1.98.1...v1.98.2)
<sup>Released on **2025-07-14**</sup>
#### 💄 Styles
- **misc**: Update i18n.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Update i18n, closes [#8422](https://github.com/lobehub/lobe-chat/issues/8422) ([5b89ec8](https://github.com/lobehub/lobe-chat/commit/5b89ec8))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.98.1](https://github.com/lobehub/lobe-chat/compare/v1.98.0...v1.98.1)
<sup>Released on **2025-07-14**</sup>
#### 💄 Styles
- **misc**: Fix discover translation.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Fix discover translation, closes [#8423](https://github.com/lobehub/lobe-chat/issues/8423) ([15ae35c](https://github.com/lobehub/lobe-chat/commit/15ae35c))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 1.98.0](https://github.com/lobehub/lobe-chat/compare/v1.97.17...v1.98.0)
<sup>Released on **2025-07-13**</sup>
#### ✨ Features
- **misc**: Add network proxy for desktop.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Add network proxy for desktop, closes [#7848](https://github.com/lobehub/lobe-chat/issues/7848) ([46d2509](https://github.com/lobehub/lobe-chat/commit/46d2509))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.97.17](https://github.com/lobehub/lobe-chat/compare/v1.97.16...v1.97.17)
<sup>Released on **2025-07-13**</sup>
#### 💄 Styles
- **misc**: Support Hunyuan A13B thinking model.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Support Hunyuan A13B thinking model, closes [#8278](https://github.com/lobehub/lobe-chat/issues/8278) ([09ca978](https://github.com/lobehub/lobe-chat/commit/09ca978))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.97.16](https://github.com/lobehub/lobe-chat/compare/v1.97.15...v1.97.16)
<sup>Released on **2025-07-13**</sup>
#### 💄 Styles
- **misc**: Update i18n.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Update i18n, closes [#8410](https://github.com/lobehub/lobe-chat/issues/8410) ([2515875](https://github.com/lobehub/lobe-chat/commit/2515875))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.97.15](https://github.com/lobehub/lobe-chat/compare/v1.97.14...v1.97.15)
<sup>Released on **2025-07-12**</sup>
#### 🐛 Bug Fixes
- **misc**: Add vision support to Grok 4.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Add vision support to Grok 4, closes [#8386](https://github.com/lobehub/lobe-chat/issues/8386) ([8512f5a](https://github.com/lobehub/lobe-chat/commit/8512f5a))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.97.14](https://github.com/lobehub/lobe-chat/compare/v1.97.13...v1.97.14)
<sup>Released on **2025-07-12**</sup>
#### 🐛 Bug Fixes
- **misc**: Revert "💄 style: Open new topic by tap Just Chat again".
#### 💄 Styles
- **misc**: Add Kimi K2 model.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Revert "💄 style: Open new topic by tap Just Chat again", closes [#8402](https://github.com/lobehub/lobe-chat/issues/8402) ([55462b9](https://github.com/lobehub/lobe-chat/commit/55462b9))
#### Styles
- **misc**: Add Kimi K2 model, closes [#8401](https://github.com/lobehub/lobe-chat/issues/8401) ([4cb1a18](https://github.com/lobehub/lobe-chat/commit/4cb1a18))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.97.13](https://github.com/lobehub/lobe-chat/compare/v1.97.12...v1.97.13)
<sup>Released on **2025-07-12**</sup>
#### 💄 Styles
- **misc**: Support new Doubao thinking models, update i18n.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Support new Doubao thinking models, closes [#8174](https://github.com/lobehub/lobe-chat/issues/8174) ([637d75c](https://github.com/lobehub/lobe-chat/commit/637d75c))
- **misc**: Update i18n, closes [#8400](https://github.com/lobehub/lobe-chat/issues/8400) ([790eeb8](https://github.com/lobehub/lobe-chat/commit/790eeb8))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.97.12](https://github.com/lobehub/lobe-chat/compare/v1.97.11...v1.97.12)
<sup>Released on **2025-07-11**</sup>
#### 🐛 Bug Fixes
- **misc**: Grok-4 reasoning model universal matching.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Grok-4 reasoning model universal matching, closes [#8390](https://github.com/lobehub/lobe-chat/issues/8390) ([d6f17f8](https://github.com/lobehub/lobe-chat/commit/d6f17f8))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.97.11](https://github.com/lobehub/lobe-chat/compare/v1.97.10...v1.97.11)
<sup>Released on **2025-07-11**</sup>
#### 💄 Styles
- **misc**: Open new topic by tap Just Chat again.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Open new topic by tap Just Chat again, closes [#8311](https://github.com/lobehub/lobe-chat/issues/8311) ([7e2f4ce](https://github.com/lobehub/lobe-chat/commit/7e2f4ce))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.97.10](https://github.com/lobehub/lobe-chat/compare/v1.97.9...v1.97.10)
<sup>Released on **2025-07-11**</sup>
#### 💄 Styles
- **misc**: Update i18n.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Update i18n, closes [#8387](https://github.com/lobehub/lobe-chat/issues/8387) ([00215c0](https://github.com/lobehub/lobe-chat/commit/00215c0))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
### [Version 1.97.9](https://github.com/lobehub/lobe-chat/compare/v1.97.8...v1.97.9)
<sup>Released on **2025-07-10**</sup>
+2
View File
@@ -53,6 +53,8 @@ ENV NEXT_PUBLIC_SENTRY_DSN="${NEXT_PUBLIC_SENTRY_DSN}" \
SENTRY_ORG="" \
SENTRY_PROJECT=""
ENV APP_URL="http://app.com"
# Posthog
ENV NEXT_PUBLIC_ANALYTICS_POSTHOG="${NEXT_PUBLIC_ANALYTICS_POSTHOG}" \
NEXT_PUBLIC_POSTHOG_HOST="${NEXT_PUBLIC_POSTHOG_HOST}" \
+3 -3
View File
@@ -383,12 +383,12 @@ In addition, these plugins are not limited to news aggregation, but can also ext
| Recent Submits | Description |
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-05-27**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-07-21**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [Speak](https://lobechat.com/discover/plugin/speak)<br/><sup>By **speak** on **2025-07-18**</sup> | Learn how to say anything in another language with Speak, your AI-powered language tutor.<br/>`education` `language` |
| [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` |
| [Google CSE](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | Searches Google through their official CSE API.<br/>`web` `search` |
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
> 📊 Total plugins: [<kbd>**43**</kbd>](https://lobechat.com/discover/plugins)
<!-- PLUGIN LIST -->
+3 -3
View File
@@ -376,12 +376,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
| 最近新增 | 描述 |
| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-05-27**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-07-21**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [Speak](https://lobechat.com/discover/plugin/speak)<br/><sup>By **speak** on **2025-07-18**</sup> | 使用 Speak,您的 AI 语言导师,学习如何用另一种语言说任何事情。<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` |
| [谷歌自定义搜索引擎](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | 通过他们的官方自定义搜索引擎 API 搜索谷歌。<br/>`网络` `搜索` |
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
> 📊 Total plugins: [<kbd>**43**</kbd>](https://lobechat.com/discover/plugins)
<!-- PLUGIN LIST -->
+2
View File
@@ -1,4 +1,6 @@
lockfile=false
shamefully-hoist=true
ignore-workspace-root-check=true
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
+7
View File
@@ -24,6 +24,13 @@ LobeHub Desktop 是 [LobeChat](https://github.com/lobehub/lobe-chat) 的跨平
pnpm install-isolated
```
### 配置环境变量
复制 `.env.desktop``.env`
> [!WARNING]
> 注意提前备份好 `.env` 文件,避免丢失配置。
### 开发模式运行
```bash
+5
View File
@@ -25,6 +25,11 @@ const config = {
artifactName: '${productName}-${version}.${ext}',
},
asar: true,
asarUnpack: [
// https://github.com/electron-userland/electron-builder/issues/9001#issuecomment-2778802044
'**/node_modules/sharp/**/*',
'**/node_modules/@img/**/*',
],
detectUpdateChannel: true,
directories: {
buildResources: 'build',
+13 -6
View File
@@ -1,6 +1,6 @@
{
"name": "lobehub-desktop-dev",
"version": "0.0.10",
"version": "0.0.0",
"description": "LobeHub Desktop Application",
"homepage": "https://lobehub.com",
"repository": {
@@ -14,6 +14,7 @@
"build-local": "npm run build && electron-builder --dir --config electron-builder.js --c.mac.notarize=false -c.mac.identity=null --c.asar=false",
"build:linux": "npm run build && electron-builder --linux --config electron-builder.js --publish never",
"build:mac": "npm run build && electron-builder --mac --config electron-builder.js --publish never",
"build:mac:local": "npm run build && UPDATE_CHANNEL=nightly electron-builder --mac --config electron-builder.js --publish never",
"build:win": "npm run build && electron-builder --win --config electron-builder.js --publish never",
"electron:dev": "electron-vite dev",
"electron:run-unpack": "electron .",
@@ -24,6 +25,7 @@
"lint": "eslint --cache ",
"pg-server": "bun run scripts/pglite-server.ts",
"start": "electron-vite preview",
"test": "vitest --run",
"typecheck": "tsgo --noEmit -p tsconfig.json"
},
"dependencies": {
@@ -45,10 +47,10 @@
"@types/resolve": "^1.20.6",
"@types/semver": "^7.7.0",
"@types/set-cookie-parser": "^2.4.10",
"@typescript/native-preview": "latest",
"@typescript/native-preview": "7.0.0-dev.20250711.1",
"consola": "^3.1.0",
"cookie": "^1.0.2",
"electron": "^37.2.0",
"electron": "~37.1.0",
"electron-builder": "^26.0.12",
"electron-is": "^3.0.0",
"electron-log": "^5.3.3",
@@ -56,19 +58,24 @@
"electron-vite": "^3.0.0",
"execa": "^9.5.2",
"fix-path": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"just-diff": "^6.0.2",
"lodash": "^4.17.21",
"pglite-server": "^0.1.4",
"lodash-es": "^4.17.21",
"resolve": "^1.22.8",
"semver": "^7.5.4",
"set-cookie-parser": "^2.7.1",
"tsx": "^4.19.3",
"typescript": "^5.7.3",
"vite": "^6.2.5"
"undici": "^7.9.0",
"vite": "^6.3.5",
"vitest": "^3.2.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"electron"
"electron",
"electron-builder"
]
}
}
-14
View File
@@ -1,14 +0,0 @@
import { PGlite } from "@electric-sql/pglite";
import { createServer } from "pglite-server";
// 创建或连接到您现有的 PGlite 数据库
const db = new PGlite("/Users/arvinxx/Library/Application Support/lobehub-desktop/lobehub-local-db");
await db.waitReady;
// 创建服务器并监听端口
const PORT = 6543;
const pgServer = createServer(db);
pgServer.listen(PORT, () => {
console.log(`PGlite 服务器已启动,监听端口 ${PORT}`);
});
+1 -1
View File
@@ -36,7 +36,7 @@ export const appBrowsers = {
autoHideMenuBar: true,
height: 800,
identifier: 'settings',
keepAlive: true,
// keepAlive: true,
minWidth: 600,
parentIdentifier: 'chat',
path: '/settings',
+3
View File
@@ -27,3 +27,6 @@ export const LOCAL_DATABASE_DIR = 'lobehub-local-db';
export const FILE_STORAGE_DIR = 'file-storage';
// Plugin 安装目录
export const INSTALL_PLUGINS_DIR = 'plugins';
// Desktop file service
export const LOCAL_STORAGE_URL_PREFIX = '/lobe-desktop-file';
+12
View File
@@ -1,6 +1,8 @@
/**
* 应用设置存储相关常量
*/
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { appStorageDir } from '@/const/dir';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import { ElectronMainStore } from '@/types/store';
@@ -10,6 +12,15 @@ import { ElectronMainStore } from '@/types/store';
*/
export const STORE_NAME = 'lobehub-settings';
export const defaultProxySettings: NetworkProxySettings = {
enableProxy: false,
proxyBypass: 'localhost, 127.0.0.1, ::1',
proxyPort: '',
proxyRequireAuth: false,
proxyServer: '',
proxyType: 'http',
};
/**
* 存储默认值
*/
@@ -17,6 +28,7 @@ export const STORE_DEFAULTS: ElectronMainStore = {
dataSyncConfig: { storageMode: 'local' },
encryptedTokens: {},
locale: 'auto',
networkProxy: defaultProxySettings,
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
storagePath: appStorageDir,
};
+310 -111
View File
@@ -1,10 +1,9 @@
import { DataSyncConfig } from '@lobechat/electron-client-ipc';
import { BrowserWindow, app, shell } from 'electron';
import { BrowserWindow, shell } from 'electron';
import crypto from 'node:crypto';
import querystring from 'node:querystring';
import { URL } from 'node:url';
import { name } from '@/../../package.json';
import { createLogger } from '@/utils/logger';
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
@@ -13,10 +12,9 @@ import { ControllerModule, ipcClientEvent } from './index';
// Create logger
const logger = createLogger('controllers:AuthCtr');
const protocolPrefix = `com.lobehub.${name}`;
/**
* Authentication Controller
* Used to implement the OAuth authorization flow
* 使用中间页 + 轮询的方式实现 OAuth 授权流程
*/
export default class AuthCtr extends ControllerModule {
/**
@@ -32,9 +30,29 @@ export default class AuthCtr extends ControllerModule {
private codeVerifier: string | null = null;
private authRequestState: string | null = null;
beforeAppReady = () => {
this.registerProtocolHandler();
};
/**
* 轮询相关参数
*/
// eslint-disable-next-line no-undef
private pollingInterval: NodeJS.Timeout | null = null;
private cachedRemoteUrl: string | null = null;
/**
* 自动刷新定时器
*/
// eslint-disable-next-line no-undef
private autoRefreshTimer: NodeJS.Timeout | null = null;
/**
* 构造 redirect_uri,确保授权和令牌交换时使用相同的 URI
* @param remoteUrl 远程服务器 URL
* @param includeHandoffId 是否包含 handoff ID(仅在授权时需要)
*/
private constructRedirectUri(remoteUrl: string): string {
const callbackUrl = new URL('/oidc/callback/desktop', remoteUrl);
return callbackUrl.toString();
}
/**
* Request OAuth authorization
@@ -43,6 +61,9 @@ export default class AuthCtr extends ControllerModule {
async requestAuthorization(config: DataSyncConfig) {
const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl(config);
// 缓存远程服务器 URL 用于后续轮询
this.cachedRemoteUrl = remoteUrl;
logger.info(
`Requesting OAuth authorization, storageMode:${config.storageMode} server URL: ${remoteUrl}`,
);
@@ -57,8 +78,11 @@ export default class AuthCtr extends ControllerModule {
this.authRequestState = crypto.randomBytes(16).toString('hex');
logger.debug(`Generated state parameter: ${this.authRequestState}`);
// Construct authorization URL
// Construct authorization URL with new redirect_uri
const authUrl = new URL('/oidc/auth', remoteUrl);
const redirectUri = this.constructRedirectUri(remoteUrl);
logger.info('redirectUri', redirectUri);
// Add query parameters
authUrl.search = querystring.stringify({
@@ -66,7 +90,9 @@ export default class AuthCtr extends ControllerModule {
code_challenge: codeChallenge,
code_challenge_method: 'S256',
prompt: 'consent',
redirect_uri: `${protocolPrefix}://auth/callback`,
redirect_uri: redirectUri,
// https://github.com/lobehub/lobe-chat/pull/8450
resource: 'urn:lobehub:chat',
response_type: 'code',
scope: 'profile email offline_access',
state: this.authRequestState,
@@ -78,6 +104,9 @@ export default class AuthCtr extends ControllerModule {
await shell.openExternal(authUrl.toString());
logger.debug('Opening authorization URL in default browser');
// Start polling for credentials
this.startPolling();
return { success: true };
} catch (error) {
logger.error('Authorization request failed:', error);
@@ -86,85 +115,188 @@ export default class AuthCtr extends ControllerModule {
}
/**
* Handle authorization callback
* This method is called when the browser redirects to our custom protocol
* 启动轮询机制获取凭证
*/
async handleAuthCallback(callbackUrl: string) {
logger.info(`Handling authorization callback: ${callbackUrl}`);
private startPolling() {
if (!this.authRequestState) {
logger.error('No handoff ID available for polling');
return;
}
logger.info('Starting credential polling');
const pollInterval = 3000; // 3 seconds
const maxPollTime = 5 * 60 * 1000; // 5 minutes
const startTime = Date.now();
this.pollingInterval = setInterval(async () => {
try {
// Check if polling has timed out
if (Date.now() - startTime > maxPollTime) {
logger.warn('Credential polling timed out');
this.stopPolling();
this.broadcastAuthorizationFailed('Authorization timed out');
return;
}
// Poll for credentials
const result = await this.pollForCredentials();
if (result) {
logger.info('Successfully received credentials from polling');
this.stopPolling();
// Validate state parameter
if (result.state !== this.authRequestState) {
logger.error(
`Invalid state parameter: expected ${this.authRequestState}, received ${result.state}`,
);
this.broadcastAuthorizationFailed('Invalid state parameter');
return;
}
// Exchange code for tokens
const exchangeResult = await this.exchangeCodeForToken(result.code, this.codeVerifier!);
if (exchangeResult.success) {
logger.info('Authorization successful');
this.broadcastAuthorizationSuccessful();
} else {
logger.warn(`Authorization failed: ${exchangeResult.error || 'Unknown error'}`);
this.broadcastAuthorizationFailed(exchangeResult.error || 'Unknown error');
}
}
} catch (error) {
logger.error('Error during credential polling:', error);
this.stopPolling();
this.broadcastAuthorizationFailed('Polling error: ' + error.message);
}
}, pollInterval);
}
/**
* 停止轮询
*/
private stopPolling() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = null;
}
}
/**
* 启动自动刷新定时器
*/
private startAutoRefresh() {
// 先停止现有的定时器
this.stopAutoRefresh();
const checkInterval = 2 * 60 * 1000; // 每 2 分钟检查一次
logger.debug('Starting auto-refresh timer');
this.autoRefreshTimer = setInterval(async () => {
try {
// 检查 token 是否即将过期 (提前 5 分钟刷新)
if (this.remoteServerConfigCtr.isTokenExpiringSoon()) {
const expiresAt = this.remoteServerConfigCtr.getTokenExpiresAt();
logger.info(
`Token is expiring soon, triggering auto-refresh. Expires at: ${expiresAt ? new Date(expiresAt).toISOString() : 'unknown'}`,
);
const result = await this.remoteServerConfigCtr.refreshAccessToken();
if (result.success) {
logger.info('Auto-refresh successful');
this.broadcastTokenRefreshed();
} else {
logger.error(`Auto-refresh failed: ${result.error}`);
// 如果自动刷新失败,停止定时器并清除 token
this.stopAutoRefresh();
await this.remoteServerConfigCtr.clearTokens();
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
this.broadcastAuthorizationRequired();
}
}
} catch (error) {
logger.error('Error during auto-refresh check:', error);
}
}, checkInterval);
}
/**
* 停止自动刷新定时器
*/
private stopAutoRefresh() {
if (this.autoRefreshTimer) {
clearInterval(this.autoRefreshTimer);
this.autoRefreshTimer = null;
logger.debug('Stopped auto-refresh timer');
}
}
/**
* 轮询获取凭证
* 直接发送 HTTP 请求到远程服务器
*/
private async pollForCredentials(): Promise<{ code: string; state: string } | null> {
if (!this.authRequestState || !this.cachedRemoteUrl) {
return null;
}
try {
const url = new URL(callbackUrl);
const params = new URLSearchParams(url.search);
// 使用缓存的远程服务器 URL
const remoteUrl = this.cachedRemoteUrl;
// Get authorization code
const code = params.get('code');
const state = params.get('state');
logger.debug(`Got parameters from callback URL: code=${code}, state=${state}`);
// 构造请求 URL
const url = new URL('/oidc/handoff', remoteUrl);
url.searchParams.set('id', this.authRequestState);
url.searchParams.set('client', 'desktop');
// Validate state parameter to prevent CSRF attacks
if (state !== this.authRequestState) {
logger.error(
`Invalid state parameter: expected ${this.authRequestState}, received ${state}`,
);
throw new Error('Invalid state parameter');
}
logger.debug('State parameter validation passed');
logger.debug(`Polling for credentials: ${url.toString()}`);
if (!code) {
logger.error('No authorization code received');
throw new Error('No authorization code received');
// 直接发送 HTTP 请求
const response = await fetch(url.toString(), {
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
});
// 检查响应状态
if (response.status === 404) {
// 凭证还未准备好,这是正常情况
return null;
}
// Get configuration information
const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
logger.debug(`Getting remote server configuration: url=${config.remoteServerUrl}`);
if (config.storageMode === 'selfHost' && !config.remoteServerUrl) {
logger.error('Server URL not configured');
throw new Error('No server URL configured');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Get the previously saved code_verifier
const codeVerifier = this.codeVerifier;
if (!codeVerifier) {
logger.error('Code verifier not found');
throw new Error('No code verifier found');
}
logger.debug('Found code verifier');
// 解析响应数据
const data = (await response.json()) as {
data: {
id: string;
payload: { code: string; state: string };
};
success: boolean;
};
// Exchange authorization code for token
logger.debug('Starting to exchange authorization code for token');
const result = await this.exchangeCodeForToken(code, codeVerifier);
if (result.success) {
logger.info('Authorization successful');
// Notify render process of successful authorization
this.broadcastAuthorizationSuccessful();
} else {
logger.warn(`Authorization failed: ${result.error || 'Unknown error'}`);
// Notify render process of failed authorization
this.broadcastAuthorizationFailed(result.error || 'Unknown error');
if (data.success && data.data?.payload) {
logger.debug('Successfully retrieved credentials from handoff');
return {
code: data.data.payload.code,
state: data.data.payload.state,
};
}
return result;
return null;
} catch (error) {
logger.error('Handling authorization callback failed:', error);
// Notify render process of failed authorization
this.broadcastAuthorizationFailed(error.message);
return { error: error.message, success: false };
} finally {
// Clear authorization request state
logger.debug('Clearing authorization request state');
this.authRequestState = null;
this.codeVerifier = null;
logger.debug('Polling attempt failed (this is normal):', error.message);
return null;
}
}
/**
* Refresh access token
*/
@ipcClientEvent('refreshAccessToken')
async refreshAccessToken() {
logger.info('Starting to refresh access token');
try {
@@ -175,6 +307,8 @@ export default class AuthCtr extends ControllerModule {
logger.info('Token refresh successful via AuthCtr call.');
// Notify render process that token has been refreshed
this.broadcastTokenRefreshed();
// Restart auto-refresh timer with new expiration time
this.startAutoRefresh();
return { success: true };
} else {
// Throw an error to be caught by the catch block below
@@ -188,6 +322,7 @@ export default class AuthCtr extends ControllerModule {
// Refresh failed, clear tokens and disable remote server
logger.warn('Refresh failed, clearing tokens and disabling remote server');
this.stopAutoRefresh();
await this.remoteServerConfigCtr.clearTokens();
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
@@ -198,48 +333,15 @@ export default class AuthCtr extends ControllerModule {
}
}
/**
* Register custom protocol handler
*/
private registerProtocolHandler() {
logger.info(`Registering custom protocol handler ${protocolPrefix}://`);
app.setAsDefaultProtocolClient(protocolPrefix);
// Register custom protocol handler
if (process.platform === 'darwin') {
// Handle open-url event on macOS
logger.debug('Registering open-url event handler for macOS');
app.on('open-url', (event, url) => {
event.preventDefault();
logger.info(`Received open-url event: ${url}`);
this.handleAuthCallback(url);
});
} else {
// Handle protocol callback via second-instance event on Windows and Linux
logger.debug('Registering second-instance event handler for Windows/Linux');
app.on('second-instance', async (event, commandLine) => {
// Find the URL from command line arguments
const url = commandLine.find((arg) => arg.startsWith(`${protocolPrefix}://`));
if (url) {
logger.info(`Found URL from second-instance command line arguments: ${url}`);
const { success } = await this.handleAuthCallback(url);
if (success) {
this.app.browserManager.getMainWindow().show();
}
} else {
logger.warn('Protocol URL not found in second-instance command line arguments');
}
});
}
logger.info(`Registered ${protocolPrefix}:// custom protocol handler`);
}
/**
* Exchange authorization code for token
*/
private async exchangeCodeForToken(code: string, codeVerifier: string) {
const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl();
if (!this.cachedRemoteUrl) {
throw new Error('No cached remote URL available for token exchange');
}
const remoteUrl = this.cachedRemoteUrl;
logger.info('Starting to exchange authorization code for token');
try {
const tokenUrl = new URL('/oidc/token', remoteUrl);
@@ -251,7 +353,7 @@ export default class AuthCtr extends ControllerModule {
code,
code_verifier: codeVerifier,
grant_type: 'authorization_code',
redirect_uri: `${protocolPrefix}://auth/callback`,
redirect_uri: this.constructRedirectUri(remoteUrl),
});
logger.debug('Sending token exchange request');
@@ -272,10 +374,20 @@ export default class AuthCtr extends ControllerModule {
throw new Error(errorMessage);
}
let data;
// Parse response
const data = await response.json();
try {
data = await response.clone().json();
} catch {
const status = response.status;
throw new Error(
`Parse JSON failed, please check your server, response status: ${status}, detail:\n\n ${await response.text()} `,
);
}
logger.debug('Successfully received token exchange response');
// console.log(data); // Keep original log for debugging, or remove/change to logger.debug as needed
// Ensure response contains necessary fields
if (!data.access_token || !data.refresh_token) {
@@ -285,13 +397,20 @@ export default class AuthCtr extends ControllerModule {
// Save tokens
logger.debug('Starting to save exchanged tokens');
await this.remoteServerConfigCtr.saveTokens(data.access_token, data.refresh_token);
await this.remoteServerConfigCtr.saveTokens(
data.access_token,
data.refresh_token,
data.expires_in,
);
logger.info('Successfully saved exchanged tokens');
// Set server to active state
logger.debug(`Setting remote server to active state: ${remoteUrl}`);
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: true });
// Start auto-refresh timer
this.startAutoRefresh();
return { success: true };
} catch (error) {
logger.error('Exchanging authorization code failed:', error);
@@ -390,4 +509,84 @@ export default class AuthCtr extends ControllerModule {
logger.debug('Generated code challenge (partial): ' + challenge.slice(0, 10) + '...'); // Avoid logging full sensitive info
return challenge;
}
/**
* 应用启动后初始化
*/
afterAppReady() {
logger.debug('AuthCtr initialized, checking for existing tokens');
this.initializeAutoRefresh();
}
/**
* 清理所有定时器
*/
cleanup() {
logger.debug('Cleaning up AuthCtr timers');
this.stopPolling();
this.stopAutoRefresh();
}
/**
* 初始化自动刷新功能
* 在应用启动时检查是否有有效的 token,如果有就启动自动刷新定时器
*/
private async initializeAutoRefresh() {
try {
const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
// 检查是否配置了远程服务器且处于活动状态
if (!config.active || !config.remoteServerUrl) {
logger.debug(
'Remote server not active or configured, skipping auto-refresh initialization',
);
return;
}
// 检查是否有有效的访问令牌
const accessToken = await this.remoteServerConfigCtr.getAccessToken();
if (!accessToken) {
logger.debug('No access token found, skipping auto-refresh initialization');
return;
}
// 检查是否有过期时间信息
const expiresAt = this.remoteServerConfigCtr.getTokenExpiresAt();
if (!expiresAt) {
logger.debug('No token expiration time found, skipping auto-refresh initialization');
return;
}
// 检查 token 是否已经过期
const currentTime = Date.now();
if (currentTime >= expiresAt) {
logger.info('Token has expired, attempting to refresh it');
// 尝试刷新 token
const refreshResult = await this.remoteServerConfigCtr.refreshAccessToken();
if (refreshResult.success) {
logger.info('Token refresh successful during initialization');
this.broadcastTokenRefreshed();
// 重新启动自动刷新定时器
this.startAutoRefresh();
return;
} else {
logger.error(`Token refresh failed during initialization: ${refreshResult.error}`);
// 只有在刷新失败时才清除 token 并要求重新授权
await this.remoteServerConfigCtr.clearTokens();
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
this.broadcastAuthorizationRequired();
return;
}
}
// 启动自动刷新定时器
logger.info(
`Token is valid, starting auto-refresh timer. Token expires at: ${new Date(expiresAt).toISOString()}`,
);
this.startAutoRefresh();
} catch (error) {
logger.error('Error during auto-refresh initialization:', error);
}
}
}
@@ -0,0 +1,172 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { merge } from 'lodash';
import { isEqual } from 'lodash-es';
import { defaultProxySettings } from '@/const/store';
import { createLogger } from '@/utils/logger';
import {
ProxyConfigValidator,
ProxyConnectionTester,
ProxyDispatcherManager,
ProxyTestResult,
} from '../modules/networkProxy';
import { ControllerModule, ipcClientEvent } from './index';
// Create logger
const logger = createLogger('controllers:NetworkProxyCtr');
/**
* 网络代理控制器
* 处理桌面应用的网络代理相关功能
*/
export default class NetworkProxyCtr extends ControllerModule {
/**
* 获取代理设置
*/
@ipcClientEvent('getProxySettings')
async getDesktopSettings(): Promise<NetworkProxySettings> {
try {
const settings = this.app.storeManager.get(
'networkProxy',
defaultProxySettings,
) as NetworkProxySettings;
logger.debug('Retrieved proxy settings:', {
enableProxy: settings.enableProxy,
proxyType: settings.proxyType,
});
return settings;
} catch (error) {
logger.error('Failed to get proxy settings:', error);
return defaultProxySettings;
}
}
/**
* 设置代理配置
*/
@ipcClientEvent('setProxySettings')
async setProxySettings(config: NetworkProxySettings): Promise<void> {
try {
// 验证配置
const validation = ProxyConfigValidator.validate(config);
if (!validation.isValid) {
const errorMessage = `Invalid proxy configuration: ${validation.errors.join(', ')}`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
// 获取当前配置
const currentConfig = this.app.storeManager.get(
'networkProxy',
defaultProxySettings,
) as NetworkProxySettings;
// 检查是否有变化
if (isEqual(currentConfig, config)) {
logger.debug('Proxy settings unchanged, skipping update');
return;
}
// 合并配置
const newConfig = merge({}, currentConfig, config);
// 应用代理设置
await ProxyDispatcherManager.applyProxySettings(newConfig);
// 保存到存储
this.app.storeManager.set('networkProxy', newConfig);
logger.info('Proxy settings updated successfully', {
enableProxy: newConfig.enableProxy,
proxyPort: newConfig.proxyPort,
proxyServer: newConfig.proxyServer,
proxyType: newConfig.proxyType,
});
} catch (error) {
logger.error('Failed to update proxy settings:', error);
throw error;
}
}
/**
* 测试代理连接
*/
@ipcClientEvent('testProxyConnection')
async testProxyConnection(url: string): Promise<{ message?: string; success: boolean }> {
try {
const result = await ProxyConnectionTester.testConnection(url);
if (result.success) {
return { success: true };
} else {
throw new Error(result.message || 'Connection test failed');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('Proxy connection test failed:', errorMessage);
throw new Error(`Connection failed: ${errorMessage}`);
}
}
/**
* 测试指定代理配置
*/
@ipcClientEvent('testProxyConfig')
async testProxyConfig({
config,
testUrl,
}: {
config: NetworkProxySettings;
testUrl?: string;
}): Promise<ProxyTestResult> {
try {
return await ProxyConnectionTester.testProxyConfig(config, testUrl);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('Proxy config test failed:', errorMessage);
return {
message: `Proxy config test failed: ${errorMessage}`,
success: false,
};
}
}
/**
* 应用初始代理设置
*/
async beforeAppReady(): Promise<void> {
try {
// 获取存储的代理设置
const networkProxy = this.app.storeManager.get(
'networkProxy',
defaultProxySettings,
) as NetworkProxySettings;
// 验证配置
const validation = ProxyConfigValidator.validate(networkProxy);
if (!validation.isValid) {
logger.warn('Invalid stored proxy configuration, using defaults:', validation.errors);
await ProxyDispatcherManager.applyProxySettings(defaultProxySettings);
return;
}
// 应用代理设置
await ProxyDispatcherManager.applyProxySettings(networkProxy);
logger.info('Initial proxy settings applied successfully', {
enableProxy: networkProxy.enableProxy,
proxyType: networkProxy.proxyType,
});
} catch (error) {
logger.error('Failed to apply initial proxy settings:', error);
// 出错时使用默认设置
try {
await ProxyDispatcherManager.applyProxySettings(defaultProxySettings);
logger.info('Fallback to default proxy settings');
} catch (fallbackError) {
logger.error('Failed to apply fallback proxy settings:', fallbackError);
}
}
}
}
@@ -0,0 +1,156 @@
import {
DesktopNotificationResult,
ShowDesktopNotificationParams,
} from '@lobechat/electron-client-ipc';
import { Notification, app } from 'electron';
import { macOS, windows } from 'electron-is';
import { createLogger } from '@/utils/logger';
import { ControllerModule, ipcClientEvent } from './index';
const logger = createLogger('controllers:NotificationCtr');
export default class NotificationCtr extends ControllerModule {
/**
* 在应用准备就绪后设置桌面通知
*/
afterAppReady() {
this.setupNotifications();
}
/**
* 设置桌面通知权限和配置
*/
private setupNotifications() {
logger.debug('Setting up desktop notifications');
try {
// 检查通知支持
if (!Notification.isSupported()) {
logger.warn('Desktop notifications are not supported on this platform');
return;
}
// 在 macOS 上,我们可能需要显式请求通知权限
if (macOS()) {
logger.debug('macOS detected, notification permissions should be handled by system');
}
// 在 Windows 上设置应用用户模型 ID
if (windows()) {
app.setAppUserModelId('com.lobehub.chat');
logger.debug('Set Windows App User Model ID for notifications');
}
logger.info('Desktop notifications setup completed');
} catch (error) {
logger.error('Failed to setup desktop notifications:', error);
}
}
/**
* 显示系统桌面通知(仅当窗口隐藏时)
*/
@ipcClientEvent('showDesktopNotification')
async showDesktopNotification(
params: ShowDesktopNotificationParams,
): Promise<DesktopNotificationResult> {
logger.debug('收到桌面通知请求:', params);
try {
// 检查通知支持
if (!Notification.isSupported()) {
logger.warn('系统不支持桌面通知');
return { error: 'Desktop notifications not supported', success: false };
}
// 检查窗口是否隐藏
const isWindowHidden = this.isMainWindowHidden();
if (!isWindowHidden) {
logger.debug('主窗口可见,跳过桌面通知');
return { reason: 'Window is visible', skipped: true, success: true };
}
logger.info('窗口已隐藏,显示桌面通知:', params.title);
const notification = new Notification({
body: params.body,
// 添加更多配置以确保通知能正常显示
hasReply: false,
silent: params.silent || false,
timeoutType: 'default',
title: params.title,
urgency: 'normal',
});
// 添加更多事件监听来调试
notification.on('show', () => {
logger.info('通知已显示');
});
notification.on('click', () => {
logger.debug('用户点击通知,显示主窗口');
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.show();
mainWindow.browserWindow.focus();
});
notification.on('close', () => {
logger.debug('通知已关闭');
});
notification.on('failed', (error) => {
logger.error('通知显示失败:', error);
});
// 使用 Promise 来确保通知显示
return new Promise((resolve) => {
notification.show();
// 给通知一些时间来显示,然后检查结果
setTimeout(() => {
logger.info('通知显示调用完成');
resolve({ success: true });
}, 100);
});
} catch (error) {
logger.error('显示桌面通知失败:', error);
return {
error: error instanceof Error ? error.message : 'Unknown error',
success: false,
};
}
}
/**
* 检查主窗口是否隐藏
*/
@ipcClientEvent('isMainWindowHidden')
isMainWindowHidden(): boolean {
try {
const mainWindow = this.app.browserManager.getMainWindow();
const browserWindow = mainWindow.browserWindow;
// 如果窗口被销毁,认为是隐藏的
if (browserWindow.isDestroyed()) {
return true;
}
// 检查窗口是否可见和聚焦
const isVisible = browserWindow.isVisible();
const isFocused = browserWindow.isFocused();
const isMinimized = browserWindow.isMinimized();
logger.debug('窗口状态检查:', { isFocused, isMinimized, isVisible });
// 窗口隐藏的条件:不可见或最小化或失去焦点
return !isVisible || isMinimized || !isFocused;
} catch (error) {
logger.error('检查窗口状态失败:', error);
return true; // 发生错误时认为窗口隐藏,确保通知能显示
}
}
}
@@ -79,6 +79,12 @@ export default class RemoteServerConfigCtr extends ControllerModule {
private encryptedAccessToken?: string;
private encryptedRefreshToken?: string;
/**
* Token expiration time (timestamp in milliseconds)
* Used for automatic token refresh
*/
private tokenExpiresAt?: number;
/**
* Promise representing the ongoing token refresh operation.
* Used to prevent concurrent refreshes and allow callers to wait.
@@ -89,10 +95,19 @@ export default class RemoteServerConfigCtr extends ControllerModule {
* Encrypt and store tokens
* @param accessToken Access token
* @param refreshToken Refresh token
* @param expiresIn Token expiration time in seconds (optional)
*/
async saveTokens(accessToken: string, refreshToken: string) {
async saveTokens(accessToken: string, refreshToken: string, expiresIn?: number) {
logger.info('Saving encrypted tokens');
// Calculate expiration time if provided
if (expiresIn) {
this.tokenExpiresAt = Date.now() + expiresIn * 1000;
logger.debug(`Token expires at: ${new Date(this.tokenExpiresAt).toISOString()}`);
} else {
this.tokenExpiresAt = undefined;
}
// If platform doesn't support secure storage, store raw tokens
if (!safeStorage.isEncryptionAvailable()) {
logger.warn('Safe storage not available, storing tokens unencrypted');
@@ -101,6 +116,7 @@ export default class RemoteServerConfigCtr extends ControllerModule {
// Persist unencrypted tokens (consider security implications)
this.app.storeManager.set(this.encryptedTokensKey, {
accessToken: this.encryptedAccessToken,
expiresAt: this.tokenExpiresAt,
refreshToken: this.encryptedRefreshToken,
});
return;
@@ -120,6 +136,7 @@ export default class RemoteServerConfigCtr extends ControllerModule {
logger.debug(`Persisting encrypted tokens to store key: ${this.encryptedTokensKey}`);
this.app.storeManager.set(this.encryptedTokensKey, {
accessToken: this.encryptedAccessToken,
expiresAt: this.tokenExpiresAt,
refreshToken: this.encryptedRefreshToken,
});
}
@@ -199,17 +216,40 @@ export default class RemoteServerConfigCtr extends ControllerModule {
logger.info('Clearing access and refresh tokens');
this.encryptedAccessToken = undefined;
this.encryptedRefreshToken = undefined;
this.tokenExpiresAt = undefined;
// Also clear from persistent storage
logger.debug(`Deleting tokens from store key: ${this.encryptedTokensKey}`);
this.app.storeManager.delete(this.encryptedTokensKey);
}
/**
* Get token expiration time
*/
getTokenExpiresAt(): number | undefined {
return this.tokenExpiresAt;
}
/**
* Check if token is expired or will expire soon
* @param bufferTimeMs Buffer time in milliseconds (default 5 minutes)
* @returns true if token is expired or will expire soon
*/
isTokenExpiringSoon(bufferTimeMs: number = 5 * 60 * 1000): boolean {
if (!this.tokenExpiresAt) {
return false; // No expiration time available
}
const currentTime = Date.now();
const bufferTime = this.tokenExpiresAt - bufferTimeMs;
return currentTime >= bufferTime;
}
/**
* 刷新访问令牌
* 使用存储的刷新令牌获取新的访问令牌
* Handles concurrent requests by returning the existing refresh promise if one is in progress.
*/
@ipcClientEvent('refreshAccessToken')
async refreshAccessToken(): Promise<{ error?: string; success: boolean }> {
// If a refresh is already in progress, return the existing promise
if (this.refreshPromise) {
@@ -290,7 +330,7 @@ export default class RemoteServerConfigCtr extends ControllerModule {
// 保存新令牌
logger.info('Token refresh successful, saving new tokens.');
await this.saveTokens(data.access_token, data.refresh_token);
await this.saveTokens(data.access_token, data.refresh_token, data.expires_in);
return { success: true };
} catch (error) {
@@ -316,6 +356,13 @@ export default class RemoteServerConfigCtr extends ControllerModule {
logger.info('Successfully loaded tokens from store into memory.');
this.encryptedAccessToken = storedTokens.accessToken;
this.encryptedRefreshToken = storedTokens.refreshToken;
this.tokenExpiresAt = storedTokens.expiresAt;
if (this.tokenExpiresAt) {
logger.debug(
`Loaded token expiration time: ${new Date(this.tokenExpiresAt).toISOString()}`,
);
}
} else {
logger.debug('No valid tokens found in store.');
}
@@ -1,12 +1,17 @@
import {
ProxyTRPCRequestParams,
ProxyTRPCRequestResult,
} from '@lobechat/electron-client-ipc/src/types/proxyTRPCRequest';
ProxyTRPCStreamRequestParams,
} from '@lobechat/electron-client-ipc';
import { IpcMainEvent, WebContents, ipcMain } from 'electron';
import { HttpProxyAgent } from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { Buffer } from 'node:buffer';
import http, { IncomingMessage, OutgoingHttpHeaders } from 'node:http';
import https from 'node:https';
import { URL } from 'node:url';
import { defaultProxySettings } from '@/const/store';
import { createLogger } from '@/utils/logger';
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
@@ -41,6 +46,137 @@ export default class RemoteServerSyncCtr extends ControllerModule {
afterAppReady() {
logger.info('RemoteServerSyncCtr initialized (IPC based)');
// No need to register protocol handler anymore
ipcMain.on('stream:start', this.handleStreamRequest);
}
/**
* 处理流式请求的 IPC 调用
*/
private handleStreamRequest = async (event: IpcMainEvent, args: ProxyTRPCStreamRequestParams) => {
const { requestId } = args;
const logPrefix = `[StreamProxy ${args.method} ${args.urlPath}][${requestId}]`;
logger.debug(`${logPrefix} Received stream:start IPC call`);
try {
const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
if (!config.active || (config.storageMode === 'selfHost' && !config.remoteServerUrl)) {
logger.warn(`${logPrefix} Remote server sync not active or configured.`);
event.sender.send(
`stream:error:${requestId}`,
new Error('Remote server sync not active or configured'),
);
return;
}
const remoteServerUrl = await this.remoteServerConfigCtr.getRemoteServerUrl();
const token = await this.remoteServerConfigCtr.getAccessToken();
if (!token) {
// 401 Unauthorized
event.sender.send(`stream:response:${requestId}`, {
headers: {},
status: 401,
statusText: 'Authentication required, missing token',
});
event.sender.send(`stream:end:${requestId}`);
return;
}
// 调用新的流式转发方法
await this.forwardStreamRequest(event.sender, {
...args,
accessToken: token,
remoteServerUrl,
});
} catch (error) {
logger.error(`${logPrefix} Unhandled error processing stream request:`, error);
event.sender.send(
`stream:error:${requestId}`,
error instanceof Error ? error : new Error('Unknown error'),
);
}
};
/**
* 执行实际的流式请求转发
*/
private async forwardStreamRequest(
sender: WebContents,
args: ProxyTRPCStreamRequestParams & { accessToken: string; remoteServerUrl: string },
) {
const {
urlPath,
method,
headers: originalHeaders,
body: requestBody,
accessToken,
remoteServerUrl,
requestId,
} = args;
const targetUrl = new URL(urlPath, remoteServerUrl);
const logPrefix = `[ForwardStream ${method} ${targetUrl.pathname}][${requestId}]`;
const { requestOptions, requester } = this.createRequester({
accessToken,
headers: originalHeaders,
method,
url: targetUrl,
});
const clientReq = requester.request(requestOptions, (clientRes: IncomingMessage) => {
logger.debug(`${logPrefix} Received response with status ${clientRes.statusCode}`);
// 添加调试信息
logger.debug(`${logPrefix} Response details:`, {
headers: clientRes.headers,
statusCode: clientRes.statusCode,
statusMessage: clientRes.statusMessage,
});
// 1. 立刻发送响应头和状态码
const responseData = {
headers: clientRes.headers || {},
status: clientRes.statusCode || 500,
statusText: clientRes.statusMessage || 'Unknown Status',
};
logger.debug(`${logPrefix} Sending response data:`, responseData);
sender.send(`stream:response:${requestId}`, responseData);
// 2. 监听数据块并转发
clientRes.on('data', (chunk: Buffer) => {
if (sender.isDestroyed()) return;
logger.debug(`${logPrefix} Received data chunk, size: ${chunk.length}. Forwarding...`);
sender.send(`stream:data:${requestId}`, chunk);
});
// 3. 监听结束信号并转发
clientRes.on('end', () => {
logger.debug(`${logPrefix} Stream ended. Forwarding end signal...`);
if (sender.isDestroyed()) return;
sender.send(`stream:end:${requestId}`);
});
// 4. 监听响应流错误并转发
clientRes.on('error', (error) => {
logger.error(`${logPrefix} Error reading response stream:`, error);
if (sender.isDestroyed()) return;
sender.send(`stream:error:${requestId}`, error);
});
});
// 5. 监听请求本身的错误(如 DNS 解析失败)
clientReq.on('error', (error) => {
logger.error(`${logPrefix} Error forwarding request:`, error);
if (sender.isDestroyed()) return;
sender.send(`stream:error:${requestId}`, error);
});
if (requestBody) {
clientReq.write(Buffer.from(requestBody));
}
clientReq.end();
}
/**
@@ -85,28 +221,12 @@ export default class RemoteServerSyncCtr extends ControllerModule {
// 1. Determine target URL and prepare request options
const targetUrl = new URL(urlPath, remoteServerUrl); // Combine base URL and path
// Prepare headers, cloning and adding Authorization
const requestHeaders: OutgoingHttpHeaders = { ...originalHeaders }; // Use OutgoingHttpHeaders
requestHeaders['Authorization'] = `Bearer ${accessToken}`;
// Let node handle Host, Content-Length etc. Remove potentially problematic headers
delete requestHeaders['host'];
delete requestHeaders['connection']; // Often causes issues
// delete requestHeaders['content-length']; // Let node handle it based on body
const requestOptions: https.RequestOptions | http.RequestOptions = {
// Use union type
headers: requestHeaders,
hostname: targetUrl.hostname,
method: method,
path: targetUrl.pathname + targetUrl.search,
port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
protocol: targetUrl.protocol,
// agent: false, // Consider for keep-alive issues if they arise
};
const requester = targetUrl.protocol === 'https:' ? https : http;
const { requestOptions, requester } = this.createRequester({
accessToken,
headers: originalHeaders,
method,
url: targetUrl,
});
// 2. Make the request and capture response
return new Promise((resolve) => {
@@ -176,6 +296,51 @@ export default class RemoteServerSyncCtr extends ControllerModule {
});
}
private createRequester({
headers,
accessToken,
method,
url,
}: {
accessToken: string;
headers: Record<string, string>;
method: string;
url: URL;
}) {
// Prepare headers, cloning and adding Oidc-Auth
const requestHeaders: OutgoingHttpHeaders = { ...headers }; // Use OutgoingHttpHeaders
requestHeaders['Oidc-Auth'] = accessToken;
// Let node handle Host, Content-Length etc. Remove potentially problematic headers
delete requestHeaders['host'];
delete requestHeaders['connection']; // Often causes issues
// delete requestHeaders['content-length']; // Let node handle it based on body
// 读取代理配置
const proxyConfig = this.app.storeManager.get('networkProxy', defaultProxySettings);
let agent;
if (proxyConfig?.enableProxy && proxyConfig.proxyServer) {
const proxyUrl = `${proxyConfig.proxyType}://${proxyConfig.proxyServer}${proxyConfig.proxyPort ? `:${proxyConfig.proxyPort}` : ''}`;
agent =
url.protocol === 'https:' ? new HttpsProxyAgent(proxyUrl) : new HttpProxyAgent(proxyUrl);
}
const requestOptions: https.RequestOptions | http.RequestOptions = {
agent,
// Use union type
headers: requestHeaders,
hostname: url.hostname,
method: method,
path: url.pathname + url.search,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
protocol: url.protocol,
};
const requester = url.protocol === 'https:' ? https : http;
return { requestOptions, requester };
}
/**
* Handles the 'proxy-trpc-request' IPC call from the renderer process.
* This method should be invoked by the ipcMain.handle setup in your main process entry point.
@@ -1,15 +1,10 @@
import { UploadFileParams } from '@lobechat/electron-client-ipc';
import { CreateFileParams } from '@lobechat/electron-server-ipc';
import FileService from '@/services/fileSrv';
import { ControllerModule, ipcClientEvent, ipcServerEvent } from './index';
interface UploadFileParams {
content: ArrayBuffer;
filename: string;
hash: string;
path: string;
type: string;
}
export default class UploadFileCtr extends ControllerModule {
private get fileService() {
return this.app.getService(FileService);
@@ -27,8 +22,18 @@ export default class UploadFileCtr extends ControllerModule {
return this.fileService.getFilePath(id);
}
@ipcServerEvent('getFileHTTPURL')
async getFileHTTPURL(path: string) {
return this.fileService.getFileHTTPURL(path);
}
@ipcServerEvent('deleteFiles')
async deleteFiles(paths: string[]) {
return this.fileService.deleteFiles(paths);
}
@ipcServerEvent('createFile')
async createFile(params: CreateFileParams) {
return this.fileService.uploadFile(params);
}
}
@@ -0,0 +1,420 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { App } from '@/core/App';
import NetworkProxyCtr from '../NetworkProxyCtr';
// 模拟 logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// 模拟 undici - 使用 vi.fn() 直接在 Mock 中创建
vi.mock('undici', () => ({
fetch: vi.fn(),
getGlobalDispatcher: vi.fn(),
setGlobalDispatcher: vi.fn(),
Agent: vi.fn(),
ProxyAgent: vi.fn(),
}));
// 模拟 defaultProxySettings
vi.mock('@/const/store', () => ({
defaultProxySettings: {
enableProxy: false,
proxyBypass: 'localhost,127.0.0.1,::1',
proxyPort: '',
proxyRequireAuth: false,
proxyServer: '',
proxyType: 'http',
},
}));
// 模拟 App 及其依赖项
const mockStoreManager = {
get: vi.fn(),
set: vi.fn(),
};
const mockApp = {
storeManager: mockStoreManager,
} as unknown as App;
describe('NetworkProxyCtr', () => {
let networkProxyCtr: NetworkProxyCtr;
// 动态导入 undici 的 Mock
let mockUndici: any;
beforeEach(async () => {
vi.clearAllMocks();
// 动态导入 undici Mock
mockUndici = await import('undici');
networkProxyCtr = new NetworkProxyCtr(mockApp);
// 设置 undici mocks 的默认返回值
vi.mocked(mockUndici.Agent).mockReturnValue({});
vi.mocked(mockUndici.ProxyAgent).mockReturnValue({});
vi.mocked(mockUndici.getGlobalDispatcher).mockReturnValue({
destroy: vi.fn().mockResolvedValue(undefined),
});
vi.mocked(mockUndici.setGlobalDispatcher).mockReturnValue(undefined);
// 设置 fetch mock 的默认返回值
vi.mocked(mockUndici.fetch).mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
});
});
describe('ProxyConfigValidator', () => {
const validConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
it('should validate enabled proxy config with all required fields', () => {
// 通过测试公共方法来间接测试验证逻辑
expect(() => networkProxyCtr.setProxySettings(validConfig)).not.toThrow();
});
it('should validate disabled proxy config', () => {
const disabledConfig: NetworkProxySettings = {
...validConfig,
enableProxy: false,
};
expect(() => networkProxyCtr.setProxySettings(disabledConfig)).not.toThrow();
});
it('should reject invalid proxy type', async () => {
const invalidConfig: NetworkProxySettings = {
...validConfig,
proxyType: 'invalid' as any,
};
await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
});
it('should reject missing proxy server', async () => {
const invalidConfig: NetworkProxySettings = {
...validConfig,
proxyServer: '',
};
await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
});
it('should reject invalid proxy port', async () => {
const invalidConfig: NetworkProxySettings = {
...validConfig,
proxyPort: 'invalid',
};
await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
});
it('should reject missing auth credentials when auth is required', async () => {
const invalidConfig: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: true,
proxyUsername: '',
proxyPassword: '',
};
await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
});
});
describe('getDesktopSettings', () => {
it('should return stored proxy settings', async () => {
const expectedSettings: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
mockStoreManager.get.mockReturnValue(expectedSettings);
const result = await networkProxyCtr.getDesktopSettings();
expect(result).toEqual(expectedSettings);
expect(mockStoreManager.get).toHaveBeenCalledWith('networkProxy', expect.any(Object));
});
it('should return default settings when store fails', async () => {
mockStoreManager.get.mockImplementation(() => {
throw new Error('Store error');
});
const result = await networkProxyCtr.getDesktopSettings();
expect(result).toEqual({
enableProxy: false,
proxyBypass: 'localhost,127.0.0.1,::1',
proxyPort: '',
proxyRequireAuth: false,
proxyServer: '',
proxyType: 'http',
});
});
});
describe('setProxySettings', () => {
const validConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
it('should save valid proxy settings', async () => {
mockStoreManager.get.mockReturnValue({
enableProxy: false,
proxyType: 'http',
proxyServer: '',
proxyPort: '',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
});
await networkProxyCtr.setProxySettings(validConfig);
expect(mockStoreManager.set).toHaveBeenCalledWith(
'networkProxy',
expect.objectContaining(validConfig),
);
});
it('should skip update if settings are unchanged', async () => {
mockStoreManager.get.mockReturnValue(validConfig);
await networkProxyCtr.setProxySettings(validConfig);
expect(mockStoreManager.set).not.toHaveBeenCalled();
});
it('should throw error for invalid configuration', async () => {
const invalidConfig: NetworkProxySettings = {
...validConfig,
proxyServer: '',
};
await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
});
});
describe('testProxyConnection', () => {
it('should return success for successful connection', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
};
vi.mocked(mockUndici.fetch).mockResolvedValueOnce(mockResponse);
const result = await networkProxyCtr.testProxyConnection('https://www.google.com');
expect(result).toEqual({ success: true });
expect(mockUndici.fetch).toHaveBeenCalledWith('https://www.google.com', expect.any(Object));
});
it('should throw error for failed connection', async () => {
const mockResponse = {
ok: false,
status: 404,
statusText: 'Not Found',
};
vi.mocked(mockUndici.fetch).mockResolvedValueOnce(mockResponse);
await expect(networkProxyCtr.testProxyConnection('https://www.google.com')).rejects.toThrow();
});
it('should throw error for network error', async () => {
vi.mocked(mockUndici.fetch).mockRejectedValueOnce(new Error('Network error'));
await expect(networkProxyCtr.testProxyConnection('https://www.google.com')).rejects.toThrow();
});
});
describe('testProxyConfig', () => {
const validConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
it('should return success for valid config and successful connection', async () => {
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
};
vi.mocked(mockUndici.fetch).mockResolvedValueOnce(mockResponse);
const result = await networkProxyCtr.testProxyConfig({ config: validConfig });
expect(result.success).toBe(true);
expect(result.responseTime).toBeGreaterThanOrEqual(0);
});
it('should return failure for invalid config', async () => {
const invalidConfig: NetworkProxySettings = {
...validConfig,
proxyServer: '',
};
const result = await networkProxyCtr.testProxyConfig({ config: invalidConfig });
expect(result.success).toBe(false);
expect(result.message).toContain('Invalid proxy configuration');
});
it('should test direct connection for disabled proxy', async () => {
const disabledConfig: NetworkProxySettings = {
...validConfig,
enableProxy: false,
};
const mockResponse = {
ok: true,
status: 200,
statusText: 'OK',
};
vi.mocked(mockUndici.fetch).mockResolvedValueOnce(mockResponse);
const result = await networkProxyCtr.testProxyConfig({ config: disabledConfig });
expect(result.success).toBe(true);
});
it('should return failure for connection error', async () => {
vi.mocked(mockUndici.fetch).mockRejectedValueOnce(new Error('Connection failed'));
const result = await networkProxyCtr.testProxyConfig({ config: validConfig });
expect(result.success).toBe(false);
expect(result.message).toContain('Connection failed');
});
});
describe('beforeAppReady', () => {
it('should apply stored proxy settings on app ready', async () => {
const storedConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
mockStoreManager.get.mockReturnValue(storedConfig);
await networkProxyCtr.beforeAppReady();
expect(mockStoreManager.get).toHaveBeenCalledWith('networkProxy', expect.any(Object));
});
it('should use default settings if stored config is invalid', async () => {
const invalidConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: '', // 无效的服务器
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
mockStoreManager.get.mockReturnValue(invalidConfig);
await networkProxyCtr.beforeAppReady();
expect(mockStoreManager.get).toHaveBeenCalledWith('networkProxy', expect.any(Object));
});
it('should handle errors gracefully', async () => {
mockStoreManager.get.mockImplementation(() => {
throw new Error('Store error');
});
// 不应该抛出错误
await expect(networkProxyCtr.beforeAppReady()).resolves.not.toThrow();
mockStoreManager.get.mockReset();
});
});
describe('ProxyUrlBuilder', () => {
it('should build URL without authentication', () => {
const config: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
// 通过测试代理设置来间接测试 URL 构建
expect(() => networkProxyCtr.setProxySettings(config)).not.toThrow();
});
it('should build URL with authentication', () => {
const config: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: true,
proxyUsername: 'user',
proxyPassword: 'pass',
proxyBypass: 'localhost,127.0.0.1,::1',
};
// 通过测试代理设置来间接测试 URL 构建
expect(() => networkProxyCtr.setProxySettings(config)).not.toThrow();
});
it('should handle special characters in credentials', () => {
const config: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: true,
proxyUsername: 'user@domain',
proxyPassword: 'pass:word',
proxyBypass: 'localhost,127.0.0.1,::1',
};
// 通过测试代理设置来间接测试 URL 构建
expect(() => networkProxyCtr.setProxySettings(config)).not.toThrow();
});
});
});
+8
View File
@@ -9,6 +9,7 @@ import { buildDir, nextStandaloneDir } from '@/const/dir';
import { isDev } from '@/const/env';
import { IControlModule } from '@/controllers';
import { IServiceModule } from '@/services';
import FileService from '@/services/fileSrv';
import { IpcClientEventSender } from '@/types/ipcClientEvent';
import { createLogger } from '@/utils/logger';
import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
@@ -18,6 +19,7 @@ import { I18nManager } from './I18nManager';
import { IoCContainer } from './IoCContainer';
import MenuManager from './MenuManager';
import { ShortcutManager } from './ShortcutManager';
import { StaticFileServerManager } from './StaticFileServerManager';
import { StoreManager } from './StoreManager';
import TrayManager from './TrayManager';
import { UpdaterManager } from './UpdaterManager';
@@ -41,6 +43,7 @@ export class App {
updaterManager: UpdaterManager;
shortcutManager: ShortcutManager;
trayManager: TrayManager;
staticFileServerManager: StaticFileServerManager;
chromeFlags: string[] = ['OverlayScrollbar', 'FluentOverlayScrollbar', 'FluentScrollbar'];
/**
@@ -97,6 +100,7 @@ export class App {
this.updaterManager = new UpdaterManager(this);
this.shortcutManager = new ShortcutManager(this);
this.trayManager = new TrayManager(this);
this.staticFileServerManager = new StaticFileServerManager(this);
// register the schema to interceptor url
// it should register before app ready
@@ -130,6 +134,9 @@ export class App {
await this.i18n.init();
this.menuManager.initialize();
// Initialize static file manager
await this.staticFileServerManager.initialize();
// Initialize global shortcuts: globalShortcut must be called after app.whenReady()
this.shortcutManager.initialize();
@@ -399,6 +406,7 @@ export class App {
}
// 执行清理操作
this.staticFileServerManager.destroy();
this.unregisterAllRequestHandlers();
};
}
+2
View File
@@ -381,6 +381,8 @@ export default class Browser {
}
broadcast = <T extends MainBroadcastEventKey>(channel: T, data?: MainBroadcastParams<T>) => {
if (this._browserWindow.isDestroyed()) return;
logger.debug(`Broadcasting to window ${this.identifier}, channel: ${channel}`);
this._browserWindow.webContents.send(channel, data);
};
+5 -2
View File
@@ -128,9 +128,12 @@ export default class BrowserManager {
*/
initializeBrowsers() {
logger.info('Initializing all browsers');
Object.values(appBrowsers).forEach((browser) => {
Object.values(appBrowsers).forEach((browser: BrowserWindowOpts) => {
logger.debug(`Initializing browser: ${browser.identifier}`);
this.retrieveOrInitialize(browser);
if (browser.keepAlive) {
this.retrieveOrInitialize(browser);
}
});
}
@@ -0,0 +1,221 @@
import { getPort } from 'get-port-please';
import { createServer } from 'node:http';
import { LOCAL_STORAGE_URL_PREFIX } from '@/const/dir';
import FileService from '@/services/fileSrv';
import { createLogger } from '@/utils/logger';
import type { App } from './App';
const logger = createLogger('core:StaticFileServerManager');
export class StaticFileServerManager {
private app: App;
private fileService: FileService;
private httpServer: any = null;
private serverPort: number = 0;
private isInitialized = false;
constructor(app: App) {
this.app = app;
this.fileService = app.getService(FileService);
logger.debug('StaticFileServerManager initialized');
}
/**
* 初始化静态文件管理器
*/
async initialize(): Promise<void> {
if (this.isInitialized) {
logger.warn('StaticFileServerManager already initialized');
return;
}
logger.info('Initializing StaticFileServerManager');
try {
// 启动 HTTP 文件服务器
await this.startHttpServer();
this.isInitialized = true;
logger.info(
`StaticFileServerManager initialization completed, server running on port ${this.serverPort}`,
);
} catch (error) {
logger.error('Failed to initialize StaticFileServerManager:', error);
throw error;
}
}
/**
* 启动 HTTP 文件服务器
*/
private async startHttpServer(): Promise<void> {
try {
// 使用 get-port-please 获取可用端口
this.serverPort = await getPort({
port: 33250, // 首选端口
ports: [33251, 33252, 33253, 33254, 33255], // 备用端口
host: '127.0.0.1',
});
logger.debug(`Found available port: ${this.serverPort}`);
return new Promise((resolve, reject) => {
const server = createServer(async (req, res) => {
// 设置请求超时
req.setTimeout(30000, () => {
logger.warn('Request timeout, closing connection');
if (!res.destroyed && !res.headersSent) {
res.writeHead(408, { 'Content-Type': 'text/plain' });
res.end('Request Timeout');
}
});
// 监听客户端断开连接
req.on('close', () => {
logger.debug('Client disconnected during request processing');
});
try {
await this.handleHttpRequest(req, res);
} catch (error) {
logger.error('Unhandled error in HTTP request handler:', error);
// 尝试发送错误响应,但确保不会导致进一步错误
try {
if (!res.destroyed && !res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
}
} catch (responseError) {
logger.error('Failed to send error response:', responseError);
}
}
});
// 监听指定端口
server.listen(this.serverPort, '127.0.0.1', () => {
this.httpServer = server;
logger.info(`HTTP file server started on port ${this.serverPort}`);
resolve();
});
server.on('error', (error) => {
logger.error('HTTP server error:', error);
reject(error);
});
});
} catch (error) {
logger.error('Failed to get available port:', error);
throw error;
}
}
/**
* 处理 HTTP 请求
*/
private async handleHttpRequest(req: any, res: any): Promise<void> {
try {
// 检查响应是否已经结束
if (res.destroyed || res.headersSent) {
logger.warn('Response already ended, skipping request processing');
return;
}
const url = new URL(req.url, `http://127.0.0.1:${this.serverPort}`);
logger.debug(`Processing HTTP file request: ${req.url}`);
// 提取文件路径:从 /desktop-file/path/to/file.png 中提取相对路径
let filePath = decodeURIComponent(url.pathname.slice(1)); // 移除开头的 /
// 如果路径以 desktop-file/ 开头,则移除该前缀
const prefixWithoutSlash = LOCAL_STORAGE_URL_PREFIX.slice(1) + '/'; // 移除开头的 / 并添加结尾的 /
if (filePath.startsWith(prefixWithoutSlash)) {
filePath = filePath.slice(prefixWithoutSlash.length);
}
if (!filePath) {
logger.warn(`Empty file path in HTTP request: ${req.url}`);
if (!res.headersSent) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad Request: Empty file path');
}
return;
}
// 使用 FileService 获取文件
const fileResult = await this.fileService.getFile(`desktop://${filePath}`);
// 再次检查响应状态
if (res.destroyed || res.headersSent) {
logger.warn('Response ended during file processing');
return;
}
// 设置响应头
res.writeHead(200, {
'Content-Type': fileResult.mimeType,
'Cache-Control': 'public, max-age=31536000', // 缓存一年
'Access-Control-Allow-Origin': 'http://localhost:*', // 允许 localhost 的任意端口
'Content-Length': Buffer.byteLength(fileResult.content),
});
// 发送文件内容
res.end(Buffer.from(fileResult.content));
logger.debug(`HTTP file served successfully: desktop://${filePath}`);
} catch (error) {
logger.error(`Error serving HTTP file: ${error}`);
// 检查响应是否仍然可写
if (!res.destroyed && !res.headersSent) {
try {
// 判断是否是文件未找到错误
if (error.name === 'FileNotFoundError') {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('File Not Found');
} else {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
}
} catch (writeError) {
logger.error('Failed to write error response:', writeError);
}
} else {
logger.warn('Cannot write error response: connection already closed');
}
}
}
/**
* 获取文件服务器域名
*/
getFileServerDomain(): string {
if (!this.isInitialized || !this.serverPort) {
throw new Error('StaticFileServerManager not initialized or server not started');
}
const serverDomain = `http://127.0.0.1:${this.serverPort}`;
return serverDomain;
}
/**
* 销毁静态文件管理器
*/
destroy() {
logger.info('Destroying StaticFileServerManager');
if (this.httpServer) {
logger.debug('Closing HTTP file server');
this.httpServer.close(() => {
logger.debug('HTTP file server closed');
});
this.httpServer = null;
this.serverPort = 0;
}
this.isInitialized = false;
logger.info('StaticFileServerManager destroyed');
}
}
@@ -0,0 +1,116 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { Agent, ProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici';
import { createLogger } from '@/utils/logger';
import { ProxyUrlBuilder } from './urlBuilder';
// Create logger
const logger = createLogger('modules:networkProxy:dispatcher');
/**
* 代理管理器
*/
export class ProxyDispatcherManager {
private static isChanging = false;
private static changeQueue: Array<() => Promise<void>> = [];
/**
* 应用代理设置(带并发控制)
*/
static async applyProxySettings(config: NetworkProxySettings): Promise<void> {
return new Promise((resolve, reject) => {
const operation = async () => {
try {
await this.doApplyProxySettings(config);
resolve();
} catch (error) {
reject(error);
}
};
if (this.isChanging) {
// 如果正在切换,加入队列
this.changeQueue.push(operation);
} else {
// 立即执行
operation();
}
});
}
/**
* 执行代理设置应用
*/
private static async doApplyProxySettings(config: NetworkProxySettings): Promise<void> {
this.isChanging = true;
try {
const currentDispatcher = getGlobalDispatcher();
// 禁用代理,恢复默认连接
if (!config.enableProxy) {
await this.safeDestroyDispatcher(currentDispatcher);
// 创建一个新的默认 Agent 来替代代理
setGlobalDispatcher(new Agent());
logger.debug('Proxy disabled, reset to direct connection mode');
return;
}
// 构建代理 URL
const proxyUrl = ProxyUrlBuilder.build(config);
// 创建代理 agent
const agent = this.createProxyAgent(config.proxyType, proxyUrl);
// 切换代理前销毁旧 dispatcher
await this.safeDestroyDispatcher(currentDispatcher);
setGlobalDispatcher(agent);
logger.info(
`Proxy settings applied: ${config.proxyType}://${config.proxyServer}:${config.proxyPort}`,
);
logger.debug(
'Global request proxy set, all Node.js network requests will go through this proxy',
);
} finally {
this.isChanging = false;
// 处理队列中的下一个操作
if (this.changeQueue.length > 0) {
const nextOperation = this.changeQueue.shift();
if (nextOperation) {
setTimeout(() => nextOperation(), 0);
}
}
}
}
/**
* 创建代理 agent
*/
static createProxyAgent(proxyType: string, proxyUrl: string) {
try {
// undici 的 ProxyAgent 支持 http, https 和 socks5
return new ProxyAgent({ uri: proxyUrl });
} catch (error) {
logger.error(`Failed to create proxy agent for ${proxyType}:`, error);
throw new Error(
`Failed to create proxy agent: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
/**
* 安全销毁 dispatcher
*/
private static async safeDestroyDispatcher(dispatcher: any): Promise<void> {
try {
if (dispatcher && typeof dispatcher.destroy === 'function') {
await dispatcher.destroy();
}
} catch (error) {
logger.warn('Failed to destroy dispatcher:', error);
}
}
}
@@ -0,0 +1,6 @@
export { ProxyDispatcherManager } from './dispatcher';
export type { ProxyTestResult } from './tester';
export { ProxyConnectionTester } from './tester';
export { ProxyUrlBuilder } from './urlBuilder';
export type { ProxyValidationResult } from './validator';
export { ProxyConfigValidator } from './validator';
@@ -0,0 +1,163 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { fetch, getGlobalDispatcher, setGlobalDispatcher } from 'undici';
import { createLogger } from '@/utils/logger';
import { ProxyDispatcherManager } from './dispatcher';
import { ProxyUrlBuilder } from './urlBuilder';
import { ProxyConfigValidator } from './validator';
// Create logger
const logger = createLogger('modules:networkProxy:tester');
/**
* 代理连接测试结果
*/
export interface ProxyTestResult {
message?: string;
responseTime?: number;
success: boolean;
}
/**
* 代理连接测试器
*/
export class ProxyConnectionTester {
private static readonly DEFAULT_TIMEOUT = 10_000; // 10秒超时
private static readonly DEFAULT_TEST_URL = 'https://www.google.com';
/**
* 测试代理连接
*/
static async testConnection(
url: string = this.DEFAULT_TEST_URL,
timeout: number = this.DEFAULT_TIMEOUT,
): Promise<ProxyTestResult> {
const startTime = Date.now();
try {
logger.info(`Testing proxy connection with URL: ${url}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
headers: {
'User-Agent': 'LobeChat-Desktop/1.0.0',
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const responseTime = Date.now() - startTime;
logger.info(`Proxy connection test successful, response time: ${responseTime}ms`);
return {
responseTime,
success: true,
};
} catch (error) {
const responseTime = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`Proxy connection test failed after ${responseTime}ms:`, errorMessage);
return {
message: errorMessage,
responseTime,
success: false,
};
}
}
/**
* 测试指定代理配置的连接
*/
static async testProxyConfig(
config: NetworkProxySettings,
testUrl: string = this.DEFAULT_TEST_URL,
): Promise<ProxyTestResult> {
// 验证配置
const validation = ProxyConfigValidator.validate(config);
if (!validation.isValid) {
return {
message: `Invalid proxy configuration: ${validation.errors.join(', ')}`,
success: false,
};
}
// 如果未启用代理,直接测试
if (!config.enableProxy) {
return this.testConnection(testUrl);
}
// 创建临时代理 agent 进行测试
try {
const proxyUrl = ProxyUrlBuilder.build(config);
logger.debug(`Testing proxy with URL: ${proxyUrl}`);
const agent = ProxyDispatcherManager.createProxyAgent(config.proxyType, proxyUrl);
const startTime = Date.now();
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.DEFAULT_TIMEOUT);
// 临时设置代理进行测试
const originalDispatcher = getGlobalDispatcher();
setGlobalDispatcher(agent);
try {
const response = await fetch(testUrl, {
dispatcher: agent,
headers: {
'User-Agent': 'LobeChat-Desktop/1.0.0',
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const responseTime = Date.now() - startTime;
logger.info(`Proxy test successful, response time: ${responseTime}ms`);
return {
responseTime,
success: true,
};
} catch (fetchError) {
clearTimeout(timeoutId);
throw fetchError;
} finally {
// 恢复原来的 dispatcher
setGlobalDispatcher(originalDispatcher);
// 清理临时创建的代理 agent
if (agent && typeof agent.destroy === 'function') {
try {
await agent.destroy();
} catch (error) {
logger.warn('Failed to destroy test agent:', error);
}
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`Proxy test failed: ${errorMessage}`, error);
return {
message: `Proxy test failed: ${errorMessage}`,
success: false,
};
}
}
}
@@ -0,0 +1,25 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
/**
* 代理 URL 构建器
*/
export const ProxyUrlBuilder = {
/**
* 构建代理 URL
*/
build(config: NetworkProxySettings): string {
const { proxyType, proxyServer, proxyPort, proxyRequireAuth, proxyUsername, proxyPassword } =
config;
let proxyUrl = `${proxyType}://${proxyServer}:${proxyPort}`;
// 添加认证信息
if (proxyRequireAuth && proxyUsername && proxyPassword) {
const encodedUsername = encodeURIComponent(proxyUsername);
const encodedPassword = encodeURIComponent(proxyPassword);
proxyUrl = `${proxyType}://${encodedUsername}:${encodedPassword}@${proxyServer}:${proxyPort}`;
}
return proxyUrl;
},
};
@@ -0,0 +1,80 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
/**
* 代理配置验证结果
*/
export interface ProxyValidationResult {
errors: string[];
isValid: boolean;
}
/**
* 代理配置验证器
*/
export class ProxyConfigValidator {
private static readonly SUPPORTED_TYPES = ['http', 'https', 'socks5'] as const;
private static readonly DEFAULT_BYPASS = 'localhost,127.0.0.1,::1';
/**
* 验证代理配置
*/
static validate(config: NetworkProxySettings): ProxyValidationResult {
const errors: string[] = [];
// 如果未启用代理,跳过验证
if (!config.enableProxy) {
return { errors: [], isValid: true };
}
// 验证代理类型
if (!this.SUPPORTED_TYPES.includes(config.proxyType as any)) {
errors.push(
`Unsupported proxy type: ${config.proxyType}. Supported types: ${this.SUPPORTED_TYPES.join(', ')}`,
);
}
// 验证代理服务器
if (!config.proxyServer?.trim()) {
errors.push('Proxy server is required when proxy is enabled');
} else if (!this.isValidHost(config.proxyServer)) {
errors.push('Invalid proxy server format');
}
// 验证代理端口
if (!config.proxyPort?.trim()) {
errors.push('Proxy port is required when proxy is enabled');
} else {
const port = parseInt(config.proxyPort, 10);
if (isNaN(port) || port < 1 || port > 65_535) {
errors.push('Proxy port must be a valid number between 1 and 65535');
}
}
// 验证认证信息
if (config.proxyRequireAuth) {
if (!config.proxyUsername?.trim()) {
errors.push('Proxy username is required when authentication is enabled');
}
if (!config.proxyPassword?.trim()) {
errors.push('Proxy password is required when authentication is enabled');
}
}
return {
errors,
isValid: errors.length === 0,
};
}
/**
* 验证主机名格式
*/
private static isValidHost(host: string): boolean {
// 简单的主机名验证(IP 地址或域名)
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
const domainRegex =
/^[\dA-Za-z]([\dA-Za-z-]*[\dA-Za-z])?(\.[\dA-Za-z]([\dA-Za-z-]*[\dA-Za-z])?)*$/;
return ipRegex.test(host) || domainRegex.test(host);
}
}
+230 -43
View File
@@ -1,15 +1,28 @@
import { DeleteFilesResponse } from '@lobechat/electron-server-ipc';
import * as fs from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import path, { join } from 'node:path';
import { promisify } from 'node:util';
import { FILE_STORAGE_DIR } from '@/const/dir';
import { FILE_STORAGE_DIR, LOCAL_STORAGE_URL_PREFIX } from '@/const/dir';
import { makeSureDirExist } from '@/utils/file-system';
import { createLogger } from '@/utils/logger';
import { ServiceModule } from './index';
/**
* 文件未找到错误类
*/
export class FileNotFoundError extends Error {
constructor(
message: string,
public path: string,
) {
super(message);
this.name = 'FileNotFoundError';
}
}
const readFilePromise = promisify(fs.readFile);
const unlinkPromise = promisify(fs.unlink);
@@ -17,7 +30,7 @@ const unlinkPromise = promisify(fs.unlink);
const logger = createLogger('services:FileService');
interface UploadFileParams {
content: ArrayBuffer;
content: ArrayBuffer | string; // ArrayBuffer from browser or Base64 string from server
filename: string;
hash: string;
path: string;
@@ -32,17 +45,16 @@ interface FileMetadata {
}
export default class FileService extends ServiceModule {
/**
* 获取旧版上传目录路径
* @deprecated 仅用于向后兼容旧版文件访问,新文件应存储在 FILE_STORAGE_DIR 的自定义路径下
*/
get UPLOADS_DIR() {
return join(this.app.appStoragePath, FILE_STORAGE_DIR, 'uploads');
}
constructor(app) {
super(app);
// Initialize file storage directory
logger.info('Initializing file storage directory');
makeSureDirExist(this.UPLOADS_DIR);
logger.debug(`Upload directory created: ${this.UPLOADS_DIR}`);
}
/**
@@ -52,31 +64,44 @@ export default class FileService extends ServiceModule {
content,
filename,
hash,
path: filePath,
type,
}: UploadFileParams): Promise<{ metadata: FileMetadata; success: boolean }> {
logger.info(`Starting to upload file: ${filename}, hash: ${hash}`);
logger.info(`Starting to upload file: ${filename}, hash: ${hash}, path: ${filePath}`);
try {
// 创建时间戳目录
const date = (Date.now() / 1000 / 60 / 60).toFixed(0);
const dirname = join(this.UPLOADS_DIR, date);
logger.debug(`Creating timestamp directory: ${dirname}`);
makeSureDirExist(dirname);
// 获取当前时间戳,避免重复调用 Date.now()
const now = Date.now();
const date = (now / 1000 / 60 / 60).toFixed(0);
// 生成文件保存路径
const fileExt = filename.split('.').pop() || '';
const savedFilename = `${hash}${fileExt ? `.${fileExt}` : ''}`;
const savedPath = join(dirname, savedFilename);
logger.debug(`Generated file save path: ${savedPath}`);
// 使用传入的 filePath 作为文件的存储路径
const fullStoragePath = join(this.app.appStoragePath, FILE_STORAGE_DIR, filePath);
logger.debug(`Target file storage path: ${fullStoragePath}`);
// 写入文件内容
const buffer = Buffer.from(content);
logger.debug(`Writing file content, size: ${buffer.length} bytes`);
// 确保目标目录存在
const targetDir = path.dirname(fullStoragePath);
logger.debug(`Ensuring target directory exists: ${targetDir}`);
makeSureDirExist(targetDir);
const savedPath = fullStoragePath;
logger.debug(`Final file save path: ${savedPath}`);
// 根据 content 类型创建 Buffer
let buffer: Buffer;
if (typeof content === 'string') {
// 来自服务端的 Base64 字符串
buffer = Buffer.from(content, 'base64');
logger.debug(`Creating buffer from Base64 string, size: ${buffer.length} bytes`);
} else {
// 来自浏览器端的 ArrayBuffer
buffer = Buffer.from(content);
logger.debug(`Creating buffer from ArrayBuffer, size: ${buffer.length} bytes`);
}
await writeFile(savedPath, buffer);
// 写入元数据文件
const metaFilePath = `${savedPath}.meta`;
const metadata = {
createdAt: Date.now(),
createdAt: now, // 使用统一的时间戳
filename,
hash,
size: buffer.length,
@@ -86,13 +111,18 @@ export default class FileService extends ServiceModule {
await writeFile(metaFilePath, JSON.stringify(metadata, null, 2));
// 返回与S3兼容的元数据格式
const desktopPath = `desktop://${date}/${savedFilename}`;
const desktopPath = `desktop://${filePath}`;
logger.info(`File upload successful: ${desktopPath}`);
// 从路径中提取文件名和目录信息
const parsedPath = path.parse(filePath);
const dirname = parsedPath.dir || '';
const savedFilename = parsedPath.base;
return {
metadata: {
date,
dirname: date,
date, // 保持时间戳格式,用于兼容性和时间追踪
dirname,
filename: savedFilename,
path: desktopPath,
},
@@ -104,6 +134,24 @@ export default class FileService extends ServiceModule {
}
}
/**
* 判断路径是否为旧版格式(时间戳目录)
*
* 旧版路径格式: {timestamp}/{hash}.{ext} (例如: 1234567890/abc123.png)
* 新版路径格式: 任意自定义路径 (例如: user_uploads/images/photo.png, ai_generations/image.jpg)
*
* @param path - 相对路径,不包含 desktop:// 前缀
* @returns true 如果是旧版格式,false 如果是新版格式
*/
private isLegacyPath(path: string): boolean {
const parts = path.split('/');
if (parts.length < 2) return false;
// 如果第一部分是纯数字(时间戳),则认为是旧版格式
// 时间戳格式:精确到小时的 Unix 时间戳,通常是 10 位数字
return /^\d+$/.test(parts[0]);
}
/**
* 获取文件内容
*/
@@ -123,13 +171,49 @@ export default class FileService extends ServiceModule {
// 解析路径
const relativePath = normalizedPath.replace('desktop://', '');
const filePath = join(this.UPLOADS_DIR, relativePath);
logger.debug(`Reading file from path: ${filePath}`);
// 读取文件内容
// 智能路由:根据路径格式决定从哪个目录读取文件
let filePath: string;
let isLegacyAttempt = false;
if (this.isLegacyPath(relativePath)) {
// 旧版路径:从 uploads 目录读取(向后兼容)
filePath = join(this.UPLOADS_DIR, relativePath);
isLegacyAttempt = true;
logger.debug(`Legacy path detected, reading from uploads directory: ${filePath}`);
} else {
// 新版路径:从 FILE_STORAGE_DIR 根目录读取
filePath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
logger.debug(`New path format, reading from storage root: ${filePath}`);
}
// 读取文件内容,如果第一次尝试失败且是 legacy 路径,则尝试新路径
logger.debug(`Starting to read file content`);
const content = await readFilePromise(filePath);
logger.debug(`File content read complete, size: ${content.length} bytes`);
let content: Buffer;
try {
content = await readFilePromise(filePath);
logger.debug(`File content read complete, size: ${content.length} bytes`);
} catch (firstError) {
if (isLegacyAttempt) {
// 如果是 legacy 路径读取失败,尝试从新路径读取
const fallbackPath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
logger.debug(
`Legacy path read failed, attempting fallback to storage root: ${fallbackPath}`,
);
try {
content = await readFilePromise(fallbackPath);
filePath = fallbackPath; // 更新 filePath 用于后续的元数据读取
logger.debug(`Fallback read successful, size: ${content.length} bytes`);
} catch (fallbackError) {
logger.error(
`Both legacy and fallback paths failed. Legacy error: ${(firstError as Error).message}, Fallback error: ${(fallbackError as Error).message}`,
);
throw firstError; // 抛出原始错误
}
} else {
throw firstError;
}
}
// 读取元数据获取MIME类型
const metaFilePath = `${filePath}.meta`;
@@ -142,7 +226,9 @@ export default class FileService extends ServiceModule {
mimeType = metadata.type || mimeType;
logger.debug(`Got MIME type from metadata: ${mimeType}`);
} catch (metaError) {
logger.warn(`Failed to read metadata file: ${(metaError as Error).message}, using default MIME type`);
logger.warn(
`Failed to read metadata file: ${(metaError as Error).message}, using default MIME type`,
);
// 如果元数据文件不存在,尝试从文件扩展名猜测MIME类型
const ext = path.split('.').pop()?.toLowerCase();
if (ext) {
@@ -184,6 +270,12 @@ export default class FileService extends ServiceModule {
};
} catch (error) {
logger.error(`File retrieval failed:`, error);
// 如果是文件不存在错误,抛出自定义的 FileNotFoundError
if (error instanceof Error && error.message.includes('ENOENT')) {
throw new FileNotFoundError(`File not found: ${path}`, path);
}
throw new Error(`File retrieval failed: ${(error as Error).message}`);
}
}
@@ -200,15 +292,53 @@ export default class FileService extends ServiceModule {
throw new Error(`Invalid desktop file path: ${path}`);
}
// 解析路径
const relativePath = path.replace('desktop://', '');
const filePath = join(this.UPLOADS_DIR, relativePath);
logger.debug(`File deletion path: ${filePath}`);
// 标准化路径格式
const normalizedPath = path.replace(/^desktop:\/+/, 'desktop://');
// 删除文件及其元数据
// 解析路径
const relativePath = normalizedPath.replace('desktop://', '');
// 智能路由:根据路径格式决定从哪个目录删除文件
let filePath: string;
let isLegacyAttempt = false;
if (this.isLegacyPath(relativePath)) {
// 旧版路径:从 uploads 目录删除(向后兼容)
filePath = join(this.UPLOADS_DIR, relativePath);
isLegacyAttempt = true;
logger.debug(`Legacy path detected, deleting from uploads directory: ${filePath}`);
} else {
// 新版路径:从 FILE_STORAGE_DIR 根目录删除
filePath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
logger.debug(`New path format, deleting from storage root: ${filePath}`);
}
// 删除文件及其元数据,如果第一次尝试失败且是 legacy 路径,则尝试新路径
logger.debug(`Starting file deletion`);
await unlinkPromise(filePath);
logger.debug(`File deletion successful`);
try {
await unlinkPromise(filePath);
logger.debug(`File deletion successful`);
} catch (firstError) {
if (isLegacyAttempt) {
// 如果是 legacy 路径删除失败,尝试从新路径删除
const fallbackPath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
logger.debug(
`Legacy path deletion failed, attempting fallback to storage root: ${fallbackPath}`,
);
try {
await unlinkPromise(fallbackPath);
filePath = fallbackPath; // 更新 filePath 用于后续的元数据删除
logger.debug(`Fallback deletion successful`);
} catch (fallbackError) {
logger.error(
`Both legacy and fallback deletion failed. Legacy error: ${(firstError as Error).message}, Fallback error: ${(fallbackError as Error).message}`,
);
throw firstError; // 抛出原始错误
}
} else {
throw firstError;
}
}
// 尝试删除元数据文件,但不强制要求存在
try {
@@ -270,7 +400,9 @@ export default class FileService extends ServiceModule {
});
const success = errors.length === 0;
logger.info(`Batch deletion operation complete, success: ${success}, error count: ${errors.length}`);
logger.info(
`Batch deletion operation complete, success: ${success}, error count: ${errors.length}`,
);
return {
success,
...(errors.length > 0 && { errors }),
@@ -285,10 +417,65 @@ export default class FileService extends ServiceModule {
throw new Error(`Invalid desktop file path: ${path}`);
}
// 标准化路径格式
const normalizedPath = path.replace(/^desktop:\/+/, 'desktop://');
// 解析路径
const relativePath = path.replace('desktop://', '');
const fullPath = join(this.UPLOADS_DIR, relativePath);
logger.debug(`Resolved filesystem path: ${fullPath}`);
const relativePath = normalizedPath.replace('desktop://', '');
// 智能路由:根据路径格式决定从哪个目录获取文件路径
let fullPath: string;
if (this.isLegacyPath(relativePath)) {
// 旧版路径:从 uploads 目录获取(向后兼容)
fullPath = join(this.UPLOADS_DIR, relativePath);
logger.debug(`Legacy path detected, resolved to uploads directory: ${fullPath}`);
// 检查文件是否存在,如果不存在则尝试新路径
try {
await fs.promises.access(fullPath, fs.constants.F_OK);
logger.debug(`Legacy path file exists: ${fullPath}`);
} catch {
// 如果 legacy 路径文件不存在,尝试新路径
const fallbackPath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
logger.debug(`Legacy path file not found, trying fallback path: ${fallbackPath}`);
try {
await fs.promises.access(fallbackPath, fs.constants.F_OK);
fullPath = fallbackPath;
logger.debug(`Fallback path file exists: ${fullPath}`);
} catch {
// 两个路径都不存在,返回原始的 legacy 路径(保持原有行为)
logger.debug(
`Neither legacy nor fallback path exists, returning legacy path: ${fullPath}`,
);
}
}
} else {
// 新版路径:从 FILE_STORAGE_DIR 根目录获取
fullPath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
logger.debug(`New path format, resolved to storage root: ${fullPath}`);
}
return fullPath;
}
async getFileHTTPURL(path: string): Promise<string> {
logger.debug(`Getting file HTTP URL: ${path}`);
// 处理desktop://路径
if (!path.startsWith('desktop://')) {
logger.error(`Invalid desktop file path: ${path}`);
throw new Error(`Invalid desktop file path: ${path}`);
}
// 标准化路径格式
const normalizedPath = path.replace(/^desktop:\/+/, 'desktop://');
// 解析路径:从 desktop://path/to/file.png 中提取 path/to/file.png
const relativePath = normalizedPath.replace('desktop://', '');
// 使用 StaticFileServerManager 获取文件服务器域名,然后构建完整 URL
const serverDomain = this.app.staticFileServerManager.getFileServerDomain();
const httpURL = `${serverDomain}${LOCAL_STORAGE_URL_PREFIX}/${relativePath}`;
logger.debug(`Generated HTTP URL: ${httpURL}`);
return httpURL;
}
}
+3 -1
View File
@@ -1,12 +1,14 @@
import { DataSyncConfig } from '@lobechat/electron-client-ipc';
import { DataSyncConfig, NetworkProxySettings } from '@lobechat/electron-client-ipc';
export interface ElectronMainStore {
dataSyncConfig: DataSyncConfig;
encryptedTokens: {
accessToken?: string;
expiresAt?: number;
refreshToken?: string;
};
locale: string;
networkProxy: NetworkProxySettings;
shortcuts: Record<string, string>;
storagePath: string;
}
+2 -1
View File
@@ -19,8 +19,9 @@ export const createLogger = (namespace: string) => {
error: (message, ...args) => {
if (process.env.NODE_ENV === 'production') {
electronLog.error(message, ...args);
} else {
console.error(message, ...args);
}
debugLogger(`ERROR: ${message}`, ...args);
},
info: (message, ...args) => {
if (process.env.NODE_ENV === 'production') {
@@ -1,6 +1,6 @@
// copy from https://github.com/kirill-konshin/next-electron-rsc
import { serialize as serializeCookie } from 'cookie';
import type { Protocol, Session } from 'electron';
import { type Protocol, type Session, protocol } from 'electron';
import type { NextConfig } from 'next';
import type NextNodeServer from 'next/dist/server/next-server';
import assert from 'node:assert';
@@ -11,6 +11,7 @@ import { parse } from 'node:url';
import resolve from 'resolve';
import { parse as parseCookie, splitCookiesString } from 'set-cookie-parser';
import { LOCAL_STORAGE_URL_PREFIX } from '@/const/dir';
import { isDev } from '@/const/env';
import { createLogger } from '@/utils/logger';
@@ -178,7 +179,9 @@ export function createHandler({
}
};
}
let registerProtocolHandle = false;
let interceptorCount = 0; // 追踪活跃的拦截器数量
protocol.registerSchemesAsPrivileged([
{
@@ -224,6 +227,14 @@ export function createHandler({
socket: Socket,
): Promise<Response> => {
try {
// 检查是否是本地文件服务请求,如果是则跳过处理
const url = new URL(request.url);
if (url.pathname.startsWith(LOCAL_STORAGE_URL_PREFIX + '/')) {
if (debug) logger.debug(`Skipping local file service request: ${request.url}`);
// 直接使用 fetch 转发请求到本地文件服务
return fetch(request);
}
// 先尝试使用自定义处理器处理请求
for (const customHandler of customHandlers) {
try {
@@ -354,19 +365,32 @@ export function createHandler({
);
const socket = new Socket();
interceptorCount++; // 增加拦截器计数
const closeSocket = () => socket.end();
process.on('SIGTERM', () => closeSocket);
process.on('SIGINT', () => closeSocket);
if (!isDev && !registerProtocolHandle) {
if (!registerProtocolHandle) {
logger.debug(
`Registering HTTP protocol handler in ${isDev ? 'development' : 'production'} mode`,
);
protocol.handle('http', async (request) => {
if (!isDev) {
assert(request.url.startsWith(localhostUrl), 'External HTTP not supported, use HTTPS');
// 检查是否是本地文件服务请求,如果是则允许通过
const isLocalhost = request.url.startsWith(localhostUrl);
const url = new URL(request.url);
const isLocalIP =
request.url.startsWith('http://127.0.0.1:') ||
request.url.startsWith('http://localhost:');
const isLocalFileService = url.pathname.startsWith(LOCAL_STORAGE_URL_PREFIX + '/');
const valid = isLocalhost || (isLocalIP && isLocalFileService);
if (!valid) {
throw new Error('External HTTP not supported, use HTTPS');
}
}
return handleRequest(request, session, socket);
@@ -374,12 +398,19 @@ export function createHandler({
registerProtocolHandle = true;
}
logger.debug(`Active interceptors count: ${interceptorCount}`);
return function stopIntercept() {
if (registerProtocolHandle) {
logger.debug('Unregistering HTTP protocol handler');
interceptorCount--; // 减少拦截器计数
logger.debug(`Stopping interceptor, remaining count: ${interceptorCount}`);
// 只有当没有活跃的拦截器时才取消注册协议处理器
if (registerProtocolHandle && interceptorCount === 0) {
logger.debug('Unregistering HTTP protocol handler (no active interceptors)');
protocol.unhandle('http');
registerProtocolHandle = false;
}
process.off('SIGTERM', () => closeSocket);
process.off('SIGINT', () => closeSocket);
closeSocket();
+2 -1
View File
@@ -2,6 +2,7 @@ import { electronAPI } from '@electron-toolkit/preload';
import { contextBridge } from 'electron';
import { invoke } from './invoke';
import { onStreamInvoke } from './streamer';
export const setupElectronApi = () => {
// Use `contextBridge` APIs to expose Electron APIs to
@@ -14,5 +15,5 @@ export const setupElectronApi = () => {
console.error(error);
}
contextBridge.exposeInMainWorld('electronAPI', { invoke });
contextBridge.exposeInMainWorld('electronAPI', { invoke, onStreamInvoke });
};
+58
View File
@@ -0,0 +1,58 @@
import type { ProxyTRPCRequestParams } from '@lobechat/electron-client-ipc';
import { ipcRenderer } from 'electron';
import { v4 as uuid } from 'uuid';
interface StreamResponse {
headers: Record<string, string>;
status: number;
statusText: string;
}
export interface StreamerCallbacks {
onData: (chunk: Uint8Array) => void;
onEnd: () => void;
onError: (error: Error) => void;
onResponse: (response: StreamResponse) => void;
}
/**
* Calls the main process method and handles the stream response via callbacks.
* @param params The request parameters.
* @param callbacks The callbacks to handle stream events.
*/
export const onStreamInvoke = (
params: ProxyTRPCRequestParams,
callbacks: StreamerCallbacks,
): (() => void) => {
const requestId = uuid();
const cleanup = () => {
ipcRenderer.removeAllListeners(`stream:data:${requestId}`);
ipcRenderer.removeAllListeners(`stream:end:${requestId}`);
ipcRenderer.removeAllListeners(`stream:error:${requestId}`);
ipcRenderer.removeAllListeners(`stream:response:${requestId}`);
};
ipcRenderer.on(`stream:data:${requestId}`, (_, chunk: Buffer) => {
callbacks.onData(new Uint8Array(chunk));
});
ipcRenderer.once(`stream:end:${requestId}`, () => {
callbacks.onEnd();
cleanup();
});
ipcRenderer.once(`stream:error:${requestId}`, (_, error: Error) => {
callbacks.onError(error);
cleanup();
});
ipcRenderer.once(`stream:response:${requestId}`, (_, response: StreamResponse) => {
callbacks.onResponse(response);
});
ipcRenderer.send('stream:start', { ...params, requestId });
// Return a cleanup function to be called on cancellation
return cleanup;
};
+182
View File
@@ -1,4 +1,186 @@
[
{
"children": {
"improvements": ["Modal list header sticky style."]
},
"date": "2025-07-21",
"version": "1.102.1"
},
{
"children": {
"features": ["Add image generation capabilities using Google AI Imagen API."]
},
"date": "2025-07-21",
"version": "1.102.0"
},
{
"children": {
"improvements": ["Fix lobehub provider /chat in desktop."]
},
"date": "2025-07-21",
"version": "1.101.2"
},
{
"children": {
"fixes": ["Try fix authorization code exchange & pin next-auto to beta.29."]
},
"date": "2025-07-19",
"version": "1.101.1"
},
{
"children": {
"features": ["Add zhipu cogview4."],
"fixes": ["Some ai image bugs."]
},
"date": "2025-07-19",
"version": "1.101.0"
},
{
"children": {
"fixes": ["Fix webapi proxy with clerk."]
},
"date": "2025-07-18",
"version": "1.100.2"
},
{
"children": {
"fixes": ["Use server env config image models."]
},
"date": "2025-07-17",
"version": "1.100.1"
},
{
"children": {
"features": ["Refactor desktop oauth and use JWTs token to support remote chat."]
},
"date": "2025-07-17",
"version": "1.100.0"
},
{
"children": {
"fixes": ["Desktop local db can't upload image."]
},
"date": "2025-07-16",
"version": "1.99.6"
},
{
"children": {
"fixes": ["Fix page error when url is not defined in web search plugin."]
},
"date": "2025-07-16",
"version": "1.99.5"
},
{
"children": {
"fixes": ["Fix apikey issue on server log."]
},
"date": "2025-07-16",
"version": "1.99.4"
},
{
"children": {
"fixes": ["Chat model list should not show image model."]
},
"date": "2025-07-16",
"version": "1.99.3"
},
{
"children": {
"fixes": ["Some ai image generation feedback issues."]
},
"date": "2025-07-15",
"version": "1.99.2"
},
{
"children": {},
"date": "2025-07-15",
"version": "1.99.1"
},
{
"children": {
"features": ["support AI Image."]
},
"date": "2025-07-14",
"version": "1.99.0"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-07-14",
"version": "1.98.2"
},
{
"children": {
"improvements": ["Fix discover translation."]
},
"date": "2025-07-14",
"version": "1.98.1"
},
{
"children": {
"features": ["Add network proxy for desktop."]
},
"date": "2025-07-13",
"version": "1.98.0"
},
{
"children": {
"improvements": ["Support Hunyuan A13B thinking model."]
},
"date": "2025-07-13",
"version": "1.97.17"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-07-13",
"version": "1.97.16"
},
{
"children": {
"fixes": ["Add vision support to Grok 4."]
},
"date": "2025-07-12",
"version": "1.97.15"
},
{
"children": {
"fixes": ["Revert \"💄 style: Open new topic by tap Just Chat again\"."],
"improvements": ["Add Kimi K2 model."]
},
"date": "2025-07-12",
"version": "1.97.14"
},
{
"children": {
"improvements": ["Support new Doubao thinking models, update i18n."]
},
"date": "2025-07-12",
"version": "1.97.13"
},
{
"children": {
"fixes": ["Grok-4 reasoning model universal matching."]
},
"date": "2025-07-11",
"version": "1.97.12"
},
{
"children": {
"improvements": ["Open new topic by tap Just Chat again."]
},
"date": "2025-07-11",
"version": "1.97.11"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-07-11",
"version": "1.97.10"
},
{
"children": {
"improvements": ["Integrate Amazon Cognito for user authentication."]
@@ -0,0 +1,36 @@
# 添加新的 AI 图像模型
## 兼容 openai 请求格式的模型
指的是可以使用 openai SDK 进行请求,并且请求参数和和返回值和 dall-e 以及 gpt-image-x 系列一致。
以智谱的 CogView-4 为例,它是一个兼容 openai 请求格式的模型,可以按照以下步骤添加:
1. 在对应的 ai models 文件 `src/config/aiModels/zhipu.ts` 中,添加模型配置,例如:
```ts
const zhipuImageModels: AIImageModelCard[] = [
// 添加模型配置
// https://bigmodel.cn/dev/howuse/image-generation-model/cogview-4
{
description:
'CogView-4 是智谱首个支持生成汉字的开源文生图模型,在语义理解、图像生成质量、中英文字生成能力等方面全面提升,支持任意长度的中英双语输入,能够生成在给定范围内的任意分辨率图像。',
displayName: 'CogView-4',
enabled: true,
id: 'cogview-4',
parameters: {
prompt: {
default: '',
},
size: {
default: '1024x1024',
enum: ['1024x1024', '768x1344', '864x1152', '1344x768', '1152x864', '1440x720', '720x1440'],
},
},
releasedAt: '2025-03-04',
type: 'image',
},
];
```
2. 执行下 `npx i18n` 命令,更新模型描述的翻译文件
+79
View File
@@ -163,6 +163,7 @@ table files {
name text [not null]
size integer [not null]
url text [not null]
source text
client_id text
metadata jsonb
chunk_task_id uuid
@@ -218,6 +219,45 @@ table knowledge_bases {
}
}
table generation_batches {
id text [pk, not null]
user_id text [not null]
generation_topic_id text [not null]
provider text [not null]
model text [not null]
prompt text [not null]
width integer
height integer
ratio varchar(64)
config jsonb
accessed_at "timestamp with time zone" [not null, default: `now()`]
created_at "timestamp with time zone" [not null, default: `now()`]
updated_at "timestamp with time zone" [not null, default: `now()`]
}
table generation_topics {
id text [pk, not null]
user_id text [not null]
title text
cover_url text
accessed_at "timestamp with time zone" [not null, default: `now()`]
created_at "timestamp with time zone" [not null, default: `now()`]
updated_at "timestamp with time zone" [not null, default: `now()`]
}
table generations {
id text [pk, not null]
user_id text [not null]
generation_batch_id varchar(64) [not null]
async_task_id uuid
file_id text
seed integer
asset jsonb
accessed_at "timestamp with time zone" [not null, default: `now()`]
created_at "timestamp with time zone" [not null, default: `now()`]
updated_at "timestamp with time zone" [not null, default: `now()`]
}
table message_chunks {
message_id text
chunk_id uuid
@@ -391,6 +431,15 @@ table nextauth_verificationtokens {
}
}
table oauth_handoffs {
id text [pk, not null]
client varchar(50) [not null]
payload jsonb [not null]
accessed_at "timestamp with time zone" [not null, default: `now()`]
created_at "timestamp with time zone" [not null, default: `now()`]
updated_at "timestamp with time zone" [not null, default: `now()`]
}
table oidc_access_tokens {
id varchar(255) [pk, not null]
data jsonb [not null]
@@ -834,6 +883,10 @@ table users {
updated_at "timestamp with time zone" [not null, default: `now()`]
}
ref: agents_files.file_id > files.id
ref: agents_files.agent_id > agents.id
ref: agents_knowledge_bases.knowledge_base_id - knowledge_bases.id
ref: agents_knowledge_bases.agent_id > agents.id
@@ -848,8 +901,34 @@ ref: document_chunks.document_id > documents.id
ref: documents.file_id > files.id
ref: file_chunks.file_id - files.id
ref: file_chunks.chunk_id - chunks.id
ref: generations.file_id - files.id
ref: files.embedding_task_id - async_tasks.id
ref: files_to_sessions.file_id > files.id
ref: files_to_sessions.session_id > sessions.id
ref: generation_batches.user_id - users.id
ref: generation_batches.generation_topic_id > generation_topics.id
ref: generation_topics.user_id - users.id
ref: generations.user_id - users.id
ref: generations.generation_batch_id > generation_batches.id
ref: generations.async_task_id - async_tasks.id
ref: messages_files.file_id > files.id
ref: messages_files.message_id > messages.id
ref: messages.session_id - sessions.id
ref: messages.parent_id - messages.id
@@ -625,4 +625,29 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
- Default: `-`
- Example: `-all,+qwq-32b,+deepseek-r1`
## FAL
### `ENABLED_FAL`
- Type: Optional
- Description: Enables FAL as a model provider by default. Set to `0` to disable the FAL service.
- Default: `1`
- Example: `0`
### `FAL_API_KEY`
- Type: Required
- Description: This is the API key you applied for in the FAL service.
- Default: -
- Example: `fal-xxxxxx...xxxxxx`
### `FAL_MODEL_LIST`
- Type: Optional
- Description: Used to control the FAL model list. Use `+` to add a model, `-` to hide a model, and `model_name=display_name` to customize the display name of a model. Separate multiple entries with commas. The definition syntax follows the same rules as other providers' model lists.
- Default: `-`
- Example: `-all,+fal-model-1,+fal-model-2=fal-special`
The above example disables all models first, then enables `fal-model-1` and `fal-model-2` (displayed as `fal-special`).
[model-list]: /docs/self-hosting/advanced/model-list
@@ -624,4 +624,29 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
- 默认值:`-`
- 示例:`-all,+qwq-32b,+deepseek-r1`
## FAL
### `ENABLED_FAL`
- 类型:可选
- 描述:默认启用 FAL 作为模型供应商,当设为 0 时关闭 FAL 服务
- 默认值:`1`
- 示例:`0`
### `FAL_API_KEY`
- 类型:必选
- 描述:这是你在 FAL 服务中申请的 API 密钥
- 默认值:-
- 示例:`fal-xxxxxx...xxxxxx`
### `FAL_MODEL_LIST`
- 类型:可选
- 描述:用来控制 FAL 模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则与其他 provider 保持一致。
- 默认值:`-`
- 示例:`-all,+fal-model-1,+fal-model-2=fal-special`
上述示例表示先禁用所有模型,再启用 `fal-model-1` 和 `fal-model-2`(显示名为 `fal-special`)。
[model-list]: /zh/docs/self-hosting/advanced/model-list
@@ -0,0 +1,65 @@
---
title: Resolving AI Image Generation Timeout on Vercel
description: >-
Learn how to resolve timeout issues when using AI image generation models like gpt-image-1 on Vercel by enabling Fluid Compute for extended execution time.
tags:
- Vercel
- AI Image Generation
- Timeout
- Fluid Compute
- gpt-image-1
---
# Resolving AI Image Generation Timeout on Vercel
## Problem Description
When using AI image generation models (such as `gpt-image-1`) on Vercel, you may encounter timeout errors. This occurs because AI image generation typically requires more than 1 minute to complete, which exceeds Vercel's default function execution time limit.
Common error symptoms include:
- Function timeout errors during image generation
- Failed image generation requests after approximately 60 seconds
- "Function execution timed out" messages
### Typical Log Symptoms
In your Vercel function logs, you may see entries like this:
```plaintext
JUL 16 18:39:09.51 POST 504 /trpc/async/image.createImage
Provider runtime map found for provider: openai
```
The key indicators are:
- **Status Code**: `504` (Gateway Timeout)
- **Endpoint**: `/trpc/async/image.createImage` or similar image generation endpoints
- **Timing**: Usually occurs around 60 seconds after the request starts
## Solution: Enable Fluid Compute
For projects created before Vercel's dashboard update, you can resolve this issue by enabling Fluid Compute, which extends the maximum execution duration to 300 seconds.
### Steps to Enable Fluid Compute (Legacy Vercel Dashboard)
1. Go to your project dashboard on Vercel
2. Navigate to the **Settings** tab
3. Find the **Functions** section
4. Enable **Fluid Compute** as shown in the screenshot below:
![Enable Fluid Compute](https://hub-apac-1.lobeobjects.space/docs/2e2ff332c4b440b584efe1d7ba46aed5.png)
5. After enabling, the maximum execution duration will be extended to 300 seconds by default
### Important Notes
- **For new projects**: Newer Vercel projects have Fluid Compute enabled by default, so this issue primarily affects legacy projects
## Additional Resources
For more information about Vercel's function limitations and Fluid Compute:
- [Vercel Fluid Compute Documentation](https://vercel.com/docs/fluid-compute)
- [Vercel Functions Limitations](https://vercel.com/docs/functions/limitations#max-duration)
@@ -0,0 +1,63 @@
---
title: 解决 Vercel 上 AI 绘画生图超时问题
description: 了解如何通过开启 Fluid Compute 来解决在 Vercel 上使用 gpt-image-1 等 AI 绘画模型时遇到的超时问题。
tags:
- Vercel
- AI 绘画
- 超时问题
- Fluid Compute
- gpt-image-1
---
# 解决 Vercel 上 AI 绘画生图超时问题
## 问题描述
在 Vercel 上使用 AI 绘画模型(如 `gpt-image-1`)时,您可能会遇到超时错误。这是因为 AI 绘画生成通常需要超过 1 分钟的时间,超出了 Vercel 默认的函数执行时间限制。
常见的错误症状包括:
- 图像生成过程中出现函数超时错误
- 图像生成请求在大约 60 秒后失败
- 出现 "函数执行超时" 的错误消息
### 典型的日志现象
在您的 Vercel 函数日志中,您可能会看到类似这样的条目:
```plaintext
JUL 16 18:39:09.51 POST 504 /trpc/async/image.createImage
Provider runtime map found for provider: openai
```
关键指标包括:
- **状态码**: `504`(网关超时)
- **端点**: `/trpc/async/image.createImage` 或类似的图像生成端点
- **时间**: 通常在请求开始后约 60 秒出现
## 解决方案:开启 Fluid Compute
对于在 Vercel 控制台更新前创建的项目,您可以通过开启 Fluid Compute 来解决此问题,这将最大执行时长延长至 300 秒。
### 开启 Fluid Compute 的步骤(旧版 Vercel 控制台)
1. 前往您在 Vercel 上的项目控制台
2. 进入 **Settings**(设置)选项卡
3. 找到 **Functions**(函数)部分
4. 按照下方截图所示开启 **Fluid Compute**
![开启 Fluid Compute](https://hub-apac-1.lobeobjects.space/docs/2e2ff332c4b440b584efe1d7ba46aed5.png)
5. 开启后,最大执行时长将默认延长至 300 秒
### 重要说明
- **新项目**:较新的 Vercel 项目默认已启用 Fluid Compute,因此此问题主要影响旧版项目
## 其他资源
有关 Vercel 函数限制和 Fluid Compute 的更多信息:
- [Vercel Fluid Compute 文档](https://vercel.com/docs/fluid-compute)
- [Vercel 函数限制说明](https://vercel.com/docs/functions/limitations#max-duration)
+1 -1
View File
@@ -30,7 +30,7 @@ This article will guide you on how to use AI21 Labs within LobeChat.
### Step 2: Configure AI21 Labs in LobeChat
- Go to the `Settings` page in LobeChat
- Under `Language Model`, find the setting for `AI21 Labs`
- Under `AI Service Provider`, find the setting for `AI21 Labs`
<Image alt={'Enter API Key'} inStep src={'https://github.com/user-attachments/assets/9336d6c5-2a83-4aa9-854e-75e245b665cb'} />
+1 -1
View File
@@ -28,7 +28,7 @@ tags:
### 步骤二:在 LobeChat 中配置 AI21 Labs
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到 `AI21labs` 的设置项
- 在`AI 服务商`下找到 `AI21labs` 的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/user-attachments/assets/9336d6c5-2a83-4aa9-854e-75e245b665cb'} />
+1 -1
View File
@@ -28,7 +28,7 @@ This article will guide you on how to use the 360AI in LobeChat.
### Step 2: Configure 360AI in LobeChat
- Access the `Settings` interface in LobeChat
- Under `Language Models`, find the option for `360`
- Under `AI Service Provider`, find the option for `360`
<Image alt={'Enter API Key'} inStep src={'https://github.com/user-attachments/assets/a53deb11-2c14-441a-8a5c-a0f3a74e2a63'} />
+1 -1
View File
@@ -28,7 +28,7 @@ tags:
### 步骤二:在 LobeChat 中配置 360 智脑
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到 `360` 的设置项
- 在`AI 服务商`下找到 `360` 的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/user-attachments/assets/a53deb11-2c14-441a-8a5c-a0f3a74e2a63'} />
+1 -1
View File
@@ -37,7 +37,7 @@ The Anthropic Claude API is now available for everyone to use. This document wil
### Step 2: Configure Anthropic Claude in LobeChat
- Access the `Settings` interface in LobeChat.
- Find the setting for `Anthropic Claude` under `Language Models`.
- Find the setting for `Anthropic Claude` under `AI Service Provider`.
<Image alt={'Enter API Key'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/ff9c3eb8-412b-4275-80be-177ae7b7acbc'} />
+1 -1
View File
@@ -36,7 +36,7 @@ Anthropic Claude API 现在可供所有人使用,本文档将指导你如何
### 步骤二:在 LobeChat 中配置 Anthropic Claude
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到`Anthropic Claude`的设置项
- 在`AI 服务商`下找到`Anthropic Claude`的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/ff9c3eb8-412b-4275-80be-177ae7b7acbc'} />
+1 -1
View File
@@ -39,7 +39,7 @@ This document will guide you on how to use [Azure OpenAI](https://oai.azure.com/
### Step 2: Configure Azure OpenAI in LobeChat
- Access the `Settings` interface in LobeChat.
- Find the setting for `Azure OpenAI` under `Language Model`.
- Find the setting for `Azure OpenAI` under `AI Service Provider`.
<Image alt={'Enter the API key'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/63d9f6d4-5b78-4c65-8cd1-ff8b7f143406'} />
+1 -1
View File
@@ -33,7 +33,7 @@ tags:
### 步骤二:在 LobeChat 中配置 Azure OpenAI
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到`Azure OpenAI`的设置项
- 在`AI 服务商`下找到`Azure OpenAI`的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/63d9f6d4-5b78-4c65-8cd1-ff8b7f143406'} />
+1 -1
View File
@@ -27,7 +27,7 @@ This article will guide you on how to use Baichuan in LobeChat:
### Step 2: Configure Baichuan in LobeChat
- Visit the `Settings` interface in LobeChat
- Find the setting for `Baichuan` under `Language Model`
- Find the setting for `Baichuan` under `AI Service Provider`
<Image alt={'Enter API Key'} inStep src={'https://github.com/user-attachments/assets/dec6665a-b3ec-4c50-a57f-7c7eb3160e7b'} />
+1 -1
View File
@@ -26,7 +26,7 @@ tags:
### 步骤二:在 LobeChat 中配置百川
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到`百川`的设置项
- 在`AI 服务商`下找到`百川`的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/user-attachments/assets/dec6665a-b3ec-4c50-a57f-7c7eb3160e7b'} />
+1 -1
View File
@@ -71,7 +71,7 @@ This document will guide you on how to use Amazon Bedrock in LobeChat:
### Step 3: Configure Amazon Bedrock in LobeChat
- Access the `Settings` interface in LobeChat
- Find the setting for `Amazon Bedrock` under `Language Models` and open it
- Find the setting for `Amazon Bedrock` under `AI Service Provider` and open it
<Image alt={'Enter Amazon Bedrock keys in LobeChat'} inStep src={'https://github.com/lobehub/lobe-chat/assets/34400653/7468594b-3355-4cb9-85bc-c9dace137653'} />
+1 -1
View File
@@ -68,7 +68,7 @@ Amazon Bedrock 是一个完全托管的基础模型 API 服务,允许用户通
### 步骤三:在 LobeChat 中配置 Amazon Bedrock
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到`Amazon Bedrock`的设置项并打开
- 在`AI 服务商`下找到`Amazon Bedrock`的设置项并打开
<Image alt={'LobeChat 中填写 Amazon Bedrock 密钥'} inStep src={'https://github.com/lobehub/lobe-chat/assets/34400653/7468594b-3355-4cb9-85bc-c9dace137653'} />
+1 -1
View File
@@ -41,7 +41,7 @@ This document will guide you on how to use Cloudflare Workers AI in LobeChat:
### Step 2: Configure Cloudflare Workers AI in LobeChat
- Go to the `Settings` interface in LobeChat.
- Under `Language Model`, find the `Cloudflare` settings.
- Under `AI Service Provider`, find the `Cloudflare` settings.
<Image alt={'Input API Token'} inStep src={'https://github.com/user-attachments/assets/82a7ebe0-69ad-43b6-8767-1316b443fa03'} />
+1 -1
View File
@@ -40,7 +40,7 @@ tags:
### 步骤二:在 LobeChat 中配置 Cloudflare Workers AI
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到 `Cloudflare` 的设置项
- 在`AI 服务商`下找到 `Cloudflare` 的设置项
<Image alt={'填入访问令牌'} inStep src={'https://github.com/user-attachments/assets/82a7ebe0-69ad-43b6-8767-1316b443fa03'} />
+1 -1
View File
@@ -46,7 +46,7 @@ This document will guide you on how to use DeepSeek in LobeChat:
### Step 2: Configure DeepSeek in LobeChat
- Access the `App Settings` interface in LobeChat.
- Find the setting for `DeepSeek` under `Language Models`.
- Find the setting for `DeepSeek` under `AI Service Provider`.
<Image alt={'Enter Deepseek API Key'} inStep src={'https://github.com/user-attachments/assets/aaa3e2c5-7f16-4cfb-86b6-2814a1aafe3a'} />
+1 -1
View File
@@ -43,7 +43,7 @@ tags:
### 步骤二:在 LobeChat 中配置 DeepSeek
- 访问 LobeChat 的 `应用设置`界面
- 在 `语言模型` 下找到 `DeepSeek` 的设置项
- 在 `AI 服务商` 下找到 `DeepSeek` 的设置项
<Image alt={'填写 Deepseek API 密钥'} inStep src={'https://github.com/user-attachments/assets/aaa3e2c5-7f16-4cfb-86b6-2814a1aafe3a'} />
+69
View File
@@ -0,0 +1,69 @@
---
title: Using Fal API Key in LobeChat
description: >-
Learn how to integrate Fal API Key in LobeChat for AI image and video generation using cutting-edge models like FLUX, Kling, and more.
tags:
- Fal AI
- Image Generation
- Video Generation
- API Key
- Web UI
---
# Using Fal in LobeChat
<Image alt={'Using Fal in LobeChat'} cover src={'https://hub-apac-1.lobeobjects.space/docs/f253e749baaa2ccac498014178f93091.png'} />
[Fal.ai](https://fal.ai/) is a lightning-fast inference platform specialized in AI media generation, hosting state-of-the-art models for image and video creation including FLUX, Kling, HiDream, and other cutting-edge generative models. This document will guide you on how to use Fal in LobeChat:
<Steps>
### Step 1: Obtain Fal API Key
- Register for a [Fal.ai account](https://fal.ai/).
- Navigate to [API Keys dashboard](https://fal.ai/dashboard/keys) and click **Add key** to create a new API key.
- Copy the generated API key and keep it secure; it will only be shown once.
<Image
alt={'Open the creation window'}
inStep
src={
'https://hub-apac-1.lobeobjects.space/docs/3f3676e7f9c04a55603bc1174b636b45.png'
}
/>
<Image
alt={'Create API Key'}
inStep
src={
'https://hub-apac-1.lobeobjects.space/docs/214cc5019d9c0810951b33215349136e.png'
}
/>
<Image
alt={'Retrieve API Key'}
inStep
src={
'https://hub-apac-1.lobeobjects.space/docs/499a447e98dcc79407d56495d0305e2a.png'
}
/>
### Step 2: Configure Fal in LobeChat
- Visit the `Settings` page in LobeChat.
- Under **AI Service Provider**, locate the **Fal** configuration section.
<Image alt={'Enter API Key'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/fa056feecba0133c76abe1ad12706c05.png'} />
- Paste the API key you obtained.
- Choose a Fal model (e.g. `fal-ai/flux-pro`, `fal-ai/kling-video`, `fal-ai/hidream-i1-fast`) for image or video generation.
<Image alt={'Select Fal model for media generation'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/7560502f31b8500032922103fc22e69b.png'} />
<Callout type={'warning'}>
During usage, you may incur charges according to Fal's pricing policy. Please review Fal's
official pricing before heavy usage.
</Callout>
</Steps>
You can now use Fal's advanced image and video generation models directly within LobeChat to create stunning visual content.
+68
View File
@@ -0,0 +1,68 @@
---
title: 在 LobeChat 中使用 Fal API Key
description: >-
学习如何在 LobeChat 中配置和使用 Fal API Key,使用 FLUX、Kling 等尖端模型进行 AI 图像和视频生成。
tags:
- Fal
- 图像生成
- 视频生成
- API Key
- Web UI
---
# 在 LobeChat 中使用 Fal
<Image alt={'在 LobeChat 中使用 Fal'} cover src={'https://hub-apac-1.lobeobjects.space/docs/f253e749baaa2ccac498014178f93091.png'} />
[Fal.ai](https://fal.ai/) 是一个专门从事 AI 媒体生成的快速推理平台,提供包括 FLUX、Kling、HiDream 等在内的最先进图像和视频生成模型。本文将指导你如何在 LobeChat 中使用 Fal
<Steps>
### 步骤一:获取 Fal API Key
- 注册 [Fal.ai](https://fal.ai/) 账户;
- 前往 [API Keys 控制台](https://fal.ai/dashboard/keys),点击 **Add key** 创建新的 API 密钥;
- 复制生成的 API Key 并妥善保存,它只会显示一次。
<Image
alt={'打开创建窗口'}
inStep
src={
'https://hub-apac-1.lobeobjects.space/docs/3f3676e7f9c04a55603bc1174b636b45.png'
}
/>
<Image
alt={'创建 API Key'}
inStep
src={
'https://hub-apac-1.lobeobjects.space/docs/214cc5019d9c0810951b33215349136e.png'
}
/>
<Image
alt={'获取 API Key'}
inStep
src={
'https://hub-apac-1.lobeobjects.space/docs/499a447e98dcc79407d56495d0305e2a.png'
}
/>
### 步骤二:在 LobeChat 中配置 Fal
- 访问 LobeChat 的 `设置` 页面;
- 在 `AI服务商` 下找到 `Fal` 的设置项;
<Image alt={'填入 API 密钥'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/fa056feecba0133c76abe1ad12706c05.png'} />
- 粘贴获取到的 API Key
- 选择一个 Fal 模型(如 `fal-ai/flux-pro`、`fal-ai/kling-video`、`fal-ai/hidream-i1-fast`)用于图像或视频生成。
<Image alt={'选择 Fal 模型进行媒体生成'} inStep src={'https://hub-apac-1.lobeobjects.space/docs/7560502f31b8500032922103fc22e69b.png'} />
<Callout type={'warning'}>
在使用过程中,你可能需要向 Fal 支付相应费用,请在大量调用前查阅 Fal 的官方计费政策。
</Callout>
</Steps>
至此,你已经可以在 LobeChat 中使用 Fal 提供的先进图像和视频生成模型来创作精美的视觉内容了。
+1 -1
View File
@@ -39,7 +39,7 @@ This article will guide you on how to use Fireworks AI in LobeChat.
### Step 2: Configure Fireworks AI in LobeChat
- Access the `Settings` interface in LobeChat
- Under `Language Model`, locate the settings for `Fireworks AI`
- Under `AI Service Provider`, locate the settings for `Fireworks AI`
<Image alt={'Enter API Key'} inStep src={'https://github.com/user-attachments/assets/12c1957d-f050-4235-95da-d55ddedfa6c9'} />
+1 -1
View File
@@ -36,7 +36,7 @@ tags:
### 步骤二:在 LobeChat 中配置 Fireworks AI
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到 `Fireworks AI` 的设置项
- 在`AI 服务商`下找到 `Fireworks AI` 的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/user-attachments/assets/12c1957d-f050-4235-95da-d55ddedfa6c9'} />
+1 -1
View File
@@ -40,7 +40,7 @@ This article will guide you on how to use Gitee AI in LobeChat.
### Step 2: Configure Gitee AI in LobeChat
- Access the `Settings` page in LobeChat
- Under `Language Models`, find the settings for `Gitee AI`
- Under `AI Service Provider`, find the settings for `Gitee AI`
<Image alt={'Enter API Key'} inStep src={'https://github.com/user-attachments/assets/eaa2a1fb-41ad-473d-ac10-a39c05886425'} />
+1 -1
View File
@@ -37,7 +37,7 @@ tags:
### 步骤二:在 LobeChat 中配置 Gitee AI
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到 `Gitee AI` 的设置项
- 在`AI 服务商`下找到 `Gitee AI` 的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/user-attachments/assets/eaa2a1fb-41ad-473d-ac10-a39c05886425'} />
+1 -1
View File
@@ -54,7 +54,7 @@ Currently, the usage of the Playground and free API is subject to limits on the
### Step 2: Configure GitHub Models in LobeChat
- Navigate to the `Settings` interface in LobeChat.
- Under `Language Models`, find the GitHub settings.
- Under `AI Service Provider`, find the GitHub settings.
<Image alt={'Entering Access Token'} inStep src={'https://github.com/user-attachments/assets/a00f06cc-da7c-41e8-a4d5-d4b675a22673'} />
+1 -1
View File
@@ -53,7 +53,7 @@ tags:
### 步骤二:在 LobeChat 中配置 GitHub Models
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到 `GitHub` 的设置项
- 在`AI 服务商`下找到 `GitHub` 的设置项
<Image alt={'填入访问令牌'} inStep src={'https://github.com/user-attachments/assets/a00f06cc-da7c-41e8-a4d5-d4b675a22673'} />
+1 -1
View File
@@ -37,7 +37,7 @@ This document will guide you on how to use Google Gemini in LobeChat:
### Step 2: Configure OpenAI in LobeChat
- Go to the `Settings` interface in LobeChat
- Find the setting for `Google Gemini` under `Language Models`
- Find the setting for `Google Gemini` under `AI Service Provider`
<Image alt={'Enter Google Gemini API Key in LobeChat'} inStep src={'https://github.com/lobehub/lobe-chat/assets/34400653/11442ce4-a615-49c4-937a-ca2ae93dd27c'} />
+1 -1
View File
@@ -35,7 +35,7 @@ Gemini AI 是由 Google AI 创建的一组大型语言模型(LLM),以其
### 步骤二:在 LobeChat 中配置 OpenAI
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到`Google Gemini`的设置项
- 在`AI 服务商`下找到`Google Gemini`的设置项
<Image alt={'LobeChat 中填写 Google Gemini API 密钥'} inStep src={'https://github.com/lobehub/lobe-chat/assets/34400653/11442ce4-a615-49c4-937a-ca2ae93dd27c'} />
+1 -1
View File
@@ -45,7 +45,7 @@ This document will guide you on how to use Groq in LobeChat:
### Configure Groq in LobeChat
You can find the Groq configuration option in `Settings` -> `Language Model`, where you can input the API Key you just obtained.
You can find the Groq configuration option in `Settings` -> `AI Service Provider`, where you can input the API Key you just obtained.
<Image alt={'Groq service provider settings'} height={274} inStep src={'https://github.com/lobehub/lobe-chat/assets/34400653/88948a3a-6681-4a8d-9734-a464e09e4957'} />
</Steps>
+1 -1
View File
@@ -40,7 +40,7 @@ Groq 的 [LPU 推理引擎](https://wow.groq.com/news_press/groq-lpu-inference-e
### 在 LobeChat 中配置 Groq
你可以在 `设置` -> `语言模型` 中找到 Groq 的配置选项,将刚才获取的 API Key 填入。
你可以在 `设置` -> `AI 服务商` 中找到 Groq 的配置选项,将刚才获取的 API Key 填入。
<Image alt={'Groq 服务商设置'} height={274} inStep src={'https://github.com/lobehub/lobe-chat/assets/34400653/88948a3a-6681-4a8d-9734-a464e09e4957'} />
</Steps>
+1 -1
View File
@@ -34,7 +34,7 @@ This article will guide you on how to use Tencent Hunyuan in LobeChat.
### Step 2: Configure Tencent Hunyuan in LobeChat
- Go to the `Settings` page in LobeChat
- Find the `Tencent Hunyuan` settings under `Language Models`
- Find the `Tencent Hunyuan` settings under `AI Service Provider`
<Image alt={'Enter API Key'} inStep src={'https://github.com/user-attachments/assets/796c94af-9bad-4e3c-b1c7-dbb17c215c56'} />
+1 -1
View File
@@ -32,7 +32,7 @@ tags:
### 步骤二:在 LobeChat 中配置腾讯混元
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到 `腾讯混元` 的设置项
- 在`AI 服务商`下找到 `腾讯混元` 的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/user-attachments/assets/796c94af-9bad-4e3c-b1c7-dbb17c215c56'} />
+1 -1
View File
@@ -35,7 +35,7 @@ This article will guide you on how to use InternLM in LobeChat.
### Step 2: Configure InternLM in LobeChat
- Go to the `Settings` interface in LobeChat
- Find the settings option for `InternLM` under `Language Models`
- Find the settings option for `InternLM` under `AI Service Provider`
<Image alt={'Enter API Key'} inStep src={'https://github.com/user-attachments/assets/8ec7656e-1e3d-41e0-95a0-f6883135c2fc'} />
+1 -1
View File
@@ -32,7 +32,7 @@ tags:
### 步骤二:在 LobeChat 中配置书生浦语
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到 `书生浦语` 的设置项
- 在`AI 服务商`下找到 `书生浦语` 的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/user-attachments/assets/8ec7656e-1e3d-41e0-95a0-f6883135c2fc'} />
+1 -1
View File
@@ -35,7 +35,7 @@ This document will guide you on how to use Jina AI in LobeChat:
### Step 2: Configure Jina AI in LobeChat
- Visit LobeChat's `Application Settings` interface.
- Find the `Jina AI` setting under `Language Model`.
- Find the `Jina AI` setting under `AI Service Provider`.
<Image alt={'Fill in Jina AI API Key'} inStep src={'https://github.com/user-attachments/assets/1077bee5-b379-4063-b7bd-23b98ec146e2'} />
+1 -1
View File
@@ -34,7 +34,7 @@ tags:
### 步骤二:在 LobeChat 中配置 Jina AI
- 访问 LobeChat 的 `应用设置`界面
- 在 `语言模型` 下找到 `Jina AI` 的设置项
- 在 `AI 服务商` 下找到 `Jina AI` 的设置项
<Image alt={'填写 Jina AI API 密钥'} inStep src={'https://github.com/user-attachments/assets/1077bee5-b379-4063-b7bd-23b98ec146e2'} />
+1 -1
View File
@@ -42,7 +42,7 @@ This document will guide you on how to use Minimax in LobeChat:
### Step 2: Configure MiniMax in LobeChat
- Go to the `Settings` interface of LobeChat
- Find the setting for `MiniMax` under `Language Model`
- Find the setting for `MiniMax` under `AI Service Provider`
<Image alt={'Enter API Key'} inStep src={'https://github.com/lobehub/lobe-chat/assets/34400653/b839e04e-0cef-46a3-bb84-0484a3f51c69'} />
+1 -1
View File
@@ -41,7 +41,7 @@ tags:
### 步骤二:在 LobeChat 中配置 MiniMax
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到`MiniMax`的设置项
- 在`AI 服务商`下找到`MiniMax`的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/lobehub/lobe-chat/assets/34400653/b839e04e-0cef-46a3-bb84-0484a3f51c69'} />
+1 -1
View File
@@ -26,7 +26,7 @@ The Mistral AI API is now available for everyone to use. This document will guid
### Step 2: Configure Mistral AI in LobeChat
- Go to the `Settings` interface in LobeChat
- Find the setting for `Mistral AI` under `Language Model`
- Find the setting for `Mistral AI` under `AI Service Provider`
<Image alt={'Enter API Key'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/ba8e688a-e0c1-4567-9013-94205f83fc60'} />
+1 -1
View File
@@ -24,7 +24,7 @@ Mistral AI API 现在可供所有人使用,本文档将指导你如何在 Lobe
### 步骤二:在 LobeChat 中配置 Mistral AI
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到`Mistral AI`的设置项
- 在`AI 服务商`下找到`Mistral AI`的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/ba8e688a-e0c1-4567-9013-94205f83fc60'} />
+1 -1
View File
@@ -25,7 +25,7 @@ The Moonshot AI API is now available for everyone to use. This document will gui
### Step 2: Configure Moonshot AI in LobeChat
- Visit the `Settings` interface in LobeChat
- Find the setting for `Moonshot AI` under `Language Models`
- Find the setting for `Moonshot AI` under `AI Service Provider`
<Image alt={'Enter API Key'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/e1b5f84f-015e-437c-98cc-a3431fa3b077'} />
+1 -1
View File
@@ -23,7 +23,7 @@ Moonshot AI API 现在可供所有人使用,本文档将指导你如何在 Lob
### 步骤二:在 LobeChat 中配置 Moonshot AI
- 访问 LobeChat 的`设置`界面
- 在`语言模型`下找到`Moonshot AI`的设置项
- 在`AI 服务商`下找到`Moonshot AI`的设置项
<Image alt={'填入 API 密钥'} inStep src={'https://github.com/lobehub/lobe-chat/assets/17870709/e1b5f84f-015e-437c-98cc-a3431fa3b077'} />
+1 -1
View File
@@ -38,7 +38,7 @@ This document will guide you on how to integrate Novita AI in LobeChat:
### Step 3: Configure Novita AI in LobeChat
- Visit the `Settings` interface in LobeChat
- Find the setting for `novita.ai` under `Language Model`
- Find the setting for `novita.ai` under `AI Service Provider`
<Image alt={'Enter Novita AI API key in LobeChat'} inStep src={'https://github.com/user-attachments/assets/00c02637-873e-4e7e-9dc3-a95085b16dd7'} />
+1 -1
View File
@@ -38,7 +38,7 @@ tags:
### 步骤三:在 LobeChat 中配置 Novita AI
- 访问 LobeChat 的 `设置` 界面
- 在 `语言模型` 下找到 `novita.ai` 的设置项
- 在 `AI 服务商` 下找到 `novita.ai` 的设置项
- 打开 novita.ai 并填入获得的 API 密钥
<Image alt={'在 LobeChat 中输入 Novita AI API 密钥'} inStep src={'https://github.com/user-attachments/assets/00c02637-873e-4e7e-9dc3-a95085b16dd7'} />

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