Compare commits

...

581 Commits

Author SHA1 Message Date
ONLY-yours b8b1ab6616 feat: add loading back 2025-11-17 17:20:03 +08:00
ONLY-yours 3891015a3d fix: test mobile 2025-11-17 16:33:16 +08:00
ONLY-yours 5babb7d826 fix: try to fixed 2025-11-17 16:11:20 +08:00
ONLY-yours a7504b696a test: test the loading error 2025-11-17 15:38:29 +08:00
ONLY-yours 9dc4308942 fix: add router ErrorBoundary 2025-11-17 15:16:37 +08:00
ONLY-yours 082117998d fix: fixed the test error 2025-11-17 11:42:26 +08:00
ONLY-yours 9a74d6c045 fix: fix the reload was loading page problem 2025-11-17 11:26:38 +08:00
ONLY-yours b1a4f24dc9 fix: mobile chat settings go back 2025-11-17 11:19:38 +08:00
ONLY-yours c47551775b fix: delete uesless code 2025-11-17 11:04:24 +08:00
ONLY-yours 2d83300795 fix: delete useless code 2025-11-17 10:51:20 +08:00
ONLY-yours 0915538da8 Merge remote-tracking branch 'origin/next' into refactor/changeAllToSpa 2025-11-17 10:35:43 +08:00
renovate[bot] b76e3c85b9 Update all non-major dependencies (#10177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 09:56:56 +08:00
lobehubbot 29ce0225b2 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-17 01:49:09 +00:00
semantic-release-bot 06878829c9 🔖 chore(release): v2.0.0-next.69 [skip ci]
## [Version&nbsp;2.0.0-next.69](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.68...v2.0.0-next.69)
<sup>Released on **2025-11-17**</sup>

#### ♻ Code Refactoring

- **misc**: Remove `language_model_settings` and remove isDeprecatedEdition.

<br/>

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

#### Code refactoring

* **misc**: Remove `language_model_settings` and remove isDeprecatedEdition, closes [#10264](https://github.com/lobehub/lobe-chat/issues/10264) ([ae613c7](https://github.com/lobehub/lobe-chat/commit/ae613c7))

</details>

<div align="right">

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

</div>
2025-11-17 01:47:59 +00:00
Arvin Xu ae613c7c35 ♻️ refactor: remove language_model_settings and remove isDeprecatedEdition (#10264) 2025-11-17 09:35:49 +08:00
lobehubbot 8184f9d097 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-16 15:20:51 +00:00
semantic-release-bot 7fac37b983 🔖 chore(release): v2.0.0-next.68 [skip ci]
## [Version&nbsp;2.0.0-next.68](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.67...v2.0.0-next.68)
<sup>Released on **2025-11-16**</sup>

#### 🐛 Bug Fixes

- **misc**: The tool to fail execution on ollama when a message contains b….

<br/>

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

#### What's fixed

* **misc**: The tool to fail execution on ollama when a message contains b…, closes [#10259](https://github.com/lobehub/lobe-chat/issues/10259) ([1ad8080](https://github.com/lobehub/lobe-chat/commit/1ad8080))

</details>

<div align="right">

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

</div>
2025-11-16 15:19:33 +00:00
Arvin Xu d3570879da 🔨 chore: unpin eta (#10260) 2025-11-16 23:07:44 +08:00
Hypo 1ad80809cf 🐛 fix: the tool to fail execution on ollama when a message contains b… (#10259) 2025-11-16 23:06:33 +08:00
lobehubbot 2c97a9e920 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-16 11:59:30 +00:00
semantic-release-bot 246cce28db 🔖 chore(release): v2.0.0-next.67 [skip ci]
## [Version&nbsp;2.0.0-next.67](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.66...v2.0.0-next.67)
<sup>Released on **2025-11-16**</sup>

#### ♻ Code Refactoring

- **misc**: Refactor to virtua.

<br/>

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

#### Code refactoring

* **misc**: Refactor to virtua, closes [#10151](https://github.com/lobehub/lobe-chat/issues/10151) ([9ffb689](https://github.com/lobehub/lobe-chat/commit/9ffb689))

</details>

<div align="right">

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

</div>
2025-11-16 11:58:19 +00:00
Arvin Xu 9ffb6891e4 ♻️ refactor: refactor to virtua (#10151)
* refactor to virtua

* try virtua

* 默认滚动到底部

* fix
2025-11-16 19:46:41 +08:00
lobehubbot 766ca942b3 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-16 07:02:41 +00:00
semantic-release-bot 147975ae46 🔖 chore(release): v2.0.0-next.66 [skip ci]
## [Version&nbsp;2.0.0-next.66](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.65...v2.0.0-next.66)
<sup>Released on **2025-11-16**</sup>

####  Features

- **misc**: Support to collapse message.

<br/>

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

#### What's improved

* **misc**: Support to collapse message, closes [#10234](https://github.com/lobehub/lobe-chat/issues/10234) ([4cd6347](https://github.com/lobehub/lobe-chat/commit/4cd6347))

</details>

<div align="right">

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

</div>
2025-11-16 07:01:10 +00:00
renovate[bot] a6c3317192 Update dependency lucide-react to ^0.553.0 (#10250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 14:48:08 +08:00
Arvin Xu 4cd6347d7e feat: support to collapse message (#10234)
*  feat: add message collapse functionality

- Add collapsed field to MessageMetadata type and schema
- Add isMessageCollapsed selector to check message collapse state
- Add toggleMessageCollapsed action with optimistic update
- Export getDisplayMessageById for internal use
- Collapse state persists to database via metadata field

* 💄 ui: add collapse UI for assistant messages

- Add collapse/expand action icons to action bar
- Add collapsed message style with 200px max height and gradient overlay
- Add collapse/expand translations (zh-CN)
- Integrate with toggleMessageCollapsed store action
- Show appropriate icon based on collapsed state

* support CollapsedMessage

* update

* improve test time

* refactor fixtures

* fix tests

* improve i18n
2025-11-16 14:46:27 +08:00
Shinji-Li cd7d955e3d 🔨 chore: change the market base url to online market.lobehub.com (#10247)
* fix: change the market base url to online market.lobehub.com

* feat: update the market callback layout
2025-11-16 12:14:27 +08:00
renovate[bot] 61901ddb07 Update dependency ollama to ^0.6.3 (#10244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 11:34:45 +08:00
renovate[bot] 77ed938cfb Update dependency @vercel/otel to v2 (#9969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 11:34:36 +08:00
renovate[bot] 4c3ac3bce7 Update dependency dayjs to >=1.11.19 (#10241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 11:33:22 +08:00
renovate[bot] a142b3384f Update aws-sdk-js-v3 monorepo to ~3.932.0 (#10119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 11:31:49 +08:00
renovate[bot] ee80f613df Update dependency nanoid to >=5.1.6 (#10243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 11:24:48 +08:00
lobehubbot 7d05d0270c 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-16 02:12:14 +00:00
semantic-release-bot acd5954f15 🔖 chore(release): v2.0.0-next.65 [skip ci]
## [Version&nbsp;2.0.0-next.65](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.64...v2.0.0-next.65)
<sup>Released on **2025-11-16**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

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

</details>

<div align="right">

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

</div>
2025-11-16 02:11:08 +00:00
LobeHub Bot a52c9e5f24 🤖 style: update i18n (#10235) 2025-11-16 09:58:21 +08:00
lobehubbot bcb998d767 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-15 16:51:17 +00:00
semantic-release-bot c6410b29c5 🔖 chore(release): v2.0.0-next.64 [skip ci]
## [Version&nbsp;2.0.0-next.64](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.63...v2.0.0-next.64)
<sup>Released on **2025-11-15**</sup>

#### ♻ Code Refactoring

- **misc**: Refactor package types.

<br/>

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

#### Code refactoring

* **misc**: Refactor package types, closes [#10233](https://github.com/lobehub/lobe-chat/issues/10233) ([9872409](https://github.com/lobehub/lobe-chat/commit/9872409))

</details>

<div align="right">

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

</div>
2025-11-15 16:50:03 +00:00
Arvin Xu 9872409d98 ♻️ refactor: refactor package types (#10233)
* refactor packages types

* remove lite mode
2025-11-16 00:37:55 +08:00
lobehubbot 319a622778 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-15 16:08:24 +00:00
semantic-release-bot 85153f2464 🔖 chore(release): v2.0.0-next.63 [skip ci]
## [Version&nbsp;2.0.0-next.63](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.62...v2.0.0-next.63)
<sup>Released on **2025-11-15**</sup>

####  Features

- **misc**: Show orphaned tool message and support delete tool message.

<br/>

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

#### What's improved

* **misc**: Show orphaned tool message and support delete tool message, closes [#10232](https://github.com/lobehub/lobe-chat/issues/10232) ([38cfd26](https://github.com/lobehub/lobe-chat/commit/38cfd26))

</details>

<div align="right">

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

</div>
2025-11-15 16:07:14 +00:00
Arvin Xu 38cfd266f4 feat: show orphaned tool message and support delete tool message (#10232)
* show  orphaned tool message

* support delete messages

* update i18n

* clean console.log

* improve system role

* fix
2025-11-15 23:55:13 +08:00
ONLY-yours 53fc0642e0 feat: use more simple way to update session hydration 2025-11-15 19:31:05 +08:00
ONLY-yours a8c725abd5 Merge remote-tracking branch 'origin/next' into refactor/changeAllToSpa 2025-11-15 19:08:58 +08:00
ONLY-yours b8a7f6e9eb feat: update the useQueryParams throttleMs params 2025-11-15 19:05:17 +08:00
lobehubbot 2c93d9bb1a 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-15 06:46:35 +00:00
semantic-release-bot a2c3b9e375 🔖 chore(release): v2.0.0-next.62 [skip ci]
## [Version&nbsp;2.0.0-next.62](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.61...v2.0.0-next.62)
<sup>Released on **2025-11-15**</sup>

#### 🐛 Bug Fixes

- **next16**: Resolve 'Response body object should not be disturbed or locked' error.

<br/>

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

#### What's fixed

* **next16**: Resolve 'Response body object should not be disturbed or locked' error, closes [#10226](https://github.com/lobehub/lobe-chat/issues/10226) ([caa9c78](https://github.com/lobehub/lobe-chat/commit/caa9c78))

</details>

<div align="right">

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

</div>
2025-11-15 06:45:30 +00:00
Arvin Xu caa9c78623 🐛 fix(next16): resolve 'Response body object should not be disturbed or locked' error (#10226)
- Add prepareRequestForTRPC utility to clone Request objects for tRPC handlers
- Update all tRPC route handlers (lambda, async, desktop, mobile, tools) to use cloned requests
- Update checkAuth middleware to clone requests before passing to handlers
- This fixes the issue where Next.js 16 internal mechanisms disturb the request body stream
- Resolves: https://github.com/vercel/next.js/issues/83453
2025-11-15 14:33:28 +08:00
lobehubbot 2072b56708 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-15 04:11:38 +00:00
semantic-release-bot f95aeb2ca6 🔖 chore(release): v2.0.0-next.61 [skip ci]
## [Version&nbsp;2.0.0-next.61](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.60...v2.0.0-next.61)
<sup>Released on **2025-11-15**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

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

</details>

<div align="right">

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

</div>
2025-11-15 04:10:28 +00:00
LobeHub Bot ca7551fb40 🤖 style: update i18n (#10224) 2025-11-15 11:58:40 +08:00
LobeHub Bot 0d6cb06d59 🌐 chore: translate non-English comments to English in database models (#10225) 2025-11-15 11:58:00 +08:00
lobehubbot 23a7c00181 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-14 16:50:11 +00:00
semantic-release-bot 51dbf94576 🔖 chore(release): v2.0.0-next.60 [skip ci]
## [Version&nbsp;2.0.0-next.60](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.59...v2.0.0-next.60)
<sup>Released on **2025-11-14**</sup>

#### 🐛 Bug Fixes

- **misc**: Reduce threshold.

<br/>

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

#### What's fixed

* **misc**: Reduce threshold, closes [#10222](https://github.com/lobehub/lobe-chat/issues/10222) ([abdfd06](https://github.com/lobehub/lobe-chat/commit/abdfd06))

</details>

<div align="right">

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

</div>
2025-11-14 16:49:01 +00:00
René Wang abdfd064e7 🐛 fix: Reduce threshold (#10222) 2025-11-15 00:37:11 +08:00
ONLY-yours bb594f87e2 fix: fixed the test 2025-11-14 23:44:57 +08:00
ONLY-yours b0ee9b434e fix: fixed the url & new url not path problem 2025-11-14 23:34:31 +08:00
Arvin Xu fe1d05a547 test: fix upload service tests after removing ClientS3 (#10220)
- Removed references to deleted clientS3Storage
- Updated tests to match current server/desktop upload flow
- Fixed XMLHttpRequest mocking for server upload tests
- Updated filename assertions to match UUID generation behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-14 20:14:14 +08:00
lobehubbot 1c15ea5907 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-14 11:42:55 +00:00
semantic-release-bot 9bb03bcb96 🔖 chore(release): v2.0.0-next.59 [skip ci]
## [Version&nbsp;2.0.0-next.59](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.58...v2.0.0-next.59)
<sup>Released on **2025-11-14**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

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

</details>

<div align="right">

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

</div>
2025-11-14 11:41:51 +00:00
LobeHub Bot fc57d2a28c 🤖 style: update i18n (#10205)
💄 style: update i18n

Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-11-14 19:28:45 +08:00
lobehubbot d7ceee2cdb 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-14 11:19:01 +00:00
semantic-release-bot e033931d4e 🔖 chore(release): v2.0.0-next.58 [skip ci]
## [Version&nbsp;2.0.0-next.58](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.57...v2.0.0-next.58)
<sup>Released on **2025-11-14**</sup>

####  Features

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

<br/>

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

#### What's improved

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

</details>

<div align="right">

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

</div>
2025-11-14 11:17:53 +00:00
Arvin Xu 3736a85473 feat: support DeepSeek Interleaved thinking (#10219)
fix tests
2025-11-14 19:06:28 +08:00
lobehubbot ca348ec0df 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-14 10:22:47 +00:00
semantic-release-bot d262fdbeaf 🔖 chore(release): v2.0.0-next.57 [skip ci]
## [Version&nbsp;2.0.0-next.57](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.56...v2.0.0-next.57)
<sup>Released on **2025-11-14**</sup>

#### 💄 Styles

- **misc**: Revert background style.

<br/>

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

#### Styles

* **misc**: Revert background style, closes [#10218](https://github.com/lobehub/lobe-chat/issues/10218) ([97b0413](https://github.com/lobehub/lobe-chat/commit/97b0413))

</details>

<div align="right">

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

</div>
2025-11-14 10:21:35 +00:00
Arvin Xu 97b0413020 💄 style: revert background style (#10218)
revert style
2025-11-14 18:09:16 +08:00
ONLY-yours cf2c5a1d37 fix: fixed router link error 2025-11-14 17:15:09 +08:00
ONLY-yours 0511e43a48 fix: fixed usage router error 2025-11-14 17:09:21 +08:00
ONLY-yours 1f128f407f Merge remote-tracking branch 'origin/next' into refactor/changeAllToSpa 2025-11-14 16:58:34 +08:00
lobehubbot 52280da8bc 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-14 08:51:07 +00:00
semantic-release-bot c23d908b3b 🔖 chore(release): v2.0.0-next.56 [skip ci]
## [Version&nbsp;2.0.0-next.56](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.55...v2.0.0-next.56)
<sup>Released on **2025-11-14**</sup>

####  Features

- **misc**: Add folder creation UI and clean up debug code.

<br/>

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

#### What's improved

* **misc**: Add folder creation UI and clean up debug code ([d5ecd0a](https://github.com/lobehub/lobe-chat/commit/d5ecd0a))

</details>

<div align="right">

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

</div>
2025-11-14 08:49:56 +00:00
Rene Wang 85e2572d26 Merge branch 'next' of github.com:lobehub/lobe-chat into feat/folder-manager 2025-11-14 16:37:59 +08:00
lobehubbot 2e8031f865 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-14 08:31:24 +00:00
semantic-release-bot 9c3ddcc99b 🔖 chore(release): v2.0.0-next.55 [skip ci]
## [Version&nbsp;2.0.0-next.55](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.54...v2.0.0-next.55)
<sup>Released on **2025-11-14**</sup>

####  Features

- **image**: Image model show price.
- **misc**: Create Pages in Knowledge Base.

<br/>

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

#### What's improved

* **image**: Image model show price, closes [#10198](https://github.com/lobehub/lobe-chat/issues/10198) ([b87e0e4](https://github.com/lobehub/lobe-chat/commit/b87e0e4))
* **misc**: Create Pages in Knowledge Base, closes [#9895](https://github.com/lobehub/lobe-chat/issues/9895) ([f46edeb](https://github.com/lobehub/lobe-chat/commit/f46edeb))

</details>

<div align="right">

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

</div>
2025-11-14 08:30:09 +00:00
Rene Wang d5ecd0a17c feat: Add folder creation UI and clean up debug code
- Add "New Folder" option in KnowledgeManager add button dropdown
- Remove debug logging from FileExplorer component
- Add i18n keys for folder management actions (newFolder, newPage, uploadFile, uploadFolder)
- Prepare UI for folder creation functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 16:23:17 +08:00
ONLY-yours f258a2e042 fix: fixed the desktop knowledge page router 2025-11-14 16:18:55 +08:00
YuTengjing b87e0e422e feat(image): image model show price (#10198) 2025-11-14 16:10:22 +08:00
ONLY-yours 7996e1c431 Merge remote-tracking branch 'origin/next' into refactor/changeAllToSpa 2025-11-14 16:07:47 +08:00
René Wang f46edeb2d1 feat: Create Pages in Knowledge Base (#9895)
* feat: New note entry

* feat: save

* feat: custom note

* feat: save

* feat: editor

* feat: editor

* feat: editor

* lint: Regroup files

* fix: Image border

* feat: editor

* feat: masonry view in chat

* style: column

* 🐛 fix: Fix editor in modal

* fix: Mansory stuck

* feat: New note view

* feat: New note view

* fix: New note draft

* fix: New note draft

* style: New sidebar

* style: Remove icon

* style: Add skeleton

* style: button style

* fix: Lint error

* fix: Preview not updating

* style: Collection style

* fix: Cannot query other data

* style: New header style

* feat: Empty placeholder

* style: Adjust padding

* feat: Upload markdown

* fix: Tab active status

* style: image placeholder

* fix: Cannot delete note

* feat: Emoji picker

* style: Move icon to leading position

* style: Fix input color

* fix: Icon not saved

* style: leading icon

* style: Adjust skelton shape

* feat: Auto save

* feat: Upgrade file

* feat: Knowlwdge home

* feat: Knowlwdge home

* feat: Knowlwdge home

* feat: Knowlwdge home

* feat: Rename files

* fix: Knowledge base not working

* fix: Knowledge base home

* fix: Knowledge base home

* feat: Three dot menu

* fix: New knowledge base modal not working

* feat: Cannot use upload

* fix: documents not aloding

* feat: Route for document

* fix: Test error

* fix: Lint

* fix: Type error

* refac: Rename symbol

* fix: Cannot save icon

* fix: Add missing translations

* feat: Use virtualso for the list

* fix: Hover style

* fix: Cannot open documents

* feat: Bump Editor version

* fix: Editor blur

* feat: Hide preview for selected item

* style: Limit max width

* feat: Auto save hint

* style: New doc list style

* style: New header

* feat: Heade tyle

* style: Adjust padding

* feat: Duplicate document

* fix: Add missing i18n

* fix: Add missing translation

* fix: Test error

* lint: Seperate code

* fix: Style pollution

* feat: Share state

* fix: Word count

* fix: Navigation

* feat: Add heading option

* fix: Add missing translation

* feat: Delete confirm

* feat: Collpased by default

* fix: Editor hot area

* fix: Add missing translation

* style: Adjust file list density

* fix: Remove website for now

* feat: Use masonry by default

* feat: Collapse switch

* fix: Remove useless query

* feat: Remove unused features

* feat: Immeditaely create knowledge base

* feat: Immedately create the document

* feat: Add missing translation

* feat: Open emoji pciker by default

* fix: Emoji picker

* feat: Rename

* feat: Rename

* fix: Emoji picker disappear

* fix: Route flickering

* feat: Refactor document

* fix: Address ts error

* feat: Reduce delay

* feat: Document -> Page

* fix: Add missing translation

* fix: URL

* fix: add missing translation

* fix: editor blurred

* fix: No skelton after successfuly deletion

* fix: Filter

* build: Add test

* fix: Test

* fix: Test coverage drop

---------

Co-authored-by: canisminor1990 <i@canisminor.cc>
2025-11-14 16:05:19 +08:00
ONLY-yours 93dddfc2e5 feat: rollback some changes about layout 2025-11-14 15:58:50 +08:00
ONLY-yours 5e4186559b fix: fix useNav in discover page error problem 2025-11-14 15:42:16 +08:00
ONLY-yours 9bfd9bb4a5 Merge remote-tracking branch 'origin/next' into refactor/changeAllToSpa 2025-11-14 15:05:02 +08:00
ONLY-yours 9ca54135b5 feat: fix a lot router problem 2025-11-14 14:45:24 +08:00
lobehubbot 9250263fd7 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-14 06:27:53 +00:00
semantic-release-bot c782d091dd 🔖 chore(release): v2.0.0-next.54 [skip ci]
## [Version&nbsp;2.0.0-next.54](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.53...v2.0.0-next.54)
<sup>Released on **2025-11-14**</sup>

#### 💄 Styles

- **misc**: Refactor and support move locale file intervention.

<br/>

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

#### Styles

* **misc**: Refactor and support move locale file intervention, closes [#10213](https://github.com/lobehub/lobe-chat/issues/10213) ([63cac81](https://github.com/lobehub/lobe-chat/commit/63cac81))

</details>

<div align="right">

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

</div>
2025-11-14 06:26:44 +00:00
Arvin Xu 63cac811cd 💄 style: refactor and support move locale file intervention (#10213)
refactor and support move locale file
2025-11-14 14:15:25 +08:00
lobehubbot 0eca6f9f4a 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-14 05:12:53 +00:00
semantic-release-bot d62733adcc 🔖 chore(release): v2.0.0-next.53 [skip ci]
## [Version&nbsp;2.0.0-next.53](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.52...v2.0.0-next.53)
<sup>Released on **2025-11-14**</sup>

####  Features

- **misc**: Add GPT-5.1 models.

#### 💄 Styles

- **misc**: Fix approving render and improve Conversation style.

<br/>

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

#### What's improved

* **misc**: Add GPT-5.1 models, closes [#10206](https://github.com/lobehub/lobe-chat/issues/10206) ([afd3a47](https://github.com/lobehub/lobe-chat/commit/afd3a47))

#### Styles

* **misc**: Fix approving render and improve Conversation style, closes [#10210](https://github.com/lobehub/lobe-chat/issues/10210) ([841b7f1](https://github.com/lobehub/lobe-chat/commit/841b7f1))

</details>

<div align="right">

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

</div>
2025-11-14 05:11:29 +00:00
Arvin Xu 841b7f1c37 💄 style: fix approving render and improve Conversation style (#10210)
fix approving render and improve chat layout style
2025-11-14 12:57:28 +08:00
sxjeru afd3a47e3d feat: Add GPT-5.1 models (#10206) 2025-11-14 12:53:09 +08:00
LobeHub Bot 14dd288d50 🌐 chore: translate non-English comments to English in electron-server-ipc (#10207)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-14 12:52:38 +08:00
lobehubbot 799395d982 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-14 03:59:54 +00:00
Arvin Xu 6868d78adb test: fix tests (#10209)
fix tests
2025-11-14 11:48:09 +08:00
ONLY-yours f162556607 fix: delete the changelog modal page 2025-11-14 10:24:31 +08:00
Arvin Xu 4388270cf4 📌 chore: pin electron to 38.x (#10204)
Downgrade electron from 39.x to 38.x for stability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-14 10:14:39 +08:00
lobehubbot ac4993a769 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-13 18:55:54 +00:00
semantic-release-bot f1db5e1f11 🔖 chore(release): v2.0.0-next.52 [skip ci]
## [Version&nbsp;2.0.0-next.52](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.51...v2.0.0-next.52)
<sup>Released on **2025-11-13**</sup>

#### 🐛 Bug Fixes

- **misc**: Filter out reasoning fields from messages in ChatCompletion API.

<br/>

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

#### What's fixed

* **misc**: Filter out reasoning fields from messages in ChatCompletion API, closes [#10203](https://github.com/lobehub/lobe-chat/issues/10203) [#10193](https://github.com/lobehub/lobe-chat/issues/10193) ([5f28b2c](https://github.com/lobehub/lobe-chat/commit/5f28b2c))

</details>

<div align="right">

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

</div>
2025-11-13 18:54:38 +00:00
Arvin Xu 5f28b2c59e 🐛 fix: filter out reasoning fields from messages in ChatCompletion API (#10203)
* fix max tokens issue

* 🐛 fix: filter out reasoning fields from messages in ChatCompletion API

Explicitly map only valid ChatCompletionMessageParam fields and exclude reasoning/reasoning_content to prevent JSON unmarshaling errors when conversation history contains reasoning objects.

Fixes #10193

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-14 02:42:36 +08:00
Arvin Xu 428f05ac8a 💄 style: make OpenAI Response API by default (#10202)
* update i18n

* 修正测试

* fix macOS impl

* fix directory params

* refactor the builtin render implement

* remove unused sql

* fix tests
2025-11-14 02:13:39 +08:00
lobehubbot ca2a7d43e9 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-13 15:14:51 +00:00
semantic-release-bot bf2f6daa1b 🔖 chore(release): v2.0.0-next.51 [skip ci]
## [Version&nbsp;2.0.0-next.51](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.50...v2.0.0-next.51)
<sup>Released on **2025-11-13**</sup>

#### 💄 Styles

- **misc**: Update ERNIE-5.0-Thinking-Preview model.

<br/>

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

#### Styles

* **misc**: Update ERNIE-5.0-Thinking-Preview model, closes [#10196](https://github.com/lobehub/lobe-chat/issues/10196) ([89f3eed](https://github.com/lobehub/lobe-chat/commit/89f3eed))

</details>

<div align="right">

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

</div>
2025-11-13 15:13:37 +00:00
sxjeru 89f3eed4c1 💄 style: Update ERNIE-5.0-Thinking-Preview model (#10196)
* feat(wenxin): add model listing and parsing for Wenxin models

- Implemented model retrieval in Wenxin provider with async models function.
- Introduced WenxinModelCard interface to define model structure.
- Updated modelParse utility to include keywords specific to Wenxin models.
- Enhanced model owner detection to recognize Wenxin models.

* feat(wenxin): add reasoning parameters to chat model settings and update payload handling

* feat(wenxin): update checkModel and modelsUrl for improved model access

* feat(wenxin): add search ability and update settings for chat models

* feat(wenxin): refine thinking budget handling in chat completion payload

* feat(wenxin): remove enableReasoning from extendParams in chat model settings
2025-11-13 23:01:55 +08:00
Arvin Xu 39cdb2057e feat: support bash tools in local system (#9676)
* wip for bash system

* refactor

* fix remark issue

* 完成批准实现

* refactor toolIntervention

* refactor toolIntervention

* use user tool config

* show InterventionModeSelector

* finish local file mode

* fix error

* update

* update i18n

* revert

* fix bug
2025-11-13 22:18:05 +08:00
lobehubbot bb33feb0f4 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-13 13:10:31 +00:00
semantic-release-bot 72afed9546 🔖 chore(release): v2.0.0-next.50 [skip ci]
## [Version&nbsp;2.0.0-next.50](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.49...v2.0.0-next.50)
<sup>Released on **2025-11-13**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix oidc accountId mismatch.

<br/>

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

#### What's fixed

* **misc**: Fix oidc accountId mismatch, closes [#10058](https://github.com/lobehub/lobe-chat/issues/10058) ([0692ba7](https://github.com/lobehub/lobe-chat/commit/0692ba7))

</details>

<div align="right">

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

</div>
2025-11-13 13:09:16 +00:00
Rdmclin2 0692ba7406 🐛 fix: fix oidc accountId mismatch (#10058)
* chore: adjust oidc login and consent page mobile style

* fix: acccount mismatch error

* test: add oidc service test case
2025-11-13 20:55:03 +08:00
ONLY-yours 3292ed83f9 fix: fix mobile router goback fc 2025-11-13 20:24:28 +08:00
ONLY-yours 561a38f788 fix: delete useless code 2025-11-13 20:08:58 +08:00
LobeHub Bot 39d91a86c0 test: add unit tests for validateRedirectHost (#10173)
Added comprehensive unit tests for the validateRedirectHost security function covering:
- Invalid input validation
- Exact host matching
- Localhost environment handling
- Subdomain validation
- Open redirect attack prevention
- Port handling with standard and custom ports
- Edge cases (IPv4, case sensitivity, malformed inputs)
- Real-world deployment scenarios

All 52 test cases pass successfully.
2025-11-13 20:05:50 +08:00
ONLY-yours 71aaf0fac5 chore: update test.ts in TopActions.tsx 2025-11-13 19:11:33 +08:00
ONLY-yours 287601f8ec fix: close the loading in the layout loading 2025-11-13 19:06:37 +08:00
ONLY-yours b36f8781e6 feat: use starTransition to navigate url 2025-11-13 18:02:16 +08:00
ONLY-yours 705450a571 fix: add files back 2025-11-13 17:17:36 +08:00
LobeHub Bot 331af68b73 🌐 chore: translate non-English comments to English in context-engine (#10180)
* 🌐 chore: translate non-English comments to English in context-engine


* 🌐 fix: complete comment translation in MessageContent.ts

- Translated remaining 3 Chinese comments to English
- Ensures all comments in context-engine package are properly translated
- Maintains code functionality while improving readability for international developers

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Arvin Xu <arvinxx@users.noreply.github.com>

* 🧪 test: update error message expectations to English in BaseProcessor tests

Updated test assertions to match English error messages ('Invalid context' and 'Invalid output context') instead of Chinese ones.
2025-11-13 17:15:22 +08:00
ONLY-yours 5272c7373f fix: add files back 2025-11-13 17:14:49 +08:00
ONLY-yours fb24b6f1b7 fix: add nuqs back & useQueryState back in oath 2025-11-13 17:09:10 +08:00
ONLY-yours 2fd65fe8a3 fix: discover find more link error fixed 2025-11-13 17:02:52 +08:00
ONLY-yours 35d5a2c937 chore: add mobile me layout back 2025-11-13 16:59:23 +08:00
ONLY-yours 42f40d2717 feat: change the mobile me layout back 2025-11-13 16:41:53 +08:00
ONLY-yours ef8a644d8c feat: delete all nuqs 2025-11-13 16:25:59 +08:00
lobehubbot 4ea759af29 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-13 07:38:20 +00:00
semantic-release-bot c73e1e2bfc 🔖 chore(release): v2.0.0-next.49 [skip ci]
## [Version&nbsp;2.0.0-next.49](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.48...v2.0.0-next.49)
<sup>Released on **2025-11-13**</sup>

####  Features

- **misc**: Support tool invention.

#### 🐛 Bug Fixes

- **misc**: Update lost i18n files.

<br/>

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

#### What's improved

* **misc**: Support tool invention, closes [#10182](https://github.com/lobehub/lobe-chat/issues/10182) ([4dca708](https://github.com/lobehub/lobe-chat/commit/4dca708))

#### What's fixed

* **misc**: Update lost i18n files, closes [#10179](https://github.com/lobehub/lobe-chat/issues/10179) ([b69c7ff](https://github.com/lobehub/lobe-chat/commit/b69c7ff))

</details>

<div align="right">

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

</div>
2025-11-13 07:36:55 +00:00
ONLY-yours 81c84348bc fix: change the changelog pages render 2025-11-13 15:25:48 +08:00
Shinji-Li b69c7ff83e 🐛 fix: update lost i18n files (#10179)
chore: update i18n
2025-11-13 15:22:32 +08:00
Arvin Xu 4dca708d2c feat: support tool invention (#10182)
* finish intervention backend

* add Intervention

* fix tests

* finish action mode

* 初步完成 reject 逻辑

* 初步完成 reject 逻辑

* wip approve tool calling

* 初步完成 approve 流程

* Update index.ts

* 完成 approve 流程

* fix tests

* fix tests
2025-11-13 15:11:28 +08:00
lobehubbot 9b9df57c59 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-13 06:18:25 +00:00
ONLY-yours 8d7a0467db fix: fix build problem 2025-11-13 14:18:08 +08:00
Arvin Xu 8bc15893b8 📌 chore: pin eta to 4.0.1 to fix ERR_PACKAGE_PATH_NOT_EXPORTED error (#10190)
* 📌 chore: pin eta to 4.0.1 to fix ERR_PACKAGE_PATH_NOT_EXPORTED error

* 🔨 chore: add overrides for bun compatibility
2025-11-13 14:07:25 +08:00
ONLY-yours e9522729c5 fix: fix hydrateFallback problem 2025-11-13 11:49:35 +08:00
ONLY-yours cf01894077 feat: change local params get use ReactRouter Outlet context 2025-11-13 11:03:12 +08:00
Shinji-Li bab0054557 🔨 chore: update market-sdk (#10171)
chore: update market-sdk
2025-11-13 10:13:08 +08:00
Neko 0baacf7301 👷 chore: improve renovate config to support grouping in the same way of npm does (#10176)
chore(ci): improve renovate config to support grouping in the same way of npm does
2025-11-12 22:34:28 +08:00
Neko 0c11d5fcee 🔨 chore(observability-otel): interval of metrics not small enough (#10175)
fix(observability-otel): interval of metrics not small enough
2025-11-12 21:05:26 +08:00
ONLY-yours b5d945b1fd fix: delete some layout tsx & update the ts 2025-11-12 17:16:26 +08:00
ONLY-yours cbee964582 feat: change NextJs Link useRouter useSearchParams change to react-router way 2025-11-12 17:01:31 +08:00
ONLY-yours 87a38ad0c4 feat: change the :slug to react-router loader to get 2025-11-12 16:27:34 +08:00
ONLY-yours f2d4745ad3 Merge remote-tracking branch 'origin/next' into refactor/changeAllToSpa 2025-11-12 15:00:36 +08:00
ONLY-yours 0167ac8e28 feat: change all routes to outer routes 2025-11-12 15:00:06 +08:00
ONLY-yours b480227fd0 feat: discover pages layout & pages routers get done 2025-11-12 11:56:20 +08:00
lobehubbot be9678e395 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-12 03:06:36 +00:00
semantic-release-bot 9d6a0a7d99 🔖 chore(release): v2.0.0-next.48 [skip ci]
## [Version&nbsp;2.0.0-next.48](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.47...v2.0.0-next.48)
<sup>Released on **2025-11-12**</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-11-12 03:05:06 +00:00
Rdmclin2 02f05a875a 🔨 chore: add type (#10165)
chore: add type
2025-11-12 10:48:25 +08:00
Arvin Xu ad34554132 👷 build: update client sql (#10169)
update sql
2025-11-12 10:35:01 +08:00
Arvin Xu 656a33359b 👷 build: add intervention tool column (#10163)
* add intervention tool

* update sql
2025-11-12 10:25:03 +08:00
lobehubbot 84c3932b41 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-11 15:28:49 +00:00
ONLY-yours 97ff98cada Merge remote-tracking branch 'origin/next' into refactor/changeAllToSpa 2025-11-11 23:17:32 +08:00
ONLY-yours 845d3ef58a feat: change all discover page to the spa 2025-11-11 23:16:57 +08:00
Shinji-Li 8d362cf6b6 feat:support LobeHub MarketPlace (#8841)
* feat: 链接market 做基本的市场接驳功能

* feat: 重写分享助手的页面

* feat: 新增market-auth 的 oidc 方案

* feat: 增加初次agent发布链路和更agent逻辑

* feat: 增加二次添加助手时候的提示

* feat: 重新授权时候唤起新的重新授权而不是自动 token 换取

* feat: 添加market-auth-callback的 layout

* feat: 调整env 中的market 引用

* fix: 解决url 双/导致的路径请求问题

* fix: fix build error

* feat: 更新sitemap 的生成逻辑

* feat: 更新pglite的session meta 定义,增加 marketIdentifier

* feat: 增加个人信息存储的逻辑 & 整理发布 agent 时候按钮的整体逻辑

* fix: delete 0030

* feat: add search myown in discover

* feat: clean cthe code & refactor agents showpannel

* feat: support assistant detail pages have unpublish & achiave hint

* feat: change text render type

* feat: add submit mode style fixed

* fix: fixed migrations

* feat: update agent publish version modal

* feat: update market publish button

* feat: update exmaple &summary show case

* feat: add token show in publish modal

* feat: add verison show tags antd version? params search

* feat: add desktop market auth request way

* feat: delete market-oidc second path,change all to base url

* feat: change sdk & api into url const & change market api into servers

* feat: change all api into /market server path

* feat: change the migrations insert position

* feat: support assistant origin checkout feature

* feat: change the item show place

* feat: add market source switch components

* feat: add 'force-dynamic' in discord/detail page

* feat: change the describe name

* feat: styles & locals add

* feat: add locals

* feat: fixed market update locals

* feat: update market-oidc layout

* feat: delete some types

* feat: support leacgcy params change

* feat: change the oidc url

* feat: agent detial page should have status show page

* feat: add db migrations

* test: update test

* feat: delete database change & update i18n

* fix: rollback mirgration

* fix: change mirgration

* fix: back snapshot

* Update print statement from 'Hello' to 'Goodbye'
2025-11-11 23:16:19 +08:00
Shinji-Li a15eda7fbf 🔨 chore: add market_identifier into agents table schema (#10164)
chore: add market_identifier into agents table schema
2025-11-11 22:37:47 +08:00
YuTengjing 4f7bc5acd2 🐛 fix: add SSRF protection (#10152) 2025-11-11 19:39:36 +08:00
lobehubbot 8219124a10 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-11 11:16:46 +00:00
semantic-release-bot 6ce223ed11 🔖 chore(release): v2.0.0-next.47 [skip ci]
## [Version&nbsp;2.0.0-next.47](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.46...v2.0.0-next.47)
<sup>Released on **2025-11-11**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix mcp server return image error.

<br/>

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

#### What's fixed

* **misc**: Fix mcp server return image error, closes [#10113](https://github.com/lobehub/lobe-chat/issues/10113) ([e5640d4](https://github.com/lobehub/lobe-chat/commit/e5640d4))

</details>

<div align="right">

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

</div>
2025-11-11 11:15:31 +00:00
Arvin Xu e5640d499a 🐛 fix: fix mcp server return image error (#10113)
* support upload image

* support upload image in mcp

* fix tests

* update

* fix

* improve tests

* fix tests

* Update route.ts
2025-11-11 19:02:54 +08:00
ONLY-yours 906917362f feat: /chat delete pages & layouts dir 2025-11-11 17:45:12 +08:00
LobeHub Bot b63be1c90a test: add unit tests for route variants (#10159)
 test: add unit tests for route variants serialization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-11 17:35:56 +08:00
ONLY-yours c69049d6da fix: refactor the memory router to browser router 2025-11-11 16:01:32 +08:00
LobeHub Bot e70a703a7e 🌐 chore: translate non-English comments to English in packages/types (#10158)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-11 13:15:34 +08:00
lobehubbot 522a3ec6fa 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-11 01:56:09 +00:00
semantic-release-bot e682b1a10d 🔖 chore(release): v2.0.0-next.46 [skip ci]
## [Version&nbsp;2.0.0-next.46](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.45...v2.0.0-next.46)
<sup>Released on **2025-11-11**</sup>

#### ♻ Code Refactoring

- **misc**: Fix thread display.

<br/>

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

#### Code refactoring

* **misc**: Fix thread display, closes [#10153](https://github.com/lobehub/lobe-chat/issues/10153) ([8fda83e](https://github.com/lobehub/lobe-chat/commit/8fda83e))

</details>

<div align="right">

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

</div>
2025-11-11 01:54:55 +00:00
LobeHub Bot 4c5cf41be3 test: add unit tests for ContentChunk module (#10145)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-11 09:43:24 +08:00
Arvin Xu 8fda83ec55 ♻️ refactor: fix thread display (#10153)
* refactor thread

* fix style

* improve

* refactor to improve rerender
2025-11-11 09:41:02 +08:00
lobehubbot cee154fc73 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-10 17:23:33 +00:00
semantic-release-bot 18eaa649b5 🔖 chore(release): v2.0.0-next.45 [skip ci]
## [Version&nbsp;2.0.0-next.45](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.44...v2.0.0-next.45)
<sup>Released on **2025-11-10**</sup>

#### ♻ Code Refactoring

- **misc**: Edge to node runtime.

<br/>

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

#### Code refactoring

* **misc**: Edge to node runtime, closes [#10149](https://github.com/lobehub/lobe-chat/issues/10149) ([2f4c25d](https://github.com/lobehub/lobe-chat/commit/2f4c25d))

</details>

<div align="right">

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

</div>
2025-11-10 17:22:24 +00:00
YuTengjing 2f4c25d826 ♻️ refactor: edge to node runtime (#10149) 2025-11-10 23:44:10 +08:00
lobehubbot 29b1eb2521 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-10 12:05:59 +00:00
semantic-release-bot 037703c8f0 🔖 chore(release): v2.0.0-next.44 [skip ci]
## [Version&nbsp;2.0.0-next.44](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.43...v2.0.0-next.44)
<sup>Released on **2025-11-10**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix reasoning issue with claude and Response API thinking.

<br/>

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

#### What's fixed

* **misc**: Fix reasoning issue with claude and Response API thinking, closes [#10147](https://github.com/lobehub/lobe-chat/issues/10147) ([cf6bd53](https://github.com/lobehub/lobe-chat/commit/cf6bd53))

</details>

<div align="right">

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

</div>
2025-11-10 12:04:46 +00:00
Arvin Xu cf6bd53141 🐛 fix: fix reasoning issue with claude and Response API thinking (#10147)
* add parse testing

* fix claude thinking issue

* fix gpt thinking

* fix mobile router

* Update src/services/message/index.ts

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

* fix tests

* fix tests and portal

* fix tests
2025-11-10 19:52:44 +08:00
LobeHub Bot 88e376272c 🌐 chore: translate non-English comments to English in packages/utils and src/services (#10143)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-10 11:34:19 +08:00
lobehubbot 84b039c4f2 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-09 11:51:23 +00:00
semantic-release-bot f178777c8d 🔖 chore(release): v2.0.0-next.43 [skip ci]
## [Version&nbsp;2.0.0-next.43](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.42...v2.0.0-next.43)
<sup>Released on **2025-11-09**</sup>

#### 🐛 Bug Fixes

- **misc**: Abnormal animation of tokens.

<br/>

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

#### What's fixed

* **misc**: Abnormal animation of tokens, closes [#10106](https://github.com/lobehub/lobe-chat/issues/10106) ([129df7b](https://github.com/lobehub/lobe-chat/commit/129df7b))

</details>

<div align="right">

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

</div>
2025-11-09 11:50:13 +00:00
sxjeru 129df7b888 🐛 fix: Abnormal animation of tokens (#10106)
*  feat(TokenDetail): add toggle for short/long format display of token values

*  feat(TokenDetail): enhance token display format persistence and toggle functionality

*  feat(TokenDetail): adjust popover trigger behavior for mobile and desktop

* replace localStorage with global store for token display format management

* add animation duration for token value display

*  feat: 强制重新挂载以防止在切换 token/credit 时出现不必要的动画
2025-11-09 19:38:05 +08:00
lobehubbot 190b28244e 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-09 11:23:31 +00:00
semantic-release-bot 5db5cf582d 🔖 chore(release): v2.0.0-next.42 [skip ci]
## [Version&nbsp;2.0.0-next.42](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.41...v2.0.0-next.42)
<sup>Released on **2025-11-09**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix missing messages when finish runtime.

<br/>

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

#### What's fixed

* **misc**: Fix missing messages when finish runtime, closes [#10138](https://github.com/lobehub/lobe-chat/issues/10138) ([b94d477](https://github.com/lobehub/lobe-chat/commit/b94d477))

</details>

<div align="right">

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

</div>
2025-11-09 11:22:13 +00:00
Arvin Xu b94d477f01 🐛 fix: fix missing messages when finish runtime (#10138)
fix missing message when finish render
2025-11-09 19:10:25 +08:00
LobeHub Bot 5c817bc304 test: add unit tests for trace utilities (#10136)
- Added comprehensive unit tests for packages/utils/src/trace.ts
- Tests cover getTracePayload, getTraceId, and createTraceHeader functions
- Includes edge cases, Unicode handling, and round-trip encoding/decoding
- All 23 test cases pass successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-09 15:21:35 +08:00
Arvin Xu b3cea58514 test(database): achieve 100% coverage for message model (#10137)
*  test(database): fix all 3 skipped tests and improve coverage to 98.6%

- Fix test: create message with file chunks and RAG query ID
  - Add proper FK setup (message -> query -> message with chunks)
  - Fix similarity precision (database stores 5 decimals)

- Fix test: handle multiple message queries for same message
  - Update test to accept any of the queries (no ordering guarantee)
  - Add documentation about messageQueries table lacking sort field

- Fix test: heatmap 19:00 time boundary issue
  - Use local time at noon to avoid timezone edge cases
  - Use explicit date strings to ensure correct date grouping

Test results:
- All 105 tests passing (no skipped tests!)
- Statement coverage: 98.6% (569/577 lines)
- Branch coverage: 91.0% (131/144 branches)
- Function coverage: 100% (34/34 functions)

* fix tests

*  test(database): achieve 100% coverage for message model

- Add edge case tests for INBOX_SESSION_ID, empty fileType, null similarity, groupId, and plugin state
- Fix similarity handling logic to properly convert null to undefined
- Add countWords tests with startDate/endDate filters
2025-11-09 15:06:08 +08:00
LobeHub Bot 2b74d0be05 🌐 chore: translate non-English comments to English in packages/utils (#10133)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-09 15:00:15 +08:00
lobehubbot 16a9c8b920 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-09 02:16:51 +00:00
semantic-release-bot e183eacf36 🔖 chore(release): v2.0.0-next.41 [skip ci]
## [Version&nbsp;2.0.0-next.41](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.40...v2.0.0-next.41)
<sup>Released on **2025-11-09**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

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

</details>

<div align="right">

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

</div>
2025-11-09 02:15:42 +00:00
LobeHub Bot 766772eaeb 🤖 style: update i18n (#10116)
💄 style: update i18n

Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-11-09 10:04:06 +08:00
Arvin Xu 00bac7e9fd test(database): split message.test.ts into modular test files (#10114)
*  test(database): add comprehensive test coverage for message query edge cases

Added critical test coverage for null parameter scenarios that were previously untested, preventing potential bugs similar to the deleteMessagesBySession issue.

**Test Coverage Added:**

1. **query() method with null parameters:**
   -  Query messages in session with null topicId (only non-topic messages)
   -  Query messages in session with null groupId (only non-group messages)
   -  Query inbox messages with null topicId when no sessionId specified
   -  Query messages with combined sessionId and topicId filters

2. **queryBySessionId() method:**
   -  Query inbox messages when sessionId is null
   -  Query inbox messages when sessionId is undefined

3. **deleteMessagesBySession() method:**
   -  Delete messages with specific groupId in session
   -  Delete messages with combined topicId and groupId filters

**Why This Matters:**

These edge cases were completely untested, creating blind spots where bugs could hide. The recent deleteMessagesBySession bug (which caused data loss) would have been caught if we had these tests. These tests verify that:

- Passing `null` explicitly filters for null values (e.g., messages without topics)
- Not passing a parameter defaults to null filtering (inbox messages)
- Parameter combinations work correctly without unexpected interactions

**Total New Tests:** 8 test cases covering critical edge cases

* update

* refactor messages tests

* 🔧 fix: use unique userIds in test files to prevent concurrent test conflicts

* ♻️ refactor(database): move conditional query logic from Model to Service layer

- Simplify MessageModel.update() to only perform update operation
- Simplify MessageModel.updatePluginState() to only perform update operation
- Remove options parameter and conditional message query logic from Model layer
- Service layer now handles all conditional query logic via queryWithSuccess()
- Update return types to use proper TypeScript types (UIChatMessage[])
- Remove 3 tests that tested Model layer business logic (now in Service layer)

This separates concerns properly:
- Model layer: pure database operations
- Service layer: business logic and conditional queries
2025-11-09 01:18:57 +08:00
Arvin Xu 508b34a5c8 test(database): add comprehensive test coverage for message query edge cases (#10111)
*  test(database): add comprehensive test coverage for message query edge cases

Added critical test coverage for null parameter scenarios that were previously untested, preventing potential bugs similar to the deleteMessagesBySession issue.

**Test Coverage Added:**

1. **query() method with null parameters:**
   -  Query messages in session with null topicId (only non-topic messages)
   -  Query messages in session with null groupId (only non-group messages)
   -  Query inbox messages with null topicId when no sessionId specified
   -  Query messages with combined sessionId and topicId filters

2. **queryBySessionId() method:**
   -  Query inbox messages when sessionId is null
   -  Query inbox messages when sessionId is undefined

3. **deleteMessagesBySession() method:**
   -  Delete messages with specific groupId in session
   -  Delete messages with combined topicId and groupId filters

**Why This Matters:**

These edge cases were completely untested, creating blind spots where bugs could hide. The recent deleteMessagesBySession bug (which caused data loss) would have been caught if we had these tests. These tests verify that:

- Passing `null` explicitly filters for null values (e.g., messages without topics)
- Not passing a parameter defaults to null filtering (inbox messages)
- Parameter combinations work correctly without unexpected interactions

**Total New Tests:** 8 test cases covering critical edge cases
2025-11-09 00:32:34 +08:00
lobehubbot dcea52bb2e 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-08 15:51:44 +00:00
semantic-release-bot c62092f63a 🔖 chore(release): v2.0.0-next.40 [skip ci]
## [Version&nbsp;2.0.0-next.40](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.39...v2.0.0-next.40)
<sup>Released on **2025-11-08**</sup>

#### 🐛 Bug Fixes

- **database**: Fix deleteMessagesBySession incorrectly deleting all messages.

<br/>

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

#### What's fixed

* **database**: Fix deleteMessagesBySession incorrectly deleting all messages, closes [#10110](https://github.com/lobehub/lobe-chat/issues/10110) ([1d7f67d](https://github.com/lobehub/lobe-chat/commit/1d7f67d))

</details>

<div align="right">

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

</div>
2025-11-08 15:50:43 +00:00
Arvin Xu 1d7f67da56 🐛 fix(database): fix deleteMessagesBySession incorrectly deleting all messages (#10110) 2025-11-08 23:40:02 +08:00
lobehubbot c48956e715 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-08 10:27:37 +00:00
semantic-release-bot 07578fe163 🔖 chore(release): v2.0.0-next.39 [skip ci]
## [Version&nbsp;2.0.0-next.39](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.38...v2.0.0-next.39)
<sup>Released on **2025-11-08**</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-11-08 10:26:19 +00:00
Neko 9f762e12be 👷 build(observability-otel): support to trace and meter for tRPC (#10086)
feat(observability-otel): support to trace and meter for tRPC
2025-11-08 18:14:53 +08:00
Arvin Xu 2ae4aeb58d ♻️ refactor: refactor mcp context use and support continueGeneration (#10096)
* refactor plugin actions

* refactor mcp invoke

* refactor MCPType display

* support Continue Generate

* support continueGeneration message

* fix tests

* fix tests

* fix parentId issue

* fix duration is NaN

* improve mcp render

* 🐛 fix(context-engine): preserve reasoning field in MessageCleanupProcessor

The MessageCleanupProcessor was removing the reasoning field from assistant messages during cleanup. This fix ensures that reasoning field is preserved along with other necessary fields like tool_calls.

Changes:
- Added reasoning field preservation in MessageCleanup.ts
- Added test case to verify reasoning field is correctly preserved

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix maxSteps

*  test: update test expectation for reasoning field preservation

Updated the test to expect the reasoning field to be preserved in the output, which is now the correct behavior after fixing MessageCleanupProcessor.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-08 17:59:50 +08:00
ONLY-yours 4f7356ffab feat: change AppRouter to Desktop Router & mobile Router to dynamic import 2025-11-08 17:52:05 +08:00
LobeHub Bot 17efa0bd52 test: add unit tests for network proxy module (#10104)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-08 15:08:11 +08:00
lobehubbot dc08f10268 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-08 06:25:05 +00:00
Rylan Cai 0851028205 💄 Style: RFC-113 Provider Model Usage Statistics (#8453)
* 💄 style: migrate panel UI

* 🌐 i18n: add i18n slots

*  lint: fix

* 🌐 i18n: add translations

*  test: add usage tests

* 📝 docs: update annotations

* 🐛 fix: always enable

* 🐛 fix: dayjs init error

* 🐛 fix: no attr len

* 🐛 fix: slice err
2025-11-08 14:13:21 +08:00
ONLY-yours d20c82c115 fix: change the router judge by servers 2025-11-08 11:59:00 +08:00
lobehubbot d600a476f0 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-08 03:40:34 +00:00
semantic-release-bot 92a62e70a9 🔖 chore(release): v2.0.0-next.38 [skip ci]
## [Version&nbsp;2.0.0-next.38](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.37...v2.0.0-next.38)
<sup>Released on **2025-11-08**</sup>

#### 🐛 Bug Fixes

- **TokenUsage**: Prevent animation when toggling between token and credit display.

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### What's fixed

* **TokenUsage**: Prevent animation when toggling between token and credit display, closes [#10098](https://github.com/lobehub/lobe-chat/issues/10098) ([f20a910](https://github.com/lobehub/lobe-chat/commit/f20a910))

#### Styles

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

</details>

<div align="right">

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

</div>
2025-11-08 03:39:22 +00:00
LobeHub Bot deb6b5e5a0 🤖 style: update i18n (#10100) 2025-11-08 11:27:16 +08:00
LobeHub Bot bd4ee89a43 🌐 chore: translate non-English comments to English in packages/web-crawler (#10101) 2025-11-08 11:26:33 +08:00
Arvin Xu f20a9108ed 🐛 fix(TokenUsage): prevent animation when toggling between token and credit display (#10098) 2025-11-08 11:24:38 +08:00
lobehubbot e56e50b2d6 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-07 18:31:32 +00:00
semantic-release-bot 48b2ec92a1 🔖 chore(release): v2.0.0-next.37 [skip ci]
## [Version&nbsp;2.0.0-next.37](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.36...v2.0.0-next.37)
<sup>Released on **2025-11-07**</sup>

#### 🐛 Bug Fixes

- **misc**: Don't include runtimeProvider in JWT for non-image operations.

<br/>

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

#### What's fixed

* **misc**: Don't include runtimeProvider in JWT for non-image operations, closes [#9959](https://github.com/lobehub/lobe-chat/issues/9959) [#9569](https://github.com/lobehub/lobe-chat/issues/9569) ([b8f25de](https://github.com/lobehub/lobe-chat/commit/b8f25de))

</details>

<div align="right">

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

</div>
2025-11-07 18:30:16 +00:00
XYenon b8f25dec30 🐛 fix: don't include runtimeProvider in JWT for non-image operations (#9959)
The lambdaClient was hardcoding provider='openai' and including it in the JWT
for ALL operations (knowledge base, chat, etc.). This caused the user's JWT
runtimeProvider to override the server's DEFAULT_FILES_CONFIG embedding provider,
resulting in InvalidProviderAPIKey errors.

Root cause:
- lambdaClient headers() set provider=ModelProvider.OpenAI by default
- This was passed to createHeaderWithAuth() for all operations
- createPayloadWithKeyVaults() added runtimeProvider='openai' to JWT
- Server's embedding operations received this JWT
- initModelRuntimeWithUserPayload() used JWT's runtimeProvider instead of server config

Solution:
- Only include provider in JWT for image operations (where user can select provider)
- For other operations (knowledge base, chat), don't pass provider
- Let server use its own DEFAULT_FILES_CONFIG for embedding operations

This fixes #9569 where users with DEFAULT_FILES_CONFIG=embedding_model=ollama/...
were getting InvalidProviderAPIKey errors because JWT was forcing provider='openai'.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-11-08 02:18:25 +08:00
ONLY-yours d617a6cd97 fix: slove ts problem 2025-11-07 23:34:43 +08:00
ONLY-yours 408391eeb6 fix: slove the router back 2025-11-07 23:14:47 +08:00
ONLY-yours 4a2e671f55 fix: fix the test 2025-11-07 22:53:49 +08:00
ONLY-yours 695a261df1 Merge remote-tracking branch 'origin/next' into refactor/changeAllToSpa 2025-11-07 22:35:37 +08:00
ONLY-yours 39b723eff4 feat: fix mobile agent settings page not work problem 2025-11-07 22:24:03 +08:00
ONLY-yours 68937d842c fix: delete useless code 2025-11-07 22:16:17 +08:00
ONLY-yours b66bc66260 feat: link replace to react-router-dom 2025-11-07 22:06:18 +08:00
ONLY-yours 4d06279abd feat: change some nextjs router to react-router-dom use 2025-11-07 21:56:03 +08:00
ONLY-yours 1a8d33fbf4 fix: change the goback & knowledge/base url 2025-11-07 21:22:56 +08:00
ONLY-yours 2c086373cc feat: use loading to dynamic loading 2025-11-07 21:09:52 +08:00
LobeHub Bot 6eb6b9010b test: add unit tests for ApiKeyModel (#10091)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-07 20:36:06 +08:00
ONLY-yours c7d49258f8 feat: change /settings labs image profile changelog to spa mode 2025-11-07 20:34:06 +08:00
ONLY-yours 2280fd6ff9 feat: disable / to /chat rewrite 2025-11-07 18:02:55 +08:00
ONLY-yours 8eb901c401 feat: change the root path to react-router-dom to render spa 2025-11-07 18:01:56 +08:00
lobehubbot 185f04e060 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-07 08:56:44 +00:00
semantic-release-bot 235a41ca54 🔖 chore(release): v2.0.0-next.36 [skip ci]
## [Version&nbsp;2.0.0-next.36](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.35...v2.0.0-next.36)
<sup>Released on **2025-11-07**</sup>

####  Features

- **misc**: Refactor to use agent runtime as the generation core and support branch mode.

<br/>

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

#### What's improved

* **misc**: Refactor to use agent runtime as the generation core and support branch mode, closes [#10080](https://github.com/lobehub/lobe-chat/issues/10080) ([b95e741](https://github.com/lobehub/lobe-chat/commit/b95e741))

</details>

<div align="right">

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

</div>
2025-11-07 08:55:27 +00:00
Arvin Xu b95e741717 feat: refactor to use agent runtime as the generation core and support branch mode (#10080)
* refactor

* refactor

* refactor message group

* wip

# Conflicts:
#	src/store/chat/slices/aiChat/actions/generateAIChatV2.ts

* refactor

* refactor agent mode

* fix style

* refactor agent executors

* finish the refactor

* remove gpt-tokenizer

* add metadata api

* add fix

* support branch

* fix branch render data

* fix send issue

* refactor style

* refactor style

* refactor tests

* refactor chatStore

* refactor from model to model

* fix tests

* refactor regenerate mode

* update style

* fix lint

* refactor

* refactor

* refactor

* fix delete

* refactor thread mode

* fix basic experience

* fix

* fix tests

* fix manual add

* fix tests

* fix group
2025-11-07 16:44:03 +08:00
LobeHub Bot c3c4319625 🌐 chore: translate non-English comments to English in file services (#10089)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-07 14:15:08 +08:00
lobehubbot 29974373f5 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-07 04:57:37 +00:00
semantic-release-bot 729dbe2a0f 🔖 chore(release): v2.0.0-next.35 [skip ci]
## [Version&nbsp;2.0.0-next.35](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.34...v2.0.0-next.35)
<sup>Released on **2025-11-07**</sup>

#### ♻ Code Refactoring

- **misc**: Use react-router-dom change /chat page to spa mode.

<br/>

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

#### Code refactoring

* **misc**: Use react-router-dom change /chat page to spa mode, closes [#10077](https://github.com/lobehub/lobe-chat/issues/10077) ([9154606](https://github.com/lobehub/lobe-chat/commit/9154606))

</details>

<div align="right">

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

</div>
2025-11-07 04:56:25 +00:00
Shinji-Li 9154606285 ♻️ refactor: use react-router-dom change /chat page to spa mode (#10077)
* feat: base change chat to spa

* feat: add /settings page layout

* feat: change workspace to components dir

* fix: restore the lamdba change
2025-11-07 12:45:26 +08:00
lobehubbot 3961a648ca 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-07 03:28:22 +00:00
semantic-release-bot 553d13c9f8 🔖 chore(release): v2.0.0-next.34 [skip ci]
## [Version&nbsp;2.0.0-next.34](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.33...v2.0.0-next.34)
<sup>Released on **2025-11-07**</sup>

#### 💄 Styles

- **misc**: Add sorting functionality for disabled models and model providers with tooltip support.

<br/>

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

#### Styles

* **misc**: Add sorting functionality for disabled models and model providers with tooltip support, closes [#10000](https://github.com/lobehub/lobe-chat/issues/10000) ([68e98b1](https://github.com/lobehub/lobe-chat/commit/68e98b1))

</details>

<div align="right">

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

</div>
2025-11-07 03:27:10 +00:00
bbbugg 68e98b1af4 💄 style: add sorting functionality for disabled models and model providers with tooltip support (#10000)
*  feat: add sorting functionality for disabled models and model providers with tooltip support

*  feat: persist sort type in localStorage for model providers and disabled models

*  feat: add dropdown menu for sorting models and providers with ascending/descending options

*  feat: add sorting options for models by release date with ascending/descending functionality

*  refactor: replace useUserStore with useGlobalStore for disabled models sorting

*  refactor: streamline sort type management in DisabledModels and List components

*  refactor: update sort type management in DisabledModels and List components to use useCallback
2025-11-07 11:15:48 +08:00
LobeHub Bot 23ed51887f test: add unit tests for MCP installation checkers (#10078)
Added comprehensive unit tests for PythonInstallationChecker, NpmInstallationChecker, and ManualInstallationChecker classes covering validation, happy paths, edge cases, error handling, and fallback mechanisms.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 17:11:24 +08:00
lobehubbot d394743d4d 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-06 05:53:42 +00:00
semantic-release-bot 65d87b4571 🔖 chore(release): v2.0.0-next.33 [skip ci]
## [Version&nbsp;2.0.0-next.33](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.32...v2.0.0-next.33)
<sup>Released on **2025-11-06**</sup>

#### ♻ Code Refactoring

- **misc**: Refactor message create name.

#### 🐛 Bug Fixes

- **misc**: Model name display in the assistant panel disappears.

<br/>

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

#### Code refactoring

* **misc**: Refactor message create name, closes [#10074](https://github.com/lobehub/lobe-chat/issues/10074) ([08ec29f](https://github.com/lobehub/lobe-chat/commit/08ec29f))

#### What's fixed

* **misc**: Model name display in the assistant panel disappears, closes [#9830](https://github.com/lobehub/lobe-chat/issues/9830) ([54f4e18](https://github.com/lobehub/lobe-chat/commit/54f4e18))

</details>

<div align="right">

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

</div>
2025-11-06 05:52:12 +00:00
Arvin Xu 08ec29f3a2 ♻️ refactor: refactor message create name (#10074)
refactor name
2025-11-06 13:41:01 +08:00
sxjeru 54f4e18c03 🐛 fix: model name display in the assistant panel disappears (#9830)
♻️ refactor: update session model visibility logic and clean up unused imports
2025-11-06 13:40:29 +08:00
XYenon 7eb78c43e6 👷 build: add INTERNAL_APP_URL for server-to-server calls (#9960)
*  feat: add INTERNAL_APP_URL for server-to-server calls

Add INTERNAL_APP_URL environment variable to bypass CDN/proxy for internal operations like embedding and file chunking. Falls back to APP_URL if not set.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* 📝 docs: add INTERNAL_APP_URL documentation

Add documentation for INTERNAL_APP_URL environment variable in:
- docker-compose .env.example
- Docker Compose deployment guide (English and Chinese)

Explains how to bypass CDN/proxy for server-to-server operations.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

*  test: add tests for INTERNAL_APP_URL feature

Add comprehensive test coverage for INTERNAL_APP_URL:
- Test fallback behavior to APP_URL when INTERNAL_APP_URL is not set
- Test explicit INTERNAL_APP_URL configuration
- Test localhost bypass for CDN/proxy
- Test createAsyncServerClient using INTERNAL_APP_URL
- Test authentication headers in async calls

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-11-06 13:28:54 +08:00
renovate[bot] 46ccddcd24 Update dependency electron to v39 (#9971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 13:28:24 +08:00
LobeHub Bot 11aa0ecad5 🌐 chore: translate non-English comments to English in packages/const (#10073)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 10:46:27 +08:00
Arvin Xu 0e2bad0a23 🔨 chore: add conversation-flow module (#10052)
* wip

* refactor

* snapshot

* fix input

* fix assistant-tools calling case

* fix assistant-tools calling case

* fix

* fix compare-mode

* fix basic-branch-mode

* refactor branch test case

* refactor branch test case

* implement branch parse

* improve compare

* improve compare

* improve compare

* refactor

* refactor the transformer

* clean tests

* add test workflow

* update

* fix issue
2025-11-05 22:24:51 +08:00
lobehubbot 4153f182fe 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-05 14:03:06 +00:00
semantic-release-bot a48841a368 🔖 chore(release): v2.0.0-next.32 [skip ci]
## [Version&nbsp;2.0.0-next.32](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.31...v2.0.0-next.32)
<sup>Released on **2025-11-05**</sup>

#### 🐛 Bug Fixes

- **misc**: Should install new version after quit this instance.

<br/>

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

#### What's fixed

* **misc**: Should install new version after quit this instance, closes [#10064](https://github.com/lobehub/lobe-chat/issues/10064) ([9ab77b2](https://github.com/lobehub/lobe-chat/commit/9ab77b2))

</details>

<div align="right">

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

</div>
2025-11-05 14:01:51 +00:00
Shinji-Li 9ab77b2ea7 🐛 fix: should install new version after quit this instance (#10064)
fix: should install new version after quit this instance
2025-11-05 21:49:27 +08:00
LobeHub Bot a91f90340e test: add unit tests for S3 module (#10059)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 21:28:48 +08:00
lobehubbot 236b825fa0 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-05 11:26:19 +00:00
semantic-release-bot df7cfa165e 🔖 chore(release): v2.0.0-next.31 [skip ci]
## [Version&nbsp;2.0.0-next.31](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.30...v2.0.0-next.31)
<sup>Released on **2025-11-05**</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-11-05 11:25:19 +00:00
Neko d98c88b78f 👷 build(database): fix cannot correctly trace sqls (#10070)
fix(database): cannot correctly trace sqls
2025-11-05 19:14:15 +08:00
lobehubbot 3d1b050003 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-05 03:55:00 +00:00
semantic-release-bot 8ec9491b48 🔖 chore(release): v2.0.0-next.30 [skip ci]
## [Version&nbsp;2.0.0-next.30](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.29...v2.0.0-next.30)
<sup>Released on **2025-11-05**</sup>

#### ♻ Code Refactoring

- **misc**: Enhance message router with service layer and comprehensive tests.

<br/>

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

#### Code refactoring

* **misc**: Enhance message router with service layer and comprehensive tests, closes [#10056](https://github.com/lobehub/lobe-chat/issues/10056) ([62110e0](https://github.com/lobehub/lobe-chat/commit/62110e0))

</details>

<div align="right">

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

</div>
2025-11-05 03:53:51 +00:00
Arvin Xu 62110e08c8 ♻️ refactor: enhance message router with service layer and comprehensive tests (#10056)
*  test: enhance message router integration test coverage

## Summary

Completed comprehensive integration tests for message router, covering all 20 endpoints:

**New Test Coverage:**
-  removeMessage (with return messages)
-  removeAllMessages
-  removeMessagesByGroup
-  getMessages with groupId/useGroup
-  update with return messages
-  updateMessagePlugin
-  updateMetadata
-  updatePluginError (with return messages)
-  updatePluginState
-  updateTranslate (create & delete)
-  getHeatmaps
-  rankModels
-  count/countWords with date range

**Skipped Tests (require complex setup):**
- removeMessageQuery (needs UUID query IDs)
- updateMessageRAG (needs chunk & embeddings setup)
- updateTTS (needs file records)

**Test Results:**
- 33 passed 
- 6 skipped (with explanatory comments)
- 0 failed

## Coverage Improvement

Before: ~40% (8/20 endpoints)
After: ~85% (17/20 endpoints)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix test

* ♻️ refactor: extract MessageService for mutation + conditional query patterns

Refactored message router to use a new MessageService that consolidates repeated "mutation + conditional query" logic. The service handles operations that perform database mutations (delete/update) followed by conditional message list returns based on sessionId/topicId presence.

Changes:
- Created MessageService in src/server/services/message/index.ts
- Centralized conditional query logic in queryWithSuccess method
- Returns { success: true } when sessionId/topicId not provided
- Returns { messages, success: true } when sessionId/topicId provided
- Simple operations (1-2 lines) remain in router using messageModel directly
- Reduced router code significantly while improving maintainability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ♻️ refactor: improve MessageService and createNewMessage

Changes:
- Changed all comments in MessageService to English
- Extracted query logic from model for updatePluginState and updateMessage methods
- Added comprehensive unit tests for MessageService (15 tests)
- Fixed createNewMessage to accept useGroup parameter instead of hardcoding groupAssistantMessages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ♻️ refactor: move createNewMessage from model to MessageService

Changes:
- Moved createNewMessage logic from MessageModel to MessageService
- MessageModel now only handles single-responsibility create operation
- MessageService handles the "create + query" pattern consistently with other methods
- Updated router to use MessageService.createNewMessage
- Added 3 unit tests for createNewMessage in MessageService (total 18 tests now)

This follows the same pattern as other service methods: keep models focused on
database operations, while services handle business logic and composite operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ♻️ refactor: remove createNewMessage from MessageModel

Changes:
- Removed createNewMessage method from MessageModel
- Removed 5 associated unit tests from message.test.ts
- This logic now lives entirely in MessageService

Rationale:
MessageModel should focus on single-responsibility database operations.
The "create + query" pattern is a business logic concern that belongs
in the service layer, not the data access layer.

All tests passing:
- MessageModel: 91 passed, 3 skipped
- Server integration: 38 passed, 1 skipped

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 11:42:19 +08:00
LobeHub Bot d17b07fda9 🌐 chore: translate non-English comments to English in utils/server (#10057)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 10:55:53 +08:00
lobehubbot 10201a2ba1 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-04 18:27:44 +00:00
semantic-release-bot 5c66dc2b02 🔖 chore(release): v2.0.0-next.29 [skip ci]
## [Version&nbsp;2.0.0-next.29](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.28...v2.0.0-next.29)
<sup>Released on **2025-11-04**</sup>

#### ♻ Code Refactoring

- **misc**: Refactor chat message model to speed up.

<br/>

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

#### Code refactoring

* **misc**: Refactor chat message model to speed up, closes [#10053](https://github.com/lobehub/lobe-chat/issues/10053) ([035994f](https://github.com/lobehub/lobe-chat/commit/035994f))

</details>

<div align="right">

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

</div>
2025-11-04 18:26:39 +00:00
Arvin Xu 035994f1a8 ♻️ refactor: refactor chat message model to speed up (#10053)
* refactor new chat message

* ♻️ refactor: Unify message creation methods into single `internal_createMessage`

## Changes

### Method Consolidation
- Merged `internal_createMessage` and `internal_createNewMessage` into a single unified method
- All message creation now returns `{ id: string, messages: UIChatMessage[] }`
- Eliminated redundant API calls by always using `createNewMessage` backend endpoint

### Updated Call Sites (11 locations)
**Store Actions:**
- `addAIMessage` & `addUserMessage` - Added result validation

**AI Chat:**
- `generateAIChat.ts` - Extract `result.id` from response
- `generateAIChatV2.ts` - Renamed from `internal_createNewMessage` to `internal_createMessage`

**Group Chat:**
- `generateAIGroupChat.ts` - Extract `result.id` in 3 locations

**Thread & Tools:**
- `thread/action.ts` - Extract `result.id`
- `builtinTool/actions/search.ts` - Extract `result.id`
- `plugin/action.ts` - Extract `result.id`

### Test Updates
- Updated mocks to return `{ id, messages }` structure
- `thread/action.test.ts` - 4 mock updates
- `plugin/action.test.ts` - 2 mock updates

## Benefits
- **Performance**: All message creation now uses single-request pattern
- **Consistency**: Unified return type across all creation flows
- **Maintainability**: Single method to maintain instead of two similar ones

## Testing
-  Type check: 0 errors
-  Unit tests: 175/175 passed
  - message/action.test.ts: 33/33
  - plugin/action.test.ts: 26/26
  - thread/action.test.ts: 39/39
  - generateAIChat.test.ts: 41/41
  - generateAIChatV2.test.ts: 36/36

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ♻️ refactor: optimize message update operations to reduce API calls

Optimized two message update operations to reduce network requests:

1. **updatePluginState**: Modified to return updated messages
   - Backend: `MessageModel.updatePluginState` now accepts options and returns `UpdateMessageResult`
   - Router: Added `sessionId`, `topicId`, and `useGroup` parameters
   - Frontend: Service layer passes lab preferences, store uses `replaceMessages` instead of `refreshMessages`
   - Reduction: 2 requests → 1 request

2. **message.update**: Added `groupAssistantMessages` support
   - Service: `updateMessage` now passes `useGroup` flag based on lab preferences
   - Backend model already had infrastructure for returning messages
   - Reduction: Ensures consistent 1-request pattern

Tests passing (26/26 plugin tests, 14/14 integration tests).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ♻️ refactor: optimize all internal message methods to reduce API calls

Optimized 6 internal message methods and 3 error handling scenarios to reduce API calls from 2 requests (update + refresh) to 1 request (update with messages returned):

**Internal methods optimized:**
- internal_updateMessagePluginError
- internal_updateMessageRAG
- internal_deleteMessage
- internal_refreshToUpdateMessageTools
- internal_updatePluginError

**Error handling optimized:**
- internal_callPluginApi error scenarios (2 locations)
- invokeStandaloneTypePlugin invalid settings

**Changes:**
- Backend: Updated message routers to accept sessionId/topicId/useGroup and return messages
- Service: Added getUseGroupPreference() getter to simplify lab preference checks
- Service: Updated methods to use getter and return UpdateMessageResult
- Store: Changed from refreshMessages() to replaceMessages(result.messages)
- Tests: Updated 4 plugin tests to verify replaceMessages instead of refreshMessages

**Performance impact:**
Each optimized method now makes 1 request instead of 2, reducing network overhead and improving UI responsiveness.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ♻️ refactor: optimize message deletion to reduce API calls and fix group message children deletion

**Problem 1 - Group message children not deleted:**
- When deleting a `role: 'group'` message, children messages (linked via `parentId`) were not deleted
- Tool result messages were also not included in deletion

**Problem 2 - Delete operations using refresh pattern:**
- `deleteMessage`, `clearMessage`, `clearAllMessages` all used refreshMessages after deletion
- This resulted in 2 requests: delete + refresh

**Solutions:**

1. **Enhanced deleteMessage in UI layer:**
   - Added logic to find all children messages via `parentId` for group role messages
   - Combined with existing tool message deletion logic
   - All related message IDs are collected and passed to backend in one call
   - Business logic stays in UI layer, model layer remains simple

2. **Optimized delete operations:**
   - Backend: `removeMessages` now accepts sessionId/topicId/useGroup and returns messages
   - Service: `removeMessages` updated to pass options and return UpdateMessageResult
   - Store: `deleteMessage` now uses replaceMessages with returned data (2 requests → 1 request)
   - Store: `clearMessage` and `clearAllMessages` directly replace with empty array

3. **Updated tests:**
   - Fixed 4 tests to verify replaceMessages instead of refreshMessages
   - Added mock for service to return messages in delete operations
   - All 33 message action tests passing
   - All 14 integration tests passing

**Performance impact:**
- deleteMessage: 2 requests → 1 request
- clearMessage/clearAllMessages: 1 delete + 1 refresh → 1 delete + direct clear

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

*  test: add comprehensive tests for group message deletion with children

Added 2 new test cases to verify group message deletion behavior:

1. **Basic group message with children deletion:**
   - Verifies that deleting a `role: 'group'` message also deletes all children (via `parentId`)
   - Tests that unrelated messages are preserved

2. **Group message with children that have tool calls:**
   - Verifies that deleting a group message also deletes:
     - The group message itself
     - All children messages (via `parentId`)
     - Tool result messages from children (via `tool_call_id`)
   - Ensures complete cleanup of the entire message tree

**Implementation enhancement:**
- Updated `deleteMessage` to also collect and delete tool results from children messages
- Ensures no orphaned tool result messages remain after group deletion

**Test results:**
- All 35 message action tests passing (2 new tests added)
- Verifies complete cascading deletion of group message trees

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 02:15:40 +08:00
lobehubbot c7b7998505 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-04 17:12:15 +00:00
semantic-release-bot a41c7a3fb7 🔖 chore(release): v2.0.0-next.28 [skip ci]
## [Version&nbsp;2.0.0-next.28](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.27...v2.0.0-next.28)
<sup>Released on **2025-11-04**</sup>

####  Features

- **misc**: Support install sreamable http mcp server on web.

<br/>

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

#### What's improved

* **misc**: Support install sreamable http mcp server on web, closes [#10044](https://github.com/lobehub/lobe-chat/issues/10044) [#9916](https://github.com/lobehub/lobe-chat/issues/9916) ([85454c5](https://github.com/lobehub/lobe-chat/commit/85454c5))

</details>

<div align="right">

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

</div>
2025-11-04 17:11:03 +00:00
Shinji-Li 85454c5e7c feat: support install sreamable http mcp server on web (#10044)
* 🐛 fix: OIDC error when connecting to self-host instance (#9916)

fix: oidc/consent redirect header

* feat: support streamable mcp install

* feat: env fixed

* feat: when in desktop only show http mcp

* feat: use http connectionType to query mcp list

* fix: delete useless code

* fix: update the actions test

* feat: change the import way

* feat: change the import way

* feat: change the enum type

* fix: slove types problem

* fix: slove types problem

* fix: mobile not show custom add mcp button

---------

Co-authored-by: Aloxaf <bailong104@gmail.com>
2025-11-05 00:57:19 +08:00
Neko 84148a8dd3 🔨 chore: improve renovate config to not group minor & major (#10051)
chore: improve renovate config to not group minor & major
2025-11-04 22:47:12 +08:00
lobehubbot d79ffa37e2 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-04 14:04:44 +00:00
semantic-release-bot c4873d1854 🔖 chore(release): v2.0.0-next.27 [skip ci]
## [Version&nbsp;2.0.0-next.27](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.26...v2.0.0-next.27)
<sup>Released on **2025-11-04**</sup>

#### ♻ Code Refactoring

- **misc**: Refactor services to a more clean structure.

<br/>

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

#### Code refactoring

* **misc**: Refactor services to a more clean structure, closes [#10050](https://github.com/lobehub/lobe-chat/issues/10050) ([de61dfa](https://github.com/lobehub/lobe-chat/commit/de61dfa))

</details>

<div align="right">

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

</div>
2025-11-04 14:03:32 +00:00
Arvin Xu de61dfaad4 ♻️ refactor: refactor services to a more clean structure (#10050)
* refactor services

* clean tests

* fix type
2025-11-04 21:52:30 +08:00
lobehubbot 1bea16f292 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-04 13:14:51 +00:00
semantic-release-bot 1da176191b 🔖 chore(release): v2.0.0-next.26 [skip ci]
## [Version&nbsp;2.0.0-next.26](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.25...v2.0.0-next.26)
<sup>Released on **2025-11-04**</sup>

#### ♻ Code Refactoring

- **misc**: Add settings (jsonb) column to `ai_models` table.

<br/>

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

#### Code refactoring

* **misc**: Add settings (jsonb) column to `ai_models` table, closes [#10042](https://github.com/lobehub/lobe-chat/issues/10042) ([7e1dd02](https://github.com/lobehub/lobe-chat/commit/7e1dd02))

</details>

<div align="right">

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

</div>
2025-11-04 13:13:38 +00:00
sxjeru 7e1dd02d7c ♻️ refactor: add settings (jsonb) column to ai_models table (#10042)
* Refactor code structure for improved readability and maintainability

* edit dbml

* feat: 更新 AiInfraRepos 以支持用户设置覆盖内置设置

* Revert "Refactor code structure for improved readability and maintainability"

This reverts commit 81453d8dc3.

* Refactor code structure for improved readability and maintainability

* 添加 IF NOT EXISTS 选项以避免重复列添加

* format
2025-11-04 21:02:40 +08:00
Arvin Xu b0dd7be095 test: add more discover bdd tests (#10048)
add bdd tests
2025-11-04 20:32:09 +08:00
lobehubbot 3cf6242877 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-04 12:10:29 +00:00
semantic-release-bot da063a726a 🔖 chore(release): v2.0.0-next.25 [skip ci]
## [Version&nbsp;2.0.0-next.25](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.24...v2.0.0-next.25)
<sup>Released on **2025-11-04**</sup>

####  Features

- **misc**: Display assistant message in group.

<br/>

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

#### What's improved

* **misc**: Display assistant message in group, closes [#9941](https://github.com/lobehub/lobe-chat/issues/9941) ([59b6ac3](https://github.com/lobehub/lobe-chat/commit/59b6ac3))

</details>

<div align="right">

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

</div>
2025-11-04 12:09:19 +00:00
Arvin Xu 59b6ac3a1c feat: display assistant message in group (#9941)
* use message group

* refactor

* fix tests

* fix tests
2025-11-04 19:58:32 +08:00
lobehubbot 8bab7ad448 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-04 11:02:27 +00:00
semantic-release-bot f69e7a22df 🔖 chore(release): v2.0.0-next.24 [skip ci]
## [Version&nbsp;2.0.0-next.24](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.23...v2.0.0-next.24)
<sup>Released on **2025-11-04**</sup>

#### 💄 Styles

- **misc**: Improve lab style.

<br/>

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

#### Styles

* **misc**: Improve lab style, closes [#10040](https://github.com/lobehub/lobe-chat/issues/10040) ([bbf1c0b](https://github.com/lobehub/lobe-chat/commit/bbf1c0b))

</details>

<div align="right">

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

</div>
2025-11-04 11:01:15 +00:00
Neko ffcdd5fac0 🔨 chore: improve renovate config separate major versions only (#10045)
chore: improve renovate config separate major versions only
2025-11-04 18:49:42 +08:00
Arvin Xu bbf1c0bbe9 💄 style: improve lab style (#10040)
* update

* refactor lab

* Update package.json
2025-11-04 18:35:49 +08:00
lobehubbot fd226d03d4 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-04 09:51:47 +00:00
semantic-release-bot 599e199b91 🔖 chore(release): v2.0.0-next.23 [skip ci]
## [Version&nbsp;2.0.0-next.23](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.22...v2.0.0-next.23)
<sup>Released on **2025-11-04**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix send message.

<br/>

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

#### What's fixed

* **misc**: Fix send message, closes [#10041](https://github.com/lobehub/lobe-chat/issues/10041) [#9984](https://github.com/lobehub/lobe-chat/issues/9984) ([7cca60f](https://github.com/lobehub/lobe-chat/commit/7cca60f))

</details>

<div align="right">

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

</div>
2025-11-04 09:50:34 +00:00
Arvin Xu 7cca60fcb8 🐛 fix: fix send message (#10041)
* fix

* Revert "Update dependency vite to v7 (#9984)"

This reverts commit f1ffaff96f.
2025-11-04 17:38:40 +08:00
Neko d517f77d1d 🔨 chore: improve renovate config to split pinned deps while keeping grouped (#10043)
chore: improve renovate config to split pinned deps while keeping grouped
2025-11-04 17:36:16 +08:00
LobeHub Bot c8e5a630ed test: add unit tests for MCPSystemDepsCheckService (#10037)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-04 16:02:27 +08:00
renovate[bot] 9a231ee6d3 Update dependency ora to v9 (#9977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 15:45:52 +08:00
lobehubbot fa89dbf6b7 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-04 06:42:06 +00:00
semantic-release-bot 6d0f09fa1e 🔖 chore(release): v2.0.0-next.22 [skip ci]
## [Version&nbsp;2.0.0-next.22](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.21...v2.0.0-next.22)
<sup>Released on **2025-11-04**</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-11-04 06:40:46 +00:00
Arvin Xu 0d8188c60b ️ perf: improve db query performance (#10036)
* improve db query performance

* Remove query timing log from hasMoreThanN

Removed timing log from hasMoreThanN method.
2025-11-04 14:29:11 +08:00
LobeHub Bot aae047265e 🌐 chore: translate non-English comments to English in desktop controllers (#10034)
* 🌐 chore: translate non-English comments to English in desktop controllers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* 🧪 fix: update Desktop test expectations to match English error messages

- Updated TrayMenuCtr.test.ts to expect English error messages instead of Chinese
- Fixes failing tests after comment translation changes
- Changed '托盘通知仅在 Windows 平台支持' to 'Tray notifications are only supported on Windows platform'
- Changed '托盘功能仅在 Windows 平台支持' to 'Tray functionality is only supported on Windows platform'

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
2025-11-04 13:38:31 +08:00
renovate[bot] f1ffaff96f Update dependency vite to v7 (#9984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 13:13:52 +08:00
renovate[bot] 74637839f5 Update dependency pino to v10 (#9979)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 10:37:43 +08:00
lobehubbot 4d9181ece0 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-04 02:28:29 +00:00
semantic-release-bot f833a461aa 🔖 chore(release): v2.0.0-next.21 [skip ci]
## [Version&nbsp;2.0.0-next.21](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.20...v2.0.0-next.21)
<sup>Released on **2025-11-04**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix oidc auth timeout issue on the desktop.

<br/>

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

#### What's fixed

* **misc**: Fix oidc auth timeout issue on the desktop, closes [#10025](https://github.com/lobehub/lobe-chat/issues/10025) ([20666db](https://github.com/lobehub/lobe-chat/commit/20666db))

</details>

<div align="right">

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

</div>
2025-11-04 02:26:52 +00:00
Neko cb8c606d06 🔨 chore: reconfigure renovate to group patch updates (#10008)
chore: reconfigure renovate to group patch updates
2025-11-04 10:13:19 +08:00
Arvin Xu 20666db14f 🐛 fix: fix oidc auth timeout issue on the desktop (#10025)
* add tests

* fix auth timeout issue

* update locale

* fix tests
2025-11-04 10:12:54 +08:00
Arvin Xu e6fc44be76 test: add unit tests for apiKey (#10031)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-04 10:12:45 +08:00
LobeHub Bot 1be312cb25 test: add unit tests for MCPService (#10032)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-04 10:12:28 +08:00
LobeHub Bot 9c0ac419d0 🌐 chore: translate non-English comments to English in utils/client (#10029) 2025-11-04 09:13:48 +08:00
Arvin Xu 3556e5986c 🔨 chore: add auto creating test workflow (#10030)
create auto test mode
2025-11-04 02:18:35 +08:00
Arvin Xu 17090f1e8c 💄 style: improve oidc mobile layout style (#10026)
improve oidc layout again
2025-11-04 02:04:03 +08:00
Arvin Xu 6cce80ee9a 🌐 chore: translate non-English comments to English in chat service (#10028)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-04 01:54:44 +08:00
Arvin Xu 2da01ca1c7 🔨 chore: add workflow for translating Chinese comments to English (#10027)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-04 01:54:07 +08:00
Arvin Xu 665e6c99f5 🔨 chore: add desktop test workflow (#10024)
* workflow

* fix tests

* update

* update
2025-11-04 01:36:05 +08:00
lobehubbot ccd2c0b510 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-03 17:11:27 +00:00
semantic-release-bot 9346900a50 🔖 chore(release): v2.0.0-next.20 [skip ci]
## [Version&nbsp;2.0.0-next.20](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.19...v2.0.0-next.20)
<sup>Released on **2025-11-03**</sup>

#### 💄 Styles

- **misc**: Improve oidc layout style.

<br/>

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

#### Styles

* **misc**: Improve oidc layout style, closes [#10023](https://github.com/lobehub/lobe-chat/issues/10023) ([5008be7](https://github.com/lobehub/lobe-chat/commit/5008be7))

</details>

<div align="right">

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

</div>
2025-11-03 17:10:18 +00:00
Arvin Xu 5008be7fe9 💄 style: improve oidc layout style (#10023)
improve oidc layout style
2025-11-04 00:57:52 +08:00
lobehubbot 9e81151487 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-03 16:48:22 +00:00
semantic-release-bot 1fdc49ec0f 🔖 chore(release): v2.0.0-next.19 [skip ci]
## [Version&nbsp;2.0.0-next.19](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.18...v2.0.0-next.19)
<sup>Released on **2025-11-03**</sup>

#### ♻ Code Refactoring

- **misc**: Remove `NEXT_PUBLIC_SERVICE_MODE` env and use server by default.

<br/>

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

#### Code refactoring

* **misc**: Remove `NEXT_PUBLIC_SERVICE_MODE` env and use server by default, closes [#10017](https://github.com/lobehub/lobe-chat/issues/10017) ([f2ab2fc](https://github.com/lobehub/lobe-chat/commit/f2ab2fc))

</details>

<div align="right">

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

</div>
2025-11-03 16:47:11 +00:00
Arvin Xu f2ab2fcef6 ♻️ refactor: remove NEXT_PUBLIC_SERVICE_MODE env and use server by default (#10017)
* remove NEXT_PUBLIC_SERVICE_MODE

* update

* fix tests

* update e2e workflow

* update config

* Rename DATABASE_TEST_URL to DATABASE_URL
2025-11-04 00:34:37 +08:00
renovate[bot] 3eaa645fb0 Update dependency dotenv to v17 (#9970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 23:10:35 +08:00
lobehubbot 1500e8cdb3 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-03 14:29:12 +00:00
semantic-release-bot 5262a73308 🔖 chore(release): v2.0.0-next.18 [skip ci]
## [Version&nbsp;2.0.0-next.18](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.17...v2.0.0-next.18)
<sup>Released on **2025-11-03**</sup>

#### ♻ Code Refactoring

- **misc**: Refactor trpc request to use zod schema.

#### 💄 Styles

- **misc**: Improve built-in client OIDC user flow.

<br/>

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

#### Code refactoring

* **misc**: Refactor trpc request to use zod schema, closes [#10016](https://github.com/lobehub/lobe-chat/issues/10016) ([1a84f2c](https://github.com/lobehub/lobe-chat/commit/1a84f2c))

#### Styles

* **misc**: Improve built-in client OIDC user flow, closes [#10020](https://github.com/lobehub/lobe-chat/issues/10020) ([80202ed](https://github.com/lobehub/lobe-chat/commit/80202ed))

</details>

<div align="right">

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

</div>
2025-11-03 14:27:58 +00:00
Arvin Xu 1a84f2cb00 ♻️ refactor: refactor trpc request to use zod schema (#10016)
refactor request api
2025-11-03 22:16:37 +08:00
Arvin Xu 80202ed4ff 💄 style: improve built-in client OIDC user flow (#10020)
* refactor

* make builtin client auto consent

* i18n
2025-11-03 22:13:14 +08:00
lobehubbot 0009816364 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-03 13:39:09 +00:00
semantic-release-bot af839cc5c3 🔖 chore(release): v2.0.0-next.17 [skip ci]
## [Version&nbsp;2.0.0-next.17](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.16...v2.0.0-next.17)
<sup>Released on **2025-11-03**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix regex ReDoS.

<br/>

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

#### What's fixed

* **misc**: Fix regex ReDoS, closes [#10012](https://github.com/lobehub/lobe-chat/issues/10012) ([1d8d5cd](https://github.com/lobehub/lobe-chat/commit/1d8d5cd))

</details>

<div align="right">

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

</div>
2025-11-03 13:37:59 +00:00
Arvin Xu 5c9baf490f test: add more smoke bdd tests (#10014)
add more smoke bdd tests
2025-11-03 21:24:46 +08:00
Arvin Xu 1d8d5cda30 🐛 fix: fix regex ReDoS (#10012)
* fix regex ReDoS

* fix regex ReDoS

* fix regex ReDoS

* fix regex ReDoS

* fix regex ReDoS
2025-11-03 21:15:34 +08:00
lobehubbot bd071fa3c4 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-03 05:54:17 +00:00
semantic-release-bot fa179fc934 🔖 chore(release): v2.0.0-next.16 [skip ci]
## [Version&nbsp;2.0.0-next.16](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.15...v2.0.0-next.16)
<sup>Released on **2025-11-03**</sup>

#### ♻ Code Refactoring

- **misc**: Remove deperated code.

<br/>

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

#### Code refactoring

* **misc**: Remove deperated code, closes [#10001](https://github.com/lobehub/lobe-chat/issues/10001) ([4ee4590](https://github.com/lobehub/lobe-chat/commit/4ee4590))

</details>

<div align="right">

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

</div>
2025-11-03 05:53:09 +00:00
Arvin Xu 4ee4590630 ♻️ refactor: remove deperated code (#10001)
* remove

* improve

* remove chatModels

* remove user model list slice

* improve lab image

* remove deprecated code

* remove clerk_sign_up

* fix tests
2025-11-03 13:14:35 +08:00
lobehubbot 788d7046ea 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-03 05:08:55 +00:00
semantic-release-bot 65cf324d07 🔖 chore(release): v2.0.0-next.15 [skip ci]
## [Version&nbsp;2.0.0-next.15](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.14...v2.0.0-next.15)
<sup>Released on **2025-11-03**</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-11-03 05:07:48 +00:00
YuTengjing 44776b40bc 👷 build: nodejs 24 (#10003)
* build: upgrade node 24

* build: run pnpm update

* fix: tsgo not support baseUrl
2025-11-03 12:56:15 +08:00
YuTengjing 637460bdf0 🔧 chore(workflow): fix desktop PR build concurrency to isolate per-PR builds (#10005) 2025-11-03 12:23:39 +08:00
lobehubbot 4363f7d306 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-02 15:49:07 +00:00
semantic-release-bot d7f927d934 🔖 chore(release): v2.0.0-next.14 [skip ci]
## [Version&nbsp;2.0.0-next.14](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.13...v2.0.0-next.14)
<sup>Released on **2025-11-02**</sup>

#### ♻ Code Refactoring

- **misc**: Remove client service.

<br/>

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

#### Code refactoring

* **misc**: Remove client service, closes [#9991](https://github.com/lobehub/lobe-chat/issues/9991) ([9137dba](https://github.com/lobehub/lobe-chat/commit/9137dba))

</details>

<div align="right">

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

</div>
2025-11-02 15:47:58 +00:00
Arvin Xu 9137dba6a0 ♻️ refactor: remove client service (#9991)
* remove client service

* remove edge client and <InitClientDB />

* fix search tests

* fix app url

* fix tests

* fix tests

* fix tests

* remove InitClientDB

* fix tests

* fix tests
2025-11-02 23:37:50 +08:00
Arvin Xu c265f6c8ad ️ perf: fix provider link issue (#9993)
* fix provider link issue

* fix again

* fix link
2025-11-02 23:37:26 +08:00
lobehubbot c58cfd98a4 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-02 15:25:36 +00:00
semantic-release-bot 9ca8f7bbe2 🔖 chore(release): v2.0.0-next.13 [skip ci]
## [Version&nbsp;2.0.0-next.13](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.12...v2.0.0-next.13)
<sup>Released on **2025-11-02**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix image prompt form.

<br/>

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

#### What's fixed

* **misc**: Fix image prompt form, closes [#9995](https://github.com/lobehub/lobe-chat/issues/9995) ([799e6fd](https://github.com/lobehub/lobe-chat/commit/799e6fd))

</details>

<div align="right">

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

</div>
2025-11-02 15:24:34 +00:00
Coooolfan 799e6fd0fd 🐛 fix: fix image prompt form (#9995)
* 🐛 fix: do not create image on Enter key during composition

* 🐛 fix: include authorization header in mock user context
2025-11-02 23:12:53 +08:00
lobehubbot 838a7d4eed 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-02 14:40:32 +00:00
semantic-release-bot 955c92e4f1 🔖 chore(release): v2.0.0-next.12 [skip ci]
## [Version&nbsp;2.0.0-next.12](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.11...v2.0.0-next.12)
<sup>Released on **2025-11-02**</sup>

#### 💄 Styles

- **misc**: Add padding to TopicList component.

<br/>

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

#### Styles

* **misc**: Add padding to TopicList component, closes [#9994](https://github.com/lobehub/lobe-chat/issues/9994) ([c1e7381](https://github.com/lobehub/lobe-chat/commit/c1e7381))

</details>

<div align="right">

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

</div>
2025-11-02 14:39:28 +00:00
Coooolfan c1e7381a33 💄 style: add padding to TopicList component (#9994)
 fix: add padding to TopicList component
2025-11-02 22:24:21 +08:00
lobehubbot adad02a93c 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-02 14:21:53 +00:00
semantic-release-bot f966f7f4a5 🔖 chore(release): v2.0.0-next.11 [skip ci]
## [Version&nbsp;2.0.0-next.11](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.10...v2.0.0-next.11)
<sup>Released on **2025-11-02**</sup>

#### 💄 Styles

- **misc**: Smoothed model descriptions in ko-KR locales.

<br/>

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

#### Styles

* **misc**: Smoothed model descriptions in ko-KR locales, closes [#9998](https://github.com/lobehub/lobe-chat/issues/9998) ([fde1d8b](https://github.com/lobehub/lobe-chat/commit/fde1d8b))

</details>

<div align="right">

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

</div>
2025-11-02 14:20:40 +00:00
sunny1234597 fde1d8bfac 💄 style: smoothed model descriptions in ko-KR locales (#9998)
smoothed model descriptions in ko-KR locales 

Updated descriptions for various AI models in Korean.
2025-11-02 22:08:55 +08:00
Arvin Xu dc5917948c 👷 build: revert turbopack for bundling (#9999)
Revert "👷 build: switch to turbopack for bundling (#9990)"

This reverts commit 4565692cb9.
2025-11-02 22:03:18 +08:00
lobehubbot 9ab6935deb 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-02 10:39:37 +00:00
semantic-release-bot 2516b89c7c 🔖 chore(release): v2.0.0-next.10 [skip ci]
## [Version&nbsp;2.0.0-next.10](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.9...v2.0.0-next.10)
<sup>Released on **2025-11-02**</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-11-02 10:38:22 +00:00
Arvin Xu 4565692cb9 👷 build: switch to turbopack for bundling (#9990)
try turbopack
2025-11-02 18:24:35 +08:00
Rylan Cai 78df7a7e86 ♻️ Refactor(V2): Remove deprecate envs to reserve Namespace for better-auth (#9954) 2025-11-02 16:45:46 +08:00
Arvin Xu b99bb9a856 🔨 chore: fix desktop-pr workflow (#9992)
fix desktop
2025-11-02 14:28:32 +08:00
lobehubbot f110d79228 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-02 05:45:09 +00:00
semantic-release-bot 3513bf4363 🔖 chore(release): v2.0.0-next.9 [skip ci]
## [Version&nbsp;2.0.0-next.9](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.8...v2.0.0-next.9)
<sup>Released on **2025-11-02**</sup>

#### ♻ Code Refactoring

- **misc**: Remove dalle builtin plugin.

<br/>

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

#### Code refactoring

* **misc**: Remove dalle builtin plugin, closes [#9952](https://github.com/lobehub/lobe-chat/issues/9952) ([2d4d70a](https://github.com/lobehub/lobe-chat/commit/2d4d70a))

</details>

<div align="right">

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

</div>
2025-11-02 05:43:47 +00:00
Arvin Xu 2d4d70a66d ♻️ refactor: remove dalle builtin plugin (#9952)
* pre merge

* fix lint

* fix lint

* clean docker workflow

* fix tests

* fix docker

* fix tests

* fix tests

* fix tests

* fix docker

* update client db

* remove dalle

* fix tests

* fix tests

* improve tests

* fix tests
2025-11-02 13:31:01 +08:00
renovate[bot] 831948a305 Update GitHub Artifact Actions (major) (#9986)
Update GitHub Artifact Actions

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-02 11:26:30 +08:00
Arvin Xu 7eaa7ca326 🔨 chore: fix docker workflow (#9987)
fix docker workflow
2025-11-02 11:08:29 +08:00
renovate[bot] 08f97dcee4 Update actions/setup-node action to v6 (#9964)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-02 10:15:08 +08:00
renovate[bot] 5f4e1d37fb Update actions/checkout action to v5 (#9963)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-02 10:14:17 +08:00
semantic-release-bot a5e0e5acb2 🔖 chore(release): v2.0.0-next.8 [skip ci]
## [Version&nbsp;2.0.0-next.8](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.7...v2.0.0-next.8)
<sup>Released on **2025-11-02**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

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

</details>

<div align="right">

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

</div>
2025-11-02 02:13:27 +00:00
LobeHub Bot f49996cc84 🤖 style: update i18n (#9958) 2025-11-02 10:01:50 +08:00
lobehubbot 6b01095243 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-01 17:04:59 +00:00
semantic-release-bot 9bb717696b 🔖 chore(release): v2.0.0-next.7 [skip ci]
## [Version&nbsp;2.0.0-next.7](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.6...v2.0.0-next.7)
<sup>Released on **2025-11-01**</sup>

####  Features

- **misc**: Upgrade to Next 16.

<br/>

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

#### What's improved

* **misc**: Upgrade to Next 16, closes [#9851](https://github.com/lobehub/lobe-chat/issues/9851) ([abb71ec](https://github.com/lobehub/lobe-chat/commit/abb71ec))

</details>

<div align="right">

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

</div>
2025-11-01 17:03:54 +00:00
Arvin Xu abb71ec846 feat: upgrade to Next 16 (#9851)
* upgrade next 16

* try to fix

* try to fix

* upgrade

* fix sitemap build

* try to fix build

* try to fix build with next 16

* fix docker permission

* 🔒 fix(ci): fix code injection vulnerability and permissions in docker workflow

- Add pull-requests: write permission to allow PR comments
- Fix code injection vulnerability by using env variables instead of direct interpolation
- Prevent potential security risks from malicious branch names in pull_request_target events

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* 🔧 chore(ci): change desktop pr build to use pull_request_target

- Change from pull_request to pull_request_target to access secrets and write permissions
- Update permissions from read-all to specific write permissions for contents and pull-requests
- This allows PR builds to create releases and comment on PRs from forks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* add comment

* fix on

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-02 00:53:13 +08:00
lobehubbot 39a7399765 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-31 16:40:36 +00:00
semantic-release-bot 57858916eb 🔖 chore(release): v2.0.0-next.6 [skip ci]
## [Version&nbsp;2.0.0-next.6](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.5...v2.0.0-next.6)
<sup>Released on **2025-10-31**</sup>

#### 🐛 Bug Fixes

- **AssistantStore**: Add missing identifier parameter.

<br/>

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

#### What's fixed

* **AssistantStore**: Add missing identifier parameter, closes [#9948](https://github.com/lobehub/lobe-chat/issues/9948) ([2e40855](https://github.com/lobehub/lobe-chat/commit/2e40855))

</details>

<div align="right">

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

</div>
2025-10-31 16:39:30 +00:00
XYenon 2e408554e2 🐛 fix(AssistantStore): add missing identifier parameter (#9948)
🐛 fix(AssistantStore): add missing identifier parameter in fallback fetch

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-11-01 00:28:12 +08:00
lobehubbot 86b5e9eb4c 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-31 04:54:07 +00:00
semantic-release-bot b3243cf6fb 🔖 chore(release): v2.0.0-next.5 [skip ci]
## [Version&nbsp;2.0.0-next.5](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.4...v2.0.0-next.5)
<sup>Released on **2025-10-31**</sup>

#### ♻ Code Refactoring

- **misc**: Migrating Firecrawl to v2.

<br/>

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

#### Code refactoring

* **misc**: Migrating Firecrawl to v2, closes [#9850](https://github.com/lobehub/lobe-chat/issues/9850) ([efb4c22](https://github.com/lobehub/lobe-chat/commit/efb4c22))

</details>

<div align="right">

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

</div>
2025-10-31 04:52:55 +00:00
sxjeru efb4c22aff ♻️ refactor: Migrating Firecrawl to v2 (#9850)
*  feat: 更新 Firecrawl API 版本,增强搜索功能和结果映射

*  feat: 更新 FirecrawlMetadata 和 FirecrawlResults 接口,添加可选字段并增强错误处理

* fix typo (native -> naive)

*  feat: 更新模型配置,移除 Claude 3.5 Sonnet 相关条目,添加 MiniMax M2 和 KAT-Dev 32B 模型

*  feat: 添加 MiniMax M2 模型并更新代理 URL

* fix test
2025-10-31 12:41:14 +08:00
Arvin Xu 8abfdecd54 ️ perf: improve db query performance (#9946)
* fix db performance issue

* fix tests
2025-10-31 12:39:59 +08:00
lobehubbot e73e877db1 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-31 03:32:07 +00:00
semantic-release-bot 4e7f383ff4 🔖 chore(release): v2.0.0-next.4 [skip ci]
## [Version&nbsp;2.0.0-next.4](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.3...v2.0.0-next.4)
<sup>Released on **2025-10-31**</sup>

#### ♻ Code Refactoring

- **misc**: Remove azure-ad auth provider.

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Code refactoring

* **misc**: Remove azure-ad auth provider, closes [#9942](https://github.com/lobehub/lobe-chat/issues/9942) ([103c4d7](https://github.com/lobehub/lobe-chat/commit/103c4d7))

#### Styles

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

</details>

<div align="right">

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

</div>
2025-10-31 03:30:54 +00:00
Arvin Xu 103c4d70fc ♻️ refactor: remove azure-ad auth provider (#9942)
remove azure ad
2025-10-31 11:19:39 +08:00
LobeHub Bot 3a6468ff48 🤖 style: update i18n (#9944)
💄 style: update i18n

Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-10-31 11:17:28 +08:00
Arvin Xu 7342315875 ♻️ refactor: improve access token duration (#9943)
update access token duaration
2025-10-31 02:06:53 +08:00
lobehubbot aa7f98c00d 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-30 17:27:33 +00:00
semantic-release-bot f78682c704 🔖 chore(release): v2.0.0-next.3 [skip ci]
## [Version&nbsp;2.0.0-next.3](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.2...v2.0.0-next.3)
<sup>Released on **2025-10-30**</sup>

#### ♻ Code Refactoring

- **misc**: Remove llm page.

#### 💄 Styles

- **misc**: Add new bedrock model support, add pricing info for Azure GPT-5 series models.

<br/>

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

#### Code refactoring

* **misc**: Remove llm page, closes [#9940](https://github.com/lobehub/lobe-chat/issues/9940) ([6ec01a3](https://github.com/lobehub/lobe-chat/commit/6ec01a3))

#### Styles

* **misc**: Add new bedrock model support, closes [#9826](https://github.com/lobehub/lobe-chat/issues/9826) ([1b8a981](https://github.com/lobehub/lobe-chat/commit/1b8a981))
* **misc**: Add pricing info for Azure GPT-5 series models, closes [#9833](https://github.com/lobehub/lobe-chat/issues/9833) ([39a80c5](https://github.com/lobehub/lobe-chat/commit/39a80c5))

</details>

<div align="right">

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

</div>
2025-10-30 17:26:18 +00:00
Arvin Xu 6ec01a3c83 ♻️ refactor: remove llm page (#9940)
remove llm page
2025-10-31 01:14:36 +08:00
Kevin Zhao 39a80c5604 💄 style: add pricing info for Azure GPT-5 series models (#9833)
* 💰 feat: add pricing info for Azure GPT-5 series models

Added comprehensive pricing information for all Azure OpenAI GPT-5 series models:
- GPT-5 Pro: $15/$120 per million tokens (input/output)
- GPT-5 Codex: $1.25/$10 per million tokens with cache support
- GPT-5: $1.25/$10 per million tokens with cache support
- GPT-5 Mini: $0.25/$2 per million tokens with cache support
- GPT-5 Nano: $0.05/$0.4 per million tokens with cache support
- GPT-5 Chat: $1.25/$10 per million tokens with cache support

Pricing aligns with OpenAI official rates and includes cache read pricing where applicable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* 🔥 feat: remove Azure GPT-5 series models

Removed all GPT-5 series model configurations from Azure provider:
- GPT-5 Pro
- GPT-5 Codex
- GPT-5
- GPT-5 Mini
- GPT-5 Nano
- GPT-5 Chat

These models are not yet officially released by Azure OpenAI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Zhao <zhaokm@ZhaodeMacBook-Pro.local>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zhao <zhaokm@842f57833757mbpnetwork.lan>
2025-10-31 01:02:48 +08:00
Kevin Zhao 1b8a981e5b 💄 style: add new bedrock model support (#9826)
*  feat: add new bedrock model support

* 🔥 chore: remove model provider config per reviewer feedback

Removed Claude Sonnet 4.5 and Claude Haiku 4.5 model configurations from the Bedrock provider config file as per reviewer request.

The model definitions remain in the model bank (packages/model-bank/src/aiModels/bedrock.ts) and can be enabled once needed.

---------

Co-authored-by: Zhao <zhaokm@ZhaodeMacBook-Pro.local>
2025-10-31 01:01:59 +08:00
lobehubbot 44247fd1f6 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-30 16:45:31 +00:00
semantic-release-bot 1b837f7dd0 🔖 chore(release): v2.0.0-next.2 [skip ci]
## [Version&nbsp;2.0.0-next.2](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.1...v2.0.0-next.2)
<sup>Released on **2025-10-30**</sup>

#### 🐛 Bug Fixes

- **misc**: Hide marketplace link from Plugin List when market disabled, OIDC error when connecting to self-host instance, only include input_fidelity parameter for gpt-image-1..

<br/>

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

#### What's fixed

* **misc**: Hide marketplace link from Plugin List when market disabled, closes [#9929](https://github.com/lobehub/lobe-chat/issues/9929) ([e303979](https://github.com/lobehub/lobe-chat/commit/e303979))
* **misc**: OIDC error when connecting to self-host instance, closes [#9916](https://github.com/lobehub/lobe-chat/issues/9916) ([7a2ca19](https://github.com/lobehub/lobe-chat/commit/7a2ca19))
* **misc**: Only include input_fidelity parameter for gpt-image-1., closes [#9920](https://github.com/lobehub/lobe-chat/issues/9920) ([65dbc63](https://github.com/lobehub/lobe-chat/commit/65dbc63))

</details>

<div align="right">

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

</div>
2025-10-30 16:44:24 +00:00
Aloxaf 7a2ca19d5b 🐛 fix: OIDC error when connecting to self-host instance (#9916)
fix: oidc/consent redirect header
(cherry picked from commit 2e2b9c4c88)
2025-10-31 00:27:06 +08:00
Taehyung Lee 65dbc6383d 🐛 fix: only include input_fidelity parameter for gpt-image-1. (#9920)
fix: only include input_fidelity parameter for gpt-image-1.

The input_fidelity parameter was incorrectly sent to all models during image editing, causing errors with DALL-E 2 and gpt-5-image-mini.
2025-10-31 00:24:24 +08:00
Michael Webb e303979e0f 🐛 fix: Hide marketplace link from Plugin List when market disabled (#9929)
Hide marketplace link from Plugin List when market disabled
2025-10-31 00:22:30 +08:00
lobehubbot f31b5294f3 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-30 13:56:06 +00:00
semantic-release-bot 9962f64eeb 🔖 chore(release): v2.0.0-next.1 [skip ci]
## [Version&nbsp;2.0.0-next.1](https://github.com/lobehub/lobe-chat/compare/v1.143.0-next.2...v2.0.0-next.1)
<sup>Released on **2025-10-30**</sup>

####  Features

- **misc**: 2.0 next baseline.

<br/>

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

#### What's improved

* **misc**: 2.0 next baseline ([8c57dfd](https://github.com/lobehub/lobe-chat/commit/8c57dfd))

</details>

#### 💥 BREAKING CHANGES

* **misc**: starting V2

<div align="right">

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

</div>
2025-10-30 13:55:04 +00:00
arvinxx 8c57dfd3fe 💥 feat: 2.0 next baseline
BREAKING CHANGE: starting V2
2025-10-30 21:33:52 +08:00
semantic-release-bot a1fc90d8b9 🔖 chore(release): v1.143.0-next.2 [skip ci]
## [Version&nbsp;1.143.0-next.2](https://github.com/lobehub/lobe-chat/compare/v1.143.0-next.1...v1.143.0-next.2)
<sup>Released on **2025-10-30**</sup>

####  Features

- **misc**: 2.0 next init.

<br/>

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

#### What's improved

* **misc**: 2.0 next init ([26daac5](https://github.com/lobehub/lobe-chat/commit/26daac5))

</details>

<div align="right">

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

</div>
2025-10-30 13:31:12 +00:00
arvinxx 26daac5a6d 💥 feat: 2.0 next init 2025-10-30 21:19:54 +08:00
arvinxx 1d95153960 update 2025-10-30 21:18:47 +08:00
lobehubbot 51d7d187e8 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-30 13:14:41 +00:00
semantic-release-bot 7c6805f819 🔖 chore(release): v1.143.0-next.1 [skip ci]
## [Version&nbsp;1.143.0-next.1](https://github.com/lobehub/lobe-chat/compare/v1.142.8...v1.143.0-next.1)
<sup>Released on **2025-10-30**</sup>

####  Features

- **misc**: Try 2.0 next.

<br/>

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

#### What's improved

* **misc**: Try 2.0 next ([e0af4e6](https://github.com/lobehub/lobe-chat/commit/e0af4e6))

</details>

#### 💥 BREAKING CHANGES

* **misc**: starting V2

<div align="right">

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

</div>
2025-10-30 13:13:38 +00:00
arvinxx 177f3f5589 update workflow 2025-10-30 21:02:42 +08:00
arvinxx e0af4e6c45 💥 feat: try 2.0 next
BREAKING CHANGE: starting V2
2025-10-30 20:59:48 +08:00
lobehubbot 7f7e571755 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-30 04:14:55 +00:00
semantic-release-bot 0df0378def 🔖 chore(release): v1.142.8 [skip ci]
### [Version&nbsp;1.142.8](https://github.com/lobehub/lobe-chat/compare/v1.142.7...v1.142.8)
<sup>Released on **2025-10-30**</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-10-30 04:13:39 +00:00
dependabot[bot] 4376ba2bb2 👷 build(deps): bump next-auth from 5.0.0-beta.29 to 5.0.0-beta.30 (#9926)
build(deps): bump next-auth from 5.0.0-beta.29 to 5.0.0-beta.30

Bumps [next-auth](https://github.com/nextauthjs/next-auth) from 5.0.0-beta.29 to 5.0.0-beta.30.
- [Release notes](https://github.com/nextauthjs/next-auth/releases)
- [Commits](https://github.com/nextauthjs/next-auth/compare/next-auth@5.0.0-beta.29...next-auth@5.0.0-beta.30)

---
updated-dependencies:
- dependency-name: next-auth
  dependency-version: 5.0.0-beta.30
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-30 12:02:12 +08:00
Shinji-Li 0fdf70944d 🔨 chore: support lobehub-marketPlace oidc in lobechat (#9922)
* chore: support market-oidc grant

* fix: update appEnv the env Market Base Url

* test: update test.ts & the oidc path

* feat: update the oidc-provider test
2025-10-30 09:59:04 +08:00
Arvin Xu 5db4f58d85 🔨 chore: pre-merge message group code (#9915)
* pre merge

* refactor

* clean

* fix types

* fix tests

* 🧪 test(plugin): update triggerToolCalls test expectations

- Update content expectation from LOADING_FLAT to empty string
- Add threadId and groupId fields to test assertions
- Use toHaveBeenNthCalledWith for clearer assertions
- Remove unused LOADING_FLAT import

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-29 09:44:58 +08:00
lobehubbot d4a5d4d443 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-28 16:25:03 +00:00
semantic-release-bot 267d4d80b1 🔖 chore(release): v1.142.7 [skip ci]
### [Version&nbsp;1.142.7](https://github.com/lobehub/lobe-chat/compare/v1.142.6...v1.142.7)
<sup>Released on **2025-10-28**</sup>

#### ♻ Code Refactoring

- **misc**: Change files page from RSC to SPA mode to improve performance.

#### 💄 Styles

- **aihubmix**: Update extendParams to include urlContext.
- **misc**: Update i18n.

<br/>

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

#### Code refactoring

* **misc**: Change files page from RSC to SPA mode to improve performance, closes [#9846](https://github.com/lobehub/lobe-chat/issues/9846) ([f46cc50](https://github.com/lobehub/lobe-chat/commit/f46cc50))

#### Styles

* **aihubmix**: Update extendParams to include urlContext, closes [#9914](https://github.com/lobehub/lobe-chat/issues/9914) ([5a8fd85](https://github.com/lobehub/lobe-chat/commit/5a8fd85))
* **misc**: Update i18n, closes [#9907](https://github.com/lobehub/lobe-chat/issues/9907) ([d149c4d](https://github.com/lobehub/lobe-chat/commit/d149c4d))

</details>

<div align="right">

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

</div>
2025-10-28 16:23:47 +00:00
Coooolfan 5a8fd853d7 💄 style(aihubmix): update extendParams to include urlContext (#9914)
 feat(aihubmix): update extendParams to include urlContext
2025-10-29 00:09:08 +08:00
LobeHub Bot d149c4dbdf 🤖 style: update i18n (#9907)
💄 style: update i18n

Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-10-29 00:07:48 +08:00
Shinji-Li f46cc50e15 ♻️ refactor: change files page from RSC to SPA mode to improve performance (#9846)
* feat: change files page to spa

* feat: add height 100%

* fix: delete useless code

* feat: update files preview from severs to client

* feat: change download action to detail right place

* feat: change the dir name and the path

* feat: change the useNavigate to the router outer

* style: use flex 1 to get all width

* feat: update files?filesId to get files loading

* feat: change all files and repo url to knowledge url

* feat: close the knowledge/base settings page & update createNew onSuccess into Modal

* feat: update the knowledge base open judge
2025-10-29 00:02:51 +08:00
lobehubbot 67cb0711d6 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-28 08:02:32 +00:00
semantic-release-bot 747c59c36a 🔖 chore(release): v1.142.6 [skip ci]
### [Version&nbsp;1.142.6](https://github.com/lobehub/lobe-chat/compare/v1.142.5...v1.142.6)
<sup>Released on **2025-10-28**</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-10-28 08:01:28 +00:00
Neko 6c5caf9184 👷 build(database): improve user memory schema (#9822)
chore(database): improve user memory schema
2025-10-28 15:50:15 +08:00
lobehubbot 4684ee7f4c 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-27 10:05:01 +00:00
semantic-release-bot 09cbde3a3b 🔖 chore(release): v1.142.5 [skip ci]
### [Version&nbsp;1.142.5](https://github.com/lobehub/lobe-chat/compare/v1.142.4...v1.142.5)
<sup>Released on **2025-10-27**</sup>

#### 💄 Styles

- **misc**: Add MiniMax-M2 model.

<br/>

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

#### Styles

* **misc**: Add MiniMax-M2 model, closes [#9897](https://github.com/lobehub/lobe-chat/issues/9897) ([d6fded2](https://github.com/lobehub/lobe-chat/commit/d6fded2))

</details>

<div align="right">

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

</div>
2025-10-27 10:03:47 +00:00
sxjeru d6fded2973 💄 style: Add MiniMax-M2 model (#9897)
*  feat: 更新多个模型的描述、定价和能力,添加新模型支持

*  fix: 处理 <think> 标签的内容分割,优化思考状态管理

*  test: 添加测试以处理包含 </think> 标签的内容分割

* fix test
2025-10-27 17:39:20 +08:00
lobehubbot ed00394c6e 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-27 09:36:51 +00:00
semantic-release-bot 8558231037 🔖 chore(release): v1.142.4 [skip ci]
### [Version&nbsp;1.142.4](https://github.com/lobehub/lobe-chat/compare/v1.142.3...v1.142.4)
<sup>Released on **2025-10-27**</sup>

#### 💄 Styles

- **misc**: Pre render ModelSwitchPanel, The error details of the connectivity check lead to a layout problem.

<br/>

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

#### Styles

* **misc**: Pre render ModelSwitchPanel, closes [#9499](https://github.com/lobehub/lobe-chat/issues/9499) ([840382b](https://github.com/lobehub/lobe-chat/commit/840382b))
* **misc**: The error details of the connectivity check lead to a layout problem, closes [#9872](https://github.com/lobehub/lobe-chat/issues/9872) ([ea42e60](https://github.com/lobehub/lobe-chat/commit/ea42e60))

</details>

<div align="right">

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

</div>
2025-10-27 09:35:14 +00:00
Neko ef843c2b81 🔨 chore(ci): better renovate config with group and weekly strategy (#9466)
chore(ci): better revonate config with group and weekly strategy
2025-10-27 17:23:28 +08:00
EldestBard ea42e60ddb 💄 style: The error details of the connectivity check lead to a layout problem (#9872)
* give ProviderChecker's Error component a maxWidth

* add warp property to Highlighter

---------

Co-authored-by: daryl saxton <admin@dylsxtn.com>
2025-10-27 17:20:42 +08:00
sxjeru 840382b5b5 💄 style: Pre render ModelSwitchPanel (#9499)
 feat: (预渲染模型切换浮窗)支持 ActionDropdown 组件的预渲染功能
2025-10-27 17:18:17 +08:00
René Wang bf53c116dd 👷 build: Add editorData column on document table (#9899)
feat: Add rawData column
2025-10-27 17:12:55 +08:00
renovate[bot] fbb96eb4f8 Update dependency @opentelemetry/exporter-metrics-otlp-http to ^0.207.0 (#9879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 14:27:16 +08:00
renovate[bot] 6fa438c522 Update dependency @opentelemetry/instrumentation to ^0.207.0 (#9881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 14:27:08 +08:00
Arvin Xu 7fa6c749db 🔨 chore: pre-merge message group code (#9896)
* update

* update
2025-10-27 13:42:50 +08:00
lobehubbot b03d6cd940 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-27 02:40:38 +00:00
semantic-release-bot 01e009e89a 🔖 chore(release): v1.142.3 [skip ci]
### [Version&nbsp;1.142.3](https://github.com/lobehub/lobe-chat/compare/v1.142.2...v1.142.3)
<sup>Released on **2025-10-27**</sup>

#### 💄 Styles

- **misc**: Adjust modal setting form styles for improved layout and responsiveness, Unzip file when uploading in knowledge base [LOB-500].

<br/>

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

#### Styles

* **misc**: Adjust modal setting form styles for improved layout and responsiveness, closes [#9890](https://github.com/lobehub/lobe-chat/issues/9890) ([1997ec5](https://github.com/lobehub/lobe-chat/commit/1997ec5))
* **misc**: Unzip file when uploading in knowledge base [LOB-500], closes [#9854](https://github.com/lobehub/lobe-chat/issues/9854) ([e568ce6](https://github.com/lobehub/lobe-chat/commit/e568ce6))

</details>

<div align="right">

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

</div>
2025-10-27 02:39:04 +00:00
bbbugg 1997ec5fe2 💄 style: adjust modal setting form styles for improved layout and responsiveness (#9890)
fix: adjust modal styles for improved layout and responsiveness
2025-10-27 10:26:57 +08:00
René Wang e568ce6f31 💄 style: Unzip file when uploading in knowledge base [LOB-500] (#9854)
* feat: Unzip file

* feat: Limit max file upload limit

* fix: Remove unused test

* opti: Update translation

* style: Adjust padding

* feat: Update translation

* fix: Test error

* fix: Test erro

* fix: Test

* fix: test error

* fix: Test

* feat: Rremove message
2025-10-27 10:25:50 +08:00
renovate[bot] 1fe6a5997b Update dependency @opentelemetry/instrumentation-http to ^0.207.0 (#9773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 21:27:29 +08:00
renovate[bot] e75ca3026b Update dependency @opentelemetry/winston-transport to ^0.18.0 (#9884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 20:01:05 +08:00
renovate[bot] a8534da718 Update dependency @opentelemetry/sdk-node to ^0.207.0 (#9883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 20:00:57 +08:00
renovate[bot] 785b0e04f1 Update dependency @opentelemetry/instrumentation-pg to ^0.60.0 (#9882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 20:00:49 +08:00
renovate[bot] 3e665a4f5c Update dependency @opentelemetry/exporter-trace-otlp-http to ^0.207.0 (#9880)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 20:00:33 +08:00
renovate[bot] 2dd134e0b8 Update dependency @opentelemetry/auto-instrumentations-node to ^0.66.0 (#9878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 20:00:19 +08:00
lobehubbot 12577ec03c 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-26 06:14:05 +00:00
semantic-release-bot 9d87676d6a 🔖 chore(release): v1.142.2 [skip ci]
### [Version&nbsp;1.142.2](https://github.com/lobehub/lobe-chat/compare/v1.142.1...v1.142.2)
<sup>Released on **2025-10-26**</sup>

#### 💄 Styles

- **misc**: Improve provider modal height when creating custom provider.

<br/>

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

#### Styles

* **misc**: Improve provider modal height when creating custom provider, closes [#9870](https://github.com/lobehub/lobe-chat/issues/9870) ([55d92c0](https://github.com/lobehub/lobe-chat/commit/55d92c0))

</details>

<div align="right">

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

</div>
2025-10-26 06:12:56 +00:00
EldestBard 55d92c0241 💄 style: improve provider modal height when creating custom provider (#9870)
Remove fixed height for create new provider modal

Co-authored-by: daryl saxton <admin@dylsxtn.com>
2025-10-26 14:01:18 +08:00
Arvin Xu dec137a21e 🔨 chore: refactor ui message type (#9877)
* rename ChatMessage To UIChatMessage
refactor db message items

* rename ChatMessage To UIChatMessage
refactor db message items
2025-10-26 13:31:53 +08:00
lobehubbot 863521105e 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-26 03:45:32 +00:00
semantic-release-bot 9faf4c6b52 🔖 chore(release): v1.142.1 [skip ci]
### [Version&nbsp;1.142.1](https://github.com/lobehub/lobe-chat/compare/v1.142.0...v1.142.1)
<sup>Released on **2025-10-26**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

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

</details>

<div align="right">

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

</div>
2025-10-26 03:44:08 +00:00
YuTengjing 932ceb218b 🔨 chore: sync some small fix changes form cloud (#9875)
* chore: sync some small fix changes form cloud

* 🐛 fix(image): fix recreateImage execution order and test assertions

- Check activeGenerationTopicId before accessing batch to prevent undefined errors
- Update test mock to include required generations array
- Use batch.generations.length in test assertions instead of hardcoded value
2025-10-26 11:31:37 +08:00
renovate[bot] 3c197360e1 Update dependency lucide-react to ^0.548.0 (#9886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 11:31:08 +08:00
LobeHub Bot 8d3bc91e46 🤖 style: update i18n (#9862) 2025-10-26 11:22:37 +08:00
lobehubbot 2558b31963 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-25 16:24:48 +00:00
Arvin Xu 2ac1ff324b ♻️ chore: refactor message type (#9876)
* refactor message position

* refactor @/message to @lobechat/types

* refactor @lobechat/types

* fix lint
2025-10-26 00:13:10 +08:00
bbbugg 26533b4938 📝 docs: update "ENABLED_COMFYUI" and remove "ENABLED_BFL" "ENABLED_VERCELAIGATEWAY" in docs (#9858)
* add ENABLED_COMFYUI, ENABLED_AWS_BEDROCK, ENABLED_OPENAI and remove ENABLED_BFL and ENABLED_VERCELAIGATEWAY

* 🐛 fix: update AiHubMix links and API key documentation

* revert change AiHubMix url
2025-10-24 16:15:56 +08:00
lobehubbot 15dd7ecea4 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-24 04:21:37 +00:00
semantic-release-bot e264c3ab9d 🔖 chore(release): v1.142.0 [skip ci]
## [Version&nbsp;1.142.0](https://github.com/lobehub/lobe-chat/compare/v1.141.10...v1.142.0)
<sup>Released on **2025-10-24**</sup>

####  Features

- **misc**: Use env to control clerk allow origin feature.

<br/>

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

#### What's improved

* **misc**: Use env to control clerk allow origin feature, closes [#9863](https://github.com/lobehub/lobe-chat/issues/9863) ([490fee0](https://github.com/lobehub/lobe-chat/commit/490fee0))

</details>

<div align="right">

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

</div>
2025-10-24 04:20:25 +00:00
Shinji-Li 490fee002e feat: use env to control clerk allow origin feature (#9863)
feat: use clerk allow origin feature
2025-10-24 12:08:22 +08:00
lobehubbot 55c1149d95 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-23 10:07:48 +00:00
semantic-release-bot fbc8d20f94 🔖 chore(release): v1.141.10 [skip ci]
### [Version&nbsp;1.141.10](https://github.com/lobehub/lobe-chat/compare/v1.141.9...v1.141.10)
<sup>Released on **2025-10-23**</sup>

#### 🐛 Bug Fixes

- **misc**: Loadmore not work & navbar not show in pwa.

<br/>

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

#### What's fixed

* **misc**: Loadmore not work & navbar not show in pwa, closes [#9855](https://github.com/lobehub/lobe-chat/issues/9855) ([411f875](https://github.com/lobehub/lobe-chat/commit/411f875))

</details>

<div align="right">

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

</div>
2025-10-23 10:06:40 +00:00
Shinji-Li 411f875584 🐛 fix: loadmore not work & navbar not show in pwa (#9855)
fix: fix pwa loadmore not work & navbar not show in pwa
2025-10-23 17:53:31 +08:00
lobehubbot 8d34065833 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-23 09:27:04 +00:00
semantic-release-bot 11fb073e2a 🔖 chore(release): v1.141.9 [skip ci]
### [Version&nbsp;1.141.9](https://github.com/lobehub/lobe-chat/compare/v1.141.8...v1.141.9)
<sup>Released on **2025-10-23**</sup>

#### 💄 Styles

- **misc**: Improve local system tools render.

<br/>

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

#### Styles

* **misc**: Improve local system tools render, closes [#9853](https://github.com/lobehub/lobe-chat/issues/9853) ([295e8fc](https://github.com/lobehub/lobe-chat/commit/295e8fc))

</details>

<div align="right">

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

</div>
2025-10-23 09:25:55 +00:00
Arvin Xu 295e8fc3b2 💄 style: improve local system tools render (#9853)
* refactor the local system tools render

* fix build

* pre merge intervention
2025-10-23 17:14:35 +08:00
lobehubbot b2f8b4e191 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-23 08:31:18 +00:00
semantic-release-bot 263ba692cb 🔖 chore(release): v1.141.8 [skip ci]
### [Version&nbsp;1.141.8](https://github.com/lobehub/lobe-chat/compare/v1.141.7...v1.141.8)
<sup>Released on **2025-10-23**</sup>

#### 💄 Styles

- **misc**: Improvement for Agent Team After Alpha Launch [LOB-517].

<br/>

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

#### Styles

* **misc**: Improvement for Agent Team After Alpha Launch [LOB-517], closes [#9748](https://github.com/lobehub/lobe-chat/issues/9748) ([28245be](https://github.com/lobehub/lobe-chat/commit/28245be))

</details>

<div align="right">

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

</div>
2025-10-23 08:30:13 +00:00
René Wang 28245be7f3 💄 style: Improvement for Agent Team After Alpha Launch [LOB-517] (#9748)
* feat: Create group member shortcut

* feat: Hide sub topic mode in group chat

* fix: persist stop thinking tag

* feat: ALlow quick

* fix: No fallback header

* feat: Control in lab

* fix: tag style

* fix: Hide virtual in the member add modal

* fix: Ts error

* feat: Replace string

* feat: Update strings

* fix: Cannot turn off host

* feat: Delete member after remove group

* feat: Rename varibles and files

* fix: Update test snap
2025-10-23 16:18:18 +08:00
lobehubbot f723b38fa8 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-23 03:38:33 +00:00
semantic-release-bot 5382df8cf8 🔖 chore(release): v1.141.7 [skip ci]
### [Version&nbsp;1.141.7](https://github.com/lobehub/lobe-chat/compare/v1.141.6...v1.141.7)
<sup>Released on **2025-10-23**</sup>

#### 💄 Styles

- **misc**: Allow removal of `top_p` and similar request parameters.

<br/>

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

#### Styles

* **misc**: Allow removal of `top_p` and similar request parameters, closes [#9498](https://github.com/lobehub/lobe-chat/issues/9498) ([4c313ce](https://github.com/lobehub/lobe-chat/commit/4c313ce))

</details>

<div align="right">

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

</div>
2025-10-23 03:37:10 +00:00
sxjeru 4c313ced5e 💄 style: Allow removal of top_p and similar request parameters (#9498)
*  feat: 增强参数管理,支持删除 undefined 和 null 值的参数

*  feat: 将模型参数中的 null 值转换为 undefined,以防止向 API 发送 null 值

*  feat: 更新参数处理逻辑,使用 null 表示禁用标记,优化前端与后端的参数同步

*  feat: 更新 Novita 和 SiliconCloud 模型,添加新模型并优化定价信息

* 🔧 refactor: remove deprecated model and update ID format in AI models

* 🔧 feat: update pricing for novita models and add new Ling 1T model to siliconcloud

* 🔧 feat: remove deprecated ERNIE model and add DeepSeek V3.2 Exp models to siliconcloud

---------

Co-authored-by: Arvin Xu <arvinx@foxmail.com>
2025-10-23 11:25:49 +08:00
Arvin Xu b6f1fc4a14 test: add BDD test framework and initial tests with Playwright and Cucumber (#9843)
* try with bdd test

* update

* update

* add workspace

* update

* fix

* ci

* ci

* fix

* update

* update

* update parallel

* update config

* ️ perf: increase e2e timeout to 120 seconds

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>

* update config

* more parallel

* fix parallel

* fix tests

* refactor to improve performance

* fix

* fix

* fix

* refactor with tsx

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
2025-10-23 02:15:24 +08:00
lobehubbot 0dc112436b 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-22 16:53:21 +00:00
semantic-release-bot 24c42f6025 🔖 chore(release): v1.141.6 [skip ci]
### [Version&nbsp;1.141.6](https://github.com/lobehub/lobe-chat/compare/v1.141.5...v1.141.6)
<sup>Released on **2025-10-22**</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-10-22 16:52:16 +00:00
Arvin Xu 48ee38cf9b 👷 build: fix build (#9847)
* fix build

* use pnpm install to keep lint stricter
2025-10-23 00:40:57 +08:00
lobehubbot 654064ff7d 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-22 15:48:53 +00:00
semantic-release-bot b5219c220a 🔖 chore(release): v1.141.5 [skip ci]
### [Version&nbsp;1.141.5](https://github.com/lobehub/lobe-chat/compare/v1.141.4...v1.141.5)
<sup>Released on **2025-10-22**</sup>

#### ♻ Code Refactoring

- **misc**: Change discover page from RSC to SPA to improve performance.

<br/>

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

#### Code refactoring

* **misc**: Change discover page from RSC to SPA to improve performance, closes [#9828](https://github.com/lobehub/lobe-chat/issues/9828) ([b59ee0a](https://github.com/lobehub/lobe-chat/commit/b59ee0a))

</details>

<div align="right">

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

</div>
2025-10-22 15:47:41 +00:00
Shinji-Li b59ee0aabe ♻️ refactor: change discover page from RSC to SPA to improve performance (#9828)
* feat: change discord page to spa

* fix: change locals

* feat: update router change

* fix: revert some files

* feat: add model provider detail page use link

* fix: add trpc back

* feat: update e2e timeout time

* feat: change discord page to spa

* fix: change locals

* feat: update router change

* fix: revert some files

* feat: add model provider detail page use link

* fix: add trpc back

* feat: update e2e timeout time

* fix: use reactrouter-dom link replace next link
2025-10-22 23:35:49 +08:00
Arvin Xu d481315a66 👷 chore: pin bun@1.2.23 to fix vercel build (#9839)
pin bun
2025-10-22 19:13:50 +08:00
lobehubbot 4f5d1ff6af 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-22 05:05:26 +00:00
semantic-release-bot 1bd46e0346 🔖 chore(release): v1.141.4 [skip ci]
### [Version&nbsp;1.141.4](https://github.com/lobehub/lobe-chat/compare/v1.141.3...v1.141.4)
<sup>Released on **2025-10-22**</sup>

#### ♻ Code Refactoring

- **misc**: Fix model runtime cost calculate with CNY.

<br/>

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

#### Code refactoring

* **misc**: Fix model runtime cost calculate with CNY, closes [#9834](https://github.com/lobehub/lobe-chat/issues/9834) ([2e911ea](https://github.com/lobehub/lobe-chat/commit/2e911ea))

</details>

<div align="right">

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

</div>
2025-10-22 05:04:18 +00:00
René Wang a8be073b65 💄 style: support files auto chunking (#9823)
* feat: Auto chunking

* feat: Auto chunking

* opti: Lazy loading for preview content
2025-10-22 12:52:11 +08:00
Arvin Xu 2e911ea9f5 ♻️ refactor: fix model runtime cost calculate with CNY (#9834)
* fix model runtime cost calculate

* add tests
2025-10-22 12:49:51 +08:00
lobehubbot a89f4c73b4 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-22 03:17:53 +00:00
semantic-release-bot ebf5a8f649 🔖 chore(release): v1.141.3 [skip ci]
### [Version&nbsp;1.141.3](https://github.com/lobehub/lobe-chat/compare/v1.141.2...v1.141.3)
<sup>Released on **2025-10-22**</sup>

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Styles

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

</details>

<div align="right">

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

</div>
2025-10-22 03:16:46 +00:00
LobeHub Bot 80b0999467 🤖 style: update i18n (#9832)
💄 style: update i18n

Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-10-22 11:05:01 +08:00
bbbugg be952f95fd 📝 docs: update GitHub stars link and tutorial URLs in README files (#9831) 2025-10-22 11:04:38 +08:00
lobehubbot d46bb9ad8b 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-21 15:21:23 +00:00
semantic-release-bot 8e7f541a0b 🔖 chore(release): v1.141.2 [skip ci]
### [Version&nbsp;1.141.2](https://github.com/lobehub/lobe-chat/compare/v1.141.1...v1.141.2)
<sup>Released on **2025-10-21**</sup>

#### 💄 Styles

- **settings**: Broadcast locale changes and update switchLocale action.

<br/>

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

#### Styles

* **settings**: Broadcast locale changes and update switchLocale action, closes [#9620](https://github.com/lobehub/lobe-chat/issues/9620) ([0eb02ca](https://github.com/lobehub/lobe-chat/commit/0eb02ca))

</details>

<div align="right">

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

</div>
2025-10-21 15:20:15 +00:00
𝑾𝒖𝒙𝒉 0eb02ca1c5 💄 style(settings): broadcast locale changes and update switchLocale action (#9620)
chore(locale): broadcast locale changes and update switchLocale action
2025-10-21 23:08:38 +08:00
Arvin Xu b90436421b build: pin posthog@1.278.0 to fix build (#9829) 2025-10-21 23:07:14 +08:00
lobehubbot e284330678 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-21 12:45:04 +00:00
semantic-release-bot ea9da27e72 🔖 chore(release): v1.141.1 [skip ci]
### [Version&nbsp;1.141.1](https://github.com/lobehub/lobe-chat/compare/v1.141.0...v1.141.1)
<sup>Released on **2025-10-21**</sup>

#### ♻ Code Refactoring

- **misc**: Refactor context engine.

<br/>

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

#### Code refactoring

* **misc**: Refactor context engine, closes [#9821](https://github.com/lobehub/lobe-chat/issues/9821) ([e99f12f](https://github.com/lobehub/lobe-chat/commit/e99f12f))

</details>

<div align="right">

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

</div>
2025-10-21 12:43:58 +00:00
Arvin Xu e99f12f840 ♻️ refactor: refactor context engine (#9821)
* refactor context engine

* fix LocalSystem issue

* refactor mcp in local
2025-10-21 20:32:42 +08:00
lobehubbot bbc037912c 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-21 09:01:24 +00:00
semantic-release-bot 9881b7b303 🔖 chore(release): v1.141.0 [skip ci]
## [Version&nbsp;1.141.0](https://github.com/lobehub/lobe-chat/compare/v1.140.0...v1.141.0)
<sup>Released on **2025-10-21**</sup>

####  Features

- **misc**: Add PDF export functionality to share modal.

#### 🐛 Bug Fixes

- **misc**: Ignore abort signal errors in TRPC client, slove when pwa user info have code cannot be viewed in full.

#### 💄 Styles

- **misc**: Add knowledge base mansory layout [LOB-496], improve rich text link display.

<br/>

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

#### What's improved

* **misc**: Add PDF export functionality to share modal, closes [#9300](https://github.com/lobehub/lobe-chat/issues/9300) [#9299](https://github.com/lobehub/lobe-chat/issues/9299) ([2b7761c](https://github.com/lobehub/lobe-chat/commit/2b7761c))

#### What's fixed

* **misc**: Ignore abort signal errors in TRPC client, closes [#9809](https://github.com/lobehub/lobe-chat/issues/9809) [#9401](https://github.com/lobehub/lobe-chat/issues/9401) ([7f7dcfb](https://github.com/lobehub/lobe-chat/commit/7f7dcfb))
* **misc**: Slove when pwa user info have code cannot be viewed in full, closes [#9817](https://github.com/lobehub/lobe-chat/issues/9817) ([6734a47](https://github.com/lobehub/lobe-chat/commit/6734a47))

#### Styles

* **misc**: Add knowledge base mansory layout [LOB-496], closes [#9722](https://github.com/lobehub/lobe-chat/issues/9722) ([69f21da](https://github.com/lobehub/lobe-chat/commit/69f21da))
* **misc**: Improve rich text link display, closes [#9816](https://github.com/lobehub/lobe-chat/issues/9816) ([af33543](https://github.com/lobehub/lobe-chat/commit/af33543))

</details>

<div align="right">

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

</div>
2025-10-21 08:59:56 +00:00
Arvin Xu e9de9e3b52 test: fix tests (#9818)
fix tests
2025-10-21 16:48:24 +08:00
René Wang 69f21da3e1 💄 style: add knowledge base mansory layout [LOB-496] (#9722)
* feat: Add knowlwdge base entry

* feat: Bump dayjs

* style: Mansory

* feat: Persist state in URL

* lint: Remove unesd file

* feat: Skelton

* fix: Persist view preference

* fix: Chunk label

* fix: Lint error

* fix: Activate style

* fix: Image size
2025-10-21 16:42:58 +08:00
Shinji-Li 2b7761c36e feat: add PDF export functionality to share modal (#9300)
* feat: add PDF export functionality to share modal

- Create usePdfExport hook with jsPDF and html2canvas
- Add "Export as PDF" button to screenshot tab in share modal
- Support multi-page PDFs for long conversations
- Add required dependencies: jspdf@^2.5.2 and html2canvas@^1.4.1
- Add localization support for PDF export button

Fixes #9299

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: LobeHub Bot <lobehubbot@users.noreply.github.com>

* ♻️ refactor: convert PDF export to separate tab with backend generation

- Create new SharePdf tab component with PDF.js preview
- Move PDF generation from frontend to backend via tRPC
- Add server-side PDF generation using jsPDF
- Remove old PDF export button from ShareImage component
- Add proper loading states and error handling
- Update localization for PDF tab

Co-authored-by: Shinji-Li <ONLY-yours@users.noreply.github.com>

* 🐛 fix: resolve unicorn/no-await-expression-member lint error in PDF exporter

Split await expression member access to avoid linting error in exporter.ts

Co-authored-by: Shinji-Li <ONLY-yours@users.noreply.github.com>

* feat: add i18n

* feat: use pdfkit to export a pdf

* feat: add fullscreen preview

* feat: update pdf preview styles

* feat: add i18n locales

* feat: add single pdf share modal

* feat: update css & client mode cant use pdf genertate

* fix: mobile style fixed

* fix: delete console.log & useless packagejson

* feat: use online otf link

---------

Co-authored-by: Shinji-Li <ONLY-yours@users.noreply.github.com>
2025-10-21 16:32:17 +08:00
Shinji-Li 6734a47759 🐛 fix: slove when pwa user info have code cannot be viewed in full (#9817)
fix: slove when pwa user info have code cCannot be viewed in full
2025-10-21 16:22:46 +08:00
Arvin Xu 7f7dcfbff9 🩹 fix: ignore abort signal errors in TRPC client (#9809)
- Add abort error detection in lambda client error handling link
- Prevent showing notifications for aborted requests (e.g., rapid settings updates)
- Check for various abort error patterns: 'aborted', 'AbortError', 'signal is aborted without reason'

Fixes #9401

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
2025-10-21 16:17:18 +08:00
Arvin Xu af33543cba 💄 style: improve rich text link display (#9816)
* fix model runtime issue

* fix model runtime issue
2025-10-21 16:17:08 +08:00
lobehubbot 8b619f0a8e 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-21 07:47:09 +00:00
semantic-release-bot b81d8f79a4 🔖 chore(release): v1.140.0 [skip ci]
## [Version&nbsp;1.140.0](https://github.com/lobehub/lobe-chat/compare/v1.139.5...v1.140.0)
<sup>Released on **2025-10-21**</sup>

####  Features

- **misc**: Add ComfyUI integration Phase1(RFC-128).

<br/>

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

#### What's improved

* **misc**: Add ComfyUI integration Phase1(RFC-128), closes [#9043](https://github.com/lobehub/lobe-chat/issues/9043) ([15ffe28](https://github.com/lobehub/lobe-chat/commit/15ffe28))

</details>

<div align="right">

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

</div>
2025-10-21 07:46:07 +00:00
Maple Gao 15ffe289f5 feat: add ComfyUI integration Phase1(RFC-128) (#9043)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2025-10-21 15:34:57 +08:00
lobehubbot 2606f93146 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-21 07:28:40 +00:00
semantic-release-bot c882e75580 🔖 chore(release): v1.139.5 [skip ci]
### [Version&nbsp;1.139.5](https://github.com/lobehub/lobe-chat/compare/v1.139.4...v1.139.5)
<sup>Released on **2025-10-21**</sup>

#### 🐛 Bug Fixes

- **desktop**: Fix desktop open error in some edge cases.

<br/>

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

#### What's fixed

* **desktop**: Fix desktop open error in some edge cases, closes [#9813](https://github.com/lobehub/lobe-chat/issues/9813) ([6334f62](https://github.com/lobehub/lobe-chat/commit/6334f62))

</details>

<div align="right">

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

</div>
2025-10-21 07:27:27 +00:00
Arvin Xu 6334f62aa1 🐛 fix(desktop): fix desktop open error in some edge cases (#9813)
fix lock file bug
2025-10-21 15:16:10 +08:00
Shinji-Li 0af13ca057 fix: sub topic fetch branching topic id was used dynmic get (#9811)
* feat: when branching topic id was dynmic fetch

* fix: add topic id into callback dep
2025-10-21 14:16:40 +08:00
lobehubbot 68d6457659 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-21 05:06:36 +00:00
semantic-release-bot 3466055968 🔖 chore(release): v1.139.4 [skip ci]
### [Version&nbsp;1.139.4](https://github.com/lobehub/lobe-chat/compare/v1.139.3...v1.139.4)
<sup>Released on **2025-10-21**</sup>

#### 🐛 Bug Fixes

- **misc**: Pass threadId to messages in sendMessageInServer.

<br/>

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

#### What's fixed

* **misc**: Pass threadId to messages in sendMessageInServer, closes [#9808](https://github.com/lobehub/lobe-chat/issues/9808) ([d99a3a8](https://github.com/lobehub/lobe-chat/commit/d99a3a8))

</details>

<div align="right">

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

</div>
2025-10-21 05:05:20 +00:00
Arvin Xu d99a3a80f8 🐛 fix: pass threadId to messages in sendMessageInServer (#9808)
* fix dev hydration

* 🐛 fix: pass threadId to messages in sendMessageInServer

- Add threadId parameter to CreateMessageParams interface
- Pass threadId when creating user and assistant messages in aiChat router
- Add comprehensive tests for threadId handling and outputJSON method

This ensures thread context is properly maintained across message creation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

*  test: add comprehensive tests for addUserMessage

- Test early return when activeId is undefined
- Test message creation with files
- Test threadId propagation when activeThreadId is set
- Test input message clearing after message creation
- Test handling messages without fileList

This ensures the addUserMessage action correctly handles all scenarios including thread context.

🤖 Generated with [Claude Code](https://claude.com/claude-code)



* fix thread fix

* move

* baseline

*  test: fix and improve message integration tests

- Mock FileService to avoid S3 initialization issues
- Mock getServerDB to use test database instance
- Add test for threadId parameter in message creation
- Fix pagination test to handle variable message counts
- Fix batchCreate test to skip rowCount assertion (undefined in PGlite)
- Skip topicId validation test (not currently enforced)

All 15 integration tests now passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


* refactor

* improve
2025-10-21 12:54:52 +08:00
renovate[bot] cc37acb30b Update actions/download-artifact action to v5 (#8740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 12:14:18 +08:00
Arvin Xu eb708e04fe 📝 docs: fix outdated server-side database documentation (#9806)
- Update environment file setup instructions to use docker-compose/local/.env.example instead of .env.example.development
- Fix references to environment file locations in both English and Chinese documentation
- Align documentation with actual Docker Compose configuration that uses env_file: .env in docker-compose/local/ directory

Fixes #9525

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
2025-10-21 11:24:12 +08:00
lobehubbot 11df190f36 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-21 02:42:55 +00:00
semantic-release-bot a094d5c04c 🔖 chore(release): v1.139.3 [skip ci]
### [Version&nbsp;1.139.3](https://github.com/lobehub/lobe-chat/compare/v1.139.2...v1.139.3)
<sup>Released on **2025-10-21**</sup>

#### 💄 Styles

- **misc**: Show message author in minimap.

<br/>

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

#### Styles

* **misc**: Show message author in minimap, closes [#9797](https://github.com/lobehub/lobe-chat/issues/9797) ([f6daefb](https://github.com/lobehub/lobe-chat/commit/f6daefb))

</details>

<div align="right">

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

</div>
2025-10-21 02:41:24 +00:00
René Wang f6daefb0a2 💄 style: Show message author in minimap (#9797)
* feat: Show message author

* fix: Use debug instead

* Update index.tsx
2025-10-21 10:30:34 +08:00
renovate[bot] b8c59be3a7 Update dependency openapi-fetch to ^0.14.0 (#5596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 10:01:09 +08:00
renovate[bot] 284faf799b Update dependency @electron-toolkit/tsconfig to v2 (#9556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 09:50:17 +08:00
lobehubbot 88d194c3b6 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-20 16:40:00 +00:00
semantic-release-bot 6c53a5f026 🔖 chore(release): v1.139.2 [skip ci]
### [Version&nbsp;1.139.2](https://github.com/lobehub/lobe-chat/compare/v1.139.1...v1.139.2)
<sup>Released on **2025-10-20**</sup>

#### 💄 Styles

- **misc**: Solve when desktop the sider agent list too long.

<br/>

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

#### Styles

* **misc**: Solve when desktop the sider agent list too long, closes [#9792](https://github.com/lobehub/lobe-chat/issues/9792) ([778dea3](https://github.com/lobehub/lobe-chat/commit/778dea3))

</details>

<div align="right">

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

</div>
2025-10-20 16:38:30 +00:00
renovate[bot] d430836f20 Update dependency @anthropic-ai/sdk to ^0.67.0 (#9771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 00:27:21 +08:00
jjangga0214 7571b66284 👷 build(deps): missing deps for model-runtime (#9782) 2025-10-21 00:26:23 +08:00
Shinji-Li 3ec394d77a fix: slove when desktop 401 not pop up many message (#9789)
* fix: slove when desktop 401 not theathore many message

* fix: fixed desktop store login judge
2025-10-21 00:23:00 +08:00
Shinji-Li 778dea3853 💄 style: solve when desktop the sider agent list too long (#9792)
fix: solve when desktop the sider agent list too long
2025-10-21 00:22:30 +08:00
Rylan Cai f599617042 🔨 chore: remove logs in message action (#9800)
🔥 chore: remove logs in message action
2025-10-20 23:58:55 +08:00
YuTengjing cc8f5d0639 🔨 chore: improve bug report template with validations and client type field (#9795)
* 🔧 feat: improve bug report template with client type field

- Rename Platform to Deployment Platform and make it optional for Desktop App users
- Fix Deployment Mode typo and improve formatting
- Add Client Type field to distinguish Web/Desktop/Mobile access methods
- Improve overall template formatting with better spacing

* 🔧 feat: add validations and PR willingness to bug report template

- Add "Willing to Submit a PR?" dropdown field to encourage contributions
- Add validation checkboxes to ensure quality issue reports
- Include checks for docs reading, duplicate issues, and concrete bugs

* ♻️ refactor: reorder bug report fields to prioritize client type

- Move Client Type to the first field for better categorization
- Move Operating System to second field
- Prioritize platform-related fields before deployment details
2025-10-20 23:54:58 +08:00
jjangga0214 32d365b544 🔨 chore(i18n): rm {{systemRole}} from ko-KR (#9793)
🐛 fix(i18n): rm {{systemRole}} from ko-KR
2025-10-20 16:10:23 +08:00
renovate[bot] 0ff41dba9c Update dependency @opentelemetry/instrumentation to ^0.206.0 (#9772)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 15:04:15 +08:00
renovate[bot] fd68cc364c Update dependency @opentelemetry/sdk-node to ^0.206.0 (#9774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 15:04:05 +08:00
lobehubbot ce7a74242f 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-20 02:56:09 +00:00
semantic-release-bot 7f9bfda652 🔖 chore(release): v1.139.1 [skip ci]
### [Version&nbsp;1.139.1](https://github.com/lobehub/lobe-chat/compare/v1.139.0...v1.139.1)
<sup>Released on **2025-10-20**</sup>

#### ♻ Code Refactoring

- **i18n**: Rm qa.

#### 💄 Styles

- **misc**: Update i18n.

<br/>

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

#### Code refactoring

* **i18n**: Rm qa, closes [#9783](https://github.com/lobehub/lobe-chat/issues/9783) ([6d14dfe](https://github.com/lobehub/lobe-chat/commit/6d14dfe))

#### Styles

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

</details>

<div align="right">

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

</div>
2025-10-20 02:54:52 +00:00
jjangga0214 5f10aace5b 👷 build(deps): add transitive phantom dependencies (#9784) 2025-10-20 10:44:35 +08:00
jjangga0214 6d14dfe50d ♻️ refactor(i18n): rm qa (#9783) 2025-10-20 10:42:33 +08:00
sxjeru 12f54759df 🔨 chore: Enable webpackBuildWorker to optimize build memory (#9350)
Enable webpackBuildWorker in next.config.ts
2025-10-20 10:40:53 +08:00
LobeHub Bot b43d4b27ad 🤖 style: update i18n (#9787)
💄 style: update i18n

Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-10-20 10:32:04 +08:00
lobehubbot dab987f64b 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-19 12:06:08 +00:00
semantic-release-bot 2e24f72b2a 🔖 chore(release): v1.139.0 [skip ci]
## [Version&nbsp;1.139.0](https://github.com/lobehub/lobe-chat/compare/v1.138.5...v1.139.0)
<sup>Released on **2025-10-19**</sup>

####  Features

- **misc**: Support image generation for siliconcloud.

<br/>

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

#### What's improved

* **misc**: Support image generation for siliconcloud, closes [#9447](https://github.com/lobehub/lobe-chat/issues/9447) ([5ebcfa5](https://github.com/lobehub/lobe-chat/commit/5ebcfa5))

</details>

<div align="right">

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

</div>
2025-10-19 12:05:00 +00:00
WangYK 5ebcfa531e feat: support image generation for siliconcloud (#9447)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2025-10-19 19:54:11 +08:00
sxjeru 1b3f88346b 🔨 chore: Update HF provider support (#9751)
* feat: update Hugging Face integration with new model fetching logic and pricing support

* fix: format adjustments in convertOpenAIMessagesToHFFormat function and update max_tokens handling

* refactor: remove enabled property from huggingfaceChatModels and clean up related tests

* feat: 添加 Qwen3-VL-8B-Instruct 和 Qwen3-VL-8B-Thinking 模型,支持视觉理解和推理

* feat: 实现 OpenAI 消息格式转换为 Hugging Face 格式,并添加相关单元测试
2025-10-19 15:29:42 +08:00
renovate[bot] 808cb21b99 Update pnpm to v10.18.3 (#9770)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-19 15:26:09 +08:00
lobehubbot c6dd22eea6 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-18 09:21:51 +00:00
semantic-release-bot da5427ccdf 🔖 chore(release): v1.138.5 [skip ci]
### [Version&nbsp;1.138.5](https://github.com/lobehub/lobe-chat/compare/v1.138.4...v1.138.5)
<sup>Released on **2025-10-18**</sup>

#### ♻ Code Refactoring

- **misc**: Refactor upload router into lambda and decide to remove it in V2.

<br/>

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

#### Code refactoring

* **misc**: Refactor upload router into lambda and decide to remove it in V2, closes [#9766](https://github.com/lobehub/lobe-chat/issues/9766) ([d1c7f41](https://github.com/lobehub/lobe-chat/commit/d1c7f41))

</details>

<div align="right">

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

</div>
2025-10-18 09:20:47 +00:00
Arvin Xu d1c7f41bd4 ♻️ refactor: refactor upload router into lambda and decide to remove it in V2 (#9766)
refactor edge router and decide to remove it in V2
2025-10-18 17:09:55 +08:00
lobehubbot 6681989fe6 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-18 08:50:51 +00:00
semantic-release-bot 48a7a08110 🔖 chore(release): v1.138.4 [skip ci]
### [Version&nbsp;1.138.4](https://github.com/lobehub/lobe-chat/compare/v1.138.3...v1.138.4)
<sup>Released on **2025-10-18**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix response API tools calling issue.

<br/>

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

#### What's fixed

* **misc**: Fix response API tools calling issue, closes [#9760](https://github.com/lobehub/lobe-chat/issues/9760) ([0596692](https://github.com/lobehub/lobe-chat/commit/0596692))

</details>

<div align="right">

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

</div>
2025-10-18 08:49:45 +00:00
Arvin Xu 0596692e95 🐛 fix: fix response API tools calling issue (#9760)
* fix response tools calling

* add log

* fix

* fix tests

* fix all text and lint and types

* refactor google context builder

* add tests
2025-10-18 16:39:22 +08:00
bbbugg 59ea77d746 test: add search unit test cases for merging the user model with the base model (#9764)
test: add search unit test cases for merging the user model with the base model.
2025-10-18 15:28:35 +08:00
lobehubbot 4aa570c904 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-18 05:48:48 +00:00
semantic-release-bot c9ea5336d3 🔖 chore(release): v1.138.3 [skip ci]
### [Version&nbsp;1.138.3](https://github.com/lobehub/lobe-chat/compare/v1.138.2...v1.138.3)
<sup>Released on **2025-10-18**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix topic fetch not correct in custom agent.

<br/>

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

#### What's fixed

* **misc**: Fix topic fetch not correct in custom agent, closes [#9761](https://github.com/lobehub/lobe-chat/issues/9761) ([ceffce2](https://github.com/lobehub/lobe-chat/commit/ceffce2))

</details>

<div align="right">

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

</div>
2025-10-18 05:47:20 +00:00
Arvin Xu ceffce27c3 🐛 fix: fix topic fetch not correct in custom agent (#9761)
* fix topic issue

* fix tests
2025-10-18 13:34:07 +08:00
bbbugg b56c9c51b8 🐛fix: update search settings handling based on explicit model search abilities (#9757) 2025-10-18 13:01:03 +08:00
Arvin Xu 97a6c8e172 🔨 chore: refactor the prompt engineering (#9744)
refactor the prompt engineering
2025-10-17 22:19:26 +08:00
renovate[bot] 727c92ad5b Update dependency @opentelemetry/auto-instrumentations-node to ^0.65.0 (#9669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 17:27:40 +08:00
renovate[bot] 25e45a4cd4 Update dependency @opentelemetry/exporter-trace-otlp-http to ^0.206.0 (#9672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 17:27:33 +08:00
renovate[bot] 296bad9fa4 Update dependency @opentelemetry/instrumentation-pg to ^0.59.0 (#9459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 16:43:21 +08:00
renovate[bot] d8d28371d6 Update ghcr.io/grafana/xk6-client-tracing Docker tag to v0.0.9 (#9666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 16:14:08 +08:00
renovate[bot] a588319cf1 Update dependency @opentelemetry/exporter-metrics-otlp-http to ^0.206.0 (#9670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 16:13:35 +08:00
lobehubbot 527ab0add7 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-16 18:28:58 +00:00
semantic-release-bot f2230ae536 🔖 chore(release): v1.138.2 [skip ci]
### [Version&nbsp;1.138.2](https://github.com/lobehub/lobe-chat/compare/v1.138.1...v1.138.2)
<sup>Released on **2025-10-16**</sup>

#### 💄 Styles

- **misc**: Improve welcome message.

<br/>

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

#### Styles

* **misc**: Improve welcome message, closes [#9747](https://github.com/lobehub/lobe-chat/issues/9747) ([c83fe13](https://github.com/lobehub/lobe-chat/commit/c83fe13))

</details>

<div align="right">

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

</div>
2025-10-16 18:27:52 +00:00
Arvin Xu c83fe13d7c 💄 style: improve welcome message (#9747)
* add feishu

* update i18n

* refactor

* rename

* rename
2025-10-17 02:16:45 +08:00
lobehubbot f4b9d6795b 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-16 16:52:22 +00:00
semantic-release-bot 77bc2614a4 🔖 chore(release): v1.138.1 [skip ci]
### [Version&nbsp;1.138.1](https://github.com/lobehub/lobe-chat/compare/v1.138.0...v1.138.1)
<sup>Released on **2025-10-16**</sup>

#### 🐛 Bug Fixes

- **misc**: Automatic topic creation switch does not work.

<br/>

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

#### What's fixed

* **misc**: Automatic topic creation switch does not work, closes [#9693](https://github.com/lobehub/lobe-chat/issues/9693) ([a02b301](https://github.com/lobehub/lobe-chat/commit/a02b301))

</details>

<div align="right">

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

</div>
2025-10-16 16:51:11 +00:00
sxjeru a02b301904 🐛 fix: Automatic topic creation switch does not work (#9693)
*  feat: 添加自动创建主题功能,支持根据消息数量和配置阈值决定是否创建新主题

* typo

* fix test

* 删除 GLM-4.6 模型的定义

*  feat: 添加 Ring-1T 和 Ling-1T 模型定义,扩展聊天模型库

*  feat: 添加 Qwen3 VL 模型,扩展聊天模型库并更新 Vercel AI Gateway 模型定价

* fix test
2025-10-17 00:41:02 +08:00
Arvin Xu 846a7a5986 🔨 chore: support Feishu OAuth Provider (#9745)
* feat: 添加 Feishu 作为新的 OAuth 认证提供者,并更新相关配置

* docs: 添加 Feishu 应用的环境变量配置说明

* refactor: 移除 Feishu 相关的环境变量配置,对齐 auth.js 环境变量规范

* improve docs

* add feishu

---------

Co-authored-by: 赵远景 <zhaoyuanjing@shouqianba.com>
2025-10-17 00:17:19 +08:00
lobehubbot bc7aa88a8f 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-16 14:28:33 +00:00
semantic-release-bot 85c4553821 🔖 chore(release): v1.138.0 [skip ci]
## [Version&nbsp;1.138.0](https://github.com/lobehub/lobe-chat/compare/v1.137.10...v1.138.0)
<sup>Released on **2025-10-16**</sup>

####  Features

- **misc**: Support Group Chat, Mention, and Multi-Agent Orchestration with feature flag.

<br/>

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

#### What's improved

* **misc**: Support Group Chat, Mention, and Multi-Agent Orchestration with feature flag, closes [#8976](https://github.com/lobehub/lobe-chat/issues/8976) ([03c2838](https://github.com/lobehub/lobe-chat/commit/03c2838))

</details>

<div align="right">

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

</div>
2025-10-16 14:27:25 +00:00
René Wang 03c28385e5 feat: support Group Chat, Mention, and Multi-Agent Orchestration with feature flag (#8976)
* feat: implement group chat functionality

* fix

* update

* revert pglite mock

* fix: Add missing test

* fix mention

* fix mention

* lint: Clear unused varibles

* fix: type check

* fix: Coverage

* build: Add missing test

* fix: add mention back

* fix: Add missing test

* fix: Add test for topic

* feat: Group chat fallback style

* fix: Revert unncessary files

* fix: circular deps

* feat: tool usage

* fix: Replace debug info

* feat: Update i18n

* opti: Better prompr

* fix claude

* feat: Filter model without function calling

* fix: DM reduction

* lint: Address build error

* fix: Test error

* feat: Store model info

* style: Clean up welcome messaeg

* feat: Use new welcome message

* fix: inbox not working

* fix: inbox not working

* fix: type error

* feat: Optimize prompt

* fix: Revert unintentional changes

* lint: Remove unused code

* fix: better test

* fix: Use debug

* refact: Move normalization postion

* opti: Better prompt

* opti: Better prompt

* opti: Better prompt

* lint: Clear console.log

* fix: Update test snap

* fix: test error

* fix: Unexpectly test fail

---------

Co-authored-by: arvinxx <arvinx@foxmail.com>
2025-10-16 22:16:40 +08:00
lobehubbot e525cb2ed6 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-16 14:02:46 +00:00
semantic-release-bot ea44ef1b4c 🔖 chore(release): v1.137.10 [skip ci]
### [Version&nbsp;1.137.10](https://github.com/lobehub/lobe-chat/compare/v1.137.9...v1.137.10)
<sup>Released on **2025-10-16**</sup>

#### 💄 Styles

- **misc**: Add Claude Haiku 4.5 model.

<br/>

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

#### Styles

* **misc**: Add Claude Haiku 4.5 model, closes [#9735](https://github.com/lobehub/lobe-chat/issues/9735) ([1cfbc87](https://github.com/lobehub/lobe-chat/commit/1cfbc87))

</details>

<div align="right">

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

</div>
2025-10-16 14:01:35 +00:00
sxjeru 1cfbc878bd 💄 style: Add Claude Haiku 4.5 model (#9735)
* feat: 添加 Claude Haiku 4.5 模型及其相关配置

* fix: 修复模型 ID 格式并移除 Llama 4 Maverick 模型

* 添加 doubao-seed-1.6-lite;更新模型价格区间格式,调整价格单位为百万 tokens

* fix: Invalid combination of reasoning_effort and thinking type: low + disabled

* feat: 添加 Qwen3 VL Flash 和 Qwen3 Coder 30B A3B 模型,更新定价和发布信息
2025-10-16 21:50:32 +08:00
Rdmclin2 61bbd596f0 🔨 chore: mobile related server implementation pick from mobile app (#9691)
* server: sync from feat/mobile-app (exclude apps/mobile)

* Update package.json

* chore(mobile): update mobile router imports to use lambda

* chore(mobile): refactor mobile router

* chore: format tsconfig.json

* chore(mobile): simplify mobile router

---------

Co-authored-by: Arvin Xu <arvinx@foxmail.com>
Co-authored-by: Tsuki <976499226@qq.com>
2025-10-16 11:32:58 +07:00
lobehubbot 6508e2fcaf 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-15 16:12:31 +00:00
semantic-release-bot 7cef9d88ea 🔖 chore(release): v1.137.9 [skip ci]
### [Version&nbsp;1.137.9](https://github.com/lobehub/lobe-chat/compare/v1.137.8...v1.137.9)
<sup>Released on **2025-10-15**</sup>

#### 💄 Styles

- **misc**: Improve update notification.

<br/>

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

#### Styles

* **misc**: Improve update notification, closes [#9717](https://github.com/lobehub/lobe-chat/issues/9717) ([16de38a](https://github.com/lobehub/lobe-chat/commit/16de38a))

</details>

<div align="right">

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

</div>
2025-10-15 16:11:22 +00:00
Arvin Xu 16de38ae32 💄 style: improve update notification (#9717)
* refactor tool source

* Revert "refactor tool source"

This reverts commit a867118a52.

* improve update notification

* improve locale
2025-10-16 00:00:35 +08:00
lobehubbot c6257f1dba 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-15 14:51:34 +00:00
semantic-release-bot b2c28d0810 🔖 chore(release): v1.137.8 [skip ci]
### [Version&nbsp;1.137.8](https://github.com/lobehub/lobe-chat/compare/v1.137.7...v1.137.8)
<sup>Released on **2025-10-15**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix duplicate tools id issue and fix link dialog issue.

#### 💄 Styles

- **misc**: Add region support for Vertex AI provider.

<br/>

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

#### What's fixed

* **misc**: Fix duplicate tools id issue and fix link dialog issue, closes [#9731](https://github.com/lobehub/lobe-chat/issues/9731) ([0a8c80d](https://github.com/lobehub/lobe-chat/commit/0a8c80d))

#### Styles

* **misc**: Add region support for Vertex AI provider, closes [#9720](https://github.com/lobehub/lobe-chat/issues/9720) ([d17b50c](https://github.com/lobehub/lobe-chat/commit/d17b50c))

</details>

<div align="right">

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

</div>
2025-10-15 14:50:23 +00:00
Arvin Xu d17b50c6dc 💄 style: add region support for Vertex AI provider (#9720)
 feat: add region support for Vertex AI provider

- Add VertexAIKeyVault interface with region support
- Update UI to include region selector with 35+ regions
- Add vertexAIRegion field to ClientSecretPayload
- Update backend to use user-selected region with fallback
- Add i18n support for English and Chinese
- Fix issue with Gemini 2.5 models requiring global region
2025-10-15 22:39:36 +08:00
Arvin Xu 0a8c80dfd2 🐛 fix: fix duplicate tools id issue and fix link dialog issue (#9731)
* add

* baseline

*  test(store): add tests for discover store plugin and mcp slices

- Add comprehensive tests for discover/slices/plugin/action.ts (15 tests)
- Add comprehensive tests for discover/slices/mcp/action.ts (11 tests)
- Update test-coverage.md with new metrics and completed work
- Coverage: 74.24% overall (+26 tests, 2 new test files)
- Action files coverage: 29/40 tested (72.5%, +2 files)

Features tested:
- Plugin/MCP categories, detail, identifiers, and list fetching
- SWR key generation with locale and parameters
- SWR configuration verification
- Service integration with discoverService

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* 📝 docs(testing): add SWR hooks testing guide and subagent workflow

Testing Guide Updates:
- Add comprehensive SWR hooks testing section with examples
- Document key differences from regular action tests
- Add examples for testing SWR key generation and configuration
- Add examples for testing conditional fetching
- Update references to include SWR hook test examples

Test Coverage Guide Updates:
- Add detailed subagent workflow for parallel testing
- Document when and how to use subagents for testing
- Add complete workflow example using subagents
- Add benefits and best practices for subagent usage
- Clarify that subagents should NOT commit or update docs
- Add step-by-step guide for launching parallel subagents

Key improvements:
- Better documentation for testing SWR-based store actions
- Clear workflow for efficient parallel testing using subagents
- Single atomic commit strategy after all subagents complete
- Improved testing efficiency and organization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ♻️ refactor(test): fix SWR mock strategy to properly test fetcher

Previously, tests were hardcoding return values instead of calling
the fetcher function. This bypassed the actual service call logic.

Changes:
- Fix useSWR mock to call fetcher and return its Promise
- Update assertions to await Promise results
- Update testing guide with correct mock pattern
- Add explanation of why this approach is correct

Before (incorrect):
```typescript
useSWRMock.mockImplementation(((key, fetcher) => {
  fetcher?.(); // Call but ignore result
  return { data: mockData }; // Hardcoded
}) as any);
expect(result.current.data).toEqual(mockData);
```

After (correct):
```typescript
useSWRMock.mockImplementation(((key, fetcher) => {
  const data = fetcher?.(); // Get Promise from fetcher
  return { data }; // Return Promise
}) as any);
const resolvedData = await result.current.data;
expect(resolvedData).toEqual(mockData);
```

Benefits:
-  Actually tests the fetcher function
-  Mirrors real SWR behavior (data is Promise)
-  Service calls are properly verified
-  Tests are more accurate and maintainable

Updated files:
- .cursor/rules/testing-guide/zustand-store-action-test.mdc
- src/store/discover/slices/plugin/action.test.ts
- src/store/discover/slices/mcp/action.test.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* 🐛 fix(test): correct SWR mock strategy to match project standards

- Remove useSWR mocking, use real SWR implementation instead
- Only mock service methods (fetchers) with vi.spyOn
- Use waitFor for async assertions
- Update testing guide with correct SWR pattern
- Add reference to src/store/chat/slices/message/action.test.ts

This fixes the incorrect mocking approach from previous commits.
All 13 tests pass with the corrected strategy.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

*  test(store): add comprehensive tests for high priority action files

- Add mcpStore action tests (41 tests, 624 LOC covered)
  - MCP plugin installation flow (normal, resume, dependencies, config)
  - Connection testing (HTTP and STDIO)
  - Plugin lifecycle management
  - Error handling and cancellation flows

- Add fileManager action tests (35 tests, 205 LOC covered)
  - File upload and processing workflows
  - Chunk embedding and parsing
  - File list management and refresh
  - SWR data fetching

Testing approach:
- Used parallel subagents for efficient development
- Followed zustand testing patterns from guide
- Proper test layering and per-test mocking
- All tests pass type-check and lint

Coverage improvement: 74.24% → ~76% (+76 tests, 2 files)
Action files: 29/40 → 31/40 tested (77.5%)

🏆 Milestone: All high priority files (>200 LOC) now have tests!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

*  test(store): complete 100% action file coverage with 160 new tests

Added comprehensive tests for all remaining 9 medium-priority action files:

Discovery Store (33 tests):
- assistant/action.ts: 10 tests (SWR hooks, categories, detail, identifiers, list)
- provider/action.ts: 11 tests (SWR hooks, detail with readme, identifiers, list with filters)
- model/action.ts: 12 tests (SWR hooks, categories, detail, identifiers, list with params)

Knowledge Base Store (29 tests):
- crud/action.ts: 19 tests (create, update, remove, refresh, loading states, SWR hooks)
- content/action.ts: 10 tests (add files, remove files, error handling)

File Store (36 tests):
- upload/action.ts: 18 tests (base64 upload, file upload with progress, type detection, KB integration)
- chunk/action.ts: 18 tests (drawer management, highlight, semantic search)

AI Infrastructure Store (23 tests):
- aiModel/action.ts: 23 tests (CRUD, batch operations, remote sync, toggle enabled, SWR hooks)

Chat Store (39 tests):
- thread/action.ts: 39 tests (CRUD, messaging, AI title generation, validation, loading states)

Testing approach:
- Used 9 parallel subagents for efficient development
- Followed zustand testing patterns from guide
- SWR hook testing for discovery slices
- Complex async flows with proper error handling
- File operations with progress callbacks
- Semantic search and RAG integration

Coverage improvement: ~76% → ~80% (+160 tests, 9 files)
Action files: 31/40 → 40/40 tested (100%)

🎉 MILESTONE: All 40 action files now have comprehensive test coverage!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix test

* fix test

* fix context-engine

* add tests

* remove

* remove tools bar

* pin bun version

* fix

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-15 22:38:53 +08:00
lobehubbot 6fd337de18 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-15 12:58:39 +00:00
semantic-release-bot 6266a85cf1 🔖 chore(release): v1.137.7 [skip ci]
### [Version&nbsp;1.137.7](https://github.com/lobehub/lobe-chat/compare/v1.137.6...v1.137.7)
<sup>Released on **2025-10-15**</sup>

#### 💄 Styles

- **misc**: Use different favicon.ico in dev mode.

<br/>

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

#### Styles

* **misc**: Use different favicon.ico in dev mode, closes [#9723](https://github.com/lobehub/lobe-chat/issues/9723) ([2f7317b](https://github.com/lobehub/lobe-chat/commit/2f7317b))

</details>

<div align="right">

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

</div>
2025-10-15 12:57:27 +00:00
René Wang 2f7317b98f 🔨 style: Use different favicon.ico in dev mode (#9723) 2025-10-15 14:47:37 +02:00
Arvin Xu 317df489ce test(store): refactor generateAIChatV2 tests following V1 patterns (#9714)
- Introduce shared test helpers and fixtures
- Use TEST_IDS and TEST_CONTENT constants instead of hardcoded strings
- Organize tests by functionality (validation, message creation, RAG integration, error handling)
- Remove commented-out test code
- Maintain V2-specific features (isServerMode, aiChatService mock)
- All 28 tests passing
2025-10-14 23:48:06 +08:00
Arvin Xu ff41f4bb82 🔨 chore: improve Claude Code triage workflow (#9713)
* 🔨 chore: unify Claude Code workflows to use claude-code-action@main

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* 🔧 chore: extract issue triage prompt to separate file

- Add .claude/prompts/issue-triage.md with comprehensive triage guide
- Update workflow to read from issue-triage.md instead of inline prompt
- Simplify workflow configuration for better maintainability
- Add provider detection rules including aihubmix

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-10-14 23:05:25 +08:00
Arvin Xu 60f43d90e5 🔨 chore: unify Claude Code workflows to use claude-code-action@main (#9712)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-10-14 21:49:52 +08:00
lobehubbot b8136ac17b 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-14 13:43:35 +00:00
semantic-release-bot f0e387fa29 🔖 chore(release): v1.137.6 [skip ci]
### [Version&nbsp;1.137.6](https://github.com/lobehub/lobe-chat/compare/v1.137.5...v1.137.6)
<sup>Released on **2025-10-14**</sup>

#### 🐛 Bug Fixes

- **misc**: Update Claude workflows to use oauth token, vertext ai create image.

<br/>

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

#### What's fixed

* **misc**: Update Claude workflows to use oauth token, closes [#9711](https://github.com/lobehub/lobe-chat/issues/9711) ([8dcb00e](https://github.com/lobehub/lobe-chat/commit/8dcb00e))
* **misc**: Vertext ai create image, closes [#9710](https://github.com/lobehub/lobe-chat/issues/9710) ([790d8fd](https://github.com/lobehub/lobe-chat/commit/790d8fd))

</details>

<div align="right">

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

</div>
2025-10-14 13:42:24 +00:00
Arvin Xu 8dcb00e2c9 🔧 fix: update Claude workflows to use oauth token (#9711) 2025-10-14 21:31:25 +08:00
YuTengjing 790d8fd498 🐛 fix: vertext ai create image (#9710) 2025-10-14 21:26:26 +08:00
Arvin Xu 3719bf4d52 🔨 chore: add claude code powered workflows (#9709)
* add claude dedupe issue workflow

* add triage

* add auto close duplicate

* improve triage

* improve
2025-10-14 21:24:26 +08:00
Arvin Xu bba23bf5a3 test: refactor aiChat tests (#9708)
* refactor tests

* ♻️ refactor(test): improve test layering and fix type errors

Improved test architecture by fixing test layer violations:

1. **internal_coreProcessMessage tests**: Changed to spy on direct dependency `internal_fetchAIChatMessage` instead of lower-level `chatService.createAssistantMessageStream`

2. **internal_fetchAIChatMessage tests**: Fixed to mock correct service (`chatService` instead of global `fetch`)

3. **beforeEach cleanup**: Removed global `spyOnChatService()` to reduce implicit coupling - tests now spy `chatService` only when needed

4. **Type fixes**:
   - Fixed mockResolvedValue() to include required undefined arguments
   - Fixed ChatMessageError type to include required `type` property
   - Fixed MessageDispatch type guard for safer property access

Benefits:
 Clear test layers - each test only spies on direct dependencies
 Correct mocks matching actual implementation
 Coverage improved from 81.48% to 82.03%
 All 52 tests passing with no type errors
2025-10-14 20:45:14 +08:00
lobehubbot 509619ba15 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-14 10:27:29 +00:00
semantic-release-bot 5de9e9fea3 🔖 chore(release): v1.137.5 [skip ci]
### [Version&nbsp;1.137.5](https://github.com/lobehub/lobe-chat/compare/v1.137.4...v1.137.5)
<sup>Released on **2025-10-14**</sup>

#### 💄 Styles

- **misc**: Add imagen model to vertex ai.

<br/>

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

#### Styles

* **misc**: Add imagen model to vertex ai, closes [#9699](https://github.com/lobehub/lobe-chat/issues/9699) ([3b2a2c1](https://github.com/lobehub/lobe-chat/commit/3b2a2c1))

</details>

<div align="right">

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

</div>
2025-10-14 10:26:28 +00:00
YuTengjing 3b2a2c1c54 💄 style: add imagen model to vertex ai (#9699)
* ♻️ refactor: rename isLocalUrl to isDesktopLocalStaticServerUrl

Rename the function to better reflect its specific purpose of checking
desktop local static server URLs (127.0.0.1 only). Update all usages
across the codebase including imports, function calls, and test cases.

*  feat(model-bank): add Vertex AI image generation models

- Add Nano Banana (Gemini 2.5 Flash Image) models
- Add Imagen 4 series (Standard, Ultra, Fast, Preview variants)
- Export shared parameters for reuse across providers

*  test(context-engine): fix mock after function rename

Update test mock from isLocalUrl to isDesktopLocalStaticServerUrl

* ♻️ refactor: use submodule imports for @lobechat/utils

- Change from barrel imports to direct submodule imports
- Update test to mock only necessary functions (imageUrlToBase64)
- Fix test URL from localhost to 127.0.0.1 for isDesktopLocalStaticServerUrl
- Update package.json exports for utils submodules

*  test: update mocks after function rename

Update test mocks from isLocalUrl to isDesktopLocalStaticServerUrl

*  test(chat): fix mocks to use submodule imports
2025-10-14 18:15:58 +08:00
lobehubbot c07d900648 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-14 07:52:51 +00:00
semantic-release-bot 938cf6ba71 🔖 chore(release): v1.137.4 [skip ci]
### [Version&nbsp;1.137.4](https://github.com/lobehub/lobe-chat/compare/v1.137.3...v1.137.4)
<sup>Released on **2025-10-14**</sup>

#### 🐛 Bug Fixes

- **misc**: Prevent Vertex AI JSON credentials from being split by comma.

<br/>

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

#### What's fixed

* **misc**: Prevent Vertex AI JSON credentials from being split by comma, closes [#9703](https://github.com/lobehub/lobe-chat/issues/9703) [#9477](https://github.com/lobehub/lobe-chat/issues/9477) ([189081d](https://github.com/lobehub/lobe-chat/commit/189081d))

</details>

<div align="right">

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

</div>
2025-10-14 07:51:42 +00:00
Arvin Xu 189081d546 🐛 fix: prevent Vertex AI JSON credentials from being split by comma (#9703)
- Add special case for VertexAI in getProviderAuthPayload to skip random API key selection
- Vertex AI uses JSON format credentials that contain commas, which should not be split
- Add test case to verify JSON credentials remain intact

Fixes the issue introduced in #9477 where random API key splitting broke Vertex AI authentication

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-10-14 15:00:44 +08:00
bbbugg 96e2ccd3fb 🐛fix(mobile): enable conditional fetching of AI provider list based on mobile view (#9621)
🐛fix: enable conditional fetching of AI provider list based on mobile view
2025-10-14 13:35:23 +08:00
Arvin Xu 8693d95e0d 🔨 chore: add e2e workflow (#9677)
* add e2e test

* Potential fix for code scanning alert no. 137: Workflow does not contain permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* remove

* update

* fix tests

* add e2e

* update

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-10-14 13:32:52 +08:00
lobehubbot 543db87745 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-14 03:10:43 +00:00
semantic-release-bot e31e7b5d2d 🔖 chore(release): v1.137.3 [skip ci]
### [Version&nbsp;1.137.3](https://github.com/lobehub/lobe-chat/compare/v1.137.2...v1.137.3)
<sup>Released on **2025-10-14**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix mcp server connect issue and refactor web search implement, fix tools calling long name length >64 issue.

<br/>

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

#### What's fixed

* **misc**: Fix mcp server connect issue and refactor web search implement, closes [#9694](https://github.com/lobehub/lobe-chat/issues/9694) ([15ebcb4](https://github.com/lobehub/lobe-chat/commit/15ebcb4))
* **misc**: Fix tools calling long name length >64 issue, closes [#9697](https://github.com/lobehub/lobe-chat/issues/9697) ([cb98604](https://github.com/lobehub/lobe-chat/commit/cb98604))

</details>

<div align="right">

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

</div>
2025-10-14 03:09:37 +00:00
Arvin Xu 15ebcb414b 🐛 fix: fix mcp server connect issue and refactor web search implement (#9694)
* add

* clean

* refactor

* refactor

* add test

* fix style

* refactor to improve search performance

* refactor types

* refactor types

* refactor types

* fix mcp retry issue

* add more tests

* fix test and types

* fix test

* fix desktop remote streamable http

* add local

* fix tests

* update
2025-10-14 10:58:47 +08:00
Arvin Xu cb986040d5 🐛 fix: fix tools calling long name length >64 issue (#9697)
* add ToolNameResolver

* fix ToolName generate tests

* fix tests
2025-10-14 10:54:40 +08:00
lobehubbot ad0fae3c2a 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-14 02:28:05 +00:00
semantic-release-bot 79955e8f8d 🔖 chore(release): v1.137.2 [skip ci]
### [Version&nbsp;1.137.2](https://github.com/lobehub/lobe-chat/compare/v1.137.1...v1.137.2)
<sup>Released on **2025-10-14**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix the Worker URL cross-origin issue.

<br/>

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

#### What's fixed

* **misc**: Fix the Worker URL cross-origin issue, closes [#9624](https://github.com/lobehub/lobe-chat/issues/9624) ([d379112](https://github.com/lobehub/lobe-chat/commit/d379112))

</details>

<div align="right">

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

</div>
2025-10-14 02:27:04 +00:00
peerless-hero d379112d74 🐛 fix: fix the Worker URL cross-origin issue (#9624)
When the origin of the Worker script is different from the current page, reconstruct the URL relative to the current origin to avoid cross-origin errors.
2025-10-14 10:16:33 +08:00
lobehubbot c1a0868ac3 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-14 02:10:13 +00:00
semantic-release-bot af71a7933a 🔖 chore(release): v1.137.1 [skip ci]
### [Version&nbsp;1.137.1](https://github.com/lobehub/lobe-chat/compare/v1.137.0...v1.137.1)
<sup>Released on **2025-10-14**</sup>

#### 💄 Styles

- **misc**: Change the user chatItem maxWidth should use flex 1.

<br/>

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

#### Styles

* **misc**: Change the user chatItem maxWidth should use flex 1, closes [#9689](https://github.com/lobehub/lobe-chat/issues/9689) ([cfd5221](https://github.com/lobehub/lobe-chat/commit/cfd5221))

</details>

<div align="right">

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

</div>
2025-10-14 02:08:56 +00:00
Shinji-Li cfd52210d5 💄 style: change the user chatItem maxWidth should use flex 1 (#9689)
* feat: change the user chatitem max flex 1

* feat: add when editing not show actions
2025-10-14 09:58:39 +08:00
iBenzene 166d715f17 🔨 chore: add Clerk authentication related variables and some other variables in Dockerfile (#9640)
* chore(docker): add Clerk authentication related variables in Dockerfile

* chore(docker): add ENABLED_FAL, S3_ENABLE_PATH_STYLE, and S3_SET_ACL environment variables to Dockerfile

* chore(docker): add CLERK_WEBHOOK_SECRET environment variable

* revert(docker): fix indentation that was mistakenly modified due to. vscode settings

* chore(docker): add ENABLE_AUTH_PROTECTION environment variable to Dockerfiles

---------

Co-authored-by: bbbugg <daming20120101@163.com>
2025-10-14 09:53:43 +08:00
Arvin Xu 1238d7fbd5 🔨 chore: output JSON support tools calling mode (#9696)
* support tools calling in generateObject method

* fix tests
2025-10-14 09:51:38 +08:00
Arvin Xu 06507fea7f test: add model runtime testing (#9683)
* baseline test docs

* batch 2

* add more tests

* baseline 2

* baseline 3

* baseline 4

* baseline 5

* baseline 6

* baseline 7

* add core tests

* fix tests
2025-10-13 10:18:13 +08:00
Arvin Xu 4b7e838008 test: add tests for agent-runtime and mode-runtime (#9682)
* update agent runtime

* update agent runtime

* add model runtime tests

* add tests

* add more tests

* add more tests
2025-10-13 00:09:07 +08:00
lobehubbot 9e09099313 📝 docs(bot): Auto sync agents & plugin to readme 2025-10-12 16:07:27 +00:00
2814 changed files with 243004 additions and 62089 deletions
+38
View File
@@ -0,0 +1,38 @@
---
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh api:*), Bash(gh issue comment:*)
description: Find duplicate GitHub issues
---
Find up to 3 likely duplicate issues for a given GitHub issue.
To do this, follow these steps precisely:
1. Use an agent to check if the Github issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #1
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
Notes (be sure to tell this to your agents, too):
- Use `gh` to interact with Github, rather than web fetch
- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.)
- Make a todo list first
- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates):
---
Found 3 possible duplicate issues:
1. <link to issue>
2. <link to issue>
3. <link to issue>
This issue will be automatically closed as a duplicate in 3 days.
- If your issue is a duplicate, please close it and 👍 the existing issue instead
- To prevent auto-closure, add a comment or 👎 this comment
> 🤖 Generated with Claude Code
---
+228
View File
@@ -0,0 +1,228 @@
# Auto Testing Coverage Assistant
You are an auto testing assistant. Your task is to add unit tests to improve code coverage in the codebase.
## Target Directories
Prioritize modules with business logic:
- apps/desktop/src/core/
- apps/desktop/src/modules/
- apps/desktop/src/controllers/
- apps/desktop/src/services/
- packages/\*/src/
- src/services/
- src/store/
- src/server/routers/
- src/server/services/
- src/server/modules/
- src/libs/
- src/utils/
**Do NOT test**:
- UI components (\*.tsx React components)
- Test files themselves
- Generated files
- Configuration files
- Type definition files
## Workflow
### 1. Select a Module to Process
**Selection Strategy**:
- Randomly pick ONE module from the target directories
- Prioritize modules that:
- Have significant business logic
- Have no or minimal test coverage
- Already have example test files (easier to follow patterns)
- Are large modules with complex logic
**Module granularity examples**:
- A single package: `packages/database/src/models`
- A desktop module: `apps/desktop/src/modules/auth`
- A service directory: `src/services/user`
- A store slice: `src/store/chat`
**Special handling**:
- If a directory has NO tests but needs coverage → create ONE example test file
- If a directory already has some tests → expand coverage to untested functions/classes
- Focus on directories with existing test examples (follow their patterns)
### 2. Analyze Module Structure
Before writing tests:
- Identify core business logic functions/classes
- Check for existing test files and patterns
- Determine testing approach based on module type:
- Database models → test CRUD operations
- Services → test business logic flows
- Controllers → test request handling
- Store slices → test state mutations and actions
- Utils → test utility functions with edge cases
### 3. Write Unit Tests
**Testing Guidelines**:
- Follow existing test patterns in the codebase
- Use Vitest as the testing framework
- Focus on business logic, not UI rendering
- Write comprehensive tests covering:
- Happy path scenarios
- Edge cases
- Error handling
- Input validation
- Use descriptive test names: `describe()` and `it()` blocks
- Mock external dependencies appropriately
- Keep tests isolated and independent
**Test File Naming**:
- Place test files next to source files: `filename.test.ts`
- Or in `__tests__` directory: `__tests__/filename.test.ts`
**Example Test Structure**:
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { functionToTest } from './module';
describe('ModuleName', () => {
describe('functionName', () => {
it('should handle normal case correctly', () => {
// Arrange
const input = 'test';
// Act
const result = functionToTest(input);
// Assert
expect(result).toBe('expected');
});
it('should handle edge case', () => {
// Test edge case
});
it('should throw error on invalid input', () => {
// Test error handling
});
});
});
```
### 4. Run Tests and Fix Issues
**CRITICAL**: Tests MUST pass before submitting!
- Run tests using the appropriate command:
- Web: `bunx vitest run --silent='passed-only' '[file-path-pattern]'`
- Packages: `cd packages/[name] && bunx vitest run --silent='passed-only' '[file-path-pattern]'`
- Wrap file paths in single quotes
- Fix any failing tests
- Ensure all tests pass before proceeding
**If tests fail**:
- Debug and fix the test logic
- Check mocks and dependencies
- Verify test isolation
- If unable to fix after 2 attempts, skip this module and document the issue
### 5. Create Pull Request
- Create a new branch: `automatic/add-tests-[module-name]-[date]`
- Commit changes with message format:
```
✅ test: add unit tests for [module-name]
```
- Push the branch
- Create a PR with:
- Title: `✅ test: add unit tests for [module-name]`
- Body following this template:
```markdown
## Summary
- Added unit tests for `[module-name]`
- Total test files added/modified: [number]
- Test cases added: [number]
- Coverage focus: [brief description of what was tested]
## Changes
- [ ] All tests pass successfully
- [ ] Business logic coverage improved
- [ ] Edge cases and error handling covered
- [ ] Tests follow existing patterns
## Module Processed
`[module-path]`
## Test Coverage
- Functions tested: [list key functions]
- Coverage type: [unit/integration]
- Test approach: [brief description]
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
```
## Important Rules
- **DO** focus on business logic testing only
- **DO** ensure all tests pass before creating PR
- **DO** follow existing test patterns in the codebase
- **DO** write descriptive test names and comments
- **DO** test edge cases and error scenarios
- **DO NOT** test UI components (\*.tsx)
- **DO NOT** create tests that will fail
- **DO NOT** modify production code unless absolutely necessary for testability
- **DO NOT** exceed 45 minutes of workflow time
- **DO NOT** create tests for generated or configuration files
## Module Selection Examples
**Good choices**:
- `packages/database/src/models/` - Core CRUD operations
- `src/services/user/client.ts` - User service business logic
- `apps/desktop/src/modules/auth/` - Authentication logic
- `src/store/chat/slices/message/` - Message state management
- `src/server/services/` - Backend service logic
**Bad choices**:
- `src/components/` - UI components (avoid)
- `src/app/` - Next.js pages (avoid)
- `src/styles/` - Styling files (avoid)
- Configuration files (avoid)
## Testing Best Practices
1. **Arrange-Act-Assert** pattern
2. **Mock external dependencies** (APIs, databases, file system)
3. **Test one thing per test case**
4. **Use descriptive test names**
5. **Keep tests fast and isolated**
6. **Follow DRY principle with beforeEach/afterEach**
7. **Test behavior, not implementation**
## Example Modules with Test Patterns
Look for existing test files to understand patterns:
- `packages/database/src/models/**/*.test.ts` - Database testing patterns
- `apps/desktop/src/controllers/**/*.test.ts` - Controller testing patterns
- `src/services/**/*.test.ts` - Service testing patterns
Follow their structure and conventions when adding new tests.
+253
View File
@@ -0,0 +1,253 @@
# Issue Triage Guide
This guide is used for batch triaging GitHub issues - analyzing issues and applying appropriate labels.
## Workflow
For EACH issue, follow these steps:
### Step 1: Get Available Labels (run once per batch)
```bash
gh label list --json name,description --limit 300
```
### Step 2: Get Issue Details
For each issue number, run:
```bash
gh issue view [ISSUE_NUMBER] --json number,title,body,labels,comments
```
### Step 3: Analyze and Select Labels
Extract information from the issue template and content:
#### Template Fields Mapping
- 📦 Platform field → `platform:web/desktop/mobile`
- 💻 Operating System → `os:windows/macos/linux/ios`
- 🌐 Browser → `device:pc/mobile`
- 📦 Deployment mode → `deployment:server/client/pglite`
- Platform (hosting) → `hosting:cloud/self-host/vercel/zeabur/railway`
#### Provider Detection
**IMPORTANT**: Always check issue title and body for provider mentions!
**Official Providers** (check for these keywords in title/body):
- `openai`, `gpt``provider:openai`
- `gemini``provider:gemini`
- `claude`, `anthropic``provider:claude`
- `deepseek``provider:deepseek`
- `google``provider:google`
- `ollama``provider:ollama`
- `azure``provider:azure`
- `bedrock``provider:bedrock`
- `vertex``provider:vertex`
- `groq`, `grok``provider:groq`
- `mistral``provider:mistral`
- `moonshot``provider:moonshot`
- `zhipu``provider:zhipu`
- `minimax``provider:minimax`
- `doubao``provider:doubao`
**Third-party Aggregation Providers**:
- `aihubmix`, `AIHubMix`, `AIHUBMIX``provider:aihubmix`
- Check environment variables like `AIHUBMIX_*` in issue body
**Multiple Providers**: If issue mentions multiple providers, add ALL applicable provider labels.
### Label Categories
#### a) Issue Type (select ONE if applicable)
- `💄 Design` - UI/UX design issues
- `📝 Documentation` - Documentation improvements
- `⚡️ Performance` - Performance optimization
#### b) Priority (select ONE if applicable)
- `priority:high` - Critical issues, data loss, security, maintainer mentions "urgent"/"serious"/"critical"
- `priority:medium` - Important issues affecting multiple users, significant functionality impact
- `priority:low` - Nice to have, minor issues, edge cases
**Priority Guidelines**:
- Set `priority:high` for: data loss, authentication failures, deployment blockers, critical bugs
- Set `priority:medium` for: feature bugs affecting multiple users, workflow issues
- Set `priority:low` for: cosmetic issues, feature requests, configuration questions
#### c) Platform (select ALL applicable)
- `platform:web`
- `platform:desktop`
- `platform:mobile`
#### d) Device (for platform:web, select ONE)
- `device:pc`
- `device:mobile`
#### e) Operating System (select ALL applicable)
- `os:windows`
- `os:macos`
- `os:linux`
- `os:ios`
- `os:android`
#### f) Hosting Platform (select ONE)
- `hosting:cloud` - Official LobeHub Cloud
- `hosting:self-host` - Self-hosted deployment
- `hosting:vercel` - Vercel deployment
- `hosting:zeabur` - Zeabur deployment
- `hosting:railway` - Railway deployment
#### g) Deployment Mode (select ONE if mentioned)
- `deployment:server` - Server-side database mode
- `deployment:client` - Client-side database mode
- `deployment:pglite` - PGLite mode
**Additional deployment tags**:
- `docker` - If using Docker deployment
- `electron` - If desktop/Electron specific
#### h) Model Provider (select ALL applicable)
See "Provider Detection" section above for complete list.
**IMPORTANT**: Always scan issue title and body for provider keywords!
#### i) Feature/Component (select ALL applicable)
Core Features:
- `feature:settings` - Settings and configuration
- `feature:agent` - Agent/Assistant functionality
- `feature:topic` - Topic/Conversation management
- `feature:marketplace` - Agent marketplace
File & Knowledge:
- `feature:files` - File upload/management
- `feature:knowledge-base` - Knowledge base and RAG
- `feature:export` - Export functionality
Model Capabilities:
- `feature:streaming` - Streaming responses
- `feature:tool` - Tool calling
- `feature:vision` - Vision/multimodal capabilities
- `feature:image` - AI image generation
- `feature:dalle` - DALL-E specific
- `feature:tts` - Text-to-speech
Technical:
- `feature:api` - Backend API
- `feature:auth` - Authentication/authorization
- `feature:sync` - Cloud sync functionality
- `feature:search` - Search functionality
- `feature:mcp` - MCP integration
- `feature:editor` - Lobe Editor
- `feature:markdown` - Markdown rendering
- `feature:thread` - Thread/Subtopic functionality
Collaboration:
- `feature:group-chat` - Group chat functionality
- `feature:memory` - Memory feature
- `feature:team-workspace` - Team workspace
#### j) Workflow/Status
- `Duplicate` - Only if duplicate of an OPEN issue (mention issue number)
- `needs-reproduction` - Cannot reproduce, needs more information
- `good-first-issue` - Good for first-time contributors
- `🤔 Need Reproduce` - Needs reproduction steps
### Step 4: Apply Labels
Add labels (comma-separated, no spaces after commas):
```bash
gh issue edit [ISSUE_NUMBER] --add-label "label1,label2,label3"
```
Remove "unconfirm" label if adding other labels:
```bash
gh issue edit [ISSUE_NUMBER] --remove-label "unconfirm"
```
**Important**: Combine both commands when possible for efficiency.
### Step 5: Log Summary
For each issue, provide reasoning (2-4 sentences):
- Labels applied and why
- Key factors from issue template/comments
- Provider detection reasoning (if applicable)
## Important Rules
1. **Read Carefully**: Read issue template fields AND issue body/title for complete context
2. **Provider Detection**: ALWAYS check title and body for provider keywords (including aihubmix, etc.)
3. **Multiple Categories**: Use ALL applicable labels from different categories
4. **Label Prefixes**: Always use proper prefixes (`feature:`, `provider:`, `os:`, `platform:`, etc.)
5. **Maintainer Comments**: Check maintainer comments for priority/status hints
6. **No Comments**: Only apply labels, DO NOT post comments to issues
7. **Batch Efficiency**: Process issues in parallel when possible
## Common Patterns
### Provider in Environment Variables
If issue body contains `AIHUBMIX_*`, add `provider:aihubmix`
### Multiple Provider Issues
If comparing providers (e.g., "works with OpenAI but not Gemini"), add both provider labels
### Desktop Issues
Desktop issues often need: `platform:desktop`, `electron`, specific `os:*`, and `deployment:client` or `deployment:server`
### Knowledge Base Issues
Usually need: `feature:knowledge-base`, often with `feature:files`, may need `provider:*` for embedding models
### Tool Calling Issues
Usually need: `feature:tool`, specific `provider:*`, may need `feature:mcp` if MCP-related
### Streaming Issues
Usually need: `feature:streaming`, specific `provider:*`, check for timeout/performance issues
## Example Triage
**Issue #8850**: "aihubmix 的优惠 app 没有生效"
**Analysis**:
- Title contains "aihubmix" → `provider:aihubmix`
- Template shows: Windows, Chrome, Docker, Client mode
- About API discount codes not working
**Labels Applied**:
```bash
gh issue edit 8850 --add-label "provider:aihubmix,platform:web,os:windows,deployment:client,hosting:self-host,docker"
gh issue edit 8850 --remove-label "unconfirm"
```
**Reasoning**: AIHubMix provider discount feature not working. Client mode deployment on Windows with Docker. Provider detection from title keyword "aihubmix".
+135
View File
@@ -0,0 +1,135 @@
# Team Assignment Guide
## Quick Reference by Name
- **@arvinxx**: Last resort only, mention for priority:high issues, tool calling , mcp
- **@canisminor1990**: Design, UI components, editor
- **@tjx666**: Image/video generation, vision, cloud, documentation, TTS
- **@ONLY-yours**: Performance, streaming, settings, general bugs, web platform, marketplace
- **@RiverTwilight**: Knowledge base, files (KB-related), group chat
- **@nekomeowww**: Memory, backend, deployment, DevOps
- **@sudongyuer**: Mobile app (React Native)
- **@sxjeru**: Model providers and configuration
- **@cy948**: Auth Modules
- **@rdmclin2**: Team workspace
Quick reference for assigning issues based on labels.
## Label to Team Member Mapping
### Provider Labels (provider:\*)
| Label | Owner | Notes |
| ---------------- | ------- | -------------------------------------------- |
| All `provider:*` | @sxjeru | Model configuration and provider integration |
### Platform Labels (platform:\*)
| Label | Owner | Notes |
| ------------------ | ----------- | -------------------------------------- |
| `platform:mobile` | @sudongyuer | React Native mobile app |
| `platform:desktop` | @ONLY-yours | Electron desktop client (general) |
| `platform:web` | @ONLY-yours | Web platform (unless specific feature) |
### Feature Labels (feature:\*)
| Label | Owner | Notes |
| ------------------------ | --------------- | ----------------------------------------------------------------------- |
| `feature:image` | @tjx666 | AI image generation |
| `feature:dalle` | @tjx666 | DALL-E related |
| `feature:vision` | @tjx666 | Vision/multimodal generation |
| `feature:knowledge-base` | @RiverTwilight | Knowledge base and RAG |
| `feature:files` | @RiverTwilight | File upload/management (when KB-related)<br>@ONLY-yours (general files) |
| `feature:editor` | @canisminor1990 | Lobe Editor |
| `feature:auth` | @cy948 | Authentication/authorization |
| `feature:api` | @nekomeowww | Backend API |
| `feature:streaming` | @arvinxx | Streaming response |
| `feature:settings` | @ONLY-yours | Settings and configuration |
| `feature:agent` | @ONLY-yours | Agent/Assistant |
| `feature:topic` | @ONLY-yours | Topic/Conversation management |
| `feature:thread` | @arvinxx | Thread/Subtopic |
| `feature:marketplace` | @ONLY-yours | Agent marketplace |
| `feature:tool` | @arvinxx | Tool calling |
| `feature:mcp` | @arvinxx | MCP integration |
| `feature:search` | @ONLY-yours | Search functionality |
| `feature:tts` | @tjx666 | Text-to-speech |
| `feature:export` | @ONLY-yours | Export functionality |
| `feature:group-chat` | @RiverTwilight | Group chat functionality |
| `feature:memory` | @nekomeowww | Memory feature |
| `feature:team-workspace` | @rdmclin2 | Team workspace application |
### Deployment Labels (deployment:\*)
| Label | Owner | Notes |
| ------------------ | ----------- | -------------------------- |
| All `deployment:*` | @nekomeowww | Server/client/pglite modes |
### Hosting Labels (hosting:\*)
| Label | Owner | Notes |
| ------------------- | ----------- | ---------------------- |
| `hosting:cloud` | @tjx666 | Official LobeHub Cloud |
| `hosting:self-host` | @nekomeowww | Self-hosting issues |
| `hosting:vercel` | @nekomeowww | Vercel deployment |
| `hosting:zeabur` | @nekomeowww | Zeabur deployment |
| `hosting:railway` | @nekomeowww | Railway deployment |
### Issue Type Labels
| Label | Owner | Notes |
| ------------------ | -------------------- | ---------------------------- |
| 💄 Design | @canisminor1990 | Design and styling |
| 📝 Documentation | @tjx666 | Documentation |
| ⚡️ Performance | @ONLY-yours | Performance optimization |
| 🐛 Bug | (depends on feature) | Assign based on other labels |
| 🌠 Feature Request | (depends on feature) | Assign based on other labels |
## Assignment Rules
### Priority Order (apply in order)
1. **Specific feature owner** - e.g., `feature:knowledge-base`@RiverTwilight
2. **Platform owner** - e.g., `platform:mobile`@sudongyuer
3. **Provider owner** - e.g., `provider:*`@sxjeru
4. **Component owner** - e.g., 💄 Design → @canisminor1990
5. **Infrastructure owner** - e.g., `deployment:*`@nekomeowww
6. **General maintainer** - @ONLY-yours for general bugs/issues
7. **Last resort** - @arvinxx (only if no clear owner)
### Special Cases
**Multiple labels with different owners:**
- Mention the **most specific** feature owner first
- Mention secondary owners if their input is valuable
- Example: `feature:knowledge-base` + `deployment:server`@RiverTwilight (primary), @nekomeowww (secondary)
**Priority:high issues:**
- Mention feature owner + @arvinxx
- Example: `priority:high` + `feature:image`@tjx666 @arvinxx
**No clear owner:**
- Assign to @ONLY-yours for general issues
- Only mention @arvinxx if critical and truly unclear
## Comment Templates
**Single owner:**
```
@username - This is a [feature/component] issue. Please take a look.
```
**Multiple owners:**
```
@primary @secondary - This involves [features]. Please coordinate.
```
**High priority:**
```
@owner @arvinxx - High priority [feature] issue.
```
+89
View File
@@ -0,0 +1,89 @@
# Code Comment Translation Assistant
You are a code comment translation assistant. Your task is to find non-English comments in the codebase and translate them to English.
## Target Directories
- apps/desktop/src/
- packages/\*/src/
- src
## Workflow
### 1. Select a Module to Process
Module granularity examples:
- A single package: `packages/database`
- A desktop module: `apps/desktop/src/modules/auth`
- A service directory: `src/services/user`
### 2. Find Non-English Comments
- Search for files containing non-English characters in comments (excluding test files)
- File types to check: `.ts`, `.tsx`
- Exclude: `*.test.ts`, `*.test.tsx`, `*.spec.ts`, `*.spec.tsx`, `node_modules`, `dist`, `build`
### 3. Translate Comments
- Translate all non-English comments to English while preserving:
- Code functionality (do not change any code)
- Comment structure and formatting
- JSDoc tags and annotations
- Markdown formatting in comments
- Translation guidelines:
- Keep technical terms accurate
- Maintain professional tone
- Preserve line breaks and indentation
- Keep TODO/FIXME/NOTE markers in English
### 4. Limit Changes
- **CRITICAL**: Ensure total changes do not exceed 500 lines
- If a module would exceed 500 lines, process only part of it
- Count lines using: `git diff --stat`
- Stop processing files once approaching the 500-line limit
### 5. Create Pull Request
- Create a new branch: `automatic/translate-comments-[module-name]-[date]`
- Commit changes with message format:
```
🌐 chore: translate non-English comments to English in [module-name]
```
- Push the branch
- Create a PR with:
- Title: `🌐 chore: translate non-English comments to English in [module-name]`
- Body following this template:
```markdown
## Summary
- Translated non-English comments to English in `[module-name]`
- Total lines changed: [number] lines
- Files affected: [number] files
## Changes
- [ ] All non-English comments translated to English
- [ ] Code functionality unchanged
- [ ] Comment formatting preserved
## Module Processed
`[module-path]`
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
```
## Important Rules
- **DO NOT** modify any code logic, only comments
- **DO NOT** translate non-English strings in code (only comments)
- **DO NOT** exceed 500 lines of changes in one PR
- **DO NOT** process test files or generated files
- **DO** preserve all code formatting and structure
- **DO** ensure translations are technically accurate
- **DO** verify changes compile without errors
@@ -0,0 +1,579 @@
---
description: Best practices for testing Zustand store actions
globs: "src/store/**/*.test.ts"
alwaysApply: false
---
# Zustand Store Action Testing Guide
This guide provides best practices for testing Zustand store actions, based on our proven testing patterns.
## Basic Test Structure
```typescript
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { messageService } from '@/services/message';
import { useChatStore } from '../../store';
// Keep zustand mock as it's needed globally
vi.mock('zustand/traditional');
beforeEach(() => {
// Reset store state
vi.clearAllMocks();
useChatStore.setState(
{
activeId: 'test-session-id',
messagesMap: {},
loadingIds: [],
},
false,
);
// ✅ Setup only spies that MOST tests need
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
// ❌ Don't setup spies that only few tests need - spy only when needed
// Setup common mock methods
act(() => {
useChatStore.setState({
refreshMessages: vi.fn(),
internal_coreProcessMessage: vi.fn(),
});
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('action name', () => {
describe('validation', () => {
// Validation tests
});
describe('normal flow', () => {
// Happy path tests
});
describe('error handling', () => {
// Error case tests
});
});
```
## Testing Best Practices
### 1. Test Layering - Spy Direct Dependencies Only
✅ **Good**: Spy on the direct dependency
```typescript
// When testing internal_coreProcessMessage, spy its direct dependency
const fetchAIChatSpy = vi
.spyOn(result.current, 'internal_fetchAIChatMessage')
.mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
```
❌ **Bad**: Spy on lower-level implementation details
```typescript
// Don't spy on services that internal_fetchAIChatMessage uses
const streamSpy = vi
.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(...);
```
**Why**: Each test should only mock its direct dependencies, not the entire call chain. This makes tests more maintainable and less brittle.
### 2. Mock Management - Minimize Global Spies
✅ **Good**: Spy only when needed
```typescript
beforeEach(() => {
// ✅ Only spy services that most tests need
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
// ✅ Don't spy chatService globally
});
it('should process message', async () => {
// ✅ Spy chatService only in tests that need it
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(...);
// test logic
streamSpy.mockRestore();
});
```
❌ **Bad**: Setup all spies globally
```typescript
beforeEach(() => {
vi.spyOn(messageService, 'createMessage').mockResolvedValue('id');
vi.spyOn(chatService, 'createAssistantMessageStream').mockResolvedValue({}); // ❌ Not all tests need this
vi.spyOn(fileService, 'uploadFile').mockResolvedValue({}); // ❌ Creates implicit coupling
});
```
### 3. Service Mocking - Mock the Correct Layer
✅ **Good**: Mock the service method
```typescript
it('should fetch AI chat response', async () => {
const streamSpy = vi
.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
await onFinish?.('Hello', {});
});
// test logic
});
```
❌ **Bad**: Mock global fetch
```typescript
it('should fetch AI chat response', async () => {
global.fetch = vi.fn().mockResolvedValue(...); // ❌ Too low level
});
```
### 4. Test Organization - Use Descriptive Nesting
✅ **Good**: Clear nested structure
```typescript
describe('sendMessage', () => {
describe('validation', () => {
it('should not send when session is inactive', async () => {});
it('should not send when message is empty', async () => {});
});
describe('message creation', () => {
it('should create user message and trigger AI processing', async () => {});
it('should send message with files attached', async () => {});
});
describe('error handling', () => {
it('should handle message creation errors gracefully', async () => {});
});
});
```
❌ **Bad**: Flat structure
```typescript
describe('sendMessage', () => {
it('test 1', async () => {});
it('test 2', async () => {});
it('test 3', async () => {});
});
```
### 5. Testing Async Actions
Always wrap async operations in `act()`:
```typescript
it('should send message', async () => {
const { result } = renderHook(() => useChatStore());
await act(async () => {
await result.current.sendMessage({ message: 'Hello' });
});
expect(messageService.createMessage).toHaveBeenCalled();
});
```
### 6. State Setup - Use act() for setState
```typescript
it('should handle disabled state', async () => {
act(() => {
useChatStore.setState({ activeId: undefined });
});
const { result } = renderHook(() => useChatStore());
// test logic
});
```
### 7. Testing Complex Flows
For complex flows with multiple steps, use clear spy setup:
```typescript
it('should handle topic creation flow', async () => {
// Setup store state
act(() => {
useChatStore.setState({
activeTopicId: undefined,
messagesMap: {
'test-session-id': [
{ id: 'msg-1', role: 'user', content: 'Message 1' },
{ id: 'msg-2', role: 'assistant', content: 'Response 1' },
{ id: 'msg-3', role: 'user', content: 'Message 2' },
],
},
});
});
const { result } = renderHook(() => useChatStore());
// Spy on action dependencies
const createTopicSpy = vi.spyOn(result.current, 'createTopic')
.mockResolvedValue('new-topic-id');
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleMessageLoading');
// Execute
await act(async () => {
await result.current.sendMessage({ message: 'Test message' });
});
// Assert
expect(createTopicSpy).toHaveBeenCalled();
expect(toggleLoadingSpy).toHaveBeenCalledWith(true, expect.any(String));
});
```
### 8. Streaming Response Mocking
When testing streaming responses, simulate the flow properly:
```typescript
it('should handle streaming chunks', async () => {
const { result } = renderHook(() => useChatStore());
const messages = [
{ id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' },
];
const streamSpy = vi
.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
// Simulate streaming chunks
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
await onMessageHandle?.({ type: 'text', text: ' World' } as any);
await onFinish?.('Hello World', {});
});
await act(async () => {
await result.current.internal_fetchAIChatMessage({
messages,
messageId: 'test-message-id',
model: 'gpt-4o-mini',
provider: 'openai',
});
});
expect(result.current.internal_dispatchMessage).toHaveBeenCalled();
streamSpy.mockRestore();
});
```
### 9. Error Handling Tests
Always test error scenarios:
```typescript
it('should handle errors gracefully', async () => {
const { result } = renderHook(() => useChatStore());
vi.spyOn(messageService, 'createMessage').mockRejectedValue(
new Error('create message error'),
);
await act(async () => {
try {
await result.current.sendMessage({ message: 'Test message' });
} catch {
// Expected to throw
}
});
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
});
```
### 10. Cleanup After Tests
Always restore mocks after each test:
```typescript
afterEach(() => {
vi.restoreAllMocks();
});
// For individual test cleanup:
it('should test something', async () => {
const spy = vi.spyOn(service, 'method').mockImplementation(...);
// test logic
spy.mockRestore(); // Optional: cleanup immediately after test
});
```
## Common Patterns
### Testing Store Methods That Call Other Store Methods
```typescript
it('should call internal methods', async () => {
const { result } = renderHook(() => useChatStore());
const internalMethodSpy = vi.spyOn(result.current, 'internal_method')
.mockResolvedValue();
await act(async () => {
await result.current.publicMethod();
});
expect(internalMethodSpy).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ key: 'value' }),
);
});
```
### Testing Conditional Logic
```typescript
describe('conditional behavior', () => {
it('should execute when condition is true', async () => {
const { result } = renderHook(() => useChatStore());
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(true);
await act(async () => {
await result.current.sendMessage({ message: 'test' });
});
expect(result.current.internal_retrieveChunks).toHaveBeenCalled();
});
it('should not execute when condition is false', async () => {
const { result } = renderHook(() => useChatStore());
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(false);
await act(async () => {
await result.current.sendMessage({ message: 'test' });
});
expect(result.current.internal_retrieveChunks).not.toHaveBeenCalled();
});
});
```
### Testing AbortController
```typescript
it('should abort generation and clear loading state', () => {
const abortController = new AbortController();
act(() => {
useChatStore.setState({ chatLoadingIdsAbortController: abortController });
});
const { result } = renderHook(() => useChatStore());
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
act(() => {
result.current.stopGenerateMessage();
});
expect(abortController.signal.aborted).toBe(true);
expect(toggleLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
});
```
## Anti-Patterns to Avoid
❌ **Don't**: Mock the entire store
```typescript
vi.mock('../../store', () => ({
useChatStore: vi.fn(() => ({
sendMessage: vi.fn(),
})),
}));
```
❌ **Don't**: Test implementation details
```typescript
// Bad: testing internal state structure
expect(result.current.messagesMap).toHaveProperty('test-session');
// Good: testing behavior
expect(result.current.refreshMessages).toHaveBeenCalled();
```
❌ **Don't**: Create tight coupling between tests
```typescript
// Bad: Tests depend on order
let messageId: string;
it('test 1', () => {
messageId = 'some-id'; // Side effect
});
it('test 2', () => {
expect(messageId).toBeDefined(); // Depends on test 1
});
```
❌ **Don't**: Over-mock services
```typescript
// Bad: Mocking everything
beforeEach(() => {
vi.mock('@/services/chat');
vi.mock('@/services/message');
vi.mock('@/services/file');
vi.mock('@/services/agent');
// ... too many global mocks
});
```
## Testing SWR Hooks in Zustand Stores
Some Zustand store slices use SWR hooks for data fetching. These require a different testing approach.
### Basic SWR Hook Test Structure
```typescript
import { renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { discoverService } from '@/services/discover';
import { globalHelpers } from '@/store/global/helpers';
import { useDiscoverStore as useStore } from '../../store';
vi.mock('zustand/traditional');
beforeEach(() => {
vi.clearAllMocks();
});
describe('SWR Hook Actions', () => {
it('should fetch data and return correct response', async () => {
const mockData = [{ id: '1', name: 'Item 1' }];
// Mock the service call (the fetcher)
vi.spyOn(discoverService, 'getPluginCategories').mockResolvedValue(mockData as any);
vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('en-US');
const params = {} as any;
const { result } = renderHook(() => useStore.getState().usePluginCategories(params));
// Use waitFor to wait for async data loading
await waitFor(() => {
expect(result.current.data).toEqual(mockData);
});
expect(discoverService.getPluginCategories).toHaveBeenCalledWith(params);
});
});
```
**Key points**:
- **DO NOT mock useSWR** - let it use the real implementation
- Only mock the **service methods** (fetchers)
- Use `waitFor` from `@testing-library/react` to wait for async operations
- Check `result.current.data` directly after waitFor completes
### Testing SWR Key Generation
```typescript
it('should generate correct SWR key with locale and params', () => {
vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('zh-CN');
const useSWRMock = vi.mocked(useSWR);
let capturedKey: string | null = null;
useSWRMock.mockImplementation(((key: string) => {
capturedKey = key;
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
}) as any);
const params = { page: 2, category: 'tools' } as any;
renderHook(() => useStore.getState().usePluginList(params));
expect(capturedKey).toBe('plugin-list-zh-CN-2-tools');
});
```
### Testing SWR Configuration
```typescript
it('should have correct SWR configuration', () => {
const useSWRMock = vi.mocked(useSWR);
let capturedOptions: any = null;
useSWRMock.mockImplementation(((key: string, fetcher: any, options: any) => {
capturedOptions = options;
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
}) as any);
renderHook(() => useStore.getState().usePluginIdentifiers());
expect(capturedOptions).toMatchObject({ revalidateOnFocus: false });
});
```
### Testing Conditional Fetching
```typescript
it('should not fetch when required parameter is missing', () => {
const useSWRMock = vi.mocked(useSWR);
let capturedKey: string | null = null;
useSWRMock.mockImplementation(((key: string | null) => {
capturedKey = key;
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
}) as any);
// When identifier is undefined, SWR key should be null
renderHook(() => useStore.getState().usePluginDetail({ identifier: undefined }));
expect(capturedKey).toBeNull();
});
```
### Key Differences from Regular Action Tests
1. **Mock useSWR globally**: Use `vi.mock('swr')` at the top level
2. **Mock the fetcher, not the result**:
- ✅ **Correct**: `const data = fetcher?.()` - call fetcher and return its Promise
- ❌ **Wrong**: `return { data: mockData }` - hardcode the result
3. **Await Promise results**: The `data` field is a Promise, use `await result.current.data`
4. **No act() wrapper needed**: SWR hooks don't trigger React state updates in these tests
5. **Test SWR key generation**: Verify keys include locale and parameters
6. **Test configuration**: Verify revalidation and other SWR options
7. **Type assertions**: Use `as any` for test mock data where type definitions are strict
**Why this matters**:
- The fetcher (service method) is what we're testing - it must be called
- Hardcoding the return value bypasses the actual fetcher logic
- SWR returns Promises in real usage, tests should mirror this behavior
## Benefits of This Approach
✅ **Clear test layers** - Each test only spies on direct dependencies
✅ **Correct mocks** - Mocks match actual implementation
✅ **Better maintainability** - Changes to implementation require fewer test updates
✅ **Improved coverage** - Structured approach ensures all branches are tested
✅ **Reduced coupling** - Tests are independent and can run in any order
## Reference
See example implementation in:
- `src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts` (Regular actions)
- `src/store/discover/slices/plugin/action.test.ts` (SWR hooks)
- `src/store/discover/slices/mcp/action.test.ts` (SWR hooks)
+1 -2
View File
@@ -4,6 +4,5 @@ FEATURE_FLAGS=-check_updates,+pin_list
KEY_VAULTS_SECRET=oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=
DATABASE_URL=postgresql://postgres@localhost:5432/postgres
SEARCH_PROVIDERS=search1api
NEXT_PUBLIC_SERVICE_MODE='server'
NEXT_PUBLIC_IS_DESKTOP_APP=1
NEXT_PUBLIC_ENABLE_NEXT_AUTH=0
NEXT_PUBLIC_ENABLE_NEXT_AUTH=0
+14 -3
View File
@@ -13,6 +13,17 @@
# Default is '0' (enabled)
# ENABLED_CSP=1
# SSRF Protection Settings
# Set to '1' to allow connections to private IP addresses (disable SSRF protection)
# WARNING: Only enable this in trusted environments
# Default is '0' (SSRF protection enabled)
# SSRF_ALLOW_PRIVATE_IP_ADDRESS=0
# Whitelist of allowed private IP addresses (comma-separated)
# Only takes effect when SSRF_ALLOW_PRIVATE_IP_ADDRESS is '0'
# Example: Allow specific internal servers while keeping SSRF protection
# SSRF_ALLOW_IP_ADDRESS_LIST=192.168.1.100,10.0.0.50
########################################
########## AI Provider Service #########
########################################
@@ -256,6 +267,9 @@ OPENAI_API_KEY=sk-xxxxxxxxx
# you need to config the clerk webhook secret key if you want to use the clerk with database
#CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxx
# Clear allow origin https://clerk.com/docs/guides/dashboard/dns-domains/satellite-domains
# Authentication across different domains , use,to splite different origin
# NEXT_PUBLIC_CLERK_AUTH_ALLOW_ORIGINS='https://market.lobehub.com,https://lobehub.com'
# NextAuth related configurations
# NEXT_PUBLIC_ENABLE_NEXT_AUTH=1
@@ -270,9 +284,6 @@ OPENAI_API_KEY=sk-xxxxxxxxx
########## Server Database #############
########################################
# Specify the service mode as server if you want to use the server database
# NEXT_PUBLIC_SERVICE_MODE=server
# Postgres database URL
# DATABASE_URL=postgres://username:password@host:port/database
-2
View File
@@ -8,8 +8,6 @@ UNSAFE_SECRET="ww+0igxjGRAAR/eTNFQ55VmhQB5KE5trFZseuntThJs="
UNSAFE_PASSWORD="CHANGE_THIS_PASSWORD_IN_PRODUCTION"
# Core Server Configuration
# Service mode - set to 'server' for server-side deployment
NEXT_PUBLIC_SERVICE_MODE=server
# Service Ports Configuration
LOBE_PORT=3010
+67 -23
View File
@@ -5,34 +5,17 @@ type: Bug
body:
- type: dropdown
attributes:
label: '📦 Platform'
label: '📱 Client Type'
description: 'Select how you are accessing LobeChat'
multiple: true
options:
- 'Official Preview'
- 'Official Cloud'
- 'Vercel'
- 'Zeabur'
- 'Sealos'
- 'Netlify'
- 'Self hosting Docker'
- 'Web (Desktop Browser)'
- 'Web (Mobile Browser)'
- 'Desktop App (Electron)'
- 'Mobile App (React Native)'
- 'Other'
validations:
required: true
- type: dropdown
attributes:
label: '📦 Deploymenet mode'
multiple: true
options:
- 'client db (lobe-chat image)'
- 'client pgelite db (lobe-chat-pglite image)'
- 'server db(lobe-chat-database image)'
validations:
required: true
- type: input
attributes:
label: '📌 Version'
validations:
required: true
- type: dropdown
attributes:
@@ -48,6 +31,39 @@ body:
- 'Other'
validations:
required: true
- type: dropdown
attributes:
label: '📦 Deployment Platform'
multiple: true
options:
- 'Official Cloud'
- 'Vercel'
- 'Zeabur'
- 'Sealos'
- 'Netlify'
- 'Self hosting Docker'
- 'Other'
validations:
required: false
- type: dropdown
attributes:
label: '🔧 Deployment Mode'
multiple: true
options:
- 'client db (lobe-chat image)'
- 'client pgelite db (lobe-chat-pglite image)'
- 'server db (lobe-chat-database image)'
validations:
required: true
- type: input
attributes:
label: '📌 Version'
validations:
required: true
- type: dropdown
attributes:
label: '🌐 Browser'
@@ -60,21 +76,49 @@ body:
- 'Other'
validations:
required: true
- type: textarea
attributes:
label: '🐛 Bug Description'
description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
validations:
required: true
- type: textarea
attributes:
label: '📷 Recurrence Steps'
description: A clear and concise description of how to recurrence.
- type: textarea
attributes:
label: '🚦 Expected Behavior'
description: A clear and concise description of what you expected to happen.
- type: textarea
attributes:
label: '📝 Additional Information'
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
- type: dropdown
attributes:
label: '🛠️ Willing to Submit a PR?'
description: Would you be willing to submit a pull request to fix this bug?
options:
- 'Yes, I am willing to submit a PR'
- 'No, but I am happy to help test the fix'
validations:
required: false
- type: checkboxes
attributes:
label: '✅ Validations'
description: Before submitting the issue, please make sure you do the following
options:
- label: Read the [docs](https://lobehub.com/zh/docs).
required: true
- label: Check that there isn't [already an issue](https://github.com/lobehub/lobe-chat/issues) that reports the same bug to avoid creating a duplicate.
required: true
- label: Make sure this is a LobeChat issue and not a third-party library or provider issue.
required: true
- label: Check that this is a concrete bug. For Q&A, please use [GitHub Discussions](https://github.com/lobehub/lobe-chat/discussions) or join our [Discord Server](https://discord.gg/rGHwKq4R).
required: true
+26
View File
@@ -12,10 +12,36 @@
- [ ] 📝 docs
- [ ] 🔨 chore
#### 🔗 Related Issue
<!-- Link to the issue that is fixed by this PR -->
<!-- Example: Fixes #123, Closes #456, Related to #789 -->
#### 🔀 Description of Change
<!-- Thank you for your Pull Request. Please provide a description above. -->
#### 🧪 How to Test
<!-- Please describe how you tested your changes -->
<!-- For AI features, please include test prompts or scenarios -->
- [ ] Tested locally
- [ ] Added/updated tests
- [ ] No tests needed
#### 📸 Screenshots / Videos
<!-- If this PR includes UI changes, please provide screenshots or videos -->
| Before | After |
| ------ | ----- |
| ... | ... |
#### 📝 Additional Information
<!-- Add any other context about the Pull Request here. -->
<!-- Breaking changes? Migration guide? Performance impact? -->
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env bun
declare global {
// @ts-ignore
// eslint-disable-next-line no-var
var process: {
env: Record<string, string | undefined>;
};
}
interface GitHubIssue {
created_at: string;
number: number;
title: string;
user: { id: number };
}
interface GitHubComment {
body: string;
created_at: string;
id: number;
user: { id: number; type: string };
}
interface GitHubReaction {
content: string;
user: { id: number };
}
async function githubRequest<T>(
endpoint: string,
token: string,
method: string = 'GET',
body?: any,
): Promise<T> {
const response = await fetch(`https://api.github.com${endpoint}`, {
headers: {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `Bearer ${token}`,
'User-Agent': 'auto-close-duplicates-script',
...(body && { 'Content-Type': 'application/json' }),
},
method,
...(body && { body: JSON.stringify(body) }),
});
if (!response.ok) {
throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
function extractDuplicateIssueNumber(commentBody: string): number | null {
// Try to match #123 format first
let match = commentBody.match(/#(\d+)/);
if (match) {
return parseInt(match[1], 10);
}
// Try to match GitHub issue URL format: https://github.com/owner/repo/issues/123
match = commentBody.match(/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
if (match) {
return parseInt(match[1], 10);
}
return null;
}
async function closeIssueAsDuplicate(
owner: string,
repo: string,
issueNumber: number,
duplicateOfNumber: number,
token: string,
): Promise<void> {
await githubRequest(`/repos/${owner}/${repo}/issues/${issueNumber}`, token, 'PATCH', {
labels: ['duplicate'],
state: 'closed',
state_reason: 'duplicate',
});
await githubRequest(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, token, 'POST', {
body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
If this is incorrect, please re-open this issue or create a new one.
🤖 Generated with [Claude Code](https://claude.ai/code)`,
});
}
async function autoCloseDuplicates(): Promise<void> {
console.log('[DEBUG] Starting auto-close duplicates script');
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GITHUB_TOKEN environment variable is required');
}
console.log('[DEBUG] GitHub token found');
const owner = process.env.GITHUB_REPOSITORY_OWNER || 'lobehub';
const repo = process.env.GITHUB_REPOSITORY_NAME || 'lobe-chat';
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
const threeDaysAgo = new Date();
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
console.log(`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`);
console.log('[DEBUG] Fetching open issues created more than 3 days ago...');
const allIssues: GitHubIssue[] = [];
let page = 1;
const perPage = 100;
// eslint-disable-next-line no-constant-condition
while (true) {
const pageIssues: GitHubIssue[] = await githubRequest(
`/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}`,
token,
);
if (pageIssues.length === 0) break;
// Filter for issues created more than 3 days ago
const oldEnoughIssues = pageIssues.filter(
(issue) => new Date(issue.created_at) <= threeDaysAgo,
);
allIssues.push(...oldEnoughIssues);
page++;
// Safety limit to avoid infinite loops
if (page > 20) break;
}
const issues = allIssues;
console.log(`[DEBUG] Found ${issues.length} open issues`);
let processedCount = 0;
let candidateCount = 0;
for (const issue of issues) {
processedCount++;
console.log(
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`,
);
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
const comments: GitHubComment[] = await githubRequest(
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
token,
);
console.log(`[DEBUG] Issue #${issue.number} has ${comments.length} comments`);
const dupeComments = comments.filter(
(comment) =>
comment.body.includes('Found') &&
comment.body.includes('possible duplicate') &&
comment.user.type === 'Bot',
);
console.log(
`[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`,
);
if (dupeComments.length === 0) {
console.log(`[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`);
continue;
}
const lastDupeComment = dupeComments.at(-1);
// @ts-ignore
const dupeCommentDate = new Date(lastDupeComment.created_at);
console.log(
`[DEBUG] Issue #${
issue.number
} - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`,
);
if (dupeCommentDate > threeDaysAgo) {
console.log(`[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`);
continue;
}
console.log(
`[DEBUG] Issue #${issue.number} - duplicate comment is old enough (${Math.floor(
(Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24),
)} days)`,
);
const commentsAfterDupe = comments.filter(
(comment) => new Date(comment.created_at) > dupeCommentDate,
);
console.log(
`[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`,
);
if (commentsAfterDupe.length > 0) {
console.log(
`[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`,
);
continue;
}
console.log(`[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`);
const reactions: GitHubReaction[] = await githubRequest(
// @ts-ignore
`/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
token,
);
console.log(
`[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`,
);
const authorThumbsDown = reactions.some(
(reaction) => reaction.user.id === issue.user.id && reaction.content === '-1',
);
console.log(
`[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`,
);
if (authorThumbsDown) {
console.log(
`[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`,
);
continue;
}
// @ts-ignore
const duplicateIssueNumber = extractDuplicateIssueNumber(lastDupeComment.body);
if (!duplicateIssueNumber) {
console.log(
`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`,
);
continue;
}
candidateCount++;
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
try {
console.log(
`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`,
);
await closeIssueAsDuplicate(owner, repo, issue.number, duplicateIssueNumber, token);
console.log(
`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`,
);
} catch (error) {
console.error(`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`);
}
}
console.log(
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`,
);
}
// eslint-disable-next-line unicorn/prefer-top-level-await
autoCloseDuplicates().catch(console.error);
// Make it a module
export {};
+73
View File
@@ -0,0 +1,73 @@
name: Claude Auto Testing Coverage
description: Automatically add unit tests to improve code coverage
on:
schedule:
# Run daily at 05:30 UTC (13:30 Beijing Time)
- cron: '30 5 * * *'
workflow_dispatch:
inputs:
target_module:
description: 'Specific module to add tests (e.g., packages/database, src/services/user)'
required: false
type: string
concurrency:
group: auto-testing
cancel-in-progress: false
jobs:
add-tests:
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Configure Git
run: |
git config --global user.name "claude-bot[bot]"
git config --global user.email "claude-bot[bot]@users.noreply.github.com"
- name: Copy testing prompt
run: |
mkdir -p /tmp/claude-prompts
cp .claude/prompts/auto-testing.md /tmp/claude-prompts/
- name: Run Claude Code for Auto Testing
uses: anthropics/claude-code-action@main
with:
github_token: ${{ secrets.GH_TOKEN }}
allowed_non_write_users: "*"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--allowed-tools Bash,Read,Edit,Write,Glob,Grep"
prompt: |
Follow the auto testing guide located at:
```bash
cat /tmp/claude-prompts/auto-testing.md
```
## Task Assignment
${{ inputs.target_module && format('Process the specified module: {0}', inputs.target_module) || 'Automatically select one module from the target directories that needs test coverage' }}
## Environment Information
- Repository: ${{ github.repository }}
- Branch: ${{ github.ref_name }}
- Target Module: ${{ inputs.target_module || 'Auto-select' }}
- Run ID: ${{ github.run_id }}
**Start the auto testing process now.**
@@ -0,0 +1,33 @@
name: Claude Issue Dedupe
description: Automatically dedupe GitHub issues using Claude Code
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to process for duplicate detection'
required: true
type: string
jobs:
claude-dedupe-issues:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Run Claude Code slash command
uses: anthropics/claude-code-action@main
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: '/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}'
+65
View File
@@ -0,0 +1,65 @@
name: Claude Issue Triage
description: Automatically triage GitHub issues using Claude Code
on:
issues:
types: [opened, labeled]
jobs:
triage-issue:
runs-on: ubuntu-latest
timeout-minutes: 10
# Only run on issue opened, or when "trigger:triage" label is added
if: github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'trigger:triage')
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Copy triage prompts
run: |
mkdir -p /tmp/claude-prompts
cp .claude/prompts/team-assignment.md /tmp/claude-prompts/
cp .claude/prompts/issue-triage.md /tmp/claude-prompts/
- name: Run Claude Code for Issue Triage
uses: anthropics/claude-code-action@main
with:
github_token: ${{ secrets.GH_TOKEN }}
allowed_non_write_users: "*"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--allowed-tools Bash(gh *),Read"
prompt: |
You're an issue triage assistant for GitHub issues. Your task is to analyze issues, apply appropriate labels, and mention the responsible team member.
REPOSITORY: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
## Instructions
Follow the complete triage guide located at:
```bash
cat /tmp/claude-prompts/issue-triage.md
```
Read the team assignment guide for determining team members:
```bash
cat /tmp/claude-prompts/team-assignment.md
```
**IMPORTANT**:
- Follow ALL steps in the issue-triage.md guide
- Apply labels according to the guide's rules
- Post a mention comment to the appropriate team member(s) based on team-assignment.md
- Replace [ISSUE_NUMBER] with: ${{ github.event.issue.number }}
**Start the triage process now.**
- name: Remove trigger label
if: github.event.action == 'labeled' && github.event.label.name == 'trigger:triage'
run: |
gh issue edit ${{ github.event.issue.number }} --remove-label "trigger:triage"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
@@ -0,0 +1,67 @@
name: Claude Translate Non-English Comments
description: Automatically detect and translate non-English comments to English in codebase
on:
schedule:
# Run daily at 02:00 UTC (10:00 Beijing Time)
- cron: '0 2 * * *'
workflow_dispatch:
inputs:
target_module:
description: 'Specific module to translate (e.g., packages/database, apps/desktop/src/modules/auth)'
required: false
type: string
concurrency:
group: translate-comments
cancel-in-progress: false
jobs:
translate-comments:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Configure Git
run: |
git config --global user.name "claude-bot[bot]"
git config --global user.email "claude-bot[bot]@users.noreply.github.com"
- name: Copy translation prompt
run: |
mkdir -p /tmp/claude-prompts
cp .claude/prompts/translate-comments.md /tmp/claude-prompts/
- name: Run Claude Code for Comment Translation
uses: anthropics/claude-code-action@main
with:
github_token: ${{ secrets.GH_TOKEN }}
allowed_non_write_users: "*"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--allowed-tools Bash,Read,Edit,Glob,Grep"
prompt: |
Follow the translation guide located at:
```bash
cat /tmp/claude-prompts/translate-comments.md
```
## Task Assignment
${{ inputs.target_module && format('Process the specified module: {0}', inputs.target_module) || 'Automatically select one module from the target directories that has not been processed recently' }}
## Environment Information
- Repository: ${{ github.repository }}
- Branch: ${{ github.ref_name }}
- Target Module: ${{ inputs.target_module || 'Auto-select' }}
- Run ID: ${{ github.run_id }}
**Start the translation process now.**
+1
View File
@@ -21,6 +21,7 @@ jobs:
(github.event_name == 'pull_request_review' && github.event.sender.type != 'Bot') ||
(github.event_name == 'pull_request_review_comment' && github.event.sender.type != 'Bot')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
# update issues/comments
+21 -19
View File
@@ -1,16 +1,18 @@
name: Desktop PR Build
on:
pull_request:
pull_request_target:
types: [synchronize, labeled, unlabeled] # PR 更新或标签变化时触发
# 确保同一时间只运行一个相同的 workflow,取消正在进行的旧的运行
# 确保同一 PR 同一时间只运行一个相同的 workflow,取消正在进行的旧的运行
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
group: pr-${{ github.event.pull_request.number }}-${{ github.workflow }}
cancel-in-progress: true
# Add default permissions
permissions: read-all
permissions:
contents: write
pull-requests: write
env:
PR_TAG_PREFIX: pr- # PR 构建版本的前缀标识
@@ -18,8 +20,8 @@ env:
jobs:
test:
name: Code quality check
# 添加 PR label 触发条件,只有添加了 Build Desktop 标签的 PR 才会触发构建
if: contains(github.event.pull_request.labels.*.name, 'Build Desktop')
# 添加 PR label 触发条件,只有添加了 trigger:build-desktop 标签的 PR 才会触发构建
if: contains(github.event.pull_request.labels.*.name, 'trigger:build-desktop')
runs-on: ubuntu-latest # 只在 ubuntu 上运行一次检查
steps:
- name: Checkout base
@@ -28,9 +30,9 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
- name: Install bun
@@ -51,7 +53,7 @@ jobs:
version:
name: Determine version
# 与 test job 相同的触发条件
if: contains(github.event.pull_request.labels.*.name, 'Build Desktop')
if: contains(github.event.pull_request.labels.*.name, 'trigger:build-desktop')
runs-on: ubuntu-latest
outputs:
# 输出版本信息,供后续 job 使用
@@ -62,9 +64,9 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
# 主要逻辑:确定构建版本号
@@ -107,9 +109,9 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
# node-linker=hoisted 模式将可以确保 asar 压缩可用
@@ -199,7 +201,7 @@ jobs:
# 上传构建产物
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: release-${{ matrix.os }}
path: |
@@ -226,9 +228,9 @@ jobs:
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
- name: Install bun
@@ -238,7 +240,7 @@ jobs:
# 下载所有平台的构建产物
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
path: release
pattern: release-*
@@ -264,7 +266,7 @@ jobs:
# 上传合并后的构建产物
- name: Upload artifacts with merged macOS files
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: merged-release-pr
path: release/
@@ -287,7 +289,7 @@ jobs:
# 下载合并后的构建产物
- name: Download merged artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: merged-release-pr
path: release
-181
View File
@@ -1,181 +0,0 @@
name: Publish Database Docker Image
permissions:
contents: read
on:
workflow_dispatch:
release:
types: [published]
pull_request:
types: [synchronize, labeled, unlabeled]
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
env:
REGISTRY_IMAGE: lobehub/lobe-chat-database
PR_TAG_PREFIX: pr-
jobs:
build:
# 添加 PR label 触发条件
if: |
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'Build Docker')) ||
github.event_name != 'pull_request'
strategy:
matrix:
include:
- platform: linux/amd64
os: ubuntu-latest
- platform: linux/arm64
os: ubuntu-24.04-arm
runs-on: ${{ matrix.os }}
name: Build ${{ matrix.platform }} Image
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout base
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 为 PR 生成特殊的 tag
- name: Generate PR metadata
if: github.event_name == 'pull_request'
id: pr_meta
run: |
branch_name="${{ github.head_ref }}"
sanitized_branch=$(echo "${branch_name}" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g')
echo "pr_tag=${sanitized_branch}-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
# PR 构建使用特殊的 tag
type=raw,value=${{ env.PR_TAG_PREFIX }}${{ steps.pr_meta.outputs.pr_tag }},enable=${{ github.event_name == 'pull_request' }}
# release 构建使用版本号
type=semver,pattern={{version}},enable=${{ github.event_name != 'pull_request' }}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
- name: Docker login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_REGISTRY_USER }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Get commit SHA
if: github.ref == 'refs/heads/main'
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Build and export
id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
context: .
file: ./Dockerfile.database
labels: ${{ steps.meta.outputs.labels }}
build-args: |
SHA=${{ steps.vars.outputs.sha_short }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
rm -rf /tmp/digests
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: digest-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
name: Merge
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout base
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 为 merge job 添加 PR metadata 生成
- name: Generate PR metadata
if: github.event_name == 'pull_request'
id: pr_meta
run: |
branch_name="${{ github.head_ref }}"
sanitized_branch=$(echo "${branch_name}" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g')
echo "pr_tag=${sanitized_branch}-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=raw,value=${{ env.PR_TAG_PREFIX }}${{ steps.pr_meta.outputs.pr_tag }},enable=${{ github.event_name == 'pull_request' }}
type=semver,pattern={{version}},enable=${{ github.event_name != 'pull_request' }}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
- name: Docker login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_REGISTRY_USER }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
- name: Comment on PR with Docker build info
if: github.event_name == 'pull_request'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prComment = require('${{ github.workspace }}/.github/scripts/docker-pr-comment.js');
const result = await prComment({
github,
context,
dockerMetaJson: ${{ toJSON(steps.meta.outputs.json) }},
image: "${{ env.REGISTRY_IMAGE }}",
version: "${{ steps.meta.outputs.version }}",
dockerhubUrl: "https://hub.docker.com/r/${{ env.REGISTRY_IMAGE }}/tags",
platforms: "linux/amd64, linux/arm64",
});
core.info(`Status: ${result.updated ? 'Updated' : 'Created'}, ID: ${result.id}`);
-163
View File
@@ -1,163 +0,0 @@
name: Publish Docker Pglite Image
permissions:
contents: read
on:
workflow_dispatch:
release:
types: [published]
pull_request:
types: [synchronize, labeled, unlabeled]
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
env:
REGISTRY_IMAGE: lobehub/lobe-chat-pglite
PR_TAG_PREFIX: pr-
jobs:
build:
# 添加 PR label 触发条件
if: |
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'Build Docker')) ||
github.event_name != 'pull_request'
strategy:
matrix:
include:
- platform: linux/amd64
os: ubuntu-latest
- platform: linux/arm64
os: ubuntu-24.04-arm
runs-on: ${{ matrix.os }}
name: Build ${{ matrix.platform }} Image
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout base
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 为 PR 生成特殊的 tag
- name: Generate PR metadata
if: github.event_name == 'pull_request'
id: pr_meta
run: |
branch_name="${{ github.head_ref }}"
sanitized_branch=$(echo "${branch_name}" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g')
echo "pr_tag=${sanitized_branch}-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
# PR 构建使用特殊的 tag
type=raw,value=${{ env.PR_TAG_PREFIX }}${{ steps.pr_meta.outputs.pr_tag }},enable=${{ github.event_name == 'pull_request' }}
# release 构建使用版本号
type=semver,pattern={{version}},enable=${{ github.event_name != 'pull_request' }}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
- name: Docker login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_REGISTRY_USER }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Get commit SHA
if: github.ref == 'refs/heads/main'
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Build and export
id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
context: .
file: ./Dockerfile.pglite
labels: ${{ steps.meta.outputs.labels }}
build-args: |
SHA=${{ steps.vars.outputs.sha_short }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
rm -rf /tmp/digests
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: digest-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
name: Merge
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout base
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 为 merge job 添加 PR metadata 生成
- name: Generate PR metadata
if: github.event_name == 'pull_request'
id: pr_meta
run: |
branch_name="${{ github.head_ref }}"
sanitized_branch=$(echo "${branch_name}" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g')
echo "pr_tag=${sanitized_branch}-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=raw,value=${{ env.PR_TAG_PREFIX }}${{ steps.pr_meta.outputs.pr_tag }},enable=${{ github.event_name == 'pull_request' }}
type=semver,pattern={{version}},enable=${{ github.event_name != 'pull_request' }}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
- name: Docker login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_REGISTRY_USER }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
+43 -20
View File
@@ -1,29 +1,32 @@
name: Publish Docker Image
permissions:
contents: read
pull-requests: write
on:
workflow_dispatch:
release:
types: [published]
pull_request:
pull_request_target:
types: [synchronize, labeled, unlabeled]
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
# PR 构建时取消旧的运行,但 release 构建不取消
cancel-in-progress: ${{ github.event_name != 'release' }}
env:
REGISTRY_IMAGE: lobehub/lobe-chat
REGISTRY_IMAGE: lobehub/lobehub
PR_TAG_PREFIX: pr-
jobs:
build:
# 添加 PR label 触发条件
if: |
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'Build Docker')) ||
github.event_name != 'pull_request'
github.event_name == 'release' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request_target' &&
contains(github.event.pull_request.labels.*.name, 'trigger:build-docker'))
strategy:
matrix:
@@ -50,11 +53,12 @@ jobs:
# 为 PR 生成特殊的 tag
- name: Generate PR metadata
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
id: pr_meta
env:
BRANCH_NAME: ${{ github.head_ref }}
run: |
branch_name="${{ github.head_ref }}"
sanitized_branch=$(echo "${branch_name}" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g')
sanitized_branch=$(echo "${BRANCH_NAME}" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g')
echo "pr_tag=${sanitized_branch}-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Docker meta
@@ -64,10 +68,10 @@ jobs:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
# PR 构建使用特殊的 tag
type=raw,value=${{ env.PR_TAG_PREFIX }}${{ steps.pr_meta.outputs.pr_tag }},enable=${{ github.event_name == 'pull_request' }}
type=raw,value=${{ env.PR_TAG_PREFIX }}${{ steps.pr_meta.outputs.pr_tag }},enable=${{ github.event_name == 'pull_request_target' }}
# release 构建使用版本号
type=semver,pattern={{version}},enable=${{ github.event_name != 'pull_request' }}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
type=semver,pattern={{version}},enable=${{ github.event_name != 'pull_request_target' }}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request_target' }}
- name: Docker login
uses: docker/login-action@v3
@@ -100,7 +104,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: digest-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -118,7 +122,7 @@ jobs:
fetch-depth: 0
- name: Download digests
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
path: /tmp/digests
pattern: digest-*
@@ -129,11 +133,12 @@ jobs:
# 为 merge job 添加 PR metadata 生成
- name: Generate PR metadata
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
id: pr_meta
env:
BRANCH_NAME: ${{ github.head_ref }}
run: |
branch_name="${{ github.head_ref }}"
sanitized_branch=$(echo "${branch_name}" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g')
sanitized_branch=$(echo "${BRANCH_NAME}" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g')
echo "pr_tag=${sanitized_branch}-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Docker meta
@@ -142,9 +147,9 @@ jobs:
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=raw,value=${{ env.PR_TAG_PREFIX }}${{ steps.pr_meta.outputs.pr_tag }},enable=${{ github.event_name == 'pull_request' }}
type=semver,pattern={{version}},enable=${{ github.event_name != 'pull_request' }}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
type=raw,value=${{ env.PR_TAG_PREFIX }}${{ steps.pr_meta.outputs.pr_tag }},enable=${{ github.event_name == 'pull_request_target' }}
type=semver,pattern={{version}},enable=${{ github.event_name != 'pull_request_target' }}
type=raw,value=latest,enable=${{ github.event_name != 'pull_request_target' }}
- name: Docker login
uses: docker/login-action@v3
@@ -161,3 +166,21 @@ jobs:
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
- name: Comment on PR with Docker build info
if: github.event_name == 'pull_request_target'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prComment = require('${{ github.workspace }}/.github/scripts/docker-pr-comment.js');
const result = await prComment({
github,
context,
dockerMetaJson: ${{ toJSON(steps.meta.outputs.json) }},
image: "${{ env.REGISTRY_IMAGE }}",
version: "${{ steps.meta.outputs.version }}",
dockerhubUrl: "https://hub.docker.com/r/${{ env.REGISTRY_IMAGE }}/tags",
platforms: "linux/amd64, linux/arm64",
});
core.info(`Status: ${result.updated ? 'Updated' : 'Created'}, ID: ${result.id}`);
+66
View File
@@ -0,0 +1,66 @@
name: E2E CI
permissions:
contents: read
on:
pull_request:
push:
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e:
name: Test Web App
runs-on: ubuntu-latest
services:
postgres:
image: paradedb/paradedb:latest
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 5432:5432
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.23
- name: Install dependencies (bun)
run: bun install
- name: Install Playwright browsers (with system deps)
run: bunx playwright install --with-deps chromium
- name: Run E2E tests
env:
PORT: 3010
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
DATABASE_DRIVER: node
KEY_VAULTS_SECRET: LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=
run: bun run e2e
- name: Upload Cucumber HTML report (on failure)
if: failure()
uses: actions/upload-artifact@v5
with:
name: cucumber-report
path: e2e/reports
if-no-files-found: ignore
- name: Upload screenshots (on failure)
if: failure()
uses: actions/upload-artifact@v5
with:
name: test-screenshots
path: e2e/screenshots
if-no-files-found: ignore
@@ -0,0 +1,30 @@
name: Auto-close duplicate issues
description: Auto-closes issues that are duplicates of existing issues
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
jobs:
auto-close-duplicates:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Auto-close duplicate issues
run: bun run .github/scripts/auto-close-duplicates.ts
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
+13 -13
View File
@@ -15,7 +15,7 @@ permissions: read-all
jobs:
test:
name: Code quality check
# 添加 PR label 触发条件,只有添加了 Build Desktop 标签的 PR 才会触发构建
# 添加 PR label 触发条件,只有添加了 trigger:build-desktop 标签的 PR 才会触发构建
runs-on: ubuntu-latest # 只在 ubuntu 上运行一次检查
steps:
- name: Checkout base
@@ -24,9 +24,9 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
- name: Install bun
@@ -53,9 +53,9 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
# 主要逻辑:确定构建版本号
@@ -94,9 +94,9 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
# node-linker=hoisted 模式将可以确保 asar 压缩可用
@@ -181,7 +181,7 @@ jobs:
# 上传构建产物 (工作流处理重命名,不依赖 electron-builder 钩子)
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: release-${{ matrix.os }}
path: |
@@ -208,9 +208,9 @@ jobs:
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
- name: Install bun
@@ -220,7 +220,7 @@ jobs:
# 下载所有平台的构建产物
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
path: release
pattern: release-*
@@ -246,7 +246,7 @@ jobs:
# 上传合并后的构建产物
- name: Upload artifacts with merged macOS files
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: merged-release
path: release/
@@ -262,7 +262,7 @@ jobs:
steps:
# 下载合并后的构建产物
- name: Download merged artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v6
with:
name: merged-release
path: release
+4 -5
View File
@@ -9,6 +9,7 @@ on:
push:
branches:
- main
- next
jobs:
release:
@@ -23,7 +24,6 @@ jobs:
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 5432:5432
@@ -33,9 +33,9 @@ jobs:
token: ${{ secrets.GH_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
- name: Install bun
@@ -54,8 +54,7 @@ jobs:
env:
DATABASE_TEST_URL: postgresql://postgres:postgres@localhost:5432/postgres
DATABASE_DRIVER: node
NEXT_PUBLIC_SERVICE_MODE: server
KEY_VAULTS_SECRET: LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=
KEY_VAULTS_SECRET: Kix2wcUONd4CX51E/ZPAd36BqM4wzJgKjPtz2sGztqQ=
S3_PUBLIC_DOMAIN: https://example.com
APP_URL: https://home.com
+2 -2
View File
@@ -50,5 +50,5 @@ jobs:
![](https://github-production-user-asset-6210df.s3.amazonaws.com/17870709/273954625-df80c890-0822-4ac2-95e6-c990785cbed5.png)
[lobechat]: https://github.com/lobehub/lobe-chat
[tutorial-zh-CN]: https://github.com/lobehub/lobe-chat/wiki/Upstream-Sync.zh-CN
[tutorial-en-US]: https://github.com/lobehub/lobe-chat/wiki/Upstream-Sync
[tutorial-zh-CN]: https://lobehub.com/zh/docs/self-hosting/advanced/upstream-sync
[tutorial-en-US]: https://lobehub.com/docs/self-hosting/advanced/upstream-sync
+54 -20
View File
@@ -21,6 +21,7 @@ jobs:
- python-interpreter
- context-engine
- agent-runtime
- conversation-flow
name: Test package ${{ matrix.package }}
@@ -28,9 +29,9 @@ jobs:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
- name: Install bun
@@ -63,9 +64,9 @@ jobs:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
- name: Install bun
@@ -96,9 +97,9 @@ jobs:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
- name: Install bun
@@ -119,6 +120,43 @@ jobs:
files: ./coverage/app/lcov.info
flags: app
test-desktop:
name: Test Desktop App
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
package-manager-cache: false
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Install deps
run: pnpm install
working-directory: apps/desktop
env:
NODE_OPTIONS: --max-old-space-size=6144
- name: Test Desktop Client
run: pnpm test
working-directory: apps/desktop
- name: Upload Desktop App Coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./apps/desktop/coverage/lcov.info
flags: desktop
test-databsae:
name: Test Database
@@ -132,7 +170,6 @@ jobs:
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 5432:5432
@@ -140,36 +177,33 @@ jobs:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
package-manager-cache: false
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.23
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install deps
run: bun i
run: pnpm i
- name: Lint
run: bun run lint
run: npm run lint
- name: Test Client DB
run: bun run --filter @lobechat/database test:client-db
run: pnpm --filter @lobechat/database test:client-db
env:
KEY_VAULTS_SECRET: LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=
KEY_VAULTS_SECRET: Kix2wcUONd4CX51E/ZPAd36BqM4wzJgKjPtz2sGztqQ=
S3_PUBLIC_DOMAIN: https://example.com
APP_URL: https://home.com
- name: Test Coverage
run: bun run --filter @lobechat/database test:coverage
run: pnpm --filter @lobechat/database test:coverage
env:
DATABASE_TEST_URL: postgresql://postgres:postgres@localhost:5432/postgres
DATABASE_DRIVER: node
NEXT_PUBLIC_SERVICE_MODE: server
KEY_VAULTS_SECRET: LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=
KEY_VAULTS_SECRET: Kix2wcUONd4CX51E/ZPAd36BqM4wzJgKjPtz2sGztqQ=
S3_PUBLIC_DOMAIN: https://example.com
APP_URL: https://home.com
+1
View File
@@ -117,3 +117,4 @@ CLAUDE.local.md
prd
GEMINI.md
e2e/reports
-2
View File
@@ -4,8 +4,6 @@ resolution-mode=highest
ignore-workspace-root-check=true
enable-pre-post-scripts=true
# Load dotenv files for all the npm scripts
node-options="--require dotenv-expand/config"
public-hoist-pattern[]=*@umijs/lint*
public-hoist-pattern[]=*changelog*
+1 -1
View File
@@ -1 +1 @@
lts/jod
lts/Krypton
+8
View File
@@ -1,5 +1,13 @@
const config = require('@lobehub/lint').semanticRelease;
config.branches = [
'main',
{
name: 'next',
prerelease: true,
},
];
config.plugins.push([
'@semantic-release/exec',
{
+2895
View File
File diff suppressed because it is too large Load Diff
+65 -8
View File
@@ -37,6 +37,9 @@ FROM base AS builder
ARG USE_CN_MIRROR
ARG NEXT_PUBLIC_BASE_PATH
ARG NEXT_PUBLIC_ENABLE_NEXT_AUTH
ARG NEXT_PUBLIC_ENABLE_CLERK_AUTH
ARG NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
ARG NEXT_PUBLIC_SENTRY_DSN
ARG NEXT_PUBLIC_ANALYTICS_POSTHOG
ARG NEXT_PUBLIC_POSTHOG_HOST
@@ -48,13 +51,21 @@ ARG FEATURE_FLAGS
ENV NEXT_PUBLIC_BASE_PATH="${NEXT_PUBLIC_BASE_PATH}" \
FEATURE_FLAGS="${FEATURE_FLAGS}"
ENV NEXT_PUBLIC_ENABLE_NEXT_AUTH="${NEXT_PUBLIC_ENABLE_NEXT_AUTH:-1}" \
NEXT_PUBLIC_ENABLE_CLERK_AUTH="${NEXT_PUBLIC_ENABLE_CLERK_AUTH:-0}" \
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}" \
CLERK_WEBHOOK_SECRET="whsec_xxx" \
APP_URL="http://app.com" \
DATABASE_DRIVER="node" \
DATABASE_URL="postgres://postgres:password@localhost:5432/postgres" \
KEY_VAULTS_SECRET="use-for-build"
# Sentry
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}" \
@@ -90,7 +101,12 @@ RUN \
# Use pnpm for corepack
&& corepack use $(sed -n 's/.*"packageManager": "\(.*\)".*/\1/p' package.json) \
# Install the dependencies
&& pnpm i
&& pnpm i \
# Add db migration dependencies
&& mkdir -p /deps \
&& cd /deps \
&& pnpm init \
&& pnpm add pg drizzle-orm
COPY . .
@@ -106,6 +122,16 @@ COPY --from=base /distroless/ /
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder /app/.next/standalone /app/
# Copy database migrations
COPY --from=builder /app/packages/database/migrations /app/migrations
COPY --from=builder /app/scripts/migrateServerDB/docker.cjs /app/docker.cjs
COPY --from=builder /app/scripts/migrateServerDB/errorHint.js /app/errorHint.js
# copy dependencies
COPY --from=builder /deps/node_modules/.pnpm /app/node_modules/.pnpm
COPY --from=builder /deps/node_modules/pg /app/node_modules/pg
COPY --from=builder /deps/node_modules/drizzle-orm /app/node_modules/drizzle-orm
# Copy server launcher
COPY --from=builder /app/scripts/serverLauncher/startServer.js /app/startServer.js
@@ -138,11 +164,37 @@ ENV HOSTNAME="0.0.0.0" \
# General Variables
ENV ACCESS_CODE="" \
APP_URL="" \
API_KEY_SELECT_MODE="" \
DEFAULT_AGENT_CONFIG="" \
SYSTEM_AGENT="" \
FEATURE_FLAGS="" \
PROXY_URL=""
PROXY_URL="" \
ENABLE_AUTH_PROTECTION=""
# Database
ENV KEY_VAULTS_SECRET="" \
DATABASE_DRIVER="node" \
DATABASE_URL=""
# Next Auth
ENV NEXT_AUTH_SECRET="" \
NEXT_AUTH_SSO_PROVIDERS="" \
NEXTAUTH_URL=""
# Clerk
ENV CLERK_SECRET_KEY="" \
CLERK_WEBHOOK_SECRET=""
# S3
ENV NEXT_PUBLIC_S3_DOMAIN="" \
S3_PUBLIC_DOMAIN="" \
S3_ACCESS_KEY_ID="" \
S3_BUCKET="" \
S3_ENDPOINT="" \
S3_SECRET_ACCESS_KEY="" \
S3_ENABLE_PATH_STYLE="" \
S3_SET_ACL=""
# Model Variables
ENV \
@@ -155,7 +207,7 @@ ENV \
# Anthropic
ANTHROPIC_API_KEY="" ANTHROPIC_MODEL_LIST="" ANTHROPIC_PROXY_URL="" \
# Amazon Bedrock
AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
ENABLED_AWS_BEDROCK="" AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
# Azure OpenAI
AZURE_API_KEY="" AZURE_API_VERSION="" AZURE_ENDPOINT="" AZURE_MODEL_LIST="" \
# Baichuan
@@ -164,6 +216,9 @@ ENV \
CLOUDFLARE_API_KEY="" CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID="" CLOUDFLARE_MODEL_LIST="" \
# Cohere
COHERE_API_KEY="" COHERE_MODEL_LIST="" COHERE_PROXY_URL="" \
# ComfyUI
ENABLED_COMFYUI="" COMFYUI_BASE_URL="" COMFYUI_AUTH_TYPE="" \
COMFYUI_API_KEY="" COMFYUI_USERNAME="" COMFYUI_PASSWORD="" COMFYUI_CUSTOM_HEADERS="" \
# DeepSeek
DEEPSEEK_API_KEY="" DEEPSEEK_MODEL_LIST="" \
# Fireworks AI
@@ -205,7 +260,7 @@ ENV \
# Ollama
ENABLED_OLLAMA="" OLLAMA_MODEL_LIST="" OLLAMA_PROXY_URL="" \
# OpenAI
OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
ENABLED_OPENAI="" OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
# OpenRouter
OPENROUTER_API_KEY="" OPENROUTER_MODEL_LIST="" \
# Perplexity
@@ -255,11 +310,13 @@ ENV \
# 302.AI
AI302_API_KEY="" AI302_MODEL_LIST="" \
# FAL
FAL_API_KEY="" FAL_MODEL_LIST="" \
ENABLED_FAL="" FAL_API_KEY="" FAL_MODEL_LIST="" \
# BFL
BFL_API_KEY="" BFL_MODEL_LIST="" \
# Vercel AI Gateway
VERCELAIGATEWAY_API_KEY="" VERCELAIGATEWAY_MODEL_LIST=""
VERCELAIGATEWAY_API_KEY="" VERCELAIGATEWAY_MODEL_LIST="" \
# Cerebras
CEREBRAS_API_KEY="" CEREBRAS_MODEL_LIST=""
USER nextjs
-314
View File
@@ -1,314 +0,0 @@
## Set global build ENV
ARG NODEJS_VERSION="24"
## Base image for all building stages
FROM node:${NODEJS_VERSION}-slim AS base
ARG USE_CN_MIRROR
ENV DEBIAN_FRONTEND="noninteractive"
RUN \
# If you want to build docker in China, build with --build-arg USE_CN_MIRROR=true
if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \
sed -i "s/deb.debian.org/mirrors.ustc.edu.cn/g" "/etc/apt/sources.list.d/debian.sources"; \
fi \
# Add required package
&& apt update \
&& apt install ca-certificates proxychains-ng -qy \
# Prepare required package to distroless
&& mkdir -p /distroless/bin /distroless/etc /distroless/etc/ssl/certs /distroless/lib \
# Copy proxychains to distroless
&& cp /usr/lib/$(arch)-linux-gnu/libproxychains.so.4 /distroless/lib/libproxychains.so.4 \
&& cp /usr/lib/$(arch)-linux-gnu/libdl.so.2 /distroless/lib/libdl.so.2 \
&& cp /usr/bin/proxychains4 /distroless/bin/proxychains \
&& cp /etc/proxychains4.conf /distroless/etc/proxychains4.conf \
# Copy node to distroless
&& cp /usr/lib/$(arch)-linux-gnu/libstdc++.so.6 /distroless/lib/libstdc++.so.6 \
&& cp /usr/lib/$(arch)-linux-gnu/libgcc_s.so.1 /distroless/lib/libgcc_s.so.1 \
&& cp /usr/local/bin/node /distroless/bin/node \
# Copy CA certificates to distroless
&& cp /etc/ssl/certs/ca-certificates.crt /distroless/etc/ssl/certs/ca-certificates.crt \
# Cleanup temp files
&& rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/*
## Builder image, install all the dependencies and build the app
FROM base AS builder
ARG USE_CN_MIRROR
ARG NEXT_PUBLIC_BASE_PATH
ARG NEXT_PUBLIC_SERVICE_MODE
ARG NEXT_PUBLIC_ENABLE_NEXT_AUTH
ARG NEXT_PUBLIC_SENTRY_DSN
ARG NEXT_PUBLIC_ANALYTICS_POSTHOG
ARG NEXT_PUBLIC_POSTHOG_HOST
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_ANALYTICS_UMAMI
ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ARG FEATURE_FLAGS
ENV NEXT_PUBLIC_BASE_PATH="${NEXT_PUBLIC_BASE_PATH}" \
FEATURE_FLAGS="${FEATURE_FLAGS}"
ENV NEXT_PUBLIC_SERVICE_MODE="${NEXT_PUBLIC_SERVICE_MODE:-server}" \
NEXT_PUBLIC_ENABLE_NEXT_AUTH="${NEXT_PUBLIC_ENABLE_NEXT_AUTH:-1}" \
APP_URL="http://app.com" \
DATABASE_DRIVER="node" \
DATABASE_URL="postgres://postgres:password@localhost:5432/postgres" \
KEY_VAULTS_SECRET="use-for-build"
# Sentry
ENV NEXT_PUBLIC_SENTRY_DSN="${NEXT_PUBLIC_SENTRY_DSN}" \
SENTRY_ORG="" \
SENTRY_PROJECT=""
# Posthog
ENV NEXT_PUBLIC_ANALYTICS_POSTHOG="${NEXT_PUBLIC_ANALYTICS_POSTHOG}" \
NEXT_PUBLIC_POSTHOG_HOST="${NEXT_PUBLIC_POSTHOG_HOST}" \
NEXT_PUBLIC_POSTHOG_KEY="${NEXT_PUBLIC_POSTHOG_KEY}"
# Umami
ENV NEXT_PUBLIC_ANALYTICS_UMAMI="${NEXT_PUBLIC_ANALYTICS_UMAMI}" \
NEXT_PUBLIC_UMAMI_SCRIPT_URL="${NEXT_PUBLIC_UMAMI_SCRIPT_URL}" \
NEXT_PUBLIC_UMAMI_WEBSITE_ID="${NEXT_PUBLIC_UMAMI_WEBSITE_ID}"
# Node
ENV NODE_OPTIONS="--max-old-space-size=6144"
WORKDIR /app
COPY package.json pnpm-workspace.yaml ./
COPY .npmrc ./
COPY packages ./packages
RUN \
# If you want to build docker in China, build with --build-arg USE_CN_MIRROR=true
if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \
export SENTRYCLI_CDNURL="https://npmmirror.com/mirrors/sentry-cli"; \
npm config set registry "https://registry.npmmirror.com/"; \
echo 'canvas_binary_host_mirror=https://npmmirror.com/mirrors/canvas' >> .npmrc; \
fi \
# Set the registry for corepack
&& export COREPACK_NPM_REGISTRY=$(npm config get registry | sed 's/\/$//') \
# Update corepack to latest (nodejs/corepack#612)
&& npm i -g corepack@latest \
# Enable corepack
&& corepack enable \
# Use pnpm for corepack
&& corepack use $(sed -n 's/.*"packageManager": "\(.*\)".*/\1/p' package.json) \
# Install the dependencies
&& pnpm i \
# Add db migration dependencies
&& mkdir -p /deps \
&& cd /deps \
&& pnpm init \
&& pnpm add pg drizzle-orm
COPY . .
# run build standalone for docker version
RUN npm run build:docker
## Application image, copy all the files for production
FROM busybox:latest AS app
COPY --from=base /distroless/ /
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder /app/.next/standalone /app/
# Copy database migrations
COPY --from=builder /app/packages/database/migrations /app/migrations
COPY --from=builder /app/scripts/migrateServerDB/docker.cjs /app/docker.cjs
COPY --from=builder /app/scripts/migrateServerDB/errorHint.js /app/errorHint.js
# copy dependencies
COPY --from=builder /deps/node_modules/.pnpm /app/node_modules/.pnpm
COPY --from=builder /deps/node_modules/pg /app/node_modules/pg
COPY --from=builder /deps/node_modules/drizzle-orm /app/node_modules/drizzle-orm
# Copy server launcher
COPY --from=builder /app/scripts/serverLauncher/startServer.js /app/startServer.js
RUN \
# Add nextjs:nodejs to run the app
addgroup -S -g 1001 nodejs \
&& adduser -D -G nodejs -H -S -h /app -u 1001 nextjs \
# Set permission for nextjs:nodejs
&& chown -R nextjs:nodejs /app /etc/proxychains4.conf
## Production image, copy all the files and run next
FROM scratch
# Copy all the files from app, set the correct permission for prerender cache
COPY --from=app / /
ENV NODE_ENV="production" \
NODE_OPTIONS="--dns-result-order=ipv4first --use-openssl-ca" \
NODE_EXTRA_CA_CERTS="" \
NODE_TLS_REJECT_UNAUTHORIZED="" \
SSL_CERT_FILE="/etc/ssl/certs/ca-certificates.crt"
# Make the middleware rewrite through local as default
# refs: https://github.com/lobehub/lobe-chat/issues/5876
ENV MIDDLEWARE_REWRITE_THROUGH_LOCAL="1"
# set hostname to localhost
ENV HOSTNAME="0.0.0.0" \
PORT="3210"
# General Variables
ENV ACCESS_CODE="" \
APP_URL="" \
API_KEY_SELECT_MODE="" \
DEFAULT_AGENT_CONFIG="" \
SYSTEM_AGENT="" \
FEATURE_FLAGS="" \
PROXY_URL=""
# Database
ENV KEY_VAULTS_SECRET="" \
DATABASE_DRIVER="node" \
DATABASE_URL=""
# Next Auth
ENV NEXT_AUTH_SECRET="" \
NEXT_AUTH_SSO_PROVIDERS="" \
NEXTAUTH_URL=""
# S3
ENV NEXT_PUBLIC_S3_DOMAIN="" \
S3_PUBLIC_DOMAIN="" \
S3_ACCESS_KEY_ID="" \
S3_BUCKET="" \
S3_ENDPOINT="" \
S3_SECRET_ACCESS_KEY=""
# Model Variables
ENV \
# AI21
AI21_API_KEY="" AI21_MODEL_LIST="" \
# Ai360
AI360_API_KEY="" AI360_MODEL_LIST="" \
# AiHubMix
AIHUBMIX_API_KEY="" AIHUBMIX_MODEL_LIST="" \
# Anthropic
ANTHROPIC_API_KEY="" ANTHROPIC_MODEL_LIST="" ANTHROPIC_PROXY_URL="" \
# Amazon Bedrock
AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
# Azure OpenAI
AZURE_API_KEY="" AZURE_API_VERSION="" AZURE_ENDPOINT="" AZURE_MODEL_LIST="" \
# Baichuan
BAICHUAN_API_KEY="" BAICHUAN_MODEL_LIST="" \
# Cloudflare
CLOUDFLARE_API_KEY="" CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID="" CLOUDFLARE_MODEL_LIST="" \
# Cohere
COHERE_API_KEY="" COHERE_MODEL_LIST="" COHERE_PROXY_URL="" \
# DeepSeek
DEEPSEEK_API_KEY="" DEEPSEEK_MODEL_LIST="" \
# Fireworks AI
FIREWORKSAI_API_KEY="" FIREWORKSAI_MODEL_LIST="" \
# Gitee AI
GITEE_AI_API_KEY="" GITEE_AI_MODEL_LIST="" \
# GitHub
GITHUB_TOKEN="" GITHUB_MODEL_LIST="" \
# Google
GOOGLE_API_KEY="" GOOGLE_MODEL_LIST="" GOOGLE_PROXY_URL="" \
# Groq
GROQ_API_KEY="" GROQ_MODEL_LIST="" GROQ_PROXY_URL="" \
# Higress
HIGRESS_API_KEY="" HIGRESS_MODEL_LIST="" HIGRESS_PROXY_URL="" \
# HuggingFace
HUGGINGFACE_API_KEY="" HUGGINGFACE_MODEL_LIST="" HUGGINGFACE_PROXY_URL="" \
# Hunyuan
HUNYUAN_API_KEY="" HUNYUAN_MODEL_LIST="" \
# InternLM
INTERNLM_API_KEY="" INTERNLM_MODEL_LIST="" \
# Jina
JINA_API_KEY="" JINA_MODEL_LIST="" JINA_PROXY_URL="" \
# Minimax
MINIMAX_API_KEY="" MINIMAX_MODEL_LIST="" \
# Mistral
MISTRAL_API_KEY="" MISTRAL_MODEL_LIST="" \
# ModelScope
MODELSCOPE_API_KEY="" MODELSCOPE_MODEL_LIST="" MODELSCOPE_PROXY_URL="" \
# Moonshot
MOONSHOT_API_KEY="" MOONSHOT_MODEL_LIST="" MOONSHOT_PROXY_URL="" \
# Nebius
NEBIUS_API_KEY="" NEBIUS_MODEL_LIST="" NEBIUS_PROXY_URL="" \
# NewAPI
NEWAPI_API_KEY="" NEWAPI_PROXY_URL="" \
# Novita
NOVITA_API_KEY="" NOVITA_MODEL_LIST="" \
# Nvidia NIM
NVIDIA_API_KEY="" NVIDIA_MODEL_LIST="" NVIDIA_PROXY_URL="" \
# Ollama
ENABLED_OLLAMA="" OLLAMA_MODEL_LIST="" OLLAMA_PROXY_URL="" \
# OpenAI
OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
# OpenRouter
OPENROUTER_API_KEY="" OPENROUTER_MODEL_LIST="" \
# Perplexity
PERPLEXITY_API_KEY="" PERPLEXITY_MODEL_LIST="" PERPLEXITY_PROXY_URL="" \
# PPIO
PPIO_API_KEY="" PPIO_MODEL_LIST="" \
# Qiniu
QINIU_API_KEY="" QINIU_MODEL_LIST="" QINIU_PROXY_URL="" \
# Qwen
QWEN_API_KEY="" QWEN_MODEL_LIST="" QWEN_PROXY_URL="" \
# SambaNova
SAMBANOVA_API_KEY="" SAMBANOVA_MODEL_LIST="" \
# Search1API
SEARCH1API_API_KEY="" SEARCH1API_MODEL_LIST="" \
# SenseNova
SENSENOVA_API_KEY="" SENSENOVA_MODEL_LIST="" \
# SiliconCloud
SILICONCLOUD_API_KEY="" SILICONCLOUD_MODEL_LIST="" SILICONCLOUD_PROXY_URL="" \
# Spark
SPARK_API_KEY="" SPARK_MODEL_LIST="" SPARK_PROXY_URL="" SPARK_SEARCH_MODE="" \
# Stepfun
STEPFUN_API_KEY="" STEPFUN_MODEL_LIST="" \
# Taichu
TAICHU_API_KEY="" TAICHU_MODEL_LIST="" \
# TogetherAI
TOGETHERAI_API_KEY="" TOGETHERAI_MODEL_LIST="" \
# Upstage
UPSTAGE_API_KEY="" UPSTAGE_MODEL_LIST="" \
# v0 (Vercel)
V0_API_KEY="" V0_MODEL_LIST="" \
# vLLM
VLLM_API_KEY="" VLLM_MODEL_LIST="" VLLM_PROXY_URL="" \
# Wenxin
WENXIN_API_KEY="" WENXIN_MODEL_LIST="" \
# xAI
XAI_API_KEY="" XAI_MODEL_LIST="" XAI_PROXY_URL="" \
# Xinference
XINFERENCE_API_KEY="" XINFERENCE_MODEL_LIST="" XINFERENCE_PROXY_URL="" \
# 01.AI
ZEROONE_API_KEY="" ZEROONE_MODEL_LIST="" \
# Zhipu
ZHIPU_API_KEY="" ZHIPU_MODEL_LIST="" \
# Tencent Cloud
TENCENT_CLOUD_API_KEY="" TENCENT_CLOUD_MODEL_LIST="" \
# Infini-AI
INFINIAI_API_KEY="" INFINIAI_MODEL_LIST="" \
# 302.AI
AI302_API_KEY="" AI302_MODEL_LIST="" \
# FAL
FAL_API_KEY="" FAL_MODEL_LIST="" \
# BFL
BFL_API_KEY="" BFL_MODEL_LIST="" \
# Vercel AI Gateway
VERCELAIGATEWAY_API_KEY="" VERCELAIGATEWAY_MODEL_LIST="" \
# Cerebras
CEREBRAS_API_KEY="" CEREBRAS_MODEL_LIST=""
USER nextjs
EXPOSE 3210/tcp
ENTRYPOINT ["/bin/node"]
CMD ["/app/startServer.js"]
-268
View File
@@ -1,268 +0,0 @@
## Set global build ENV
ARG NODEJS_VERSION="24"
## Base image for all building stages
FROM node:${NODEJS_VERSION}-slim AS base
ARG USE_CN_MIRROR
ENV DEBIAN_FRONTEND="noninteractive"
RUN \
# If you want to build docker in China, build with --build-arg USE_CN_MIRROR=true
if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \
sed -i "s/deb.debian.org/mirrors.ustc.edu.cn/g" "/etc/apt/sources.list.d/debian.sources"; \
fi \
# Add required package
&& apt update \
&& apt install ca-certificates proxychains-ng -qy \
# Prepare required package to distroless
&& mkdir -p /distroless/bin /distroless/etc /distroless/etc/ssl/certs /distroless/lib \
# Copy proxychains to distroless
&& cp /usr/lib/$(arch)-linux-gnu/libproxychains.so.4 /distroless/lib/libproxychains.so.4 \
&& cp /usr/lib/$(arch)-linux-gnu/libdl.so.2 /distroless/lib/libdl.so.2 \
&& cp /usr/bin/proxychains4 /distroless/bin/proxychains \
&& cp /etc/proxychains4.conf /distroless/etc/proxychains4.conf \
# Copy node to distroless
&& cp /usr/lib/$(arch)-linux-gnu/libstdc++.so.6 /distroless/lib/libstdc++.so.6 \
&& cp /usr/lib/$(arch)-linux-gnu/libgcc_s.so.1 /distroless/lib/libgcc_s.so.1 \
&& cp /usr/local/bin/node /distroless/bin/node \
# Copy CA certificates to distroless
&& cp /etc/ssl/certs/ca-certificates.crt /distroless/etc/ssl/certs/ca-certificates.crt \
# Cleanup temp files
&& rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/*
## Builder image, install all the dependencies and build the app
FROM base AS builder
ARG USE_CN_MIRROR
ARG NEXT_PUBLIC_BASE_PATH
ARG NEXT_PUBLIC_SENTRY_DSN
ARG NEXT_PUBLIC_ANALYTICS_POSTHOG
ARG NEXT_PUBLIC_POSTHOG_HOST
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_ANALYTICS_UMAMI
ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ARG FEATURE_FLAGS
ENV NEXT_PUBLIC_CLIENT_DB="pglite"
ENV NEXT_PUBLIC_BASE_PATH="${NEXT_PUBLIC_BASE_PATH}" \
FEATURE_FLAGS="${FEATURE_FLAGS}"
# Sentry
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}" \
NEXT_PUBLIC_POSTHOG_KEY="${NEXT_PUBLIC_POSTHOG_KEY}"
# Umami
ENV NEXT_PUBLIC_ANALYTICS_UMAMI="${NEXT_PUBLIC_ANALYTICS_UMAMI}" \
NEXT_PUBLIC_UMAMI_SCRIPT_URL="${NEXT_PUBLIC_UMAMI_SCRIPT_URL}" \
NEXT_PUBLIC_UMAMI_WEBSITE_ID="${NEXT_PUBLIC_UMAMI_WEBSITE_ID}"
# Node
ENV NODE_OPTIONS="--max-old-space-size=6144"
WORKDIR /app
COPY package.json pnpm-workspace.yaml ./
COPY .npmrc ./
COPY packages ./packages
RUN \
# If you want to build docker in China, build with --build-arg USE_CN_MIRROR=true
if [ "${USE_CN_MIRROR:-false}" = "true" ]; then \
export SENTRYCLI_CDNURL="https://npmmirror.com/mirrors/sentry-cli"; \
npm config set registry "https://registry.npmmirror.com/"; \
echo 'canvas_binary_host_mirror=https://npmmirror.com/mirrors/canvas' >> .npmrc; \
fi \
# Set the registry for corepack
&& export COREPACK_NPM_REGISTRY=$(npm config get registry | sed 's/\/$//') \
# Update corepack to latest (nodejs/corepack#612)
&& npm i -g corepack@latest \
# Enable corepack
&& corepack enable \
# Use pnpm for corepack
&& corepack use $(sed -n 's/.*"packageManager": "\(.*\)".*/\1/p' package.json) \
# Install the dependencies
&& pnpm i
COPY . .
# run build standalone for docker version
RUN npm run build:docker
## Application image, copy all the files for production
FROM busybox:latest AS app
COPY --from=base /distroless/ /
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder /app/.next/standalone /app/
# Copy server launcher
COPY --from=builder /app/scripts/serverLauncher/startServer.js /app/startServer.js
RUN \
# Add nextjs:nodejs to run the app
addgroup -S -g 1001 nodejs \
&& adduser -D -G nodejs -H -S -h /app -u 1001 nextjs \
# Set permission for nextjs:nodejs
&& chown -R nextjs:nodejs /app /etc/proxychains4.conf
## Production image, copy all the files and run next
FROM scratch
# Copy all the files from app, set the correct permission for prerender cache
COPY --from=app / /
ENV NODE_ENV="production" \
NODE_OPTIONS="--dns-result-order=ipv4first --use-openssl-ca" \
NODE_EXTRA_CA_CERTS="" \
NODE_TLS_REJECT_UNAUTHORIZED="" \
SSL_CERT_FILE="/etc/ssl/certs/ca-certificates.crt"
# Make the middleware rewrite through local as default
# refs: https://github.com/lobehub/lobe-chat/issues/5876
ENV MIDDLEWARE_REWRITE_THROUGH_LOCAL="1"
# set hostname to localhost
ENV HOSTNAME="0.0.0.0" \
PORT="3210"
# General Variables
ENV ACCESS_CODE="" \
API_KEY_SELECT_MODE="" \
DEFAULT_AGENT_CONFIG="" \
SYSTEM_AGENT="" \
FEATURE_FLAGS="" \
PROXY_URL=""
# Model Variables
ENV \
# AI21
AI21_API_KEY="" AI21_MODEL_LIST="" \
# Ai360
AI360_API_KEY="" AI360_MODEL_LIST="" \
# AiHubMix
AIHUBMIX_API_KEY="" AIHUBMIX_MODEL_LIST="" \
# Anthropic
ANTHROPIC_API_KEY="" ANTHROPIC_MODEL_LIST="" ANTHROPIC_PROXY_URL="" \
# Amazon Bedrock
AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_REGION="" AWS_BEDROCK_MODEL_LIST="" \
# Azure OpenAI
AZURE_API_KEY="" AZURE_API_VERSION="" AZURE_ENDPOINT="" AZURE_MODEL_LIST="" \
# Baichuan
BAICHUAN_API_KEY="" BAICHUAN_MODEL_LIST="" \
# Cloudflare
CLOUDFLARE_API_KEY="" CLOUDFLARE_BASE_URL_OR_ACCOUNT_ID="" CLOUDFLARE_MODEL_LIST="" \
# Cohere
COHERE_API_KEY="" COHERE_MODEL_LIST="" COHERE_PROXY_URL="" \
# DeepSeek
DEEPSEEK_API_KEY="" DEEPSEEK_MODEL_LIST="" \
# Fireworks AI
FIREWORKSAI_API_KEY="" FIREWORKSAI_MODEL_LIST="" \
# Gitee AI
GITEE_AI_API_KEY="" GITEE_AI_MODEL_LIST="" \
# GitHub
GITHUB_TOKEN="" GITHUB_MODEL_LIST="" \
# Google
GOOGLE_API_KEY="" GOOGLE_MODEL_LIST="" GOOGLE_PROXY_URL="" \
# Groq
GROQ_API_KEY="" GROQ_MODEL_LIST="" GROQ_PROXY_URL="" \
# Higress
HIGRESS_API_KEY="" HIGRESS_MODEL_LIST="" HIGRESS_PROXY_URL="" \
# HuggingFace
HUGGINGFACE_API_KEY="" HUGGINGFACE_MODEL_LIST="" HUGGINGFACE_PROXY_URL="" \
# Hunyuan
HUNYUAN_API_KEY="" HUNYUAN_MODEL_LIST="" \
# InternLM
INTERNLM_API_KEY="" INTERNLM_MODEL_LIST="" \
# Jina
JINA_API_KEY="" JINA_MODEL_LIST="" JINA_PROXY_URL="" \
# Minimax
MINIMAX_API_KEY="" MINIMAX_MODEL_LIST="" \
# Mistral
MISTRAL_API_KEY="" MISTRAL_MODEL_LIST="" \
# ModelScope
MODELSCOPE_API_KEY="" MODELSCOPE_MODEL_LIST="" MODELSCOPE_PROXY_URL="" \
# Moonshot
MOONSHOT_API_KEY="" MOONSHOT_MODEL_LIST="" MOONSHOT_PROXY_URL="" \
# Nebius
NEBIUS_API_KEY="" NEBIUS_MODEL_LIST="" NEBIUS_PROXY_URL="" \
# NewAPI
NEWAPI_API_KEY="" NEWAPI_PROXY_URL="" \
# Novita
NOVITA_API_KEY="" NOVITA_MODEL_LIST="" \
# Nvidia NIM
NVIDIA_API_KEY="" NVIDIA_MODEL_LIST="" NVIDIA_PROXY_URL="" \
# Ollama
ENABLED_OLLAMA="" OLLAMA_MODEL_LIST="" OLLAMA_PROXY_URL="" \
# OpenAI
OPENAI_API_KEY="" OPENAI_MODEL_LIST="" OPENAI_PROXY_URL="" \
# OpenRouter
OPENROUTER_API_KEY="" OPENROUTER_MODEL_LIST="" \
# Perplexity
PERPLEXITY_API_KEY="" PERPLEXITY_MODEL_LIST="" PERPLEXITY_PROXY_URL="" \
# Qiniu
QINIU_API_KEY="" QINIU_MODEL_LIST="" QINIU_PROXY_URL="" \
# Qwen
QWEN_API_KEY="" QWEN_MODEL_LIST="" QWEN_PROXY_URL="" \
# SambaNova
SAMBANOVA_API_KEY="" SAMBANOVA_MODEL_LIST="" \
# SenseNova
SENSENOVA_API_KEY="" SENSENOVA_MODEL_LIST="" \
# SiliconCloud
SILICONCLOUD_API_KEY="" SILICONCLOUD_MODEL_LIST="" SILICONCLOUD_PROXY_URL="" \
# Spark
SPARK_API_KEY="" SPARK_MODEL_LIST="" SPARK_PROXY_URL="" SPARK_SEARCH_MODE="" \
# Stepfun
STEPFUN_API_KEY="" STEPFUN_MODEL_LIST="" \
# Taichu
TAICHU_API_KEY="" TAICHU_MODEL_LIST="" \
# TogetherAI
TOGETHERAI_API_KEY="" TOGETHERAI_MODEL_LIST="" \
# Upstage
UPSTAGE_API_KEY="" UPSTAGE_MODEL_LIST="" \
# v0 (Vercel)
V0_API_KEY="" V0_MODEL_LIST="" \
# vLLM
VLLM_API_KEY="" VLLM_MODEL_LIST="" VLLM_PROXY_URL="" \
# Wenxin
WENXIN_API_KEY="" WENXIN_MODEL_LIST="" \
# xAI
XAI_API_KEY="" XAI_MODEL_LIST="" XAI_PROXY_URL="" \
# Xinference
XINFERENCE_API_KEY="" XINFERENCE_MODEL_LIST="" XINFERENCE_PROXY_URL="" \
# 01.AI
ZEROONE_API_KEY="" ZEROONE_MODEL_LIST="" \
# Zhipu
ZHIPU_API_KEY="" ZHIPU_MODEL_LIST="" \
# Tencent Cloud
TENCENT_CLOUD_API_KEY="" TENCENT_CLOUD_MODEL_LIST="" \
# Infini-AI
INFINIAI_API_KEY="" INFINIAI_MODEL_LIST="" \
# 302.AI
AI302_API_KEY="" AI302_MODEL_LIST="" \
# FAL
FAL_API_KEY="" FAL_MODEL_LIST="" \
# BFL
BFL_API_KEY="" BFL_MODEL_LIST="" \
# Vercel AI Gateway
VERCELAIGATEWAY_API_KEY="" VERCELAIGATEWAY_MODEL_LIST=""
USER nextjs
EXPOSE 3210/tcp
ENTRYPOINT ["/bin/node"]
CMD ["/app/startServer.js"]
+16 -53
View File
@@ -1,3 +1,10 @@
> \[!NOTE]
>
> **Version Information**
>
> - **v1.x** (Stable): Available on the [`main`](https://github.com/lobehub/lobe-chat/tree/main) branch
> - **v2.x** (In Development): Currently being actively developed on the [`next`](https://github.com/lobehub/lobe-chat/tree/next) branch 🔥
<div align="center"><a name="readme-top"></a>
[![][image-banner]][vercel-link]
@@ -239,55 +246,11 @@ We have implemented support for the following model service providers:
<!-- PROVIDER LIST -->
- **[OpenAI](https://lobechat.com/discover/provider/openai)**: OpenAI is a global leader in artificial intelligence research, with models like the GPT series pushing the frontiers of natural language processing. OpenAI is committed to transforming multiple industries through innovative and efficient AI solutions. Their products demonstrate significant performance and cost-effectiveness, widely used in research, business, and innovative applications.
- **[Ollama](https://lobechat.com/discover/provider/ollama)**: Ollama provides models that cover a wide range of fields, including code generation, mathematical operations, multilingual processing, and conversational interaction, catering to diverse enterprise-level and localized deployment needs.
- **[Anthropic](https://lobechat.com/discover/provider/anthropic)**: Anthropic is a company focused on AI research and development, offering a range of advanced language models such as Claude 3.5 Sonnet, Claude 3 Sonnet, Claude 3 Opus, and Claude 3 Haiku. These models achieve an ideal balance between intelligence, speed, and cost, suitable for various applications from enterprise workloads to rapid-response scenarios. Claude 3.5 Sonnet, as their latest model, has excelled in multiple evaluations while maintaining a high cost-performance ratio.
- **[Bedrock](https://lobechat.com/discover/provider/bedrock)**: Bedrock is a service provided by Amazon AWS, focusing on delivering advanced AI language and visual models for enterprises. Its model family includes Anthropic's Claude series, Meta's Llama 3.1 series, and more, offering a range of options from lightweight to high-performance, supporting tasks such as text generation, conversation, and image processing for businesses of varying scales and needs.
- **[Google](https://lobechat.com/discover/provider/google)**: Google's Gemini series represents its most advanced, versatile AI models, developed by Google DeepMind, designed for multimodal capabilities, supporting seamless understanding and processing of text, code, images, audio, and video. Suitable for various environments from data centers to mobile devices, it significantly enhances the efficiency and applicability of AI models.
- **[DeepSeek](https://lobechat.com/discover/provider/deepseek)**: DeepSeek is a company focused on AI technology research and application, with its latest model DeepSeek-V2.5 integrating general dialogue and code processing capabilities, achieving significant improvements in human preference alignment, writing tasks, and instruction following.
- **[Moonshot](https://lobechat.com/discover/provider/moonshot)**: Moonshot is an open-source platform launched by Beijing Dark Side Technology Co., Ltd., providing various natural language processing models with a wide range of applications, including but not limited to content creation, academic research, intelligent recommendations, and medical diagnosis, supporting long text processing and complex generation tasks.
- **[OpenRouter](https://lobechat.com/discover/provider/openrouter)**: OpenRouter is a service platform providing access to various cutting-edge large model interfaces, supporting OpenAI, Anthropic, LLaMA, and more, suitable for diverse development and application needs. Users can flexibly choose the optimal model and pricing based on their requirements, enhancing the AI experience.
- **[HuggingFace](https://lobechat.com/discover/provider/huggingface)**: The HuggingFace Inference API provides a fast and free way for you to explore thousands of models for various tasks. Whether you are prototyping for a new application or experimenting with the capabilities of machine learning, this API gives you instant access to high-performance models across multiple domains.
- **[Cloudflare Workers AI](https://lobechat.com/discover/provider/cloudflare)**: Run serverless GPU-powered machine learning models on Cloudflare's global network.
<details><summary><kbd>See more providers (+32)</kbd></summary>
- **[GitHub](https://lobechat.com/discover/provider/github)**: With GitHub Models, developers can become AI engineers and leverage the industry's leading AI models.
- **[Novita](https://lobechat.com/discover/provider/novita)**: Novita AI is a platform providing a variety of large language models and AI image generation API services, flexible, reliable, and cost-effective. It supports the latest open-source models like Llama3 and Mistral, offering a comprehensive, user-friendly, and auto-scaling API solution for generative AI application development, suitable for the rapid growth of AI startups.
- **[PPIO](https://lobechat.com/discover/provider/ppio)**: PPIO supports stable and cost-efficient open-source LLM APIs, such as DeepSeek, Llama, Qwen etc.
- **[302.AI](https://lobechat.com/discover/provider/ai302)**: 302.AI is an on-demand AI application platform offering the most comprehensive AI APIs and online AI applications available on the market.
- **[Together AI](https://lobechat.com/discover/provider/togetherai)**: Together AI is dedicated to achieving leading performance through innovative AI models, offering extensive customization capabilities, including rapid scaling support and intuitive deployment processes to meet various enterprise needs.
- **[Fireworks AI](https://lobechat.com/discover/provider/fireworksai)**: Fireworks AI is a leading provider of advanced language model services, focusing on functional calling and multimodal processing. Its latest model, Firefunction V2, is based on Llama-3, optimized for function calling, conversation, and instruction following. The visual language model FireLLaVA-13B supports mixed input of images and text. Other notable models include the Llama series and Mixtral series, providing efficient multilingual instruction following and generation support.
- **[Groq](https://lobechat.com/discover/provider/groq)**: Groq's LPU inference engine has excelled in the latest independent large language model (LLM) benchmarks, redefining the standards for AI solutions with its remarkable speed and efficiency. Groq represents instant inference speed, demonstrating strong performance in cloud-based deployments.
- **[Perplexity](https://lobechat.com/discover/provider/perplexity)**: Perplexity is a leading provider of conversational generation models, offering various advanced Llama 3.1 models that support both online and offline applications, particularly suited for complex natural language processing tasks.
- **[Mistral](https://lobechat.com/discover/provider/mistral)**: Mistral provides advanced general, specialized, and research models widely used in complex reasoning, multilingual tasks, and code generation. Through functional calling interfaces, users can integrate custom functionalities for specific applications.
- **[ModelScope](https://lobechat.com/discover/provider/modelscope)**: ModelScope is a model-as-a-service platform launched by Alibaba Cloud, offering a wide range of AI models and inference services.
- **[Ai21Labs](https://lobechat.com/discover/provider/ai21)**: AI21 Labs builds foundational models and AI systems for enterprises, accelerating the application of generative AI in production.
- **[Upstage](https://lobechat.com/discover/provider/upstage)**: Upstage focuses on developing AI models for various business needs, including Solar LLM and document AI, aiming to achieve artificial general intelligence (AGI) for work. It allows for the creation of simple conversational agents through Chat API and supports functional calling, translation, embedding, and domain-specific applications.
- **[xAI (Grok)](https://lobechat.com/discover/provider/xai)**: xAI is a company dedicated to building artificial intelligence to accelerate human scientific discovery. Our mission is to advance our collective understanding of the universe.
- **[Aliyun Bailian](https://lobechat.com/discover/provider/qwen)**: Tongyi Qianwen is a large-scale language model independently developed by Alibaba Cloud, featuring strong natural language understanding and generation capabilities. It can answer various questions, create written content, express opinions, and write code, playing a role in multiple fields.
- **[Wenxin](https://lobechat.com/discover/provider/wenxin)**: An enterprise-level one-stop platform for large model and AI-native application development and services, providing the most comprehensive and user-friendly toolchain for the entire process of generative artificial intelligence model development and application development.
- **[Hunyuan](https://lobechat.com/discover/provider/hunyuan)**: A large language model developed by Tencent, equipped with powerful Chinese creative capabilities, logical reasoning abilities in complex contexts, and reliable task execution skills.
- **[ZhiPu](https://lobechat.com/discover/provider/zhipu)**: Zhipu AI offers an open platform for multimodal and language models, supporting a wide range of AI application scenarios, including text processing, image understanding, and programming assistance.
- **[SiliconCloud](https://lobechat.com/discover/provider/siliconcloud)**: SiliconFlow is dedicated to accelerating AGI for the benefit of humanity, enhancing large-scale AI efficiency through an easy-to-use and cost-effective GenAI stack.
- **[01.AI](https://lobechat.com/discover/provider/zeroone)**: 01.AI focuses on AI 2.0 era technologies, vigorously promoting the innovation and application of 'human + artificial intelligence', using powerful models and advanced AI technologies to enhance human productivity and achieve technological empowerment.
- **[Spark](https://lobechat.com/discover/provider/spark)**: iFlytek's Spark model provides powerful AI capabilities across multiple domains and languages, utilizing advanced natural language processing technology to build innovative applications suitable for smart hardware, smart healthcare, smart finance, and other vertical scenarios.
- **[SenseNova](https://lobechat.com/discover/provider/sensenova)**: SenseNova, backed by SenseTime's robust infrastructure, offers efficient and user-friendly full-stack large model services.
- **[Stepfun](https://lobechat.com/discover/provider/stepfun)**: StepFun's large model possesses industry-leading multimodal and complex reasoning capabilities, supporting ultra-long text understanding and powerful autonomous scheduling search engine functions.
- **[Baichuan](https://lobechat.com/discover/provider/baichuan)**: Baichuan Intelligence is a company focused on the research and development of large AI models, with its models excelling in domestic knowledge encyclopedias, long text processing, and generative creation tasks in Chinese, surpassing mainstream foreign models. Baichuan Intelligence also possesses industry-leading multimodal capabilities, performing excellently in multiple authoritative evaluations. Its models include Baichuan 4, Baichuan 3 Turbo, and Baichuan 3 Turbo 128k, each optimized for different application scenarios, providing cost-effective solutions.
- **[Minimax](https://lobechat.com/discover/provider/minimax)**: MiniMax is a general artificial intelligence technology company established in 2021, dedicated to co-creating intelligence with users. MiniMax has independently developed general large models of different modalities, including trillion-parameter MoE text models, voice models, and image models, and has launched applications such as Conch AI.
- **[InternLM](https://lobechat.com/discover/provider/internlm)**: An open-source organization dedicated to the research and development of large model toolchains. It provides an efficient and user-friendly open-source platform for all AI developers, making cutting-edge large models and algorithm technologies easily accessible.
- **[Higress](https://lobechat.com/discover/provider/higress)**: Higress is a cloud-native API gateway that was developed internally at Alibaba to address the issues of Tengine reload affecting long-lived connections and the insufficient load balancing capabilities for gRPC/Dubbo.
- **[Gitee AI](https://lobechat.com/discover/provider/giteeai)**: Gitee AI's Serverless API provides AI developers with an out of the box large model inference API service.
- **[Taichu](https://lobechat.com/discover/provider/taichu)**: The Institute of Automation, Chinese Academy of Sciences, and Wuhan Artificial Intelligence Research Institute have launched a new generation of multimodal large models, supporting comprehensive question-answering tasks such as multi-turn Q\&A, text creation, image generation, 3D understanding, and signal analysis, with stronger cognitive, understanding, and creative abilities, providing a new interactive experience.
- **[360 AI](https://lobechat.com/discover/provider/ai360)**: 360 AI is an AI model and service platform launched by 360 Company, offering various advanced natural language processing models, including 360GPT2 Pro, 360GPT Pro, 360GPT Turbo, and 360GPT Turbo Responsibility 8K. These models combine large-scale parameters and multimodal capabilities, widely applied in text generation, semantic understanding, dialogue systems, and code generation. With flexible pricing strategies, 360 AI meets diverse user needs, supports developer integration, and promotes the innovation and development of intelligent applications.
- **[Search1API](https://lobechat.com/discover/provider/search1api)**: Search1API provides access to the DeepSeek series of models that can connect to the internet as needed, including standard and fast versions, supporting a variety of model sizes.
- **[InfiniAI](https://lobechat.com/discover/provider/infiniai)**: Provides high-performance, easy-to-use, and secure large model services for application developers, covering the entire process from large model development to service deployment.
- **[Qiniu](https://lobechat.com/discover/provider/qiniu)**: Qiniu, as a long-established cloud service provider, delivers cost-effective and reliable AI inference services for both real-time and batch processing, with a simple and user-friendly experience.
<details><summary><kbd>See more providers (+-10)</kbd></summary>
</details>
> 📊 Total providers: [<kbd>**42**</kbd>](https://lobechat.com/discover/providers)
> 📊 Total providers: [<kbd>**0**</kbd>](https://lobechat.com/discover/providers)
<!-- PROVIDER LIST -->
@@ -382,12 +345,12 @@ In addition, these plugins are not limited to news aggregation, but can also ext
<!-- PLUGIN LIST -->
| Recent Submits | Description |
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
| [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
| [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` |
| Recent Submits | Description |
| -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | Enter any URL and keyword and get an On-Page SEO analysis & insights!<br/>`seo` |
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping` `e-bay` `ali-express` `coupons` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
@@ -902,7 +865,7 @@ This project is [LobeHub Community License](./LICENSE) licensed.
[github-release-shield]: https://img.shields.io/github/v/release/lobehub/lobe-chat?color=369eff&labelColor=black&logo=github&style=flat-square
[github-releasedate-link]: https://github.com/lobehub/lobe-chat/releases
[github-releasedate-shield]: https://img.shields.io/github/release-date/lobehub/lobe-chat?labelColor=black&style=flat-square
[github-stars-link]: https://github.com/lobehub/lobe-chat/network/stargazers
[github-stars-link]: https://github.com/lobehub/lobe-chat/stargazers
[github-stars-shield]: https://img.shields.io/github/stars/lobehub/lobe-chat?color=ffcb47&labelColor=black&style=flat-square
[github-trending-shield]: https://trendshift.io/api/badge/repositories/2256
[github-trending-url]: https://trendshift.io/repositories/2256
+16 -53
View File
@@ -1,3 +1,10 @@
> \[!NOTE]
>
> **版本信息**
>
> - **v1.x** (稳定版):位于 [`main`](https://github.com/lobehub/lobe-chat/tree/main) 分支
> - **v2.x** (开发中):正在 [`next`](https://github.com/lobehub/lobe-chat/tree/next) 分支火热开发中 🔥
<div align="center"><a name="readme-top"></a>
[![][image-banner]][vercel-link]
@@ -239,55 +246,11 @@ LobeChat 支持文件上传与知识库功能,你可以上传文件、图片
<!-- PROVIDER LIST -->
- **[OpenAI](https://lobechat.com/discover/provider/openai)**: OpenAI 是全球领先的人工智能研究机构,其开发的模型如 GPT 系列推动了自然语言处理的前沿。OpenAI 致力于通过创新和高效的 AI 解决方案改变多个行业。他们的产品具有显著的性能和经济性,广泛用于研究、商业和创新应用。
- **[Ollama](https://lobechat.com/discover/provider/ollama)**: Ollama 提供的模型广泛涵盖代码生成、数学运算、多语种处理和对话互动等领域,支持企业级和本地化部署的多样化需求。
- **[Anthropic](https://lobechat.com/discover/provider/anthropic)**: Anthropic 是一家专注于人工智能研究和开发的公司,提供了一系列先进的语言模型,如 Claude 3.5 Sonnet、Claude 3 Sonnet、Claude 3 Opus 和 Claude 3 Haiku。这些模型在智能、速度和成本之间取得了理想的平衡,适用于从企业级工作负载到快速响应的各种应用场景。Claude 3.5 Sonnet 作为其最新模型,在多项评估中表现优异,同时保持了较高的性价比。
- **[Bedrock](https://lobechat.com/discover/provider/bedrock)**: Bedrock 是亚马逊 AWS 提供的一项服务,专注于为企业提供先进的 AI 语言模型和视觉模型。其模型家族包括 Anthropic 的 Claude 系列、Meta 的 Llama 3.1 系列等,涵盖从轻量级到高性能的多种选择,支持文本生成、对话、图像处理等多种任务,适用于不同规模和需求的企业应用。
- **[Google](https://lobechat.com/discover/provider/google)**: Google 的 Gemini 系列是其最先进、通用的 AI 模型,由 Google DeepMind 打造,专为多模态设计,支持文本、代码、图像、音频和视频的无缝理解与处理。适用于从数据中心到移动设备的多种环境,极大提升了 AI 模型的效率与应用广泛性。
- **[DeepSeek](https://lobechat.com/discover/provider/deepseek)**: DeepSeek 是一家专注于人工智能技术研究和应用的公司,其最新模型 DeepSeek-V3 多项评测成绩超越 Qwen2.5-72B 和 Llama-3.1-405B 等开源模型,性能对齐领军闭源模型 GPT-4o 与 Claude-3.5-Sonnet。
- **[Moonshot](https://lobechat.com/discover/provider/moonshot)**: Moonshot 是由北京月之暗面科技有限公司推出的开源平台,提供多种自然语言处理模型,应用领域广泛,包括但不限于内容创作、学术研究、智能推荐、医疗诊断等,支持长文本处理和复杂生成任务。
- **[OpenRouter](https://lobechat.com/discover/provider/openrouter)**: OpenRouter 是一个提供多种前沿大模型接口的服务平台,支持 OpenAI、Anthropic、LLaMA 及更多,适合多样化的开发和应用需求。用户可根据自身需求灵活选择最优的模型和价格,助力 AI 体验的提升。
- **[HuggingFace](https://lobechat.com/discover/provider/huggingface)**: HuggingFace Inference API 提供了一种快速且免费的方式,让您可以探索成千上万种模型,适用于各种任务。无论您是在为新应用程序进行原型设计,还是在尝试机器学习的功能,这个 API 都能让您即时访问多个领域的高性能模型。
- **[Cloudflare Workers AI](https://lobechat.com/discover/provider/cloudflare)**: 在 Cloudflare 的全球网络上运行由无服务器 GPU 驱动的机器学习模型。
<details><summary><kbd>See more providers (+32)</kbd></summary>
- **[GitHub](https://lobechat.com/discover/provider/github)**: 通过 GitHub 模型,开发人员可以成为 AI 工程师,并使用行业领先的 AI 模型进行构建。
- **[Novita](https://lobechat.com/discover/provider/novita)**: Novita AI 是一个提供多种大语言模型与 AI 图像生成的 API 服务的平台,灵活、可靠且具有成本效益。它支持 Llama3、Mistral 等最新的开源模型,并为生成式 AI 应用开发提供了全面、用户友好且自动扩展的 API 解决方案,适合 AI 初创公司的快速发展。
- **[PPIO](https://lobechat.com/discover/provider/ppio)**: PPIO 派欧云提供稳定、高性价比的开源模型 API 服务,支持 DeepSeek 全系列、Llama、Qwen 等行业领先大模型。
- **[302.AI](https://lobechat.com/discover/provider/ai302)**: 302.AI 是一个按需付费的 AI 应用平台,提供市面上最全的 AI API 和 AI 在线应用
- **[Together AI](https://lobechat.com/discover/provider/togetherai)**: Together AI 致力于通过创新的 AI 模型实现领先的性能,提供广泛的自定义能力,包括快速扩展支持和直观的部署流程,满足企业的各种需求。
- **[Fireworks AI](https://lobechat.com/discover/provider/fireworksai)**: Fireworks AI 是一家领先的高级语言模型服务商,专注于功能调用和多模态处理。其最新模型 Firefunction V2 基于 Llama-3,优化用于函数调用、对话及指令跟随。视觉语言模型 FireLLaVA-13B 支持图像和文本混合输入。其他 notable 模型包括 Llama 系列和 Mixtral 系列,提供高效的多语言指令跟随与生成支持。
- **[Groq](https://lobechat.com/discover/provider/groq)**: Groq 的 LPU 推理引擎在最新的独立大语言模型(LLM)基准测试中表现卓越,以其惊人的速度和效率重新定义了 AI 解决方案的标准。Groq 是一种即时推理速度的代表,在基于云的部署中展现了良好的性能。
- **[Perplexity](https://lobechat.com/discover/provider/perplexity)**: Perplexity 是一家领先的对话生成模型提供商,提供多种先进的 Llama 3.1 模型,支持在线和离线应用,特别适用于复杂的自然语言处理任务。
- **[Mistral](https://lobechat.com/discover/provider/mistral)**: Mistral 提供先进的通用、专业和研究型模型,广泛应用于复杂推理、多语言任务、代码生成等领域,通过功能调用接口,用户可以集成自定义功能,实现特定应用。
- **[ModelScope](https://lobechat.com/discover/provider/modelscope)**: ModelScope 是阿里云推出的模型即服务平台,提供丰富的 AI 模型和推理服务。
- **[Ai21Labs](https://lobechat.com/discover/provider/ai21)**: AI21 Labs 为企业构建基础模型和人工智能系统,加速生成性人工智能在生产中的应用。
- **[Upstage](https://lobechat.com/discover/provider/upstage)**: Upstage 专注于为各种商业需求开发 AI 模型,包括 Solar LLM 和文档 AI,旨在实现工作的人造通用智能(AGI)。通过 Chat API 创建简单的对话代理,并支持功能调用、翻译、嵌入以及特定领域应用。
- **[xAI (Grok)](https://lobechat.com/discover/provider/xai)**: xAI 是一家致力于构建人工智能以加速人类科学发现的公司。我们的使命是推动我们对宇宙的共同理解。
- **[Aliyun Bailian](https://lobechat.com/discover/provider/qwen)**: 通义千问是阿里云自主研发的超大规模语言模型,具有强大的自然语言理解和生成能力。它可以回答各种问题、创作文字内容、表达观点看法、撰写代码等,在多个领域发挥作用。
- **[Wenxin](https://lobechat.com/discover/provider/wenxin)**: 企业级一站式大模型与 AI 原生应用开发及服务平台,提供最全面易用的生成式人工智能模型开发、应用开发全流程工具链
- **[Hunyuan](https://lobechat.com/discover/provider/hunyuan)**: 由腾讯研发的大语言模型,具备强大的中文创作能力,复杂语境下的逻辑推理能力,以及可靠的任务执行能力
- **[ZhiPu](https://lobechat.com/discover/provider/zhipu)**: 智谱 AI 提供多模态与语言模型的开放平台,支持广泛的 AI 应用场景,包括文本处理、图像理解与编程辅助等。
- **[SiliconCloud](https://lobechat.com/discover/provider/siliconcloud)**: SiliconCloud,基于优秀开源基础模型的高性价比 GenAI 云服务
- **[01.AI](https://lobechat.com/discover/provider/zeroone)**: 零一万物致力于推动以人为本的 AI 2.0 技术革命,旨在通过大语言模型创造巨大的经济和社会价值,并开创新的 AI 生态与商业模式。
- **[Spark](https://lobechat.com/discover/provider/spark)**: 科大讯飞星火大模型提供多领域、多语言的强大 AI 能力,利用先进的自然语言处理技术,构建适用于智能硬件、智慧医疗、智慧金融等多种垂直场景的创新应用。
- **[SenseNova](https://lobechat.com/discover/provider/sensenova)**: 商汤日日新,依托商汤大装置的强大的基础支撑,提供高效易用的全栈大模型服务。
- **[Stepfun](https://lobechat.com/discover/provider/stepfun)**: 阶级星辰大模型具备行业领先的多模态及复杂推理能力,支持超长文本理解和强大的自主调度搜索引擎功能。
- **[Baichuan](https://lobechat.com/discover/provider/baichuan)**: 百川智能是一家专注于人工智能大模型研发的公司,其模型在国内知识百科、长文本处理和生成创作等中文任务上表现卓越,超越了国外主流模型。百川智能还具备行业领先的多模态能力,在多项权威评测中表现优异。其模型包括 Baichuan 4、Baichuan 3 Turbo 和 Baichuan 3 Turbo 128k 等,分别针对不同应用场景进行优化,提供高性价比的解决方案。
- **[Minimax](https://lobechat.com/discover/provider/minimax)**: MiniMax 是 2021 年成立的通用人工智能科技公司,致力于与用户共创智能。MiniMax 自主研发了不同模态的通用大模型,其中包括万亿参数的 MoE 文本大模型、语音大模型以及图像大模型。并推出了海螺 AI 等应用。
- **[InternLM](https://lobechat.com/discover/provider/internlm)**: 致力于大模型研究与开发工具链的开源组织。为所有 AI 开发者提供高效、易用的开源平台,让最前沿的大模型与算法技术触手可及
- **[Higress](https://lobechat.com/discover/provider/higress)**: Higress 是一款云原生 API 网关,在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
- **[Gitee AI](https://lobechat.com/discover/provider/giteeai)**: Gitee AI 的 Serverless API 为 AI 开发者提供开箱即用的大模型推理 API 服务。
- **[Taichu](https://lobechat.com/discover/provider/taichu)**: 中科院自动化研究所和武汉人工智能研究院推出新一代多模态大模型,支持多轮问答、文本创作、图像生成、3D 理解、信号分析等全面问答任务,拥有更强的认知、理解、创作能力,带来全新互动体验。
- **[360 AI](https://lobechat.com/discover/provider/ai360)**: 360 AI 是 360 公司推出的 AI 模型和服务平台,提供多种先进的自然语言处理模型,包括 360GPT2 Pro、360GPT Pro、360GPT Turbo 和 360GPT Turbo Responsibility 8K。这些模型结合了大规模参数和多模态能力,广泛应用于文本生成、语义理解、对话系统与代码生成等领域。通过灵活的定价策略,360 AI 满足多样化用户需求,支持开发者集成,推动智能化应用的革新和发展。
- **[Search1API](https://lobechat.com/discover/provider/search1api)**: Search1API 提供可根据需要自行联网的 DeepSeek 系列模型的访问,包括标准版和快速版本,支持多种参数规模的模型选择。
- **[InfiniAI](https://lobechat.com/discover/provider/infiniai)**: 为应用开发者提供高性能、易上手、安全可靠的大模型服务,覆盖从大模型开发到大模型服务化部署的全流程。
- **[Qiniu](https://lobechat.com/discover/provider/qiniu)**: 七牛作为老牌云服务厂商,提供高性价比稳定的实时、批量 AI 推理服务,简单易用。
<details><summary><kbd>See more providers (+-10)</kbd></summary>
</details>
> 📊 Total providers: [<kbd>**42**</kbd>](https://lobechat.com/discover/providers)
> 📊 Total providers: [<kbd>**0**</kbd>](https://lobechat.com/discover/providers)
<!-- PROVIDER LIST -->
@@ -375,12 +338,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
<!-- PLUGIN LIST -->
| 最近新增 | 描述 |
| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
| [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
| [谷歌自定义搜索引擎](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | 通过他们的官方自定义搜索引擎 API 搜索谷歌。<br/>`网` `搜索` |
| 最近新增 | 描述 |
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| [SEO](https://lobechat.com/discover/plugin/SEO)<br/><sup>By **orrenprunckun** on **2025-11-14**</sup> | 输入任何 URL 和关键词,获取页面 SEO 分析和见解!<br/>`seo` |
| [购物工具](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2025-10-27**</sup> | 在 eBay 和 AliExpress 上搜索产品,查找 eBay 活动和优惠券。获取快速示例。<br/>`购物` `e-bay` `ali-express` `优惠券` |
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-09-27**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网` `搜索` |
> 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
@@ -923,7 +886,7 @@ This project is [LobeHub Community License](./LICENSE) licensed.
[github-release-shield]: https://img.shields.io/github/v/release/lobehub/lobe-chat?color=369eff&labelColor=black&logo=github&style=flat-square
[github-releasedate-link]: https://github.com/lobehub/lobe-chat/releases
[github-releasedate-shield]: https://img.shields.io/github/release-date/lobehub/lobe-chat?labelColor=black&style=flat-square
[github-stars-link]: https://github.com/lobehub/lobe-chat/network/stargazers
[github-stars-link]: https://github.com/lobehub/lobe-chat/stargazers
[github-stars-shield]: https://img.shields.io/github/stars/lobehub/lobe-chat?color=ffcb47&labelColor=black&style=flat-square
[github-trending-shield]: https://trendshift.io/api/badge/repositories/2256
[github-trending-url]: https://trendshift.io/repositories/2256
+20 -19
View File
@@ -32,46 +32,47 @@
"electron-updater": "^6.6.2",
"electron-window-state": "^5.0.3",
"fetch-socks": "^1.3.2",
"get-port-please": "^3.1.2",
"get-port-please": "^3.2.0",
"pdfjs-dist": "4.10.38"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/tsconfig": "^2.0.0",
"@electron-toolkit/utils": "^4.0.0",
"@lobechat/electron-client-ipc": "workspace:*",
"@lobechat/electron-server-ipc": "workspace:*",
"@lobechat/file-loaders": "workspace:*",
"@lobehub/i18n-cli": "^1.20.3",
"@types/lodash": "^4.17.0",
"@lobehub/i18n-cli": "^1.25.1",
"@types/lodash": "^4.17.20",
"@types/resolve": "^1.20.6",
"@types/semver": "^7.7.0",
"@types/semver": "^7.7.1",
"@types/set-cookie-parser": "^2.4.10",
"@typescript/native-preview": "7.0.0-dev.20250711.1",
"consola": "^3.1.0",
"consola": "^3.4.2",
"cookie": "^1.0.2",
"electron": "^38.0.0",
"electron": "^38.7.0",
"electron-builder": "^26.0.12",
"electron-is": "^3.0.0",
"electron-log": "^5.3.3",
"electron-log": "^5.4.3",
"electron-store": "^8.2.0",
"electron-vite": "^3.0.0",
"execa": "^9.5.2",
"electron-vite": "^3.1.0",
"execa": "^9.6.0",
"fast-glob": "^3.3.3",
"fix-path": "^5.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"just-diff": "^6.0.2",
"lodash": "^4.17.21",
"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",
"undici": "^7.9.0",
"vite": "^6.3.5",
"resolve": "^1.22.11",
"semver": "^7.7.3",
"set-cookie-parser": "^2.7.2",
"tsx": "^4.20.6",
"typescript": "^5.9.3",
"undici": "^7.16.0",
"vite": "^6.4.1",
"vitest": "^3.2.4"
},
"pnpm": {
+78 -39
View File
@@ -1,4 +1,4 @@
import { DataSyncConfig } from '@lobechat/electron-client-ipc';
import { DataSyncConfig, MarketAuthorizationParams } from '@lobechat/electron-client-ipc';
import { BrowserWindow, shell } from 'electron';
import crypto from 'node:crypto';
import querystring from 'node:querystring';
@@ -14,39 +14,38 @@ const logger = createLogger('controllers:AuthCtr');
/**
* Authentication Controller
* 使用中间页 + 轮询的方式实现 OAuth 授权流程
* Implements OAuth authorization flow using intermediate page + polling mechanism
*/
export default class AuthCtr extends ControllerModule {
/**
* 远程服务器配置控制器
* Remote server configuration controller
*/
private get remoteServerConfigCtr() {
return this.app.getController(RemoteServerConfigCtr);
}
/**
* 当前的 PKCE 参数
* Current PKCE parameters
*/
private codeVerifier: string | null = null;
private authRequestState: string | null = null;
/**
* 轮询相关参数
* Polling related parameters
*/
// eslint-disable-next-line no-undef
private pollingInterval: NodeJS.Timeout | null = null;
private cachedRemoteUrl: string | null = null;
/**
* 自动刷新定时器
* Auto-refresh timer
*/
// eslint-disable-next-line no-undef
private autoRefreshTimer: NodeJS.Timeout | null = null;
/**
* 构造 redirect_uri,确保授权和令牌交换时使用相同的 URI
* @param remoteUrl 远程服务器 URL
* @param includeHandoffId 是否包含 handoff ID(仅在授权时需要)
* Construct redirect_uri, ensuring the same URI is used for authorization and token exchange
* @param remoteUrl Remote server URL
*/
private constructRedirectUri(remoteUrl: string): string {
const callbackUrl = new URL('/oidc/callback/desktop', remoteUrl);
@@ -59,9 +58,12 @@ export default class AuthCtr extends ControllerModule {
*/
@ipcClientEvent('requestAuthorization')
async requestAuthorization(config: DataSyncConfig) {
// Clear any old authorization state
this.clearAuthorizationState();
const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl(config);
// 缓存远程服务器 URL 用于后续轮询
// Cache remote server URL for subsequent polling
this.cachedRemoteUrl = remoteUrl;
logger.info(
@@ -114,6 +116,31 @@ export default class AuthCtr extends ControllerModule {
}
}
/**
* Request Market OAuth authorization (desktop)
*/
@ipcClientEvent('requestMarketAuthorization')
async requestMarketAuthorization(params: MarketAuthorizationParams) {
const { authUrl } = params;
if (!authUrl) {
const errorMessage = 'Market authorization URL is required';
logger.error(errorMessage);
return { error: errorMessage, success: false };
}
logger.info(`Requesting market authorization via: ${authUrl}`);
try {
await shell.openExternal(authUrl);
logger.debug('Opening market authorization URL in default browser');
return { success: true };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('Market authorization request failed:', error);
return { error: message, success: false };
}
}
/**
* 启动轮询机制获取凭证
*/
@@ -133,7 +160,7 @@ export default class AuthCtr extends ControllerModule {
// Check if polling has timed out
if (Date.now() - startTime > maxPollTime) {
logger.warn('Credential polling timed out');
this.stopPolling();
this.clearAuthorizationState();
this.broadcastAuthorizationFailed('Authorization timed out');
return;
}
@@ -167,14 +194,14 @@ export default class AuthCtr extends ControllerModule {
}
} catch (error) {
logger.error('Error during credential polling:', error);
this.stopPolling();
this.clearAuthorizationState();
this.broadcastAuthorizationFailed('Polling error: ' + error.message);
}
}, pollInterval);
}
/**
* 停止轮询
* Stop polling
*/
private stopPolling() {
if (this.pollingInterval) {
@@ -184,18 +211,30 @@ export default class AuthCtr extends ControllerModule {
}
/**
* 启动自动刷新定时器
* Clear authorization state
* Called before starting a new authorization flow or after authorization failure/timeout
*/
private clearAuthorizationState() {
logger.debug('Clearing authorization state');
this.stopPolling();
this.codeVerifier = null;
this.authRequestState = null;
this.cachedRemoteUrl = null;
}
/**
* Start auto-refresh timer
*/
private startAutoRefresh() {
// 先停止现有的定时器
// Stop existing timer first
this.stopAutoRefresh();
const checkInterval = 2 * 60 * 1000; // 每 2 分钟检查一次
const checkInterval = 2 * 60 * 1000; // Check every 2 minutes
logger.debug('Starting auto-refresh timer');
this.autoRefreshTimer = setInterval(async () => {
try {
// 检查 token 是否即将过期 (提前 5 分钟刷新)
// Check if token is expiring soon (refresh 5 minutes in advance)
if (this.remoteServerConfigCtr.isTokenExpiringSoon()) {
const expiresAt = this.remoteServerConfigCtr.getTokenExpiresAt();
logger.info(
@@ -208,7 +247,7 @@ export default class AuthCtr extends ControllerModule {
this.broadcastTokenRefreshed();
} else {
logger.error(`Auto-refresh failed: ${result.error}`);
// 如果自动刷新失败,停止定时器并清除 token
// If auto-refresh fails, stop timer and clear token
this.stopAutoRefresh();
await this.remoteServerConfigCtr.clearTokens();
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
@@ -222,7 +261,7 @@ export default class AuthCtr extends ControllerModule {
}
/**
* 停止自动刷新定时器
* Stop auto-refresh timer
*/
private stopAutoRefresh() {
if (this.autoRefreshTimer) {
@@ -233,8 +272,8 @@ export default class AuthCtr extends ControllerModule {
}
/**
* 轮询获取凭证
* 直接发送 HTTP 请求到远程服务器
* Poll for credentials
* Sends HTTP request directly to remote server
*/
private async pollForCredentials(): Promise<{ code: string; state: string } | null> {
if (!this.authRequestState || !this.cachedRemoteUrl) {
@@ -242,17 +281,17 @@ export default class AuthCtr extends ControllerModule {
}
try {
// 使用缓存的远程服务器 URL
// Use cached remote server URL
const remoteUrl = this.cachedRemoteUrl;
// 构造请求 URL
// Construct request URL
const url = new URL('/oidc/handoff', remoteUrl);
url.searchParams.set('id', this.authRequestState);
url.searchParams.set('client', 'desktop');
logger.debug(`Polling for credentials: ${url.toString()}`);
// 直接发送 HTTP 请求
// Send HTTP request directly
const response = await fetch(url.toString(), {
headers: {
'Content-Type': 'application/json',
@@ -260,9 +299,9 @@ export default class AuthCtr extends ControllerModule {
method: 'GET',
});
// 检查响应状态
// Check response status
if (response.status === 404) {
// 凭证还未准备好,这是正常情况
// Credentials not ready yet, this is normal
return null;
}
@@ -270,7 +309,7 @@ export default class AuthCtr extends ControllerModule {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// 解析响应数据
// Parse response data
const data = (await response.json()) as {
data: {
id: string;
@@ -511,7 +550,7 @@ export default class AuthCtr extends ControllerModule {
}
/**
* 应用启动后初始化
* Initialize after app is ready
*/
afterAppReady() {
logger.debug('AuthCtr initialized, checking for existing tokens');
@@ -519,7 +558,7 @@ export default class AuthCtr extends ControllerModule {
}
/**
* 清理所有定时器
* Clean up all timers
*/
cleanup() {
logger.debug('Cleaning up AuthCtr timers');
@@ -528,14 +567,14 @@ export default class AuthCtr extends ControllerModule {
}
/**
* 初始化自动刷新功能
* 在应用启动时检查是否有有效的 token,如果有就启动自动刷新定时器
* Initialize auto-refresh functionality
* Checks for valid token at app startup and starts auto-refresh timer if token exists
*/
private async initializeAutoRefresh() {
try {
const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
// 检查是否配置了远程服务器且处于活动状态
// Check if remote server is configured and active
if (!config.active || !config.remoteServerUrl) {
logger.debug(
'Remote server not active or configured, skipping auto-refresh initialization',
@@ -543,36 +582,36 @@ export default class AuthCtr extends ControllerModule {
return;
}
// 检查是否有有效的访问令牌
// Check if valid access token exists
const accessToken = await this.remoteServerConfigCtr.getAccessToken();
if (!accessToken) {
logger.debug('No access token found, skipping auto-refresh initialization');
return;
}
// 检查是否有过期时间信息
// Check if token expiration time exists
const expiresAt = this.remoteServerConfigCtr.getTokenExpiresAt();
if (!expiresAt) {
logger.debug('No token expiration time found, skipping auto-refresh initialization');
return;
}
// 检查 token 是否已经过期
// Check if token has already expired
const currentTime = Date.now();
if (currentTime >= expiresAt) {
logger.info('Token has expired, attempting to refresh it');
// 尝试刷新 token
// Attempt to refresh token
const refreshResult = await this.remoteServerConfigCtr.refreshAccessToken();
if (refreshResult.success) {
logger.info('Token refresh successful during initialization');
this.broadcastTokenRefreshed();
// 重新启动自动刷新定时器
// Restart auto-refresh timer
this.startAutoRefresh();
return;
} else {
logger.error(`Token refresh failed during initialization: ${refreshResult.error}`);
// 只有在刷新失败时才清除 token 并要求重新授权
// Clear token and require re-authorization only on refresh failure
await this.remoteServerConfigCtr.clearTokens();
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
this.broadcastAuthorizationRequired();
@@ -580,7 +619,7 @@ export default class AuthCtr extends ControllerModule {
}
}
// 启动自动刷新定时器
// Start auto-refresh timer
logger.info(
`Token is valid, starting auto-refresh timer. Token expires at: ${new Date(expiresAt).toISOString()}`,
);
@@ -1,7 +1,11 @@
import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
import { InterceptRouteParams, OpenSettingsWindowOptions } from '@lobechat/electron-client-ipc';
import { extractSubPath, findMatchingRoute } from '~common/routes';
import { AppBrowsersIdentifiers, BrowsersIdentifiers, WindowTemplateIdentifiers } from '@/appBrowsers';
import {
AppBrowsersIdentifiers,
BrowsersIdentifiers,
WindowTemplateIdentifiers,
} from '@/appBrowsers';
import { IpcClientEventSender } from '@/types/ipcClientEvent';
import { ControllerModule, ipcClientEvent, shortcut } from './index';
@@ -14,11 +18,16 @@ export default class BrowserWindowsCtr extends ControllerModule {
}
@ipcClientEvent('openSettingsWindow')
async openSettingsWindow(tab?: string) {
console.log('[BrowserWindowsCtr] Received request to open settings window', tab);
async openSettingsWindow(options?: string | OpenSettingsWindowOptions) {
const normalizedOptions: OpenSettingsWindowOptions =
typeof options === 'string' || options === undefined
? { tab: typeof options === 'string' ? options : undefined }
: options;
console.log('[BrowserWindowsCtr] Received request to open settings window', normalizedOptions);
try {
await this.app.browserManager.showSettingsWindowWithTab(tab);
await this.app.browserManager.showSettingsWindowWithTab(normalizedOptions);
return { success: true };
} catch (error) {
@@ -68,15 +77,37 @@ export default class BrowserWindowsCtr extends ControllerModule {
try {
if (matchedRoute.targetWindow === BrowsersIdentifiers.settings) {
const subPath = extractSubPath(path, matchedRoute.pathPrefix);
const extractedSubPath = extractSubPath(path, matchedRoute.pathPrefix);
const sanitizedSubPath =
extractedSubPath && !extractedSubPath.startsWith('?') ? extractedSubPath : undefined;
let searchParams: Record<string, string> | undefined;
try {
const url = new URL(params.url);
const entries = Array.from(url.searchParams.entries());
if (entries.length > 0) {
searchParams = entries.reduce<Record<string, string>>((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
}
} catch (error) {
console.warn(
'[BrowserWindowsCtr] Failed to parse URL for settings route interception:',
params.url,
error,
);
}
await this.app.browserManager.showSettingsWindowWithTab(subPath);
await this.app.browserManager.showSettingsWindowWithTab({
searchParams,
tab: sanitizedSubPath,
});
return {
intercepted: true,
path,
source,
subPath,
subPath: sanitizedSubPath,
targetWindow: matchedRoute.targetWindow,
};
} else {
@@ -105,8 +136,8 @@ export default class BrowserWindowsCtr extends ControllerModule {
*/
@ipcClientEvent('createMultiInstanceWindow')
async createMultiInstanceWindow(params: {
templateId: WindowTemplateIdentifiers;
path: string;
templateId: WindowTemplateIdentifiers;
uniqueId?: string;
}) {
try {
+299 -52
View File
@@ -1,4 +1,10 @@
import {
EditLocalFileParams,
EditLocalFileResult,
GlobFilesParams,
GlobFilesResult,
GrepContentParams,
GrepContentResult,
ListLocalFileParams,
LocalMoveFilesResultItem,
LocalReadFileParams,
@@ -13,10 +19,10 @@ import {
} from '@lobechat/electron-client-ipc';
import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
import { shell } from 'electron';
import * as fs from 'node:fs';
import { rename as renamePromise } from 'node:fs/promises';
import fg from 'fast-glob';
import { Stats, constants } from 'node:fs';
import { access, mkdir, readFile, readdir, rename, stat, writeFile } from 'node:fs/promises';
import * as path from 'node:path';
import { promisify } from 'node:util';
import FileSearchService from '@/services/fileSearchSrv';
import { FileResult, SearchOptions } from '@/types/fileSearch';
@@ -25,40 +31,15 @@ import { createLogger } from '@/utils/logger';
import { ControllerModule, ipcClientEvent } from './index';
// 创建日志记录器
// Create logger
const logger = createLogger('controllers:LocalFileCtr');
const statPromise = promisify(fs.stat);
const readdirPromise = promisify(fs.readdir);
const renamePromiseFs = promisify(fs.rename);
const accessPromise = promisify(fs.access);
const writeFilePromise = promisify(fs.writeFile);
export default class LocalFileCtr extends ControllerModule {
private get searchService() {
return this.app.getService(FileSearchService);
}
/**
* Handle IPC event for local file search
*/
@ipcClientEvent('searchLocalFiles')
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
logger.debug('Received file search request:', { keywords: params.keywords });
const options: Omit<SearchOptions, 'keywords'> = {
limit: 30,
};
try {
const results = await this.searchService.search(params.keywords, options);
logger.debug('File search completed', { count: results.length });
return results;
} catch (error) {
logger.error('File search failed:', error);
return [];
}
}
// ==================== File Operation ====================
@ipcClientEvent('openLocalFile')
async handleOpenLocalFile({ path: filePath }: OpenLocalFileParams): Promise<{
@@ -102,7 +83,7 @@ export default class LocalFileCtr extends ControllerModule {
const results: LocalReadFileResult[] = [];
for (const filePath of paths) {
// 初始化结果对象
// Initialize result object
logger.debug('Reading single file:', { filePath });
const result = await this.readFile({ path: filePath });
results.push(result);
@@ -158,7 +139,7 @@ export default class LocalFileCtr extends ControllerModule {
};
try {
const stats = await statPromise(filePath);
const stats = await stat(filePath);
if (stats.isDirectory()) {
logger.warn('Attempted to read directory content:', { filePath });
result.content = 'This is a directory and cannot be read as plain text.';
@@ -197,7 +178,7 @@ export default class LocalFileCtr extends ControllerModule {
const results: FileResult[] = [];
try {
const entries = await readdirPromise(dirPath);
const entries = await readdir(dirPath);
logger.debug('Directory entries retrieved successfully:', {
dirPath,
entriesCount: entries.length,
@@ -212,7 +193,7 @@ export default class LocalFileCtr extends ControllerModule {
const fullPath = path.join(dirPath, entry);
try {
const stats = await statPromise(fullPath);
const stats = await stat(fullPath);
const isDirectory = stats.isDirectory();
results.push({
createdTime: stats.birthtime,
@@ -260,7 +241,7 @@ export default class LocalFileCtr extends ControllerModule {
return [];
}
// 逐个处理移动请求
// Process each move request
for (const item of items) {
const { oldPath: sourcePath, newPath } = item;
const logPrefix = `[Moving file ${sourcePath} -> ${newPath}]`;
@@ -272,7 +253,7 @@ export default class LocalFileCtr extends ControllerModule {
success: false,
};
// 基本验证
// Basic validation
if (!sourcePath || !newPath) {
logger.error(`${logPrefix} Parameter validation failed: source or target path is empty`);
resultItem.error = 'Both oldPath and newPath are required for each item.';
@@ -281,9 +262,9 @@ export default class LocalFileCtr extends ControllerModule {
}
try {
// 检查源是否存在
// Check if source exists
try {
await accessPromise(sourcePath, fs.constants.F_OK);
await access(sourcePath, constants.F_OK);
logger.debug(`${logPrefix} Source file exists`);
} catch (accessError: any) {
if (accessError.code === 'ENOENT') {
@@ -297,28 +278,28 @@ export default class LocalFileCtr extends ControllerModule {
}
}
// 检查目标路径是否与源路径相同
// Check if target path is the same as source path
if (path.normalize(sourcePath) === path.normalize(newPath)) {
logger.info(`${logPrefix} Source and target paths are identical, skipping move`);
resultItem.success = true;
resultItem.newPath = newPath; // 即使未移动,也报告目标路径
resultItem.newPath = newPath; // Report target path even if not moved
results.push(resultItem);
continue;
}
// LBYL: 确保目标目录存在
// LBYL: Ensure target directory exists
const targetDir = path.dirname(newPath);
makeSureDirExist(targetDir);
logger.debug(`${logPrefix} Ensured target directory exists: ${targetDir}`);
// 执行移动 (rename)
await renamePromiseFs(sourcePath, newPath);
// Execute move (rename)
await rename(sourcePath, newPath);
resultItem.success = true;
resultItem.newPath = newPath;
logger.info(`${logPrefix} Move successful`);
} catch (error) {
logger.error(`${logPrefix} Move failed:`, error);
// 使用与 handleMoveFile 类似的错误处理逻辑
// Use similar error handling logic as handleMoveFile
let errorMessage = (error as Error).message;
if ((error as any).code === 'ENOENT')
errorMessage = `Source path not found: ${sourcePath}.`;
@@ -334,7 +315,7 @@ export default class LocalFileCtr extends ControllerModule {
errorMessage = `The target directory ${newPath} is not empty (relevant on some systems if target exists and is a directory).`;
else if ((error as any).code === 'EEXIST')
errorMessage = `An item already exists at the target path: ${newPath}.`;
// 保留来自访问检查或目录检查的更具体错误
// Keep more specific errors from access or directory checks
else if (
!errorMessage.startsWith('Source path not found') &&
!errorMessage.startsWith('Permission denied accessing source path') &&
@@ -411,9 +392,9 @@ export default class LocalFileCtr extends ControllerModule {
};
}
// Perform the rename operation using fs.promises.rename directly
// Perform the rename operation using rename directly
try {
await renamePromise(currentPath, newPath);
await rename(currentPath, newPath);
logger.info(`${logPrefix} Rename successful: ${currentPath} -> ${newPath}`);
// Optionally return the newPath if frontend needs it
// return { success: true, newPath: newPath };
@@ -444,7 +425,7 @@ export default class LocalFileCtr extends ControllerModule {
const logPrefix = `[Writing file ${filePath}]`;
logger.debug(`${logPrefix} Starting to write file`, { contentLength: content?.length });
// 验证参数
// Validate parameters
if (!filePath) {
logger.error(`${logPrefix} Parameter validation failed: path is empty`);
return { error: 'Path cannot be empty', success: false };
@@ -456,14 +437,14 @@ export default class LocalFileCtr extends ControllerModule {
}
try {
// 确保目标目录存在
// Ensure target directory exists (use async to avoid blocking main thread)
const dirname = path.dirname(filePath);
logger.debug(`${logPrefix} Creating directory: ${dirname}`);
fs.mkdirSync(dirname, { recursive: true });
await mkdir(dirname, { recursive: true });
// 写入文件内容
// Write file content
logger.debug(`${logPrefix} Starting to write content to file`);
await writeFilePromise(filePath, content, 'utf8');
await writeFile(filePath, content, 'utf8');
logger.info(`${logPrefix} File written successfully`, {
path: filePath,
size: content.length,
@@ -478,4 +459,270 @@ export default class LocalFileCtr extends ControllerModule {
};
}
}
// ==================== Search & Find ====================
/**
* Handle IPC event for local file search
*/
@ipcClientEvent('searchLocalFiles')
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
logger.debug('Received file search request:', {
directory: params.directory,
keywords: params.keywords,
});
// Build search options from params, mapping directory to onlyIn
const options: SearchOptions = {
contentContains: params.contentContains,
createdAfter: params.createdAfter ? new Date(params.createdAfter) : undefined,
createdBefore: params.createdBefore ? new Date(params.createdBefore) : undefined,
detailed: params.detailed,
exclude: params.exclude,
fileTypes: params.fileTypes,
keywords: params.keywords,
limit: params.limit || 30,
liveUpdate: params.liveUpdate,
modifiedAfter: params.modifiedAfter ? new Date(params.modifiedAfter) : undefined,
modifiedBefore: params.modifiedBefore ? new Date(params.modifiedBefore) : undefined,
onlyIn: params.directory, // Map directory param to onlyIn option
sortBy: params.sortBy,
sortDirection: params.sortDirection,
};
try {
const results = await this.searchService.search(options.keywords, options);
logger.debug('File search completed', {
count: results.length,
directory: params.directory,
});
return results;
} catch (error) {
logger.error('File search failed:', error);
return [];
}
}
@ipcClientEvent('grepContent')
async handleGrepContent(params: GrepContentParams): Promise<GrepContentResult> {
const {
pattern,
path: searchPath = process.cwd(),
output_mode = 'files_with_matches',
} = params;
const logPrefix = `[grepContent: ${pattern}]`;
logger.debug(`${logPrefix} Starting content search`, { output_mode, searchPath });
try {
const regex = new RegExp(
pattern,
`g${params['-i'] ? 'i' : ''}${params.multiline ? 's' : ''}`,
);
// Determine files to search
let filesToSearch: string[] = [];
const stats = await stat(searchPath);
if (stats.isFile()) {
filesToSearch = [searchPath];
} else {
// Use glob pattern if provided, otherwise search all files
const globPattern = params.glob || '**/*';
filesToSearch = await fg(globPattern, {
absolute: true,
cwd: searchPath,
dot: true,
ignore: ['**/node_modules/**', '**/.git/**'],
});
// Filter by type if provided
if (params.type) {
const ext = `.${params.type}`;
filesToSearch = filesToSearch.filter((file) => file.endsWith(ext));
}
}
logger.debug(`${logPrefix} Found ${filesToSearch.length} files to search`);
const matches: string[] = [];
let totalMatches = 0;
for (const filePath of filesToSearch) {
try {
const fileStats = await stat(filePath);
if (!fileStats.isFile()) continue;
const content = await readFile(filePath, 'utf8');
const lines = content.split('\n');
switch (output_mode) {
case 'files_with_matches': {
if (regex.test(content)) {
matches.push(filePath);
totalMatches++;
if (params.head_limit && matches.length >= params.head_limit) break;
}
break;
}
case 'content': {
const matchedLines: string[] = [];
for (let i = 0; i < lines.length; i++) {
if (regex.test(lines[i])) {
const contextBefore = params['-B'] || params['-C'] || 0;
const contextAfter = params['-A'] || params['-C'] || 0;
const startLine = Math.max(0, i - contextBefore);
const endLine = Math.min(lines.length - 1, i + contextAfter);
for (let j = startLine; j <= endLine; j++) {
const lineNum = params['-n'] ? `${j + 1}:` : '';
matchedLines.push(`${filePath}:${lineNum}${lines[j]}`);
}
totalMatches++;
}
}
matches.push(...matchedLines);
if (params.head_limit && matches.length >= params.head_limit) break;
break;
}
case 'count': {
const fileMatches = (content.match(regex) || []).length;
if (fileMatches > 0) {
matches.push(`${filePath}:${fileMatches}`);
totalMatches += fileMatches;
}
break;
}
}
} catch (error) {
logger.debug(`${logPrefix} Skipping file ${filePath}:`, error);
}
}
logger.info(`${logPrefix} Search completed`, {
matchCount: matches.length,
totalMatches,
});
return {
matches: params.head_limit ? matches.slice(0, params.head_limit) : matches,
success: true,
total_matches: totalMatches,
};
} catch (error) {
logger.error(`${logPrefix} Grep failed:`, error);
return {
matches: [],
success: false,
total_matches: 0,
};
}
}
@ipcClientEvent('globLocalFiles')
async handleGlobFiles({
path: searchPath = process.cwd(),
pattern,
}: GlobFilesParams): Promise<GlobFilesResult> {
const logPrefix = `[globFiles: ${pattern}]`;
logger.debug(`${logPrefix} Starting glob search`, { searchPath });
try {
const files = await fg(pattern, {
absolute: true,
cwd: searchPath,
dot: true,
onlyFiles: false,
stats: true,
});
// Sort by modification time (most recent first)
const sortedFiles = (files as unknown as Array<{ path: string; stats: Stats }>)
.sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime())
.map((f) => f.path);
logger.info(`${logPrefix} Glob completed`, { fileCount: sortedFiles.length });
return {
files: sortedFiles,
success: true,
total_files: sortedFiles.length,
};
} catch (error) {
logger.error(`${logPrefix} Glob failed:`, error);
return {
files: [],
success: false,
total_files: 0,
};
}
}
// ==================== File Editing ====================
@ipcClientEvent('editLocalFile')
async handleEditFile({
file_path: filePath,
new_string,
old_string,
replace_all = false,
}: EditLocalFileParams): Promise<EditLocalFileResult> {
const logPrefix = `[editFile: ${filePath}]`;
logger.debug(`${logPrefix} Starting file edit`, { replace_all });
try {
// Read file content
const content = await readFile(filePath, 'utf8');
// Check if old_string exists
if (!content.includes(old_string)) {
logger.error(`${logPrefix} Old string not found in file`);
return {
error: 'The specified old_string was not found in the file',
replacements: 0,
success: false,
};
}
// Perform replacement
let newContent: string;
let replacements: number;
if (replace_all) {
const regex = new RegExp(old_string.replaceAll(/[$()*+.?[\\\]^{|}]/g, '\\$&'), 'g');
const matches = content.match(regex);
replacements = matches ? matches.length : 0;
newContent = content.replaceAll(old_string, new_string);
} else {
// Replace only first occurrence
const index = content.indexOf(old_string);
if (index === -1) {
return {
error: 'Old string not found',
replacements: 0,
success: false,
};
}
newContent =
content.slice(0, index) + new_string + content.slice(index + old_string.length);
replacements = 1;
}
// Write back to file
await writeFile(filePath, newContent, 'utf8');
logger.info(`${logPrefix} File edited successfully`, { replacements });
return {
replacements,
success: true,
};
} catch (error) {
logger.error(`${logPrefix} Edit failed:`, error);
return {
error: (error as Error).message,
replacements: 0,
success: false,
};
}
}
}
+5 -5
View File
@@ -2,16 +2,16 @@ import { ControllerModule, ipcClientEvent } from './index';
export default class MenuController extends ControllerModule {
/**
* 刷新菜单
* Refresh menu
*/
@ipcClientEvent('refreshAppMenu')
refreshAppMenu() {
// 注意:可能需要根据具体情况决定是否允许渲染进程刷新所有菜单
// Note: May need to decide whether to allow renderer process to refresh all menus based on specific circumstances
return this.app.menuManager.refreshMenus();
}
/**
* 显示上下文菜单
* Show context menu
*/
@ipcClientEvent('showContextMenu')
showContextMenu(params: { data?: any; type: string }) {
@@ -19,11 +19,11 @@ export default class MenuController extends ControllerModule {
}
/**
* 设置开发菜单可见性
* Set development menu visibility
*/
@ipcClientEvent('setDevMenuVisibility')
setDevMenuVisibility(visible: boolean) {
// 调用 MenuManager 的方法来重建应用菜单
// Call MenuManager method to rebuild application menu
return this.app.menuManager.rebuildAppMenu({ showDevItems: visible });
}
}
@@ -13,31 +13,31 @@ const logger = createLogger('controllers:NotificationCtr');
export default class NotificationCtr extends ControllerModule {
/**
* 在应用准备就绪后设置桌面通知
* Set up desktop notifications after the application is ready
*/
afterAppReady() {
this.setupNotifications();
}
/**
* 设置桌面通知权限和配置
* Set up desktop notification permissions and configuration
*/
private setupNotifications() {
logger.debug('Setting up desktop notifications');
try {
// 检查通知支持
// Check notification support
if (!Notification.isSupported()) {
logger.warn('Desktop notifications are not supported on this platform');
return;
}
// macOS 上,我们可能需要显式请求通知权限
// On macOS, we may need to explicitly request notification permissions
if (macOS()) {
logger.debug('macOS detected, notification permissions should be handled by system');
}
// 在 Windows 上设置应用用户模型 ID
// Set app user model ID on Windows
if (windows()) {
app.setAppUserModelId('com.lobehub.chat');
logger.debug('Set Windows App User Model ID for notifications');
@@ -49,34 +49,34 @@ export default class NotificationCtr extends ControllerModule {
}
}
/**
* 显示系统桌面通知(仅当窗口隐藏时)
* Show system desktop notification (only when window is hidden)
*/
@ipcClientEvent('showDesktopNotification')
async showDesktopNotification(
params: ShowDesktopNotificationParams,
): Promise<DesktopNotificationResult> {
logger.debug('收到桌面通知请求:', params);
logger.debug('Received desktop notification request:', params);
try {
// 检查通知支持
// Check notification support
if (!Notification.isSupported()) {
logger.warn('系统不支持桌面通知');
logger.warn('System does not support desktop notifications');
return { error: 'Desktop notifications not supported', success: false };
}
// 检查窗口是否隐藏
// Check if window is hidden
const isWindowHidden = this.isMainWindowHidden();
if (!isWindowHidden) {
logger.debug('主窗口可见,跳过桌面通知');
logger.debug('Main window is visible, skipping desktop notification');
return { reason: 'Window is visible', skipped: true, success: true };
}
logger.info('窗口已隐藏,显示桌面通知:', params.title);
logger.info('Window is hidden, showing desktop notification:', params.title);
const notification = new Notification({
body: params.body,
// 添加更多配置以确保通知能正常显示
// Add more configuration to ensure notifications display properly
hasReply: false,
silent: params.silent || false,
timeoutType: 'default',
@@ -84,38 +84,38 @@ export default class NotificationCtr extends ControllerModule {
urgency: 'normal',
});
// 添加更多事件监听来调试
// Add more event listeners for debugging
notification.on('show', () => {
logger.info('通知已显示');
logger.info('Notification shown');
});
notification.on('click', () => {
logger.debug('用户点击通知,显示主窗口');
logger.debug('User clicked notification, showing main window');
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.show();
mainWindow.browserWindow.focus();
});
notification.on('close', () => {
logger.debug('通知已关闭');
logger.debug('Notification closed');
});
notification.on('failed', (error) => {
logger.error('通知显示失败:', error);
logger.error('Notification display failed:', error);
});
// 使用 Promise 来确保通知显示
// Use Promise to ensure notification is shown
return new Promise((resolve) => {
notification.show();
// 给通知一些时间来显示,然后检查结果
// Give the notification some time to display, then check the result
setTimeout(() => {
logger.info('通知显示调用完成');
logger.info('Notification display call completed');
resolve({ success: true });
}, 100);
});
} catch (error) {
logger.error('显示桌面通知失败:', error);
logger.error('Failed to show desktop notification:', error);
return {
error: error instanceof Error ? error.message : 'Unknown error',
success: false,
@@ -124,7 +124,7 @@ export default class NotificationCtr extends ControllerModule {
}
/**
* 检查主窗口是否隐藏
* Check if the main window is hidden
*/
@ipcClientEvent('isMainWindowHidden')
isMainWindowHidden(): boolean {
@@ -132,23 +132,23 @@ export default class NotificationCtr extends ControllerModule {
const mainWindow = this.app.browserManager.getMainWindow();
const browserWindow = mainWindow.browserWindow;
// 如果窗口被销毁,认为是隐藏的
// If window is destroyed, consider it hidden
if (browserWindow.isDestroyed()) {
return true;
}
// 检查窗口是否可见和聚焦
// Check if window is visible and focused
const isVisible = browserWindow.isVisible();
const isFocused = browserWindow.isFocused();
const isMinimized = browserWindow.isMinimized();
logger.debug('窗口状态检查:', { isFocused, isMinimized, isVisible });
logger.debug('Window state check:', { isFocused, isMinimized, isVisible });
// 窗口隐藏的条件:不可见或最小化或失去焦点
// Window is hidden if: not visible, minimized, or not focused
return !isVisible || isMinimized || !isFocused;
} catch (error) {
logger.error('检查窗口状态失败:', error);
return true; // 发生错误时认为窗口隐藏,确保通知能显示
logger.error('Failed to check window state:', error);
return true; // Consider window hidden on error to ensure notifications can be shown
}
}
}
@@ -246,8 +246,8 @@ export default class RemoteServerConfigCtr extends ControllerModule {
}
/**
* 刷新访问令牌
* 使用存储的刷新令牌获取新的访问令牌
* Refresh access token
* Use stored refresh token to obtain a new access token
* Handles concurrent requests by returning the existing refresh promise if one is in progress.
*/
async refreshAccessToken(): Promise<{ error?: string; success: boolean }> {
@@ -271,27 +271,27 @@ export default class RemoteServerConfigCtr extends ControllerModule {
*/
private async performTokenRefresh(): Promise<{ error?: string; success: boolean }> {
try {
// 获取配置信息
// Get configuration information
const config = await this.getRemoteServerConfig();
if (!config.remoteServerUrl || !config.active) {
logger.warn('Remote server not active or configured, skipping refresh.');
return { error: '远程服务器未激活或未配置', success: false };
return { error: 'Remote server is not active or configured', success: false };
}
// 获取刷新令牌
// Get refresh token
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
logger.error('No refresh token available for refresh operation.');
return { error: '没有可用的刷新令牌', success: false };
return { error: 'No refresh token available', success: false };
}
// 构造刷新请求
// Construct refresh request
const remoteUrl = await this.getRemoteServerUrl(config);
const tokenUrl = new URL('/oidc/token', remoteUrl);
// 构造请求体
// Construct request body
const body = querystring.stringify({
client_id: 'lobehub-desktop',
grant_type: 'refresh_token',
@@ -300,7 +300,7 @@ export default class RemoteServerConfigCtr extends ControllerModule {
logger.debug(`Sending token refresh request to ${tokenUrl.toString()}`);
// 发送请求
// Send request
const response = await fetch(tokenUrl.toString(), {
body,
headers: {
@@ -310,25 +310,25 @@ export default class RemoteServerConfigCtr extends ControllerModule {
});
if (!response.ok) {
// 尝试解析错误响应
// Try to parse error response
const errorData = await response.json().catch(() => ({}));
const errorMessage = `刷新令牌失败: ${response.status} ${response.statusText} ${
const errorMessage = `Token refresh failed: ${response.status} ${response.statusText} ${
errorData.error_description || errorData.error || ''
}`.trim();
logger.error(errorMessage, errorData);
return { error: errorMessage, success: false };
}
// 解析响应
// Parse response
const data = await response.json();
// 检查响应中是否包含必要令牌
// Check if response contains necessary tokens
if (!data.access_token || !data.refresh_token) {
logger.error('Refresh response missing access_token or refresh_token', data);
return { error: '刷新响应中缺少令牌', success: false };
return { error: 'Missing tokens in refresh response', success: false };
}
// 保存新令牌
// Save new tokens
logger.info('Token refresh successful, saving new tokens.');
await this.saveTokens(data.access_token, data.refresh_token, data.expires_in);
@@ -336,7 +336,7 @@ export default class RemoteServerConfigCtr extends ControllerModule {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Exception during token refresh operation:', errorMessage, error);
return { error: `刷新令牌时发生异常: ${errorMessage}`, success: false };
return { error: `Exception occurred during token refresh: ${errorMessage}`, success: false };
} finally {
// Ensure the promise reference is cleared once the operation completes
logger.debug('Clearing the refresh promise reference.');
@@ -0,0 +1,242 @@
import {
GetCommandOutputParams,
GetCommandOutputResult,
KillCommandParams,
KillCommandResult,
RunCommandParams,
RunCommandResult,
} from '@lobechat/electron-client-ipc';
import { ChildProcess, spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { createLogger } from '@/utils/logger';
import { ControllerModule, ipcClientEvent } from './index';
const logger = createLogger('controllers:ShellCommandCtr');
interface ShellProcess {
lastReadStderr: number;
lastReadStdout: number;
process: ChildProcess;
stderr: string[];
stdout: string[];
}
export default class ShellCommandCtr extends ControllerModule {
// Shell process management
private shellProcesses = new Map<string, ShellProcess>();
@ipcClientEvent('runCommand')
async handleRunCommand({
command,
description,
run_in_background,
timeout = 120_000,
}: RunCommandParams): Promise<RunCommandResult> {
const logPrefix = `[runCommand: ${description || command.slice(0, 50)}]`;
logger.debug(`${logPrefix} Starting command execution`, {
background: run_in_background,
timeout,
});
// Validate timeout
const effectiveTimeout = Math.min(Math.max(timeout, 1000), 600_000);
// Cross-platform shell selection
const shellConfig =
process.platform === 'win32'
? { args: ['/c', command], cmd: 'cmd.exe' }
: { args: ['-c', command], cmd: '/bin/sh' };
try {
if (run_in_background) {
// Background execution
const shellId = randomUUID();
const childProcess = spawn(shellConfig.cmd, shellConfig.args, {
env: process.env,
shell: false,
});
const shellProcess: ShellProcess = {
lastReadStderr: 0,
lastReadStdout: 0,
process: childProcess,
stderr: [],
stdout: [],
};
// Capture output
childProcess.stdout?.on('data', (data) => {
shellProcess.stdout.push(data.toString());
});
childProcess.stderr?.on('data', (data) => {
shellProcess.stderr.push(data.toString());
});
childProcess.on('exit', (code) => {
logger.debug(`${logPrefix} Background process exited`, { code, shellId });
});
this.shellProcesses.set(shellId, shellProcess);
logger.info(`${logPrefix} Started background execution`, { shellId });
return {
shell_id: shellId,
success: true,
};
} else {
// Synchronous execution with timeout
return new Promise((resolve) => {
const childProcess = spawn(shellConfig.cmd, shellConfig.args, {
env: process.env,
shell: false,
});
let stdout = '';
let stderr = '';
let killed = false;
const timeoutHandle = setTimeout(() => {
killed = true;
childProcess.kill();
resolve({
error: `Command timed out after ${effectiveTimeout}ms`,
stderr,
stdout,
success: false,
});
}, effectiveTimeout);
childProcess.stdout?.on('data', (data) => {
stdout += data.toString();
});
childProcess.stderr?.on('data', (data) => {
stderr += data.toString();
});
childProcess.on('exit', (code) => {
if (!killed) {
clearTimeout(timeoutHandle);
const success = code === 0;
logger.info(`${logPrefix} Command completed`, { code, success });
resolve({
exit_code: code || 0,
output: stdout + stderr,
stderr,
stdout,
success,
});
}
});
childProcess.on('error', (error) => {
clearTimeout(timeoutHandle);
logger.error(`${logPrefix} Command failed:`, error);
resolve({
error: error.message,
stderr,
stdout,
success: false,
});
});
});
}
} catch (error) {
logger.error(`${logPrefix} Failed to execute command:`, error);
return {
error: (error as Error).message,
success: false,
};
}
}
@ipcClientEvent('getCommandOutput')
async handleGetCommandOutput({
filter,
shell_id,
}: GetCommandOutputParams): Promise<GetCommandOutputResult> {
const logPrefix = `[getCommandOutput: ${shell_id}]`;
logger.debug(`${logPrefix} Retrieving output`);
const shellProcess = this.shellProcesses.get(shell_id);
if (!shellProcess) {
logger.error(`${logPrefix} Shell process not found`);
return {
error: `Shell ID ${shell_id} not found`,
output: '',
running: false,
stderr: '',
stdout: '',
success: false,
};
}
const { lastReadStderr, lastReadStdout, process: childProcess, stderr, stdout } = shellProcess;
// Get new output since last read
const newStdout = stdout.slice(lastReadStdout).join('');
const newStderr = stderr.slice(lastReadStderr).join('');
let output = newStdout + newStderr;
// Apply filter if provided
if (filter) {
try {
const regex = new RegExp(filter, 'gm');
const lines = output.split('\n');
output = lines.filter((line) => regex.test(line)).join('\n');
} catch (error) {
logger.error(`${logPrefix} Invalid filter regex:`, error);
}
}
// Update last read positions separately
shellProcess.lastReadStdout = stdout.length;
shellProcess.lastReadStderr = stderr.length;
const running = childProcess.exitCode === null;
logger.debug(`${logPrefix} Output retrieved`, {
outputLength: output.length,
running,
});
return {
output,
running,
stderr: newStderr,
stdout: newStdout,
success: true,
};
}
@ipcClientEvent('killCommand')
async handleKillCommand({ shell_id }: KillCommandParams): Promise<KillCommandResult> {
const logPrefix = `[killCommand: ${shell_id}]`;
logger.debug(`${logPrefix} Attempting to kill shell`);
const shellProcess = this.shellProcesses.get(shell_id);
if (!shellProcess) {
logger.error(`${logPrefix} Shell process not found`);
return {
error: `Shell ID ${shell_id} not found`,
success: false,
};
}
try {
shellProcess.process.kill();
this.shellProcesses.delete(shell_id);
logger.info(`${logPrefix} Shell killed successfully`);
return { success: true };
} catch (error) {
logger.error(`${logPrefix} Failed to kill shell:`, error);
return {
error: (error as Error).message,
success: false,
};
}
}
}
@@ -4,7 +4,7 @@ import { ControllerModule, ipcClientEvent } from '.';
export default class ShortcutController extends ControllerModule {
/**
* 获取所有快捷键配置
* Get all shortcut configurations
*/
@ipcClientEvent('getShortcutsConfig')
getShortcutsConfig() {
@@ -12,7 +12,7 @@ export default class ShortcutController extends ControllerModule {
}
/**
* 更新单个快捷键配置
* Update a single shortcut configuration
*/
@ipcClientEvent('updateShortcutConfig')
updateShortcutConfig({
@@ -77,6 +77,7 @@ export default class SystemController extends ControllerModule {
// 更新i18n实例的语言
await this.app.i18n.changeLanguage(locale === 'auto' ? app.getLocale() : locale);
this.app.browserManager.broadcastToAllWindows('localeChanged', { locale });
return { success: true };
}
@@ -8,24 +8,24 @@ import { createLogger } from '@/utils/logger';
import { ControllerModule, ipcClientEvent } from './index';
// 创建日志记录器
// Create logger
const logger = createLogger('controllers:TrayMenuCtr');
export default class TrayMenuCtr extends ControllerModule {
async toggleMainWindow() {
logger.debug('通过快捷键切换主窗口可见性');
logger.debug('Toggle main window visibility via shortcut');
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.toggleVisible();
}
/**
* 显示托盘气泡通知
* @param options 气泡选项
* @returns 操作结果
* Show tray balloon notification
* @param options Balloon options
* @returns Operation result
*/
@ipcClientEvent('showTrayNotification')
async showNotification(options: ShowTrayNotificationParams) {
logger.debug('显示托盘气泡通知');
logger.debug('Show tray balloon notification');
if (process.platform === 'win32') {
const mainTray = this.app.trayManager.getMainTray();
@@ -42,19 +42,19 @@ export default class TrayMenuCtr extends ControllerModule {
}
return {
error: '托盘通知仅在 Windows 平台支持',
error: 'Tray notifications are only supported on Windows platform',
success: false,
};
}
/**
* 更新托盘图标
* @param options 图标选项
* @returns 操作结果
* Update tray icon
* @param options Icon options
* @returns Operation result
*/
@ipcClientEvent('updateTrayIcon')
async updateTrayIcon(options: UpdateTrayIconParams) {
logger.debug('更新托盘图标');
logger.debug('Update tray icon');
if (process.platform === 'win32') {
const mainTray = this.app.trayManager.getMainTray();
@@ -64,7 +64,7 @@ export default class TrayMenuCtr extends ControllerModule {
mainTray.updateIcon(options.iconPath);
return { success: true };
} catch (error) {
logger.error('更新托盘图标失败:', error);
logger.error('Failed to update tray icon:', error);
return {
error: String(error),
success: false,
@@ -74,19 +74,19 @@ export default class TrayMenuCtr extends ControllerModule {
}
return {
error: '托盘功能仅在 Windows 平台支持',
error: 'Tray functionality is only supported on Windows platform',
success: false,
};
}
/**
* 更新托盘提示文本
* @param options 提示文本选项
* @returns 操作结果
* Update tray tooltip text
* @param options Tooltip text options
* @returns Operation result
*/
@ipcClientEvent('updateTrayTooltip')
async updateTrayTooltip(options: UpdateTrayTooltipParams) {
logger.debug('更新托盘提示文本');
logger.debug('Update tray tooltip text');
if (process.platform === 'win32') {
const mainTray = this.app.trayManager.getMainTray();
@@ -98,7 +98,7 @@ export default class TrayMenuCtr extends ControllerModule {
}
return {
error: '托盘功能仅在 Windows 平台支持',
error: 'Tray functionality is only supported on Windows platform',
success: false,
};
}
@@ -6,7 +6,7 @@ const logger = createLogger('controllers:UpdaterCtr');
export default class UpdaterCtr extends ControllerModule {
/**
* 检查更新
* Check for updates
*/
@ipcClientEvent('checkUpdate')
async checkForUpdates() {
@@ -15,7 +15,7 @@ export default class UpdaterCtr extends ControllerModule {
}
/**
* 下载更新
* Download update
*/
@ipcClientEvent('downloadUpdate')
async downloadUpdate() {
@@ -24,7 +24,7 @@ export default class UpdaterCtr extends ControllerModule {
}
/**
* 关闭应用并安装更新
* Quit application and install update
*/
@ipcClientEvent('installNow')
quitAndInstallUpdate() {
@@ -33,7 +33,7 @@ export default class UpdaterCtr extends ControllerModule {
}
/**
* 下次启动时安装更新
* Install update on next startup
*/
@ipcClientEvent('installLater')
installLater() {
@@ -0,0 +1,706 @@
import { DataSyncConfig } from '@lobechat/electron-client-ipc';
import { BrowserWindow, shell } from 'electron';
import crypto from 'node:crypto';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { App } from '@/core/App';
import AuthCtr from '../AuthCtr';
import RemoteServerConfigCtr from '../RemoteServerConfigCtr';
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// Mock electron
vi.mock('electron', () => ({
BrowserWindow: {
getAllWindows: vi.fn(() => []),
},
shell: {
openExternal: vi.fn().mockResolvedValue(undefined),
},
safeStorage: {
isEncryptionAvailable: vi.fn(() => true),
encryptString: vi.fn((str: string) => Buffer.from(str)),
decryptString: vi.fn((buffer: Buffer) => buffer.toString()),
},
}));
// Mock electron-is
vi.mock('electron-is', () => ({
macOS: vi.fn(() => false),
windows: vi.fn(() => false),
linux: vi.fn(() => false),
}));
// Mock OFFICIAL_CLOUD_SERVER
vi.mock('@/const/env', () => ({
OFFICIAL_CLOUD_SERVER: 'https://lobehub-cloud.com',
isMac: false,
isWindows: false,
isLinux: false,
isDev: false,
}));
// Mock crypto
let randomBytesCounter = 0;
vi.mock('node:crypto', () => ({
default: {
randomBytes: vi.fn((size: number) => {
randomBytesCounter++;
return {
toString: vi.fn(() => `mock-random-${randomBytesCounter}`),
};
}),
subtle: {
digest: vi.fn(() => Promise.resolve(new ArrayBuffer(32))),
},
},
}));
// Create mock App and RemoteServerConfigCtr
const mockRemoteServerConfigCtr = {
clearTokens: vi.fn().mockResolvedValue(undefined),
getAccessToken: vi.fn().mockResolvedValue('mock-access-token'),
getRemoteServerConfig: vi.fn().mockResolvedValue({ active: true, storageMode: 'cloud' }),
getRemoteServerUrl: vi.fn().mockImplementation(async (config?: DataSyncConfig) => {
if (config?.storageMode === 'selfHost') {
return config.remoteServerUrl || 'https://mock-server.com';
}
return 'https://lobehub-cloud.com'; // OFFICIAL_CLOUD_SERVER
}),
getTokenExpiresAt: vi.fn().mockReturnValue(Date.now() + 3600000),
isTokenExpiringSoon: vi.fn().mockReturnValue(false),
refreshAccessToken: vi.fn().mockResolvedValue({ success: true }),
saveTokens: vi.fn().mockResolvedValue(undefined),
setRemoteServerConfig: vi.fn().mockResolvedValue(true),
} as unknown as RemoteServerConfigCtr;
const mockApp = {
getController: vi.fn((ControllerClass) => {
if (ControllerClass === RemoteServerConfigCtr) {
return mockRemoteServerConfigCtr;
}
return null;
}),
} as unknown as App;
describe('AuthCtr', () => {
let authCtr: AuthCtr;
let mockFetch: ReturnType<typeof vi.fn>;
let mockWindow: any;
beforeEach(() => {
vi.clearAllMocks();
randomBytesCounter = 0; // Reset counter for each test
// Reset shell.openExternal to default successful behavior
vi.mocked(shell.openExternal).mockResolvedValue(undefined);
// Create fresh instance for each test
authCtr = new AuthCtr(mockApp);
// Mock global fetch
mockFetch = vi.fn();
global.fetch = mockFetch;
// Mock BrowserWindow with send spy
mockWindow = {
isDestroyed: vi.fn(() => false),
webContents: {
send: vi.fn(),
},
};
vi.mocked(BrowserWindow.getAllWindows).mockReturnValue([mockWindow]);
});
afterEach(() => {
// Clean up authCtr intervals (using real timers, not fake timers)
authCtr.cleanup();
// Clean up any fake timers if used
vi.clearAllTimers();
});
describe('Basic functionality', () => {
// Use real timers for all tests since setInterval with async doesn't work well with fake timers
describe('requestAuthorization', () => {
it('should generate PKCE parameters and open authorization URL', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
mockFetch.mockResolvedValue({
status: 404,
ok: false,
});
const result = await authCtr.requestAuthorization(config);
// Verify success response
expect(result).toEqual({ success: true });
// Verify shell.openExternal was called with correct URL
expect(shell.openExternal).toHaveBeenCalledWith(
expect.stringContaining('https://lobehub-cloud.com/oidc/auth'),
);
// Verify URL contains required parameters
const authUrl = vi.mocked(shell.openExternal).mock.calls[0][0];
expect(authUrl).toContain('client_id=lobehub-desktop');
expect(authUrl).toContain('response_type=code');
expect(authUrl).toContain('code_challenge_method=S256');
expect(authUrl).toContain('scope=profile%20email%20offline_access');
});
it('should start polling after authorization request', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
mockFetch.mockResolvedValue({
status: 404,
ok: false,
});
const result = await authCtr.requestAuthorization(config);
expect(result.success).toBe(true);
// Wait a bit for polling to start
await new Promise((resolve) => setTimeout(resolve, 3500));
// Verify fetch was called for polling
const pollingCalls = mockFetch.mock.calls.filter((call) =>
(call[0] as string).includes('/oidc/handoff'),
);
expect(pollingCalls.length).toBeGreaterThan(0);
});
it('should use self-hosted server URL when storageMode is selfHost', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'selfHost',
remoteServerUrl: 'https://my-custom-server.com',
};
mockFetch.mockResolvedValue({
status: 404,
ok: false,
});
await authCtr.requestAuthorization(config);
// Verify shell.openExternal was called with custom URL
expect(shell.openExternal).toHaveBeenCalledWith(
expect.stringContaining('https://my-custom-server.com/oidc/auth'),
);
});
it('should handle authorization request error gracefully', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
vi.mocked(shell.openExternal).mockRejectedValue(new Error('Failed to open browser'));
const result = await authCtr.requestAuthorization(config);
expect(result.success).toBe(false);
expect(result.error).toContain('Failed to open browser');
});
});
describe('polling mechanism', () => {
it('should poll every 3 seconds', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
mockFetch.mockResolvedValue({
status: 404,
ok: false,
});
await authCtr.requestAuthorization(config);
// Wait for first poll
await new Promise((resolve) => setTimeout(resolve, 3100));
const firstCallCount = mockFetch.mock.calls.filter((call) =>
(call[0] as string).includes('/oidc/handoff'),
).length;
expect(firstCallCount).toBeGreaterThanOrEqual(1);
// Wait for second poll
await new Promise((resolve) => setTimeout(resolve, 3000));
const secondCallCount = mockFetch.mock.calls.filter((call) =>
(call[0] as string).includes('/oidc/handoff'),
).length;
expect(secondCallCount).toBeGreaterThanOrEqual(2);
}, 10000);
it('should stop polling when credentials are received', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
let pollCount = 0;
mockFetch.mockImplementation((url: string) => {
const urlObj = new URL(url);
// Return success on third poll
if (urlObj.pathname.includes('/oidc/handoff')) {
pollCount++;
if (pollCount >= 3) {
return Promise.resolve({
status: 200,
ok: true,
json: () =>
Promise.resolve({
success: true,
data: {
payload: {
code: 'mock-auth-code',
state: 'mock-random-2', // Second randomBytes call is for state
},
},
}),
text: () => Promise.resolve('mock response'),
});
}
}
// Token exchange endpoint
if (urlObj.pathname.includes('/oidc/token')) {
return Promise.resolve({
status: 200,
ok: true,
json: () =>
Promise.resolve({
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 3600,
}),
text: () => Promise.resolve('mock response'),
clone: () => ({
json: () =>
Promise.resolve({
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 3600,
}),
}),
});
}
return Promise.resolve({
status: 404,
ok: false,
});
});
await authCtr.requestAuthorization(config);
// Wait for polling to complete
await new Promise((resolve) => setTimeout(resolve, 10000));
const pollCountBefore = pollCount;
// Wait more time and verify no more polling
await new Promise((resolve) => setTimeout(resolve, 3500));
expect(pollCount).toBe(pollCountBefore);
}, 15000);
it('should broadcast authorizationSuccessful when credentials are exchanged', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
mockFetch.mockImplementation((url: string) => {
const urlObj = new URL(url);
if (urlObj.pathname.includes('/oidc/handoff')) {
return Promise.resolve({
status: 200,
ok: true,
json: () =>
Promise.resolve({
success: true,
data: {
payload: {
code: 'mock-auth-code',
state: 'mock-random-2', // Second randomBytes call is for state
},
},
}),
text: () => Promise.resolve('mock response'),
});
}
if (urlObj.pathname.includes('/oidc/token')) {
return Promise.resolve({
status: 200,
ok: true,
json: () =>
Promise.resolve({
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 3600,
}),
text: () => Promise.resolve('mock response'),
clone: () => ({
json: () =>
Promise.resolve({
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 3600,
}),
}),
});
}
return Promise.resolve({ status: 404, ok: false });
});
await authCtr.requestAuthorization(config);
// Wait for polling to complete and token exchange
await new Promise((resolve) => setTimeout(resolve, 4000));
// Verify authorizationSuccessful was broadcast
expect(mockWindow.webContents.send).toHaveBeenCalledWith('authorizationSuccessful');
}, 6000);
it('should validate state parameter and reject mismatched state', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
mockFetch.mockImplementation((url: string) => {
const urlObj = new URL(url);
if (urlObj.pathname.includes('/oidc/handoff')) {
return Promise.resolve({
status: 200,
ok: true,
json: () =>
Promise.resolve({
success: true,
data: {
payload: {
code: 'mock-auth-code',
state: 'wrong-state', // Mismatched state
},
},
}),
});
}
return Promise.resolve({ status: 404, ok: false });
});
await authCtr.requestAuthorization(config);
// Wait for polling and state validation
await new Promise((resolve) => setTimeout(resolve, 4000));
// Verify authorizationFailed was broadcast with state error
expect(mockWindow.webContents.send).toHaveBeenCalledWith('authorizationFailed', {
error: 'Invalid state parameter',
});
}, 6000);
});
describe('token refresh', () => {
it('should start auto-refresh after successful authorization', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
mockFetch.mockImplementation((url: string) => {
const urlObj = new URL(url);
if (urlObj.pathname.includes('/oidc/handoff')) {
return Promise.resolve({
status: 200,
ok: true,
json: () =>
Promise.resolve({
success: true,
data: {
payload: {
code: 'mock-auth-code',
state: 'mock-random-2', // Second randomBytes call is for state
},
},
}),
text: () => Promise.resolve('mock response'),
});
}
if (urlObj.pathname.includes('/oidc/token')) {
return Promise.resolve({
status: 200,
ok: true,
json: () =>
Promise.resolve({
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 3600,
}),
text: () => Promise.resolve('mock response'),
clone: () => ({
json: () =>
Promise.resolve({
access_token: 'new-access-token',
refresh_token: 'new-refresh-token',
expires_in: 3600,
}),
}),
});
}
return Promise.resolve({ status: 404, ok: false });
});
await authCtr.requestAuthorization(config);
// Wait for polling and token exchange
await new Promise((resolve) => setTimeout(resolve, 4000));
// Verify saveTokens was called
expect(mockRemoteServerConfigCtr.saveTokens).toHaveBeenCalledWith(
'new-access-token',
'new-refresh-token',
3600,
);
// Verify remote server was set to active
expect(mockRemoteServerConfigCtr.setRemoteServerConfig).toHaveBeenCalledWith({
active: true,
});
}, 6000);
});
});
describe('Scenario: Authorization Timeout and Retry', () => {
// All scenario tests use real timers
it('Step 1: User requests authorization but does not complete it within 5 minutes', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
// Mock: User never completes authorization, so polling always returns 404
mockFetch.mockResolvedValue({
status: 404,
ok: false,
});
// User clicks "Connect to Cloud" button
await authCtr.requestAuthorization(config);
// Wait for some polling to happen
await new Promise((resolve) => setTimeout(resolve, 10000));
const handoffCallsBeforeTimeout = mockFetch.mock.calls.filter((call) =>
(call[0] as string).includes('/oidc/handoff'),
).length;
expect(handoffCallsBeforeTimeout).toBeGreaterThan(0);
// Verify polling is active by checking calls increased
const callsBefore = handoffCallsBeforeTimeout;
await new Promise((resolve) => setTimeout(resolve, 3500));
const callsAfter = mockFetch.mock.calls.filter((call) =>
(call[0] as string).includes('/oidc/handoff'),
).length;
expect(callsAfter).toBeGreaterThan(callsBefore);
}, 15000); // Increase test timeout
it('Step 2: User clicks retry button after previous attempt', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
mockFetch.mockResolvedValue({
status: 404,
ok: false,
});
// First attempt
await authCtr.requestAuthorization(config);
await new Promise((resolve) => setTimeout(resolve, 3500));
// Reset mock to track retry
mockFetch.mockClear();
// User clicks retry button - should start fresh authorization
await authCtr.requestAuthorization(config);
// Verify: New polling started
await new Promise((resolve) => setTimeout(resolve, 3500));
const handoffCalls = mockFetch.mock.calls.filter((call) =>
(call[0] as string).includes('/oidc/handoff'),
);
expect(handoffCalls.length).toBeGreaterThan(0);
}, 10000);
it('Step 3: Retry generates new state parameter (not reusing old state)', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
const capturedStates: string[] = [];
mockFetch.mockImplementation((url: string) => {
const urlObj = new URL(url);
const stateParam = urlObj.searchParams.get('id');
if (stateParam && !capturedStates.includes(stateParam)) {
capturedStates.push(stateParam);
}
return Promise.resolve({ status: 404, ok: false });
});
// First authorization attempt
await authCtr.requestAuthorization(config);
await new Promise((resolve) => setTimeout(resolve, 3500));
const firstState = capturedStates[0];
// Clear for second attempt tracking
const firstAttemptStates = [...capturedStates];
capturedStates.length = 0;
// Retry - should generate NEW state
await authCtr.requestAuthorization(config);
await new Promise((resolve) => setTimeout(resolve, 3500));
const secondState = capturedStates[0];
// CRITICAL: States must be different
expect(firstState).toBeDefined();
expect(secondState).toBeDefined();
expect(secondState).not.toBe(firstState);
expect(firstAttemptStates).not.toContain(secondState);
}, 10000);
it('Step 4: User completes authorization on retry successfully', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
// First attempt - incomplete
mockFetch.mockResolvedValue({ status: 404, ok: false });
await authCtr.requestAuthorization(config);
await new Promise((resolve) => setTimeout(resolve, 3500));
// Second attempt - user completes it this time
mockFetch.mockImplementation((url: string) => {
const urlObj = new URL(url);
// Handoff returns credentials immediately
if (urlObj.pathname.includes('/oidc/handoff')) {
return Promise.resolve({
status: 200,
ok: true,
json: () =>
Promise.resolve({
success: true,
data: {
payload: {
code: 'authorization-code',
state: 'mock-random-4', // Matches second request's state (3rd and 4th randomBytes calls)
},
},
}),
text: () => Promise.resolve('mock response'),
});
}
// Token exchange succeeds
if (urlObj.pathname.includes('/oidc/token')) {
return Promise.resolve({
status: 200,
ok: true,
json: () =>
Promise.resolve({
access_token: 'access-token',
refresh_token: 'refresh-token',
expires_in: 3600,
}),
text: () => Promise.resolve('mock response'),
clone: () => ({
json: () =>
Promise.resolve({
access_token: 'access-token',
refresh_token: 'refresh-token',
expires_in: 3600,
}),
}),
});
}
return Promise.resolve({ status: 404, ok: false });
});
await authCtr.requestAuthorization(config);
// Wait longer for polling and token exchange
await new Promise((resolve) => setTimeout(resolve, 4000));
// Verify: Success message shown
const successCall = mockWindow.webContents.send.mock.calls.find(
(call: any[]) => call[0] === 'authorizationSuccessful',
);
expect(successCall).toBeDefined();
// Verify: Tokens saved
expect(mockRemoteServerConfigCtr.saveTokens).toHaveBeenCalled();
}, 12000);
it('Edge case: Rapid retry clicks should not create multiple polling intervals', async () => {
const config: DataSyncConfig = {
active: false,
storageMode: 'cloud',
};
mockFetch.mockResolvedValue({ status: 404, ok: false });
// User rapidly clicks retry multiple times
await authCtr.requestAuthorization(config);
await authCtr.requestAuthorization(config);
await authCtr.requestAuthorization(config);
// Wait for some polling to happen
await new Promise((resolve) => setTimeout(resolve, 9000));
// Count handoff requests
const handoffCalls = mockFetch.mock.calls.filter((call) =>
(call[0] as string).includes('/oidc/handoff'),
);
// Should have ~3 calls (one per 3-second interval), not ~9 (3 intervals running)
// Allow some tolerance for timing
expect(handoffCalls.length).toBeLessThanOrEqual(5);
}, 10000);
});
});
@@ -64,7 +64,7 @@ describe('BrowserWindowsCtr', () => {
it('should show the settings window with the specified tab', async () => {
const tab = 'appearance';
const result = await browserWindowsCtr.openSettingsWindow(tab);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith(tab);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({ tab });
expect(result).toEqual({ success: true });
});
@@ -120,11 +120,11 @@ describe('BrowserWindowsCtr', () => {
it('should show settings window if matched route target is settings', async () => {
const params: InterceptRouteParams = {
...baseParams,
path: '/settings?active=common',
url: 'app://host/settings?active=common',
path: '/settings/provider',
url: 'app://host/settings/provider?active=provider&provider=ollama',
};
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
const subPath = 'common';
const subPath = 'provider';
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
(extractSubPath as Mock).mockReturnValue(subPath);
@@ -132,7 +132,10 @@ describe('BrowserWindowsCtr', () => {
expect(findMatchingRoute).toHaveBeenCalledWith(params.path);
expect(extractSubPath).toHaveBeenCalledWith(params.path, matchedRoute.pathPrefix);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith(subPath);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({
searchParams: { active: 'provider', provider: 'ollama' },
tab: subPath,
});
expect(result).toEqual({
intercepted: true,
path: params.path,
@@ -170,11 +173,11 @@ describe('BrowserWindowsCtr', () => {
it('should return error if processing route interception fails for settings', async () => {
const params: InterceptRouteParams = {
...baseParams,
path: '/settings?active=general',
path: '/settings',
url: 'app://host/settings?active=general',
};
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
const subPath = 'general';
const subPath = undefined;
const errorMessage = 'Processing error for settings';
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
(extractSubPath as Mock).mockReturnValue(subPath);
@@ -182,6 +185,10 @@ describe('BrowserWindowsCtr', () => {
const result = await browserWindowsCtr.interceptRoute(params);
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({
searchParams: { active: 'general' },
tab: subPath,
});
expect(result).toEqual({
error: errorMessage,
intercepted: false,
@@ -0,0 +1,395 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { App } from '@/core/App';
import LocalFileCtr from '../LocalFileCtr';
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// Mock file-loaders
vi.mock('@lobechat/file-loaders', () => ({
SYSTEM_FILES_TO_IGNORE: ['.DS_Store', 'Thumbs.db'],
loadFile: vi.fn(),
}));
// Mock electron
vi.mock('electron', () => ({
shell: {
openPath: vi.fn(),
},
}));
// Mock fast-glob
vi.mock('fast-glob', () => ({
default: vi.fn(),
}));
// Mock node:fs/promises and node:fs
vi.mock('node:fs/promises', () => ({
stat: vi.fn(),
readdir: vi.fn(),
rename: vi.fn(),
access: vi.fn(),
writeFile: vi.fn(),
readFile: vi.fn(),
mkdir: vi.fn(),
}));
vi.mock('node:fs', () => ({
Stats: class Stats {},
constants: {
F_OK: 0,
},
stat: vi.fn(),
readdir: vi.fn(),
rename: vi.fn(),
access: vi.fn(),
writeFile: vi.fn(),
readFile: vi.fn(),
}));
// Mock FileSearchService
const mockSearchService = {
search: vi.fn(),
};
// Mock makeSureDirExist
vi.mock('@/utils/file-system', () => ({
makeSureDirExist: vi.fn(),
}));
const mockApp = {
getService: vi.fn(() => mockSearchService),
} as unknown as App;
describe('LocalFileCtr', () => {
let localFileCtr: LocalFileCtr;
let mockShell: any;
let mockFg: any;
let mockLoadFile: any;
let mockFsPromises: any;
beforeEach(async () => {
vi.clearAllMocks();
// Import mocks
mockShell = (await import('electron')).shell;
mockFg = (await import('fast-glob')).default;
mockLoadFile = (await import('@lobechat/file-loaders')).loadFile;
mockFsPromises = await import('node:fs/promises');
localFileCtr = new LocalFileCtr(mockApp);
});
describe('handleOpenLocalFile', () => {
it('should open file successfully', async () => {
vi.mocked(mockShell.openPath).mockResolvedValue('');
const result = await localFileCtr.handleOpenLocalFile({ path: '/test/file.txt' });
expect(result).toEqual({ success: true });
expect(mockShell.openPath).toHaveBeenCalledWith('/test/file.txt');
});
it('should return error when opening file fails', async () => {
const error = new Error('Failed to open');
vi.mocked(mockShell.openPath).mockRejectedValue(error);
const result = await localFileCtr.handleOpenLocalFile({ path: '/test/file.txt' });
expect(result).toEqual({ success: false, error: 'Failed to open' });
});
});
describe('handleOpenLocalFolder', () => {
it('should open directory when isDirectory is true', async () => {
vi.mocked(mockShell.openPath).mockResolvedValue('');
const result = await localFileCtr.handleOpenLocalFolder({
path: '/test/folder',
isDirectory: true,
});
expect(result).toEqual({ success: true });
expect(mockShell.openPath).toHaveBeenCalledWith('/test/folder');
});
it('should open parent directory when isDirectory is false', async () => {
vi.mocked(mockShell.openPath).mockResolvedValue('');
const result = await localFileCtr.handleOpenLocalFolder({
path: '/test/folder/file.txt',
isDirectory: false,
});
expect(result).toEqual({ success: true });
expect(mockShell.openPath).toHaveBeenCalledWith('/test/folder');
});
it('should return error when opening folder fails', async () => {
const error = new Error('Failed to open folder');
vi.mocked(mockShell.openPath).mockRejectedValue(error);
const result = await localFileCtr.handleOpenLocalFolder({
path: '/test/folder',
isDirectory: true,
});
expect(result).toEqual({ success: false, error: 'Failed to open folder' });
});
});
describe('readFile', () => {
it('should read file successfully with default location', async () => {
const mockFileContent = 'line1\nline2\nline3\nline4\nline5';
vi.mocked(mockLoadFile).mockResolvedValue({
content: mockFileContent,
filename: 'test.txt',
fileType: 'txt',
createdTime: new Date('2024-01-01'),
modifiedTime: new Date('2024-01-02'),
});
const result = await localFileCtr.readFile({ path: '/test/file.txt' });
expect(result.filename).toBe('test.txt');
expect(result.fileType).toBe('txt');
expect(result.totalLineCount).toBe(5);
expect(result.content).toBe(mockFileContent);
});
it('should read file with custom location range', async () => {
const mockFileContent = 'line1\nline2\nline3\nline4\nline5';
vi.mocked(mockLoadFile).mockResolvedValue({
content: mockFileContent,
filename: 'test.txt',
fileType: 'txt',
createdTime: new Date('2024-01-01'),
modifiedTime: new Date('2024-01-02'),
});
const result = await localFileCtr.readFile({ path: '/test/file.txt', loc: [1, 3] });
expect(result.content).toBe('line2\nline3');
expect(result.lineCount).toBe(2);
expect(result.totalLineCount).toBe(5);
});
it('should handle file read error', async () => {
vi.mocked(mockLoadFile).mockRejectedValue(new Error('File not found'));
const result = await localFileCtr.readFile({ path: '/test/missing.txt' });
expect(result.content).toContain('Error accessing or processing file');
expect(result.lineCount).toBe(0);
expect(result.charCount).toBe(0);
});
});
describe('readFiles', () => {
it('should read multiple files successfully', async () => {
vi.mocked(mockLoadFile).mockResolvedValue({
content: 'file content',
filename: 'test.txt',
fileType: 'txt',
createdTime: new Date('2024-01-01'),
modifiedTime: new Date('2024-01-02'),
});
const result = await localFileCtr.readFiles({
paths: ['/test/file1.txt', '/test/file2.txt'],
});
expect(result).toHaveLength(2);
expect(mockLoadFile).toHaveBeenCalledTimes(2);
});
});
describe('handleWriteFile', () => {
it('should write file successfully', async () => {
vi.mocked(mockFsPromises.mkdir).mockResolvedValue(undefined);
vi.mocked(mockFsPromises.writeFile).mockResolvedValue(undefined);
const result = await localFileCtr.handleWriteFile({
path: '/test/file.txt',
content: 'test content',
});
expect(result).toEqual({ success: true });
});
it('should return error when path is empty', async () => {
const result = await localFileCtr.handleWriteFile({
path: '',
content: 'test content',
});
expect(result).toEqual({ success: false, error: 'Path cannot be empty' });
});
it('should return error when content is undefined', async () => {
const result = await localFileCtr.handleWriteFile({
path: '/test/file.txt',
content: undefined as any,
});
expect(result).toEqual({ success: false, error: 'Content cannot be empty' });
});
it('should handle write error', async () => {
vi.mocked(mockFsPromises.mkdir).mockResolvedValue(undefined);
vi.mocked(mockFsPromises.writeFile).mockRejectedValue(new Error('Write failed'));
const result = await localFileCtr.handleWriteFile({
path: '/test/file.txt',
content: 'test content',
});
expect(result).toEqual({ success: false, error: 'Failed to write file: Write failed' });
});
});
describe('handleRenameFile', () => {
it('should rename file successfully', async () => {
vi.mocked(mockFsPromises.rename).mockResolvedValue(undefined);
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: 'new.txt',
});
expect(result).toEqual({ success: true, newPath: '/test/new.txt' });
expect(mockFsPromises.rename).toHaveBeenCalledWith('/test/old.txt', '/test/new.txt');
});
it('should skip rename when paths are identical', async () => {
const result = await localFileCtr.handleRenameFile({
path: '/test/file.txt',
newName: 'file.txt',
});
expect(result).toEqual({ success: true, newPath: '/test/file.txt' });
expect(mockFsPromises.rename).not.toHaveBeenCalled();
});
it('should reject invalid new name with path separators', async () => {
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: '../new.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid new name');
});
it('should reject invalid new name with special characters', async () => {
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: 'new:file.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid new name');
});
it('should handle file not found error', async () => {
const error: any = new Error('File not found');
error.code = 'ENOENT';
vi.mocked(mockFsPromises.rename).mockRejectedValue(error);
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: 'new.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('File or directory not found');
});
it('should handle file already exists error', async () => {
const error: any = new Error('File exists');
error.code = 'EEXIST';
vi.mocked(mockFsPromises.rename).mockRejectedValue(error);
const result = await localFileCtr.handleRenameFile({
path: '/test/old.txt',
newName: 'new.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('already exists');
});
});
describe('handleLocalFilesSearch', () => {
it('should search files successfully', async () => {
const mockResults = [
{
name: 'test.txt',
path: '/test/test.txt',
isDirectory: false,
size: 100,
type: 'txt',
},
];
mockSearchService.search.mockResolvedValue(mockResults);
const result = await localFileCtr.handleLocalFilesSearch({ keywords: 'test' });
expect(result).toEqual(mockResults);
expect(mockSearchService.search).toHaveBeenCalledWith('test', {
keywords: 'test',
limit: 30,
});
});
it('should return empty array on search error', async () => {
mockSearchService.search.mockRejectedValue(new Error('Search failed'));
const result = await localFileCtr.handleLocalFilesSearch({ keywords: 'test' });
expect(result).toEqual([]);
});
});
describe('handleGlobFiles', () => {
it('should glob files successfully', async () => {
const mockFiles = [
{ path: '/test/file1.txt', stats: { mtime: new Date('2024-01-02') } },
{ path: '/test/file2.txt', stats: { mtime: new Date('2024-01-01') } },
];
vi.mocked(mockFg).mockResolvedValue(mockFiles);
const result = await localFileCtr.handleGlobFiles({
pattern: '*.txt',
path: '/test',
});
expect(result.success).toBe(true);
expect(result.files).toEqual(['/test/file1.txt', '/test/file2.txt']);
expect(result.total_files).toBe(2);
});
it('should handle glob error', async () => {
vi.mocked(mockFg).mockRejectedValue(new Error('Glob failed'));
const result = await localFileCtr.handleGlobFiles({
pattern: '*.txt',
});
expect(result).toEqual({
success: false,
files: [],
total_files: 0,
});
});
});
});
@@ -0,0 +1,499 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { App } from '@/core/App';
import ShellCommandCtr from '../ShellCommandCtr';
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// Mock child_process
vi.mock('node:child_process', () => ({
spawn: vi.fn(),
}));
// Mock crypto
vi.mock('node:crypto', () => ({
randomUUID: vi.fn(() => 'test-uuid-123'),
}));
const mockApp = {} as unknown as App;
describe('ShellCommandCtr', () => {
let shellCommandCtr: ShellCommandCtr;
let mockSpawn: any;
let mockChildProcess: any;
beforeEach(async () => {
vi.clearAllMocks();
// Import mocks
const childProcessModule = await import('node:child_process');
mockSpawn = vi.mocked(childProcessModule.spawn);
// Create mock child process
mockChildProcess = {
stdout: {
on: vi.fn(),
},
stderr: {
on: vi.fn(),
},
on: vi.fn(),
kill: vi.fn(),
exitCode: null,
};
mockSpawn.mockReturnValue(mockChildProcess);
shellCommandCtr = new ShellCommandCtr(mockApp);
});
describe('handleRunCommand', () => {
describe('synchronous mode', () => {
it('should execute command successfully', async () => {
let exitCallback: (code: number) => void;
let stdoutCallback: (data: Buffer) => void;
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
if (event === 'exit') {
exitCallback = callback;
// Simulate successful exit
setTimeout(() => exitCallback(0), 10);
}
return mockChildProcess;
});
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
if (event === 'data') {
stdoutCallback = callback;
// Simulate output
setTimeout(() => stdoutCallback(Buffer.from('test output\n')), 5);
}
return mockChildProcess.stdout;
});
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
const result = await shellCommandCtr.handleRunCommand({
command: 'echo "test"',
description: 'test command',
});
expect(result.success).toBe(true);
expect(result.stdout).toBe('test output\n');
expect(result.exit_code).toBe(0);
});
it('should handle command timeout', async () => {
mockChildProcess.on.mockImplementation(() => mockChildProcess);
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
const result = await shellCommandCtr.handleRunCommand({
command: 'sleep 10',
description: 'long running command',
timeout: 100,
});
expect(result.success).toBe(false);
expect(result.error).toContain('timed out');
expect(mockChildProcess.kill).toHaveBeenCalled();
});
it('should handle command execution error', async () => {
let errorCallback: (error: Error) => void;
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
if (event === 'error') {
errorCallback = callback;
setTimeout(() => errorCallback(new Error('Command not found')), 10);
}
return mockChildProcess;
});
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
const result = await shellCommandCtr.handleRunCommand({
command: 'invalid-command',
description: 'invalid command',
});
expect(result.success).toBe(false);
expect(result.error).toBe('Command not found');
});
it('should handle non-zero exit code', async () => {
let exitCallback: (code: number) => void;
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
if (event === 'exit') {
exitCallback = callback;
setTimeout(() => exitCallback(1), 10);
}
return mockChildProcess;
});
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
const result = await shellCommandCtr.handleRunCommand({
command: 'exit 1',
description: 'failing command',
});
expect(result.success).toBe(false);
expect(result.exit_code).toBe(1);
});
it('should capture stderr output', async () => {
let exitCallback: (code: number) => void;
let stderrCallback: (data: Buffer) => void;
mockChildProcess.on.mockImplementation((event: string, callback: any) => {
if (event === 'exit') {
exitCallback = callback;
setTimeout(() => exitCallback(1), 10);
}
return mockChildProcess;
});
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
mockChildProcess.stderr.on.mockImplementation((event: string, callback: any) => {
if (event === 'data') {
stderrCallback = callback;
setTimeout(() => stderrCallback(Buffer.from('error message\n')), 5);
}
return mockChildProcess.stderr;
});
const result = await shellCommandCtr.handleRunCommand({
command: 'command-with-error',
description: 'command with stderr',
});
expect(result.stderr).toBe('error message\n');
});
it('should enforce timeout limits', async () => {
mockChildProcess.on.mockImplementation(() => mockChildProcess);
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
// Test minimum timeout
const minResult = await shellCommandCtr.handleRunCommand({
command: 'sleep 5',
timeout: 500, // Below 1000ms minimum
});
expect(minResult.success).toBe(false);
expect(minResult.error).toContain('1000ms'); // Should use 1000ms minimum
});
});
describe('background mode', () => {
it('should start command in background', async () => {
mockChildProcess.on.mockImplementation(() => mockChildProcess);
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
const result = await shellCommandCtr.handleRunCommand({
command: 'long-running-task',
description: 'background task',
run_in_background: true,
});
expect(result.success).toBe(true);
expect(result.shell_id).toBe('test-uuid-123');
});
it('should use correct shell on Windows', async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'win32' });
mockChildProcess.on.mockImplementation(() => mockChildProcess);
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
await shellCommandCtr.handleRunCommand({
command: 'dir',
description: 'windows command',
run_in_background: true,
});
expect(mockSpawn).toHaveBeenCalledWith('cmd.exe', ['/c', 'dir'], expect.any(Object));
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should use correct shell on Unix', async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'darwin' });
mockChildProcess.on.mockImplementation(() => mockChildProcess);
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
await shellCommandCtr.handleRunCommand({
command: 'ls',
description: 'unix command',
run_in_background: true,
});
expect(mockSpawn).toHaveBeenCalledWith('/bin/sh', ['-c', 'ls'], expect.any(Object));
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
});
});
describe('handleGetCommandOutput', () => {
beforeEach(async () => {
mockChildProcess.on.mockImplementation(() => mockChildProcess);
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
if (event === 'data') {
// Simulate some output
setTimeout(() => callback(Buffer.from('line 1\n')), 5);
setTimeout(() => callback(Buffer.from('line 2\n')), 10);
}
return mockChildProcess.stdout;
});
mockChildProcess.stderr.on.mockImplementation((event: string, callback: any) => {
if (event === 'data') {
setTimeout(() => callback(Buffer.from('error line\n')), 7);
}
return mockChildProcess.stderr;
});
// Start a background process first
await shellCommandCtr.handleRunCommand({
command: 'test-command',
run_in_background: true,
});
});
it('should retrieve command output', async () => {
// Wait for output to be captured
await new Promise((resolve) => setTimeout(resolve, 20));
const result = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(result.success).toBe(true);
expect(result.stdout).toContain('line 1');
expect(result.stderr).toContain('error line');
});
it('should return error for non-existent shell_id', async () => {
const result = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'non-existent-id',
});
expect(result.success).toBe(false);
expect(result.error).toContain('not found');
});
it('should filter output with regex', async () => {
// Wait for output to be captured
await new Promise((resolve) => setTimeout(resolve, 20));
const result = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
filter: 'line 1',
});
expect(result.success).toBe(true);
expect(result.output).toContain('line 1');
expect(result.output).not.toContain('line 2');
});
it('should only return new output since last read', async () => {
// Wait for initial output
await new Promise((resolve) => setTimeout(resolve, 20));
// First read
const firstResult = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(firstResult.stdout).toContain('line 1');
// Second read should return empty (no new output)
const secondResult = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(secondResult.stdout).toBe('');
expect(secondResult.stderr).toBe('');
});
it('should handle invalid regex filter gracefully', async () => {
await new Promise((resolve) => setTimeout(resolve, 20));
const result = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
filter: '[invalid(regex',
});
expect(result.success).toBe(true);
// Should return unfiltered output when filter is invalid
});
it('should report running status correctly', async () => {
mockChildProcess.exitCode = null;
const runningResult = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(runningResult.running).toBe(true);
// Simulate process exit
mockChildProcess.exitCode = 0;
const exitedResult = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(exitedResult.running).toBe(false);
});
it('should track stdout and stderr offsets separately when streaming output', async () => {
// Create a new background process with manual control over stdout/stderr
let stdoutCallback: (data: Buffer) => void;
let stderrCallback: (data: Buffer) => void;
mockChildProcess.stdout.on.mockImplementation((event: string, callback: any) => {
if (event === 'data') {
stdoutCallback = callback;
}
return mockChildProcess.stdout;
});
mockChildProcess.stderr.on.mockImplementation((event: string, callback: any) => {
if (event === 'data') {
stderrCallback = callback;
}
return mockChildProcess.stderr;
});
// Start a new background process
await shellCommandCtr.handleRunCommand({
command: 'test-interleaved',
run_in_background: true,
});
// Simulate stderr output first
stderrCallback(Buffer.from('error 1\n'));
await new Promise((resolve) => setTimeout(resolve, 5));
// First read - should get stderr
const firstRead = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(firstRead.stderr).toBe('error 1\n');
expect(firstRead.stdout).toBe('');
// Simulate stdout output after stderr
stdoutCallback(Buffer.from('output 1\n'));
await new Promise((resolve) => setTimeout(resolve, 5));
// Second read - should get stdout without losing data
const secondRead = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(secondRead.stdout).toBe('output 1\n');
expect(secondRead.stderr).toBe('');
// Simulate more stderr
stderrCallback(Buffer.from('error 2\n'));
await new Promise((resolve) => setTimeout(resolve, 5));
// Third read - should get new stderr
const thirdRead = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(thirdRead.stderr).toBe('error 2\n');
expect(thirdRead.stdout).toBe('');
// Simulate more stdout
stdoutCallback(Buffer.from('output 2\n'));
await new Promise((resolve) => setTimeout(resolve, 5));
// Fourth read - should get new stdout
const fourthRead = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(fourthRead.stdout).toBe('output 2\n');
expect(fourthRead.stderr).toBe('');
});
});
describe('handleKillCommand', () => {
beforeEach(async () => {
mockChildProcess.on.mockImplementation(() => mockChildProcess);
mockChildProcess.stdout.on.mockImplementation(() => mockChildProcess.stdout);
mockChildProcess.stderr.on.mockImplementation(() => mockChildProcess.stderr);
// Start a background process
await shellCommandCtr.handleRunCommand({
command: 'test-command',
run_in_background: true,
});
});
it('should kill command successfully', async () => {
const result = await shellCommandCtr.handleKillCommand({
shell_id: 'test-uuid-123',
});
expect(result.success).toBe(true);
expect(mockChildProcess.kill).toHaveBeenCalled();
});
it('should return error for non-existent shell_id', async () => {
const result = await shellCommandCtr.handleKillCommand({
shell_id: 'non-existent-id',
});
expect(result.success).toBe(false);
expect(result.error).toContain('not found');
});
it('should remove process from map after killing', async () => {
await shellCommandCtr.handleKillCommand({
shell_id: 'test-uuid-123',
});
// Try to get output from killed process
const outputResult = await shellCommandCtr.handleGetCommandOutput({
shell_id: 'test-uuid-123',
});
expect(outputResult.success).toBe(false);
expect(outputResult.error).toContain('not found');
});
it('should handle kill error gracefully', async () => {
mockChildProcess.kill.mockImplementation(() => {
throw new Error('Kill failed');
});
const result = await shellCommandCtr.handleKillCommand({
shell_id: 'test-uuid-123',
});
expect(result.success).toBe(false);
expect(result.error).toBe('Kill failed');
});
});
});
@@ -106,7 +106,7 @@ describe('TrayMenuCtr', () => {
expect(mockGetMainTray).not.toHaveBeenCalled();
expect(mockDisplayBalloon).not.toHaveBeenCalled();
expect(result).toEqual({
error: '托盘通知仅在 Windows 平台支持',
error: 'Tray notifications are only supported on Windows platform',
success: false,
});
});
@@ -126,7 +126,7 @@ describe('TrayMenuCtr', () => {
expect(mockGetMainTray).toHaveBeenCalled();
expect(mockDisplayBalloon).not.toHaveBeenCalled();
expect(result).toEqual({
error: '托盘通知仅在 Windows 平台支持',
error: 'Tray notifications are only supported on Windows platform',
success: false
});
});
@@ -188,7 +188,7 @@ describe('TrayMenuCtr', () => {
const result = await trayMenuCtr.updateTrayIcon(options);
expect(result).toEqual({
error: '托盘功能仅在 Windows 平台支持',
error: 'Tray functionality is only supported on Windows platform',
success: false,
});
});
@@ -226,7 +226,7 @@ describe('TrayMenuCtr', () => {
const result = await trayMenuCtr.updateTrayTooltip(options);
expect(result).toEqual({
error: '托盘功能仅在 Windows 平台支持',
error: 'Tray functionality is only supported on Windows platform',
success: false,
});
});
@@ -248,7 +248,7 @@ describe('TrayMenuCtr', () => {
expect(mockUpdateTooltip).not.toHaveBeenCalled();
expect(result).toEqual({
error: '托盘功能仅在 Windows 平台支持',
error: 'Tray functionality is only supported on Windows platform',
success: false,
});
});
+4 -4
View File
@@ -19,13 +19,13 @@ const ipcDecorator =
};
/**
* controller 用的 ipc client event 装饰器
* IPC client event decorator for controllers
*/
export const ipcClientEvent = (method: keyof ClientDispatchEvents) =>
ipcDecorator(method, 'client');
/**
* controller 用的 ipc server event 装饰器
* IPC server event decorator for controllers
*/
export const ipcServerEvent = (method: keyof ServerDispatchEvents) =>
ipcDecorator(method, 'server');
@@ -56,8 +56,8 @@ const protocolDecorator =
/**
* Protocol handler decorator
* @param urlType 协议URL类型 (如: 'plugin')
* @param action 操作类型 (如: 'install')
* @param urlType Protocol URL type (e.g., 'plugin')
* @param action Action type (e.g., 'install')
*/
export const createProtocolHandler = (urlType: string) => (action: string) =>
protocolDecorator(urlType, action);
+26 -1
View File
@@ -1,11 +1,12 @@
import { ElectronIPCEventHandler, ElectronIPCServer } from '@lobechat/electron-server-ipc';
import { Session, app, ipcMain, protocol } from 'electron';
import { macOS, windows } from 'electron-is';
import { pathExistsSync, remove } from 'fs-extra';
import os from 'node:os';
import { join } from 'node:path';
import { name } from '@/../../package.json';
import { buildDir, nextStandaloneDir } from '@/const/dir';
import { buildDir, LOCAL_DATABASE_DIR, nextStandaloneDir } from '@/const/dir';
import { isDev } from '@/const/env';
import { IControlModule } from '@/controllers';
import { IServiceModule } from '@/services';
@@ -129,6 +130,9 @@ export class App {
this.initDevBranding();
// Clean up stale database lock file before starting IPC server
await this.cleanupDatabaseLock();
// ==============
await this.ipcServer.start();
logger.debug('IPC server started');
@@ -371,6 +375,27 @@ export class App {
}
};
/**
* Clean up stale database lock file from previous crashes or abnormal exits
*/
private cleanupDatabaseLock = async () => {
try {
const dbPath = join(this.appStoragePath, LOCAL_DATABASE_DIR);
const lockPath = `${dbPath}.lock`;
if (pathExistsSync(lockPath)) {
logger.info(`Cleaning up stale database lock file: ${lockPath}`);
await remove(lockPath);
logger.info('Database lock file removed successfully');
} else {
logger.debug('No database lock file found, skipping cleanup');
}
} catch (error) {
logger.error('Failed to cleanup database lock file:', error);
// Non-fatal error, allow application to continue
}
};
private registerNextHandler() {
logger.debug('Registering Next.js handler');
const handler = createHandler({
@@ -0,0 +1,282 @@
import { app } from 'electron';
import { pathExistsSync, remove } from 'fs-extra';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LOCAL_DATABASE_DIR } from '@/const/dir';
// Mock electron modules
vi.mock('electron', () => ({
app: {
getAppPath: vi.fn(() => '/mock/app/path'),
getLocale: vi.fn(() => 'en-US'),
getPath: vi.fn(() => '/mock/user/path'),
requestSingleInstanceLock: vi.fn(() => true),
whenReady: vi.fn(() => Promise.resolve()),
on: vi.fn(),
commandLine: {
appendSwitch: vi.fn(),
},
dock: {
setIcon: vi.fn(),
},
exit: vi.fn(),
},
ipcMain: {
handle: vi.fn(),
},
nativeTheme: {
on: vi.fn(),
shouldUseDarkColors: false,
},
protocol: {
registerSchemesAsPrivileged: vi.fn(),
},
}));
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// Mock fs-extra module
vi.mock('fs-extra', async () => {
const actual = await vi.importActual('fs-extra');
return {
...actual,
pathExistsSync: vi.fn(),
remove: vi.fn(),
};
});
// Mock common/routes
vi.mock('~common/routes', () => ({
findMatchingRoute: vi.fn(),
extractSubPath: vi.fn(),
}));
// Mock other dependencies
vi.mock('electron-is', () => ({
macOS: vi.fn(() => false),
windows: vi.fn(() => false),
}));
vi.mock('fix-path', () => ({
default: vi.fn(),
}));
vi.mock('@/const/env', () => ({
isDev: false,
}));
vi.mock('@/const/dir', () => ({
buildDir: '/mock/build',
nextStandaloneDir: '/mock/standalone',
LOCAL_DATABASE_DIR: 'lobehub-local-db',
appStorageDir: '/mock/storage/path',
userDataDir: '/mock/user/data',
DB_SCHEMA_HASH_FILENAME: 'lobehub-local-db-schema-hash',
FILE_STORAGE_DIR: 'file-storage',
INSTALL_PLUGINS_DIR: 'plugins',
LOCAL_STORAGE_URL_PREFIX: '/lobe-desktop-file',
}));
vi.mock('@lobechat/electron-server-ipc', () => ({
ElectronIPCServer: vi.fn().mockImplementation(() => ({
start: vi.fn().mockResolvedValue(undefined),
})),
}));
// Mock all infrastructure managers
vi.mock('../infrastructure/I18nManager', () => ({
I18nManager: vi.fn().mockImplementation(() => ({
init: vi.fn().mockResolvedValue(undefined),
})),
}));
vi.mock('../infrastructure/StoreManager', () => ({
StoreManager: vi.fn().mockImplementation(() => ({
get: vi.fn((key) => {
if (key === 'storagePath') return '/mock/storage/path';
return undefined;
}),
set: vi.fn(),
})),
}));
vi.mock('../infrastructure/StaticFileServerManager', () => ({
StaticFileServerManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn().mockResolvedValue(undefined),
destroy: vi.fn(),
})),
}));
vi.mock('../infrastructure/UpdaterManager', () => ({
UpdaterManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn().mockResolvedValue(undefined),
})),
}));
vi.mock('../infrastructure/ProtocolManager', () => ({
ProtocolManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
processPendingUrls: vi.fn().mockResolvedValue(undefined),
})),
}));
vi.mock('../browser/BrowserManager', () => ({
BrowserManager: vi.fn().mockImplementation(() => ({
initializeBrowsers: vi.fn(),
getIdentifierByWebContents: vi.fn(),
})),
}));
vi.mock('../ui/MenuManager', () => ({
MenuManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
})),
}));
vi.mock('../ui/ShortcutManager', () => ({
ShortcutManager: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
})),
}));
vi.mock('../ui/TrayManager', () => ({
TrayManager: vi.fn().mockImplementation(() => ({
initializeTrays: vi.fn(),
destroyAll: vi.fn(),
})),
}));
vi.mock('@/utils/next-electron-rsc', () => ({
createHandler: vi.fn(() => ({
createInterceptor: vi.fn(),
registerCustomHandler: vi.fn(),
})),
}));
// Mock controllers and services
vi.mock('../../controllers/*Ctr.ts', () => ({}));
vi.mock('../../services/*Srv.ts', () => ({}));
// Import after mocks are set up
import { App } from '../App';
describe('App - Database Lock Cleanup', () => {
let appInstance: App;
let mockLockPath: string;
beforeEach(() => {
vi.clearAllMocks();
// Mock glob imports to return empty arrays
(import.meta as any).glob = vi.fn(() => ({}));
mockLockPath = join('/mock/storage/path', LOCAL_DATABASE_DIR) + '.lock';
});
afterEach(() => {
vi.clearAllMocks();
});
describe('bootstrap - database lock cleanup', () => {
it('should remove stale lock file if it exists during bootstrap', async () => {
// Setup: simulate existing lock file
vi.mocked(pathExistsSync).mockReturnValue(true);
vi.mocked(remove).mockResolvedValue(undefined);
// Create app instance
appInstance = new App();
// Call bootstrap which should trigger cleanup
await appInstance.bootstrap();
// Verify: lock file check was called
expect(pathExistsSync).toHaveBeenCalledWith(mockLockPath);
// Verify: lock file was removed
expect(remove).toHaveBeenCalledWith(mockLockPath);
});
it('should not attempt to remove lock file if it does not exist', async () => {
// Setup: no lock file exists
vi.mocked(pathExistsSync).mockReturnValue(false);
// Create app instance
appInstance = new App();
// Call bootstrap
await appInstance.bootstrap();
// Verify: lock file check was called
expect(pathExistsSync).toHaveBeenCalledWith(mockLockPath);
// Verify: remove was NOT called since file doesn't exist
expect(remove).not.toHaveBeenCalled();
});
it('should continue bootstrap even if lock cleanup fails', async () => {
// Setup: simulate lock file exists but cleanup fails
vi.mocked(pathExistsSync).mockReturnValue(true);
vi.mocked(remove).mockRejectedValue(new Error('Permission denied'));
// Create app instance
appInstance = new App();
// Bootstrap should not throw even if cleanup fails
await expect(appInstance.bootstrap()).resolves.not.toThrow();
// Verify: cleanup was attempted
expect(pathExistsSync).toHaveBeenCalledWith(mockLockPath);
expect(remove).toHaveBeenCalledWith(mockLockPath);
});
it('should clean up lock file before starting IPC server', async () => {
// Setup
vi.mocked(pathExistsSync).mockReturnValue(true);
const callOrder: string[] = [];
vi.mocked(remove).mockImplementation(async () => {
callOrder.push('remove');
});
// Mock IPC server start to track call order
const { ElectronIPCServer } = await import('@lobechat/electron-server-ipc');
const mockStart = vi.fn().mockImplementation(() => {
callOrder.push('ipcServer.start');
return Promise.resolve();
});
vi.mocked(ElectronIPCServer).mockImplementation(
() =>
({
start: mockStart,
}) as any,
);
// Create app instance and bootstrap
appInstance = new App();
await appInstance.bootstrap();
// Verify: cleanup happens before IPC server starts
expect(callOrder).toEqual(['remove', 'ipcServer.start']);
});
});
describe('appStoragePath', () => {
it('should return storage path from store manager', () => {
appInstance = new App();
const storagePath = appInstance.appStoragePath;
expect(storagePath).toBe('/mock/storage/path');
});
});
});
@@ -358,6 +358,9 @@ export default class Browser {
session: browserWindow.webContents.session,
});
// Setup CORS bypass for local file server
this.setupCORSBypass(browserWindow);
logger.debug(`[${this.identifier}] Initiating placeholder and URL loading sequence.`);
this.loadPlaceholder().then(() => {
this.loadUrl(path).catch((e) => {
@@ -491,4 +494,37 @@ export default class Browser {
logger.debug(`[${this.identifier}] Manually reapplying visual effects via Browser.`);
this.applyVisualEffects();
}
/**
* Setup CORS bypass for local file server (127.0.0.1:*)
* This is needed for Electron to access files from the local static file server
*/
private setupCORSBypass(browserWindow: BrowserWindow): void {
logger.debug(`[${this.identifier}] Setting up CORS bypass for local file server`);
const session = browserWindow.webContents.session;
// Intercept response headers to add CORS headers
session.webRequest.onHeadersReceived((details, callback) => {
const url = details.url;
// Only modify headers for local file server requests (127.0.0.1)
if (url.includes('127.0.0.1') || url.includes('lobe-desktop-file')) {
const responseHeaders = details.responseHeaders || {};
// Add CORS headers
responseHeaders['Access-Control-Allow-Origin'] = ['*'];
responseHeaders['Access-Control-Allow-Methods'] = ['GET, POST, PUT, DELETE, OPTIONS'];
responseHeaders['Access-Control-Allow-Headers'] = ['*'];
callback({
responseHeaders,
});
} else {
callback({ responseHeaders: details.responseHeaders });
}
});
logger.debug(`[${this.identifier}] CORS bypass setup completed`);
}
}
@@ -1,9 +1,18 @@
import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
import {
MainBroadcastEventKey,
MainBroadcastParams,
OpenSettingsWindowOptions,
} from '@lobechat/electron-client-ipc';
import { WebContents } from 'electron';
import { createLogger } from '@/utils/logger';
import { AppBrowsersIdentifiers, appBrowsers, WindowTemplate, WindowTemplateIdentifiers, windowTemplates } from '../../appBrowsers';
import {
AppBrowsersIdentifiers,
WindowTemplateIdentifiers,
appBrowsers,
windowTemplates,
} from '../../appBrowsers';
import type { App } from '../App';
import type { BrowserWindowOpts } from './Browser';
import Browser from './Browser';
@@ -63,14 +72,35 @@ export class BrowserManager {
* Display the settings window and navigate to a specific tab
* @param tab Settings window sub-path tab
*/
async showSettingsWindowWithTab(tab?: string) {
logger.debug(`Showing settings window with tab: ${tab || 'default'}`);
// common is the main path for settings route
if (tab && tab !== 'common') {
const browser = await this.redirectToPage('settings', tab);
async showSettingsWindowWithTab(options?: OpenSettingsWindowOptions) {
const tab = options?.tab;
const searchParams = options?.searchParams;
const query = new URLSearchParams();
if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => {
if (value !== undefined) query.set(key, value);
});
}
if (tab && tab !== 'common' && !query.has('active')) {
query.set('active', tab);
}
const queryString = query.toString();
const activeTab = query.get('active') ?? tab;
logger.debug(
`Showing settings window with navigation: active=${activeTab || 'default'}, query=${
queryString || 'none'
}`,
);
if (queryString) {
const browser = await this.redirectToPage('settings', undefined, queryString);
// make provider page more large
if (tab.startsWith('provider/')) {
if (activeTab?.startsWith('provider')) {
logger.debug('Resizing window for provider settings');
browser.setWindowSize({ height: 1000, width: 1400 });
browser.moveToCenter();
@@ -87,7 +117,7 @@ export class BrowserManager {
* @param identifier Window identifier
* @param subPath Sub-path, such as 'agent', 'about', etc.
*/
async redirectToPage(identifier: string, subPath?: string) {
async redirectToPage(identifier: string, subPath?: string, search?: string) {
try {
// Ensure window is retrieved or created
const browser = this.retrieveByIdentifier(identifier);
@@ -105,11 +135,14 @@ export class BrowserManager {
// Build complete URL path
const fullPath = subPath ? `${baseRoute}/${subPath}` : baseRoute;
const normalizedSearch =
search && search.length > 0 ? (search.startsWith('?') ? search : `?${search}`) : '';
const fullUrl = `${fullPath}${normalizedSearch}`;
logger.debug(`Redirecting to: ${fullPath}`);
logger.debug(`Redirecting to: ${fullUrl}`);
// Load URL and show window
await browser.loadUrl(fullPath);
await browser.loadUrl(fullUrl);
browser.show();
return browser;
@@ -143,14 +176,20 @@ export class BrowserManager {
* @param uniqueId Optional unique identifier, will be generated if not provided
* @returns The window identifier and Browser instance
*/
createMultiInstanceWindow(templateId: WindowTemplateIdentifiers, path: string, uniqueId?: string) {
createMultiInstanceWindow(
templateId: WindowTemplateIdentifiers,
path: string,
uniqueId?: string,
) {
const template = windowTemplates[templateId];
if (!template) {
throw new Error(`Window template ${templateId} not found`);
}
// Generate unique identifier
const windowId = uniqueId || `${template.baseIdentifier}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const windowId =
uniqueId ||
`${template.baseIdentifier}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
// Create browser options from template
const browserOpts: BrowserWindowOpts = {
@@ -164,8 +203,8 @@ export class BrowserManager {
const browser = this.retrieveOrInitialize(browserOpts);
return {
identifier: windowId,
browser: browser,
identifier: windowId,
};
}
@@ -176,7 +215,7 @@ export class BrowserManager {
*/
getWindowsByTemplate(templateId: string): string[] {
const prefix = `${templateId}_`;
return Array.from(this.browsers.keys()).filter(id => id.startsWith(prefix));
return Array.from(this.browsers.keys()).filter((id) => id.startsWith(prefix));
}
/**
@@ -185,7 +224,7 @@ export class BrowserManager {
*/
closeWindowsByTemplate(templateId: string): void {
const windowIds = this.getWindowsByTemplate(templateId);
windowIds.forEach(id => {
windowIds.forEach((id) => {
const browser = this.browsers.get(id);
if (browser) {
browser.close();
@@ -235,8 +274,7 @@ export class BrowserManager {
});
browser.browserWindow.on('show', () => {
if (browser.webContents)
this.webContentsMap.set(browser.webContents, browser.identifier);
if (browser.webContents) this.webContentsMap.set(browser.webContents, browser.identifier);
});
return browser;
@@ -50,7 +50,9 @@ export class ProtocolManager {
// Check if already registered
const isCurrentlyRegistered = app.isDefaultProtocolClient(this.protocolScheme);
logger.debug(`🔗 [Protocol] Is currently default protocol client: ${isCurrentlyRegistered}`);
logger.debug(
`🔗 [Protocol] ${this.protocolScheme}:// is currently registered: ${isCurrentlyRegistered}`,
);
// Register as default protocol client
let registrationResult: boolean;
@@ -71,7 +73,9 @@ export class ProtocolManager {
registrationResult = app.setAsDefaultProtocolClient(this.protocolScheme);
}
logger.debug(`🔗 [Protocol] Registration result: ${registrationResult}`);
logger.debug(
`🔗 [Protocol] Registration result for ${this.protocolScheme}://: ${registrationResult}`,
);
if (!registrationResult) {
logger.error(
@@ -83,7 +87,9 @@ export class ProtocolManager {
// Verify registration
const isRegisteredAfter = app.isDefaultProtocolClient(this.protocolScheme);
logger.debug(`🔗 [Protocol] Final registration status: ${isRegisteredAfter}`);
logger.debug(
`🔗 [Protocol] Final registration status for ${this.protocolScheme}://: ${isRegisteredAfter}`,
);
}
/**
@@ -123,7 +129,6 @@ export class ProtocolManager {
*/
private getProtocolUrlFromArgs(args: string[]): string | null {
const protocolPrefix = `${this.protocolScheme}://`;
logger.debug(`🔗 [Protocol] Searching for protocol URLs in args: ${JSON.stringify(args)}`);
logger.debug(`🔗 [Protocol] Looking for prefix: ${protocolPrefix}`);
@@ -9,6 +9,21 @@ import type { App } from '../App';
const logger = createLogger('core:StaticFileServerManager');
const getAllowedOrigin = (rawOrigin?: string) => {
if (!rawOrigin) return '*';
try {
const url = new URL(rawOrigin);
const normalizedOrigin = `${url.protocol}//${url.host}`;
return url.hostname === 'localhost' || url.hostname === '127.0.0.1' ? normalizedOrigin : '*';
} catch {
const normalizedOrigin = rawOrigin.replace(/\/$/, '');
return normalizedOrigin.includes('localhost') || normalizedOrigin.includes('127.0.0.1')
? normalizedOrigin
: '*';
}
};
export class StaticFileServerManager {
private app: App;
private fileService: FileService;
@@ -126,16 +141,38 @@ export class StaticFileServerManager {
return;
}
// 获取请求的 Origin 并设置 CORS
const origin = req.headers.origin || req.headers.referer;
const allowedOrigin = getAllowedOrigin(origin);
// 处理 CORS 预检请求
if (req.method === 'OPTIONS') {
res.writeHead(204, {
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Origin': allowedOrigin,
'Access-Control-Max-Age': '86400',
});
res.end();
return;
}
const url = new URL(req.url, `http://127.0.0.1:${this.serverPort}`);
logger.debug(`Processing HTTP file request: ${req.url}`);
logger.debug(`Request method: ${req.method}`);
logger.debug(`Request headers: ${JSON.stringify(req.headers)}`);
// 提取文件路径:从 /desktop-file/path/to/file.png 中提取相对路径
let filePath = decodeURIComponent(url.pathname.slice(1)); // 移除开头的 /
logger.debug(`Initial file path after decode: ${filePath}`);
// 如果路径以 desktop-file/ 开头,则移除该前缀
const prefixWithoutSlash = LOCAL_STORAGE_URL_PREFIX.slice(1) + '/'; // 移除开头的 / 并添加结尾的 /
logger.debug(`Prefix to remove: ${prefixWithoutSlash}`);
if (filePath.startsWith(prefixWithoutSlash)) {
filePath = filePath.slice(prefixWithoutSlash.length);
logger.debug(`File path after removing prefix: ${filePath}`);
}
if (!filePath) {
@@ -148,7 +185,12 @@ export class StaticFileServerManager {
}
// 使用 FileService 获取文件
const fileResult = await this.fileService.getFile(`desktop://${filePath}`);
const desktopPath = `desktop://${filePath}`;
logger.debug(`Attempting to get file: ${desktopPath}`);
const fileResult = await this.fileService.getFile(desktopPath);
logger.debug(
`File retrieved successfully, mime type: ${fileResult.mimeType}, size: ${fileResult.content.byteLength} bytes`,
);
// 再次检查响应状态
if (res.destroyed || res.headersSent) {
@@ -158,11 +200,8 @@ export class StaticFileServerManager {
// 设置响应头
res.writeHead(200, {
// 缓存一年
'Access-Control-Allow-Origin': 'http://localhost:*',
'Access-Control-Allow-Origin': allowedOrigin,
'Cache-Control': 'public, max-age=31536000',
// 允许 localhost 的任意端口
'Content-Length': Buffer.byteLength(fileResult.content),
'Content-Type': fileResult.mimeType,
});
@@ -173,16 +212,27 @@ export class StaticFileServerManager {
logger.debug(`HTTP file served successfully: desktop://${filePath}`);
} catch (error) {
logger.error(`Error serving HTTP file: ${error}`);
logger.error(`Error stack: ${error.stack}`);
// 检查响应是否仍然可写
if (!res.destroyed && !res.headersSent) {
try {
// 获取请求的 Origin 并设置 CORS(错误响应也需要!)
const origin = req.headers.origin || req.headers.referer;
const allowedOrigin = getAllowedOrigin(origin);
// 判断是否是文件未找到错误
if (error.name === 'FileNotFoundError') {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.writeHead(404, {
'Access-Control-Allow-Origin': allowedOrigin,
'Content-Type': 'text/plain',
});
res.end('File Not Found');
} else {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.writeHead(500, {
'Access-Control-Allow-Origin': allowedOrigin,
'Content-Type': 'text/plain',
});
res.end('Internal Server Error');
}
} catch (writeError) {
@@ -141,8 +141,29 @@ export class UpdaterManager {
// Mark application for exit
this.app.isQuiting = true;
// Delay installation by 1 second to ensure window is closed
autoUpdater.quitAndInstall();
// Close all windows first to ensure clean exit
logger.info('Closing all windows before update installation...');
const { BrowserWindow, app } = require('electron');
const allWindows = BrowserWindow.getAllWindows();
allWindows.forEach((window) => {
if (!window.isDestroyed()) {
window.close();
}
});
// Release single instance lock before quitting
// This ensures the new instance can acquire the lock
logger.info('Releasing single instance lock...');
app.releaseSingleInstanceLock();
// Small delay to ensure windows are closed and lock is released
setTimeout(() => {
// quitAndInstall parameters:
// - isSilent: true (don't show installation UI)
// - isForceRunAfter: true (force start app after installation)
logger.info('Calling autoUpdater.quitAndInstall...');
autoUpdater.quitAndInstall(true, true);
}, 100);
};
/**
@@ -0,0 +1,357 @@
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { MacOSSearchServiceImpl } from '../impl/macOS';
/**
* macOS File Search Integration Tests
*
* These tests run against the real macOS Spotlight service
* using files in the current repository.
*
* Run with: bunx vitest run 'macOS.integration.test'
*/
// Get repository root path (assumes test runs from apps/desktop)
const repoRoot = path.resolve(__dirname, '../../../../..');
describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integration', () => {
const searchService = new MacOSSearchServiceImpl();
describe('checkSearchServiceStatus', () => {
it('should verify Spotlight is available on macOS', async () => {
const isAvailable = await searchService.checkSearchServiceStatus();
expect(isAvailable).toBe(true);
});
});
describe('search for known repository files', () => {
it('should find package.json in repo root', async () => {
const results = await searchService.search({
keywords: 'package.json',
limit: 10,
onlyIn: repoRoot,
});
expect(results.length).toBeGreaterThan(0);
// Should find at least one package.json
const packageJson = results.find((r) => r.name === 'package.json');
expect(packageJson).toBeDefined();
expect(packageJson!.type).toBe('json');
expect(packageJson!.path).toContain(repoRoot);
});
it('should find README files', async () => {
const results = await searchService.search({
keywords: 'README',
limit: 10,
onlyIn: repoRoot,
});
expect(results.length).toBeGreaterThan(0);
// Should contain markdown files
const mdFile = results.find((r) => r.type === 'md');
expect(mdFile).toBeDefined();
expect(mdFile!.name).toMatch(/README/i);
});
it('should find TypeScript files', async () => {
const results = await searchService.search({
keywords: 'macOS',
limit: 10,
onlyIn: repoRoot,
});
expect(results.length).toBeGreaterThan(0);
// Should find the macOS.ts implementation file
const macOSFile = results.find((r) => r.name.includes('macOS') && r.type === 'ts');
expect(macOSFile).toBeDefined();
expect(macOSFile!.contentType).toBe('code');
});
it('should find files in apps/desktop directory', async () => {
const desktopPath = path.join(repoRoot, 'apps/desktop');
const results = await searchService.search({
keywords: 'src',
limit: 20,
onlyIn: desktopPath,
});
// Spotlight indexing may not be complete for this directory
// so we make the test lenient
if (results.length > 0) {
// All results should be within apps/desktop
results.forEach((result) => {
expect(result.path).toContain('apps/desktop');
});
} else {
// eslint-disable-next-line no-console
console.warn(
'⚠️ No results found in apps/desktop - Spotlight indexing may not be complete',
);
}
// At minimum, verify the search completed without error
expect(Array.isArray(results)).toBe(true);
});
it('should find test files', async () => {
const results = await searchService.search({
keywords: 'test.ts',
limit: 10,
onlyIn: repoRoot,
});
expect(results.length).toBeGreaterThan(0);
// Should find test files
const testFile = results.find((r) => r.name.endsWith('.test.ts'));
expect(testFile).toBeDefined();
expect(testFile!.path).toContain('__tests__');
});
});
describe('search with filters', () => {
it('should respect limit parameter', async () => {
const limit = 3;
const results = await searchService.search({
keywords: 'src',
limit,
onlyIn: repoRoot,
});
expect(results.length).toBeLessThanOrEqual(limit);
});
it('should search in specific subdirectory only', async () => {
const srcPath = path.join(repoRoot, 'apps/desktop/src');
const results = await searchService.search({
keywords: 'index',
limit: 10,
onlyIn: srcPath,
});
// All results should be within the specified directory
results.forEach((result) => {
expect(result.path).toContain('apps/desktop/src');
});
});
it('should return empty array for non-existent keywords', async () => {
const results = await searchService.search({
keywords: 'xyzabc123unlikely-keyword-that-does-not-exist-12345',
limit: 5,
onlyIn: repoRoot,
});
expect(results).toEqual([]);
});
});
describe('file type detection', () => {
it('should correctly identify TypeScript files', async () => {
const results = await searchService.search({
keywords: 'LocalFileCtr',
limit: 5,
onlyIn: repoRoot,
});
const tsFile = results.find((r) => r.name === 'LocalFileCtr.ts');
if (tsFile) {
expect(tsFile.type).toBe('ts');
expect(tsFile.contentType).toBe('code');
expect(tsFile.isDirectory).toBe(false);
}
});
it('should correctly identify JSON files', async () => {
const results = await searchService.search({
keywords: 'tsconfig',
limit: 5,
onlyIn: repoRoot,
});
const jsonFile = results.find((r) => r.name.includes('tsconfig') && r.type === 'json');
if (jsonFile) {
expect(jsonFile.type).toBe('json');
expect(jsonFile.contentType).toBe('code');
expect(jsonFile.size).toBeGreaterThan(0);
}
});
it('should correctly identify directories', async () => {
const results = await searchService.search({
keywords: '__tests__',
limit: 10,
onlyIn: repoRoot,
});
const testDir = results.find((r) => r.name === '__tests__' && r.isDirectory);
if (testDir) {
expect(testDir.isDirectory).toBe(true);
expect(testDir.type).toBe('');
}
});
it('should correctly identify markdown files', async () => {
const results = await searchService.search({
keywords: 'CLAUDE.md',
limit: 5,
onlyIn: repoRoot,
});
const mdFile = results.find((r) => r.name === 'CLAUDE.md');
if (mdFile) {
expect(mdFile.type).toBe('md');
expect(mdFile.contentType).toBe('text');
}
});
});
describe('file metadata', () => {
it('should return valid file metadata', async () => {
const results = await searchService.search({
keywords: 'package.json',
limit: 1,
onlyIn: repoRoot,
});
expect(results.length).toBeGreaterThan(0);
const file = results[0];
// Verify all metadata fields are present
expect(file.path).toBeTruthy();
expect(file.name).toBeTruthy();
expect(typeof file.isDirectory).toBe('boolean');
expect(typeof file.size).toBe('number');
expect(file.size).toBeGreaterThanOrEqual(0);
expect(file.type).toBeDefined();
expect(file.contentType).toBeDefined();
expect(file.modifiedTime).toBeInstanceOf(Date);
expect(file.createdTime).toBeInstanceOf(Date);
expect(file.lastAccessTime).toBeInstanceOf(Date);
// Dates should be valid
expect(file.modifiedTime.getTime()).toBeGreaterThan(0);
expect(file.createdTime.getTime()).toBeGreaterThan(0);
});
it('should handle files with different extensions', async () => {
const testCases = [
{ keyword: '.ts', expectedType: 'ts', expectedContentType: 'code' },
{ keyword: '.json', expectedType: 'json', expectedContentType: 'code' },
{ keyword: '.txt', expectedType: 'txt', expectedContentType: 'text' },
];
for (const { keyword, expectedType, expectedContentType } of testCases) {
const results = await searchService.search({
keywords: keyword,
limit: 5,
onlyIn: repoRoot,
});
if (results.length > 0) {
const file = results.find((r) => r.type === expectedType);
if (file) {
expect(file.type).toBe(expectedType);
expect(file.contentType).toBe(expectedContentType);
}
}
}
});
});
describe('search accuracy after fix', () => {
it('should use fuzzy matching instead of exact phrase', async () => {
// Test the fix: keywords should do fuzzy matching, not exact phrase
// Before fix: "local file" would only match exact phrase "local file"
// After fix: "local file" should match "LocalFileCtr" (contains "local" and "file")
const results = await searchService.search({
keywords: 'LocalFile',
limit: 10,
onlyIn: repoRoot,
});
expect(results.length).toBeGreaterThan(0);
// Should find LocalFileCtr.ts or similar files
const found = results.some(
(r) => r.name.includes('LocalFile') || r.name.includes('localFile'),
);
expect(found).toBe(true);
});
it('should handle paths with spaces correctly', async () => {
// Test the fix: command args should be properly split
// This test verifies spawn receives correct arguments array
const pathWithSpaces = repoRoot; // May contain spaces in CI or certain setups
const results = await searchService.search({
keywords: 'test',
limit: 5,
onlyIn: pathWithSpaces,
});
// Should not throw error even if path contains spaces
expect(Array.isArray(results)).toBe(true);
});
it('should search case-insensitively', async () => {
// The "cd" flag in kMDItemFSName makes it case-insensitive
const lowerResults = await searchService.search({
keywords: 'readme',
limit: 5,
onlyIn: repoRoot,
});
const upperResults = await searchService.search({
keywords: 'README',
limit: 5,
onlyIn: repoRoot,
});
// Both searches should find similar files
expect(lowerResults.length).toBeGreaterThan(0);
expect(upperResults.length).toBeGreaterThan(0);
});
});
describe('error handling', () => {
it('should handle non-existent directory gracefully', async () => {
const nonExistentPath = path.join(repoRoot, 'this-directory-does-not-exist-12345');
const results = await searchService.search({
keywords: 'test',
limit: 5,
onlyIn: nonExistentPath,
});
// Should return empty array instead of throwing
expect(results).toEqual([]);
});
});
describe('updateSearchIndex', () => {
it.skip('should handle index update request', async () => {
// Index update requires elevated permissions, may fail in restricted environments
const result = await searchService.updateSearchIndex(repoRoot);
// Should return boolean (true if succeeded, false if failed)
expect(typeof result).toBe('boolean');
}, 15000); // Index update can take time
});
});
// Skip message for non-macOS platforms
if (process.platform !== 'darwin') {
// eslint-disable-next-line no-console
console.log('⏭️ Skipping macOS integration tests on', process.platform, '(only runs on darwin)');
}
@@ -23,12 +23,11 @@ export class MacOSSearchServiceImpl extends FileSearchImpl {
*/
async search(options: SearchOptions): Promise<FileResult[]> {
// Build the command first, regardless of execution method
const command = this.buildSearchCommand(options);
logger.debug(`Executing command: ${command}`);
const { cmd, args, commandString } = this.buildSearchCommand(options);
logger.debug(`Executing command: ${commandString}`);
// Use spawn for both live and non-live updates to handle large outputs
return new Promise((resolve, reject) => {
const [cmd, ...args] = command.split(' ');
const childProcess = spawn(cmd, args);
let results: string[] = []; // Store raw file paths
@@ -137,31 +136,39 @@ export class MacOSSearchServiceImpl extends FileSearchImpl {
/**
* Build mdfind command string
* @param options Search options
* @returns Complete command string
* @returns Command components (cmd, args array, and command string for logging)
*/
private buildSearchCommand(options: SearchOptions): string {
// Basic command
let command = 'mdfind';
// Add options
const mdFindOptions: string[] = [];
private buildSearchCommand(options: SearchOptions): {
args: string[];
cmd: string;
commandString: string;
} {
// Command and arguments array
const cmd = 'mdfind';
const args: string[] = [];
// macOS mdfind doesn't support -limit parameter, we'll limit results in post-processing
// Search in specific directory
if (options.onlyIn) {
mdFindOptions.push(`-onlyin "${options.onlyIn}"`);
args.push('-onlyin', options.onlyIn);
}
// Live update
if (options.liveUpdate) {
mdFindOptions.push('-live');
args.push('-live');
}
// Detailed metadata
if (options.detailed) {
mdFindOptions.push(
'-attr kMDItemDisplayName kMDItemContentType kMDItemKind kMDItemFSSize kMDItemFSCreationDate kMDItemFSContentChangeDate',
args.push(
'-attr',
'kMDItemDisplayName',
'kMDItemContentType',
'kMDItemKind',
'kMDItemFSSize',
'kMDItemFSCreationDate',
'kMDItemFSContentChangeDate',
);
}
@@ -171,9 +178,10 @@ export class MacOSSearchServiceImpl extends FileSearchImpl {
// Basic query
if (options.keywords) {
// If the query string doesn't use Spotlight query syntax (doesn't contain kMDItem properties),
// treat it as plain text search
// treat it as a flexible name search rather than exact phrase match
if (!options.keywords.includes('kMDItem')) {
queryExpression = `"${options.keywords.replaceAll('"', '\\"')}"`;
// Use kMDItemFSName for filename matching with wildcards for better flexibility
queryExpression = `kMDItemFSName == "*${options.keywords.replaceAll('"', '\\"')}*"cd`;
} else {
queryExpression = options.keywords;
}
@@ -244,15 +252,15 @@ export class MacOSSearchServiceImpl extends FileSearchImpl {
}
}
// Combine complete command
if (mdFindOptions.length > 0) {
command += ' ' + mdFindOptions.join(' ');
// Add query expression to args
if (queryExpression) {
args.push(queryExpression);
}
// Finally add query expression
command += ` ${queryExpression}`;
// Build command string for logging
const commandString = `${cmd} ${args.map((arg) => (arg.includes(' ') || arg.includes('*') ? `"${arg}"` : arg)).join(' ')}`;
return command;
return { args, cmd, commandString };
}
/**
@@ -0,0 +1,401 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ProxyDispatcherManager } from '../dispatcher';
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// Mock undici
vi.mock('undici', () => ({
Agent: vi.fn(),
ProxyAgent: vi.fn(),
getGlobalDispatcher: vi.fn(),
setGlobalDispatcher: vi.fn(),
}));
// Mock fetch-socks
vi.mock('fetch-socks', () => ({
socksDispatcher: vi.fn(),
}));
// Mock ProxyUrlBuilder
vi.mock('../urlBuilder', () => ({
ProxyUrlBuilder: {
build: vi.fn(),
},
}));
describe('ProxyDispatcherManager', () => {
let mockDispatcher: any;
let mockAgent: any;
let mockProxyAgent: any;
let mockGetGlobalDispatcher: any;
let mockSetGlobalDispatcher: any;
let mockSocksDispatcher: any;
let mockProxyUrlBuilder: any;
beforeEach(async () => {
vi.clearAllMocks();
// Import mocked modules
const undici = await import('undici');
const fetchSocks = await import('fetch-socks');
const urlBuilder = await import('../urlBuilder');
mockAgent = vi.mocked(undici.Agent);
mockProxyAgent = vi.mocked(undici.ProxyAgent);
mockGetGlobalDispatcher = vi.mocked(undici.getGlobalDispatcher);
mockSetGlobalDispatcher = vi.mocked(undici.setGlobalDispatcher);
mockSocksDispatcher = vi.mocked(fetchSocks.socksDispatcher);
mockProxyUrlBuilder = vi.mocked(urlBuilder.ProxyUrlBuilder.build);
// Setup mock dispatcher with destroy method
mockDispatcher = {
destroy: vi.fn().mockResolvedValue(undefined),
};
mockGetGlobalDispatcher.mockReturnValue(mockDispatcher);
mockAgent.mockReturnValue({ destroy: vi.fn().mockResolvedValue(undefined) });
mockProxyAgent.mockReturnValue({ destroy: vi.fn().mockResolvedValue(undefined) });
mockSocksDispatcher.mockReturnValue({ destroy: vi.fn().mockResolvedValue(undefined) });
// Setup ProxyUrlBuilder mock to return properly formatted URLs
mockProxyUrlBuilder.mockImplementation((config: NetworkProxySettings) => {
if (config.proxyRequireAuth && config.proxyUsername && config.proxyPassword) {
return `${config.proxyType}://${config.proxyUsername}:${config.proxyPassword}@${config.proxyServer}:${config.proxyPort}`;
}
return `${config.proxyType}://${config.proxyServer}:${config.proxyPort}`;
});
});
describe('createProxyAgent', () => {
describe('HTTP/HTTPS proxy', () => {
it('should create ProxyAgent for http proxy', () => {
const proxyUrl = 'http://proxy.example.com:8080';
ProxyDispatcherManager.createProxyAgent('http', proxyUrl);
expect(mockProxyAgent).toHaveBeenCalledWith({ uri: proxyUrl });
});
it('should create ProxyAgent for https proxy', () => {
const proxyUrl = 'https://proxy.example.com:8080';
ProxyDispatcherManager.createProxyAgent('https', proxyUrl);
expect(mockProxyAgent).toHaveBeenCalledWith({ uri: proxyUrl });
});
it('should create ProxyAgent with authentication', () => {
const proxyUrl = 'http://user:pass@proxy.example.com:8080';
ProxyDispatcherManager.createProxyAgent('http', proxyUrl);
expect(mockProxyAgent).toHaveBeenCalledWith({ uri: proxyUrl });
});
});
describe('SOCKS5 proxy', () => {
it('should create socksDispatcher for socks5 proxy without auth', () => {
const proxyUrl = 'socks5://proxy.example.com:1080';
ProxyDispatcherManager.createProxyAgent('socks5', proxyUrl);
expect(mockSocksDispatcher).toHaveBeenCalledWith([
{
host: 'proxy.example.com',
port: 1080,
type: 5,
},
]);
});
it('should create socksDispatcher for socks5 proxy with auth', () => {
const proxyUrl = 'socks5://user:pass@proxy.example.com:1080';
ProxyDispatcherManager.createProxyAgent('socks5', proxyUrl);
expect(mockSocksDispatcher).toHaveBeenCalledWith([
{
host: 'proxy.example.com',
port: 1080,
type: 5,
userId: 'user',
password: 'pass',
},
]);
});
it('should create socksDispatcher with IPv4 address', () => {
const proxyUrl = 'socks5://192.168.1.1:1080';
ProxyDispatcherManager.createProxyAgent('socks5', proxyUrl);
expect(mockSocksDispatcher).toHaveBeenCalledWith([
{
host: '192.168.1.1',
port: 1080,
type: 5,
},
]);
});
it('should create socksDispatcher with different port', () => {
const proxyUrl = 'socks5://proxy.example.com:9050';
ProxyDispatcherManager.createProxyAgent('socks5', proxyUrl);
expect(mockSocksDispatcher).toHaveBeenCalledWith([
{
host: 'proxy.example.com',
port: 9050,
type: 5,
},
]);
});
});
describe('error handling', () => {
it('should throw error when ProxyAgent creation fails', () => {
mockProxyAgent.mockImplementationOnce(() => {
throw new Error('ProxyAgent creation failed');
});
expect(() => {
ProxyDispatcherManager.createProxyAgent('http', 'http://invalid');
}).toThrow('Failed to create proxy agent: ProxyAgent creation failed');
});
it('should throw error when socksDispatcher creation fails', () => {
mockSocksDispatcher.mockImplementationOnce(() => {
throw new Error('SOCKS dispatcher creation failed');
});
expect(() => {
ProxyDispatcherManager.createProxyAgent('socks5', 'socks5://invalid');
}).toThrow('Failed to create proxy agent: SOCKS dispatcher creation failed');
});
it('should throw error with unknown error type', () => {
mockProxyAgent.mockImplementationOnce(() => {
throw 'String error';
});
expect(() => {
ProxyDispatcherManager.createProxyAgent('http', 'http://invalid');
}).toThrow('Failed to create proxy agent: Unknown error');
});
});
});
describe('applyProxySettings', () => {
const validConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
describe('disable proxy', () => {
it('should reset to direct connection when proxy is disabled', async () => {
const config: NetworkProxySettings = {
...validConfig,
enableProxy: false,
};
await ProxyDispatcherManager.applyProxySettings(config);
expect(mockDispatcher.destroy).toHaveBeenCalled();
expect(mockAgent).toHaveBeenCalled();
expect(mockSetGlobalDispatcher).toHaveBeenCalled();
});
it('should handle dispatcher destruction failure gracefully', async () => {
mockDispatcher.destroy.mockRejectedValueOnce(new Error('Destroy failed'));
const config: NetworkProxySettings = {
...validConfig,
enableProxy: false,
};
// Should not throw even if destroy fails
await expect(ProxyDispatcherManager.applyProxySettings(config)).resolves.not.toThrow();
});
it('should handle dispatcher without destroy method', async () => {
mockGetGlobalDispatcher.mockReturnValueOnce({});
const config: NetworkProxySettings = {
...validConfig,
enableProxy: false,
};
await expect(ProxyDispatcherManager.applyProxySettings(config)).resolves.not.toThrow();
});
});
describe('enable proxy', () => {
it('should apply http proxy settings', async () => {
const config: NetworkProxySettings = {
...validConfig,
proxyType: 'http',
};
await ProxyDispatcherManager.applyProxySettings(config);
expect(mockDispatcher.destroy).toHaveBeenCalled();
expect(mockProxyAgent).toHaveBeenCalledWith({
uri: 'http://proxy.example.com:8080',
});
expect(mockSetGlobalDispatcher).toHaveBeenCalled();
});
it('should apply https proxy settings', async () => {
const config: NetworkProxySettings = {
...validConfig,
proxyType: 'https',
};
await ProxyDispatcherManager.applyProxySettings(config);
expect(mockProxyAgent).toHaveBeenCalledWith({
uri: 'https://proxy.example.com:8080',
});
});
it('should apply socks5 proxy settings', async () => {
const config: NetworkProxySettings = {
...validConfig,
proxyType: 'socks5',
};
await ProxyDispatcherManager.applyProxySettings(config);
expect(mockSocksDispatcher).toHaveBeenCalled();
expect(mockSetGlobalDispatcher).toHaveBeenCalled();
});
it('should apply proxy with authentication', async () => {
const config: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: true,
proxyUsername: 'testuser',
proxyPassword: 'testpass',
};
await ProxyDispatcherManager.applyProxySettings(config);
expect(mockProxyAgent).toHaveBeenCalledWith({
uri: 'http://testuser:testpass@proxy.example.com:8080',
});
});
it('should destroy old dispatcher before applying new proxy', async () => {
const destroySpy = vi.fn().mockResolvedValue(undefined);
mockGetGlobalDispatcher.mockReturnValue({ destroy: destroySpy });
await ProxyDispatcherManager.applyProxySettings(validConfig);
expect(destroySpy).toHaveBeenCalled();
expect(mockSetGlobalDispatcher).toHaveBeenCalled();
});
});
describe('concurrent proxy changes', () => {
it('should queue concurrent proxy setting changes', async () => {
const config1: NetworkProxySettings = {
...validConfig,
proxyPort: '8080',
};
const config2: NetworkProxySettings = {
...validConfig,
proxyPort: '8081',
};
// Start both operations concurrently
const promise1 = ProxyDispatcherManager.applyProxySettings(config1);
const promise2 = ProxyDispatcherManager.applyProxySettings(config2);
await Promise.all([promise1, promise2]);
// Both operations should complete successfully
expect(mockSetGlobalDispatcher).toHaveBeenCalled();
});
it('should process queued operations sequentially', async () => {
const operations: Promise<void>[] = [];
// Queue multiple operations
for (let i = 0; i < 5; i++) {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: `${8080 + i}`,
};
operations.push(ProxyDispatcherManager.applyProxySettings(config));
}
await Promise.all(operations);
// All operations should complete
expect(mockSetGlobalDispatcher).toHaveBeenCalledTimes(5);
});
it('should handle errors in queued operations', async () => {
mockProxyAgent.mockReturnValueOnce({ destroy: vi.fn() }).mockImplementationOnce(() => {
throw new Error('Agent creation failed');
});
const config1: NetworkProxySettings = {
...validConfig,
proxyPort: '8080',
};
const config2: NetworkProxySettings = {
...validConfig,
proxyPort: '8081',
};
const promise1 = ProxyDispatcherManager.applyProxySettings(config1);
const promise2 = ProxyDispatcherManager.applyProxySettings(config2);
await expect(promise1).resolves.not.toThrow();
await expect(promise2).rejects.toThrow();
});
});
describe('error handling', () => {
it('should propagate error when agent creation fails', async () => {
mockProxyAgent.mockImplementationOnce(() => {
throw new Error('Agent creation failed');
});
await expect(ProxyDispatcherManager.applyProxySettings(validConfig)).rejects.toThrow(
'Failed to create proxy agent',
);
});
it('should handle null dispatcher gracefully', async () => {
mockGetGlobalDispatcher.mockReturnValueOnce(null);
await expect(ProxyDispatcherManager.applyProxySettings(validConfig)).resolves.not.toThrow();
});
it('should handle undefined dispatcher gracefully', async () => {
mockGetGlobalDispatcher.mockReturnValueOnce(undefined);
await expect(ProxyDispatcherManager.applyProxySettings(validConfig)).resolves.not.toThrow();
});
});
});
});
@@ -0,0 +1,531 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ProxyConnectionTester } from '../tester';
// Mock logger
vi.mock('@/utils/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
// Mock undici
vi.mock('undici', () => ({
fetch: vi.fn(),
getGlobalDispatcher: vi.fn(),
setGlobalDispatcher: vi.fn(),
}));
// Mock ProxyConfigValidator
vi.mock('../validator', () => ({
ProxyConfigValidator: {
validate: vi.fn(),
},
}));
// Mock ProxyUrlBuilder
vi.mock('../urlBuilder', () => ({
ProxyUrlBuilder: {
build: vi.fn(),
},
}));
// Mock ProxyDispatcherManager
vi.mock('../dispatcher', () => ({
ProxyDispatcherManager: {
createProxyAgent: vi.fn(),
},
}));
describe('ProxyConnectionTester', () => {
let mockAgent: any;
let mockOriginalDispatcher: any;
let mockFetch: any;
let mockGetGlobalDispatcher: any;
let mockSetGlobalDispatcher: any;
let mockProxyDispatcherManager: any;
let mockProxyConfigValidator: any;
let mockProxyUrlBuilder: any;
beforeEach(async () => {
vi.clearAllMocks();
// Import mocked modules
const undici = await import('undici');
const dispatcher = await import('../dispatcher');
const validator = await import('../validator');
const urlBuilder = await import('../urlBuilder');
mockFetch = vi.mocked(undici.fetch);
mockGetGlobalDispatcher = vi.mocked(undici.getGlobalDispatcher);
mockSetGlobalDispatcher = vi.mocked(undici.setGlobalDispatcher);
mockProxyDispatcherManager = vi.mocked(dispatcher.ProxyDispatcherManager);
mockProxyConfigValidator = vi.mocked(validator.ProxyConfigValidator);
mockProxyUrlBuilder = vi.mocked(urlBuilder.ProxyUrlBuilder.build);
// Setup mock agent
mockAgent = {
destroy: vi.fn().mockResolvedValue(undefined),
};
mockOriginalDispatcher = {
destroy: vi.fn().mockResolvedValue(undefined),
};
mockGetGlobalDispatcher.mockReturnValue(mockOriginalDispatcher);
mockProxyDispatcherManager.createProxyAgent.mockReturnValue(mockAgent);
mockProxyConfigValidator.validate.mockReturnValue({ isValid: true, errors: [] });
mockProxyUrlBuilder.mockImplementation((config: NetworkProxySettings) => {
return `${config.proxyType}://${config.proxyServer}:${config.proxyPort}`;
});
});
describe('testConnection', () => {
describe('successful connection', () => {
it('should return success for successful HTTP request', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const result = await ProxyConnectionTester.testConnection();
expect(result.success).toBe(true);
expect(result.responseTime).toBeGreaterThanOrEqual(0);
expect(result.message).toBeUndefined();
expect(mockFetch).toHaveBeenCalledWith(
'https://www.google.com',
expect.objectContaining({
headers: expect.objectContaining({
'User-Agent': 'LobeChat-Desktop/1.0.0',
}),
signal: expect.any(AbortSignal),
}),
);
});
it('should return success with custom URL', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const customUrl = 'https://api.example.com';
const result = await ProxyConnectionTester.testConnection(customUrl);
expect(result.success).toBe(true);
expect(mockFetch).toHaveBeenCalledWith(customUrl, expect.any(Object));
});
it('should return success with custom timeout', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const result = await ProxyConnectionTester.testConnection('https://www.google.com', 5000);
expect(result.success).toBe(true);
});
it('should include response time in result', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const result = await ProxyConnectionTester.testConnection();
expect(result.responseTime).toBeDefined();
expect(typeof result.responseTime).toBe('number');
expect(result.responseTime).toBeGreaterThanOrEqual(0);
});
});
describe('connection failures', () => {
it('should return failure for HTTP error status', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
});
const result = await ProxyConnectionTester.testConnection();
expect(result.success).toBe(false);
expect(result.message).toContain('HTTP 404');
expect(result.message).toContain('Not Found');
expect(result.responseTime).toBeGreaterThanOrEqual(0);
});
it('should return failure for HTTP 500 error', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
});
const result = await ProxyConnectionTester.testConnection();
expect(result.success).toBe(false);
expect(result.message).toContain('HTTP 500');
});
it('should return failure for network error', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'));
const result = await ProxyConnectionTester.testConnection();
expect(result.success).toBe(false);
expect(result.message).toBe('Network error');
expect(result.responseTime).toBeGreaterThanOrEqual(0);
});
it('should return failure for timeout', async () => {
mockFetch.mockImplementationOnce(() => {
return new Promise((_, reject) => {
const error = new Error('Request aborted');
error.name = 'AbortError';
setTimeout(() => reject(error), 50);
});
});
const result = await ProxyConnectionTester.testConnection('https://www.google.com', 100);
expect(result.success).toBe(false);
expect(result.message).toBeTruthy();
});
it('should return failure for connection refused', async () => {
mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));
const result = await ProxyConnectionTester.testConnection();
expect(result.success).toBe(false);
expect(result.message).toBe('ECONNREFUSED');
});
it('should handle unknown error type', async () => {
mockFetch.mockRejectedValueOnce('String error');
const result = await ProxyConnectionTester.testConnection();
expect(result.success).toBe(false);
expect(result.message).toBe('Unknown error');
});
});
});
describe('testProxyConfig', () => {
const validConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
describe('config validation', () => {
it('should return failure for invalid config', async () => {
mockProxyConfigValidator.validate.mockReturnValueOnce({
isValid: false,
errors: ['Proxy server is required', 'Invalid port'],
});
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
expect(result.success).toBe(false);
expect(result.message).toContain('Invalid proxy configuration');
expect(result.message).toContain('Proxy server is required');
expect(result.message).toContain('Invalid port');
});
it('should validate config before testing', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
await ProxyConnectionTester.testProxyConfig(validConfig);
expect(mockProxyConfigValidator.validate).toHaveBeenCalledWith(validConfig);
});
});
describe('disabled proxy', () => {
it('should test direct connection when proxy is disabled', async () => {
const disabledConfig: NetworkProxySettings = {
...validConfig,
enableProxy: false,
};
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const result = await ProxyConnectionTester.testProxyConfig(disabledConfig);
expect(result.success).toBe(true);
expect(mockFetch).toHaveBeenCalled();
});
it('should use custom test URL for disabled proxy', async () => {
const disabledConfig: NetworkProxySettings = {
...validConfig,
enableProxy: false,
};
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const customUrl = 'https://api.example.com';
await ProxyConnectionTester.testProxyConfig(disabledConfig, customUrl);
expect(mockFetch).toHaveBeenCalledWith(customUrl, expect.any(Object));
});
});
describe('enabled proxy', () => {
it('should test proxy connection successfully', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
expect(result.success).toBe(true);
expect(result.responseTime).toBeGreaterThanOrEqual(0);
});
it('should create temporary proxy agent for testing', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
await ProxyConnectionTester.testProxyConfig(validConfig);
expect(mockProxyDispatcherManager.createProxyAgent).toHaveBeenCalledWith(
'http',
'http://proxy.example.com:8080',
);
});
it('should restore original dispatcher after test', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
await ProxyConnectionTester.testProxyConfig(validConfig);
expect(mockSetGlobalDispatcher).toHaveBeenCalledWith(mockOriginalDispatcher);
});
it('should destroy temporary agent after test', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
await ProxyConnectionTester.testProxyConfig(validConfig);
expect(mockAgent.destroy).toHaveBeenCalled();
});
it('should restore dispatcher even if test fails', async () => {
mockFetch.mockRejectedValueOnce(new Error('Connection failed'));
await ProxyConnectionTester.testProxyConfig(validConfig);
expect(mockSetGlobalDispatcher).toHaveBeenCalledWith(mockOriginalDispatcher);
});
it('should destroy agent even if test fails', async () => {
mockFetch.mockRejectedValueOnce(new Error('Connection failed'));
await ProxyConnectionTester.testProxyConfig(validConfig);
expect(mockAgent.destroy).toHaveBeenCalled();
});
it('should handle agent destroy failure gracefully', async () => {
mockAgent.destroy.mockRejectedValueOnce(new Error('Destroy failed'));
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
expect(result.success).toBe(true);
});
it('should test with custom URL', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const customUrl = 'https://httpbin.org/ip';
await ProxyConnectionTester.testProxyConfig(validConfig, customUrl);
expect(mockFetch).toHaveBeenCalledWith(
customUrl,
expect.objectContaining({
dispatcher: mockAgent,
}),
);
});
it('should test socks5 proxy', async () => {
const socks5Config: NetworkProxySettings = {
...validConfig,
proxyType: 'socks5',
};
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
await ProxyConnectionTester.testProxyConfig(socks5Config);
expect(mockProxyDispatcherManager.createProxyAgent).toHaveBeenCalledWith(
'socks5',
expect.any(String),
);
});
it('should test proxy with authentication', async () => {
const authConfig: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: true,
proxyUsername: 'user',
proxyPassword: 'pass',
};
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
await ProxyConnectionTester.testProxyConfig(authConfig);
expect(mockProxyUrlBuilder).toHaveBeenCalledWith(authConfig);
});
});
describe('error handling', () => {
it('should return failure when agent creation fails', async () => {
mockProxyDispatcherManager.createProxyAgent.mockImplementationOnce(() => {
throw new Error('Agent creation failed');
});
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
expect(result.success).toBe(false);
expect(result.message).toContain('Proxy test failed');
expect(result.message).toContain('Agent creation failed');
});
it('should return failure when fetch fails', async () => {
mockFetch.mockRejectedValueOnce(new Error('Connection timeout'));
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
expect(result.success).toBe(false);
expect(result.message).toContain('Connection timeout');
});
it('should return failure for HTTP error response', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 407,
statusText: 'Proxy Authentication Required',
});
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
expect(result.success).toBe(false);
expect(result.message).toContain('HTTP 407');
});
it('should handle timeout correctly', async () => {
mockFetch.mockImplementationOnce(() => {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), 50);
});
});
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
expect(result.success).toBe(false);
});
it('should handle unknown error type', async () => {
mockProxyDispatcherManager.createProxyAgent.mockImplementationOnce(() => {
throw 'String error';
});
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
expect(result.success).toBe(false);
expect(result.message).toContain('Unknown error');
});
it('should handle null agent', async () => {
mockProxyDispatcherManager.createProxyAgent.mockReturnValueOnce(null);
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
// Should handle gracefully
expect(result).toBeDefined();
});
it('should handle agent without destroy method', async () => {
mockProxyDispatcherManager.createProxyAgent.mockReturnValueOnce({});
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
});
const result = await ProxyConnectionTester.testProxyConfig(validConfig);
expect(result.success).toBe(true);
});
});
});
});
@@ -0,0 +1,349 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { describe, expect, it } from 'vitest';
import { ProxyUrlBuilder } from '../urlBuilder';
describe('ProxyUrlBuilder', () => {
const baseConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
describe('build', () => {
describe('without authentication', () => {
it('should build URL with http proxy type', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyType: 'http',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://proxy.example.com:8080');
});
it('should build URL with https proxy type', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyType: 'https',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('https://proxy.example.com:8080');
});
it('should build URL with socks5 proxy type', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyType: 'socks5',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('socks5://proxy.example.com:8080');
});
it('should build URL with IPv4 address', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyServer: '192.168.1.1',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://192.168.1.1:8080');
});
it('should build URL with localhost', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyServer: 'localhost',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://localhost:8080');
});
it('should build URL with different port', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyPort: '3128',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://proxy.example.com:3128');
});
});
describe('with authentication', () => {
it('should build URL with username and password', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'testuser',
proxyPassword: 'testpass',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://testuser:testpass@proxy.example.com:8080');
});
it('should build URL with encoded username containing @ symbol', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'user@domain.com',
proxyPassword: 'password',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://user%40domain.com:password@proxy.example.com:8080');
// Verify encoding
expect(url).toContain('user%40domain.com');
});
it('should build URL with encoded password containing colon', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'user',
proxyPassword: 'pass:word',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://user:pass%3Aword@proxy.example.com:8080');
// Verify encoding
expect(url).toContain('pass%3Aword');
});
it('should build URL with encoded special characters in username', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'user name',
proxyPassword: 'password',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://user%20name:password@proxy.example.com:8080');
// Verify encoding
expect(url).toContain('user%20name');
});
it('should build URL with encoded special characters in password', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'user',
proxyPassword: 'p@ss w0rd!',
};
const url = ProxyUrlBuilder.build(config);
// Verify encoding of special characters
expect(url).toContain(encodeURIComponent('p@ss w0rd!'));
expect(url).toContain('user:');
expect(url).toContain('@proxy.example.com:8080');
});
it('should build URL with encoded slash in credentials', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'domain/user',
proxyPassword: 'pass/word',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://domain%2Fuser:pass%2Fword@proxy.example.com:8080');
// Verify encoding
expect(url).toContain('domain%2Fuser');
expect(url).toContain('pass%2Fword');
});
it('should build URL with encoded hash in credentials', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'user#123',
proxyPassword: 'pass#word',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://user%23123:pass%23word@proxy.example.com:8080');
// Verify encoding
expect(url).toContain('user%23123');
expect(url).toContain('pass%23word');
});
it('should build URL with encoded question mark in credentials', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'user?name',
proxyPassword: 'pass?word',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://user%3Fname:pass%3Fword@proxy.example.com:8080');
// Verify encoding
expect(url).toContain('user%3Fname');
expect(url).toContain('pass%3Fword');
});
it('should build URL with https proxy type and auth', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyType: 'https',
proxyRequireAuth: true,
proxyUsername: 'testuser',
proxyPassword: 'testpass',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('https://testuser:testpass@proxy.example.com:8080');
});
it('should build URL with socks5 proxy type and auth', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyType: 'socks5',
proxyRequireAuth: true,
proxyUsername: 'sockuser',
proxyPassword: 'sockpass',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('socks5://sockuser:sockpass@proxy.example.com:8080');
});
});
describe('edge cases', () => {
it('should not include auth when proxyRequireAuth is false but credentials are provided', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: false,
proxyUsername: 'testuser',
proxyPassword: 'testpass',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://proxy.example.com:8080');
expect(url).not.toContain('testuser');
expect(url).not.toContain('testpass');
});
it('should not include auth when username is empty', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: '',
proxyPassword: 'testpass',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://proxy.example.com:8080');
expect(url).not.toContain('@');
});
it('should not include auth when password is empty', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'testuser',
proxyPassword: '',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://proxy.example.com:8080');
expect(url).not.toContain('@');
});
it('should not include auth when username is undefined', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: undefined,
proxyPassword: 'testpass',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://proxy.example.com:8080');
expect(url).not.toContain('@');
});
it('should not include auth when password is undefined', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'testuser',
proxyPassword: undefined,
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://proxy.example.com:8080');
expect(url).not.toContain('@');
});
it('should handle minimum port number', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyPort: '1',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://proxy.example.com:1');
});
it('should handle maximum port number', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyPort: '65535',
};
const url = ProxyUrlBuilder.build(config);
expect(url).toBe('http://proxy.example.com:65535');
});
it('should handle complex URL encoding scenario', () => {
const config: NetworkProxySettings = {
...baseConfig,
proxyRequireAuth: true,
proxyUsername: 'user@example.com:admin',
proxyPassword: 'p@ss:w0rd#123',
};
const url = ProxyUrlBuilder.build(config);
// Verify all special characters are encoded
const expectedUsername = encodeURIComponent('user@example.com:admin');
const expectedPassword = encodeURIComponent('p@ss:w0rd#123');
expect(url).toBe(`http://${expectedUsername}:${expectedPassword}@proxy.example.com:8080`);
});
});
});
});
@@ -0,0 +1,492 @@
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { describe, expect, it } from 'vitest';
import { ProxyConfigValidator } from '../validator';
describe('ProxyConfigValidator', () => {
const validConfig: NetworkProxySettings = {
enableProxy: true,
proxyType: 'http',
proxyServer: 'proxy.example.com',
proxyPort: '8080',
proxyRequireAuth: false,
proxyBypass: 'localhost,127.0.0.1,::1',
};
describe('validate', () => {
describe('disabled proxy', () => {
it('should validate successfully when proxy is disabled', () => {
const config: NetworkProxySettings = {
...validConfig,
enableProxy: false,
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should skip validation for disabled proxy even with invalid fields', () => {
const config: NetworkProxySettings = {
enableProxy: false,
proxyType: 'invalid' as any,
proxyServer: '',
proxyPort: 'invalid',
proxyRequireAuth: false,
proxyBypass: 'localhost',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
});
describe('proxy type validation', () => {
it('should accept http proxy type', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyType: 'http',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should accept https proxy type', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyType: 'https',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should accept socks5 proxy type', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyType: 'socks5',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject unsupported proxy type', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyType: 'socks4' as any,
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toContain('Unsupported proxy type');
expect(result.errors[0]).toContain('socks4');
});
it('should reject invalid proxy type', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyType: 'ftp' as any,
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors[0]).toContain('Supported types: http, https, socks5');
});
});
describe('proxy server validation', () => {
it('should accept valid domain name', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: 'proxy.example.com',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should accept valid IPv4 address', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: '192.168.1.1',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should accept localhost', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: 'localhost',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should accept subdomain', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: 'proxy.subdomain.example.com',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject empty proxy server', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: '',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Proxy server is required when proxy is enabled');
});
it('should reject whitespace-only proxy server', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: ' ',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Proxy server is required when proxy is enabled');
});
it('should reject invalid domain format', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: 'invalid..domain',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Invalid proxy server format');
});
it('should reject domain starting with hyphen', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: '-proxy.com',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Invalid proxy server format');
});
it('should reject domain with invalid characters', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: 'proxy@example.com',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Invalid proxy server format');
});
});
describe('proxy port validation', () => {
it('should accept valid port 1', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: '1',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should accept valid port 65535', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: '65535',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should accept common proxy port 8080', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: '8080',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject empty proxy port', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: '',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Proxy port is required when proxy is enabled');
});
it('should reject whitespace-only proxy port', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: ' ',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Proxy port is required when proxy is enabled');
});
it('should reject port 0', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: '0',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Proxy port must be a valid number between 1 and 65535');
});
it('should reject port above 65535', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: '65536',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Proxy port must be a valid number between 1 and 65535');
});
it('should reject negative port', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: '-1',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Proxy port must be a valid number between 1 and 65535');
});
it('should reject non-numeric port', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyPort: 'abc',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Proxy port must be a valid number between 1 and 65535');
});
});
describe('authentication validation', () => {
it('should validate successfully with auth disabled', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: false,
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should validate successfully with auth enabled and credentials provided', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: true,
proxyUsername: 'testuser',
proxyPassword: 'testpass',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject when auth is enabled but username is missing', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: true,
proxyUsername: '',
proxyPassword: 'testpass',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain(
'Proxy username is required when authentication is enabled',
);
});
it('should reject when auth is enabled but username is whitespace', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: true,
proxyUsername: ' ',
proxyPassword: 'testpass',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain(
'Proxy username is required when authentication is enabled',
);
});
it('should reject when auth is enabled but password is missing', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: true,
proxyUsername: 'testuser',
proxyPassword: '',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain(
'Proxy password is required when authentication is enabled',
);
});
it('should reject when auth is enabled but password is whitespace', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: true,
proxyUsername: 'testuser',
proxyPassword: ' ',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toContain(
'Proxy password is required when authentication is enabled',
);
});
it('should reject when auth is enabled but both username and password are missing', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: true,
proxyUsername: '',
proxyPassword: '',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toHaveLength(2);
expect(result.errors).toContain(
'Proxy username is required when authentication is enabled',
);
expect(result.errors).toContain(
'Proxy password is required when authentication is enabled',
);
});
it('should allow missing credentials when auth is disabled', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyRequireAuth: false,
proxyUsername: undefined,
proxyPassword: undefined,
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
});
describe('multiple validation errors', () => {
it('should collect all validation errors', () => {
const config: NetworkProxySettings = {
enableProxy: true,
proxyType: 'invalid' as any,
proxyServer: '',
proxyPort: 'abc',
proxyRequireAuth: true,
proxyUsername: '',
proxyPassword: '',
proxyBypass: 'localhost',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(1);
});
it('should collect errors for invalid server and port', () => {
const config: NetworkProxySettings = {
...validConfig,
proxyServer: 'invalid..domain',
proxyPort: '99999',
};
const result = ProxyConfigValidator.validate(config);
expect(result.isValid).toBe(false);
expect(result.errors).toHaveLength(2);
expect(result.errors).toContain('Invalid proxy server format');
expect(result.errors).toContain('Proxy port must be a valid number between 1 and 65535');
});
});
});
});
@@ -1,6 +1,6 @@
// copy from https://github.com/kirill-konshin/next-electron-rsc
import { serialize as serializeCookie } from 'cookie';
import { type Protocol, type Session, protocol } from 'electron';
import { type Protocol, type Session } from 'electron';
import type { NextConfig } from 'next';
import type NextNodeServer from 'next/dist/server/next-server';
import assert from 'node:assert';
@@ -202,6 +202,11 @@ export function createHandler({
if (!isDev) {
logger.info('Initializing Next.js app for production');
// https://github.com/lobehub/lobe-chat/pull/9851
// @ts-expect-error
// noinspection JSConstantReassignment
process.env.NODE_ENV = 'production';
const next = require(resolve.sync('next', { basedir: standaloneDir }));
// @see https://github.com/vercel/next.js/issues/64031#issuecomment-2078708340
@@ -209,10 +214,7 @@ export function createHandler({
.config as NextConfig;
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config);
app = next({
dev: false,
dir: standaloneDir,
}) as NextNodeServer;
app = next({ dir: standaloneDir }) as NextNodeServer;
handler = app.getRequestHandler();
preparePromise = app.prepare();
-1
View File
@@ -11,7 +11,6 @@
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/main/*"],
"~common/*": ["src/common/*"]
+801
View File
@@ -1,4 +1,805 @@
[
{
"children": {
"improvements": ["Remove language_model_settings and remove isDeprecatedEdition."]
},
"date": "2025-11-17",
"version": "2.0.0-next.69"
},
{
"children": {
"fixes": ["The tool to fail execution on ollama when a message contains b…."]
},
"date": "2025-11-16",
"version": "2.0.0-next.68"
},
{
"children": {
"improvements": ["Refactor to virtua."]
},
"date": "2025-11-16",
"version": "2.0.0-next.67"
},
{
"children": {
"features": ["Support to collapse message."]
},
"date": "2025-11-16",
"version": "2.0.0-next.66"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-11-16",
"version": "2.0.0-next.65"
},
{
"children": {
"improvements": ["Refactor package types."]
},
"date": "2025-11-15",
"version": "2.0.0-next.64"
},
{
"children": {
"features": ["Show orphaned tool message and support delete tool message."]
},
"date": "2025-11-15",
"version": "2.0.0-next.63"
},
{
"children": {},
"date": "2025-11-15",
"version": "2.0.0-next.62"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-11-15",
"version": "2.0.0-next.61"
},
{
"children": {
"fixes": ["Reduce threshold."]
},
"date": "2025-11-14",
"version": "2.0.0-next.60"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-11-14",
"version": "2.0.0-next.59"
},
{
"children": {
"features": ["Support DeepSeek Interleaved thinking."]
},
"date": "2025-11-14",
"version": "2.0.0-next.58"
},
{
"children": {
"improvements": ["Revert background style."]
},
"date": "2025-11-14",
"version": "2.0.0-next.57"
},
{
"children": {
"features": ["Add folder creation UI and clean up debug code."]
},
"date": "2025-11-14",
"version": "2.0.0-next.56"
},
{
"children": {
"features": ["Create Pages in Knowledge Base."]
},
"date": "2025-11-14",
"version": "2.0.0-next.55"
},
{
"children": {
"improvements": ["Refactor and support move locale file intervention."]
},
"date": "2025-11-14",
"version": "2.0.0-next.54"
},
{
"children": {
"features": ["Add GPT-5.1 models."],
"improvements": ["Fix approving render and improve Conversation style."]
},
"date": "2025-11-14",
"version": "2.0.0-next.53"
},
{
"children": {
"fixes": ["Filter out reasoning fields from messages in ChatCompletion API."]
},
"date": "2025-11-13",
"version": "2.0.0-next.52"
},
{
"children": {
"improvements": ["Update ERNIE-5.0-Thinking-Preview model."]
},
"date": "2025-11-13",
"version": "2.0.0-next.51"
},
{
"children": {
"fixes": ["Fix oidc accountId mismatch."]
},
"date": "2025-11-13",
"version": "2.0.0-next.50"
},
{
"children": {
"features": ["Support tool invention."],
"fixes": ["Update lost i18n files."]
},
"date": "2025-11-13",
"version": "2.0.0-next.49"
},
{
"children": {},
"date": "2025-11-12",
"version": "2.0.0-next.48"
},
{
"children": {
"fixes": ["Fix mcp server return image error."]
},
"date": "2025-11-11",
"version": "2.0.0-next.47"
},
{
"children": {
"improvements": ["Fix thread display."]
},
"date": "2025-11-11",
"version": "2.0.0-next.46"
},
{
"children": {
"improvements": ["Edge to node runtime."]
},
"date": "2025-11-10",
"version": "2.0.0-next.45"
},
{
"children": {
"fixes": ["Fix reasoning issue with claude and Response API thinking."]
},
"date": "2025-11-10",
"version": "2.0.0-next.44"
},
{
"children": {
"fixes": ["Abnormal animation of tokens."]
},
"date": "2025-11-09",
"version": "2.0.0-next.43"
},
{
"children": {
"fixes": ["Fix missing messages when finish runtime."]
},
"date": "2025-11-09",
"version": "2.0.0-next.42"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-11-09",
"version": "2.0.0-next.41"
},
{
"children": {},
"date": "2025-11-08",
"version": "2.0.0-next.40"
},
{
"children": {},
"date": "2025-11-08",
"version": "2.0.0-next.39"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-11-08",
"version": "2.0.0-next.38"
},
{
"children": {
"fixes": ["Don't include runtimeProvider in JWT for non-image operations."]
},
"date": "2025-11-07",
"version": "2.0.0-next.37"
},
{
"children": {
"features": ["Refactor to use agent runtime as the generation core and support branch mode."]
},
"date": "2025-11-07",
"version": "2.0.0-next.36"
},
{
"children": {
"improvements": ["Use react-router-dom change /chat page to spa mode."]
},
"date": "2025-11-07",
"version": "2.0.0-next.35"
},
{
"children": {
"improvements": [
"Add sorting functionality for disabled models and model providers with tooltip support."
]
},
"date": "2025-11-07",
"version": "2.0.0-next.34"
},
{
"children": {
"improvements": ["Refactor message create name."],
"fixes": ["Model name display in the assistant panel disappears."]
},
"date": "2025-11-06",
"version": "2.0.0-next.33"
},
{
"children": {
"fixes": ["Should install new version after quit this instance."]
},
"date": "2025-11-05",
"version": "2.0.0-next.32"
},
{
"children": {},
"date": "2025-11-05",
"version": "2.0.0-next.31"
},
{
"children": {
"improvements": ["Enhance message router with service layer and comprehensive tests."]
},
"date": "2025-11-05",
"version": "2.0.0-next.30"
},
{
"children": {
"improvements": ["Refactor chat message model to speed up."]
},
"date": "2025-11-04",
"version": "2.0.0-next.29"
},
{
"children": {
"features": ["Support install sreamable http mcp server on web."]
},
"date": "2025-11-04",
"version": "2.0.0-next.28"
},
{
"children": {
"improvements": ["Refactor services to a more clean structure."]
},
"date": "2025-11-04",
"version": "2.0.0-next.27"
},
{
"children": {
"improvements": ["Add settings (jsonb) column to ai_models table."]
},
"date": "2025-11-04",
"version": "2.0.0-next.26"
},
{
"children": {
"features": ["Display assistant message in group."]
},
"date": "2025-11-04",
"version": "2.0.0-next.25"
},
{
"children": {
"improvements": ["Improve lab style."]
},
"date": "2025-11-04",
"version": "2.0.0-next.24"
},
{
"children": {
"fixes": ["Fix send message."]
},
"date": "2025-11-04",
"version": "2.0.0-next.23"
},
{
"children": {},
"date": "2025-11-04",
"version": "2.0.0-next.22"
},
{
"children": {
"fixes": ["Fix oidc auth timeout issue on the desktop."]
},
"date": "2025-11-04",
"version": "2.0.0-next.21"
},
{
"children": {
"improvements": ["Improve oidc layout style."]
},
"date": "2025-11-03",
"version": "2.0.0-next.20"
},
{
"children": {
"improvements": ["Remove NEXT_PUBLIC_SERVICE_MODE env and use server by default."]
},
"date": "2025-11-03",
"version": "2.0.0-next.19"
},
{
"children": {
"improvements": ["Improve built-in client OIDC user flow."]
},
"date": "2025-11-03",
"version": "2.0.0-next.18"
},
{
"children": {
"fixes": ["Fix regex ReDoS."]
},
"date": "2025-11-03",
"version": "2.0.0-next.17"
},
{
"children": {
"improvements": ["Remove deperated code."]
},
"date": "2025-11-03",
"version": "2.0.0-next.16"
},
{
"children": {},
"date": "2025-11-03",
"version": "2.0.0-next.15"
},
{
"children": {
"improvements": ["Remove client service."]
},
"date": "2025-11-02",
"version": "2.0.0-next.14"
},
{
"children": {
"fixes": ["Fix image prompt form."]
},
"date": "2025-11-02",
"version": "2.0.0-next.13"
},
{
"children": {
"improvements": ["Add padding to TopicList component."]
},
"date": "2025-11-02",
"version": "2.0.0-next.12"
},
{
"children": {
"improvements": ["Smoothed model descriptions in ko-KR locales."]
},
"date": "2025-11-02",
"version": "2.0.0-next.11"
},
{
"children": {},
"date": "2025-11-02",
"version": "2.0.0-next.10"
},
{
"children": {
"improvements": ["Remove dalle builtin plugin."]
},
"date": "2025-11-02",
"version": "2.0.0-next.9"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-11-02",
"version": "2.0.0-next.8"
},
{
"children": {
"features": ["Upgrade to Next 16."]
},
"date": "2025-11-01",
"version": "2.0.0-next.7"
},
{
"children": {},
"date": "2025-10-31",
"version": "2.0.0-next.6"
},
{
"children": {
"improvements": ["Migrating Firecrawl to v2."]
},
"date": "2025-10-31",
"version": "2.0.0-next.5"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-10-31",
"version": "2.0.0-next.4"
},
{
"children": {
"improvements": [
"Add new bedrock model support, add pricing info for Azure GPT-5 series models."
]
},
"date": "2025-10-30",
"version": "2.0.0-next.3"
},
{
"children": {
"fixes": [
"Hide marketplace link from Plugin List when market disabled, OIDC error when connecting to self-host instance, only include input_fidelity parameter for gpt-image-1.."
]
},
"date": "2025-10-30",
"version": "2.0.0-next.2"
},
{
"children": {
"features": ["2.0 next baseline."],
"improvements": ["starting V2"]
},
"date": "2025-10-30",
"version": "2.0.0-next.1"
},
{
"children": {
"features": ["2.0 next init."]
},
"date": "2025-10-30",
"version": "1.143.0-next.2"
},
{
"children": {
"features": ["Try 2.0 next."],
"improvements": ["starting V2"]
},
"date": "2025-10-30",
"version": "1.143.0-next.1"
},
{
"children": {},
"date": "2025-10-30",
"version": "1.142.8"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-10-28",
"version": "1.142.7"
},
{
"children": {},
"date": "2025-10-28",
"version": "1.142.6"
},
{
"children": {
"improvements": ["Add MiniMax-M2 model."]
},
"date": "2025-10-27",
"version": "1.142.5"
},
{
"children": {
"improvements": [
"Pre render ModelSwitchPanel, The error details of the connectivity check lead to a layout problem."
]
},
"date": "2025-10-27",
"version": "1.142.4"
},
{
"children": {
"improvements": [
"Adjust modal setting form styles for improved layout and responsiveness, Unzip file when uploading in knowledge base [LOB-500]."
]
},
"date": "2025-10-27",
"version": "1.142.3"
},
{
"children": {
"improvements": ["Improve provider modal height when creating custom provider."]
},
"date": "2025-10-26",
"version": "1.142.2"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-10-26",
"version": "1.142.1"
},
{
"children": {
"features": ["Use env to control clerk allow origin feature."]
},
"date": "2025-10-24",
"version": "1.142.0"
},
{
"children": {
"fixes": ["Loadmore not work & navbar not show in pwa."]
},
"date": "2025-10-23",
"version": "1.141.10"
},
{
"children": {
"improvements": ["Improve local system tools render."]
},
"date": "2025-10-23",
"version": "1.141.9"
},
{
"children": {
"improvements": ["Improvement for Agent Team After Alpha Launch [LOB-517]."]
},
"date": "2025-10-23",
"version": "1.141.8"
},
{
"children": {
"improvements": ["Allow removal of top_p and similar request parameters."]
},
"date": "2025-10-23",
"version": "1.141.7"
},
{
"children": {},
"date": "2025-10-22",
"version": "1.141.6"
},
{
"children": {
"improvements": ["Change discover page from RSC to SPA to improve performance."]
},
"date": "2025-10-22",
"version": "1.141.5"
},
{
"children": {
"improvements": ["Fix model runtime cost calculate with CNY."]
},
"date": "2025-10-22",
"version": "1.141.4"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-10-22",
"version": "1.141.3"
},
{
"children": {},
"date": "2025-10-21",
"version": "1.141.2"
},
{
"children": {
"improvements": ["Refactor context engine."]
},
"date": "2025-10-21",
"version": "1.141.1"
},
{
"children": {
"features": ["Add PDF export functionality to share modal."],
"fixes": [
"Ignore abort signal errors in TRPC client, slove when pwa user info have code cannot be viewed in full."
],
"improvements": [
"Add knowledge base mansory layout [LOB-496], improve rich text link display."
]
},
"date": "2025-10-21",
"version": "1.141.0"
},
{
"children": {
"features": ["Add ComfyUI integration Phase1(RFC-128)."]
},
"date": "2025-10-21",
"version": "1.140.0"
},
{
"children": {},
"date": "2025-10-21",
"version": "1.139.5"
},
{
"children": {
"fixes": ["Pass threadId to messages in sendMessageInServer."]
},
"date": "2025-10-21",
"version": "1.139.4"
},
{
"children": {
"improvements": ["Show message author in minimap."]
},
"date": "2025-10-21",
"version": "1.139.3"
},
{
"children": {
"improvements": ["Solve when desktop the sider agent list too long."]
},
"date": "2025-10-20",
"version": "1.139.2"
},
{
"children": {
"improvements": ["Update i18n."]
},
"date": "2025-10-20",
"version": "1.139.1"
},
{
"children": {
"features": ["Support image generation for siliconcloud."]
},
"date": "2025-10-19",
"version": "1.139.0"
},
{
"children": {
"improvements": ["Refactor upload router into lambda and decide to remove it in V2."]
},
"date": "2025-10-18",
"version": "1.138.5"
},
{
"children": {
"fixes": ["Fix response API tools calling issue."]
},
"date": "2025-10-18",
"version": "1.138.4"
},
{
"children": {
"fixes": ["Fix topic fetch not correct in custom agent."]
},
"date": "2025-10-18",
"version": "1.138.3"
},
{
"children": {
"improvements": ["Improve welcome message."]
},
"date": "2025-10-16",
"version": "1.138.2"
},
{
"children": {
"fixes": ["Automatic topic creation switch does not work."]
},
"date": "2025-10-16",
"version": "1.138.1"
},
{
"children": {
"features": ["Support Group Chat, Mention, and Multi-Agent Orchestration with feature flag."]
},
"date": "2025-10-16",
"version": "1.138.0"
},
{
"children": {
"improvements": ["Add Claude Haiku 4.5 model."]
},
"date": "2025-10-16",
"version": "1.137.10"
},
{
"children": {
"improvements": ["Improve update notification."]
},
"date": "2025-10-15",
"version": "1.137.9"
},
{
"children": {
"fixes": ["Fix duplicate tools id issue and fix link dialog issue."],
"improvements": ["Add region support for Vertex AI provider."]
},
"date": "2025-10-15",
"version": "1.137.8"
},
{
"children": {
"improvements": ["Use different favicon.ico in dev mode."]
},
"date": "2025-10-15",
"version": "1.137.7"
},
{
"children": {
"fixes": ["Update Claude workflows to use oauth token, vertext ai create image."]
},
"date": "2025-10-14",
"version": "1.137.6"
},
{
"children": {
"improvements": ["Add imagen model to vertex ai."]
},
"date": "2025-10-14",
"version": "1.137.5"
},
{
"children": {
"fixes": ["Prevent Vertex AI JSON credentials from being split by comma."]
},
"date": "2025-10-14",
"version": "1.137.4"
},
{
"children": {
"fixes": [
"Fix mcp server connect issue and refactor web search implement, fix tools calling long name length >64 issue."
]
},
"date": "2025-10-14",
"version": "1.137.3"
},
{
"children": {
"fixes": ["Fix the Worker URL cross-origin issue."]
},
"date": "2025-10-14",
"version": "1.137.2"
},
{
"children": {
"improvements": ["Change the user chatItem maxWidth should use flex 1."]
},
"date": "2025-10-14",
"version": "1.137.1"
},
{
"children": {
"features": ["Add new setting for default image num."]
},
"date": "2025-10-12",
"version": "1.137.0"
},
{
"children": {
"fixes": ["Fix input cannot send markdown."],
+3
View File
@@ -17,6 +17,9 @@ LOBE_PORT=3210
CASDOOR_PORT=8000
MINIO_PORT=9000
APP_URL=http://localhost:3210
# INTERNAL_APP_URL is optional, used for server-to-server calls
# to bypass CDN/proxy. If not set, defaults to APP_URL.
# Example: INTERNAL_APP_URL=http://localhost:3210
AUTH_URL=http://localhost:3210/api/auth
# Postgres related, which are the necessary environment variables for DB
+1 -1
View File
@@ -230,7 +230,7 @@ services:
otel-tracing-test:
profiles:
- otel-test
image: ghcr.io/grafana/xk6-client-tracing:v0.0.7
image: ghcr.io/grafana/xk6-client-tracing:v0.0.9
container_name: lobe-otel-tracing-test
network_mode: 'service:network-service'
restart: always
@@ -151,7 +151,7 @@ services:
otel-tracing-test:
profiles:
- otel-test
image: ghcr.io/grafana/xk6-client-tracing:v0.0.7
image: ghcr.io/grafana/xk6-client-tracing:v0.0.9
container_name: lobe-otel-tracing-test
network_mode: 'service:network-service'
restart: always
@@ -149,7 +149,7 @@ services:
otel-tracing-test:
profiles:
- otel-test
image: ghcr.io/grafana/xk6-client-tracing:v0.0.7
image: ghcr.io/grafana/xk6-client-tracing:v0.0.9
container_name: lobe-otel-tracing-test
network_mode: 'service:network-service'
restart: always
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,998 @@
---
title: ComfyUI 扩展开发指南
description: 学习如何为 LobeChat ComfyUI 集成添加新模型、工作流和功能扩展
tags:
- ComfyUI
- 开发指南
- 模型扩展
- 工作流开发
---
# ComfyUI 扩展开发指南
本指南基于实际代码实现,帮助开发者扩展 LobeChat 的 ComfyUI 集成功能。
## 架构概览
LobeChat ComfyUI 集成采用四层服务架构,围绕 `LobeComfyUI` 主类构建:
```plaintext
packages/model-runtime/src/providers/comfyui/
├── index.ts # LobeComfyUI 主类入口
├── services/ # 四大核心服务
│ ├── comfyuiClient.ts # ComfyUIClientService - 客户端和认证
│ ├── modelResolver.ts # ModelResolverService - 模型解析
│ ├── workflowBuilder.ts # WorkflowBuilderService - 工作流构建
│ └── imageService.ts # ImageService - 图像生成
├── config/ # 配置系统
│ ├── modelRegistry.ts # 主模型注册表(222个模型)
│ ├── fluxModelRegistry.ts # 130个FLUX模型配置
│ ├── sdModelRegistry.ts # 92个SD系列模型配置
│ ├── systemComponents.ts # VAE/CLIP/T5/LoRA/ControlNet组件
│ └── workflowRegistry.ts # 工作流路由配置
├── workflows/ # 工作流实现
│ ├── flux-dev.ts # FLUX Dev 20步工作流
│ ├── flux-schnell.ts # FLUX Schnell 4步快速工作流
│ ├── flux-kontext.ts # FLUX Kontext 填充工作流
│ ├── sd35.ts # SD3.5 外部编码器工作流
│ ├── simple-sd.ts # 通用SD工作流
│ └── index.ts # 工作流导出
├── utils/ # 工具层
│ ├── staticModelLookup.ts # 模型查找函数
│ ├── workflowDetector.ts # 模型架构检测
│ ├── promptSplitter.ts # FLUX双提示词分割
│ ├── seedGenerator.ts # 随机种子生成
│ ├── cacheManager.ts # TTL缓存管理
│ └── workflowUtils.ts # 工作流工具函数
└── errors/ # 错误处理
├── base.ts # 基础错误类
├── modelResolverError.ts # 模型解析错误
├── workflowError.ts # 工作流错误
└── servicesError.ts # 服务错误
src/server/services/comfyui/ # 服务端实现
├── core/ # 核心服务器服务
│ ├── comfyUIAuthService.ts # 认证服务
│ ├── comfyUIClientService.ts # 客户端服务
│ ├── comfyUIConnectionService.ts # 连接服务
│ ├── errorHandlerService.ts # 错误处理服务
│ ├── imageService.ts # 图像生成服务
│ ├── modelResolverService.ts # 模型解析服务
│ └── workflowBuilderService.ts # 工作流构建服务
├── config/ # 服务器端配置
│ ├── constants.ts # 常量和默认值
│ ├── modelRegistry.ts # 模型注册表
│ ├── fluxModelRegistry.ts # FLUX模型
│ ├── sdModelRegistry.ts # SD模型
│ ├── systemComponents.ts # 系统组件
│ └── workflowRegistry.ts # 工作流注册表
├── workflows/ # 服务端工作流实现
│ ├── flux-dev.ts # FLUX Dev 工作流
│ ├── flux-schnell.ts # FLUX Schnell 工作流
│ ├── flux-kontext.ts # FLUX Kontext 工作流
│ ├── sd35.ts # SD3.5 工作流
│ └── simple-sd.ts # Simple SD 工作流
├── utils/ # 服务器工具
│ ├── cacheManager.ts # 缓存管理
│ ├── componentInfo.ts # 组件信息
│ ├── imageResizer.ts # 图像调整
│ ├── promptSplitter.ts # 提示词分割
│ ├── staticModelLookup.ts # 模型查找
│ ├── weightDType.ts # 权重数据类型工具
│ ├── workflowDetector.ts # 工作流检测
│ └── workflowUtils.ts # 工作流工具
└── errors/ # 服务器错误处理
├── base.ts # 基础错误类
├── configError.ts # 配置错误
├── modelResolverError.ts # 模型解析器错误
├── servicesError.ts # 服务错误
├── utilsError.ts # 工具错误
└── workflowError.ts # 工作流错误
packages/model-runtime/src/utils/ # 共享工具
└── comfyuiErrorParser.ts # 客户端/服务器统一错误解析器
```
### 核心服务架构
`LobeComfyUI` 主类初始化四个核心服务:
```typescript
// packages/model-runtime/src/providers/comfyui/index.ts
export class LobeComfyUI implements LobeRuntimeAI, AuthenticatedImageRuntime {
constructor(options: ComfyUIKeyVault = {}) {
// 1. 客户端服务 - 处理认证和API调用
this.clientService = new ComfyUIClientService(options);
// 2. 模型解析服务 - 模型查找和组件选择
const modelResolverService = new ModelResolverService(this.clientService);
// 3. 工作流构建服务 - 路由和构建工作流
const workflowBuilderService = new WorkflowBuilderService({
clientService: this.clientService,
modelResolverService: modelResolverService,
});
// 4. 图像服务 - 统一的图像生成入口
this.imageService = new ImageService(
this.clientService,
modelResolverService,
workflowBuilderService,
);
}
}
```
## 认证系统
ComfyUI 集成支持四种认证方式,由 `ComfyUIClientService` 内的 `AuthManager` 处理:
### 支持的认证类型
```typescript
interface ComfyUIKeyVault {
baseURL: string;
authType?: 'none' | 'basic' | 'bearer' | 'custom';
// Basic Auth
username?: string;
password?: string;
// Bearer Token
apiKey?: string;
// Custom Headers
customHeaders?: Record<string, string>;
}
```
### 认证配置示例
```typescript
// 无认证
const comfyUI = new LobeComfyUI({
baseURL: 'http://localhost:8000',
authType: 'none'
});
// 基础认证
const comfyUI = new LobeComfyUI({
baseURL: 'https://your-comfyui-server.com',
authType: 'basic',
username: 'your-username',
password: 'your-password'
});
// Bearer Token
const comfyUI = new LobeComfyUI({
baseURL: 'https://your-comfyui-server.com',
authType: 'bearer',
apiKey: 'your-api-key'
});
// 自定义头部
const comfyUI = new LobeComfyUI({
baseURL: 'https://your-comfyui-server.com',
authType: 'custom',
customHeaders: {
'X-API-Key': 'your-custom-key',
'Authorization': 'Custom your-token'
}
});
```
## WebAPI 路由
ComfyUI 提供了用于图像生成的 REST WebAPI 路由,支持常规认证和内部服务认证:
### 路由详情
```typescript
// src/app/(backend)/webapi/create-image/comfyui/route.ts
export const runtime = 'nodejs';
export const maxDuration = 300; // 最长5分钟
// POST /api/create-image/comfyui
{
model: string; // 模型标识符
params: { // 生成参数
prompt: string;
width?: number;
height?: number;
// ... 其他参数
};
options?: { // 可选生成选项
// ... 额外选项
};
}
```
### 认证中间件
WebAPI 路由使用 `checkAuth` 中间件进行认证:
```typescript
import { checkAuth } from '@/app/(backend)/middleware/auth';
// 路由自动验证 JWT 令牌
// 并将认证上下文传递给 tRPC 调用器
```
### 错误处理
WebAPI 路由提供结构化的错误响应:
```typescript
// 从 TRPCError 的 cause 中提取 AgentRuntimeError
if (agentError && 'errorType' in agentError) {
// 将 errorType 转换为适当的 HTTP 状态码
// 401 对应 InvalidProviderAPIKey
// 403 对应 PermissionDenied
// 404 对应 NotFound
// 500+ 对应服务器错误
}
```
## 添加新模型
### 1. 理解模型注册表结构
模型配置存储在配置文件中:
```typescript
// packages/model-runtime/src/providers/comfyui/config/modelRegistry.ts
export interface ModelConfig {
modelFamily: 'FLUX' | 'SD1' | 'SDXL' | 'SD3';
priority: number; // 1=官方, 2=企业, 3=社区
recommendedDtype?: 'default' | 'fp8_e4m3fn' | 'fp8_e5m2';
variant: string; // 模型变体标识符
}
```
### 2. 添加 FLUX 模型
在 `fluxModelRegistry.ts` 中添加新模型:
```typescript
// packages/model-runtime/src/providers/comfyui/config/fluxModelRegistry.ts
export const FLUX_MODEL_REGISTRY: Record<string, ModelConfig> = {
// 现有模型...
// 添加新的FLUX Dev模型
'your-custom-flux-dev.safetensors': {
modelFamily: 'FLUX',
priority: 2, // 企业级模型
variant: 'dev',
recommendedDtype: 'default',
},
// 添加量化版本
'your-custom-flux-dev-fp8.safetensors': {
modelFamily: 'FLUX',
priority: 2,
variant: 'dev',
recommendedDtype: 'fp8_e4m3fn',
},
};
```
### 3. 添加 SD 系列模型
在 `sdModelRegistry.ts` 中添加:
```typescript
// packages/model-runtime/src/providers/comfyui/config/sdModelRegistry.ts
export const SD_MODEL_REGISTRY: Record<string, ModelConfig> = {
// 现有模型...
// 添加新的SD3.5模型
'your-custom-sd35.safetensors': {
modelFamily: 'SD3',
priority: 2,
variant: 'sd35',
recommendedDtype: 'default',
},
};
```
### 4. 更新模型 ID 映射(可选)
如果需要为前端提供友好的模型 ID,在 `modelRegistry.ts` 中添加映射:
```typescript
// packages/model-runtime/src/providers/comfyui/config/modelRegistry.ts
export const MODEL_ID_VARIANT_MAP: Record<string, string> = {
// 现有映射...
// 添加新模型的友好ID
'my-custom-flux': 'dev', // 映射到dev变体
'my-custom-sd35': 'sd35', // 映射到sd35变体
};
```
## 创建新工作流
### 工作流创建原理
**重要:工作流节点结构来自 ComfyUI 原生导出**
1. 在 ComfyUI 界面中设计工作流
2. 使用 "Export (API Format)" 导出 JSON
3. 将 JSON 结构复制到 TypeScript 文件
4. 使用`PromptBuilder`包装并参数化
### 1. 从 ComfyUI 导出工作流
在 ComfyUI 界面中:
1. 拖拽节点构建所需工作流
2. 连接各节点的输入输出
3. 右键点击空白处 → "Export (API Format)"
4. 复制生成的 JSON 结构
### 2. 工作流文件模板
创建新文件 `workflows/your-workflow.ts`
```typescript
import { PromptBuilder } from '@saintno/comfyui-sdk';
import type { WorkflowContext } from '../services/workflowBuilder';
import { generateUniqueSeeds } from '../utils/seedGenerator';
import { getWorkflowFilenamePrefix } from '../utils/workflowUtils';
/**
* 构建自定义工作流
* @param modelFileName - 模型文件名
* @param params - 生成参数
* @param context - 工作流上下文
*/
export async function buildYourCustomWorkflow(
modelFileName: string,
params: Record<string, any>,
context: WorkflowContext,
): Promise<PromptBuilder<any, any, any>> {
// 从ComfyUI "Export (API Format)" 获得的JSON结构
const workflow = {
'1': {
_meta: { title: 'Load Checkpoint' },
class_type: 'CheckpointLoaderSimple',
inputs: {
ckpt_name: modelFileName,
},
},
'2': {
_meta: { title: 'CLIP Text Encode' },
class_type: 'CLIPTextEncode',
inputs: {
clip: ['1', 1], // 连接到节点1的CLIP输出
text: params.prompt,
},
},
'3': {
_meta: { title: 'Empty Latent' },
class_type: 'EmptyLatentImage',
inputs: {
width: params.width,
height: params.height,
batch_size: 1,
},
},
'4': {
_meta: { title: 'KSampler' },
class_type: 'KSampler',
inputs: {
model: ['1', 0], // 连接到节点1的MODEL输出
positive: ['2', 0], // 连接到节点2的CONDITIONING输出
negative: ['2', 0], // 可以配置负面提示词
latent_image: ['3', 0],
seed: params.seed ?? generateUniqueSeeds(1)[0],
steps: params.steps,
cfg: params.cfg,
sampler_name: 'euler',
scheduler: 'normal',
denoise: 1.0,
},
},
'5': {
_meta: { title: 'VAE Decode' },
class_type: 'VAEDecode',
inputs: {
samples: ['4', 0],
vae: ['1', 2], // 连接到节点1的VAE输出
},
},
'6': {
_meta: { title: 'Save Image' },
class_type: 'SaveImage',
inputs: {
filename_prefix: getWorkflowFilenamePrefix('buildYourCustomWorkflow', context.variant),
images: ['5', 0],
},
},
};
// 使用PromptBuilder包装静态JSON
const builder = new PromptBuilder(
workflow,
['width', 'height', 'steps', 'cfg', 'seed'], // 输入参数
['images'], // 输出参数
);
// 设置输出节点
builder.setOutputNode('images', '6');
// 设置输入节点路径
builder.setInputNode('width', '3.inputs.width');
builder.setInputNode('height', '3.inputs.height');
builder.setInputNode('steps', '4.inputs.steps');
builder.setInputNode('cfg', '4.inputs.cfg');
builder.setInputNode('seed', '4.inputs.seed');
// 设置参数值
builder
.input('width', params.width)
.input('height', params.height)
.input('steps', params.steps)
.input('cfg', params.cfg)
.input('seed', params.seed ?? generateUniqueSeeds(1)[0]);
return builder;
}
```
### 3. 注册新工作流
在 `workflowRegistry.ts` 中添加工作流映射:
```typescript
// packages/model-runtime/src/providers/comfyui/config/workflowRegistry.ts
import { buildYourCustomWorkflow } from '../workflows/your-workflow';
export const VARIANT_WORKFLOW_MAP: Record<string, WorkflowBuilder> = {
// 现有映射...
// 添加新工作流
'your-variant': buildYourCustomWorkflow,
};
```
### 4. 实际工作流示例
参考 `flux-dev.ts` 的真实实现:
```typescript
// packages/model-runtime/src/providers/comfyui/workflows/flux-dev.ts (简化版)
export async function buildFluxDevWorkflow(
modelFileName: string,
params: Record<string, any>,
context: WorkflowContext,
): Promise<PromptBuilder<any, any, any>> {
// 获取所需组件
const selectedT5Model = await context.modelResolverService.getOptimalComponent('t5', 'FLUX');
const selectedVAE = await context.modelResolverService.getOptimalComponent('vae', 'FLUX');
const selectedCLIP = await context.modelResolverService.getOptimalComponent('clip', 'FLUX');
// 处理双提示词分割
const { t5xxlPrompt, clipLPrompt } = splitPromptForDualCLIP(params.prompt);
// 静态工作流定义(来自ComfyUI导出)
const workflow = {
'1': {
class_type: 'DualCLIPLoader',
inputs: {
clip_name1: selectedT5Model,
clip_name2: selectedCLIP,
type: 'flux',
},
},
// ... 更多节点
};
// 参数注入(必须在workflow文件内完成)
workflow['5'].inputs.clip_l = clipLPrompt;
workflow['5'].inputs.t5xxl = t5xxlPrompt;
workflow['4'].inputs.width = params.width;
workflow['4'].inputs.height = params.height;
// 创建并配置PromptBuilder
const builder = new PromptBuilder(workflow, inputs, outputs);
// 配置输入输出映射...
return builder;
}
```
## 系统组件管理
### 组件配置结构
所有系统组件(VAE、CLIP、T5、LoRA、ControlNet)统一配置在 `systemComponents.ts`
```typescript
// packages/model-runtime/src/providers/comfyui/config/systemComponents.ts
export interface ComponentConfig {
modelFamily: string; // 模型家族
priority: number; // 1=必需, 2=标准, 3=可选
type: string; // 组件类型
compatibleVariants?: string[]; // 兼容变体(LoRA/ControlNet
controlnetType?: string; // ControlNet类型
}
export const SYSTEM_COMPONENTS: Record<string, ComponentConfig> = {
// VAE组件
'ae.safetensors': {
modelFamily: 'FLUX',
priority: 1,
type: 'vae',
},
// CLIP组件
'clip_l.safetensors': {
modelFamily: 'FLUX',
priority: 1,
type: 'clip',
},
// T5编码器
't5xxl_fp16.safetensors': {
modelFamily: 'FLUX',
priority: 1,
type: 't5',
},
// LoRA适配器
'realism_lora.safetensors': {
compatibleVariants: ['dev'],
modelFamily: 'FLUX',
priority: 1,
type: 'lora',
},
// ControlNet模型
'flux-controlnet-canny-v3.safetensors': {
compatibleVariants: ['dev'],
controlnetType: 'canny',
modelFamily: 'FLUX',
priority: 1,
type: 'controlnet',
},
};
```
### 添加新组件
```typescript
// 添加新的LoRA
'your-custom-lora.safetensors': {
compatibleVariants: ['dev', 'schnell'],
modelFamily: 'FLUX',
priority: 2,
type: 'lora',
},
// 添加新的ControlNet
'your-controlnet-pose.safetensors': {
compatibleVariants: ['dev'],
controlnetType: 'pose',
modelFamily: 'FLUX',
priority: 2,
type: 'controlnet',
},
```
### 组件查询 API
```typescript
import { getAllComponentsWithNames, getOptimalComponent } from '../config/systemComponents';
// 获取最优组件
const bestVAE = getOptimalComponent('vae', 'FLUX');
const bestT5 = getOptimalComponent('t5', 'FLUX');
// 查询特定类型的组件
const availableLoras = getAllComponentsWithNames({
type: 'lora',
modelFamily: 'FLUX',
compatibleVariant: 'dev'
});
// 查询ControlNet
const cannyControlNets = getAllComponentsWithNames({
type: 'controlnet',
controlnetType: 'canny',
modelFamily: 'FLUX'
});
```
## 模型解析和查找
### ModelResolverService 工作原理
```typescript
// packages/model-runtime/src/providers/comfyui/services/modelResolver.ts
export class ModelResolverService {
async resolveModelFileName(modelId: string): Promise<string | undefined> {
// 1. 清理模型ID
const cleanId = modelId.replace(/^comfyui\//, '');
// 2. 检查模型ID映射
const mappedVariant = MODEL_ID_VARIANT_MAP[cleanId];
if (mappedVariant) {
const prioritizedModels = getModelsByVariant(mappedVariant);
const serverModels = await this.getAvailableModelFiles();
// 按优先级查找第一个可用模型
for (const filename of prioritizedModels) {
if (serverModels.includes(filename)) {
return filename;
}
}
}
// 3. 直接注册表查找
if (MODEL_REGISTRY[cleanId]) {
return cleanId;
}
// 4. 检查服务器文件存在性
if (isModelFile(cleanId)) {
const serverModels = await this.getAvailableModelFiles();
if (serverModels.includes(cleanId)) {
return cleanId;
}
}
return undefined;
}
}
```
### 模型查找示例
```typescript
// 实际使用示例
const resolver = new ModelResolverService(clientService);
// 友好ID查找
const fluxDevFile = await resolver.resolveModelFileName('flux-dev');
// 返回: 'flux1-dev.safetensors' (如果存在)
// 直接文件名查找
const directFile = await resolver.resolveModelFileName('my-custom-model.safetensors');
// 返回: 'my-custom-model.safetensors' (如果存在)
// 变体查找
const devModels = getModelsByVariant('dev');
console.log(devModels.slice(0, 3));
// 输出: ['flux1-dev.safetensors', 'flux1-dev-fp8.safetensors', ...]
```
## 错误处理
### 错误类型层次
```plaintext
// packages/model-runtime/src/providers/comfyui/errors/
ComfyUIInternalError // 基础错误
├── ModelResolverError // 模型解析错误
├── WorkflowError // 工作流错误
├── ServicesError // 服务错误
└── UtilsError // 工具错误
```
### 错误处理示例
```typescript
import { ModelResolverError, WorkflowError } from '../errors';
try {
const result = await comfyUI.createImage({
model: 'nonexistent-model',
params: { prompt: '测试' }
});
} catch (error) {
if (error instanceof ModelResolverError) {
console.log('模型解析失败:', error.message);
console.log('错误原因:', error.reason);
console.log('错误详情:', error.details);
} else if (error instanceof WorkflowError) {
console.log('工作流错误:', error.message);
}
}
```
### 统一错误解析器
客户端和服务器端错误处理可使用共享的错误解析器:
```typescript
// packages/model-runtime/src/utils/comfyuiErrorParser.ts
import { parseComfyUIErrorMessage, cleanComfyUIErrorMessage } from '../utils/comfyuiErrorParser';
// 解析错误消息并确定错误类型
const { error, errorType } = parseComfyUIErrorMessage(rawError);
// 清理 ComfyUI 格式的错误消息
const cleanMessage = cleanComfyUIErrorMessage(errorMessage);
```
错误解析器处理:
- HTTP 状态码映射到错误类型
- 服务器端错误增强
- 模型文件缺失检测
- 网络错误识别
- 工作流验证错误
## 测试架构与开发
### 测试架构概述
ComfyUI 集成使用了统一的测试架构,确保测试的可维护性和定制友好性。该架构包括:
- **统一 Mock 系统**:集中管理所有外部依赖的模拟
- **参数化测试**:自动适应新模型,无需修改现有测试
- **夹具系统**:从配置文件中获取测试数据,确保准确性
- **覆盖率目标**ComfyUI 模块维持 97%+ 覆盖率
### 测试文件结构
```plaintext
packages/model-runtime/src/providers/comfyui/__tests__/
├── setup/
│ └── unifiedMocks.ts # 统一Mock配置
├── fixtures/
│ ├── parameters.fixture.ts # 参数测试夹具
│ └── workflow.fixture.ts # 工作流测试夹具
├── integration/
│ ├── parameterMapping.test.ts # 参数映射集成测试
│ └── workflowBuilder.test.ts # 工作流构建测试
├── services/ # 各服务单元测试
└── workflows/ # 工作流单元测试
```
### 添加新模型测试
当添加新模型时,测试会自动识别并运行相应的参数映射测试。你只需要:
#### 1. 在模型配置中添加参数架构
```typescript
// packages/model-bank/src/aiModels/comfyui.ts
export const myNewModelParamsSchema = {
prompt: { type: 'string', required: true },
steps: { type: 'number', default: 20, min: 1, max: 150 },
cfg: { type: 'number', default: 7.0, min: 1.0, max: 30.0 }
};
```
#### 2. 创建工作流构建器
```typescript
// packages/model-runtime/src/providers/comfyui/workflows/myNewModel.ts
export async function buildMyNewModelWorkflow(
modelName: string,
params: MyNewModelParams,
context: ComfyUIContext
) {
const workflow = { /* 工作流定义 */ };
// 参数注入
workflow['1'].inputs.prompt = params.prompt;
workflow['2'].inputs.steps = params.steps;
return workflow;
}
```
#### 3. 在夹具中注册模型
```typescript
// packages/model-runtime/src/providers/comfyui/__tests__/fixtures/parameters.fixture.ts
import {
myNewModelParamsSchema,
// ... 其他架构
} from '../../../../../model-bank/src/aiModels/comfyui';
export const parametersFixture = {
models: {
'my-new-model': {
schema: myNewModelParamsSchema,
defaults: {
steps: myNewModelParamsSchema.steps.default,
cfg: myNewModelParamsSchema.cfg.default,
},
boundaries: {
min: { steps: myNewModelParamsSchema.steps.min },
max: { steps: myNewModelParamsSchema.steps.max }
}
}
}
};
```
### 测试最佳实践
#### 使用统一 Mock 系统
```typescript
import { setupAllMocks } from '../setup/unifiedMocks';
describe('MyTest', () => {
const mocks = setupAllMocks();
beforeEach(() => {
vi.clearAllMocks();
});
});
```
#### 编写参数映射测试
参数映射测试会自动运行,验证前端参数正确注入到工作流中:
```typescript
// 测试会自动包含新注册的模型
describe.each(
Object.entries(models).filter(([name]) => workflowBuilders[name])
)(
'%s parameter mapping',
(modelName, modelConfig) => {
it('should map schema parameters to workflow', async () => {
const params = {
prompt: 'test prompt',
...modelConfig.defaults,
};
const workflow = await builder(`${modelName}.safetensors`, params, mockContext);
expect(workflow).toBeDefined();
});
}
);
```
#### 定制友好的测试原则
- **不测试工作流结构**:工作流是 ComfyUI 官方格式,只测试参数映射
- **使用配置驱动的数据**:测试数据来自模型配置文件,确保一致性
- **避免脆性断言**:不检查具体的节点 ID 或内部结构
- **支持扩展**:新增模型应该只影响覆盖率,不破坏现有测试
### 运行测试
```bash
# 运行 ComfyUI 相关测试
cd packages/model-runtime
bunx vitest run --silent='passed-only' 'src/comfyui'
# 查看覆盖率
bunx vitest run --coverage 'src/comfyui'
# 运行特定测试文件
bunx vitest run 'src/comfyui/__tests__/integration/parameterMapping.test.ts'
```
### 覆盖率目标
- **整体覆盖率**ComfyUI 模块维持 97%+ 覆盖率
- **核心功能**:100% 分支覆盖率
- **新增功能**:保持或提升现有覆盖率水平
## 开发和测试
### 1. 本地开发设置
```bash
# 启动ComfyUI调试模式
DEBUG=lobe-image:* pnpm dev
```
### 2. 测试新功能
```typescript
// 创建测试文件
import { buildYourCustomWorkflow } from './your-workflow';
describe('Custom Workflow', () => {
test('should build workflow correctly', async () => {
const mockContext = {
clientService: mockClientService,
modelResolverService: mockModelResolver,
};
const workflow = await buildYourCustomWorkflow(
'test-model.safetensors',
{ prompt: '测试', width: 512, height: 512 },
mockContext
);
expect(workflow).toBeDefined();
// 验证工作流结构...
});
});
```
### 3. 模型配置测试
```typescript
import { getModelConfig, getAllModelNames } from '../config/modelRegistry';
describe('Model Registry', () => {
test('should find new model', () => {
const config = getModelConfig('your-new-model.safetensors');
expect(config).toBeDefined();
expect(config?.variant).toBe('dev');
expect(config?.modelFamily).toBe('FLUX');
});
});
```
## 完整使用示例
### 基础图像生成
```typescript
import { LobeComfyUI } from '@/libs/model-runtime/comfyui';
const comfyUI = new LobeComfyUI({
baseURL: 'http://localhost:8000',
authType: 'none'
});
// FLUX Dev模型生成
const result = await comfyUI.createImage({
model: 'flux-dev',
params: {
prompt: '美丽的风景画,高质量,详细',
width: 1024,
height: 1024,
steps: 20,
cfg: 3.5,
seed: -1
}
});
console.log('生成图像URL:', result.imageUrl);
```
### SD3.5 模型使用
```typescript
// SD3.5会自动检测可用编码器
const sd35Result = await comfyUI.createImage({
model: 'stable-diffusion-35',
params: {
prompt: '未来主义城市景观',
width: 1344,
height: 768,
steps: 28,
cfg: 4.5
}
});
```
### 企业优化模型
```typescript
// 系统会自动选择最佳可用变体(如FP8量化版本)
const optimizedResult = await comfyUI.createImage({
model: 'flux-dev',
params: {
prompt: '专业商务肖像',
width: 768,
height: 1024,
steps: 15 // FP8模型可以用更少步数
}
});
```
## 注意事项
- 确保 ComfyUI 服务正常运行并可访问
- 检查所有必需的模型文件是否已正确安装
- 注意模型文件的命名规范和路径配置
- 定期检查和更新工作流配置以支持新功能
- 注意不同模型系列的参数差异和兼容性
- 添加新模型时,请遵循测试架构指南确保测试完整性
- 在提交代码前务必运行相关测试确保覆盖率达标
通过遵循这些指南,开发者可以有效地在 LobeChat 中使用和扩展 ComfyUI 功能,为用户提供强大的图像生成和处理能力。
@@ -11,13 +11,13 @@ But here is the easier approach that can reduce your pain.
### Environment Configuration
First, copy the example environment file to create your development configuration:
First, copy the example environment file to create your Docker Compose configuration:
```bash
cp .env.example.development .env.development
cp docker-compose/local/.env.example docker-compose/local/.env
```
This file contains all necessary environment variables for server-side database mode and configures:
Edit `docker-compose/local/.env` as needed for your development setup. This file contains all necessary environment variables for the Docker services and configures:
- **Service Mode**: `NEXT_PUBLIC_SERVICE_MODE=server`
- **Database**: PostgreSQL with connection string
@@ -72,7 +72,7 @@ When working with image generation features (text-to-image, image-to-image), the
### Image Generation Configuration
The existing Docker Compose configuration already includes MinIO storage service and all necessary environment variables in `.env.example.development`. No additional setup is required.
The existing Docker Compose configuration already includes MinIO storage service and all necessary environment variables in `docker-compose/local/.env.example`. No additional setup is required.
### Image Generation Architecture
@@ -84,7 +84,7 @@ The image generation feature requires:
### Storage Configuration
The `.env.example.development` file includes all necessary S3 environment variables:
The `docker-compose/local/.env.example` file includes all necessary S3 environment variables:
```bash
# S3 Storage Configuration (MinIO for local development)
@@ -11,13 +11,13 @@ LobeChat 提供了内置的客户端数据库体验。
### 环境配置
首先,复制示例环境文件来创建你的开发配置:
首先,复制示例环境文件来创建你的 Docker Compose 配置:
```bash
cp .env.example.development .env.development
cp docker-compose/local/.env.example docker-compose/local/.env
```
此文件包含服务端数据库模式所需的所有环境变量,配置了:
根据需要编辑 `docker-compose/local/.env` 文件以适应你的开发设置。此文件包含 Docker 服务所需的所有环境变量,配置了:
- **服务模式**: `NEXT_PUBLIC_SERVICE_MODE=server`
- **数据库**: 带连接字符串的 PostgreSQL
@@ -72,7 +72,7 @@ docker-compose -f docker-compose.development.yml ps
### 图像生成配置
现有的 Docker Compose 配置已经包含了 MinIO 存储服务以及 `.env.example.development` 中的所有必要环境变量。无需额外配置。
现有的 Docker Compose 配置已经包含了 MinIO 存储服务以及 `docker-compose/local/.env.example` 中的所有必要环境变量。无需额外配置。
### 图像生成架构
@@ -84,7 +84,7 @@ docker-compose -f docker-compose.development.yml ps
### 存储配置
`.env.example.development` 文件包含所有必要的 S3 环境变量:
`docker-compose/local/.env.example` 文件包含所有必要的 S3 环境变量:
```bash
# S3 存储配置(本地开发使用 MinIO)
+35 -18
View File
@@ -6,6 +6,7 @@ table agents {
tags jsonb [default: `[]`]
avatar text
background_color text
market_identifier text
plugins jsonb [default: `[]`]
client_id text
user_id text [not null]
@@ -41,6 +42,7 @@ table agents_files {
indexes {
(file_id, agent_id, user_id) [pk]
agent_id [name: 'agents_files_agent_id_idx']
}
}
@@ -55,6 +57,7 @@ table agents_knowledge_bases {
indexes {
(agent_id, knowledge_base_id) [pk]
agent_id [name: 'agents_knowledge_bases_agent_id_idx']
}
}
@@ -75,6 +78,7 @@ table ai_models {
context_window_tokens integer
source varchar(20)
released_at varchar(10)
settings jsonb [default: `{}`]
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()`]
@@ -193,6 +197,7 @@ table documents {
file_id text
user_id text [not null]
client_id text
editor_data 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()`]
@@ -333,6 +338,7 @@ table message_groups {
indexes {
(client_id, user_id) [name: 'message_groups_client_id_user_id_unique', unique]
topic_id [name: 'message_groups_topic_id_idx']
}
}
@@ -340,6 +346,7 @@ table message_plugins {
id text [pk, not null]
tool_call_id text
type text [default: 'default']
intervention jsonb
api_name text
arguments text
identifier text
@@ -443,6 +450,7 @@ table messages {
user_id [name: 'messages_user_id_idx']
session_id [name: 'messages_session_id_idx']
thread_id [name: 'messages_thread_id_idx']
agent_id [name: 'messages_agent_id_idx']
}
}
@@ -804,6 +812,8 @@ table agents_to_sessions {
indexes {
(agent_id, session_id) [pk]
session_id [name: 'agents_to_sessions_session_id_idx']
agent_id [name: 'agents_to_sessions_agent_id_idx']
}
}
@@ -864,6 +874,8 @@ table sessions {
(client_id, user_id) [name: 'sessions_client_id_user_id_unique', unique]
user_id [name: 'sessions_user_id_idx']
(id, user_id) [name: 'sessions_id_user_id_idx']
(user_id, updated_at) [name: 'sessions_user_id_updated_at_idx']
group_id [name: 'sessions_group_id_idx']
}
}
@@ -884,6 +896,7 @@ table threads {
indexes {
(client_id, user_id) [name: 'threads_client_id_user_id_unique', unique]
topic_id [name: 'threads_topic_id_idx']
}
}
@@ -916,6 +929,8 @@ table topics {
(client_id, user_id) [name: 'topics_client_id_user_id_unique', unique]
user_id [name: 'topics_user_id_idx']
(id, user_id) [name: 'topics_id_user_id_idx']
session_id [name: 'topics_session_id_idx']
group_id [name: 'topics_group_id_idx']
}
}
@@ -972,6 +987,8 @@ table user_memories {
memory_category varchar(255)
memory_layer varchar(255)
memory_type varchar(255)
metadata jsonb
tags text[]
title varchar(255)
summary text
summary_vector_1024 vector(1024)
@@ -992,9 +1009,10 @@ table user_memories {
table user_memories_contexts {
id varchar(255) [pk, not null]
user_id text
user_memory_ids jsonb
labels jsonb
extracted_labels jsonb
metadata jsonb
tags text[]
associated_objects jsonb
associated_subjects jsonb
title text
@@ -1018,9 +1036,10 @@ table user_memories_contexts {
table user_memories_experiences {
id varchar(255) [pk, not null]
user_memory_id text
labels jsonb
extracted_labels jsonb
user_id text
user_memory_id varchar(255)
metadata jsonb
tags text[]
type varchar(255)
situation text
situation_vector vector(1024)
@@ -1030,7 +1049,6 @@ table user_memories_experiences {
action_vector vector(1024)
key_learning text
key_learning_vector vector(1024)
metadata jsonb
score_confidence real [default: 0]
accessed_at "timestamp with time zone" [not null, default: `now()`]
created_at "timestamp with time zone" [not null, default: `now()`]
@@ -1045,17 +1063,17 @@ table user_memories_experiences {
}
table user_memories_identities {
current_focuses text
id varchar(255) [pk, not null]
user_id text
user_memory_id varchar(255)
metadata jsonb
tags text[]
type varchar(255)
description text
description_vector vector(1024)
experience text
extracted_labels jsonb
id varchar(255) [pk, not null]
labels jsonb
relationship text
episodic_date "timestamp with time zone"
relationship varchar(255)
role text
type varchar(255)
user_memory_id 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()`]
@@ -1068,11 +1086,10 @@ table user_memories_identities {
table user_memories_preferences {
id varchar(255) [pk, not null]
context_id varchar(255)
user_id text
user_memory_id varchar(255)
labels jsonb
extracted_labels jsonb
extracted_scopes jsonb
metadata jsonb
tags text[]
conclusion_directives text
conclusion_directives_vector vector(1024)
type varchar(255)
@@ -0,0 +1,399 @@
# 集成测试指南
## 概述
集成测试验证多个模块协同工作的正确性,确保完整的调用链路(Router → Service → Model → Database)正常运行。
## 为什么需要集成测试?
即使单元测试覆盖率很高(80%+),仍可能出现集成问题:
### 常见问题示例
```typescript
// ❌ 问题:参数在调用链中丢失
// Router 层
const messageId = await messageModel.create({
content: 'test',
sessionId: 'xxx',
topicId: 'yyy', // ← 传入了 topicId
});
// Model 层(假设实现有问题)
async create(data) {
return this.db.insert(messages).values({
content: data.content,
sessionId: data.sessionId,
// ❌ 忘记传递 topicId
});
}
// 结果:单元测试通过(因为 mock 了 Model),但实际运行时 topicId 丢失
```
### 集成测试能发现的问题
1. **参数传递遗漏**: containerId、threadId、topicId 等在调用链中丢失
2. **数据库约束**: 外键关系、级联删除等在 mock 中无法验证
3. **事务完整性**: 跨表操作的原子性
4. **权限验证**: 跨用户访问控制
5. **真实场景**: 模拟用户的完整操作流程
## 运行集成测试
```bash
# 运行所有集成测试
pnpm test:integration
# 运行特定文件
pnpm vitest tests/integration/routers/message.integration.test.ts
# 监听模式
pnpm vitest tests/integration --watch
# 生成覆盖率报告
pnpm test:integration --coverage
```
## 目录结构
```
tests/integration/
├── README.md # 集成测试说明
├── setup.ts # 通用设置和工具函数
└── routers/ # Router 层集成测试
├── message.integration.test.ts # Message Router 测试
├── session.integration.test.ts # Session Router 测试
├── topic.integration.test.ts # Topic Router 测试
└── chat-flow.integration.test.ts # 完整聊天流程测试
```
## 编写集成测试
### 基本模板
```typescript
// @vitest-environment node
import { eq } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '@/database/models/__tests__/_util';
import { messages, sessions, users } from '@/database/schemas';
import { LobeChatDatabase } from '@/database/type';
import { messageRouter } from '@/server/routers/lambda/message';
import { cleanupTestUser, createTestContext, createTestUser } from '../setup';
describe('Your Feature Integration Tests', () => {
let serverDB: LobeChatDatabase;
let userId: string;
beforeEach(async () => {
// 1. 获取测试数据库
serverDB = await getTestDB();
// 2. 创建测试用户
userId = await createTestUser(serverDB);
// 3. 准备其他测试数据
// ...
});
afterEach(async () => {
// 清理测试数据
await cleanupTestUser(serverDB, userId);
});
it('should do something', async () => {
// 1. 创建 tRPC caller
const caller = messageRouter.createCaller(createTestContext(userId));
// 2. 执行操作
const result = await caller.someMethod({
/* params */
});
// 3. 验证结果
expect(result).toBeDefined();
// 4. 🔥 关键:从数据库验证
const [dbRecord] = await serverDB.select().from(messages).where(eq(messages.id, result));
expect(dbRecord).toMatchObject({
// 验证所有关键字段
});
});
});
```
### 最佳实践
#### 1. 测试完整的调用链路
```typescript
it('should create message with correct associations', async () => {
const caller = messageRouter.createCaller(createTestContext(userId));
// 执行操作
const messageId = await caller.createMessage({
content: 'Test',
sessionId: testSessionId,
topicId: testTopicId,
});
// ✅ 从数据库验证,而不是只验证返回值
const [message] = await serverDB.select().from(messages).where(eq(messages.id, messageId));
expect(message.sessionId).toBe(testSessionId);
expect(message.topicId).toBe(testTopicId);
expect(message.userId).toBe(userId);
});
```
#### 2. 测试级联操作
```typescript
it('should cascade delete messages when session is deleted', async () => {
const sessionCaller = sessionRouter.createCaller(createTestContext(userId));
const messageCaller = messageRouter.createCaller(createTestContext(userId));
// 创建 session 和 messages
const sessionId = await sessionCaller.createSession({
/* ... */
});
await messageCaller.createMessage({ sessionId /* ... */ });
// 删除 session
await sessionCaller.removeSession({ id: sessionId });
// ✅ 验证相关消息也被删除
const remainingMessages = await serverDB
.select()
.from(messages)
.where(eq(messages.sessionId, sessionId));
expect(remainingMessages).toHaveLength(0);
});
```
#### 3. 测试跨 Router 协作
```typescript
it('should handle complete chat flow', async () => {
const sessionCaller = sessionRouter.createCaller(createTestContext(userId));
const topicCaller = topicRouter.createCaller(createTestContext(userId));
const messageCaller = messageRouter.createCaller(createTestContext(userId));
// 1. 创建 session
const sessionId = await sessionCaller.createSession({
/* ... */
});
// 2. 创建 topic
const topicId = await topicCaller.createTopic({ sessionId /* ... */ });
// 3. 创建 message
const messageId = await messageCaller.createMessage({
sessionId,
topicId,
/* ... */
});
// ✅ 验证完整的关联关系
const [message] = await serverDB.select().from(messages).where(eq(messages.id, messageId));
expect(message.sessionId).toBe(sessionId);
expect(message.topicId).toBe(topicId);
});
```
#### 4. 测试错误场景
```typescript
it('should prevent cross-user access', async () => {
// 用户 A 创建 session
const sessionId = await sessionRouter.createCaller(createTestContext(userA)).createSession({
/* ... */
});
// 用户 B 尝试访问
const callerB = messageRouter.createCaller(createTestContext(userB));
// ✅ 应该抛出错误
await expect(
callerB.createMessage({
sessionId,
content: 'Unauthorized',
}),
).rejects.toThrow();
});
```
#### 5. 测试并发场景
```typescript
it('should handle concurrent operations', async () => {
const caller = messageRouter.createCaller(createTestContext(userId));
// 并发创建多个消息
const promises = Array.from({ length: 10 }, (_, i) =>
caller.createMessage({
content: `Message ${i}`,
sessionId: testSessionId,
}),
);
const messageIds = await Promise.all(promises);
// ✅ 验证所有消息都创建成功且唯一
expect(messageIds).toHaveLength(10);
expect(new Set(messageIds).size).toBe(10);
});
```
### 数据隔离
每个测试用例应该独立,不依赖其他测试:
```typescript
beforeEach(async () => {
// 为每个测试创建新的数据
userId = await createTestUser(serverDB);
testSessionId = await createTestSession(serverDB, userId);
});
afterEach(async () => {
// 清理测试数据
await cleanupTestUser(serverDB, userId);
});
```
### 测试命名
使用清晰的命名描述测试意图:
```typescript
// ✅ 好的命名
it('should create message with correct sessionId and topicId');
it('should cascade delete messages when session is deleted');
it('should prevent cross-user access to messages');
// ❌ 不好的命名
it('test message creation');
it('test delete');
```
## 与单元测试的区别
| 维度 | 单元测试 | 集成测试 |
| ------- | --------- | ------- |
| **范围** | 单个函数 / 类 | 多个模块协作 |
| **依赖** | Mock 外部依赖 | 使用真实依赖 |
| **数据库** | Mock | 真实测试数据库 |
| **速度** | 快(毫秒级) | 慢(秒级) |
| **数量** | 多(60% | 少(30% |
| **目的** | 验证逻辑正确性 | 验证集成正确性 |
## 测试金字塔
```
/\
/E2E\ ← 10% (关键业务流程)
/------\
/ 集成 \ ← 30% (API 集成测试) ⭐ 本指南重点
/----------\
/ 单元测试 \ ← 60% (已有 80%+)
/--------------\
```
## 覆盖目标
### 优先级 P0(必须覆盖)
- ✅ 跨层级的 ID 传递(sessionId、topicId、containerId、threadId
- ✅ 权限验证(用户只能访问自己的资源)
- ✅ 级联删除(删除 session 时相关数据也删除)
- ✅ 外键约束(不能创建不存在的关联)
### 优先级 P1(应该覆盖)
- 并发场景(多个请求同时操作)
- 分页查询(正确的数据分页)
- 搜索功能(关键词搜索)
- 批量操作(批量创建 / 删除)
### 优先级 P2(可以覆盖)
- 统计功能(计数、排名)
- 复杂查询(多条件筛选)
- 性能测试(大量数据场景)
## 调试技巧
### 1. 查看测试数据库状态
```typescript
it('debug test', async () => {
// 执行操作
await caller.createMessage({
/* ... */
});
// 打印数据库状态
const allMessages = await serverDB.select().from(messages);
console.log('All messages:', allMessages);
});
```
### 2. 使用 Drizzle Studio
```bash
# 启动 Drizzle Studio 查看测试数据库
pnpm db:studio
```
### 3. 保留测试数据
```typescript
afterEach(async () => {
// 临时注释掉清理代码,保留数据用于调试
// await cleanupTestUser(serverDB, userId);
});
```
## 常见问题
### Q: 集成测试很慢怎么办?
A:
1. 只测试关键路径,不要过度测试
2. 使用 `test.concurrent` 并行执行独立的测试
3. 优化测试数据准备,避免重复创建
### Q: 测试之间相互影响怎么办?
A:
1. 确保每个测试使用独立的 userId
2. 在 `afterEach` 中彻底清理数据
3. 使用事务隔离(如果数据库支持)
### Q: 如何测试需要认证的 API?
A: 使用 `createTestContext(userId)` 创建带认证信息的上下文:
```typescript
const caller = messageRouter.createCaller(createTestContext(userId));
```
## 参考资料
- [Vitest 文档](https://vitest.dev/)
- [Drizzle ORM 文档](https://orm.drizzle.team/)
- [tRPC 测试指南](https://trpc.io/docs/server/testing)
- [测试金字塔](https://martinfowler.com/articles/practical-test-pyramid.html)
## 贡献
欢迎补充更多集成测试用例!请参考现有测试文件的风格。
+32 -21
View File
@@ -26,12 +26,13 @@ By setting the environment variables `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CL
Before using NextAuth, please set the following variables in LobeChat's environment variables:
| Environment Variable | Type | Description |
| ------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `NEXT_PUBLIC_ENABLE_NEXT_AUTH` | Required | This is used to enable the NextAuth service. Set it to `1` to enable it; changing this setting requires recompiling the application. Users deploying with the `lobehub/lobe-chat-database` image have this configuration added by default. |
| `NEXT_AUTH_SECRET` | Required | The key used to encrypt Auth.js session tokens. You can use the following command: `openssl rand -base64 32`, or visit `https://generate-secret.vercel.app/32` to generate the key. |
| `NEXTAUTH_URL` | Required | This URL specifies the callback address for Auth.js when performing OAuth verification. Set this only if the default generated redirect address is incorrect. `https://example.com/api/auth` |
| `NEXT_AUTH_SSO_PROVIDERS` | Optional | This environment variable is used to enable multiple identity verification sources simultaneously, separated by commas, for example, `auth0,microsoft-entra-id,authentik`. |
| Environment Variable | Type | Description |
| -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `NEXT_PUBLIC_ENABLE_NEXT_AUTH` | Required | This is used to enable the NextAuth service. Set it to `1` to enable it; changing this setting requires recompiling the application. Users deploying with the `lobehub/lobe-chat-database` image have this configuration added by default. |
| `AUTH_SECRET` | Required | The key used to encrypt Auth.js session tokens. You can use the following command: `openssl rand -base64 32`, or visit `https://generate-secret.vercel.app/32` to generate the key. |
| `AUTH_URL` | Required | This URL specifies the callback address for Auth.js when performing OAuth verification. Set this only if the default generated redirect address is incorrect. `https://example.com/api/auth` |
| `NEXT_AUTH_SSO_PROVIDERS` | Optional | This environment variable is used to enable multiple identity verification sources simultaneously, separated by commas, for example, `auth0,microsoft-entra-id,authentik`. |
| `NEXT_AUTH_SSO_SESSION_STRATEGY` | Optional | The session strategy for Auth.js. Options are `jwt` or `database`. Default is `jwt`. |
Currently supported identity verification services include:
@@ -67,21 +68,31 @@ To simultaneously enable multiple identity verification sources, please set the
The order corresponds to the display order of the SSO providers.
| SSO Provider | Value |
| --------------------- | ----------------------- |
| Auth0 | `auth0` |
| Authenlia | `authenlia` |
| Authentik | `authentik` |
| Casdoor | `casdoor` |
| Cloudflare Zero Trust | `cloudflare-zero-trust` |
| Github | `github` |
| Logto | `logto` |
| Microsoft Entra ID | `microsoft-entra-id` |
| ZITADEL | `zitadel` |
| Keycloak | `keycloak` |
| Google | `google` |
| Okta | `okta` |
| SSO Provider | Value | Additional Features |
| --------------------- | ----------------------- | ------------------- |
| Auth0 | `auth0` | |
| Authenlia | `authenlia` | |
| Authentik | `authentik` | |
| Casdoor | `casdoor` | `Webhook` |
| Cloudflare Zero Trust | `cloudflare-zero-trust` | |
| Github | `github` | |
| Logto | `logto` | `Webhook` |
| Microsoft Entra ID | `microsoft-entra-id` | |
| ZITADEL | `zitadel` | |
| Keycloak | `keycloak` | |
| Google | `google` | |
| Okta | `okta` | |
## Additional Features
### Webhook Support
Allow LobeChat to receive notifications when user information is updated in the identity provider. Supported providers include Casdoor and Logto. Please refer to the specific provider documentation for configuration details.
### Database Session
Allow the session store in database, see also the [Auth.js Session Documentation](https://authjs.dev/concepts/session-strategies#database-session).
## Other SSO Providers
Please refer to the [NextAuth.js](https://next-auth.js.org/providers) documentation and feel free to submit a Pull Request.
Please refer to the [Auth.js](https://authjs.dev/getting-started/authentication/oauth) documentation and feel free to submit a Pull Request.
+30 -19
View File
@@ -24,12 +24,13 @@ LobeChat 与 Clerk 做了深度集成,能够为用户提供一个更加安全
在使用 NextAuth 之前,请先在 LobeChat 的环境变量中设置以下变量:
| 环境变量 | 类型 | 描述 |
| ------------------------------ | -- | ------------------------------------------------------------------------------------------------------------ |
| `NEXT_PUBLIC_ENABLE_NEXT_AUTH` | 必选 | 用于启用 NextAuth 服务,设置为 `1` 以启用,更改此项需要重新编译应用。使用 `lobehub/lobe-chat-database` 镜像部署的用户已经默认添加了该项配置。 |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令: `openssl rand -base64 32`,或者访问 `https://generate-secret.vercel.app/32` 生成秘钥。 |
| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
| `NEXT_AUTH_SSO_PROVIDERS` | 可选 | 该环境变量用于同时启用多个身份验证源,以逗号 `,` 分割,例如 `auth0,microsoft-entra-id,authentik`。 |
| 环境变量 | 类型 | 描述 |
| -------------------------------- | -- | ------------------------------------------------------------------------------------------------------------ |
| `NEXT_PUBLIC_ENABLE_NEXT_AUTH` | 必选 | 用于启用 NextAuth 服务,设置为 `1` 以启用,更改此项需要重新编译应用。使用 `lobehub/lobe-chat-database` 镜像部署的用户已经默认添加了该项配置。 |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令: `openssl rand -base64 32`,或者访问 `https://generate-secret.vercel.app/32` 生成秘钥。 |
| `AUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
| `NEXT_AUTH_SSO_PROVIDERS` | 可选 | 该环境变量用于同时启用多个身份验证源,以逗号 `,` 分割,例如 `auth0,microsoft-entra-id,authentik`。 |
| `NEXT_AUTH_SSO_SESSION_STRATEGY` | 可选 | Auth.js 的会话策略。选项为 `jwt` 或 `database`。默认值为 `jwt`。 |
目前支持的身份验证服务有:
@@ -63,19 +64,29 @@ LobeChat 与 Clerk 做了深度集成,能够为用户提供一个更加安全
顺序为 SSO 提供商的显示顺序。
| SSO 提供商 | 值 |
| --------------------- | ----------------------- |
| Auth0 | `auth0` |
| Authenlia | `authenlia` |
| Authentik | `authentik` |
| Casdoor | `casdoor` |
| Cloudflare Zero Trust | `cloudflare-zero-trust` |
| Github | `github` |
| Logto | `logto` |
| Microsoft Entra ID | `microsoft-entra-id` |
| ZITADEL | `zitadel` |
| Keycloak | `keycloak` |
| Okta | `okta` |
| SSO 提供商 | 值 | 额外功能 |
| --------------------- | ----------------------- | --------- |
| Auth0 | `auth0` | |
| Authenlia | `authenlia` | |
| Authentik | `authentik` | |
| Casdoor | `casdoor` | `Webhook` |
| Cloudflare Zero Trust | `cloudflare-zero-trust` | |
| Github | `github` | |
| Logto | `logto` | `Webhook` |
| Microsoft Entra ID | `microsoft-entra-id` | |
| ZITADEL | `zitadel` | |
| Keycloak | `keycloak` | |
| Okta | `okta` | |
## 额外功能
### Webhook 支持
允许 LobeChat 在身份提供商中用户信息更新时接收通知。支持的提供商包括 Casdoor 和 Logto。请参考具体提供商文档进行配置。
### 数据库会话
允许会话存储在数据库中,详情请参阅 [Auth.js 会话文档](https://authjs.dev/concepts/session-strategies#database-session)。
## 其他 SSO 提供商
@@ -55,12 +55,12 @@ tags:
| Environment Variable | Type | Description |
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | Key used to encrypt Auth.js session tokens. You can generate a key using the following command: `openssl rand -base64 32` |
| `AUTH_SECRET` | Required | Key used to encrypt Auth.js session tokens. You can generate a key using the following command: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the single sign-on provider for LoboChat. Use `auth0` for Auth0. |
| `AUTH_AUTH0_ID` | Required | Client ID of the Auth0 application |
| `AUTH_AUTH0_SECRET` | Required | Client Secret of the Auth0 application |
| `AUTH_AUTH0_ISSUER` | Required | Domain of the Auth0 application, `https://example.auth0.com` |
| `NEXTAUTH_URL` | Required | The URL is used to specify the callback address for the execution of OAuth authentication in Auth.js. It needs to be set only when the default address is incorrect. `https://example.com/api/auth` |
| `AUTH_URL` | Required | The URL is used to specify the callback address for the execution of OAuth authentication in Auth.js. It needs to be set only when the default address is incorrect. `https://example.com/api/auth` |
<Callout type={'tip'}>
You can refer to the related variable details at [📘Environment Variables](/docs/self-hosting/environment-variable#auth0).
@@ -51,12 +51,12 @@ tags:
| 环境变量 | 类型 | 描述 |
| ------------------------- | -- | ------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | 必选 | 选择 LoboChat 的单点登录提供商。使用 Auth0 请填写 `auth0`。 |
| `AUTH_AUTH0_ID` | 必选 | Auth0 应用程序的 Client ID |
| `AUTH_AUTH0_SECRET` | 必选 | Auth0 应用程序的 Client Secret |
| `AUTH_AUTH0_ISSUER` | 必选 | Auth0 应用程序的 Domain`https://example.auth0.com` |
| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
| `AUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
<Callout type={'tip'}>
前往 [📘 环境变量](/zh/docs/self-hosting/environment-variables/auth#auth-0) 可查阅相关变量详情。
@@ -54,12 +54,12 @@ tags:
| Environment Variable | Type | Description |
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | The secret used to encrypt Auth.js session tokens. You can generate a secret using the following command: `openssl rand -base64 32` |
| `AUTH_SECRET` | Required | The secret used to encrypt Auth.js session tokens. You can generate a secret using the following command: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the SSO provider for LoboChat. Use `authentik` for Authentik. |
| `AUTH_AUTHELIA_ID` | Required | The id just configured in Authelia, example value is lobe-chat |
| `AUTH_AUTHELIA_SECRET` | Required | The plaintext corresponding to the secret just configured in Authelia, example value is `insecure_secret` |
| `AUTH_AUTHELIA_ISSUER` | Required | Your Authelia URL, for example [https://sso.example.com](https://sso.example.com) |
| `NEXTAUTH_URL` | Required | This URL is used to specify the callback address for Auth.js when performing OAuth verification. It only needs to be set when the default generated redirect address is incorrect. [https://chat.example.com/api/auth](https://chat.example.com/api/auth) |
| `AUTH_URL` | Required | This URL is used to specify the callback address for Auth.js when performing OAuth verification. It only needs to be set when the default generated redirect address is incorrect. [https://chat.example.com/api/auth](https://chat.example.com/api/auth) |
<Callout type={'tip'}>
Go to [📘 Environment Variables](/docs/self-hosting/environment-variable#Authelia) for details about the variables.
@@ -53,12 +53,12 @@ tags:
| 环境变量 | 类型 | 描述 |
| ------------------------- | -- | ------------------------------------------------------------------------------------------------ |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | 必选 | 选择 LoboChat 的单点登录提供商。使用 Authelia 请填写 `authelia`。 |
| `AUTH_AUTHELIA_ID` | 必选 | 刚刚在 Authelia 配置的 `id`,示例值是 `lobe-chat` |
| `AUTH_AUTHELIA_SECRET` | 必选 | 刚刚在 Authelia 配置的 `secret` 对应的明文,示例值是 `insecure_secret` |
| `AUTH_AUTHELIA_ISSUER` | 必选 | 您的 Authelia 的网址,例如 `https://sso.example.com` |
| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://chat.example.com/api/auth` |
| `AUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://chat.example.com/api/auth` |
<Callout type={'tip'}>
前往 [📘 环境变量](/zh/docs/self-hosting/environment-variable#Authelia) 可查阅相关变量详情。
@@ -49,12 +49,12 @@ tags:
| Environment Variable | Type | Description |
| ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | The secret used to encrypt Auth.js session tokens. You can generate a secret using the following command: `openssl rand -base64 32` |
| `AUTH_SECRET` | Required | The secret used to encrypt Auth.js session tokens. You can generate a secret using the following command: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the SSO provider for LoboChat. Use `authentik` for Authentik. |
| `AUTH_AUTHENTIK_ID` | Required | The Client ID from the Authentik application provider details page |
| `AUTH_AUTHENTIK_SECRET` | Required | The Client Secret from the Authentik application provider details page |
| `AUTH_AUTHENTIK_ISSUER` | Required | The OpenID Configuration Issuer from the Authentik application provider details page |
| `NEXTAUTH_URL` | Required | This URL is used to specify the callback address for Auth.js when performing OAuth authentication. It only needs to be set when the default generated redirect address is incorrect. `https://example.com/api/auth` |
| `AUTH_URL` | Required | This URL is used to specify the callback address for Auth.js when performing OAuth authentication. It only needs to be set when the default generated redirect address is incorrect. `https://example.com/api/auth` |
<Callout type={'tip'}>
Go to [📘 Environment Variables](/docs/self-hosting/environment-variable#Authentik) for details about the variables.
@@ -45,12 +45,12 @@ tags:
| 环境变量 | 类型 | 描述 |
| ------------------------- | -- | ------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | 必选 | 选择 LoboChat 的单点登录提供商。使用 Authentik 请填写 `authentik`。 |
| `AUTH_AUTHENTIK_ID` | 必选 | Authentik 提供程序详情页的 客户端 ID |
| `AUTH_AUTHENTIK_SECRET` | 必选 | Authentik 提供程序详情页的 客户端 Secret |
| `AUTH_AUTHENTIK_ISSUER` | 必选 | Authentik 提供程序详情页的 OpenID 配置颁发者 |
| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
| `AUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
<Callout type={'tip'}>
前往 [📘 环境变量](/zh/docs/self-hosting/environment-variable#Authentik) 可查阅相关变量详情。
@@ -134,12 +134,12 @@ If you are deploying using a public network, the following assumptions apply:
| Environment Variable | Type | Description |
| ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | A key for encrypting Auth.js session tokens. You can generate a key using the command: `openssl rand -base64 32`. |
| `AUTH_SECRET` | Required | A key for encrypting Auth.js session tokens. You can generate a key using the command: `openssl rand -base64 32`. |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the single sign-on provider for LobeChat. Fill in `casdoor` for using Casdoor. |
| `AUTH_CASDOOR_ID` | Required | The client ID from the Casdoor application details page. |
| `AUTH_CASDOOR_SECRET` | Required | The client secret from the Casdoor application details page. |
| `AUTH_CASDOOR_ISSUER` | Required | The OpenID Connect issuer for the Casdoor provider. |
| `NEXTAUTH_URL` | Required | This URL specifies the callback address for Auth.js during OAuth verification and needs to be set only if the default generated redirect address is incorrect. `https://lobe.example.com/api/auth` |
| `AUTH_URL` | Required | This URL specifies the callback address for Auth.js during OAuth verification and needs to be set only if the default generated redirect address is incorrect. `https://lobe.example.com/api/auth` |
| `CASDOOR_WEBHOOK_SECRET` | Optional | A key used to verify whether the request sent by Casdoor is legal. |
<Callout type={'tip'}>
@@ -133,12 +133,12 @@ tags:
| 环境变量 | 类型 | 描述 |
| ------------------------- | -- | ------------------------------------------------------------------------------------------------ |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | 必选 | 选择 LoboChat 的单点登录提供商。使用 Casdoor 请填写 `casdoor`。 |
| `AUTH_CASDOOR_ID` | 必选 | Casdoor 应用详情页的客户端 ID |
| `AUTH_CASDOOR_SECRET` | 必选 | Casdoor 应用详情页的客户端密钥 |
| `AUTH_CASDOOR_ISSUER` | 必选 | Casdoor 提供程序的 OpenID Connect 颁发者。 |
| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://lobe.example.com/api/auth` |
| `AUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://lobe.example.com/api/auth` |
| `CASDOOR_WEBHOOK_SECRET` | 可选 | 用于验证 Casdoor 发送的 Webhook 请求是否合法的密钥。 |
<Callout type={'tip'}>
@@ -48,12 +48,12 @@ tags:
| Environment Variable | Type | Description |
| ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | The secret used to encrypt Auth.js session tokens. You can generate a secret using the following command: `openssl rand -base64 32` |
| `AUTH_SECRET` | Required | The secret used to encrypt Auth.js session tokens. You can generate a secret using the following command: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the SSO provider for LoboChat. Use `cloudflare-zero-trust` for Cloudflare Zero Trust. |
| `AUTH_CLOUDFLARE_ZERO_TRUST_ID` | Required | The Client ID from the Cloudflare Zero Trust application provider details page |
| `AUTH_CLOUDFLARE_ZERO_TRUST_SECRET` | Required | The Client Secret from the Cloudflare Zero Trust application provider details page |
| `AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER` | Required | The OpenID Configuration Issuer from the Cloudflare Zero Trust application provider details page |
| `NEXTAUTH_URL` | Required | This URL is used to specify the callback address for Auth.js when performing OAuth authentication. It only needs to be set when the default generated redirect address is incorrect. `https://example.com/api/auth` |
| `AUTH_URL` | Required | This URL is used to specify the callback address for Auth.js when performing OAuth authentication. It only needs to be set when the default generated redirect address is incorrect. `https://example.com/api/auth` |
<Callout type={'tip'}>
Go to [📘 Environment Variables](/docs/self-hosting/environment-variable#Cloudflare%20Zero%20Trust) for details about the variables.
@@ -46,12 +46,12 @@ tags:
| 环境变量 | 类型 | 描述 |
| ----------------------------------- | -- | ------------------------------------------------------------------------------------------------------------ |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | 必选 | 选择 LoboChat 的单点登录提供商。使用 Cloudflare Zero Trust 请填写 `cloudflare-zero-trust`。 |
| `AUTH_CLOUDFLARE_ZERO_TRUST_ID` | 必选 | 在 Cloudflare Zero Trust 生成的 `Client ID`,示例值是 `lobe-chat` |
| `AUTH_CLOUDFLARE_ZERO_TRUST_SECRET` | 必选 | 在 Cloudflare Zero Trust 生成的 `Client secret`,示例值是 `insecure_secret` |
| `AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER` | 必选 | 在 Cloudflare Zero Trust 生成的 `Issuer`,例如 `https://example.cloudflareaccess.com/cdn-cgi/access/sso/oidc/7db0f` |
| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://chat.example.com/api/auth` |
| `AUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://chat.example.com/api/auth` |
<Callout type={'tip'}>
前往 [📘 环境变量](/zh/docs/self-hosting/environment-variable#Cloudflare%20Zero%20Trust) 可查阅相关变量详情。
@@ -54,11 +54,11 @@ tags:
| Environment Variable | Type | Description |
| ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | Key used to encrypt Auth.js session tokens. You can generate the key using the command: `openssl rand -base64 32` |
| `AUTH_SECRET` | Required | Key used to encrypt Auth.js session tokens. You can generate the key using the command: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the Single Sign-On provider for LobeChat. Use `github` for Github. |
| `AUTH_GITHUB_ID` | Required | Client ID in the Github App details page. |
| `AUTH_GITHUB_SECRET` | Required | Client Secret in the Github App details page. |
| `NEXTAUTH_URL` | Required | This URL is used to specify the callback address for Auth.js when performing OAuth authentication. Only set it if the default generated redirect address is incorrect. `https://example.com/api/auth` |
| `AUTH_URL` | Required | This URL is used to specify the callback address for Auth.js when performing OAuth authentication. Only set it if the default generated redirect address is incorrect. `https://example.com/api/auth` |
<Callout type={'tip'}>
Go to [📘 Environment Variables](/docs/self-hosting/environment-variables/auth#github) for detailed
@@ -52,11 +52,11 @@ tags:
| 环境变量 | 类型 | 描述 |
| ------------------------- | -- | ------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | 必选 | 选择 LoboChat 的单点登录提供商。使用 Github 请填写 `github`。 |
| `AUTH_GITHUB_ID` | 必选 | Github App 详情页的 客户端 ID |
| `AUTH_GITHUB_SECRET` | 必选 | Github App 详情页的 客户端 Secret |
| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
| `AUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
<Callout type={'tip'}>
前往 [📘 环境变量](/zh/docs/self-hosting/environment-variables/auth#github) 可查阅相关变量详情。
@@ -21,49 +21,52 @@ tags:
<Steps>
### Create a Google Cloud OAuth 2.0 Client
In your [Google Cloud Console][google-cloud-console], navigate to **APIs & Services > Credentials**.
In your [Google Cloud Console][google-cloud-console], navigate to **APIs & Services > Credentials**.
Click on **Create Credentials** and select **OAuth client ID**.
Click on **Create Credentials** and select **OAuth client ID**.
If you haven't already set up a consent screen, you will be prompted to do so. Complete the OAuth consent screen setup (specify app name, support email, and add authorized users if needed).
If you haven't already set up a consent screen, you will be prompted to do so. Complete the OAuth consent screen setup (specify app name, support email, and add authorized users if needed).
Select **Web application** as the application type.
Select **Web application** as the application type.
In the **Authorized redirect URIs** section, enter:
In the **Authorized redirect URIs** section, enter:
```bash
https://your-domain/api/auth/callback/google
```
```bash
https://your-domain/api/auth/callback/google
```
\<Callout type={'info'}>
\- You can add or modify redirect URIs after registration, but make sure the URL matches your deployed LobeChat instance.
\- Replace "your-domain" with your actual domain. </Callout>
<Callout type={'info'}>
\- You can add or modify redirect URIs after registration, but make sure the URL matches your deployed LobeChat instance.
\- Replace "your-domain" with your actual domain.
</Callout>
Click **Create**.
Click **Create**.
After creation, copy the **Client ID** and **Client Secret**.
After creation, copy the **Client ID** and **Client Secret**.
<Image alt="Google OAuth Setup" inStep src="https://developers.google.com/static/identity/images/gsi/web/gcs-signin-2.png" />
### Add Users (Optional for Internal Use Only)
### Add Users (Optional for Internal Use Only)
If your application is in **Testing** or **Internal** publishing status, add user emails in the OAuth consent screen under **Test users**.
Users not added here will not be able to authenticate.
If your application is in **Testing** or **Internal** publishing status, add user emails in the OAuth consent screen under **Test users**.
Users not added here will not be able to authenticate.
### Configure Environment Variables
### Configure Environment Variables
When deploying LobeChat, configure the following environment variables:
When deploying LobeChat, configure the following environment variables:
| Environment Variable | Type | Description |
| ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | Key to encrypt Auth.js session tokens. Generate using: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the single sign-on provider for LobeChat. Use `google` for Google SSO. |
| `AUTH_GOOGLE_ID` | Required | Client ID from Google Cloud OAuth. |
| `AUTH_GOOGLE_SECRET` | Required | Client Secret from Google Cloud OAuth. |
| `NEXTAUTH_URL` | Required | Specifies the callback address for Auth.js when performing OAuth authentication. E.g. `https://your-domain/api/auth` |
| Environment Variable | Type | Description |
| ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | Key to encrypt Auth.js session tokens. Generate using: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the single sign-on provider for LobeChat. Use `google` for Google SSO. |
| `AUTH_GOOGLE_ID` | Required | Client ID from Google Cloud OAuth. |
| `AUTH_GOOGLE_SECRET` | Required | Client Secret from Google Cloud OAuth. |
| `AUTH_URL` | Required | Specifies the callback address for Auth.js when performing OAuth authentication. E.g. `https://your-domain/api/auth` |
\<Callout type={'tip'}>
See [📘 environment variables](/docs/self-hosting/environment-variable#google) for more details on these variables. </Callout> </Steps>
<Callout type={'tip'}>
See [📘 environment variables](/docs/self-hosting/environment-variable#google) for more details on these variables.
</Callout>
</Steps>
<Callout>
After successful deployment, users can sign in to LobeChat using their Google accounts (those added as Test Users, if not in production).
@@ -75,8 +78,8 @@ See the [Google Identity Platform Documentation][google-identity-docs] for advan
## Related Resources
* [Quickstart: Configure a Google OAuth client][google-oauth-quickstart]
- [Quickstart: Configure a Google OAuth client][google-oauth-quickstart]
[google-cloud-console]: https://console.cloud.google.com/apis/credentials
[google-oauth-quickstart]: https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred
[google-identity-docs]: https://developers.google.com/identity
[google-oauth-quickstart]: https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred
@@ -108,12 +108,12 @@ If you deploy using a public network, this guide assumes:
| Environment Variable | Type | Description |
| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | Key used to encrypt Auth.js session tokens. You can generate a key using: `openssl rand -base64 32` |
| `AUTH_SECRET` | Required | Key used to encrypt Auth.js session tokens. You can generate a key using: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the single sign-on provider for LobeChat. For Keycloak, fill in `keycloak`. |
| `AUTH_KEYCLOAK_ID` | Required | Keycloak client ID |
| `AUTH_KEYCLOAK_SECRET` | Required | Keycloak client secret |
| `AUTH_KEYCLOAK_ISSUER` | Required | OpenID Connect issuer URL for the Keycloak provider, in the format `{keycloak_url}/realms/{realm_name}` |
| `NEXTAUTH_URL` | Required | This URL specifies the callback address for Auth.js during OAuth verification. Only needed when the default generated redirect address is incorrect. `https://lobe.example.com/api/auth` |
| `AUTH_URL` | Required | This URL specifies the callback address for Auth.js during OAuth verification. Only needed when the default generated redirect address is incorrect. `https://lobe.example.com/api/auth` |
<Callout type={'tip'}>
Visit [📘 Environment Variables](/zh/docs/self-hosting/environment-variables/auth#keycloak) for details on related variables.
@@ -105,12 +105,12 @@ tags:
| 环境变量 | 类型 | 描述 |
| ------------------------- | -- | ------------------------------------------------------------------------------------------------ |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | 必选 | 选择 LoboChat 的单点登录提供商。使用 Keycloak 请填写 `keycloak`。 |
| `AUTH_KEYCLOAK_ID` | 必选 | Keycloak 客户端 ID |
| `AUTH_KEYCLOAK_SECRET` | 必选 | Keycloak 客户端密钥 |
| `AUTH_KEYCLOAK_ISSUER` | 必选 | Keycloak 提供程序的 OpenID Connect 颁发者 URL,格式为 `{keycloak_url}/realms/{realm_name}` |
| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://lobe.example.com/api/auth` |
| `AUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://lobe.example.com/api/auth` |
<Callout type={'tip'}>
前往 [📘 环境变量](/zh/docs/self-hosting/environment-variables/auth#keycloak) 可查阅相关变量详情。
@@ -50,7 +50,9 @@ If you are using Logto Cloud, assume its endpoint domain is `https://example.log
Go to `Webhooks`, create a Webhook, and fill in the following fields:
- Endpoint URL: `https://lobe.example.com/api/webhooks/logto`
- Events: `User.Data.Updated`
- Events:
- `User.Data.Updated`: Allow LobeChat to synchronize user profile information updates from Logto.
- `User.SuspensionStatus.Updated`: Allow LobeChat to remove the active session from suspended users from logging in, only available when database session strategy is `database`.
After successful creation, copy the Webhook's `Signing Key` and fill it in the `LOGTO_WEBHOOK_SIGNING_KEY` environment variable.
@@ -69,12 +71,12 @@ If you are using Logto Cloud, assume its endpoint domain is `https://example.log
| Environment Variable | Type | Description |
| --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_AUTH_SECRET` | Required | The key used to encrypt Auth.js session tokens. You can generate a key using the command: `openssl rand -base64 32` |
| `AUTH_SECRET` | Required | The key used to encrypt Auth.js session tokens. You can generate a key using the command: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the single sign-on provider for LobeChat. For Logto, enter `logto`. |
| `AUTH_LOGTO_ID` | Required | The Client ID from the Logto App details page |
| `AUTH_LOGTO_SECRET` | Required | The Client Secret from the Logto App details page |
| `AUTH_LOGTO_ISSUER` | Required | OpenID Connect issuer of the Logto provider |
| `NEXTAUTH_URL` | Required | This URL specifies the callback address for Auth.js during OAuth verification, needed only if the default generated redirect address is incorrect. `https://lobe.example.com/api/auth` |
| `AUTH_URL` | Required | This URL specifies the callback address for Auth.js during OAuth verification, needed only if the default generated redirect address is incorrect. `https://lobe.example.com/api/auth` |
| `LOGTO_WEBHOOK_SIGNING_KEY` | Optional | The key used to verify the legality of Webhook requests sent by Logto. |
<Callout type={'tip'}>
@@ -47,7 +47,9 @@ tags:
前往 `Webhooks` ,创建一个 Webhook,填写以下字段:
- 端点 URL `https://lobe.example.com/api/webhooks/logto`
- 事件: `User.Data.Updated`
- 事件:
- `User.Data.Updated`: 允许 LobeChat 同步 Logto 中用户资料信息的更新。
- `User.SuspensionStatus.Updated`: 允许 LobeChat 将被暂停的用户移除登录会话,仅在数据库会话策略为 `database` 时可用。
创建成功后,复制 Webhook 的 `签名密钥`。填写到环境变量中的 `LOGTO_WEBHOOK_SIGNING_KEY`。
@@ -66,12 +68,12 @@ tags:
| 环境变量 | 类型 | 描述 |
| --------------------------- | -- | ------------------------------------------------------------------------------------------------ |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` |
| `NEXT_AUTH_SSO_PROVIDERS` | 必选 | 选择 LoboChat 的单点登录提供商。使用 Logto 请填写 `logto`。 |
| `AUTH_LOGTO_ID` | 必选 | Logto App 详情页的 Client ID |
| `AUTH_LOGTO_SECRET` | 必选 | Logto App 详情页的 Client Secret |
| `AUTH_LOGTO_ISSUER` | 必选 | Logto 提供程序的 OpenID Connect 颁发者 |
| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://lobe.example.com/api/auth` |
| `AUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://lobe.example.com/api/auth` |
| `LOGTO_WEBHOOK_SIGNING_KEY` | 可选 | 用于验证 Logto 发送的 Webhook 请求是否合法的密钥。 |
<Callout type={'tip'}>

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