ageer 1 ano atrás
pai
commit
279e6abbe3

+ 0 - 4
.env

@@ -3,10 +3,6 @@ VITE_GLOB_API_URL=/api
 
 VITE_APP_API_BASE_URL = http://127.0.0.1:6039/
 
-VITE_MJ_API_BASE_URL = http://127.0.0.1:6039/
-
-VITE_MJ_API_SECRET= 'sk-uMCP3lTg1dQ9L7Xs2bF352Fa2xxx'
-
 # Whether long replies are supported, which may result in higher API fees
 VITE_GLOB_OPEN_LONG_REPLY=false
 

+ 2 - 2
README.md

@@ -1,2 +1,2 @@
-# ruoyi-web
-基于ruoyi-plus实现AI聊天和绘画功能-前端
+# ruoyi-web-pro
+基于ruoyi-plus实现AI聊天和绘画功能

+ 0 - 8
api/session.js

@@ -1,5 +1,4 @@
 module.exports = (req, res) => {
-  console.log('session.js', req.body);
   try {
     let data = req.body.data; 
     let obj ={
@@ -16,13 +15,6 @@ module.exports = (req, res) => {
             ,baiduId : process.env.TJ_BAIDU_ID?? "" 
             ,googleId: process.env.TJ_GOOGLE_ID?? ""
             , notify : process.env.SYS_NOTIFY?? "" 
-            ,disableGpt4 : process.env.DISABLE_GPT4?? "" 
-            ,isWsrv:  process.env.MJ_IMG_WSRV?? "" 
-            ,uploadImgSize: process.env.UPLOAD_IMG_SIZE?? "1" 
-            ,gptUrl : process.env.GPT_URL?? ""
-            ,theme : process.env.SYS_THEME?? "dark"
-            ,isCloseMdPreview : process.env.CLOSE_MD_PREVIEW?true:false
-
         }
     }
     res.writeHead(200).end(

+ 0 - 104
changlog.md

@@ -1,108 +1,4 @@
 # 功能升级日志
-
-#  计划
-# 2.15.7
-- 🐞 修复:mj提交错误提示 规范化
-- 😄 新增:实时语音识别
-
-# 2.15.6
-- 🐞 修复:微信绘图弹窗兼容问题 #177 
-- 😄 新增:环境变量控制 `CLOSE_MD_PREVIEW=1`,是否开启输入MD预览 #124
-- 😄 新增:防爆破验证 相关变量 `AUTH_SECRET_ERROR_COUNT=3` `AUTH_SECRET_ERROR_TIME=10`
-
-# 2.15.5
-- 🐞 修复:授权提示 `请重新输入授权访问密码`
-- 😄 新增:提交版本信息
-- 😄 新增:@触发使用过的GPTs #140
-
-# 2.15.4
-- 🐞 修复:mj 上传文件 服务端授权认证( vercel 暂不支持) #157
-# 2.15.3
-- 🐞 修复:服务端授权认证( vercel 暂不支持) #138
-
-# 2.15.2
-- 😄 新增: 输入预览 增加md功能 #124
-- 🐞 修复: 更新模型logo未变化 #137
-- 😄 新增: 模型 `gpt-3.5-turbo-0125`
-- 😄 新增: MJ 模型 `niji V6`
-
-# 2.15.1
-- 🐞 修复: tts转化在微信内播放有问题 #126 @yoke1990
-- 🐞 修复: 自定义GPTs 在vercel 不可使用 #115
-- 🐞 修复: 针对每一个对话分别设置模型无效 #128 感谢
-- 🔨 优化: MJ 绘画选项组合优化,减少提示词给mj提示错误 #127 感谢 @yoke1990
-- 🔨 优化: 清空同时清空标题 #117  
-- 😄 新增: 环境变量主题 `SYS_THEME` 主题 `light`或者`dark` 默认 `dark` ( 新版本需要 清缓存 ) #123
-
-
-# 2.14.10
-- 😄 新增: TTS 人物设置
-- 😄 新增: 环境变量 `UPLOAD_IMG_SIZE` #27
-- 🔨 优化: GPTs 踩、赞 
-- 🔨 优化: 自定义GPTs
-
-# 2.14.9
-- 😄 新增: 模型 `gpt-4-0125-preview`
-
-# 2.14.8
-- 🐞 修复:垫图图片不能2次使用
-- 😄 新增:标头 列表页
-
-# 2.14.7
-- 😄 新增: 补回`temperature` 和 `top_p` `presence_penalty` `frequency_penalty`设置  #86
-- 😄 优化: 切换模型中  所有设置能随着对话框保存
-- 🐞 修复: 默认画图打开 #99
-
-# 2.14.6
-- 😄 新增: 语言版本`法语` `土耳其语` 感谢 @M4K4R PR
-- 🐞 修复: 手机端token被遮挡  #98
-- 🐞 修复: 环境变量 打开wsrv图片图床 `MJ_IMG_WSRV=1`
-- 🐞 修复: midjourney图片 wsrv图床 bug
-
-# 2.14.5
-- 😄 新增: midjourney wsrv访问图片
-# 2.14.4
-- 🐞 修复:手机端的页面绘画不出图 #59
-- 🐞 修复:midjourney 强制刷新不起作用
-# 2.14.3
-- 😄 新增: 角色自定到会话 #75 #40
-- 😄 新增: 支持one-api部署聊天 https://vercel.ddaiai.com/#/?settings={%22key%22:%22sk-abc%22,%22url%22:%22https://api.openai.com%22}
-
-# 2.14.2
-- 🐞 修复: gpt-4-1106-preview 模型 128000 #66
-- 😄 新增: 录音whisper转文本对话ChatGPT
-- 😄 新增: 对话内容tts转语音
-- 😄 新增: 文件上传支持 `cloudflare r2 存储` 感谢 @xuzhenjun130 PR
-
-# 2.14.1
-- 🐞 修复: gpt-4-1106-preview 模型 128k #66
-- 🐞 修复: 余额显示方式 #61
-- 😄 新增: `DISABLE_GPT4=1` 控制不让用 GPT-4 #65
-- 😄 新增: `OpenAi Api Key 错误` 对话框中出现引导设置
-
-# 2.13.10
-- 🔨 优化: 输入实时计算剩余tokens #63
-- 🐞 修复: 重新获取bug
-
-# 2.13.9
-- 😄 新增: 超链设置
-
-# 2.13.8
-- 🐞 修复: #55 将画图提示词加到2000字
-- 🔨 优化: 上传.加入进度条
-- 😄 新增: midjourney `清设置`按钮
-- 😄 新增: midjourney `自定义变焦`功能
-
-
-# 2.13.7
-- 🐞 修复: #55 名称错误
-- 😄 新增: midjourney shorten 操作
-- 🐞 修复: 手机端无GPTs入口
-
-# 2.13.6
-- 🔨 优化: UI服务端保存
-- 😄 新增: 国际化语言包
-
 ## 2.13.5
 - 🐞 修复: 历史记录未带附件链接
 - 😄 新增: 系统通知 #47

+ 36 - 46
docker-compose/docker-compose.yml

@@ -1,57 +1,47 @@
 version: '3'
 
 services:
-  gptweb:
-    container_name: chatgpt-web-midjourney-proxy
-    image: ydlhero/chatgpt-web-midjourney-proxy # 总是使用latest,更新时重新pull该tag镜像即可
+  app:
+    container_name: chatgpt-web
+    image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可
     ports:
-      - 6050:3002
+      - 3002:3002
     environment:
-      TZ: Asia/Shanghai # 指定时区
-      # 必选
-      OPENAI_API_KEY: sk-xxxx
+      # 二选一
+      OPENAI_API_KEY: 
+      # 二选一
+      OPENAI_ACCESS_TOKEN: 
       # API接口地址,可选,设置 OPENAI_API_KEY 时可用
       OPENAI_API_BASE_URL:
       # API模型,可选,设置 OPENAI_API_KEY 时可用
       OPENAI_API_MODEL:
+      # 反向代理,可选
+      API_REVERSE_PROXY:
       # 访问权限密钥,可选
       AUTH_SECRET_KEY:
-      # midjourney 服务器地址,可选 可用下面的 http://midjourney-proxy:8080
-      MJ_SERVER:
-      # midjourney API密钥,可选
-      MJ_API_SECRET:
-      #API_UPLOADER 是否可以上传 1 可以其他都不可以,可选
-      API_UPLOADER:
-      #HIDE_SERVER 隐藏服务端 1,可选
-      HIDE_SERVER:
-      #自定义模型  CUSTOM_MODELS=-gpt-3.5-turbo-0301,gpt-4.5 不显示 gpt-3.5-turbo-0301 新增加 gpt-4.5,可选
-      CUSTOM_MODELS:
-      #TJ_BAIDU_ID 百度统计ID,可选
-      TJ_BAIDU_ID:
-      #TJ_GOOGLE_ID 谷歌统计ID,可选
-      TJ_GOOGLE_ID:
-      #SYS_NOTIFY 系统通知 支持HTML ,可选
-      SYS_NOTIFY:
-      #FILE_SERVER 文件服务器,可选 可以用下面的 http://fileserver:3012
-      FILE_SERVER: 
-      #DISABLE_GPT4=1 前端限制GPT4调用,可选
-      DISABLE_GPT4:
-      # cloudflare r2 存储 10 GB/月 免费 https://www.cloudflare.com/zh-cn/developer-platform/r2/
-      R2_DOMAIN:
-      R2_BUCKET_NAME:
-      R2_ACCOUNT_ID:
-      R2_KEY_ID:
-      R2_KEY_SECRET:
-      ## UPLOAD_IMG_SIZE gpt-4-version 图片上传大小 单位是兆 注意别带单位 最好别超过10 
-      UPLOAD_IMG_SIZE: 1
-      # GPT_URL=/gpts.json  自定义GPTs模型
-      GPT_URL:
-      # SYS_THEME 主题 theme   light 或者 dark 
-      SYS_THEME: dark
-      #关闭MD预览  CLOSE_MD_PREVIEW=1
-      CLOSE_MD_PREVIEW:
-      #爆破:验证次数 注意: vercel 不支持 nginx 请设置  proxy_set_header   X-Forwarded-For  $remote_addr;
-      AUTH_SECRET_ERROR_COUNT: 3
-      #爆破:验证停留时间 单位分钟 注意: vercel 不支持
-      AUTH_SECRET_ERROR_TIME: 10
-
+      # 每小时最大请求次数,可选,默认无限
+      MAX_REQUEST_PER_HOUR: 0
+      # 超时,单位毫秒,可选
+      TIMEOUT_MS: 60000
+      # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效
+      SOCKS_PROXY_HOST:
+      # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效
+      SOCKS_PROXY_PORT:
+      # Socks代理用户名,可选,和 SOCKS_PROXY_HOST & SOCKS_PROXY_PORT 一起时生效
+      SOCKS_PROXY_USERNAME:
+      # Socks代理密码,可选,和 SOCKS_PROXY_HOST & SOCKS_PROXY_PORT 一起时生效
+      SOCKS_PROXY_PASSWORD:
+      # HTTPS_PROXY 代理,可选
+      HTTPS_PROXY:
+  nginx:
+    container_name: nginx
+    image: nginx:alpine
+    ports:
+      - '80:80'
+    expose:
+      - '80'
+    volumes:
+      - ./nginx/html:/usr/share/nginx/html
+      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
+    links:
+      - app

+ 27 - 0
docker-compose/nginx/nginx.conf

@@ -0,0 +1,27 @@
+server {
+	listen 80;
+	server_name  localhost;
+	charset utf-8;
+	error_page   500 502 503 504  /50x.html;
+	
+	# 防止爬虫抓取
+	if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot")
+	{
+		return 403;
+	}
+	
+	location / {
+			root /usr/share/nginx/html;
+   		try_files $uri /index.html;
+	}
+
+	location /api {
+			proxy_set_header   X-Real-IP $remote_addr; #转发用户IP
+			proxy_pass http://app:3002;
+	}
+
+	proxy_set_header Host $host;
+	proxy_set_header X-Real-IP $remote_addr;
+	proxy_set_header REMOTE-HOST $remote_addr;
+	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+}

+ 84 - 108
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "chatgpt-web-midjourney-proxy",
-  "version": "2.15.4",
+  "version": "2.16.2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "chatgpt-web-midjourney-proxy",
-      "version": "2.15.4",
+      "version": "2.16.2",
       "dependencies": {
         "@traptitech/markdown-it-katex": "^3.6.0",
         "@vueuse/core": "^9.13.0",
@@ -64,9 +64,6 @@
       "dev": true,
       "engines": {
         "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/@ampproject/remapping": {
@@ -2876,11 +2873,7 @@
     "node_modules/@popperjs/core": {
       "version": "2.11.8",
       "resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
-      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/popperjs"
-      }
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     "node_modules/@rollup/plugin-replace": {
       "version": "5.0.2",
@@ -2904,9 +2897,9 @@
       }
     },
     "node_modules/@rollup/pluginutils": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
-      "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
+      "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
       "dev": true,
       "dependencies": {
         "@types/estree": "^1.0.0",
@@ -2917,7 +2910,7 @@
         "node": ">=14.0.0"
       },
       "peerDependencies": {
-        "rollup": "^1.20.0||^2.0.0||^3.0.0"
+        "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
       },
       "peerDependenciesMeta": {
         "rollup": {
@@ -3629,9 +3622,9 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.8.2",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
-      "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+      "version": "8.11.3",
+      "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.11.3.tgz",
+      "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
       "dev": true,
       "bin": {
         "acorn": "bin/acorn"
@@ -4950,9 +4943,6 @@
         "@vueuse/shared": "8.9.4",
         "vue-demi": "*"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      },
       "peerDependencies": {
         "@vue/composition-api": "^1.1.0",
         "vue": "^2.6.0 || ^3.2.0"
@@ -4973,9 +4963,6 @@
       "dependencies": {
         "vue-demi": "*"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      },
       "peerDependencies": {
         "@vue/composition-api": "^1.1.0",
         "vue": "^2.6.0 || ^3.2.0"
@@ -4990,9 +4977,9 @@
       }
     },
     "node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": {
-      "version": "0.14.7",
-      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.7.tgz",
-      "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==",
+      "version": "0.14.6",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz",
+      "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
       "hasInstallScript": true,
       "bin": {
         "vue-demi-fix": "bin/vue-demi-fix.js",
@@ -5001,9 +4988,6 @@
       "engines": {
         "node": ">=12"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      },
       "peerDependencies": {
         "@vue/composition-api": "^1.0.0-rc.1",
         "vue": "^3.0.0-0 || ^2.6.0"
@@ -5017,10 +5001,7 @@
     "node_modules/element-plus/node_modules/@vueuse/metadata": {
       "version": "8.9.4",
       "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-8.9.4.tgz",
-      "integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==",
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      }
+      "integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw=="
     },
     "node_modules/emoji-regex": {
       "version": "9.2.2",
@@ -5862,9 +5843,9 @@
       "dev": true
     },
     "node_modules/eventsource-parser": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
-      "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.1.tgz",
+      "integrity": "sha512-3Ej2iLj6ZnX+5CMxqyUb8syl9yVZwcwm8IIMrOJlF7I51zxOOrRlU3zxSb/6hFbl03ts1ZxHAGJdWLZOLyKG7w==",
       "engines": {
         "node": ">=14.18"
       }
@@ -6166,9 +6147,9 @@
       }
     },
     "node_modules/function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
       "dev": true
     },
     "node_modules/function.prototype.name": {
@@ -6521,6 +6502,18 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/hasown": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.0.tgz",
+      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/he": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -6626,9 +6619,6 @@
       "dev": true,
       "engines": {
         "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/human-signals": {
@@ -6892,15 +6882,12 @@
       }
     },
     "node_modules/is-core-module": {
-      "version": "2.12.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
-      "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
+      "version": "2.13.1",
+      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.13.1.tgz",
+      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
       "dev": true,
       "dependencies": {
-        "has": "^1.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "hasown": "^2.0.0"
       }
     },
     "node_modules/is-date-object": {
@@ -9117,16 +9104,6 @@
       "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
       "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
       "dev": true,
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/postcss/"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
       "dependencies": {
         "lilconfig": "^3.0.0",
         "yaml": "^2.3.4"
@@ -9148,15 +9125,12 @@
       }
     },
     "node_modules/postcss-load-config/node_modules/lilconfig": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.1.tgz",
-      "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.0.0.tgz",
+      "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==",
       "dev": true,
       "engines": {
         "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antonk52"
       }
     },
     "node_modules/postcss-nested": {
@@ -9170,10 +9144,6 @@
       "engines": {
         "node": ">=12.0"
       },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/postcss/"
-      },
       "peerDependencies": {
         "postcss": "^8.2.14"
       }
@@ -9670,20 +9640,17 @@
       "dev": true
     },
     "node_modules/resolve": {
-      "version": "1.22.2",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
-      "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+      "version": "1.22.8",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
       "dev": true,
       "dependencies": {
-        "is-core-module": "^2.11.0",
+        "is-core-module": "^2.13.0",
         "path-parse": "^1.0.7",
         "supports-preserve-symlinks-flag": "^1.0.0"
       },
       "bin": {
         "resolve": "bin/resolve"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/resolve-from": {
@@ -10358,9 +10325,9 @@
       }
     },
     "node_modules/tailwindcss": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.1.tgz",
-      "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.0.tgz",
+      "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
       "dev": true,
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
@@ -13721,9 +13688,9 @@
       }
     },
     "@rollup/pluginutils": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
-      "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
+      "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
       "dev": true,
       "requires": {
         "@types/estree": "^1.0.0",
@@ -14286,9 +14253,9 @@
       }
     },
     "acorn": {
-      "version": "8.8.2",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
-      "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+      "version": "8.11.3",
+      "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.11.3.tgz",
+      "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
       "dev": true
     },
     "acorn-jsx": {
@@ -15242,9 +15209,9 @@
               }
             },
             "vue-demi": {
-              "version": "0.14.7",
-              "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.7.tgz",
-              "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==",
+              "version": "0.14.6",
+              "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz",
+              "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
               "requires": {}
             }
           }
@@ -15871,9 +15838,9 @@
       "dev": true
     },
     "eventsource-parser": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
-      "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA=="
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.1.tgz",
+      "integrity": "sha512-3Ej2iLj6ZnX+5CMxqyUb8syl9yVZwcwm8IIMrOJlF7I51zxOOrRlU3zxSb/6hFbl03ts1ZxHAGJdWLZOLyKG7w=="
     },
     "evtd": {
       "version": "0.2.4",
@@ -16102,9 +16069,9 @@
       "optional": true
     },
     "function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
       "dev": true
     },
     "function.prototype.name": {
@@ -16351,6 +16318,15 @@
         "has-symbols": "^1.0.2"
       }
     },
+    "hasown": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.0.tgz",
+      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.2"
+      }
+    },
     "he": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -16610,12 +16586,12 @@
       "dev": true
     },
     "is-core-module": {
-      "version": "2.12.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
-      "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
+      "version": "2.13.1",
+      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.13.1.tgz",
+      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
       "dev": true,
       "requires": {
-        "has": "^1.0.3"
+        "hasown": "^2.0.0"
       }
     },
     "is-date-object": {
@@ -18198,9 +18174,9 @@
       },
       "dependencies": {
         "lilconfig": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.1.tgz",
-          "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
+          "version": "3.0.0",
+          "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.0.0.tgz",
+          "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==",
           "dev": true
         }
       }
@@ -18590,12 +18566,12 @@
       "dev": true
     },
     "resolve": {
-      "version": "1.22.2",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
-      "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+      "version": "1.22.8",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
       "dev": true,
       "requires": {
-        "is-core-module": "^2.11.0",
+        "is-core-module": "^2.13.0",
         "path-parse": "^1.0.7",
         "supports-preserve-symlinks-flag": "^1.0.0"
       }
@@ -19086,9 +19062,9 @@
       "dev": true
     },
     "tailwindcss": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.1.tgz",
-      "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.0.tgz",
+      "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
       "dev": true,
       "requires": {
         "@alloc/quick-lru": "^5.2.0",

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "chatgpt-web-midjourney-proxy",
-  "version": "2.15.7",
+  "version": "2.16.2",
   "private": false,
   "description": "ChatGPT Web Midjourney Proxy",
   "author": "Dooy <ydlhero@gmail.com>",
@@ -31,6 +31,7 @@
   "dependencies": {
     "@traptitech/markdown-it-katex": "^3.6.0",
     "@vueuse/core": "^9.13.0",
+    "await-to-js": "^3.0.0",
     "eventsource-parser": "^1.1.1",
     "form-data": "^4.0.0",
     "gpt-tokenizer": "^2.1.2",

+ 0 - 20
pnpm-lock.yaml

@@ -13,18 +13,12 @@ dependencies:
   form-data:
     specifier: ^4.0.0
     version: 4.0.0
-  gpt-tokenizer:
-    specifier: ^2.1.2
-    version: 2.1.2
   highlight.js:
     specifier: ^11.7.0
     version: 11.7.0
   html2canvas:
     specifier: ^1.4.1
     version: 1.4.1
-  js-audio-recorder:
-    specifier: ^1.0.7
-    version: 1.0.7
   katex:
     specifier: ^0.16.4
     version: 0.16.4
@@ -4271,12 +4265,6 @@ packages:
       get-intrinsic: 1.2.0
     dev: true
 
-  /gpt-tokenizer@2.1.2:
-    resolution: {integrity: sha512-HSuI5d6uey+c7x/VzQlPfCoGrfLyAc28vxWofKbjR9PJHm0AjQGSWkKw/OJnb+8S1g7nzgRsf0WH3dK+NNWYbg==}
-    dependencies:
-      rfc4648: 1.5.3
-    dev: false
-
   /graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
     dev: true
@@ -4746,10 +4734,6 @@ packages:
     hasBin: true
     dev: true
 
-  /js-audio-recorder@1.0.7:
-    resolution: {integrity: sha512-JiDODCElVHGrFyjGYwYyNi7zCbKk9va9C77w+zCPMmi4C6ix7zsX2h3ddHugmo4dOTOTCym9++b/wVW9nC0IaA==}
-    dev: false
-
   /js-sdsl@4.3.0:
     resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==}
     dev: true
@@ -6023,10 +6007,6 @@ packages:
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
     dev: true
 
-  /rfc4648@1.5.3:
-    resolution: {integrity: sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==}
-    dev: false
-
   /rfdc@1.3.0:
     resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
     dev: true

+ 1 - 1
public/favicon.svg

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 24 24"><path fill="#8f4fc9" d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91a6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9a6.046 6.046 0 0 0 .743 7.097a5.98 5.98 0 0 0 .51 4.911a6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206a5.99 5.99 0 0 0 3.997-2.9a6.056 6.056 0 0 0-.747-7.073M13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081l4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494M3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085l4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646M2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.677l5.815 3.354l-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667m2.01-3.023l-.141-.085l-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5l2.607 1.5v2.999l-2.597 1.5l-2.607-1.5Z"/></svg>
+<svg id="openai-symbol" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"/></svg>

+ 50 - 0
src/api/knowledge.ts

@@ -0,0 +1,50 @@
+import request from '@/utils/request/req';
+export interface KnowledgeReq {
+	id:string; // 知识库id
+	kid:string; // 附件id
+	uid:string;//  用户id  
+	kname:string; // 知识库名称
+	description:string;// 知识库描述 
+}
+
+export interface SimpleGenerate {
+	model: string,
+	randomness: number,
+	stability_boost: number,
+	voiceId: string,
+	text: string
+}
+
+export function createKnowledgeReq(params:KnowledgeReq) {
+	return request({
+		url: '/knowledge/save',
+		method: 'post',
+		data: params,
+	})
+}
+
+export function getKnowledge() {
+	return request({
+		url: '/knowledge/list',
+		method: 'get',
+	})
+}
+
+
+export function getKnowledgeDetail(kid: string) {
+	return request({
+		url: '/knowledge/detail/'+kid,
+		method: 'get',
+	})
+}
+
+
+export function getfragmentList(docId: string) {
+	return request({
+		url: '/knowledge/fragment/list/'+docId,
+		method: 'get',
+	})
+}
+
+
+

+ 401 - 415
src/api/mjapi.ts

@@ -1,432 +1,418 @@
 
- import { gptServerStore, homeStore, useAuthStore } from "@/store";
- import { copyToClip } from "@/utils/copy";
- import { localGet, localSaveAny } from "./mjsave";
- import { t } from "@/locales";
- import { getToken } from "@/store/modules/auth/helper";
-
- export interface gptsType{
-     gid:string
-     name:string
-     logo:string
-     info:string
-     use_cnt?:string
-     bad?:string|number
- }
-  //const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
- export function upImg(file:any   ):Promise<any>
- {
-     const maxSize= homeStore.myData.session.uploadImgSize? (+homeStore.myData.session.uploadImgSize):1
-     return new Promise((h,r)=>{
-         const filename = file.name;
-         if(file.size>(1024*1024 * maxSize)){
-             r(t('mjchat.no1m',{m:maxSize}))
-             return ;
-         }
-         if (! (filename.endsWith('.jpg') ||
-             filename.endsWith('.gif') ||
-             filename.endsWith('.png') ||
-             filename.endsWith('.jpeg') )) {
-             r(t('mjchat.imgExt') );
-             return ;
-         }
-         const reader = new FileReader();
-         // 当读取操作完成时触发该事件
-         //reader.onload = (e:any)=> st.value.fileBase64 = e.target.result;
-         reader.onload = (e:any)=>  h( e.target.result);
-         reader.readAsDataURL(file);
-     })
-     
- }
- 
- export const file2blob= (selectedFile: any  )=>{
-     return new Promise<{blob:Blob,filename:string}>((resolve, reject) => {
-         const reader = new FileReader();
-         mlog('selectedFile', selectedFile )
-         reader.onload = function (event:any ) {
-             // 将文件内容转换为 Blob
-             const blob = new Blob([event.target.result], { type: selectedFile.type });
- 
-             // 在这里可以使用生成的 Blob 对象
-             //console.log(blob);
-             resolve({blob,filename:selectedFile.name });
-         };
-         reader.onerror = (e)=> reject(e);
- 
-         // 开始读取文件
-         reader.readAsArrayBuffer(selectedFile);
-         
-     })
-      
- }
- 
- export const blob2file= ( blob:Blob,fileName:string )=>{
-     const file = new File([blob], fileName, { type: blob.type, lastModified: Date.now() });
-     return file;
- }
- 
- export const  isFileMp3= (filename:string )=>{
-     let arr='.mp3, .mp4, .mpeg, .mpga, .m4a, .wav, .webm'.split(/[, ]+/ig);
-     mlog('fileIsMp3', arr );
-     filename= filename.toLocaleLowerCase();
-     for(let ext of arr ){
-         if(filename.endsWith(ext)) return true;
-     }
-     return false;
- }
- 
- function containsChinese(str:string ) {
-   return false; //11.18 都不需要翻译
- //   var reg = /[\u4e00-\u9fa5]/g; // 匹配中文的正则表达式
- //   return reg.test(str);
- }
- 
- export  async function train( text:string){
- 
-     return new Promise<string>((resolve, reject) => {
- 
- 
-         if( text.trim()  =='') {
-            reject( t('mjchat.placeInput'));
-             return ;
-         }
- 
-         
-         if( !containsChinese(text.trim()) ){
-             resolve( text.trim() );
-             return ;
-         }
-         
-         // myTranslate( text.trim())
-         //     .then((d:any)=>  resolve( d.content.replace(/[?.!]+$/, "")))
-         //     .catch(( )=>   reject('翻译发生错误'))
-         resolve( text.trim() )
-     }) 
- }
- 
- export const mlog = (msg: string, ...args: unknown[]) => {
-     //localStorage.setItem('debug',1 )
-     const logStyles = [
-     // 'padding: 4px 8px',
-     // 'color: #fff',
-     // 'border-radius: 3px',
-     'color:',
-   ].join(';')
-     const debug= localStorage.getItem('debug')
-     if( !debug  ) return ;
-     const style = `${logStyles}${msg.includes('error') ? 'red' : '#dd9089'}`
-     console.log(`%c[mjgpt]`,  style, msg , ...args)
- }
- 
- export const myTrim = (str: string, delimiter: string)=>{
-     // 构建正则表达式,使用动态的定界符
-     const regex = new RegExp(`^${delimiter}+|${delimiter}+$`, 'g');
+import {  gptServerStore, homeStore, useAuthStore } from "@/store";
+import { copyToClip } from "@/utils/copy";
+import { getToken } from "@/store/modules/auth/helper";
+import { localGet, localSaveAny } from "./mjsave";
+import { t } from "@/locales";
+//import { useMessage } from "naive-ui";
+export interface gptsType{
+    gid:string
+    name:string
+    logo:string
+    info:string
+    use_cnt?:string
+    bad?:string|number
+}
+ //const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
+export function upImg(file:any   ):Promise<any>
+{
+    const maxSize= homeStore.myData.session.uploadImgSize? (+homeStore.myData.session.uploadImgSize):1
+    return new Promise((h,r)=>{
+        const filename = file.name;
+        if(file.size>(1024*1024 * maxSize)){
+            r(t('mjchat.no1m',{m:maxSize}))
+            return ;
+        }
+        if (! (filename.endsWith('.jpg') ||
+            filename.endsWith('.gif') ||
+            filename.endsWith('.png') ||
+            filename.endsWith('.jpeg') )) {
+            r(t('mjchat.imgExt') );
+            return ;
+        }
+        const reader = new FileReader();
+        // 当读取操作完成时触发该事件
+        //reader.onload = (e:any)=> st.value.fileBase64 = e.target.result;
+        reader.onload = (e:any)=>  h( e.target.result);
+        reader.readAsDataURL(file);
+    })
+
+}
+
+export const file2blob= (selectedFile: any  )=>{
+    return new Promise<{blob:Blob,filename:string}>((resolve, reject) => {
+        const reader = new FileReader();
+        mlog('selectedFile', selectedFile )
+        reader.onload = function (event:any ) {
+            // 将文件内容转换为 Blob
+            const blob = new Blob([event.target.result], { type: selectedFile.type });
+
+            // 在这里可以使用生成的 Blob 对象
+            //console.log(blob);
+            resolve({blob,filename:selectedFile.name });
+        };
+        reader.onerror = (e)=> reject(e);
+
+        // 开始读取文件
+        reader.readAsArrayBuffer(selectedFile);
+
+    })
+
+}
+
+export const blob2file= ( blob:Blob,fileName:string )=>{
+    const file = new File([blob], fileName, { type: blob.type, lastModified: Date.now() });
+    return file;
+}
+
+export const  isFileMp3= (filename:string )=>{
+    let arr='.mp3, .mp4, .mpeg, .mpga, .m4a, .wav, .webm'.split(/[, ]+/ig);
+    mlog('fileIsMp3', arr );
+    filename= filename.toLocaleLowerCase();
+    for(let ext of arr ){
+        if(filename.endsWith(ext)) return true;
+    }
+    return false;
+}
+
+function containsChinese(str:string ) {
+  return false; //11.18 都不需要翻译
+//   var reg = /[\u4e00-\u9fa5]/g; // 匹配中文的正则表达式
+//   return reg.test(str);
+}
+
+export  async function train( text:string){
+
+    return new Promise<string>((resolve, reject) => {
+
+
+        if( text.trim()  =='') {
+           reject( t('mjchat.placeInput'));
+            return ;
+        }
+
+
+        if( !containsChinese(text.trim()) ){
+            resolve( text.trim() );
+            return ;
+        }
+
+        // myTranslate( text.trim())
+        //     .then((d:any)=>  resolve( d.content.replace(/[?.!]+$/, "")))
+        //     .catch(( )=>   reject('翻译发生错误'))
+        resolve( text.trim() )
+    })
+}
+
+export const mlog = (msg: string, ...args: unknown[]) => {
+    //localStorage.setItem('debug',1 )
+    const logStyles = [
+    // 'padding: 4px 8px',
+    // 'color: #fff',
+    // 'border-radius: 3px',
+    'color:',
+  ].join(';')
+    const debug= localStorage.getItem('debug')
+    if( !debug  ) return ;
+    const style = `${logStyles}${msg.includes('error') ? 'red' : '#dd9089'}`
+    console.log(`%c[mjgpt]`,  style, msg , ...args)
+}
+
+export const myTrim = (str: string, delimiter: string)=>{
+    // 构建正则表达式,使用动态的定界符
+    const regex = new RegExp(`^${delimiter}+|${delimiter}+$`, 'g');
+
+    // 使用正则表达式去除字符串两端的定界符
+    return str.replace(regex, '');
+}
+
+function getHeaderApiSecret(){
+    if(!gptServerStore.myData.MJ_API_SECRET){
+        const authStore = useAuthStore()
+        if( authStore.token ) return { 'x-ptoken':  authStore.token };
+        return {}
+    }
+    return {
+        'mj-api-secret':  import.meta.env.VITE_MJ_API_SECRET
+    }
+}
+
+const getUrl=(url:string)=>{
+    if(url.indexOf('http')==0) return url;
+    return `/api${url}`;
+}
+
+export const mjFetch=(url:string,data?:any)=>{
+    mlog('mjFetch2024', url  );
+    let header = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() };
+    header= {...header,...getHeaderApiSecret() }
+
+    return new Promise<any>((resolve, reject) => {
+        let opt:RequestInit ={method:'GET'}; 
+        opt.headers=header;
+        if(data) {
+            opt.body= JSON.stringify(data) ;
+             opt.method='POST';
+        }
+        fetch(getUrl(url),  opt )
+        .then(d2=>d2.json().then(d=> {
+                if(d2.ok) resolve(d);
+                else{
+                    reject({error: d.error??  (d??'Network response was not ok!'),code:'response_fail',url:getUrl(url), status:d2.status })
+                }
+            }).catch(e=>reject({error:e? e.toString() :'json_error',code:'json_error',url:getUrl(url) , status:d2.status  }))
+        ).catch(e=>reject({error:e? e.toString() :'fetch fail',data ,code:'fetch_fail',url:getUrl(url)  }))
+    })
      
-     // 使用正则表达式去除字符串两端的定界符
-     return str.replace(regex, '');
- }
- 
- function getHeaderApiSecret(){
-     if(!gptServerStore.myData.MJ_API_SECRET){
-         const authStore = useAuthStore()
-         if( authStore.token ) return { 'x-ptoken':  authStore.token };
-         return {}
-     }
-     return {
-         'mj-api-secret':  gptServerStore.myData.MJ_API_SECRET
-     }
- }
- 
- const getUrl=(url:string)=>{
-     if(url.indexOf('http')==0) return url;
-     if(gptServerStore.myData.MJ_SERVER){
-         return `${ gptServerStore.myData.MJ_SERVER}${url}`;
-     }
-     return `/mjapi${url}`;
- }
- 
- export const mjFetch=(url:string,data?:any)=>{
-     mlog('mjFetch', url  );
-     let header = {'Content-Type':'application/json'};
-     header= {...header,...getHeaderApiSecret() }
- 
-     return new Promise<any>((resolve, reject) => {
-         let opt:RequestInit ={method:'GET'}; 
-         opt.headers=header;
-         if(data) {
-             opt.body= JSON.stringify(data) ;
-              opt.method='POST';
-         }
-         fetch(getUrl(url),  opt )
-         .then(d=>d.json().then(d=> resolve(d))
-         .catch(e=>reject(e)))
-         .catch(e=>reject(e))
-     })
-      
- }
- 
- export const myFetch=(url:string,data?:any)=>{
-     mlog('mjFetch', url  );
-     let header = {'Content-Type':'application/json'};
-     //header= {...header  }
- 
-     return new Promise<any>((resolve, reject) => {
-         let opt:RequestInit ={method:'GET'}; 
-         opt.headers=header;
-         if(data) {
-             opt.body= JSON.stringify(data) ;
-              opt.method='POST';
-         }
-         fetch(getUrl(url),  opt )
-         .then(d=>d.json().then(d=> resolve(d))
-         .catch(e=>reject(e)))
-         .catch(e=>reject(e))
-     })
-      
- }
- export const my2Fetch=(url:string,data?:any)=>{
-     mlog('mjFetch', url  );
-     let header = {'Content-Type':'application/json'};
-     //header= {...header  }
- 
-     return new Promise<any>((resolve, reject) => {
-         let opt:RequestInit ={method:'GET'}; 
-         opt.headers=header;
-         if(data) {
-             opt.body= JSON.stringify(data) ;
-              opt.method='POST';
-         }
-         fetch((url),  opt )
-         .then(d=>d.json().then(d=> resolve(d))
-         .catch(e=>reject(e)))
-         .catch(e=>reject(e))
-     })
-      
- }
- 
- 
- export const flechTask= ( chat:Chat.Chat)=>{
-     let cnt=0;
-     const check= async ()=>{
-         cnt++;
-         if(!chat.mjID){
-             chat.text +="\n获取失败" ;
-             chat.loading=false;
-             homeStore.setMyData({act:'updateTask', actData:chat });
-             return ;
-         }
-         const ts=  await mjFetch(`/mj/task/${chat.mjID}/fetch`);
-         chat.opt= ts;
-         chat.loading=   (cnt>=99)?false:true; 
-         //chat.progress=ts.progress;
+}
+
+export const myFetch=(url:string,data?:any)=>{
+    //mlog('myFetch', url  );
+    let header = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() };
+    //header= {...header  }
+
+    return new Promise<any>((resolve, reject) => {
+        let opt:RequestInit ={method:'GET'}; 
+        opt.headers=header;
+        if(data) {
+            opt.body= JSON.stringify(data) ;
+             opt.method='POST';
+        }
+        fetch(getUrl(url),  opt )
+        .then(d=>d.json().then(d=> resolve(d))
+        .catch(e=>reject(e)))
+        .catch(e=>reject(e))
+    })
      
-         if(ts.progress && ts.progress== "100%") chat.loading=false;
- 
-         homeStore.setMyData({act:'updateChat', actData:chat });
-         //"NOT_START" //["SUBMITTED","IN_PROGRESS"].indexOf(ts.status)>-1
-         if( ["FAILURE","SUCCESS"].indexOf(ts.status)==-1 && cnt<100 ){
-            
-             setTimeout(() =>   check( ) , 5000 )
-         } 
-         mlog('task', ts.progress,ts, chat.uuid,chat.index  );
-     }
-     check();
- }
- export const subTask= async (data:any, chat:Chat.Chat )=>{
-    let d:any;
-    
+}
+export const my2Fetch=(url:string,data?:any)=>{
+    mlog('mjFetch', url  );
+    let header = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() };
+    //header= {...header  }
+
+    return new Promise<any>((resolve, reject) => {
+        let opt:RequestInit ={method:'GET'}; 
+        opt.headers=header;
+        if(data) {
+            opt.body= JSON.stringify(data) ;
+             opt.method='POST';
+        }
+        fetch((url),  opt )
+        .then(d=>d.json().then(d=> resolve(d))
+        .catch(e=>reject(e)))
+        .catch(e=>reject(e))
+    })
+}
+
+export const flechTask= ( chat:Chat.Chat)=>{
+    let cnt=0;
+    const check= async ()=>{
+        cnt++;
+        if(!chat.mjID){
+            chat.text +="\n获取失败" ;
+            chat.loading=false;
+            homeStore.setMyData({act:'updateTask', actData:chat });
+            return ;
+        }
+        const ts=  await mjFetch(`/mj/task/${chat.mjID}/fetch`);
+        chat.opt= ts;
+        chat.loading=   (cnt>=99)?false:true;
+        //chat.progress=ts.progress;
+
+        if(ts.progress && ts.progress== "100%") chat.loading=false;
+
+        homeStore.setMyData({act:'updateChat', actData:chat });
+        //"NOT_START" //["SUBMITTED","IN_PROGRESS"].indexOf(ts.status)>-1
+        if( ["FAILURE","SUCCESS"].indexOf(ts.status)==-1 && cnt<100 ){
+
+            setTimeout(() =>   check( ) , 5000 )
+        }
+        mlog('task', ts.progress,ts, chat.uuid,chat.index  );
+    }
+    check();
+}
+export const subTask= async (data:any, chat:Chat.Chat )=>{
+   let d:any;
+   try{
     //return ;
     if(  data.action &&data.action=='change' ){ //执行变化
-      d=  await mjFetch('/mj/submit/change' , data.data  );
+        d=  await mjFetch('/mj/submit/change' , data.data  );
     }else if( data.action &&data.action=="CustomZoom") { //自定义变焦
-          d =  await mjFetch('/mj/submit/action' , data.data  );
-         if(d.result){
-             let bdata= data.maskData;
-             bdata.taskId= d.result;
-             d=  await mjFetch('/mj/submit/modal' , bdata );
-         }
+            d =  await mjFetch('/mj/submit/action' , data.data  );
+            if(d.result){
+                let bdata= data.maskData;
+                bdata.taskId= d.result;
+                d=  await mjFetch('/mj/submit/modal' , bdata );
+            }
     }else if( data.action &&data.action=='mask') { //局部重绘
-      d =  await mjFetch('/mj/submit/action' , data.data  );
-      if(d.result){
-         let bdata= data.maskData;
-         bdata.taskId= d.result;
-         d=  await mjFetch('/mj/submit/modal' , bdata );
-      }
+        d =  await mjFetch('/mj/submit/action' , data.data  );
+        if(d.result){
+            let bdata= data.maskData;
+            bdata.taskId= d.result;
+            d=  await mjFetch('/mj/submit/modal' , bdata );
+        }
     }else if( data.action &&data.action=='blend') { //blend
-       d=  await mjFetch('/mj/submit/blend' ,  data.data );
-    }else if( data.action &&data.action=='shorten') { //shorten 
-       d=  await mjFetch('/mj/submit/shorten' ,  data.data );
-      //  mlog('mjFetch shorten' , data );
-    }else if( data.action &&data.action=='face') { //换脸 
-       d=  await mjFetch('/mj/insight-face/swap' , data.data  ); 
-       //mlog('换年服务', data.data );
-       //return; 
-    }else if( data.action &&data.action=='img2txt') { //图生文 
-         d=  await mjFetch('/mj/submit/describe' , data.data  ); 
+        d=  await mjFetch('/mj/submit/blend' ,  data.data );
+    }else if( data.action &&data.action=='shorten') { //shorten
+        d=  await mjFetch('/mj/submit/shorten' ,  data.data );
+        //  mlog('mjFetch shorten' , data );
+    }else if( data.action &&data.action=='face') { //换脸
+        d=  await mjFetch('/mj/insight-face/swap' , data.data  );
+        //mlog('换年服务', data.data );
+        //return;
+    }else if( data.action &&data.action=='img2txt') { //图生文
+            d=  await mjFetch('/mj/submit/describe' , data.data  );
     }else if( data.action &&data.action=='changeV2') { //执行动作!
-      d=  await mjFetch('/mj/submit/action' , data.data  );
+        d=  await mjFetch('/mj/submit/action' , data.data  );
     }else {
-     let toData =  {
-         "base64Array":data.fileBase64??[],
-         "notifyHook": "",
-         "prompt": data.drawText,
-         "state": "",
-         botType:'MID_JOURNEY'
-         };
-         if(data.bot && data.bot=='NIJI_JOURNEY'){
-             toData.botType= data.bot;
-         }
-         let headerAi = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() };
-         let opt: RequestInit = { method: 'POST' };
-         let prompt = {prompt:data.drawText};
-         opt.body= JSON.stringify(prompt) ;
-         opt.headers = headerAi;
-
-         // 验证是否是付费用户
-         const response = fetch('/api/mjTask', opt);
-         const movies = JSON.parse(await (await response).text()); 
-         if(movies.code == 500){
-             chat.text='失败!'+"\n```json\n"+movies.msg+"\n```\n";
-             chat.loading=false;
-             homeStore.setMyData({act:'updateChat', actData:chat });
-             console.log("movies======",movies);
-             return
-         }
-
-         d=  await mjFetch('/mj/submit/imagine' ,toData );
-         mlog('submit',d );
-         //return ;
+        let toData =  {
+            "base64Array":data.fileBase64??[],
+            "notifyHook": "",
+            "prompt": data.drawText,
+            "state": "",
+            botType:'MID_JOURNEY'
+            };
+            if(data.bot && data.bot=='NIJI_JOURNEY'){
+                toData.botType= data.bot;
+            }
+            d=  await mjFetch('/mj/submit/imagine' ,toData );
+            mlog('submit',d );
+            //return ;
     }
     if(d.code==21){
         d=  await mjFetch('/mj/submit/modal' , { taskId:d.result} );
     }
-      
-    backOpt(d, chat);
-    
-     
-     //if( chat.uuid &&  chat.index) updateChat(chat.uuid,chat.index, chat)
- }
- const backOpt= (d:any, chat:Chat.Chat )=>{
-      if(d.code==1){
-         chat.text='提交成功!';
-         chat.mjID= d.result;
-         flechTask( chat )
-         chat.loading=true;
-         homeStore.setMyData({act:'updateChat', actData:chat });
-         //chat.m= d.result;
-     }else{
-         chat.text='失败!'+"\n```json\n"+JSON.stringify(d, null, 2)+"\n```\n";
-         chat.loading=false;
-         homeStore.setMyData({act:'updateChat', actData:chat });
-     }
- }
- 
- export const mjSeed= async ( mjid:string )=>{
-      const ts=  await mjFetch(`/mj/task/${mjid}/image-seed`);
-      return ts;
- }
- 
- 
- 
- export const getSeed = async (cchat:Chat.Chat,message:any )=>{
-    // const message = useMessage();
-   // let cchat = props.chat;
-   if(!cchat.mjID ) return ;
-   let seed=0 ;
-   if(cchat.opt?.seed) seed =cchat.opt?.seed;
-   else{
-    try{
-         message.info('获取中...');
-       const res:any  = await mjSeed( cchat.mjID);
-       seed= res.result;
-       if(seed>0 ) {
-        
-         if ( cchat.opt ){
-           cchat.opt.seed = seed;
- 
-            homeStore.setMyData({act:'updateChat', actData:cchat });
-         }
-         message.success('获取成功');
-       }
-       
-    } catch(e){
-       message.error('获取失败')
-    }
-   }
-   mlog('getSeed',seed);
-   if(seed>0 ) {
-     await copyToClip(`${seed}`);
-     message.success('复制seed成功');
+
+     backOpt(d, chat);
+   }catch(e:any ){
+     mlog('mjFetchError', e )
+     chat.text='失败!'+"\n```json\n"+JSON.stringify(e, null, 2)+"\n```\n";
+     chat.loading=false;
+     homeStore.setMyData({act:'updateChat', actData:chat });
    }
-   
- }
- 
- export const getLastVersion=  async ()=>{
-     const url='https://api.github.com/repos/Dooy/chatgpt-web-midjourney-proxy/tags?per_page=1';
-     const a= await myFetch(url);
-     mlog('lastVersion', a ); 
-     return a;
-     
- }
- 
- export const canVisionModel= (model:string)=>{
-     //['gpt-4-all','gpt-4-v'].indexOf(model)==-1 && model.indexOf('gpt-4-gizmo')==-1
-     if( ['gpt-4-all','gpt-4-v','gpt-4v','gpt-3.5-net'].indexOf(model)>-1 ) return true;
-     if(model.indexOf('gpt-4-gizmo')>-1 )return true; 
-     return false;
- }
- 
- export const isTTS= ( model:string )=>{
-     if(model.indexOf('tts-1')===0 )return true; 
-     return false ;
- }
- 
- function isStringOnlyDigits(input: string): boolean {
-     // 使用正则表达式检查字符串是否只包含数字
-     const regex = /^[0-9]+$/;
-     return regex.test(input);
- }
- export const loadGallery  = async ()=>{
-      let localKey= 'mj-list-condition';
-      const d2:any = await localGet(localKey);
-      //mlog('d2',d2 , (Date.now()- d2.ctime));
-      if(d2 && (Date.now()- d2.ctime)<300*1000 ){
- 
-         return d2.d as any[];
+}
+const backOpt= (d:any, chat:Chat.Chat )=>{
+     if(d.code==1){
+        chat.text='提交成功!';
+        chat.mjID= d.result;
+        flechTask( chat )
+        chat.loading=true;
+        homeStore.setMyData({act:'updateChat', actData:chat });
+        //chat.m= d.result;
+    }else{
+        chat.text='失败!'+"\n```json\n"+JSON.stringify(d, null, 2)+"\n```\n";
+        chat.loading=false;
+        homeStore.setMyData({act:'updateChat', actData:chat });
+    }
+}
+
+export const mjSeed= async ( mjid:string )=>{
+     const ts=  await mjFetch(`/mj/task/${mjid}/image-seed`);
+     return ts;
+}
+
+
+
+export const getSeed = async (cchat:Chat.Chat,message:any )=>{
+   // const message = useMessage();
+  // let cchat = props.chat;
+  if(!cchat.mjID ) return ;
+  let seed=0 ;
+  if(cchat.opt?.seed) seed =cchat.opt?.seed;
+  else{
+   try{
+        message.info('获取中...');
+      const res:any  = await mjSeed( cchat.mjID);
+      seed= res.result;
+      if(seed>0 ) {
+
+        if ( cchat.opt ){
+          cchat.opt.seed = seed;
+
+           homeStore.setMyData({act:'updateChat', actData:cchat });
+        }
+        message.success('获取成功');
       }
-      let  d =  await mjFetch(`/mj/gallery`);
-      //mlog('tsList', d.data.list   );
-      if( !d.data.list  ||  d.data.list.length ==0 ) return [];
-      const list =d.data.list as any[];
-      const ids = list.filter(v=> isStringOnlyDigits(v.reqid)).map(v=> +v.reqid ) ;
-      mlog('ids',  ids   );
-      if(ids.length==0) return [];
-      ///mj/task/list-by-condition
-      d=  await mjFetch('/mj/task/list-by-condition',{ids } );
- 
-      if( d.length>0 ) localSaveAny({ctime: Date.now(), d}, localKey);
-      return d as any[] ;
- }
- 
- //从剪贴板中读取文件
- export   function getFileFromClipboard(event:any ){
-     let rz=[];
-     if ( event.clipboardData || event.originalEvent ) {
-         let clipboardData = (event.clipboardData || event.originalEvent.clipboardData);
-         if (clipboardData.items) {
-             let items = clipboardData.items;
-             // mlog('getFileFromClipboard',  items  );
-             for (let i = 0; i < items.length; i++) {
-                 if (items[i].type.indexOf("image") !== -1 || items[i].kind === 'file') {
-                     //rz.push( await fileToBase64(  items[i].getAsFile()) );
-                     //mlog('fff', items[i] );
-                     rz.push( items[i].getAsFile()) 
-                 }
-             }
- 
-         }
+
+   } catch(e){
+      message.error('获取失败')
+   }
+  }
+  mlog('getSeed',seed);
+  if(seed>0 ) {
+    await copyToClip(`${seed}`);
+    message.success('复制seed成功');
+  }
+
+}
+
+export const getLastVersion=  async ()=>{
+    const url='https://api.github.com/repos/Dooy/chatgpt-web-midjourney-proxy/tags?per_page=1';
+    const a= await myFetch(url);
+    mlog('lastVersion', a );
+    return a;
+
+}
+
+export const canVisionModel= (model:string)=>{
+    //['gpt-4-all','gpt-4-v'].indexOf(model)==-1 && model.indexOf('gpt-4-gizmo')==-1
+    if( ['gpt-4-all','gpt-4-v','gpt-4v','gpt-3.5-net'].indexOf(model)>-1 ) return true;
+    if(model.indexOf('gpt-4-gizmo')>-1 || model.indexOf('claude-3-opus')>-1 )return true;
+
+    if(model.indexOf('gpt-4-gizmo')>-1 )return true;
+    return false;
+}
+
+export const isTTS= ( model:string )=>{
+    if(model.indexOf('tts-1')===0 )return true;
+    return false ;
+}
+
+function isStringOnlyDigits(input: string): boolean {
+    // 使用正则表达式检查字符串是否只包含数字
+    const regex = /^[0-9]+$/;
+    return regex.test(input);
+}
+export const loadGallery  = async ()=>{
+     let localKey= 'mj-list-condition';
+     const d2:any = await localGet(localKey);
+     //mlog('d2',d2 , (Date.now()- d2.ctime));
+     if(d2 && (Date.now()- d2.ctime)<300*1000 ){
+
+        return d2.d as any[];
      }
-     //console.log('passs>>' ,rz );
-     return rz;
- }
- 
+     let  d =  await mjFetch(`/mj/gallery`);
+     //mlog('tsList', d.data.list   );
+     if( !d.data.list  ||  d.data.list.length ==0 ) return [];
+     const list =d.data.list as any[];
+     const ids = list.filter(v=> isStringOnlyDigits(v.reqid)).map(v=> +v.reqid ) ;
+     mlog('ids',  ids   );
+     if(ids.length==0) return [];
+     ///mj/task/list-by-condition
+     d=  await mjFetch('/mj/task/list-by-condition',{ids } );
+
+     if( d.length>0 ) localSaveAny({ctime: Date.now(), d}, localKey);
+     return d as any[] ;
+}
+
+//从剪贴板中读取文件
+export   function getFileFromClipboard(event:any ){
+    let rz=[];
+    if ( event.clipboardData || event.originalEvent ) {
+        let clipboardData = (event.clipboardData || event.originalEvent.clipboardData);
+        if (clipboardData.items) {
+            let items = clipboardData.items;
+            // mlog('getFileFromClipboard',  items  );
+            for (let i = 0; i < items.length; i++) {
+                if (items[i].type.indexOf("image") !== -1 || items[i].kind === 'file') {
+                    //rz.push( await fileToBase64(  items[i].getAsFile()) );
+                    //mlog('fff', items[i] );
+                    rz.push( items[i].getAsFile())
+                }
+            }
+
+        }
+    }
+    //console.log('passs>>' ,rz );
+    return rz;
+}

+ 109 - 50
src/api/openapi.ts

@@ -21,6 +21,8 @@ export const KnowledgeCutOffDate: Record<string, string> = {
   "gpt-4-1106-preview": "2023-04",
   "gpt-4-0125-preview": "2023-04",
   "gpt-4-vision-preview": "2023-04",
+  "claude-3-opus-20240229": "2023-08",
+  "claude-3-sonnet-20240229": "2023-08",
 };
 
 const getUrl=(url:string)=>{
@@ -33,7 +35,7 @@ const getUrl=(url:string)=>{
 export const gptGetUrl = getUrl
 export const gptFetch=(url:string,data?:any,opt2?:any )=>{
     mlog('gptFetch', url  );
-    let headers= {'Content-Type':'application/json'}
+    let headers= {'Content-Type':'application/json','Authorization':'Bearer ' + getToken()}
     if(opt2 && opt2.headers ) headers= opt2.headers;
 
     headers={...headers,...getHeaderAuthorization()}
@@ -60,7 +62,7 @@ function uploadR2(file: File) {
 	return new Promise<any>((resolve, reject) => {
 			//预签名
 			axios.post(gptGetUrl("/pre_signed"), { file_name: file.name, content_type: file.type }, {
-					headers: {'Content-Type':'application/json','Authorization':'Bearer ' + getToken()}
+					headers: { 'Content-Type': 'application/json' }
 			}).then(response => {
 							if (response.data.status == "Success") {
 									const signedUrl = response.data.data.up;
@@ -90,38 +92,74 @@ function uploadR2(file: File) {
 	});
 }
 
-export const GptUploader =   ( url:string, FormData:FormData )=>{
-	 if(homeStore.myData.session.isUploadR2){
-			const file = FormData.get('file') as File;
-			return uploadR2(file);
-	 }
+export const GptUploader =   ( _url :string, FormData:FormData )=>{
 
-    // if(gptServerStore.myData.OPENAI_API_BASE_URL){
-    //     return `${ gptServerStore.myData.OPENAI_API_BASE_URL}${url}`;
-    // }
-    url= gptServerStore.myData.UPLOADER_URL? gptServerStore.myData.UPLOADER_URL :  gptGetUrl( url );
-    let headers=   {'Content-Type': 'multipart/form-data' }
-    //
+    //R2上传
+    const upLoaderR2= ()=>{
+        const file = FormData.get('file') as File;
+		return uploadR2(file);
+    }
 
+    //执行上传
+    const uploadNomalDo = (url:string, headers:any)=>{
+        return new Promise<any>((resolve, reject) => {
+                axios.post( url , FormData, {
+                headers
+            }).then(response =>  resolve(response.data )
+            ).catch(error =>reject(error)  );
+        })
+    }
 
+    //除R2外默认流程
+    const uploadNomal= (url:string)=>{
+        url= gptServerStore.myData.UPLOADER_URL? gptServerStore.myData.UPLOADER_URL :  gptGetUrl( url );
+        let headers=   {'Content-Type': 'multipart/form-data' }
+        if(gptServerStore.myData.OPENAI_API_BASE_URL && url.indexOf(gptServerStore.myData.OPENAI_API_BASE_URL)>-1  ) {
+            headers={...headers,...getHeaderAuthorization()}
+
+        }else{
+            const authStore = useAuthStore()
+            if( authStore.token ) {
+                const  header2={ 'x-ptoken':  authStore.token };
+                headers= {...headers, ...header2}
+            }
+        }
+        return  uploadNomalDo(url,headers );
 
-    if(gptServerStore.myData.OPENAI_API_BASE_URL && url.indexOf(gptServerStore.myData.OPENAI_API_BASE_URL)>-1  ) {
-        headers={...headers,...getHeaderAuthorization()}
-        //mlog("headers", headers );
-    }else{
+    }
+
+    //处理上传流程
+    const uploadType=   ( (homeStore.myData.session.uploadType??'') as string).toLocaleLowerCase() ;
+    let headers=   {'Content-Type': 'multipart/form-data' }
+    //R2
+    if(uploadType=='r2' ){
+        return upLoaderR2();
+    //容器
+    }else if( uploadType=='container' ) {
          const authStore = useAuthStore()
         if( authStore.token ) {
             const  header2={ 'x-ptoken':  authStore.token };
             headers= {...headers, ...header2}
         }
+        let url= `/openapi${_url}`
+        return  uploadNomalDo(url,headers );
+
+    //前端API
+    }else if( uploadType=='api' ) {
+        headers={...headers,...getHeaderAuthorization()}
+        let url= `${ gptServerStore.myData.OPENAI_API_BASE_URL}${_url}`
+        return  uploadNomalDo(url,headers );
+
+    //自定义链接
+    }else if( uploadType=='myurl' ) {
+        return  uploadNomalDo(_url,headers );
     }
-    return new Promise<any>((resolve, reject) => {
-            axios.post( url , FormData, {
-            headers
-        }).then(response =>  resolve(response.data )
-        ).catch(error =>reject(error)  );
-    })
 
+    //默认上传流程
+    if(homeStore.myData.session.isUploadR2){
+    return upLoaderR2();
+    }
+    return uploadNomal( _url);
 }
 
 export const whisperUpload = ( FormData:FormData )=>{
@@ -161,7 +199,9 @@ export const subGPT= async (data:any, chat:Chat.Chat )=>{
 }
 
 interface subModelType{
+    kid: string;
     message:any[]
+    imageContent:any[]
     onMessage:(d:{text:string,isFinish:boolean})=>void
     onError?:(d?:any)=>void
     signal?:AbortSignal
@@ -169,13 +209,13 @@ interface subModelType{
     uuid?:string|number
 }
 function getHeaderAuthorization(){
-    if(!gptServerStore.myData.OPENAI_API_KEY){
-        const authStore = useAuthStore()
-        if( authStore.token ) return { 'x-ptoken':  authStore.token };
-        return {}
-    }
+    // if(!gptServerStore.myData.OPENAI_API_KEY){
+    //     const authStore = useAuthStore()
+    //     if( authStore.token ) return { 'x-ptoken':  authStore.token };
+    //     return {}
+    // }
     return {
-        'Authorization': 'Bearer ' +gptServerStore.myData.OPENAI_API_KEY
+        'Authorization': 'Bearer ' + getToken()
     }
 }
 
@@ -188,7 +228,9 @@ export const getSystemMessage = (uuid?:number )=>{
     }
     if(  sysTem ) return sysTem;
     let model= gptConfigStore.myData.model?gptConfigStore.myData.model: "gpt-3.5-turbo";
-      const DEFAULT_SYSTEM_TEMPLATE = `You are ChatGPT, a large language model trained by OpenAI.
+    let producer= 'You are ChatGPT, a large language model trained by OpenAI.'
+    if(model.includes('claude-3')) producer=  'You are Claude, a large language model trained by Anthropic.';
+      const DEFAULT_SYSTEM_TEMPLATE = `${producer}
 Knowledge cutoff: ${KnowledgeCutOffDate[model]}
 Current model: ${model}
 Current time: ${ new Date().toLocaleString()}
@@ -221,28 +263,41 @@ export const subModel= async (opt: subModelType)=>{
             temperature,
             top_p,
             presence_penalty ,frequency_penalty,
-            "messages": opt.message
+            "messages": opt.message,
+            "imageContent": opt.imageContent
            ,stream:true
+           ,kid:gptConfigStore.myData.kid
         }
-        
+
         let headers=   {'Content-Type': 'application/json;charset=UTF-8',
-        'Authorization':'Bearer ' + getToken(),
-        'Accept': 'text/event-stream '}
+                        'Authorization':'Bearer ' + getToken(),
+                        'Accept': 'text/event-stream '}
         headers={...headers,...getHeaderAuthorization()}
-
         try {
-         await fetchSSE( gptGetUrl('/chat'),{
+            let url = "/chat"
+            // 如果选择了数据库 切换地址
+            if(gptConfigStore.myData.kid){
+                url = "/knowledge/chat"
+            }
+         await fetchSSE( gptGetUrl(url),{
             method: 'POST',
             headers: headers,
             signal:opt.signal,
             onMessage: async (data:string)=> {
                  //mlog('🐞测试'  ,  data )  ;
-                 try{
-                    const obj= JSON.parse(data );
-                    opt.onMessage({text:obj.choices[0].delta?.content??'' ,isFinish:obj.choices[0].finish_reason!=null })
-                }catch{
-                    opt.onMessage({text:data,isFinish:true})    
-                }
+                 if(data=='[DONE]') opt.onMessage({text:'',isFinish:true})
+                 else {
+                    try{
+                        const obj= JSON.parse(data );
+                        opt.onMessage({text:obj.choices[0].delta?.content??'' ,isFinish:obj.choices[0].finish_reason!=null })
+                    }catch{
+                        opt.onMessage({
+                            text: data,
+                            isFinish: false
+                        });
+                    }
+                 
+                 }
             },
             onError(e ){
                 //console.log('eee>>', e )
@@ -361,9 +416,9 @@ export const openaiSetting= ( q:any )=>{
             gptServerStore.setMyData(  {OPENAI_API_BASE_URL:url, MJ_SERVER:url, OPENAI_API_KEY:key,MJ_API_SECRET:key } )
             blurClean();
             gptServerStore.setMyData( gptServerStore.myData );
-            
+
         } catch (error) {
-            
+
         }
     }
     else if(isObject(q)){
@@ -403,7 +458,7 @@ export const countTokens= async ( dataSources:Chat.Chat[], input:string ,uuid:nu
     const msg= await getHistoryMessage(  dataSources,1 ) ;
     rz.history= msg.length==0?0: encodeChat(msg, model.indexOf('gpt-4')>-1? 'gpt-4':'gpt-3.5-turbo').length
     //
-    rz.remain = unit *max- rz.history- rz.planOuter- rz.input- rz.system; 
+    rz.remain = unit *max- rz.history- rz.planOuter- rz.input- rz.system;
 
     return rz ;
 }
@@ -418,13 +473,17 @@ const getModelMax=( model:string )=>{
         return 32;
     }else if( model.indexOf('64k')>-1  ){
         return 64;
-    }else if( model.indexOf('128k')>-1 
-    || model=='gpt-4-1106-preview' 
-    || model=='gpt-4-0125-preview' 
+    }else if( model.indexOf('128k')>-1
+    || model=='gpt-4-1106-preview'
+    || model=='gpt-4-0125-preview'
     || model=='gpt-4-vision-preview' ){
-        return 128; 
-    }else if( model.indexOf('gpt-4')>-1  ){  
+        return 128;
+    }else if( model.indexOf('gpt-4')>-1  ){
         max=8;
+    }else if( model.toLowerCase().includes('claude-3') ){
+        //options.maxModelTokens = 120*1024;
+        //options.maxResponseTokens = 4096
+        return 120;
     }
 
     return max;

+ 40 - 0
src/api/voice.ts

@@ -0,0 +1,40 @@
+import request from '@/utils/request/req';
+export interface RoleReq {
+	name:string; // 角色名称
+	description:string; //角色描述
+	prompt:string;//音频地址  
+}
+
+export interface SimpleGenerate {
+	model: string,
+	randomness: number,
+	stability_boost: number,
+	voiceId: string,
+	text: string
+}
+
+export function createRole(params:RoleReq) {
+	return request({
+		url: '/system/voice/add',
+		method: 'post',
+		data: params,
+	})
+}
+
+
+export function simpleGenerateReq(params:SimpleGenerate) {
+	return request({
+		url: '/system/voice/simpleGenerate',
+		method: 'post',
+		data: params,
+	})
+}
+
+export function getRole() {
+	return request({
+		url: '/system/voice/list',
+		method: 'get',
+	})
+}
+
+

+ 28 - 26
src/components/common/PromptStore/index.vue

@@ -192,9 +192,7 @@ watch(showMeVisible, (newValue, oldValue) => {
 									<SvgIcon icon="icon-park-twotone:correct" />
 								</template>
 							</n-tag>
-							<n-tag type="error" size="large"  :bordered="false">
-							    限时双倍活动 充30送30
-							</n-tag>
+						
 							<!-- <n-tag type="success" :bordered="false">
 								10次退还卡
 								<template #icon>
@@ -262,9 +260,6 @@ watch(showMeVisible, (newValue, oldValue) => {
 								</template>
 							</n-tag>
 
-							<n-tag type="error" size="large"  :bordered="false">
-							    限时双倍活动 充60送60
-							</n-tag>
 							<!-- <n-tag type="success" :bordered="false">
 								30次退还卡
 								<template #icon>
@@ -305,41 +300,41 @@ watch(showMeVisible, (newValue, oldValue) => {
 							<tbody>
 							<tr>
 								<td>gpt-3.5-turbo-1106</td>
-								<td>0.03元/1K tokens</td>
+								<td>0.05元/1K tokens</td>
 								<td>GPT3.5最新模型,用于文本生成、对话系统、内容摘要等</td>
 							</tr>
-							<tr>
-								<td>net-gpt-3.5-turbo</td>
-								<td>0.03元/次</td>
-								<td>使用langchain 实现的联网功能,联网没有上下文</td>
-							</tr>
-							<tr>
-								<td>net-gpt-4</td>
-								<td>0.03元/次</td>
-								<td>使用langchain 实现的联网功能,联网没有上下文</td>
-							</tr>
+				
 							<tr>
 								<td>gpt-4-1106-preview</td>
-								<td>0.3元/1K tokens</td>
+								<td>0.2元/1K tokens</td>
 								<td>最新版GPT-4,相对GPT-3.5更先进、拥有更多的参数和更强大的语言处理能力</td>
-								
 							</tr>
+
 							<tr>
 								<td>gpt-4-1106-vision-preview</td>
-								<td>0.3元/1K tokens</td>
+								<td>0.2元/次</td>
 								<td> GPT-4 的一个包含视觉处理能力的预览版本,结合了视觉信息处理的能力</td>
 							</tr>	
+
 							<tr>
-								<td>gpt4all</td>
-								<td>0.3元/次</td>
+								<td>gpt-4-all</td>
+								<td>0.2元/次</td>
 								<td>同时拥有联网查询,高级数据分析,画图 DALL.E功能,GPT 会自动识别并调取相关能力工具</td>
-								
 							</tr>
+
+				
 							<tr>
 								<td>gpt-4-gizmo</td>
-								<td>0.3元/次</td>
+								<td>0.2元/次</td>
 								<td>gpts商店中的模型,使用方式:gpt-4-gizmo-g-xxx</td>
 							</tr>
+
+							<tr>
+								<td>claude-3</td>
+								<td>0.2元/次</td>
+								<td>Claude模型的最新版本,具有最先进的语言处理技术</td>
+							</tr>
+
 							<tr>
 								<td>dall·e 3</td>
 								<td>0.3元/次</td>
@@ -348,14 +343,21 @@ watch(showMeVisible, (newValue, oldValue) => {
 							</tr>
 							<tr>
 								<td>dall·e 3(1790px)</td>
-								<td>0.6元/次</td>
+								<td>0.5元/次</td>
 								<td>DALL·E 是一个专注于图像生成的模型</td>
 							</tr>
 							<tr>
 								<td>midjourney</td>
 								<td>0.3元/次</td>
-								<td>目前最强的AI绘图模型</td>
+								<td>高级图像生成和处理模型,擅长创建逼真的视觉效果</td>
+							</tr>
+							
+							<tr>
+								<td>stable-diffusion</td>
+								<td>0.1元/次</td>
+								<td>高级图像生成和处理模型,擅长创建逼真的视觉效果</td>
 							</tr>
+			
 							</tbody>
 						</n-table>
 				</div>

+ 106 - 17
src/locales/zh-CN.ts

@@ -76,7 +76,7 @@ export default {
     monthlyUsage: '本月使用量',
   },
   store: {
-    siderButton: '提示词商店',
+    siderButton: '进入市场选购',
     local: '本地',
     online: '在线',
     title: '标题',
@@ -94,12 +94,12 @@ export default {
     downloadError: '请检查网络状态与 JSON 文件有效性',
   },
 
-  
+
   mjset:{
     server:'服务端'
     ,about:'关于'
     ,model:'模型'
-    ,sysname:'AI绘图'
+    ,sysname:'熊猫助手'
   }
 
   ,mjtab:{
@@ -124,7 +124,7 @@ export default {
     ,draw:'绘图'
     ,submiting:'提交中'
     ,submit:'提交'
-    ,wait3:'请勿关闭! 图片生成中...' 
+    ,wait3:'请勿关闭! 图片生成中...'
     ,success:'保存成功'
     ,successTitle:'成功'
     ,modlePlaceholder:'自定义模型多个用空格隔开,不是必须'
@@ -166,7 +166,7 @@ export default {
     ,img2textinfo:'不知如何写提示词?用图生文试试!<br/>提交图片,出提示词'
     ,traning:'翻译中...'
     ,imgcreate:'生成图片'
-    ,imginfo:'其他参数:  <li>1 --no 忽略 --no car 图中不出现车 </li><li>2 --seed 可先获取种子 --seed 123456 </li> <li>3 --chaos 10 混合(范围:0-100)</li> <li>4 --tile 碎片化 </li>  <li>5 --sref 图片url 生成风格一致的图像  </li> '
+    ,imginfo:'其他参数:  <li>1 --no 忽略 --no car 图中不出现车 </li><li>2 --seed 可先获取种子 --seed 123456 </li> <li>3 --chaos 10 混合(范围:0-100)</li> <li>4 --tile 碎片化 </li>  <li>5 --sref 图片url 生成风格一致的图像 <li>6 --cref 图片url 生成<b>角色</b>一致的图像  </li> '
     ,tStyle:'风格'
     ,tView:'视角'
     ,tShot:'人物镜头'
@@ -192,7 +192,7 @@ export default {
     ,setMj:'Midjourney 相关'
     ,setMjUrl:'Midjourney接口地址:'
     ,setMjKeyPlaceholder:'使用自定义 Api Secret 绕过密码访问限制'
-    ,setUploader:'上传相关' 
+    ,setUploader:'上传相关'
     ,setUploaderUrl:'上传地址:'
     ,setBtSave:'保存'
     ,setBtBack:'恢复默认'
@@ -206,7 +206,7 @@ export default {
     ,p15:'变焦1.5倍'
     ,p20:'变焦2倍'
     ,p100:'方正'
-    
+
     ,retry:'重分析'
     ,pan_left:'向左'
     ,pan_right:'向右'
@@ -214,7 +214,7 @@ export default {
     ,pan_down:'向下'
     ,up2:'高清2倍'
     ,up4:'高清4倍',
-    
+
     thinking:'思考中...'
     ,noReUpload:'不能重复上传'
     ,uploading:'上传中...'
@@ -222,21 +222,21 @@ export default {
     ,uploadFail:'上传失败:'
     ,upPdf:'<span>上传图片、附件<br/>能上传图片、PDF、EXCEL等文档</span><p>支持拖拽</p>'
     ,upImg:'<span><b>上传图片</b><br/>会自动调用 gpt-4-vision-preview 模型<br>注意:会有额外的图片费用<br/>格式: jpeg jpg png gif</span><p>支持拖拽</p> <p class="pt-2"><b>上传MP3 MP4</b> <br>会自动直接调用 whisper-1 模型<br>格式有:mp3 mp4 mpeg mpga m4a wav webm</p>'
-    ,clearAll:'清参数'  
+    ,clearAll:'清参数'
     ,czoom:'自定义'
     ,customTitle:'自定义变焦'
     ,zoominfo:'修改zoom值,范围在 1.0~2.0 默认设置为1.8',
-    
+
     modleSuccess:'模型加载成功'
     ,setingSuccess:'设置成功'
 
     ,tokenInfo1:'剩余Tokens = 模型长度 - 角色设定 - 上下文(会话历史) - 回复数 - 当前输入'
     ,tokenInfo2:'角色设定留空,系统会给一个默认的'
     ,noSuppertModel:'刷新,暂不支持此模型!'
-    ,failOcr:'识别失败' 
-    ,remain:'剩:' 
-    
-    ,totalUsage:'订阅总额' 
+    ,failOcr:'识别失败'
+    ,remain:'剩:'
+
+    ,totalUsage:'订阅总额'
     ,disableGpt4:'已禁用GPT4'
     ,setTextInfo:'OpenAi Api Key 错误,点击这里重新'
 
@@ -261,7 +261,7 @@ export default {
     ,findVersion:'发现更新版本'
     ,yesLastVersion:'已是最新版本'
     ,infoStar:'此项目开源于 <a  class="text-blue-600 dark:text-blue-500" href="https://github.com/Dooy/chatgpt-web-midjourney-proxy" target="_blank"> GitHub </a>,免费且基于 MIT 协议,没有任何形式的付费行为! </p><p>如果你觉得此项目对你有帮助,请在 GitHub 帮我点个 Star,谢谢!'
-    ,setBtSaveChat:'保存会话'
+    ,setBtSaveChat:'保存会话'
     ,setBtSaveSys: '保存至系统'
 
     ,wsrvClose:'关闭 wsrv'
@@ -286,6 +286,95 @@ export default {
     ,micAsr:'即时识别'
     ,micRec:'开始录音,请说话!2秒内无声音将自动关闭'
     ,micRecEnd:'录音已结束'
-    
-  }
+
+  },
+
+	draw: {
+		qualityList: {
+			general: "一般",
+			clear: "清晰",
+			hd: "高清",
+			ultraHd: "超高清",
+		},
+		styleList: {
+			cyberpunk: "赛博朋克",
+			star: "星际",
+			anime: "动漫",
+			japaneseComicsManga: "日本漫画",
+			inkWashPaintingStyle: "水墨画风格",
+			original: "原创",
+			landscape: "风景画",
+			illustration: "插画",
+			manga: "漫画",
+			modernOrganic: "现代自然",
+			genesis: "创世纪",
+			posterstyle: "海报风格",
+			surrealism: "超现实主义",
+			sketch: "素描",
+			realism: "写实",
+			watercolorPainting: "水彩画",
+			cubism: "立体主义",
+			blackAndWhite: "黑白",
+			fmPhotography: "胶片摄影风格",
+			cinematic: "电影化",
+			clearFacialFeatures: "清晰的面部特征",
+		},
+		viewList: {
+			wideView: "宽视角",
+			birdView: "鸟瞰视角",
+			topView: "顶视角",
+			upview: "仰视角",
+			frontView: "正面视角",
+			headshot: "头部特写",
+			ultrawideshot: "超广角视角",
+			mediumShot: "中景",
+			longShot: "远景",
+			depthOfField: "景深",
+		},
+		shotList: {
+			faceShot: "脸部特写",
+			bigCloseUp: "大特写",
+			closeUp: "特写",
+			waistShot: "腰部以上",
+			kneeShot: "膝盖以上",
+			fullLengthShot: "全身照",
+			extraLongShot: "极远景",
+		},
+		stylesList: {
+			styleLow: "低强度风格",
+			styleMed: "中等强度风格",
+			styleHigh: "高强度风格",
+			styleVeryHigh: "非常高强度风格",
+		},
+		lightList: {
+			coldLight: "冷光",
+			warmLight: "暖光",
+			hardLighting: "硬光",
+			dramaticLight: "戏剧性光线",
+			reflectionLight: "反射光",
+			mistyFoggy: "薄雾",
+			naturalLight: "自然光",
+			sunLight: "阳光",
+			moody: "情绪化",
+		},
+		versionList: {
+			mjV6: "MJ V6",
+			mjV52: "MJ V5.2",
+			mjV51: "MJ V5.1",
+			nijiV6: "Niji V6",
+			nijiV5: "Niji V5",
+			nijiV4: "Niji V4",
+			nijiJourney: "Niji Journey",
+		},
+		botList: {
+			midjourneyBot: "Midjourney 机器人",
+			nijiJourney: "Niji Journey",
+		},
+		dimensionsList: {
+			square: "正方形 (1:1)",
+			portrait: "肖像 (2:3)",
+			landscape: "风景 (3:2)",
+		},
+	}
+
 }

+ 90 - 3
src/locales/zh-TW.ts

@@ -121,7 +121,7 @@ export default {
     "pan_down": "向下",
     "up2": "高清2倍",
     "up4": "高清4倍",
-    
+
     "thinking": "思考中...",
     "noReUpload": "不能重複上傳",
     "uploading": "上傳中...",
@@ -167,7 +167,7 @@ export default {
     "findVersion": "發現更新版本",
     "yesLastVersion": "已是最新版本",
     "infoStar": "此專案在 <a class=\"text-blue-600 dark:text-blue-500\" href=\"https://github.com/Dooy/chatgpt-web-midjourney-proxy\" target=\"_blank\">GitHub</a> 上以 MIT 協議開源,免費且沒有任何付費行為! </p><p>如果你覺得這個專案對你有幫助,請在 GitHub 上給它一顆星,謝謝!",
-    "setBtSaveChat": "保存對話",
+    "setBtSaveChat": "保存對話",
     "setBtSaveSys": "保存至系統",
     "wsrvClose": "關閉 wsrv",
     "wsrvOpen": "開啟 wsrv",
@@ -273,5 +273,92 @@ export default {
     "add2more": "請添加兩張以上圖片",
     "no1m": "圖片大小不能超過1M",
     "imgExt": "圖片僅支持jpg,gif,png,jpeg格式"
-  }
+  },
+	draw: {
+		qualityList: {
+			general: "一般",
+			clear: "清晰",
+			hd: "高清",
+			ultraHd: "超高清",
+		},
+		styleList: {
+			cyberpunk: "賽博朋克",
+			star: "星際",
+			anime: "動漫",
+			japaneseComicsManga: "日本漫畫",
+			inkWashPaintingStyle: "水墨畫風格",
+			original: "原創",
+			landscape: "風景畫",
+			illustration: "插畫",
+			manga: "漫畫",
+			modernOrganic: "現代自然",
+			genesis: "創世紀",
+			posterstyle: "海報風格",
+			surrealism: "超現實主義",
+			sketch: "素描",
+			realism: "寫實",
+			watercolorPainting: "水彩畫",
+			cubism: "立體主義",
+			blackAndWhite: "黑白",
+			fmPhotography: "膠片攝影風格",
+			cinematic: "電影化",
+			clearFacialFeatures: "清晰的面部特徵",
+		},
+		viewList: {
+			wideView: "寬視角",
+			birdView: "鳥瞰視角",
+			topView: "頂視角",
+			upview: "仰視角",
+			frontView: "正面視角",
+			headshot: "頭部特寫",
+			ultrawideshot: "超廣角視角",
+			mediumShot: "中景",
+			longShot: "遠景",
+			depthOfField: "景深",
+		},
+		shotList: {
+			faceShot: "臉部特寫",
+			bigCloseUp: "大特寫",
+			closeUp: "特寫",
+			waistShot: "腰部以上",
+			kneeShot: "膝蓋以上",
+			fullLengthShot: "全身照",
+			extraLongShot: "極遠景",
+		},
+		stylesList: {
+			styleLow: "低強度風格",
+			styleMed: "中等強度風格",
+			styleHigh: "高強度風格",
+			styleVeryHigh: "非常高強度風格",
+		},
+		lightList: {
+			coldLight: "冷光",
+			warmLight: "暖光",
+			hardLighting: "硬光",
+			dramaticLight: "戲劇性光線",
+			reflectionLight: "反射光",
+			mistyFoggy: "薄霧",
+			naturalLight: "自然光",
+			sunLight: "陽光",
+			moody: "情緒化",
+		},
+		versionList: {
+			mjV6: "MJ V6",
+			mjV52: "MJ V5.2",
+			mjV51: "MJ V5.1",
+			nijiV6: "Niji V6",
+			nijiV5: "Niji V5",
+			nijiV4: "Niji V4",
+			nijiJourney: "Niji Journey",
+		},
+		botList: {
+			midjourneyBot: "Midjourney 機器人",
+			nijiJourney: "Niji Journey",
+		},
+		dimensionsList: {
+			square: "正方形 (1:1)",
+			portrait: "肖像 (2:3)",
+			landscape: "風景 (3:2)",
+		},
+	}
 }

+ 56 - 0
src/router/index.ts

@@ -60,6 +60,62 @@ const routes: RouteRecordRaw[] = [
     ],
   },
 
+  {
+    path: '/sound',
+    name: 'Sound',
+    component: ChatLayout,
+    redirect: '/sound/t',
+    children: [
+      {
+        path: 't',
+        name: 'sound1',
+        component: () => import('@/views/sound/index.vue'),
+      },
+    ],
+  },
+
+  {
+    path: '/knowledge',
+    name: 'Knowledge',
+    component: ChatLayout,
+    redirect: '/knowledge/t',
+    children: [
+      {
+        path: 't',
+        name: 'knowledge1',
+        component: () => import('@/views/knowledge/index.vue'),
+      },
+    ],
+  },
+
+  {
+    path: '/annex',
+    name: 'Annex',
+    component: ChatLayout,
+    redirect: '/annex/t',
+    children: [
+      {
+        path: 't',
+        name: 'annex1',
+        component: () => import('@/views/knowledge/annex.vue'),
+      },
+    ],
+  },
+
+  {
+    path: '/fragment',
+    name: 'Fragment',
+    component: ChatLayout,
+    redirect: '/fragment/t',
+    children: [
+      {
+        path: 't',
+        name: 'fragment1',
+        component: () => import('@/views/knowledge/fragment.vue'),
+      },
+    ],
+  },
+
   {
     path: '/404',
     name: '404',

+ 15 - 11
src/store/homeStore.ts

@@ -30,6 +30,8 @@ export interface gptConfigType{
     userModel?:string //自定义
     talkCount:number //联系对话
     systemMessage:string //自定义系统提示语
+    kid:string //知识库id
+    kName:string //知识库名称
     gpts?:gptsType
     uuid?:number
     temperature?:number // 随机性 : 值越大,回复越随机
@@ -51,17 +53,19 @@ const getGptInt= ():gptConfigType =>{
 const  getDefault=()=>{
 const amodel = homeStore.myData.session.amodel??'gpt-3.5-turbo'
 let v:gptConfigType={
-        model: amodel,
-        max_tokens:1024,
-        userModel:'',
-        talkCount:10,
-        systemMessage:'',
-        temperature:0.5,
-        top_p:1,
-        presence_penalty:0,
-        frequency_penalty:0,
-        tts_voice:"alloy"
-    }
+    model: amodel,
+    max_tokens: 1024,
+    userModel: '',
+    talkCount: 10,
+    systemMessage: '',
+    temperature: 0.5,
+    top_p: 1,
+    presence_penalty: 0,
+    frequency_penalty: 0,
+    tts_voice: "alloy",
+    kid: '',
+    kName: ''
+}
     return v ;
 }
 export const gptConfigStore= reactive({

+ 16 - 2
src/views/chat/index.vue

@@ -22,6 +22,7 @@ import aiGPT from '../mj/aiGpt.vue'
 import AiSiderInput from '../mj/aiSiderInput.vue'
 import aiGptInput from '../mj/aiGptInput.vue'
 
+
 let controller = new AbortController()
 
 const openLongReply = import.meta.env.VITE_GLOB_OPEN_LONG_REPLY === 'true'
@@ -453,7 +454,7 @@ const goUseGpts= async ( item: gptsType)=>{
     const saveObj= {model:  `${ item.gid }`   ,gpts:item}
     gptConfigStore.setMyData(saveObj); 
     if(chatStore.active){ //保存到对话框
-        const  chatSet = new chatSetting( chatStore.active );
+        const chatSet = new chatSetting( chatStore.active );
         if( chatSet.findIndex()>-1 ) chatSet.save( saveObj )
     }
     ms.success(t('mjchat.success2'));
@@ -538,6 +539,8 @@ const ychat = computed( ()=>{
   }
   return { text, dateTime: t('chat.preview')} as Chat.Chat;
 }) 
+
+
 </script>
 
 <template>
@@ -550,6 +553,14 @@ const ychat = computed( ()=>{
       @handle-clear="handleClear"
     />
     <main class="flex-1 overflow-hidden">
+
+      <template v-if="gptConfigStore.myData.kid">
+        <div class="flex  mt-4  text-neutral-300">
+           <SvgIcon icon="material-symbols:book" class="mr-1 text-2xl" ></SvgIcon>
+           <span>{{ gptConfigStore.myData.kName }}</span>
+        </div>
+      </template>
+
       <div id="scrollRef" ref="scrollRef" class="h-full overflow-hidden overflow-y-auto">
         <div
           id="image-wrapper"
@@ -557,12 +568,15 @@ const ychat = computed( ()=>{
           :class="[isMobile ? 'p-2' : 'p-4']"
         >
           <template v-if="!dataSources.length">
-            <div v-if="homeStore.myData.session.notify" v-html="homeStore.myData.session.notify" class="text-neutral-300 mt-4"></div>
+            <div v-if="homeStore.myData.session.notify" v-html="homeStore.myData.session.notify" class="text-neutral-300 mt-4">
+            </div>
+
             <div class="flex items-center justify-center mt-4 text-center text-neutral-300" v-else>
               <SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
               <span>Aha~</span>
             </div>
           </template>
+      
           <template v-else>
             <div>
               <Message

+ 2 - 2
src/views/chat/layout/sider/index.vue

@@ -108,11 +108,11 @@ watch(
           </NButton>
         </div>
       </main>
-      <Footer />
+      <Footer></Footer>
     </div>
   </NLayoutSider>
   <template v-if="isMobile">
     <div v-show="!collapsed" class="fixed inset-0 z-40 w-full h-full bg-black/40" @click="handleUpdateCollapsed" />
   </template>
-  <PromptStore v-model:visible="show" />
+  <PromptStore v-model:visible="show"></PromptStore>
 </template>

+ 182 - 0
src/views/knowledge/annex.vue

@@ -0,0 +1,182 @@
+<script setup lang="ts">
+
+import {  h, onMounted, ref , computed } from 'vue'
+import { NButton,NDataTable,DrawerPlacement,NDrawer,NDrawerContent,
+  NForm,NFormItem,NInput,NSpin,NSpace,UploadFileInfo,NUpload,NUploadDragger,
+  NText,NIcon,NP,useMessage,NModal
+} from 'naive-ui'
+import { getToken } from '@/store/modules/auth/helper'
+import { getKnowledgeDetail } from '@/api/knowledge'
+
+import to from "await-to-js";
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+onMounted(() => { 
+  kid.value = router.currentRoute.value.query.kid as string
+
+  fetchData() 
+});
+
+const kid = ref<string>('');
+
+const token = getToken()
+
+const message = useMessage()
+
+const headers = {
+  Authorization: `Bearer ${token}`
+}
+
+const spinShow = ref(false);
+
+
+
+
+
+
+function handleFinish({event,file}: {
+  file: UploadFileInfo
+  event?: ProgressEvent
+}) {
+  message.success('附件上传成功!')
+  showModal.value = false
+  // 关闭加载条
+  spinShow.value = false
+}
+
+function handleActionButtonClick(row: any,action1:string): void {
+  // 跳转到知识片段页面
+  router.push({ path: '/fragment/t', query: { docId: row.docId } });
+}
+
+// 上传之前触发事件
+function handleBeforeUpload(){
+    // 开启加载条
+    spinShow.value = true
+}
+
+
+const showModal = ref(false)
+// 定义一个激活抽屉的函数,接受一个 DrawerPlacement 类型的参数
+const activate = (place: DrawerPlacement) => {
+    showModal.value = true
+}
+
+
+const createColumns = () => {
+  return [
+  ...(false
+      ? [{
+          title: '附件ID',
+          key: 'id',
+          width: 80,
+          ellipsis: true,
+        }]
+      : []),
+    {
+      title: '文档编号',
+      key: 'docId'
+    },
+    {
+      title: '文档名称',
+      key: 'docName'
+    },
+    {
+      title: '文档类型',
+      key: 'docType'
+    },
+    {
+      title: '操作',
+      key: 'actions',
+      render: (row: any) => {
+        return [
+     
+          h(NButton, {
+            onClick: () => handleActionButtonClick(row, 'action2'),
+            style: 'margin-left: 8px; color: #FF4500;',
+          }, { default: () => '删除' }),
+
+          h(NButton, {
+            onClick: () => handleActionButtonClick(row, 'action4'),
+            style: 'margin-left: 8px; color: #32CD32;', 
+          }, { default: () => '知识片段' })
+        ];
+      }
+    }
+  ]
+}
+
+const tableData = ref([]);
+  const fetchData = async () => {
+    try {
+       // 发起一个请求
+      const [err, result] = await to(getKnowledgeDetail(kid.value));
+      console.log("result===", result)
+      if (err) {
+       message.error(err.message)
+      } else {
+       tableData.value = result;
+      }
+    } catch (error) {
+      console.error('Error fetching data:', error);
+    }
+  };
+
+
+const columns = ref(createColumns());
+
+</script>
+<template>
+<br>
+<div style="display: flex; justify-content: flex-start; margin:10px;">
+    <n-button @click="activate('right')" type="primary">
+       上传附件
+    </n-button>
+</div>
+
+<div class="flex h-full"> 
+    <main class="flex-1 overflow-hidden h-full">
+      <n-data-table :columns="columns" :data="tableData" />
+    </main>
+</div>
+
+<n-modal v-model:show="showModal" title="上传附件" :auto-focus="false" preset="card"
+		style="width: 90%; max-width: 450px;">
+  <n-space vertical>
+    <n-form-item>
+      <n-spin :show="spinShow">
+         <!-- @before-upload="beforeUpload" -->
+         <n-upload
+          directory-dnd
+          action="/api/knowledge/attach/upload"
+          name="file"
+          :data="{ kid: kid}"
+          :on-before-upload="handleBeforeUpload"
+          :headers="headers"
+          @finish="handleFinish"
+          :max="1">
+              <n-upload-dragger>
+                <div style="margin-bottom: 12px">
+                  <n-icon size="48" :depth="3">
+                    <archive-icon />
+                  </n-icon>
+                </div>
+                <n-text style="font-size: 16px">
+                  请上传一个5MB以内的文件
+                </n-text>
+                <n-p depth="3" style="margin: 8px 0 0 0">
+                  已支持 md、pdf、docx、txt、csv 等文件格式
+                </n-p>
+              </n-upload-dragger>
+        </n-upload>
+      </n-spin> 
+    </n-form-item> 
+  </n-space>
+  <br>
+
+</n-modal>
+		
+</template>
+

+ 181 - 0
src/views/knowledge/fragment.vue

@@ -0,0 +1,181 @@
+<script setup lang="ts">
+
+import { h, onMounted, ref, computed } from 'vue'
+import {
+    NButton, NDataTable, DrawerPlacement, NDrawer, NDrawerContent,
+    NForm, NFormItem, NInput, NDivider, NSpace, UploadFileInfo, NUpload, NUploadDragger,
+    NText, NIcon, NP, useMessage, NModal
+} from 'naive-ui'
+import { getToken } from '@/store/modules/auth/helper'
+import { getfragmentList } from '@/api/knowledge'
+
+import to from "await-to-js";
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+const docId = ref<string>('');
+
+onMounted(() => { 
+    docId.value = router.currentRoute.value.query.docId as string
+    fetchData() 
+});
+
+
+
+
+
+const token = getToken()
+
+const message = useMessage()
+
+const headers = {
+    Authorization: `Bearer ${token}`
+}
+
+
+// 初始化表单数据对象
+const formValue = ref({
+    id: '',// 知识库id
+    kid: '', // 附件id
+    uid: '',//  用户id  
+    kname: '', // 知识库名称
+    description: '',// 知识库描述 
+});
+
+export interface KnowledgeReq {
+
+}
+
+function handleFinish({ event, file }: {
+    file: UploadFileInfo
+    event?: ProgressEvent
+}) {
+    const ext = (event?.target as XMLHttpRequest).response
+    // ext 转成json对象
+    const fileData = JSON.parse(ext);
+    formValue.value.prompt = fileData.data.url
+    console.log("ext===================" + fileData.data.url, file.file?.size)
+    message.success('上传成功!')
+}
+
+async function submitSimpleGenerate() {
+
+}
+
+const showModal = ref(false)
+// 定义一个激活抽屉的函数,接受一个 DrawerPlacement 类型的参数
+const activate = (place: DrawerPlacement) => {
+    showModal.value = true
+}
+
+
+const createColumns = () => {
+    return [
+        ...(false
+            ? [{
+                title: '片段ID',
+                key: 'id',
+                width: 80,
+                ellipsis: true,
+            }]
+            : []),
+        {
+            title: '片段编号',
+            key: 'fid'
+        },
+
+        {
+            title: '片段内容',
+            key: 'content',
+            width: 300,
+            ellipsis: {
+                tooltip: {
+                    contentStyle: 'max-width:300px'
+                }
+            }
+        },
+
+        {
+            title: '操作',
+            key: 'actions',
+            render: (row: any) => {
+                return [
+                    h(NButton, {
+                        onClick: () => handleActionButtonClick(row, 'action2'),
+                        style: 'margin-left: 8px; color: #FF4500;',
+                    }, { default: () => '删除' })
+                ];
+            }
+        }
+    ]
+}
+
+const tableData = ref([]);
+const fetchData = async () => {
+    try {
+        // 发起一个请求
+        const [err, result] = await to(getfragmentList(docId.value));
+        console.log("result===", result)
+        if (err) {
+            message.error(err.message)
+        } else {
+            tableData.value = result;
+        }
+    } catch (error) {
+        console.error('Error fetching data:', error);
+    }
+};
+
+
+const columns = ref(createColumns());
+
+</script>
+<template>
+    <br>
+    <div style="display: flex; justify-content: flex-start; margin:10px;">
+        <n-button @click="activate('right')" type="primary">
+            添加片段
+        </n-button>
+    </div>
+
+    <div class="flex h-full">
+        <main class="flex-1 overflow-hidden h-full">
+            <n-data-table :columns="columns" :data="tableData" />
+        </main>
+    </div>
+
+    <n-modal v-model:show="showModal" title="添加片段" :auto-focus="false" preset="card"
+        style="width: 95%; max-width: 500px;">
+        <n-space vertical>
+            <n-form-item>
+
+                <!-- @before-upload="beforeUpload" -->
+                <n-upload directory-dnd action="/api/knowledge/attach/upload" name="file" :data="{ kid: kid }"
+                    :headers="headers" @finish="handleFinish" :max="1">
+                    <n-upload-dragger>
+                        <div style="margin-bottom: 12px">
+                            <n-icon size="48" :depth="3">
+                                <archive-icon />
+                            </n-icon>
+                        </div>
+                        <n-text style="font-size: 16px">
+                            请上传一个5MB以内的文件
+                        </n-text>
+                        <n-p depth="3" style="margin: 8px 0 0 0">
+                            已支持 md、pdf、docx、txt、csv 等文件格式
+                        </n-p>
+                    </n-upload-dragger>
+                </n-upload>
+            </n-form-item>
+        </n-space>
+
+        <br>
+        <div style="display: flex; justify-content: flex-end">
+            <n-button @click="submitSimpleGenerate" type="primary">
+                添加到知识库
+            </n-button>
+        </div>
+    </n-modal>
+
+</template>

+ 169 - 0
src/views/knowledge/index.vue

@@ -0,0 +1,169 @@
+<script setup lang="ts">
+
+import {  h, onMounted, ref } from 'vue'
+import { NButton,NDataTable,DrawerPlacement,NDrawer,NDrawerContent,NForm,NFormItem,NInput,NDivider,NSpace,useMessage
+} from 'naive-ui'
+import { createKnowledgeReq,getKnowledge } from '@/api/knowledge'
+import to from "await-to-js";
+import { useRouter } from 'vue-router'
+
+onMounted(() => { fetchData() });
+
+const router = useRouter()
+const message = useMessage()
+
+// 初始化表单数据对象
+const formValue = ref({
+	id:'',// 知识库id
+	kid:'', // 附件id
+	uid:'',//  用户id  
+	kname:'', // 知识库名称
+	description:'',// 知识库描述 
+});
+
+ async function submitForm() {
+  //关闭弹框
+  active.value = false
+  // 发起一个请求
+  const [err, result] = await to(createKnowledgeReq(formValue.value));
+	  console.log("result===", result)
+    if (err) {
+      message.error(err.message)
+    } else {
+      message.success('知识库创建成功!')
+    }
+}
+
+function handleActionButtonClick(row: any,action1:string): void {
+  // 跳转到知识库附件页面
+  router.push({ path: '/annex/t', query: { kid: row.id } });
+}
+
+// 定义一个激活抽屉的函数,接受一个 DrawerPlacement 类型的参数
+const activate = (place: DrawerPlacement) => {
+  active.value = true
+  placement.value = place
+}
+
+// 使用 ref 来创建响应式变量
+const active = ref(false)
+
+const placement = ref<DrawerPlacement>('right')
+
+const createColumns = () => {
+  return [
+  ...(false
+      ? [{
+          title: '角色ID',
+          key: 'id',
+          width: 80,
+          ellipsis: true,
+        }]
+      : []),
+      {
+      title: '编号',
+      key: 'kid'
+    },
+    {
+      title: '知识名称',
+      key: 'kname'
+    },
+    {
+      title: '知识描述',
+      key: 'description'
+    },
+  
+    {
+      title: '操作',
+      key: 'actions',
+      render: (row: any) => {
+        return [
+          h(NButton, {
+            onClick: () => handleActionButtonClick(row, 'action1'),
+            style: 'margin-left: 0px; color: #1E90FF;', 
+          }, { default: () => '编辑' }),
+
+          h(NButton, {
+            onClick: () => handleActionButtonClick(row, 'action2'),
+            style: 'margin-left: 8px; color: #FF4500;',
+          }, { default: () => '删除' }),
+
+          h(NButton, {
+            onClick: () => handleActionButtonClick(row, 'action3'),
+            style: 'margin-left: 8px; color: #32CD32;', 
+          }, { default: () => '附件' }),
+
+        ];
+      }
+    }
+  ]
+}
+
+const tableData = ref([]);
+
+  const fetchData = async () => {
+    try {
+       // 发起一个请求
+      const [err, result] = await to(getKnowledge());
+      console.log("result===", result)
+      if (err) {
+       message.error(err.message)
+      } else {
+       tableData.value = result;
+      }
+    } catch (error) {
+      console.error('Error fetching data:', error);
+    }
+  };
+
+
+const columns = ref(createColumns());
+
+</script>
+<template>
+<br>
+<div style="display: flex; justify-content: flex-start; margin:10px;">
+    <n-button @click="activate('right')" type="primary">
+       创建知识库
+    </n-button>
+</div>
+
+<div class="flex h-full"> 
+    <main class="flex-1 overflow-hidden h-full">
+      <n-data-table :columns="columns" :data="tableData" />
+    </main>
+</div>
+
+<n-drawer v-model:show="active" :width="502" :placement="placement">
+      <n-drawer-content title="创建知识库">
+          在这里创建你的知识库
+          <n-divider />
+          <n-space vertical>
+            <n-form ref="formRef" >
+              <n-form-item
+                label="知识名称"  path="formValue.kname">
+                <n-input  v-model:value="formValue.kname" placeholder="输入知识名称" />
+              </n-form-item>
+              <n-form-item label="知识描述" path="formValue.description">
+                <n-input maxlength="1000"
+                  type="textarea"
+                  v-model:value="formValue.description"
+                  placeholder="输入知识描述" 
+                />
+              </n-form-item>
+
+              <n-col :span="24">
+                <div style="display: flex; justify-content: flex-end">
+                  <n-button @click="submitForm"  type="primary">
+                    添加
+                  </n-button>
+                </div>
+              </n-col>
+
+            </n-form>
+          </n-space>
+      </n-drawer-content>
+</n-drawer>
+
+</template>
+

+ 1 - 1
src/views/login/index.vue

@@ -84,7 +84,7 @@ const handleRegistBtnClick = async (e: MouseEvent) => {
 									<h2 class="text-3xl font-bold tracking-tight text-center text-gray-900">登录</h2>
 									<p class="mt-2 text-sm text-center text-gray-600"> 或者
 										<a @click="handleRegistBtnClick" style="font-size: 18px"
-											class="font-semibold text-teal-500 hover:text-teal-600">注册</a> 并免费体验熊猫助手
+											class="font-semibold text-teal-500 hover:text-teal-600">注册</a> 并免费体验问答助手
 									</p>
 								</div>
 								<div class="sm:mx-auto sm:w-full sm:max-w-sm">

+ 99 - 59
src/views/mj/aiDrawInputItem.vue

@@ -1,14 +1,13 @@
-<script setup lang="ts">
-//boy, Cyberpunk , Top view , Face Shot (VCU) , Warm light  --style raw  --ar 3:4 --q 0.5 --v 5.2
-import { ref,computed,watch,onMounted } from "vue";
+<script setup lang="ts"> 
+import { ref,computed,watch,onMounted } from "vue"; 
 import config from "./draw.json";
-import {  NSelect,NInput,NButton,NTag,NPopover, useMessage,NDivider} from 'naive-ui'; 
+import {  NSelect,NInput,NButton,NTag,NPopover, useMessage,NInputNumber} from 'naive-ui';
 import {  SvgIcon } from '@/components/common'
 import { useBasicLayout } from '@/hooks/useBasicLayout'
 const { isMobile } = useBasicLayout()
-import AiMsg from './aiMsg.vue' 
-//import aiFace from './aiFace.vue' 
-import { mlog, train, upImg ,getMjAll } from '@/api' 
+import AiMsg from './aiMsg.vue'
+//import aiFace from './aiFace.vue'
+import { mlog, train, upImg ,getMjAll } from '@/api'
 //import {copyText3} from "@/utils/format";
 import { homeStore ,useChatStore} from "@/store";
 const chatStore = useChatStore()
@@ -22,7 +21,7 @@ const vf=[{s:'width: 100%; height: 100%;',label:'1:1'}
 ,{s:'width: 50%; height: 100%;',label:'9:16'}
  ];
 
-const f=ref({bili:-1, quality:'',view:'',light:'',shot:'',style:'', styles:'',version:'--v 5.2'});
+const f=ref({bili:-1, quality:'',view:'',light:'',shot:'',style:'', styles:'',version:'--v 6.0',sref:'',cref:'',cw:'',});
 const st =ref({text:'',isDisabled:false,isLoad:false
     ,fileBase64:[],bot:'',showFace:false
 });
@@ -36,8 +35,24 @@ const farr= [
 ,{ k:'version',v:t('mjchat.tVersion') }
  ];
 
+const drawlocalized = computed(() => {
+	let localizedConfig = {};
+	Object.keys(config).forEach((key) => {
+		localizedConfig[key] = config[key].map((option) => {
+			// 假设 labelKey 如 "draw.qualityList.general"
+			let path = option.labelKey; // 直接使用 labelKey 作为路径
+			return {
+				...option,
+				label: t(path), // 从 i18n 中获取本地化的标签
+			};
+		});
+	});
+	return localizedConfig;
+});
+
+
 const msgRef = ref()
-const fsRef= ref() 
+const fsRef= ref()
 const fsRef2 = ref()
 const $emit=defineEmits(['drawSent','close']);
 const props = defineProps({buttonDisabled:Boolean});
@@ -47,8 +62,8 @@ const isDisabled = computed(() => {
 })
 const ms=   useMessage();
 function create( ){
-   
-   
+
+
     st.value.isLoad=true
     train( st.value.text.trim()).then(ps=>{
         const rz={ prompt: st.value.text.trim() , drawText: createPrompt( ps) }
@@ -60,7 +75,7 @@ function create( ){
         st.value.isLoad=false
     })
 
-    
+
 }
 
 const shorten= ()=>{
@@ -72,7 +87,7 @@ const shorten= ()=>{
     }
 
     let obj={
-            action:'shorten', 
+            action:'shorten',
             data:{prompt: st.value.text.trim(),botType: st.value.bot=='NIJI_JOURNEY'? 'NIJI_JOURNEY': 'MID_JOURNEY'}
         }
     homeStore.setMyData({act:'draw',actData:obj});
@@ -93,8 +108,8 @@ function createPrompt(rz:string){
         msgRef.value.showError(t('mjchat.placeInput') );
         return '';
     }
-     
-   
+
+
     // for(let v of farr){
     //     if( ! f.value[v.k] || f.value[v.k]==null || f.value[v.k]=='' ) continue;
     //      mlog('k ', rz,  f.value  );
@@ -127,11 +142,14 @@ function createPrompt(rz:string){
     }
 
     mlog('createPrompt ', rz,  f.value  );
-    if(f.value.bili>-1) rzp +=` --ar ${vf[f.value.bili].label}`;
-    rz = rzk + rz +rzp; 
+    if( f.value.sref.trim() != '' ) rzp += ` --sref ${f.value.sref}`
+    if( f.value.cref.trim() != '' ) rzp += ` --cref ${f.value.cref}`
+    if( f.value.cw && f.value.cw!='' ) rzp += ` --cw ${f.value.cw}`
+    if (f.value.bili > -1) rzp += ` --ar ${vf[f.value.bili].label}` 
+    rz = rzk + rz +rzp;
     return rz ;
 }
- 
+
 // const copy=()=>{
 //     copyText3( '哦们sd').then(()=>msgRef.value.showMsg('复制成功345!'));
 // }
@@ -152,16 +170,16 @@ function selectFile(input:any){
         st.value.fileBase64.push(d);
         fsRef.value.value='';
     }).catch(e=>msgRef.value.showError(e));
-    
+
 }
 
 //图生文
 function selectFile2(input:any){
-     
+
     upImg(input.target.files[0]).then(d=>{
         mlog('f2base64>> ',d );
         let obj={
-            action:'img2txt', 
+            action:'img2txt',
             data:{
                 "base64":d
                 ,"botType": "MID_JOURNEY"
@@ -170,7 +188,7 @@ function selectFile2(input:any){
         homeStore.setMyData({act:'draw',actData:obj});
         //input.value.value='';
         fsRef2.value.value='';
-       
+
     })
     .catch(e=>msgRef.value.showError(e))
 }
@@ -193,7 +211,7 @@ onMounted(()=>{
 const exportToTxt= async ()=>{
     let txtContent ='';
     mlog('sss',txtContent,chatStore.$state.chat.length  );
-   
+
     let d = await getMjAll( chatStore.$state);
     if(d.length==0) {
         //ms.info('暂时没作品');
@@ -216,11 +234,23 @@ const exportToTxt= async ()=>{
     a.click();
     ms.success( t('mjchat.exSuccess'));
 }
+
 const clearAll=()=>{
-    //f.value
-    Object.keys(f.value).map(k=>f.value[k]='');
-    f.value.bili= -1;
+  st.value.fileBase64=[];
+  st.value.text='';
+  f.value.bili=-1;
+  f.value.version='';
+  f.value.quality='';
+  f.value.shot='';
+  f.value.light='';
+  f.value.style='';
+  f.value.styles='';
+  f.value.view='';
+  f.value.cref='';
+  f.value.cw='';
+  f.value.sref='';
 }
+
 //const config=
 </script>
 <template>
@@ -229,7 +259,7 @@ const clearAll=()=>{
 <input type="file"  @change="selectFile2" ref="fsRef2" style="display: none" accept="image/jpeg, image/jpg, image/png, image/gif"/>
 
 <div class="overflow-y-auto bg-[#fafbfc] px-4 dark:bg-[#18181c] h-full ">
-    
+
     <section class="mb-4">
         <div class="mr-1  mb-2 flex justify-between items-center">
             <div class="text-sm">{{ $t('mjchat.imgBili') }}</div>
@@ -251,21 +281,31 @@ const clearAll=()=>{
                 <p class="mb-1 text-center text-sm">{{ item.label }}</p>
             </section>
             </template>
-             
+
         </div>
     </section>
     <section class="mb-4 flex justify-between items-center" v-for=" v in farr">
-     <div>{{ v.v }}</div>
-     
-    <n-select v-model:value="f[v.k]" :options="config[v.k+'List']" size="small"  class="!w-[60%]" :clearable="true" />
-    </section>
-    <!-- <section class="mb-4 flex justify-between items-center"  >
-     <div>机器人</div>
-    <n-select v-model:value="st.bot" :options="config.botList" size="small"  class="!w-[60%]" :clearable="true" />
-
-     </section> -->
+        <div>{{ v.v }}</div>
+        <n-select v-model:value="f[v.k]" :options="drawlocalized[v.k+'List']" size="small"  class="!w-[60%]" :clearable="true" />
+	</section>
+    <template v-if="!isMobile"> 
+        <section class="mb-4 flex justify-between items-center"  >
+        <div  >cw(0-100)</div>
+        <NInputNumber :min="0" :max="100" v-model:value="f.cw" class="!w-[60%]" size="small" clearable placeholder="0-100 角色参考程度" />
+        </section >
+    
+        <section class="mb-4 flex justify-between items-center"  >
+        <div class="w-[60px]">sref</div>
+        <NInput v-model:value="f.sref" size="small" placeholder="图片url 生成风格一致的图像" clearable />
+        </section>
+        <section class="mb-4 flex justify-between items-center"  >
+        <div class="w-[60px]">cref</div>
+        <NInput  v-model:value="f.cref" size="small" placeholder="图片url 生成角色一致的图像" clearable/>
+        </section>
+    </template>
+    
     <div class="mb-1">
-     <n-input    type="textarea"  v-model:value="st.text"   :placeholder="$t('mjchat.prompt')" round clearable maxlength="2000" show-count 
+     <n-input    type="textarea"  v-model:value="st.text"   :placeholder="$t('mjchat.prompt')" round clearable maxlength="2000" show-count
       :autosize="{   minRows:2, maxRows:5 }" />
     </div>
     <div class="mb-4 flex justify-between items-center">
@@ -282,15 +322,15 @@ const clearAll=()=>{
                 </template>
                 <div  style="max-width: 240px;">
                 <p v-html="$t('mjchat.imgCInfo')"></p>
-                
-                3.<a class="text-green-500 cursor-pointer"  @click="fsRef.click()" v-html="$t('mjchat.imgCadd')"></a><br/> 
+
+                3.<a class="text-green-500 cursor-pointer"  @click="fsRef.click()" v-html="$t('mjchat.imgCadd')"></a><br/>
                 <div  v-if="st.fileBase64.length>0" class="flex justify-start items-baseline">
                     <div class="p-1" v-for="(v ) in st.fileBase64">
                         <img  class="w-[60px]" :src="v">
-                        <br/> 
+                        <br/>
                         <NButton size="small" @click="st.fileBase64= st.fileBase64.filter((item)=>item!=v) " type="warning" >{{$t('mjchat.del')}}</NButton>
                     </div>
-                    
+
                 </div>
                 </div>
              </NPopover>
@@ -312,35 +352,35 @@ const clearAll=()=>{
                      <div style="display: flex;">  <SvgIcon icon="game-icons:bouncing-spring" /> Shorten </div>
                 </n-tag>
             </div>
-            
+
         </div>
-        
+
 
         <!-- <div class="flex "  v-if="$t('mjchat.imgcreate').indexOf('生成图片')!==-1">
          <n-button type="primary" :block="true" :disabled="isDisabled"  @click="create()">
-            <SvgIcon icon="mingcute:send-plane-fill" />  
-            
+            <SvgIcon icon="mingcute:send-plane-fill" />
+
             <template v-if="st.isLoad">{{$t('mjchat.traning')}} </template>
             <template v-else> {{$t('mjchat.imgcreate')}}</template>
-            
+
         </n-button>
         </div> -->
 
-        
+
     </div>
 
-    
-          
+
+
         <div class="flex">
             <n-button type="primary" :block="true" :disabled="isDisabled"  @click="create()">
-            <SvgIcon icon="mingcute:send-plane-fill" />  
-            
+            <SvgIcon icon="mingcute:send-plane-fill" />
+
             <template v-if="st.isLoad">{{$t('mjchat.traning')}} </template>
-            <template v-else> {{$t('mjchat.imgcreate')}}</template> 
+            <template v-else> {{$t('mjchat.imgcreate')}}</template>
             </n-button>
-        </div> 
+        </div>
         <div class="flex justify-start items-center py-1">
-            
+
             <div >
                 <n-tag type="success" round size="small" style="cursor: pointer; " :bordered="false" @click="clearAll()"   >
                      <div style="display: flex;">  <SvgIcon icon="ant-design:clear-outlined" />{{   $t('mj.clearAll')  }}  </div>
@@ -356,11 +396,11 @@ const clearAll=()=>{
     </div> -->
     <!-- <div class="mb-4 flex justify-between items-center">
         <div @click="copy()" ref="copyRef">复制</div>
-        <div @click="copy2()"  >复制2</div> 
+        <div @click="copy2()"  >复制2</div>
     </div> -->
 
    <ul class="pt-4"  v-if="!isMobile" v-html="$t('mjchat.imginfo')"></ul>
-   
+
 
 </div>
 
@@ -370,6 +410,6 @@ const clearAll=()=>{
 <style>
     .aspect-item.active, .aspect-item.active .aspect-box{
         border-color:#86dfba ;
-         
+
     }
-</style>
+</style>

+ 20 - 18
src/views/mj/aiGpt.vue

@@ -46,8 +46,6 @@ watch( ()=>textRz.value, (n)=>{
 const { uuid } = useRoute().params as { uuid: string }
 watch(()=>homeStore.myData.act, async (n)=>{
 
-   
-    
     if(n=='gpt.submit' ||  n=='gpt.whisper'  ){
         
         const dd:any = homeStore.myData.actData;
@@ -114,14 +112,10 @@ watch(()=>homeStore.myData.act, async (n)=>{
             }
             
         }else{
-        
             addChat(  +uuid2, promptMsg );
             homeStore.setMyData({act:'scrollToBottom'});
         }
        
-
-
-       
         let outMsg: Chat.Chat={
             dateTime: new Date().toLocaleString(),
             text: t('mj.thinking') ,//'思考中...',
@@ -153,16 +147,27 @@ watch(()=>homeStore.myData.act, async (n)=>{
         //return ;
         let message= [ {  "role": "system", "content": getSystemMessage(  +uuid2) },
                 ...historyMesg ];
+        let imageContent = [];         
         if( dd.fileBase64 && dd.fileBase64.length>0 ){
             if(  model=='gpt-4-vision-preview' ){
                 let obj={
                         "role": "user",
-                        "content": [] as any
+                        "content": ""
                 }
                 // //"Generate code for a web page that looks exactly like this."
-                obj.content.push({ "type": "text",      "text": dd.prompt  });
+                //obj.content.push({,      "text": dd.prompt  });
+                imageContent.push(  {
+                        "type": "text",
+                        "text": dd.prompt,
+                    })
                 dd.fileBase64.forEach((f:any)=>{
-                    obj.content.push({ "type": "image_url",  "image_url": {url:f }   });
+                    //obj.content.push({ "type": "image_url",  "image_url": {url:f }   });
+                    imageContent.push(
+                        {
+                            "type": "image_url",
+                            "image_url": { url: f }
+                        }
+                    )
                 });
                 message.push(obj); 
             }else{
@@ -181,7 +186,8 @@ watch(()=>homeStore.myData.act, async (n)=>{
                 file: dd.file
             }
         }
-        submit(model , message,opt );
+    
+        submit(model,message,imageContent,opt);
  
     }else if(n=='abort'){
        controller.value && controller.value.abort();
@@ -195,9 +201,6 @@ watch(()=>homeStore.myData.act, async (n)=>{
         st.value.uuid =  uuid2 ;
         st.value.index = +dd.index;
         
-         
-        
-
         mlog('gpt.resubmit', dd  ) ;
         let historyMesg= await  getMessage( (+dd.index)-1,1  ); //
         mlog('gpt.resubmit historyMesg', historyMesg );
@@ -219,7 +222,8 @@ watch(()=>homeStore.myData.act, async (n)=>{
         let message= [ {  "role": "system", "content": getSystemMessage(+st.value.uuid ) },
                 ...historyMesg ]; 
         textRz.value=[];
-        submit(model, message );
+      
+        submit(model, message);
 
     }else if(n=='gpt.ttsv2'){ 
         const actData:any = homeStore.myData.actData;
@@ -250,12 +254,10 @@ watch(()=>homeStore.myData.act, async (n)=>{
                 if(e.message!='canceled' && emsg.indexOf('aborted')==-1 ) textRz.value.push("\n"+t('mjchat.failReason')+" \n```\n"+emsg+"\n```\n");
                 //goFinish();
             });
-
     }  
-    
 })
 
-const submit= (model:string, message:any[] ,  opt?:any )=>{
+const submit= (model:string, message:any[],imageContent?:any[],opt?:any)=>{
     mlog('提交Model', model  );
     const chatSet = new chatSetting(   +st.value.uuid  );
     const nGptStore =   chatSet.getGptConfig()  ; 
@@ -304,7 +306,7 @@ const submit= (model:string, message:any[] ,  opt?:any )=>{
 
         }else{
         //controller.signal
-            subModel( {message,model
+            subModel( {message,model,imageContent
             ,uuid:st.value.uuid //当前会话
             ,onMessage:(d)=>{
                 mlog('🐞消息',d);

+ 0 - 1
src/views/mj/aiGptsCom.vue

@@ -7,7 +7,6 @@ import { SvgIcon } from '@/components/common';
 import { useRouter } from 'vue-router';
 import { t } from '@/locales'; 
 
-
 const router = useRouter()
 const ms = useMessage();
 const chatStore = useChatStore()

+ 1 - 1
src/views/mj/aiMobileMenu.vue

@@ -53,7 +53,7 @@ watch(()=>homeStore.myData.act, (n:string)=>{
       </div> 
   </div>
 
-  <n-drawer v-model:show="st.show" :height="650"  placement="bottom" v-if="goHome=='draw'">
+  <n-drawer v-model:show="st.show"  class="!h-[90vh] !max-h-[660px]"  placement="bottom" v-if="goHome=='draw'">
     <n-drawer-content   style="--n-body-padding:0" class="h-full">
       <aiDrawInput @draw-sent="drawSent" />
     </n-drawer-content>

+ 21 - 19
src/views/mj/aiModel.vue

@@ -14,13 +14,13 @@ const chatSet = new chatSetting( uuid==null?1002:uuid);
 const nGptStore = ref(  chatSet.getGptConfig() );
 
 const config = ref({
-model:['gpt-3.5-turbo','gpt-4-0125-preview','gpt-4-all']
+model:[ 'gpt-4-0125-preview','gpt-3.5-turbo','gpt-4-all','claude-3-sonnet-20240229','stable-diffusion','suno-v3']
 ,maxToken:2048
-}); 
+});
 const st= ref({openMore:false });
 const voiceList= computed(()=>{
     let rz=[];
-    for(let o of "alloy,echo,fable,onyx,nova,shimmer".split(/[ ,]+/ig))rz.push({label:o,value:o}) 
+    for(let o of "alloy,echo,fable,onyx,nova,shimmer".split(/[ ,]+/ig))rz.push({label:o,value:o})
     return rz;
 });
 const modellist = computed(() => { //
@@ -60,20 +60,16 @@ const modellist = computed(() => { //
     return uniqueArray ;
 });
 const ms= useMessage();
-const save = ()=>{ 
-    gptConfigStore.setMyData( nGptStore.value );
-    ms.success( t('common.saveSuccess')); //'保存成功'
-    emit('close');
-}
-const saveChat=()=>{
+
+const saveChat=(type:string)=>{
      chatSet.save(  nGptStore.value );
-     homeStore.setMyData({act:'saveChat'});
-     //gptConfigStore.setInit(); //恢复下默认
-     //gptConfigStore.myData.systemMessage= '';
-     ms.success( t('common.saveSuccess'));
+     gptConfigStore.setMyData( nGptStore.value );
+     homeStore.setMyData({act:'saveChat'}); 
+     if(type!='hide')ms.success( t('common.saveSuccess'));
      emit('close');
 }
- 
+
+
 watch(()=>nGptStore.value.model,(n)=>{
     nGptStore.value.gpts=undefined;
     let max=4096;
@@ -81,6 +77,8 @@ watch(()=>nGptStore.value.model,(n)=>{
         max=4096;
     }else if( n.indexOf('gpt-4')>-1 ||  n.indexOf('16k')>-1 ){ //['16k','8k','32k','gpt-4'].indexOf(n)>-1
         max=4096*2;
+    }else if( n.toLowerCase().includes('claude-3') ){
+         max=4096*2;
     }
     config.value.maxToken=max/2;
     if(nGptStore.value.max_tokens> config.value.maxToken ) nGptStore.value.max_tokens= config.value.maxToken;
@@ -104,6 +102,7 @@ onMounted(() => {
      <div ><span class="text-red-500">*</span>  {{ $t('mjset.model') }}</div>
     <n-select v-model:value="nGptStore.model" :options="modellist" size="small"  class="!w-[50%]"   />
 </section>
+
 <section class="mb-4 flex justify-between items-center"  >
     <n-input   :placeholder="$t('mjchat.modlePlaceholder')" v-model:value="nGptStore.userModel">
       <template #prefix>
@@ -111,6 +110,8 @@ onMounted(() => {
       </template>
     </n-input>
  </section>
+
+
  <section class=" flex justify-between items-center"  >
      <div> {{ $t('mjchat.historyCnt') }}
      </div>
@@ -122,7 +123,7 @@ onMounted(() => {
 <div class="mb-4 text-[12px] text-gray-300 dark:text-gray-300/20">{{ $t('mjchat.historyToken') }}</div>
 
  <section class=" flex justify-between items-center"  >
-     <div> {{ $t('mjchat.historyTCnt') }} 
+     <div> {{ $t('mjchat.historyTCnt') }}
      </div>
      <div class=" flex justify-end items-center w-[80%] max-w-[240px]">
         <div class=" w-[200px]"><n-slider v-model:value="nGptStore.max_tokens" :step="1" :max="config.maxToken" :min="1" /></div>
@@ -189,9 +190,10 @@ onMounted(() => {
     <NTag  type="primary" round size="small" :bordered="false" class="!cursor-pointer">More...</NTag>
 </div>
 
- <section class=" text-right flex justify-end space-x-2"  >
+<section class=" text-right flex justify-end space-x-2"  >
     <NButton   @click="reSet()">{{ $t('mj.setBtBack') }}</NButton>
-    <NButton type="primary" @click="saveChat">{{ $t('mj.setBtSaveChat') }}</NButton>
-    <NButton type="primary" @click="save">{{ $t('mj.setBtSaveSys') }}</NButton>
+    <!-- <NButton type="primary" @click="saveChat">{{ $t('mj.setBtSaveChat') }}</NButton>
+    <NButton type="primary" @click="save">{{ $t('mj.setBtSaveSys') }}</NButton> -->
+    <NButton type="primary" @click="saveChat('no')">{{ $t('common.save') }}</NButton>
  </section>
-</template>
+</template>

+ 1 - 3
src/views/mj/aiSider.vue

@@ -62,6 +62,7 @@ const chatId= computed(()=>chatStore.active??'1002' );
             </a>
 
 
+
             <a :href="`#/draw/${chatId}`" @click="st.active='draw'" class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
                 <n-tooltip placement="right" trigger="hover">
                   <template #trigger> 
@@ -74,8 +75,6 @@ const chatId= computed(()=>chatStore.active??'1002' );
                 </n-tooltip>
             </a>
 
-
-
              <a   @click="homeStore.setMyData({act:'gallery'}) " class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]">
                 <n-tooltip placement="right" trigger="hover">
                   <template #trigger> 
@@ -88,7 +87,6 @@ const chatId= computed(()=>chatStore.active??'1002' );
                 </n-tooltip>
             </a>
 
-
             <!-- <section  class=" router-link-exact-active h-12 w-12 cursor-pointer rounded-xl bg-white duration-300 dark:bg-[#34373c] hover:bg-[#bbb] dark:hover:bg-[#555]"
              >
                 <n-tooltip placement="right" trigger="hover">

+ 287 - 222
src/views/mj/draw.json

@@ -1,223 +1,288 @@
 {
-"qualityList": [{
-                "label": "一般",
-                "value": "0.25"
-            }, {
-                "label": "清晰",
-                "value": "0.5"
-            }, {
-                "label": "高清",
-                "value": "1"
-            }, {
-                "label": "超高清",
-                "value": "2"
-            }],
-    "styleList": [
-        {
-                 "label": "赛博朋克",
-                "value": "Cyberpunk"
-            }, {
-                "label": "星际",
-                "value": "Warframe"
-            }, {
-                "label": "二次元",
-                "value": "ACGN"
-            }, {
-                "label": "日本漫画",
-                "value": "Japanese comics/manga"
-            }, {
-                "label": "水墨",
-                "value": "Ink Wash Painting Style"
-            }, {
-                "label": "原画风格",
-                "value": "Original"
-            }, {
-               "label": "山水画",
-                "value": "landscape"
-            }, {
-                "label": "插画",
-                "value": "illustration"
-            }, {
-                "label": "漫画",
-                "value": "Manga"
-            }, {
-                "label": "现代自然",
-                "value": "modern organic"
-            }, {
-                "label": "创世纪",
-                "value": "Genesis"
-            }, {
-                "label": "海报",
-                "value": "posterstyle"
-            }, {
-                "label": "超现实主义",
-                "value": "surrealism"
-            }, {
-                "label": "素描",
-                "value": "sketch"
-            }, {
-                "label": "写实",
-                "value": "realism"
-            }, {
-                "label": "水彩绘画",
-                "value": "Watercolor painting"
-            }, {
-                "label": "立体派",
-                "value": "Cubism"
-            }, {
-                "label": "黑白",
-                "value": "black and white"
-            }, {
-                "label": "电影摄影风格",
-                "value": "fm photography"
-            }, {
-                "label": "电影般的",
-                "value": "cinematic"
-            }, {
-                "label": "清晰的面部特征",
-                "value": "dlear facial features"
-            }],
-    "viewList": [{
-                "label": "宽景",
-                "value": "Wide view"
-            }, {
-                "label": "鸟瞰",
-                "value": "Bird view"
-            }, {
-                "label": "俯视",
-                "value": "Top view"
-            }, {
-                "label": "仰视",
-                "value": "Upview"
-            }, {
-                "label": "正面",
-                "value": "Front view"
-            }, {
-                "label": "头部特写",
-                "value": "Headshot"
-            }, {
-                "label": "超广角",
-                "value": "Ultrawideshot"
-            }, {
-                "label": "中景",
-                "value": "Medium Shot(MS)"
-            }, {
-                "label": "远景",
-                "value": "Long Shot(LS)"
-            }, {
-                "label": "景深",
-                "value": "depth offield(dof)"
-            }],
-    "shotList": [{
-                "label": "脸部特写",
-                "value": "Face Shot (VCU)"
-            }, {
-                "label": "头部以上",
-                "value": "Big Close-Up(BCU)"
-            }, {
-                "label": "颈部以上",
-                "value": "Close-Up(CU)"
-            }, {
-                "label": "腰部以上",
-                "value": "Waist Shot(WS)"
-            }, {
-                "label": "膝盖以上",
-                "value": "KneeShot(KS)"
-            }, {
-                "label": "全身",
-                "value": "Full Length Shot(FLS)"
-            }, {
-                "label": "人在远方",
-                "value": "Extra Long Shot(ELS)"
-            }],
-    "stylesList":[
-            {
-                "label": "Style low",
-                "value": "--s 50"
-            }, {
-                "label": "Style med",
-                "value": "--s 100"
-            }, {
-                "label": "Style high",
-                "value": "--s 250"
-            }, {
-                "label": "Style very high",
-                "value": "--s 750"
-            }
-    ],        
-    "lightList": [{
-                "label": "冷光",
-                "value": "Cold light"
-            }, {
-                "label": "暖光",
-                "value": "Warm light"
-            }, {
-                "label": "强光",
-                "value": "hard lighting"
-            }, {
-                "label": "戏剧光",
-                "value": "Dramatic light"
-            }, {
-                "label": "反光",
-                "value": "reflection light"
-            }, {
-                "label": "雾气朦胧",
-                "value": "Misty foggy"
-            }, {
-                "label": "自然光",
-                "value": "Natural light"
-            }, {
-                "label": "太阳光",
-                "value": "Sun light"
-            }, {
-                "label": "暗黑的",
-                "value": "moody"
-            }],
-     "versionList": [
-            {
-                "label": "MJ V6",
-                "value": "--v 6.0"
-            }, {
-                "label": "MJ V5.2",
-                "value": "--v 5.2"
-            }, {
-                "label": "MJ V5.1",
-                "value": "--v 5.1" 
-            }, {
-                "label": "二次元 niji V6",
-                "value": "--niji 6"
-            }, {
-                "label": "二次元 niji V5",
-                "value": "--niji 5"
-            }, {
-                "label": "二次元 niji V4",
-                "value": "--niji 4"
-                
-            },{
-                "label": "Niji・journey",
-                "value": "NIJI_JOURNEY"  
-          
-            }
-     ],
-
-     "botList": [{
-                "label": "Midjourney Bot",
-                "value": "MID_JOURNEY"
-            }, {
-                "label": "Niji・journey",
-                "value": "NIJI_JOURNEY"  
-            }
-     ],
-     "dimensionsList": [{
-                "label": "广场 SQUARE(1:1)",
-                "value": "SQUARE"
-            }, {
-                "label": "肖像 PORTRAIT(2:3)",
-                "value": "PORTRAIT"  
-            }, {
-                "label": "景观 LANDSCAPE(3:2)",
-                "value": "LANDSCAPE"  
-            }
-     ]
-     
-}
+	"qualityList": [
+		{
+			"labelKey": "draw.qualityList.general",
+			"value": "0.25"
+		},
+		{
+			"labelKey": "draw.qualityList.clear",
+			"value": "0.5"
+		},
+		{
+			"labelKey": "draw.qualityList.hd",
+			"value": "1"
+		},
+		{
+			"labelKey": "draw.qualityList.ultraHd",
+			"value": "2"
+		}
+	],
+	"styleList": [
+		{
+			"labelKey": "draw.styleList.cyberpunk",
+			"value": "Cyberpunk"
+		},
+		{
+			"labelKey": "draw.styleList.star",
+			"value": "Warframe"
+		},
+		{
+			"labelKey": "draw.styleList.anime",
+			"value": "ACGN"
+		},
+		{
+			"labelKey": "draw.styleList.japaneseComicsManga",
+			"value": "Japanese comics/manga"
+		},
+		{
+			"labelKey": "draw.styleList.inkWashPaintingStyle",
+			"value": "Ink Wash Painting Style"
+		},
+		{
+			"labelKey": "draw.styleList.original",
+			"value": "Original"
+		},
+		{
+			"labelKey": "draw.styleList.landscape",
+			"value": "landscape"
+		},
+		{
+			"labelKey": "draw.styleList.illustration",
+			"value": "illustration"
+		},
+		{
+			"labelKey": "draw.styleList.manga",
+			"value": "Manga"
+		},
+		{
+			"labelKey": "draw.styleList.modernOrganic",
+			"value": "modern organic"
+		},
+		{
+			"labelKey": "draw.styleList.genesis",
+			"value": "Genesis"
+		},
+		{
+			"labelKey": "draw.styleList.posterstyle",
+			"value": "posterstyle"
+		},
+		{
+			"labelKey": "draw.styleList.surrealism",
+			"value": "surrealism"
+		},
+		{
+			"labelKey": "draw.styleList.sketch",
+			"value": "sketch"
+		},
+		{
+			"labelKey": "draw.styleList.realism",
+			"value": "realism"
+		},
+		{
+			"labelKey": "draw.styleList.watercolorPainting",
+			"value": "Watercolor painting"
+		},
+		{
+			"labelKey": "draw.styleList.cubism",
+			"value": "Cubism"
+		},
+		{
+			"labelKey": "draw.styleList.blackAndWhite",
+			"value": "black and white"
+		},
+		{
+			"labelKey": "draw.styleList.fmPhotography",
+			"value": "fm photography"
+		},
+		{
+			"labelKey": "draw.styleList.cinematic",
+			"value": "cinematic"
+		},
+		{
+			"labelKey": "draw.styleList.clearFacialFeatures",
+			"value": "dlear facial features"
+		}
+	],
+	"viewList": [
+		{
+			"labelKey": "draw.viewList.wideView",
+			"value": "Wide view"
+		},
+		{
+			"labelKey": "draw.viewList.birdView",
+			"value": "Aerial view"
+		},
+		{
+			"labelKey": "draw.viewList.topView",
+			"value": "Top view"
+		},
+		{
+			"labelKey": "draw.viewList.upview",
+			"value": "Upview"
+		},
+		{
+			"labelKey": "draw.viewList.frontView",
+			"value": "Front view"
+		},
+		{
+			"labelKey": "draw.viewList.headshot",
+			"value": "Headshot"
+		},
+		{
+			"labelKey": "draw.viewList.ultrawideshot",
+			"value": "Ultrawideshot"
+		},
+		{
+			"labelKey": "draw.viewList.mediumShot",
+			"value": "Medium Shot(MS)"
+		},
+		{
+			"labelKey": "draw.viewList.longShot",
+			"value": "Long Shot(LS)"
+		},
+		{
+			"labelKey": "draw.viewList.depthOfField",
+			"value": "depth offield(dof)"
+		}
+	],
+	"shotList": [
+		{
+			"labelKey": "draw.shotList.faceShot",
+			"value": "Face Shot (VCU)"
+		},
+		{
+			"labelKey": "draw.shotList.bigCloseUp",
+			"value": "Big Close-Up(BCU)"
+		},
+		{
+			"labelKey": "draw.shotList.closeUp",
+			"value": "Close-Up(CU)"
+		},
+		{
+			"labelKey": "draw.shotList.waistShot",
+			"value": "Waist Shot(WS)"
+		},
+		{
+			"labelKey": "draw.shotList.kneeShot",
+			"value": "KneeShot(KS)"
+		},
+		{
+			"labelKey": "draw.shotList.fullLengthShot",
+			"value": "Full Length Shot(FLS)"
+		},
+		{
+			"labelKey": "draw.shotList.extraLongShot",
+			"value": "Extra Long Shot(ELS)"
+		}
+	],
+	"stylesList": [
+		{
+			"labelKey": "draw.stylesList.styleLow",
+			"value": "--s 50"
+		},
+		{
+			"labelKey": "draw.stylesList.styleMed",
+			"value": "--s 100"
+		},
+		{
+			"labelKey": "draw.stylesList.styleHigh",
+			"value": "--s 250"
+		},
+		{
+			"labelKey": "draw.stylesList.styleVeryHigh",
+			"value": "--s 750"
+		}
+	],
+	"lightList": [
+		{
+			"labelKey": "draw.lightList.coldLight",
+			"value": "Cold light"
+		},
+		{
+			"labelKey": "draw.lightList.warmLight",
+			"value": "Warm light"
+		},
+		{
+			"labelKey": "draw.lightList.hardLighting",
+			"value": "hard lighting"
+		},
+		{
+			"labelKey": "draw.lightList.dramaticLight",
+			"value": "Dramatic light"
+		},
+		{
+			"labelKey": "draw.lightList.reflectionLight",
+			"value": "reflection light"
+		},
+		{
+			"labelKey": "draw.lightList.mistyFoggy",
+			"value": "Misty foggy"
+		},
+		{
+			"labelKey": "draw.lightList.naturalLight",
+			"value": "Natural light"
+		},
+		{
+			"labelKey": "draw.lightList.sunLight",
+			"value": "Sun light"
+		},
+		{
+			"labelKey": "draw.lightList.moody",
+			"value": "moody"
+		}
+	],
+	"versionList": [
+		{
+			"labelKey": "draw.versionList.mjV6",
+			"value": "--v 6.0"
+		},
+		{
+			"labelKey": "draw.versionList.mjV52",
+			"value": "--v 5.2"
+		},
+		{
+			"labelKey": "draw.versionList.mjV51",
+			"value": "--v 5.1"
+		},
+		{
+			"labelKey": "draw.versionList.nijiV6",
+			"value": "--niji 6"
+		},
+		{
+			"labelKey": "draw.versionList.nijiV5",
+			"value": "--niji 5"
+		},
+		{
+			"labelKey": "draw.versionList.nijiV4",
+			"value": "--niji 4"
+		},
+		{
+			"labelKey": "draw.versionList.nijiJourney",
+			"value": "NIJI_JOURNEY"
+		}
+	],
+	"botList": [
+		{
+			"labelKey": "draw.botList.midjourneyBot",
+			"value": "MID_JOURNEY"
+		},
+		{
+			"labelKey": "draw.botList.nijiJourney",
+			"value": "NIJI_JOURNEY"
+		}
+	],
+	"dimensionsList": [
+		{
+			"labelKey": "draw.dimensionsList.square",
+			"value": "SQUARE"
+		},
+		{
+			"labelKey": "draw.dimensionsList.portrait",
+			"value": "PORTRAIT"
+		},
+		{
+			"labelKey": "draw.dimensionsList.landscape",
+			"value": "LANDSCAPE"
+		}
+	]
+}

+ 1 - 0
src/views/mj/mjText.vue

@@ -302,4 +302,5 @@ load();
 <style>
 .markdown-body img.maxCss,img.maxCss ,.maxCss img  { max-width: 400px!important; max-height: 400px!important;}
 .mmWidth{ max-width: 600px;}
+html.dark .markdown-body pre code { color:#abb2bf; }
 </style>

+ 324 - 0
src/views/sound/index.vue

@@ -0,0 +1,324 @@
+<script setup lang="ts">
+
+import {  h, onMounted, ref } from 'vue'
+import { NButton,NDataTable,DrawerPlacement,NDrawer,NDrawerContent,
+  NForm,NFormItem,NInput,NDivider,NSpace,UploadFileInfo,NUpload,NUploadDragger,
+  NText,NIcon,NP,useMessage,NModal,NSlider,NProgress
+} from 'naive-ui'
+import { getToken } from '@/store/modules/auth/helper'
+import { createRole,getRole,simpleGenerateReq } from '@/api/voice'
+import { Icon } from '@iconify/vue';
+import to from "await-to-js";
+
+onMounted(() => { fetchData() });
+
+const token = getToken()
+
+const message = useMessage()
+
+const audioUrl = ref()
+
+// 是否显示进度条 
+const isPercentage = ref(false)
+
+
+// 定义进度值为响应式数据
+const percentage = ref(0);
+
+function increaseProgress() {
+  // 打开进度条
+  isPercentage.value = true
+  const interval = setInterval(() => {
+    // 只要进度小于99,就随机增加进度
+    if (percentage.value < 99) {
+      // 随机增加的进度值,这里假设每次增加1-5之间的随机值
+      percentage.value += Math.floor(Math.random() * 5) + 1;
+      // 防止进度超过99
+      if (percentage.value > 99) {
+        percentage.value = 99;
+      }
+    } else {
+      // 达到或超过99,停止增加
+      clearInterval(interval);
+    }
+  }, 1000); // 每1000毫秒(即1秒)更新一次进度
+}
+
+const headers = {
+  Authorization: `Bearer ${token}`
+}
+
+
+
+// 初始化表单数据对象
+const formValue = ref({
+  name: '',
+  description: '',
+  prompt: '',
+});
+
+// 初始化表单数据对象
+const simpleGenerate = ref({
+  model: 'reecho-neural-voice-001',
+  randomness: 97,
+  stability_boost: 0,
+  voiceId: '',
+  text: ''
+});
+
+function handleFinish({event,file}: {
+  file: UploadFileInfo
+  event?: ProgressEvent
+}) {
+  const ext = (event?.target as XMLHttpRequest).response
+  // ext 转成json对象
+  const fileData =  JSON.parse(ext);
+  formValue.value.prompt = fileData.data.url
+  console.log("ext==================="+fileData.data.url,file.file?.size)
+  message.success('上传成功!')
+}
+
+ async function submitForm() {
+  //关闭弹框
+  active.value = false
+  // 发起一个请求
+  const [err, result] = await to(createRole(formValue.value));
+	  console.log("result===", result)
+    if (err) {
+      message.error(err.message)
+    } else {
+      message.success('角色创建成功!')
+    }
+    // 处理响应
+    console.log('角色创建成功',formValue.value);
+}
+
+async function submitSimpleGenerate() {
+  try {
+    //打开进度条
+  increaseProgress();
+   //发起一个请求
+  const [err, result] = await to(simpleGenerateReq(simpleGenerate.value));
+  console.log("语音生成成功result===", result)
+  if (err) {
+    // 关闭进度条
+    isPercentage.value = false
+		message.error(err.message)
+    return
+	}else{
+    message.success("语音生成成功,消耗点数:"+result?.data.credit_used)
+  }
+
+  audioUrl.value  = result?.data.audio
+  if(audioUrl.value){
+      // 关闭进度条
+      isPercentage.value = false
+  }
+  } catch (error) {
+    // 处理错误
+    console.error('语音生成失败', error);
+  }
+}
+
+function handleActionButtonClick(row: any): void {
+  simpleGenerate.value.voiceId = row.voiceId
+  showModal.value = true
+}
+
+// 使用 ref 来创建响应式变量
+const active = ref(false)
+
+const placement = ref<DrawerPlacement>('right')
+
+const showModal = ref(false)
+// 定义一个激活抽屉的函数,接受一个 DrawerPlacement 类型的参数
+const activate = (place: DrawerPlacement) => {
+  active.value = true
+  placement.value = place
+}
+
+
+const createColumns = () => {
+  return [
+  ...(false
+      ? [{
+          title: '角色ID',
+          key: 'voiceId',
+          width: 80,
+          ellipsis: true,
+        }]
+      : []),
+    {
+      title: '角色名称',
+      key: 'name'
+    },
+    {
+      title: '角色描述',
+      key: 'description'
+    },
+  
+    {
+      title: '预览',
+      key: 'fileUrl',
+      render: (row: { fileUrl: string | undefined; }) => {
+        return h(NButton, {
+          icon: () =>
+            h(NIcon, null, {
+              default: () => h(Icon, { icon: 'ic:baseline-play-arrow' }),
+            }),
+          onClick: () => {
+            const audio = new Audio(row.fileUrl);
+            audio.play();
+          },
+        }, '播放');
+      },
+    },
+    {
+      title: '操作',
+      key: 'actions',
+      render: (row: any) => {
+        return h(NButton, {
+          onClick: () => handleActionButtonClick(row)
+        }, { default: () => '生成语音' })
+      }
+    }
+  ]
+}
+
+  const fetchData = async () => {
+    try {
+       // 发起一个请求
+      const [err, result] = await to(getRole());
+      console.log("result===", result)
+      if (err) {
+        message.error(err.message)
+      } else {
+        tableData.value = result;
+      }
+    } catch (error) {
+      console.error('Error fetching data:', error);
+    }
+  };
+
+const tableData = ref([]);
+const columns = ref(createColumns());
+
+</script>
+<template>
+<br>
+<div style="display: flex; justify-content: flex-start; margin:10px;">
+    <n-button @click="activate('right')" type="primary">
+       创建角色
+    </n-button>
+</div>
+
+<div class="flex h-full"> 
+    <main class="flex-1 overflow-hidden h-full">
+      <n-data-table :columns="columns" :data="tableData" />
+    </main>
+</div>
+
+<n-drawer v-model:show="active" :width="502" :placement="placement">
+      <n-drawer-content title="添加角色">
+          在这里添加你的角色
+          <n-divider />
+          <n-space vertical>
+            <n-form ref="formRef" >
+              <n-form-item
+                label="角色名称"  path="formValue.name">
+                <n-input  v-model:value="formValue.name" placeholder="输入角色名称" />
+              </n-form-item>
+              <n-form-item label="角色描述" path="formValue.description">
+                <n-input v-model:value="formValue.description"  placeholder="输入角色描述" />
+              </n-form-item>
+              <n-form-item label="音频样本">
+                <!-- @before-upload="beforeUpload" -->
+                <n-upload
+                  directory-dnd
+                  action="/api/resource/oss/upload"
+                  name="file"
+                  :headers="headers"
+                  @finish="handleFinish"
+                  :max="1">
+                      <n-upload-dragger>
+                        <div style="margin-bottom: 12px">
+                          <n-icon size="48" :depth="3">
+                            <archive-icon />
+                          </n-icon>
+                        </div>
+                        <n-text style="font-size: 16px">
+                          请上传一个5MB以内的音频文件
+                        </n-text>
+                        <n-p depth="3" style="margin: 8px 0 0 0">
+                          *样本语音需大于2秒,样本质量比长度更重要,过长的样本反而可能导致效果不如预期,通常将样本长度控制在5-8秒的最具代表性片段即可。
+                        </n-p>
+                      </n-upload-dragger>
+                </n-upload>
+              </n-form-item>
+              <n-col :span="24"
+              >
+                <div style="display: flex; justify-content: flex-end">
+                  <n-button @click="submitForm"  type="primary">
+                    添加
+                  </n-button>
+                </div>
+              </n-col>
+
+            </n-form>
+          </n-space>
+      </n-drawer-content>
+</n-drawer>
+
+<n-modal v-model:show="showModal" title="生成语音" :auto-focus="false" preset="card"
+		style="width: 95%; max-width: 500px;">
+  <n-input maxlength="1000"
+    type="textarea"
+    v-model:value="simpleGenerate.text"
+    placeholder="建议填写不超过50个字符的单句以保证最佳效果(消耗1元/1000字符)" 
+  />
+  <n-space vertical>
+    <br>
+    <section class=" flex justify-between items-center"  >
+      <div> 多样性 (0-100,默认为97)
+      </div>
+      <div class=" flex justify-end items-center w-[80%] max-w-[240px]">
+          <div class=" w-[200px]"><n-slider v-model:value="simpleGenerate.randomness" :step="1" :max="100" /></div>
+          <div  class="w-[40px] text-right">{{ simpleGenerate.randomness }}</div>
+      </div>
+    </section>  
+    <div class="mb-4 text-[12px] text-gray-300 dark:text-gray-300/20">控制语音生成的多样性,值越大,生成的语音将具备更高的表现力上限与随机性范围;应为0到100的整数,建议不小于95</div>
+
+    <section class=" flex justify-between items-center"  >
+      <div> 稳定性过滤 (0-100,默认为0)
+      </div>
+      <div class=" flex justify-end items-center w-[80%] max-w-[240px]">
+          <div class=" w-[200px]"><n-slider v-model:value="simpleGenerate.stability_boost" :step="1" :max="100" /></div>
+          <div  class="w-[40px] text-right">{{ simpleGenerate.stability_boost }}</div>
+      </div>
+    </section>  
+    <div class="mb-4 text-[12px] text-gray-300 dark:text-gray-300/20">限制在生成过程中仅选择前n个最佳的路径,值越小,生成的语音通常越平淡与稳定,但同时也可能导致表达某些内容或音色时的效果下降或异常;应为1到100的整数,为0时关闭限制,启用时建议不小于40</div>
+
+    
+    <!-- 进度条 -->
+    <n-progress v-if="isPercentage" :percentage="percentage"></n-progress>
+
+    <audio v-if="audioUrl" :src="audioUrl" controls></audio>
+    
+  </n-space>
+
+
+
+  <br>
+  <div style="display: flex; justify-content: flex-end">
+                  <n-button @click="submitSimpleGenerate"  type="primary">
+                    开始生成
+                  </n-button>
+  </div>
+
+
+
+</n-modal>
+		
+
+</template>
+

+ 6 - 10
vite.config.ts

@@ -7,11 +7,11 @@ import { VitePWA } from 'vite-plugin-pwa'
 function setupPlugins(env: ImportMetaEnv): PluginOption[] {
   return [
     vue(),
-    VitePWA({ // env.VITE_GLOB_APP_PWA === 'true' &&
+    env.VITE_GLOB_APP_PWA === 'true' && VitePWA({
       injectRegister: 'auto',
       manifest: {
-        name: 'chatGPT-MJ',
-        short_name: 'chatGPT-MJ',
+        name: 'chatGPT',
+        short_name: 'chatGPT',
         icons: [
           { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
           { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
@@ -41,22 +41,18 @@ export default defineConfig((env) => {
           changeOrigin: true, // 允许跨域
           rewrite: path => path.replace('/api/', '/'),
         },
-        '/mjapi': {
-          target: viteEnv.VITE_MJ_API_BASE_URL,
-          changeOrigin: true, // 允许跨域
-          rewrite: path => path.replace('/mjapi/', '/'),
-        },
+     
          '/uploads': {
           target: viteEnv.VITE_APP_API_BASE_URL,
           changeOrigin: true, // 允许跨域
           //rewrite: path => path.replace('/api/', '/'),
-        },
+        }, 
         '/openapi': {
           target: viteEnv.VITE_APP_API_BASE_URL,
           changeOrigin: true, // 允许跨域
           //rewrite: path => path.replace('/api/', '/'),
         },
-
+        
       },
     },
     build: {