Compare commits

..

142 Commits

Author SHA1 Message Date
GH Action - Upstream Sync
f80da8a263 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-10-11 01:07:41 +00:00
Lloyd Zhou
c5074f0aa4 Merge pull request #5581 from ConnectAI-E/feature/gemini-functioncall
google gemini support function call
2024-10-10 21:02:36 +08:00
Lloyd Zhou
ba58018a15 Merge pull request #5211 from ConnectAI-E/feature/jest
feat: jest
2024-10-10 21:02:05 +08:00
Lloyd Zhou
63ab83c3c8 Merge pull request #5621 from ConnectAI-E/hotfix/plugin-result
hotfix plugin result is not string #5614
2024-10-10 12:48:55 +08:00
lloydzhou
268cf3b606 hotfix plugin result is not string #5614 2024-10-10 12:47:25 +08:00
GH Action - Upstream Sync
98ab561607 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-10-10 01:07:14 +00:00
Lloyd Zhou
fbc68fa776 Merge pull request #5602 from PeterDaveHello/ImproveTwLocale
i18n: improve tw Traditional Chinese locale
2024-10-09 19:38:06 +08:00
织梦人
9a025ae196 Merge tag 'v2.15.4' 2024-10-09 19:08:43 +08:00
lloydzhou
4ae34ea3ee merge main 2024-10-09 18:27:23 +08:00
Lloyd Zhou
96273fd75e Merge pull request #5611 from ConnectAI-E/feature/tauri-fetch-update
make sure get request_id before body chunk
2024-10-09 16:18:37 +08:00
lloydzhou
3e63d405c1 update 2024-10-09 16:12:01 +08:00
Lloyd Zhou
19b42aac5d Merge pull request #5608 from ConnectAI-E/fix-readme
fix: [#5574] readme
2024-10-09 14:49:34 +08:00
Lloyd Zhou
b67a23200e Merge pull request #5610 from ChatGPTNextWeb/lloydzhou-patch-1
Update README.md
2024-10-09 14:48:55 +08:00
Lloyd Zhou
1dac02e4d6 Update README.md 2024-10-09 14:48:43 +08:00
Lloyd Zhou
acad5b1d08 Merge pull request #5609 from ElricLiu/main
Update README.md
2024-10-09 14:45:27 +08:00
ElricLiu
4e9bb51d2f Update README.md 2024-10-09 14:43:49 +08:00
DDMeaqua
c0c8cdbbf3 fix: [#5574] 文档错误 2024-10-09 14:36:58 +08:00
Lloyd Zhou
cbdc611b54 Merge pull request #5607 from ConnectAI-E/hotfix/summarize-model
fix compressModel, related #5426, fix #5606 #5603 #5575
2024-10-09 14:08:13 +08:00
lloydzhou
93ca303b6c fix ts error 2024-10-09 13:49:33 +08:00
lloydzhou
a925b424a8 fix compressModel, related #5426, fix #5606 #5603 #5575 2024-10-09 13:42:25 +08:00
Lloyd Zhou
5b4d423b58 Merge pull request #5565 from ConnectAI-E/feature/using-tauri-fetch
Feat: using tauri fetch api in App
2024-10-09 13:03:01 +08:00
lloydzhou
6c1cbe120c update 2024-10-09 11:46:49 +08:00
GH Action - Upstream Sync
c4ae73d8a1 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-10-09 01:07:24 +00:00
Peter Dave Hello
77a58bc4b0 i18n: improve tw Traditional Chinese locale 2024-10-09 03:14:38 +08:00
Dogtiti
8ad63a6c25 Merge pull request #5586 from little-huang/patch-1
fix: correct typo in variable name from ALLOWD_PATH to ALLOWED_PATH
2024-10-08 15:26:41 +08:00
Dogtiti
acf9fa36f9 Merge branch 'main' of https://github.com/ConnectAI-E/ChatGPT-Next-Web into feature/jest 2024-10-08 10:30:47 +08:00
Dogtiti
461154bb03 fix: format package 2024-10-08 10:29:42 +08:00
little_huang
cd75461f9e fix: correct typo in variable name from ALLOWD_PATH to ALLOWED_PATH 2024-10-07 10:30:25 +08:00
GH Action - Upstream Sync
31900cbff3 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-10-07 01:10:00 +00:00
Dogtiti
2bac174e6f Merge pull request #4393 from ChatGPTNextWeb/dean-delete-escapeDollarNumber
bugfix: Delete the escapeDollarNumber function, which causes errors i…
2024-10-06 12:41:03 +08:00
Lloyd Zhou
65f80f81ad Merge branch 'main' into dean-delete-escapeDollarNumber 2024-10-04 14:31:00 +08:00
GH Action - Upstream Sync
c6657d3d0c Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-10-04 01:07:46 +00:00
lloydzhou
450766a44b google gemini support function call 2024-10-03 20:28:15 +08:00
Lloyd Zhou
05e6e4bffb Merge pull request #5578 from code-october/fix/safe-equal
use safe equal operation
2024-10-03 10:59:32 +08:00
code-october
fbb66a4a5d use safe equal operation 2024-10-03 02:08:10 +00:00
lloydzhou
d51d31a559 update 2024-10-01 14:40:23 +08:00
lloydzhou
919ee51dca hover show errorMsg when plugin run error 2024-10-01 13:58:50 +08:00
lloydzhou
9c577ad9d5 hotfix for plugin runtime 2024-10-01 12:55:57 +08:00
lloydzhou
953114041b add connect timeout 2024-10-01 12:02:29 +08:00
GH Action - Upstream Sync
cf7c6f2b9a Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-10-01 01:14:19 +00:00
lloydzhou
d830c23dab hotfix for run plugin call post api 2024-09-30 15:38:13 +08:00
lloydzhou
fd3568c459 hotfix for run plugin call post api 2024-09-30 15:33:40 +08:00
lloydzhou
3029dcb2f6 hotfix for run plugin call post api 2024-09-30 15:32:47 +08:00
lloydzhou
35e03e1bca remove code 2024-09-30 13:44:01 +08:00
Lloyd Zhou
cea5b91f96 Merge pull request #5567 from ChatGPTNextWeb/fix-readme
update  readme
2024-09-30 13:31:34 +08:00
lyf
d2984db6e7 fix readme 2024-09-30 13:28:14 +08:00
lyf
deb215ccd1 fix readme 2024-09-30 13:23:24 +08:00
lloydzhou
7173cf2184 update 2024-09-30 13:07:06 +08:00
Lloyd Zhou
0c697e123d Merge pull request #5564 from code-october/fix/html-code
fix quoteEnd extract regex
2024-09-30 13:06:52 +08:00
lloydzhou
edfa6d14ee update 2024-09-30 10:23:24 +08:00
lloydzhou
b6d9ba93fa update 2024-09-30 10:18:30 +08:00
lloydzhou
6293b95a3b update default api base url 2024-09-30 10:13:11 +08:00
GH Action - Upstream Sync
41242cae9b Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-09-30 01:10:08 +00:00
lloydzhou
ef4665cd8b update 2024-09-30 02:57:51 +08:00
lloydzhou
8030e71a5a update 2024-09-30 02:33:02 +08:00
lloydzhou
f42488d4cb using stream fetch replace old tauri http fetch 2024-09-30 02:28:19 +08:00
lloydzhou
af49ed4fdc update 2024-09-30 01:51:14 +08:00
lloydzhou
b174a40634 update 2024-09-30 01:44:27 +08:00
lloydzhou
3c01738c29 update 2024-09-30 01:37:16 +08:00
lloydzhou
9be58f3eb4 fix ts error 2024-09-30 01:30:20 +08:00
lloydzhou
a50c282d01 remove DEFAULT_API_HOST 2024-09-30 01:19:20 +08:00
lloydzhou
5141145e4d revert plugin runtime using tarui/api/http, not using fetch_stream 2024-09-30 00:58:50 +08:00
lloydzhou
b5f6e5a598 update 2024-09-30 00:38:30 +08:00
lloydzhou
7df308d655 Merge remote-tracking branch 'connectai/main' into feature/using-tauri-fetch 2024-09-29 23:36:17 +08:00
code-october
f5ad51a35e fix quoteEnd extract regex 2024-09-29 14:29:42 +00:00
lloydzhou
f9d4105170 stash code 2024-09-29 21:47:38 +08:00
lloydzhou
9e6ee50fa6 using stream_fetch in App 2024-09-29 20:32:36 +08:00
lloydzhou
dd77ad5d74 Merge remote-tracking branch 'connectai/main' into feature/using-tauri-fetch 2024-09-29 19:44:28 +08:00
lloydzhou
3898c507c4 using stream_fetch in App 2024-09-29 19:44:09 +08:00
Lloyd Zhou
fcba50f041 Merge pull request #5547 from ConnectAI-E/hotfix/plugin-opration-id
Hotfix/plugin opration
2024-09-29 16:15:02 +08:00
Lloyd Zhou
452fc86ad1 Merge pull request #5562 from ChatGPTNextWeb/hotfix-google-api
hotfix for `x-goog-api-key`
2024-09-29 15:57:20 +08:00
lloydzhou
5bdf411399 hotfix for x-goog-api-key 2024-09-29 15:51:28 +08:00
lloydzhou
2d920f7ccc using stream: schema to fetch in App 2024-09-28 15:05:41 +08:00
GH Action - Upstream Sync
89edebd93c Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-09-28 01:07:15 +00:00
lloydzhou
d84d51b475 using sse: schema to fetch in App 2024-09-28 01:19:39 +08:00
Dogtiti
f9d6f4f9da Merge pull request #5553 from ConnectAI-E/fix/default-model
fix: default model
2024-09-27 21:13:26 +08:00
Lloyd Zhou
a13bd624e8 Merge pull request #5552 from joetsuihk/hotfix/upstream-sync-doc
docs: Hotfix/upstream sync doc update
2024-09-27 20:36:16 +08:00
Joe
8fb019b2e2 revert, leave sync.yml untouched
revert commit 19c4ed4463
2024-09-27 17:34:38 +08:00
Joe
2f3457e73d Update correct links to manualy code update section (JP) 2024-09-27 17:33:02 +08:00
Dogtiti
c6ebd6e73c fix: default model 2024-09-27 17:00:24 +08:00
Joe
2333a47c55 Update links in doc to manual code update section (CN) 2024-09-27 16:50:51 +08:00
Joe
b35895b551 Update correct links to manualy code update section 2024-09-27 16:49:08 +08:00
Joe
19c4ed4463 docs links updated sync.yml
https://github.com/Yidadaa/ChatGPT-Next-Web is renamed to https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/
2024-09-27 16:43:50 +08:00
lloydzhou
22aa1698b4 try using method and path when operationId is undefined #5525 2024-09-27 13:31:49 +08:00
lloydzhou
07d089a2bd try using method and path when operationId is undefined #5525 2024-09-27 13:31:07 +08:00
Dogtiti
870ad913cc Merge pull request #5545 from ConnectAI-E/hotfix/google-auth-header
fix: build error
2024-09-27 11:43:43 +08:00
Dogtiti
3fb389551b fix: build error 2024-09-27 11:42:16 +08:00
Dogtiti
d12a4adfb5 Merge pull request #5541 from ConnectAI-E/hotfix/google-auth-header
google api using `x-google-api-key` header
2024-09-27 11:04:10 +08:00
lloydzhou
702e17c96b google api using x-google-api-key header 2024-09-26 23:21:42 +08:00
织梦人
60bd3c56c1 Merge remote-tracking branch 'up/main'
# Conflicts:
#	app/store/chat.ts
2024-09-26 20:47:29 +08:00
Lloyd Zhou
93ff7d26cc Merge pull request #5529 from Leizhenpeng/support-saas-readme
Support saas version in readme
2024-09-25 16:34:25 +08:00
river
13777786c4 chore: ja 2024-09-25 16:30:26 +08:00
river
6655c64e55 chore: cn 2024-09-25 16:29:59 +08:00
river
b7892b58f5 chore: support saas 2024-09-25 13:34:04 +08:00
GH Action - Upstream Sync
659a389fd4 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-09-14 01:03:57 +00:00
GH Action - Upstream Sync
144fdc9b7c Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-09-13 01:04:37 +00:00
织梦人
35f52886c4 Merge remote-tracking branch 'up/main'
# Conflicts:
#	app/store/chat.ts
2024-09-12 09:19:04 +08:00
织梦人
9551f5dfc6 Merge branch 'website'
# Conflicts:
#	app/components/chat.tsx
#	app/utils.ts
#	app/utils/sync.ts
2024-09-07 13:00:33 +08:00
织梦人
370ce3eeca Merge remote-tracking branch 'up/website' into website
# Conflicts:
#	app/components/chat.tsx
#	app/utils.ts
2024-09-07 12:51:37 +08:00
织梦人
5ae4921ee0 fix: 优化云同步功能,自动去除掉非首个空会话,避免多个空会话在中间,更方便管理 2024-09-06 20:59:53 +08:00
lloydzhou
6f3d7530b9 Merge remote-tracking branch 'origin/main' into website 2024-09-06 20:18:21 +08:00
织梦人
6dc868154d fix: 优化云同步功能,使access配置按更新时间合并,解决自定义模型配置在同步后丢失的问题 2024-09-05 21:52:25 +08:00
织梦人
ccacfec918 feat: 优化聊天窗口,使支持复制会话 2024-09-05 21:19:03 +08:00
GH Action - Upstream Sync
c204031ea7 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-09-05 01:04:08 +00:00
GH Action - Upstream Sync
2bf72d0324 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-08-30 01:03:42 +00:00
GH Action - Upstream Sync
e8c7ac0c45 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-08-28 01:02:59 +00:00
GH Action - Upstream Sync
0638db146e Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-08-25 01:06:24 +00:00
GH Action - Upstream Sync
2d68f179d7 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-08-22 01:02:16 +00:00
GH Action - Upstream Sync
f1d69cb312 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-08-21 01:00:45 +00:00
李超
31baa10363 fix: 解决会话列表按最新操作时间倒序排序,当前会话判断失败的bug 2024-08-20 22:23:17 +08:00
李超
2fdb35bcc8 fix: 解决会话列表按最新操作时间倒序排序,当前会话判断失败的bug 2024-08-20 22:21:38 +08:00
李超
5c51fd2ed8 feat: 优化会话列表按最后更新时间倒序排序,更方便查看与管理 2024-08-20 21:19:31 +08:00
李超
d0b7ddc1d6 feat: 优化会话列表按最后更新时间倒序排序,更方便查看与管理 2024-08-20 21:18:28 +08:00
GH Action - Upstream Sync
0745b6498d Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-08-20 01:00:40 +00:00
织梦人
fc97c4b06f 更新docker.yml,使image名自适应,不影响主仓库
(cherry picked from commit fdb89af355)
2024-08-18 21:20:20 +08:00
织梦人
fdb89af355 更新docker.yml,使image名自适应,不影响主仓库 2024-08-18 21:19:06 +08:00
织梦人
e515f0f957 更新docker.yml, 修改自动编译的镜像为自己的账号
(cherry picked from commit b2336f5ed9)
2024-08-18 20:23:18 +08:00
织梦人
31f282970b Merge remote-tracking branch 'up/website' into website 2024-08-18 20:22:30 +08:00
织梦人
b2336f5ed9 更新docker.yml, 修改自动编译的镜像为自己的账号 2024-08-18 19:55:55 +08:00
GH Action - Upstream Sync
0a6ddda992 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-08-17 00:59:04 +00:00
lloydzhou
5e1064a5c8 Merge branch 'main' into website 2024-08-16 16:58:30 +08:00
李超
2ee2d50ae6 Merge remote-tracking branch 'up/website' into website 2024-08-15 22:59:03 +08:00
李超
eae593d660 feat: Add automatic data synchronization settings and implementation, enabling auto-sync after completing replies or deleting conversations 2024-08-15 22:42:23 +08:00
织梦人
621b1480c2 Merge branch 'ChatGPTNextWeb:main' into main 2024-08-15 22:41:31 +08:00
李超
4b22aaf979 feat: Add automatic data synchronization settings and implementation, enabling auto-sync after completing replies or deleting conversations 2024-08-15 22:39:30 +08:00
GH Action - Upstream Sync
93bfb55822 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-08-14 01:01:30 +00:00
李超
648e60028d feat: The cloud synchronization feature is enhanced to support the synchronization of deleted conversations and deleted messages 2024-08-08 22:02:15 +08:00
李超
4f876f3e65 Merge tag 'v2.14.1' into website
# Conflicts:
#	app/components/chat.tsx
#	app/utils.ts
2024-08-08 12:48:36 +08:00
lloydzhou
faac0d9817 Merge remote-tracking branch 'origin/main' into website 2024-08-06 22:45:16 +08:00
Dogtiti
1287e39cc6 feat: run test before build 2024-08-06 19:24:47 +08:00
Dogtiti
1ef2aa35e9 feat: jest 2024-08-06 18:03:27 +08:00
李超
22c79595fb feat: The cloud synchronization feature is enhanced to support the synchronization of deleted conversations and deleted messages 2024-08-03 20:53:36 +08:00
李超
5065091b74 fix: Fixed the issue that WebDAV synchronization could not check the status and failed during the first backup
(cherry picked from commit 716899c030)
2024-08-03 12:41:36 +08:00
李超
22f61295bc fix: Fixed an issue where the sample of the reply content was displayed out of order
(cherry picked from commit 8498cadae8)
2024-08-03 12:41:36 +08:00
lloydzhou
c440637ad0 Merge remote-tracking branch 'origin/main' into website 2024-07-27 01:32:47 +08:00
lloydzhou
284d33bcdf Merge remote-tracking branch 'origin/main' into website 2024-07-19 18:37:32 +08:00
lloydzhou
d9573973ca Merge remote-tracking branch 'origin/main' into website 2024-07-13 21:31:15 +08:00
fred-bf
cd354cf045 Merge pull request #4685 from ChatGPTNextWeb/main
feat: update upstream
2024-05-14 17:40:46 +08:00
butterfly
4d6b981a54 bugfix: Delete the escapeDollarNumber function, which causes errors in rendering a latex string 2024-03-26 11:43:55 +08:00
fred-bf
1cce87acaa Merge pull request #4181 from ChatGPTNextWeb/main
merge main
2024-03-01 11:10:11 +08:00
fred-bf
78c4084501 Merge pull request #4148 from ChatGPTNextWeb/main
feat: catch up latest commit
2024-02-27 10:43:15 +08:00
Fred Liang
1d0a40b9e8 chore: low the google safety setting to avoid unexpected blocking 2023-12-31 19:50:06 +08:00
45 changed files with 2813 additions and 372 deletions

View File

@@ -19,26 +19,26 @@ jobs:
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
-
name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: yidadaa/chatgpt-next-web
images: ${{ secrets.DOCKER_USERNAME }}/chatgpt-next-web
tags: |
type=raw,value=latest
type=ref,event=tag
-
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
-
name: Build and push Docker image
uses: docker/build-push-action@v4
with:
@@ -49,4 +49,4 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -12,15 +12,18 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。
[![Saas][Saas-image]][saas-url]
[![Web][Web-image]][web-url]
[![Windows][Windows-image]][download-url]
[![MacOS][MacOS-image]][download-url]
[![Linux][Linux-image]][download-url]
[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[NextChatAI](https://nextchat.dev/chat) / [网页版](https://app.nextchat.dev) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[saas-url]: https://nextchat.dev/chat?utm_source=readme
[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
[web-url]: https://app.nextchat.dev/
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
@@ -60,7 +63,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
企业版咨询: **business@nextchat.dev**
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
<img width="300" src="https://github.com/user-attachments/assets/3d4305ac-6e95-489e-884b-51d51db5f692">
## Features
@@ -97,6 +100,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
## What's New
- 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 Now supports Artifacts & SD
- 🚀 v2.10.1 support Google Gemini Pro model.
@@ -134,6 +138,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
## 最新动态
- 🚀 v2.15.4 客户端支持Tauri本地直接调用大模型API更安全[#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
@@ -172,7 +177,7 @@ We recommend that you follow the steps below to re-deploy:
### Enable Automatic Updates
> If you encounter a failure of Upstream Sync execution, please manually sync fork once.
> If you encounter a failure of Upstream Sync execution, please [manually update code](./README.md#manually-updating-code).
After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
@@ -329,9 +334,9 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
User `-all` to disable all default models, `+all` to enable all default models.
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name.
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list.
> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list.
For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
> Example: `+gpt-3.5-turbo@Azure=gpt35` will show option `gpt35(Azure)` in model list.
> If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option in model list.
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.

View File

@@ -8,7 +8,7 @@
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
[企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) /[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
@@ -54,7 +54,7 @@
### 打开自动更新
> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次!
> 如果你遇到了 Upstream Sync 执行错误,请[手动 Sync Fork 一次](./README_CN.md#手动更新代码)
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows并启用 Upstream Sync Action启用之后即可开启每小时定时自动更新
@@ -216,9 +216,9 @@ ByteDance Api Url.
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
在Azure的模式下支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
> 如果你只能使用Azure模式那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
在Azure的模式下支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
> 如果你只能使用Azure模式那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
在ByteDance的模式下支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项

View File

@@ -5,7 +5,7 @@
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
[企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
@@ -54,7 +54,7 @@
### 自動更新を開く
> Upstream Sync の実行エラーが発生した場合は、手動で Sync Fork してください!
> Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください!
プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
@@ -207,8 +207,8 @@ ByteDance API の URL。
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。

View File

@@ -23,7 +23,8 @@ export async function handle(
});
}
const bearToken = req.headers.get("Authorization") ?? "";
const bearToken =
req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || "";
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const apiKey = token ? token : serverConfig.googleApiKey;
@@ -91,8 +92,8 @@ async function request(req: NextRequest, apiKey: string) {
},
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}?key=${apiKey}${
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : ""
const fetchUrl = `${baseUrl}${path}${
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
}`;
console.log("[Fetch Url] ", fetchUrl);
@@ -100,6 +101,9 @@ async function request(req: NextRequest, apiKey: string) {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"x-goog-api-key":
req.headers.get("x-goog-api-key") ||
(req.headers.get("Authorization") ?? "").replace("Bearer ", ""),
},
method: req.method,
body: req.body,

View File

@@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth";
import { requestOpenai } from "./common";
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
function getModels(remoteModelRes: OpenAIListModelResponse) {
const config = getServerSideConfig();
@@ -34,7 +34,7 @@ export async function handle(
const subpath = params.path.join("/");
if (!ALLOWD_PATH.has(subpath)) {
if (!ALLOWED_PATH.has(subpath)) {
console.log("[OpenAI Route] forbidden path ", subpath);
return NextResponse.json(
{

View File

@@ -231,7 +231,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
function getConfig() {
const modelConfig = chatStore.currentSession().mask.modelConfig;
const isGoogle = modelConfig.providerName == ServiceProvider.Google;
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
@@ -272,7 +272,13 @@ export function getHeaders(ignoreHeaders: boolean = false) {
}
function getAuthHeader(): string {
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
return isAzure
? "api-key"
: isAnthropic
? "x-api-key"
: isGoogle
? "x-goog-api-key"
: "Authorization";
}
const {
@@ -283,14 +289,15 @@ export function getHeaders(ignoreHeaders: boolean = false) {
apiKey,
isEnabledAccessControl,
} = getConfig();
// when using google api in app, not set auth header
if (isGoogle && clientConfig?.isApp) return headers;
// when using baidu api in app, not set auth header
if (isBaidu && clientConfig?.isApp) return headers;
const authHeader = getAuthHeader();
const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic);
const bearerToken = getBearerToken(
apiKey,
isAzure || isAnthropic || isGoogle,
);
if (bearerToken) {
headers[authHeader] = bearerToken;

View File

@@ -23,6 +23,7 @@ import {
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
@@ -178,6 +179,7 @@ export class QwenApi implements LLMApi {
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);

View File

@@ -8,7 +8,7 @@ import {
ChatMessageTool,
} from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { DEFAULT_API_HOST } from "@/app/constant";
import { ANTHROPIC_BASE_URL } from "@/app/constant";
import { getMessageTextContent, isVisionModel } from "@/app/utils";
import { preProcessImageContent, stream } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
@@ -388,9 +388,7 @@ export class ClaudeApi implements LLMApi {
if (baseUrl.trim().length === 0) {
const isApp = !!getClientConfig()?.isApp;
baseUrl = isApp
? DEFAULT_API_HOST + "/api/proxy/anthropic"
: ApiPath.Anthropic;
baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {

View File

@@ -24,6 +24,7 @@ import {
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
@@ -197,6 +198,7 @@ export class ErnieApi implements LLMApi {
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);

View File

@@ -23,6 +23,7 @@ import {
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
@@ -165,6 +166,7 @@ export class DoubaoApi implements LLMApi {
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);

View File

@@ -7,21 +7,26 @@ import {
LLMUsage,
SpeechOptions,
} from "../api";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { DEFAULT_API_HOST } from "@/app/constant";
import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
ChatMessageTool,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import { getClientConfig } from "@/app/config/client";
import { GEMINI_BASE_URL } from "@/app/constant";
import {
getMessageTextContent,
getMessageImages,
isVisionModel,
} from "@/app/utils";
import { preProcessImageContent } from "@/app/utils/chat";
import { nanoid } from "nanoid";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class GeminiProApi implements LLMApi {
path(path: string): string {
@@ -34,7 +39,7 @@ export class GeminiProApi implements LLMApi {
const isApp = !!getClientConfig()?.isApp;
if (baseUrl.length === 0) {
baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
@@ -48,10 +53,6 @@ export class GeminiProApi implements LLMApi {
let chatPath = [baseUrl, path].join("/");
chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
// if chatPath.startsWith('http') then add key in query string
if (chatPath.startsWith("http") && accessStore.googleApiKey) {
chatPath += `&key=${accessStore.googleApiKey}`;
}
return chatPath;
}
extractMessage(res: any) {
@@ -181,114 +182,81 @@ export class GeminiProApi implements LLMApi {
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let finished = false;
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
chatPath,
requestPayload,
getHeaders(),
// @ts-ignore
[{ functionDeclarations: tools.map((tool) => tool.function) }],
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const chunkJson = JSON.parse(text);
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
}
};
// animate response to make it looks smooth
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
finish();
return;
}
if (remainText.length > 0) {
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
const fetchText = remainText.slice(0, fetchCount);
responseText += fetchText;
remainText = remainText.slice(fetchCount);
options.onUpdate?.(responseText, fetchText);
}
requestAnimationFrame(animateResponseText);
}
// start animaion
animateResponseText();
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[Gemini] request response content type: ",
contentType,
const functionCall = chunkJson?.candidates
?.at(0)
?.content.parts.at(0)?.functionCall;
if (functionCall) {
const { name, args } = functionCall;
runTools.push({
id: nanoid(),
type: "function",
function: {
name,
arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
},
});
}
return chunkJson?.candidates?.at(0)?.content.parts.at(0)?.text;
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.contents?.splice(
// @ts-ignore
requestPayload?.contents?.length,
0,
{
role: "model",
parts: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
functionCall: {
name: tool?.function?.name,
args: JSON.parse(tool?.function?.arguments as string),
},
}),
),
},
// @ts-ignore
...toolCallResult.map((result) => ({
role: "function",
parts: [
{
functionResponse: {
name: result.name,
response: {
name: result.name,
content: result.content, // TODO just text content...
},
},
},
],
})),
);
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const delta = apiClient.extractMessage(json);
if (delta) {
remainText += delta;
}
const blockReason = json?.promptFeedback?.blockReason;
if (blockReason) {
// being blocked
console.log(`[Google] [Safety Ratings] result:`, blockReason);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
options,
);
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);

View File

@@ -1,7 +1,7 @@
"use client";
import {
ApiPath,
DEFAULT_API_HOST,
IFLYTEK_BASE_URL,
Iflytek,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
@@ -22,6 +22,7 @@ import {
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
import { RequestPayload } from "./openai";
@@ -40,7 +41,7 @@ export class SparkApi implements LLMApi {
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.Iflytek;
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
@@ -149,6 +150,7 @@ export class SparkApi implements LLMApi {
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);

View File

@@ -2,7 +2,7 @@
// azure and openai, using same models. so using same LLMApi.
import {
ApiPath,
DEFAULT_API_HOST,
MOONSHOT_BASE_URL,
Moonshot,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
@@ -40,7 +40,7 @@ export class MoonshotApi implements LLMApi {
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.Moonshot;
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {

View File

@@ -2,7 +2,7 @@
// azure and openai, using same models. so using same LLMApi.
import {
ApiPath,
DEFAULT_API_HOST,
OPENAI_BASE_URL,
DEFAULT_MODELS,
OpenaiPath,
Azure,
@@ -98,7 +98,7 @@ export class ChatGPTApi implements LLMApi {
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {

View File

@@ -1,5 +1,5 @@
"use client";
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { ApiPath, TENCENT_BASE_URL, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
@@ -22,6 +22,7 @@ import mapKeys from "lodash-es/mapKeys";
import mapValues from "lodash-es/mapValues";
import isArray from "lodash-es/isArray";
import isObject from "lodash-es/isObject";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
@@ -70,9 +71,7 @@ export class HunyuanApi implements LLMApi {
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
baseUrl = isApp
? DEFAULT_API_HOST + "/api/proxy/tencent"
: ApiPath.Tencent;
baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
}
if (baseUrl.endsWith("/")) {
@@ -179,6 +178,7 @@ export class HunyuanApi implements LLMApi {
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);

View File

@@ -35,6 +35,7 @@ export function useCommand(commands: Commands = {}) {
interface ChatCommands {
new?: Command;
newm?: Command;
copy?: Command;
next?: Command;
prev?: Command;
clear?: Command;

View File

@@ -70,6 +70,7 @@ import {
getMessageImages,
isVisionModel,
isDalle3,
removeOutdatedEntries,
showPlugins,
safeLocalStorage,
} from "../utils";
@@ -984,6 +985,7 @@ function _Chat() {
const chatCommands = useChatCommand({
new: () => chatStore.newSession(),
newm: () => navigate(Path.NewChat),
copy: () => chatStore.copySession(),
prev: () => chatStore.nextSession(-1),
next: () => chatStore.nextSession(1),
clear: () =>
@@ -1115,10 +1117,20 @@ function _Chat() {
};
const deleteMessage = (msgId?: string) => {
chatStore.updateCurrentSession(
(session) =>
(session.messages = session.messages.filter((m) => m.id !== msgId)),
);
chatStore.updateCurrentSession((session) => {
session.deletedMessageIds &&
removeOutdatedEntries(session.deletedMessageIds);
session.messages = session.messages.filter((m) => {
if (m.id !== msgId) {
return true;
}
if (!session.deletedMessageIds) {
session.deletedMessageIds = {} as Record<string, number>;
}
session.deletedMessageIds[m.id] = Date.now();
return false;
});
});
};
const onDelete = (msgId: string) => {
@@ -1815,6 +1827,7 @@ function _Chat() {
{message?.tools?.map((tool) => (
<div
key={tool.id}
title={tool?.errorMsg}
className={styles["chat-message-tool"]}
>
{tool.isError === false ? (

View File

@@ -207,23 +207,6 @@ function CustomCode(props: { children: any; className?: string }) {
);
}
function escapeDollarNumber(text: string) {
let escapedText = "";
for (let i = 0; i < text.length; i += 1) {
let char = text[i];
const nextChar = text[i + 1] || " ";
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
char = "\\$";
}
escapedText += char;
}
return escapedText;
}
function escapeBrackets(text: string) {
const pattern =
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
@@ -252,7 +235,7 @@ function tryWrapHtmlCode(text: string) {
},
)
.replace(
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
},
@@ -261,7 +244,7 @@ function tryWrapHtmlCode(text: string) {
function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(() => {
return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
return tryWrapHtmlCode(escapeBrackets(props.content));
}, [props.content]);
return (

View File

@@ -360,6 +360,21 @@ function SyncConfigModal(props: { onClose?: () => void }) {
</select>
</ListItem>
<ListItem
title={Locale.Settings.Sync.Config.EnableAutoSync.Title}
subTitle={Locale.Settings.Sync.Config.EnableAutoSync.SubTitle}
>
<input
type="checkbox"
checked={syncStore.enableAutoSync}
onChange={(e) => {
syncStore.update(
(config) => (config.enableAutoSync = e.currentTarget.checked),
);
}}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Sync.Config.Proxy.Title}
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}

View File

@@ -11,7 +11,6 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export const STABILITY_BASE_URL = "https://api.stability.ai";
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
export const OPENAI_BASE_URL = "https://api.openai.com";
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";

View File

@@ -62,6 +62,7 @@ const cn = {
Commands: {
new: "新建聊天",
newm: "从面具新建聊天",
copy: "复制当前聊天",
next: "下一个聊天",
prev: "上一个聊天",
clear: "清除上下文",
@@ -232,6 +233,10 @@ const cn = {
Title: "同步类型",
SubTitle: "选择喜爱的同步服务器",
},
EnableAutoSync: {
Title: "自动同步设置",
SubTitle: "在回复完成或删除消息后自动同步数据",
},
Proxy: {
Title: "启用代理",
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",

View File

@@ -63,6 +63,7 @@ const en: LocaleType = {
Commands: {
new: "Start a new chat",
newm: "Start a new chat with mask",
copy: "Copy the current Chat",
next: "Next Chat",
prev: "Previous Chat",
clear: "Clear Context",
@@ -234,6 +235,11 @@ const en: LocaleType = {
Title: "Sync Type",
SubTitle: "Choose your favorite sync service",
},
EnableAutoSync: {
Title: "Auto Sync Settings",
SubTitle:
"Automatically synchronize data after replying or deleting messages",
},
Proxy: {
Title: "Enable CORS Proxy",
SubTitle: "Enable a proxy to avoid cross-origin restrictions",

View File

@@ -8,12 +8,12 @@ const tw = {
Error: {
Unauthorized: isApp
? `😆 對話遇到了一些問題,不用慌:
\\ 1⃣ 想要零配置開箱即用,[點這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ 如果你想消耗自己的 OpenAI 資源,點[這裡](/#/settings)修改設定 ⚙️`
\\ 1⃣ 想要無須設定開箱即用,[點這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ 如果你想消耗自己的 OpenAI 資源,點[這裡](/#/settings)修改設定 ⚙️`
: `😆 對話遇到了一些問題,不用慌:
\ 1⃣ 想要零配置開箱即用,[點這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ 如果你正在使用私有部署版本,點[這裡](/#/auth)輸入訪問秘鑰 🔑
\ 3⃣ 如果你想消耗自己的 OpenAI 資源,點[這裡](/#/settings)修改設定 ⚙️
\ 1⃣ 想要無須設定開箱即用,[點這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ 如果你正在使用私有部署版本,點[這裡](/#/auth)輸入存取金鑰 🔑
\ 3⃣ 如果你想消耗自己的 OpenAI 資源,點[這裡](/#/settings)修改設定 ⚙️
`,
},
@@ -25,9 +25,9 @@ const tw = {
Confirm: "確認",
Later: "稍候再說",
Return: "返回",
SaasTips: "配置太麻煩,想要立即使用",
SaasTips: "設定太麻煩,想要立即使用",
TopTips:
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新模型",
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新的大型語言模型",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 則對話`,
@@ -53,8 +53,8 @@ const tw = {
PinToastAction: "檢視",
Delete: "刪除",
Edit: "編輯",
RefreshTitle: "刷新標題",
RefreshToast: "已發送刷新標題請求",
RefreshTitle: "重新整理標題",
RefreshToast: "已傳送重新整理標題請求",
},
Commands: {
new: "新建聊天",
@@ -95,10 +95,10 @@ const tw = {
IsContext: "預設提示詞",
ShortcutKey: {
Title: "鍵盤快捷方式",
newChat: "開新聊天",
newChat: "開新聊天",
focusInput: "聚焦輸入框",
copyLastMessage: "複製最後一個回覆",
copyLastCode: "複製最後一個代碼塊",
copyLastCode: "複製最後一個程式碼區塊",
showShortcutKey: "顯示快捷方式",
},
},
@@ -174,9 +174,9 @@ const tw = {
SubTitle: "聊天內容的字型大小",
},
FontFamily: {
Title: "聊天字",
SubTitle: "聊天內容的字,若空則用全局默認字體",
Placeholder: "字名稱",
Title: "聊天字",
SubTitle: "聊天內容的字,若空則用全域預設字型",
Placeholder: "字名稱",
},
InjectSystemPrompts: {
Title: "匯入系統提示",
@@ -301,8 +301,8 @@ const tw = {
Title: "使用 NextChat AI",
Label: "(性價比最高的方案)",
SubTitle:
"由 NextChat 官方維護,零配置開箱即用,支 OpenAI o1、GPT-4o、Claude-3.5 等最新模型",
ChatNow: "立刻對話",
"由 NextChat 官方維護,無須設定開箱即用,支 OpenAI o1、GPT-4o、Claude-3.5 等最新的大型語言模型",
ChatNow: "立刻開始對話",
},
AccessCode: {
@@ -485,18 +485,18 @@ const tw = {
},
},
SearchChat: {
Name: "搜",
Name: "搜",
Page: {
Title: "搜聊天記錄",
Search: "輸入搜關鍵詞",
Title: "搜聊天記錄",
Search: "輸入搜關鍵詞",
NoResult: "沒有找到結果",
NoData: "沒有數據",
Loading: "載中",
NoData: "沒有資料",
Loading: "載中",
SubTitle: (count: number) => `找到 ${count} 條結果`,
},
Item: {
View: "查看",
View: "檢視",
},
},
NewChat: {

View File

@@ -1,9 +1,18 @@
import {
ApiPath,
DEFAULT_API_HOST,
GoogleSafetySettingsThreshold,
ServiceProvider,
StoreKey,
ApiPath,
OPENAI_BASE_URL,
ANTHROPIC_BASE_URL,
GEMINI_BASE_URL,
BAIDU_BASE_URL,
BYTEDANCE_BASE_URL,
ALIBABA_BASE_URL,
TENCENT_BASE_URL,
MOONSHOT_BASE_URL,
STABILITY_BASE_URL,
IFLYTEK_BASE_URL,
} from "../constant";
import { getHeaders } from "../client/api";
import { getClientConfig } from "../config/client";
@@ -15,45 +24,25 @@ let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
const isApp = getClientConfig()?.buildMode === "export";
const DEFAULT_OPENAI_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/openai"
: ApiPath.OpenAI;
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
const DEFAULT_GOOGLE_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/google"
: ApiPath.Google;
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
const DEFAULT_ANTHROPIC_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/anthropic"
: ApiPath.Anthropic;
const DEFAULT_ANTHROPIC_URL = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
const DEFAULT_BAIDU_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/baidu"
: ApiPath.Baidu;
const DEFAULT_BAIDU_URL = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
const DEFAULT_BYTEDANCE_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/bytedance"
: ApiPath.ByteDance;
const DEFAULT_BYTEDANCE_URL = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
const DEFAULT_ALIBABA_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/alibaba"
: ApiPath.Alibaba;
const DEFAULT_ALIBABA_URL = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
const DEFAULT_TENCENT_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/tencent"
: ApiPath.Tencent;
const DEFAULT_TENCENT_URL = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
const DEFAULT_MOONSHOT_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/moonshot"
: ApiPath.Moonshot;
const DEFAULT_MOONSHOT_URL = isApp ? MOONSHOT_BASE_URL : ApiPath.Moonshot;
const DEFAULT_STABILITY_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/stability"
: ApiPath.Stability;
const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability;
const DEFAULT_IFLYTEK_URL = isApp
? DEFAULT_API_HOST + "/api/proxy/iflytek"
: ApiPath.Iflytek;
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
const DEFAULT_ACCESS_STATE = {
accessCode: "",
@@ -211,15 +200,18 @@ export const useAccessStore = createPersistStore(
})
.then((res) => res.json())
.then((res) => {
// Set default model from env request
let defaultModel = res.defaultModel ?? "";
if (defaultModel !== "")
DEFAULT_CONFIG.modelConfig.model = defaultModel;
const defaultModel = res.defaultModel ?? "";
if (defaultModel !== "") {
const [model, providerName] = defaultModel.split("@");
DEFAULT_CONFIG.modelConfig.model = model;
DEFAULT_CONFIG.modelConfig.providerName = providerName;
}
return res;
})
.then((res: DangerConfig) => {
console.log("[Config] got config from server", res);
set(() => ({ ...res }));
set(() => ({ lastUpdateTime: Date.now(), ...res }));
})
.catch(() => {
console.error("[Config] failed to fetch config");

View File

@@ -1,4 +1,8 @@
import { getMessageTextContent, trimTopic } from "../utils";
import {
getMessageTextContent,
trimTopic,
removeOutdatedEntries,
} from "../utils";
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
import { nanoid } from "nanoid";
@@ -16,6 +20,9 @@ import {
DEFAULT_SYSTEM_TEMPLATE,
KnowledgeCutOffDate,
StoreKey,
SUMMARIZE_MODEL,
GEMINI_SUMMARIZE_MODEL,
ServiceProvider,
} from "../constant";
import Locale, { getLang } from "../locales";
import { isDalle3, safeLocalStorage } from "../utils";
@@ -23,7 +30,10 @@ import { prettyObject } from "../utils/format";
import { createPersistStore } from "../utils/store";
import { estimateTokenLength } from "../utils/token";
import { ModelConfig, ModelType, useAppConfig } from "./config";
import { useAccessStore } from "./access";
import { collectModelsWithDefaultModel } from "../utils/model";
import { createEmptyMask, Mask } from "./mask";
import { useSyncStore } from "./sync";
const localStorage = safeLocalStorage();
@@ -37,6 +47,7 @@ export type ChatMessageTool = {
};
content?: string;
isError?: boolean;
errorMsg?: string;
};
export type ChatMessage = RequestMessage & {
@@ -74,6 +85,7 @@ export interface ChatSession {
lastUpdate: number;
lastSummarizeIndex: number;
clearContextIndex?: number;
deletedMessageIds?: Record<string, number>;
mask: Mask;
}
@@ -97,11 +109,41 @@ function createEmptySession(): ChatSession {
},
lastUpdate: Date.now(),
lastSummarizeIndex: 0,
deletedMessageIds: {},
mask: createEmptyMask(),
};
}
function getSummarizeModel(
currentModel: string,
providerName: string,
): string[] {
// if it is using gpt-* models, force to use 4o-mini to summarize
if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) {
const configStore = useAppConfig.getState();
const accessStore = useAccessStore.getState();
const allModel = collectModelsWithDefaultModel(
configStore.models,
[configStore.customModels, accessStore.customModels].join(","),
accessStore.defaultModel,
);
const summarizeModel = allModel.find(
(m) => m.name === SUMMARIZE_MODEL && m.available,
);
if (summarizeModel) {
return [
summarizeModel.name,
summarizeModel.provider?.providerName as string,
];
}
}
if (currentModel.startsWith("gemini")) {
return [GEMINI_SUMMARIZE_MODEL, ServiceProvider.Google];
}
return [currentModel, providerName];
}
function countMessages(msgs: ChatMessage[]) {
return msgs.reduce(
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
@@ -153,9 +195,19 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
return output;
}
let cloudSyncTimer: any = null;
function noticeCloudSync(): void {
const syncStore = useSyncStore.getState();
cloudSyncTimer && clearTimeout(cloudSyncTimer);
cloudSyncTimer = setTimeout(() => {
syncStore.autoSync();
}, 500);
}
const DEFAULT_CHAT_STATE = {
sessions: [createEmptySession()],
currentSessionIndex: 0,
deletedSessionIds: {} as Record<string, number>,
lastInput: "",
};
@@ -205,6 +257,28 @@ export const useChatStore = createPersistStore(
});
},
copySession() {
set((state) => {
const { sessions, currentSessionIndex } = state;
const emptySession = createEmptySession();
// copy the session
const curSession = JSON.parse(
JSON.stringify(sessions[currentSessionIndex]),
);
curSession.id = emptySession.id;
curSession.lastUpdate = emptySession.lastUpdate;
const newSessions = [...sessions];
newSessions.splice(0, 0, curSession);
return {
currentSessionIndex: 0,
sessions: newSessions,
};
});
},
moveSession(from: number, to: number) {
set((state) => {
const { sessions, currentSessionIndex: oldIndex } = state;
@@ -267,7 +341,18 @@ export const useChatStore = createPersistStore(
if (!deletedSession) return;
const sessions = get().sessions.slice();
sessions.splice(index, 1);
const deletedSessionIds = { ...get().deletedSessionIds };
removeOutdatedEntries(deletedSessionIds);
const hasDelSessions = sessions.splice(index, 1);
if (hasDelSessions?.length) {
hasDelSessions.forEach((session) => {
if (session.messages.length > 0) {
deletedSessionIds[session.id] = Date.now();
}
});
}
const currentIndex = get().currentSessionIndex;
let nextIndex = Math.min(
@@ -284,19 +369,24 @@ export const useChatStore = createPersistStore(
const restoreState = {
currentSessionIndex: get().currentSessionIndex,
sessions: get().sessions.slice(),
deletedSessionIds: get().deletedSessionIds,
};
set(() => ({
currentSessionIndex: nextIndex,
sessions,
deletedSessionIds,
}));
noticeCloudSync();
showToast(
Locale.Home.DeleteToast,
{
text: Locale.Home.Revert,
onClick() {
set(() => restoreState);
noticeCloudSync();
},
},
5000,
@@ -317,6 +407,24 @@ export const useChatStore = createPersistStore(
return session;
},
sortSessions() {
const currentSession = get().currentSession();
const sessions = get().sessions.slice();
sessions.sort(
(a, b) =>
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
);
const currentSessionIndex = sessions.findIndex((session) => {
return session && currentSession && session.id === currentSession.id;
});
set((state) => ({
currentSessionIndex,
sessions,
}));
},
onNewMessage(message: ChatMessage) {
get().updateCurrentSession((session) => {
session.messages = session.messages.concat();
@@ -324,6 +432,8 @@ export const useChatStore = createPersistStore(
});
get().updateStat(message);
get().summarizeSession();
get().sortSessions();
noticeCloudSync();
},
async onUserInput(content: string, attachImages?: string[]) {
@@ -578,8 +688,14 @@ export const useChatStore = createPersistStore(
return;
}
const providerName = modelConfig.compressProviderName;
const api: ClientApi = getClientApi(providerName);
// if not config compressModel, then using getSummarizeModel
const [model, providerName] = modelConfig.compressModel
? [modelConfig.compressModel, modelConfig.compressProviderName]
: getSummarizeModel(
session.mask.modelConfig.model,
session.mask.modelConfig.providerName,
);
const api: ClientApi = getClientApi(providerName as ServiceProvider);
// remove error messages if any
const messages = session.messages;
@@ -610,7 +726,7 @@ export const useChatStore = createPersistStore(
api.llm.chat({
messages: topicMessages,
config: {
model: modelConfig.compressModel,
model,
stream: false,
providerName,
},
@@ -674,7 +790,8 @@ export const useChatStore = createPersistStore(
config: {
...modelcfg,
stream: true,
model: modelConfig.compressModel,
model,
providerName,
},
onUpdate(message) {
session.memoryPrompt = message;
@@ -727,7 +844,7 @@ export const useChatStore = createPersistStore(
},
{
name: StoreKey.Chat,
version: 3.2,
version: 3.3,
migrate(persistedState, version) {
const state = persistedState as any;
const newState = JSON.parse(
@@ -783,6 +900,14 @@ export const useChatStore = createPersistStore(
config.modelConfig.compressProviderName;
});
}
// revert default summarize model for every session
if (version < 3.3) {
newState.sessions.forEach((s) => {
const config = useAppConfig.getState();
s.mask.modelConfig.compressModel = "";
s.mask.modelConfig.compressProviderName = "";
});
}
return newState as any;
},

View File

@@ -71,8 +71,8 @@ export const DEFAULT_CONFIG = {
sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
compressModel: "gpt-4o-mini" as ModelType,
compressProviderName: "OpenAI" as ServiceProvider,
compressModel: "",
compressProviderName: "",
enableInjectSystemPrompts: true,
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
size: "1024x1024" as DalleSize,
@@ -178,7 +178,7 @@ export const useAppConfig = createPersistStore(
}),
{
name: StoreKey.Config,
version: 4,
version: 4.1,
merge(persistedState, currentState) {
const state = persistedState as ChatConfig | undefined;
@@ -231,7 +231,7 @@ export const useAppConfig = createPersistStore(
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
}
if (version < 4) {
if (version < 4.1) {
state.modelConfig.compressModel =
DEFAULT_CONFIG.modelConfig.compressModel;
state.modelConfig.compressProviderName =

View File

@@ -4,10 +4,10 @@ import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
import { getClientConfig } from "../config/client";
import yaml from "js-yaml";
import { adapter } from "../utils";
import { adapter, getOperationId } from "../utils";
import { useAccessStore } from "./access";
const isApp = getClientConfig()?.isApp;
const isApp = getClientConfig()?.isApp !== false;
export type Plugin = {
id: string;
@@ -116,7 +116,7 @@ export const FunctionToolService = {
return {
type: "function",
function: {
name: o.operationId,
name: getOperationId(o),
description: o.description || o.summary,
parameters: parameters,
},
@@ -124,7 +124,7 @@ export const FunctionToolService = {
}),
funcs: operations.reduce((s, o) => {
// @ts-ignore
s[o.operationId] = function (args) {
s[getOperationId(o)] = function (args) {
const parameters: Record<string, any> = {};
if (o.parameters instanceof Array) {
o.parameters.forEach((p) => {
@@ -139,8 +139,8 @@ export const FunctionToolService = {
} else if (authLocation == "body") {
args[headerName] = tokenValue;
}
// @ts-ignore
return api.client[o.operationId](
// @ts-ignore if o.operationId is null, then using o.path and o.method
return api.client.paths[o.path][o.method](
parameters,
args,
api.axiosConfigDefaults,

View File

@@ -151,7 +151,7 @@ export const usePromptStore = createPersistStore(
if (typeof window === "undefined") {
return;
}
const PROMPT_URL = "./prompts.json";
type PromptList = Array<[string, string]>;

View File

@@ -12,7 +12,6 @@ import { downloadAs, readFromFile } from "../utils";
import { showToast } from "../components/ui-lib";
import Locale from "../locales";
import { createSyncClient, ProviderType } from "../utils/cloud";
import { corsPath } from "../utils/cors";
export interface WebDavConfig {
server: string;
@@ -25,8 +24,9 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
const DEFAULT_SYNC_STATE = {
provider: ProviderType.WebDAV,
enableAutoSync: true,
useProxy: true,
proxyUrl: corsPath(ApiPath.Cors),
proxyUrl: ApiPath.Cors as string,
webdav: {
endpoint: "",
@@ -44,6 +44,8 @@ const DEFAULT_SYNC_STATE = {
lastProvider: "",
};
let lastSyncTime = 0;
export const useSyncStore = createPersistStore(
DEFAULT_SYNC_STATE,
(set, get) => ({
@@ -90,6 +92,16 @@ export const useSyncStore = createPersistStore(
},
async sync() {
if (lastSyncTime && lastSyncTime >= Date.now() - 800) {
return;
}
lastSyncTime = Date.now();
const enableAutoSync = get().enableAutoSync;
if (!enableAutoSync) {
return;
}
const localState = getLocalAppState();
const provider = get().provider;
const config = get()[provider];
@@ -104,9 +116,7 @@ export const useSyncStore = createPersistStore(
);
return;
} else {
const parsedRemoteState = JSON.parse(
await client.get(config.username),
) as AppState;
const parsedRemoteState = JSON.parse(remoteState) as AppState;
mergeAppState(localState, parsedRemoteState);
setLocalAppState(localState);
}
@@ -124,6 +134,14 @@ export const useSyncStore = createPersistStore(
const client = this.getClient();
return await client.check();
},
async autoSync() {
const { lastSyncTime, provider } = get();
const syncStore = useSyncStore.getState();
if (lastSyncTime && syncStore.cloudSync()) {
syncStore.sync();
}
},
}),
{
name: StoreKey.Sync,

View File

@@ -2,8 +2,9 @@ import { useEffect, useState } from "react";
import { showToast } from "./components/ui-lib";
import Locale from "./locales";
import { RequestMessage } from "./client/api";
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
import { ServiceProvider } from "./constant";
// import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
import { fetch as tauriStreamFetch } from "./utils/stream";
export function trimTopic(topic: string) {
// Fix an issue where double quotes still show in the Indonesian language
@@ -273,6 +274,19 @@ export function isDalle3(model: string) {
return "dall-e-3" === model;
}
export function removeOutdatedEntries(
timeMap: Record<string, number>,
): Record<string, number> {
const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
// Delete data from a month ago
Object.keys(timeMap).forEach((id) => {
if (timeMap[id] < oneMonthAgo) {
delete timeMap[id];
}
});
return timeMap;
}
export function showPlugins(provider: ServiceProvider, model: string) {
if (
provider == ServiceProvider.OpenAI ||
@@ -284,6 +298,9 @@ export function showPlugins(provider: ServiceProvider, model: string) {
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
return true;
}
if (provider == ServiceProvider.Google && !model.includes("vision")) {
return true;
}
return false;
}
@@ -292,30 +309,23 @@ export function fetch(
options?: Record<string, unknown>,
): Promise<any> {
if (window.__TAURI__) {
const payload = options?.body || options?.data;
return tauriFetch(url, {
...options,
body:
payload &&
({
type: "Text",
payload,
} as any),
timeout: ((options?.timeout as number) || REQUEST_TIMEOUT_MS) / 1000,
responseType:
options?.responseType == "text" ? ResponseType.Text : ResponseType.JSON,
} as any);
return tauriStreamFetch(url, options);
}
return window.fetch(url, options);
}
export function adapter(config: Record<string, unknown>) {
const { baseURL, url, params, ...rest } = config;
const { baseURL, url, params, data: body, ...rest } = config;
const path = baseURL ? `${baseURL}${url}` : url;
const fetchUrl = params
? `${path}?${new URLSearchParams(params as any).toString()}`
: path;
return fetch(fetchUrl as string, { ...rest, responseType: "text" });
return fetch(fetchUrl as string, { ...rest, body }).then((res) => {
const { status, headers, statusText } = res;
return res
.text()
.then((data: string) => ({ status, statusText, headers, data }));
});
}
export function safeLocalStorage(): {
@@ -377,3 +387,15 @@ export function safeLocalStorage(): {
},
};
}
export function getOperationId(operation: {
operationId?: string;
method: string;
path: string;
}) {
// pattern '^[a-zA-Z0-9_-]+$'
return (
operation?.operationId ||
`${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
);
}

View File

@@ -10,6 +10,7 @@ import {
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "./format";
import { fetch as tauriFetch } from "./stream";
export function compressImage(file: Blob, maxSize: number): Promise<string> {
return new Promise((resolve, reject) => {
@@ -221,7 +222,12 @@ export function stream(
),
)
.then((res) => {
const content = JSON.stringify(res.data);
let content = res.data || res?.statusText;
// hotfix #5614
content =
typeof content === "string"
? content
: JSON.stringify(content);
if (res.status >= 300) {
return Promise.reject(content);
}
@@ -236,10 +242,15 @@ export function stream(
return content;
})
.catch((e) => {
options?.onAfterTool?.({ ...tool, isError: true });
options?.onAfterTool?.({
...tool,
isError: true,
errorMsg: e.toString(),
});
return e.toString();
})
.then((content) => ({
name: tool.function.name,
role: "tool",
content,
tool_call_id: tool.id,
@@ -287,6 +298,7 @@ export function stream(
REQUEST_TIMEOUT_MS,
);
fetchEventSource(chatPath, {
fetch: tauriFetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);

View File

@@ -1,19 +0,0 @@
import { getClientConfig } from "../config/client";
import { DEFAULT_API_HOST } from "../constant";
export function corsPath(path: string) {
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_API_HOST}` : "";
if (baseUrl === "" && path === "") {
return "";
}
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!path.endsWith("/")) {
path += "/";
}
return `${baseUrl}${path}`;
}

107
app/utils/stream.ts Normal file
View File

@@ -0,0 +1,107 @@
// using tauri command to send request
// see src-tauri/src/stream.rs, and src-tauri/src/main.rs
// 1. invoke('stream_fetch', {url, method, headers, body}), get response with headers.
// 2. listen event: `stream-response` multi times to get body
type ResponseEvent = {
id: number;
payload: {
request_id: number;
status?: number;
chunk?: number[];
};
};
type StreamResponse = {
request_id: number;
status: number;
status_text: string;
headers: Record<string, string>;
};
export function fetch(url: string, options?: RequestInit): Promise<any> {
if (window.__TAURI__) {
const {
signal,
method = "GET",
headers: _headers = {},
body = [],
} = options || {};
let unlisten: Function | undefined;
let setRequestId: Function | undefined;
const requestIdPromise = new Promise((resolve) => (setRequestId = resolve));
const ts = new TransformStream();
const writer = ts.writable.getWriter();
let closed = false;
const close = () => {
if (closed) return;
closed = true;
unlisten && unlisten();
writer.ready.then(() => {
writer.close().catch((e) => console.error(e));
});
};
if (signal) {
signal.addEventListener("abort", () => close());
}
// @ts-ignore 2. listen response multi times, and write to Response.body
window.__TAURI__.event
.listen("stream-response", (e: ResponseEvent) =>
requestIdPromise.then((request_id) => {
const { request_id: rid, chunk, status } = e?.payload || {};
if (request_id != rid) {
return;
}
if (chunk) {
writer.ready.then(() => {
writer.write(new Uint8Array(chunk));
});
} else if (status === 0) {
// end of body
close();
}
}),
)
.then((u: Function) => (unlisten = u));
const headers: Record<string, string> = {
Accept: "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
"User-Agent": navigator.userAgent,
};
for (const item of new Headers(_headers || {})) {
headers[item[0]] = item[1];
}
return window.__TAURI__
.invoke("stream_fetch", {
method: method.toUpperCase(),
url,
headers,
// TODO FormData
body:
typeof body === "string"
? Array.from(new TextEncoder().encode(body))
: [],
})
.then((res: StreamResponse) => {
const { request_id, status, status_text: statusText, headers } = res;
setRequestId?.(request_id);
const response = new Response(ts.readable, {
status,
statusText,
headers,
});
if (status >= 300) {
setTimeout(close, 100);
}
return response;
})
.catch((e) => {
console.error("stream error", e);
throw e;
});
}
return window.fetch(url, options);
}

View File

@@ -8,6 +8,7 @@ import { useMaskStore } from "../store/mask";
import { usePromptStore } from "../store/prompt";
import { StoreKey } from "../constant";
import { merge } from "./merge";
import { removeOutdatedEntries } from "@/app/utils";
type NonFunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
@@ -65,7 +66,10 @@ type StateMerger = {
const MergeStates: StateMerger = {
[StoreKey.Chat]: (localState, remoteState) => {
// merge sessions
const currentSession = useChatStore.getState().currentSession();
const localSessions: Record<string, ChatSession> = {};
const localDeletedSessionIds = localState.deletedSessionIds || {};
localState.sessions.forEach((s) => (localSessions[s.id] = s));
remoteState.sessions.forEach((remoteSession) => {
@@ -75,29 +79,98 @@ const MergeStates: StateMerger = {
const localSession = localSessions[remoteSession.id];
if (!localSession) {
// if remote session is new, just merge it
localState.sessions.push(remoteSession);
if (
(localDeletedSessionIds[remoteSession.id] || -1) <
remoteSession.lastUpdate
) {
localState.sessions.push(remoteSession);
}
} else {
// if both have the same session id, merge the messages
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
const localDeletedMessageIds = localSession.deletedMessageIds || {};
remoteSession.messages.forEach((m) => {
if (!localMessageIds.has(m.id)) {
localSession.messages.push(m);
if (
!localDeletedMessageIds[m.id] ||
new Date(localDeletedMessageIds[m.id]).toLocaleString() < m.date
) {
localSession.messages.push(m);
}
}
});
const remoteDeletedMessageIds = remoteSession.deletedMessageIds || {};
localSession.messages = localSession.messages.filter((localMessage) => {
return (
!remoteDeletedMessageIds[localMessage.id] ||
new Date(localDeletedMessageIds[localMessage.id]).toLocaleString() <
localMessage.date
);
});
// sort local messages with date field in asc order
localSession.messages.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
);
localSession.lastUpdate = Math.max(
remoteSession.lastUpdate,
localSession.lastUpdate,
);
const deletedMessageIds = {
...remoteDeletedMessageIds,
...localDeletedMessageIds,
};
removeOutdatedEntries(deletedMessageIds);
localSession.deletedMessageIds = deletedMessageIds;
}
});
const remoteDeletedSessionIds = remoteState.deletedSessionIds || {};
const finalIds: Record<string, any> = {};
localState.sessions = localState.sessions.filter((localSession) => {
// 去除掉重复的会话
if (finalIds[localSession.id]) {
return false;
}
finalIds[localSession.id] = true;
// 去除掉非首个空会话,避免多个空会话在中间,不方便管理
if (
localSession.messages.length === 0 &&
localSession != localState.sessions[0]
) {
return false;
}
// 去除云端删除并且删除时间小于本地修改时间的会话
return (
(remoteDeletedSessionIds[localSession.id] || -1) <=
localSession.lastUpdate
);
});
// sort local sessions with date field in desc order
localState.sessions.sort(
(a, b) =>
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
);
const deletedSessionIds = {
...remoteDeletedSessionIds,
...localDeletedSessionIds,
};
removeOutdatedEntries(deletedSessionIds);
localState.deletedSessionIds = deletedSessionIds;
localState.currentSessionIndex = localState.sessions.findIndex(
(session) => {
return session && currentSession && session.id === currentSession.id;
},
);
return localState;
},
[StoreKey.Prompt]: (localState, remoteState) => {
@@ -153,9 +226,9 @@ export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
remoteState: T,
) {
const localUpdateTime = localState.lastUpdateTime ?? 0;
const remoteUpdateTime = localState.lastUpdateTime ?? 1;
const remoteUpdateTime = remoteState.lastUpdateTime ?? 1;
if (localUpdateTime < remoteUpdateTime) {
if (localUpdateTime >= remoteUpdateTime) {
merge(remoteState, localState);
return { ...remoteState };
} else {

21
jest.config.ts Normal file
View File

@@ -0,0 +1,21 @@
import type { Config } from "jest";
import nextJest from "next/jest.js";
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});
// Add any custom config to be passed to Jest
const config: Config = {
coverageProvider: "v8",
testEnvironment: "jsdom",
testMatch: ["**/*.test.js", "**/*.test.ts", "**/*.test.jsx", "**/*.test.tsx"],
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/$1",
},
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);

2
jest.setup.ts Normal file
View File

@@ -0,0 +1,2 @@
// Learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";

View File

@@ -6,16 +6,18 @@
"mask": "npx tsx app/masks/build.ts",
"mask:watch": "npx watch \"yarn mask\" app/masks",
"dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
"build": "yarn mask && cross-env BUILD_MODE=standalone next build",
"build": "yarn test:ci && yarn mask && cross-env BUILD_MODE=standalone next build",
"start": "next start",
"lint": "next lint",
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
"export": "yarn test:ci && yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
"app:build": "yarn mask && yarn tauri build",
"app:build": "yarn test:ci && yarn mask && yarn tauri build",
"prompts": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install",
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
"test": "jest --watch",
"test:ci": "jest --ci"
},
"dependencies": {
"@fortaine/fetch-event-source": "^3.0.6",
@@ -54,6 +56,9 @@
"devDependencies": {
"@tauri-apps/api": "^1.6.0",
"@tauri-apps/cli": "1.5.11",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.5.12",
"@types/js-yaml": "4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.30",
@@ -69,8 +74,11 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unused-imports": "^3.2.0",
"husky": "^8.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^13.2.2",
"prettier": "^3.0.2",
"ts-node": "^10.9.2",
"tsx": "^4.16.0",
"typescript": "5.2.2",
"watch": "^1.0.2",

52
src-tauri/Cargo.lock generated
View File

@@ -348,9 +348,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.4.0"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
dependencies = [
"serde",
]
@@ -942,9 +942,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
@@ -970,9 +970,9 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-executor"
@@ -987,9 +987,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-lite"
@@ -1008,9 +1008,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
@@ -1019,21 +1019,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.28"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-core",
"futures-io",
@@ -1555,9 +1555,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.3.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@@ -1986,6 +1986,10 @@ checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
name = "nextchat"
version = "0.1.0"
dependencies = [
"bytes",
"futures-util",
"percent-encoding",
"reqwest",
"serde",
"serde_json",
"tauri",
@@ -2281,9 +2285,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "percent-encoding"
version = "2.2.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "phf"
@@ -2545,9 +2549,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.58"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
@@ -3889,9 +3893,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "url"
version = "2.3.1"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",

View File

@@ -37,6 +37,10 @@ tauri = { version = "1.5.4", features = [ "http-all",
"window-unminimize",
] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
percent-encoding = "2.3.1"
reqwest = "0.11.18"
futures-util = "0.3.30"
bytes = "1.7.2"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.

View File

@@ -1,8 +1,11 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod stream;
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![stream::stream_fetch])
.plugin(tauri_plugin_window_state::Builder::default().build())
.run(tauri::generate_context!())
.expect("error while running tauri application");

134
src-tauri/src/stream.rs Normal file
View File

@@ -0,0 +1,134 @@
//
//
use std::time::Duration;
use std::error::Error;
use std::sync::atomic::{AtomicU32, Ordering};
use std::collections::HashMap;
use futures_util::{StreamExt};
use reqwest::Client;
use reqwest::header::{HeaderName, HeaderMap};
static REQUEST_COUNTER: AtomicU32 = AtomicU32::new(0);
#[derive(Debug, Clone, serde::Serialize)]
pub struct StreamResponse {
request_id: u32,
status: u16,
status_text: String,
headers: HashMap<String, String>
}
#[derive(Clone, serde::Serialize)]
pub struct EndPayload {
request_id: u32,
status: u16,
}
#[derive(Clone, serde::Serialize)]
pub struct ChunkPayload {
request_id: u32,
chunk: bytes::Bytes,
}
#[tauri::command]
pub async fn stream_fetch(
window: tauri::Window,
method: String,
url: String,
headers: HashMap<String, String>,
body: Vec<u8>,
) -> Result<StreamResponse, String> {
let event_name = "stream-response";
let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let mut _headers = HeaderMap::new();
for (key, value) in &headers {
_headers.insert(key.parse::<HeaderName>().unwrap(), value.parse().unwrap());
}
// println!("method: {:?}", method);
// println!("url: {:?}", url);
// println!("headers: {:?}", headers);
// println!("headers: {:?}", _headers);
let method = method.parse::<reqwest::Method>().map_err(|err| format!("failed to parse method: {}", err))?;
let client = Client::builder()
.default_headers(_headers)
.redirect(reqwest::redirect::Policy::limited(3))
.connect_timeout(Duration::new(3, 0))
.build()
.map_err(|err| format!("failed to generate client: {}", err))?;
let mut request = client.request(
method.clone(),
url.parse::<reqwest::Url>().map_err(|err| format!("failed to parse url: {}", err))?
);
if method == reqwest::Method::POST || method == reqwest::Method::PUT || method == reqwest::Method::PATCH {
let body = bytes::Bytes::from(body);
// println!("body: {:?}", body);
request = request.body(body);
}
// println!("client: {:?}", client);
// println!("request: {:?}", request);
let response_future = request.send();
let res = response_future.await;
let response = match res {
Ok(res) => {
// get response and emit to client
let mut headers = HashMap::new();
for (name, value) in res.headers() {
headers.insert(
name.as_str().to_string(),
std::str::from_utf8(value.as_bytes()).unwrap().to_string()
);
}
let status = res.status().as_u16();
tauri::async_runtime::spawn(async move {
let mut stream = res.bytes_stream();
while let Some(chunk) = stream.next().await {
match chunk {
Ok(bytes) => {
// println!("chunk: {:?}", bytes);
if let Err(e) = window.emit(event_name, ChunkPayload{ request_id, chunk: bytes }) {
println!("Failed to emit chunk payload: {:?}", e);
}
}
Err(err) => {
println!("Error chunk: {:?}", err);
}
}
}
if let Err(e) = window.emit(event_name, EndPayload{ request_id, status: 0 }) {
println!("Failed to emit end payload: {:?}", e);
}
});
StreamResponse {
request_id,
status,
status_text: "OK".to_string(),
headers,
}
}
Err(err) => {
println!("Error response: {:?}", err.source().expect("REASON").to_string());
StreamResponse {
request_id,
status: 599,
status_text: err.source().expect("REASON").to_string(),
headers: HashMap::new(),
}
}
};
// println!("Response: {:?}", response);
Ok(response)
}

View File

@@ -9,7 +9,7 @@
},
"package": {
"productName": "NextChat",
"version": "2.15.3"
"version": "2.15.4"
},
"tauri": {
"allowlist": {

9
test/sum-module.test.ts Normal file
View File

@@ -0,0 +1,9 @@
function sum(a: number, b: number) {
return a + b;
}
describe("sum module", () => {
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});
});

1970
yarn.lock

File diff suppressed because it is too large Load Diff