mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	Compare commits
	
		
			185 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					18c033d57f | ||
| 
						 | 
					b676f80110 | ||
| 
						 | 
					f7fbaa534d | ||
| 
						 | 
					ea93a22e14 | ||
| 
						 | 
					9f7e6778c5 | ||
| 
						 | 
					6c31a2bfa6 | ||
| 
						 | 
					f943669e18 | ||
| 
						 | 
					3b26735998 | ||
| 
						 | 
					79d25769ee | ||
| 
						 | 
					1dd6800987 | ||
| 
						 | 
					5e673a9ee0 | ||
| 
						 | 
					92eb67a2af | ||
| 
						 | 
					b1bed59be2 | ||
| 
						 | 
					0ac732a3a3 | ||
| 
						 | 
					bf3f68fa19 | ||
| 
						 | 
					46a551df16 | ||
| 
						 | 
					20a12462b1 | ||
| 
						 | 
					a49fb1940e | ||
| 
						 | 
					32774d23c7 | ||
| 
						 | 
					7ecd7eeba1 | ||
| 
						 | 
					0cc9cf8b45 | ||
| 
						 | 
					d06f94bddd | ||
| 
						 | 
					b5955f08c9 | ||
| 
						 | 
					c120569894 | ||
| 
						 | 
					aa376f1737 | ||
| 
						 | 
					0f8a0f89e3 | ||
| 
						 | 
					68dc261b44 | ||
| 
						 | 
					4cf3af0c7b | ||
| 
						 | 
					b99b6735d9 | ||
| 
						 | 
					52189b7880 | ||
| 
						 | 
					3dbeb1ccb6 | ||
| 
						 | 
					5a0f272fa8 | ||
| 
						 | 
					6561b99f8f | ||
| 
						 | 
					329e3eee21 | ||
| 
						 | 
					07049c9afb | ||
| 
						 | 
					36c5dd7eaa | ||
| 
						 | 
					b84039b506 | ||
| 
						 | 
					fab43097dc | ||
| 
						 | 
					c8998ba294 | ||
| 
						 | 
					40b2466adc | ||
| 
						 | 
					35fedbe817 | ||
| 
						 | 
					827acdd3f9 | ||
| 
						 | 
					6c76086916 | ||
| 
						 | 
					373370fde5 | ||
| 
						 | 
					2165ba3406 | ||
| 
						 | 
					b0e02b43fc | ||
| 
						 | 
					2107c13b3d | ||
| 
						 | 
					5f41aecc8d | ||
| 
						 | 
					6840a13370 | ||
| 
						 | 
					8f1e28c0ab | ||
| 
						 | 
					7903eed284 | ||
| 
						 | 
					0d49ea0d41 | ||
| 
						 | 
					2ee4db5e48 | ||
| 
						 | 
					48c4789505 | ||
| 
						 | 
					4e65a5b1a1 | ||
| 
						 | 
					b09d23f97f | ||
| 
						 | 
					3529649ba9 | ||
| 
						 | 
					fdd659f393 | ||
| 
						 | 
					9eb8da2789 | ||
| 
						 | 
					ffb1ef0470 | ||
| 
						 | 
					862c6aea43 | ||
| 
						 | 
					54fe4b7588 | ||
| 
						 | 
					c6062ee70e | ||
| 
						 | 
					bed184dc1f | ||
| 
						 | 
					29094ba3b3 | ||
| 
						 | 
					a18188876c | ||
| 
						 | 
					4faee3e48e | ||
| 
						 | 
					1a6afcd266 | ||
| 
						 | 
					f567831d92 | ||
| 
						 | 
					cf36ca4285 | ||
| 
						 | 
					0e4ae01498 | ||
| 
						 | 
					7b90f8cb13 | ||
| 
						 | 
					c33215529a | ||
| 
						 | 
					c5be114db2 | ||
| 
						 | 
					cab955c292 | ||
| 
						 | 
					ca8c8e6490 | ||
| 
						 | 
					253951b4b3 | ||
| 
						 | 
					4d6444ebf3 | ||
| 
						 | 
					94d8d8a9d4 | ||
| 
						 | 
					e02badf7bb | ||
| 
						 | 
					dd88622c64 | ||
| 
						 | 
					c4d7126c4d | ||
| 
						 | 
					86bc063941 | ||
| 
						 | 
					dce85eb519 | ||
| 
						 | 
					4ab879d697 | ||
| 
						 | 
					681e52df50 | ||
| 
						 | 
					fb554c0315 | ||
| 
						 | 
					accf8eeb77 | ||
| 
						 | 
					3e41edd3b5 | ||
| 
						 | 
					9126cfff20 | ||
| 
						 | 
					9806d5ff4c | ||
| 
						 | 
					d1d13a72e4 | ||
| 
						 | 
					00c520d066 | ||
| 
						 | 
					797ff66474 | ||
| 
						 | 
					9d51a478b9 | ||
| 
						 | 
					1d4179df75 | ||
| 
						 | 
					917b6012e8 | ||
| 
						 | 
					da14632794 | ||
| 
						 | 
					a868a8a8b7 | ||
| 
						 | 
					5037df744f | ||
| 
						 | 
					da88a501ad | ||
| 
						 | 
					b9885e8de4 | ||
| 
						 | 
					22efe81080 | ||
| 
						 | 
					2926717aef | ||
| 
						 | 
					a49d54d66c | ||
| 
						 | 
					ce0267e25b | ||
| 
						 | 
					9088d22a66 | ||
| 
						 | 
					1ff32d5d0a | ||
| 
						 | 
					adf6916598 | ||
| 
						 | 
					31c14bf748 | ||
| 
						 | 
					5395385d1e | ||
| 
						 | 
					0035da548b | ||
| 
						 | 
					9bceaade05 | ||
| 
						 | 
					3194becdad | ||
| 
						 | 
					6174b17c24 | ||
| 
						 | 
					53fa4a20e9 | ||
| 
						 | 
					43c1de51f5 | ||
| 
						 | 
					7eb8c5ec35 | ||
| 
						 | 
					296bf63196 | ||
| 
						 | 
					6c65a21692 | ||
| 
						 | 
					daf83cfc84 | ||
| 
						 | 
					871f5d39e4 | ||
| 
						 | 
					3f91f37aff | ||
| 
						 | 
					a08981f876 | ||
| 
						 | 
					5187a43543 | ||
| 
						 | 
					6a733de556 | ||
| 
						 | 
					b9e9eae93f | ||
| 
						 | 
					811f12135a | ||
| 
						 | 
					2c172c0851 | ||
| 
						 | 
					399a16fa28 | ||
| 
						 | 
					d971e95900 | ||
| 
						 | 
					0b6940b121 | ||
| 
						 | 
					ad0f96fcb1 | ||
| 
						 | 
					063b5655f7 | ||
| 
						 | 
					d03ed6570b | ||
| 
						 | 
					1795a891ce | ||
| 
						 | 
					05bdd81646 | ||
| 
						 | 
					cba54be913 | ||
| 
						 | 
					da0acfe851 | ||
| 
						 | 
					133a127d8a | ||
| 
						 | 
					8da9b52eae | ||
| 
						 | 
					2700b63887 | ||
| 
						 | 
					17713d05ec | ||
| 
						 | 
					3674d9da85 | ||
| 
						 | 
					0e6606e469 | ||
| 
						 | 
					feff1684c4 | ||
| 
						 | 
					81e08e02ff | ||
| 
						 | 
					f593526bd4 | ||
| 
						 | 
					d244ad9983 | ||
| 
						 | 
					65a01f4776 | ||
| 
						 | 
					5f812ae649 | ||
| 
						 | 
					fda811de97 | ||
| 
						 | 
					fe7f021ddb | ||
| 
						 | 
					120e54fb29 | ||
| 
						 | 
					cd809d17d3 | ||
| 
						 | 
					e7fd29b9cb | ||
| 
						 | 
					c008f33bc3 | ||
| 
						 | 
					7b5cacd6f1 | ||
| 
						 | 
					d88219c726 | ||
| 
						 | 
					a3ee7ca2d8 | ||
| 
						 | 
					6f37024e34 | ||
| 
						 | 
					3347b4c990 | ||
| 
						 | 
					6a2122e1ac | ||
| 
						 | 
					4b024e0ad7 | ||
| 
						 | 
					b06a065d44 | ||
| 
						 | 
					15b77ad10a | ||
| 
						 | 
					180bd2a1db | ||
| 
						 | 
					06fb7b41b4 | ||
| 
						 | 
					088fd14c03 | ||
| 
						 | 
					1e84332119 | ||
| 
						 | 
					01d61ab19b | ||
| 
						 | 
					b4569d7fe2 | ||
| 
						 | 
					a9ecaed5bd | ||
| 
						 | 
					654057e7a7 | ||
| 
						 | 
					f4e5265aaf | ||
| 
						 | 
					85d85fbe10 | ||
| 
						 | 
					d4bd071487 | ||
| 
						 | 
					3cee291997 | ||
| 
						 | 
					1849d3ddaa | ||
| 
						 | 
					7c6dfa545a | ||
| 
						 | 
					9a72e51a55 | ||
| 
						 | 
					c2d5da27e1 | ||
| 
						 | 
					176e614457 | ||
| 
						 | 
					a008b89b42 | ||
| 
						 | 
					fb2108d88b | 
							
								
								
									
										47
									
								
								.github/ISSUE_TEMPLATE/1.bug.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								.github/ISSUE_TEMPLATE/1.bug.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
name: Bug 报告 🐛
 | 
			
		||||
description: 为 chatgpt-plus 提交错误报告
 | 
			
		||||
labels: ['Bug']
 | 
			
		||||
body:
 | 
			
		||||
  - type: checkboxes
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: ⚠️  确认 issue 是否已存在 ⚠️
 | 
			
		||||
      description: 在提交 Issue 之前,请在 issue 列表搜索一下,确保你不是在提交一个重复的 issue。
 | 
			
		||||
      options:
 | 
			
		||||
        - label: 我已经搜索了现有的问题,没有找到跟我问题相关的问题。
 | 
			
		||||
          required: true
 | 
			
		||||
  - type: dropdown
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: GPT-3 or GPT-4
 | 
			
		||||
      description: 请选择你使用的 GPT 模型
 | 
			
		||||
      options:
 | 
			
		||||
        - GPT-3.5
 | 
			
		||||
        - GPT-4
 | 
			
		||||
    validations:
 | 
			
		||||
      required: true
 | 
			
		||||
  - type: dropdown
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: 操作系统
 | 
			
		||||
      description: 请选择你使用的操作系统
 | 
			
		||||
      options:
 | 
			
		||||
        - Windows
 | 
			
		||||
        - Linux
 | 
			
		||||
        - MacOS
 | 
			
		||||
    validations:
 | 
			
		||||
      required: true
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: Bug 描述 📝
 | 
			
		||||
      description: 请简单描述你发现的问题。
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: 重现步骤 🕹
 | 
			
		||||
      description: |
 | 
			
		||||
        **⚠️  无法重现的 issue 将会被关闭**
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: 你的应用配置信息
 | 
			
		||||
      description: 请提供你的配置文档,**请注意数据脱敏**。
 | 
			
		||||
      value: |
 | 
			
		||||
        ```toml
 | 
			
		||||
        把你的配置信息粘贴到这里
 | 
			
		||||
        ```
 | 
			
		||||
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE/2.feature.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.github/ISSUE_TEMPLATE/2.feature.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
name: 功能优化 🚀
 | 
			
		||||
description: 为 chatgpt-plus 提交优化建议
 | 
			
		||||
labels: ['feature']
 | 
			
		||||
body:
 | 
			
		||||
  - type: checkboxes
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: ⚠️  确认 issue 是否已存在 ⚠️
 | 
			
		||||
      description: >
 | 
			
		||||
        在提交 Issue 之前,请在 issue 列表搜索一下,确保你不是在提交一个重复的 issue。
 | 
			
		||||
      options:
 | 
			
		||||
        - label: 我已经搜索了现有的问题,没有找到相关 issue。
 | 
			
		||||
          required: true
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: 功能描述 📝
 | 
			
		||||
      description: |
 | 
			
		||||
        描述此功能该如何工作。
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: 示例 🌈
 | 
			
		||||
      description: 是否可以提供类似的样例,比如提供参考项目的链接或者截图。
 | 
			
		||||
  - type: textarea
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: 动机 🔦
 | 
			
		||||
      description: 为什么要新增或者优化这个功能,缺少这个功能会给你或者其他用户带来什么不便?
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
<!-- 📢 注意:
 | 
			
		||||
 | 
			
		||||
在提交的 PR 的时候请确保每个 PR 只包含一个功能修改或者优化,请不要把多个更改组合到一个 PR 提交。保持干净可管理的 git 历史记录至关重要。 为确保我们存储库的质量,我们恳请您在提交 PR 时遵守以下准则:
 | 
			
		||||
 | 
			
		||||
1. 每个 PR 专注于一个单一的、具体的改进。
 | 
			
		||||
2. 不要包括任何不相关或[额外]的修改。
 | 
			
		||||
3. 为所做的更改提供清晰的文档和解释。
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
### Background
 | 
			
		||||
<!-- 简要概述此更改背后的基本原理。 包括相关背景、先前的讨论或相关 issue 的链接。 确保变更与项目的总体方向一致。-->
 | 
			
		||||
 | 
			
		||||
### Changes
 | 
			
		||||
<!-- 简要说明你改动的内容  -->
 | 
			
		||||
 | 
			
		||||
### Test Plan
 | 
			
		||||
<!-- 描述您如何测试此功能。 包括重现步骤、相关测试用例和任何其他相关信息-->
 | 
			
		||||
 | 
			
		||||
### PR 规则验证列表
 | 
			
		||||
- [ ] 确保本次 PR 只包含单一的功能修改。
 | 
			
		||||
- [ ] 我已经对我的代码更改进行了充分的测试。
 | 
			
		||||
- [ ] 我已经考虑了我的更改的潜在风险和缓解措施。
 | 
			
		||||
- [ ] 我已经修正了相关文档 
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,15 +1,7 @@
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
pnpm-debug.log*
 | 
			
		||||
lerna-debug.log*
 | 
			
		||||
 | 
			
		||||
node_modules
 | 
			
		||||
src/dist
 | 
			
		||||
dist-ssr
 | 
			
		||||
*.local
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
@@ -22,8 +14,3 @@ dist-ssr
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
src/tmp
 | 
			
		||||
src/bin
 | 
			
		||||
src/data
 | 
			
		||||
web/.env.development
 | 
			
		||||
config.toml
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										101
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
			
		||||
# 更新日志
 | 
			
		||||
 | 
			
		||||
## v3.1.2
 | 
			
		||||
1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。
 | 
			
		||||
2. 功能新增:新增桌面版,使用 electron 套壳网页版。
 | 
			
		||||
3. Bug修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。
 | 
			
		||||
4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。
 | 
			
		||||
5. 功能优化:取消导出聊天页面的授权验证
 | 
			
		||||
6. 功能优化:所有路由跳转都使用绝对路径
 | 
			
		||||
 | 
			
		||||
## v3.1.1
 | 
			
		||||
紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug
 | 
			
		||||
 | 
			
		||||
## v3.1.0(大版本更新)
 | 
			
		||||
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及 ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。
 | 
			
		||||
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
 | 
			
		||||
3. 功能重构:重构文件夹上传服务,支持多种文件上传存储handler,目前已经实现本地存储和 minio oss 存储。
 | 
			
		||||
4. 功能优化:更新头像自动删除旧的图片资源。
 | 
			
		||||
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
 | 
			
		||||
6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。
 | 
			
		||||
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
 | 
			
		||||
8. 升级 gin, element-plus,redis 组件到最新版本。 
 | 
			
		||||
9. Bug修复:修复若干已知的的 Bug
 | 
			
		||||
 | 
			
		||||
## v3.0.7
 | 
			
		||||
 | 
			
		||||
1. 聊天主界面:新增聊天引导页面,介绍产品功能
 | 
			
		||||
2. 功能重构:拆分项目,将函数插件以及微信机器人,MidJourney 机器人等功能拆分新项目独立部署。
 | 
			
		||||
3. 功能新增:新增 MidJourney AI 绘画支持,当识别到用户的绘画需求时,自动调用 MidJourney 绘画函数进行绘画。
 | 
			
		||||
4. 功能新增:支持导出聊天记录为 PDF 文件。
 | 
			
		||||
5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。
 | 
			
		||||
6. 功能优化:支持用户设置默认的 GPT 模型
 | 
			
		||||
7. Bug修复:修复若干已知的的 Bug
 | 
			
		||||
 | 
			
		||||
## v3.0.6
 | 
			
		||||
 | 
			
		||||
1. 管理后台:新增用户名和手机号码搜索功能
 | 
			
		||||
2. 管理后台:新增重置用户密码功能
 | 
			
		||||
3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景
 | 
			
		||||
4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗
 | 
			
		||||
5. Bug修复:修复注册页面验证码不显示 Bug
 | 
			
		||||
6. Bug修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
 | 
			
		||||
7. 功能修正:允许用户使用手机号码登录
 | 
			
		||||
8. 功能优化:更新系统配置后同步更新服务端内存变量数据
 | 
			
		||||
9. 功能优化:优化打包脚本,减少容器镜像大小
 | 
			
		||||
 | 
			
		||||
## v3.0.5
 | 
			
		||||
 | 
			
		||||
重磅功能更新!!! 新增函数插件支持,可以轻松地接入你的第三方插件服务,ChatGPT 自动帮您调用对应的函数完成任务。
 | 
			
		||||
 | 
			
		||||
1. 新增函数功能支持,全球早报,今日头条和微博热搜等插件服务,您也可以接入自己的第三方服务。
 | 
			
		||||
2. 集成微信机器人模块,可以通过微信个人收款码来完成充值,无需接入微信支付功能也可以完成收款功能。
 | 
			
		||||
3. 用户注册添加短信验证码功能,引入交互安全认证服务,有效防刷短信。
 | 
			
		||||
4. 支持配置聊天上下文深度,精确统计每轮对话所消耗的总 TOKEN 数量。
 | 
			
		||||
5. 修复已知的 Bug。
 | 
			
		||||
 | 
			
		||||
## v3.0.4
 | 
			
		||||
 | 
			
		||||
1. 调整项目目录结构,移除其他语言 API 目录
 | 
			
		||||
2. 修复 nodejs apple M1 跨平台打包,运行报错 exec format error
 | 
			
		||||
3. 增加用户 token 消耗统计功能
 | 
			
		||||
 | 
			
		||||
## v3.0.3
 | 
			
		||||
 | 
			
		||||
1. 优化启动参数接收处理,支持环境变量传参
 | 
			
		||||
2. 修复 PC 端聊天界面出现滚动条的 Bug
 | 
			
		||||
3. 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题
 | 
			
		||||
4. 更改 docker 构建镜像的基础镜像,改用作者的阿里云镜像,这样打包更快一些。
 | 
			
		||||
 | 
			
		||||
## v3.0.2
 | 
			
		||||
 | 
			
		||||
1. Feat:新增移动端的聊天和用户设置功能
 | 
			
		||||
2. Fix: 修复 markdown 换行符解析的 Bug
 | 
			
		||||
3. Feat: 新增头像上传功能
 | 
			
		||||
4. Docs: 增加容器部署支持,支持 docker-compose 一键部署
 | 
			
		||||
5. Fix: 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug
 | 
			
		||||
 | 
			
		||||
## v3.0.1
 | 
			
		||||
 | 
			
		||||
1. 紧急修复前端 Home 组件路由被后台管理 Home 组件路由覆盖的 Bug。
 | 
			
		||||
2. 增加 docker-compose 部署脚本
 | 
			
		||||
 | 
			
		||||
## v3.0.0
 | 
			
		||||
 | 
			
		||||
全新的重构版本!!!
 | 
			
		||||
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
 | 
			
		||||
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。
 | 
			
		||||
前后台技术架构还是基于 `Vue3 + Element-Plus` ,但是页面风格已经全部变了,几乎所有页面样式代码都重写了,希望会你是希望的风格!
 | 
			
		||||
 | 
			
		||||
此次重构改版主要是为了后面功能的扩展准备了。
 | 
			
		||||
 | 
			
		||||
新版本已经实现的功能如下:
 | 
			
		||||
 | 
			
		||||
1. 引入用户体系,新增用户注册和登录功能。
 | 
			
		||||
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
 | 
			
		||||
3. 创建会话的时候可以选择聊天角色和模型。
 | 
			
		||||
4. 新增聊天设置功能,用户可以导入自己的 API KEY
 | 
			
		||||
5. 保存聊天记录,支持聊天上下文。
 | 
			
		||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
 | 
			
		||||
7. 引入 ip2region 组件,记录用户的登录IP和地址。
 | 
			
		||||
8. 支持会话搜索过滤。
 | 
			
		||||
							
								
								
									
										359
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										359
									
								
								README.md
									
									
									
									
									
								
							@@ -1,68 +1,333 @@
 | 
			
		||||
# ChatGPT-Plus
 | 
			
		||||
 | 
			
		||||
基于 OpenAI API 实现的 ChatGPT Web 应用,一共分为两个版本:
 | 
			
		||||
**ChatGPT-PLUS** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure, ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。主要有如下特性:
 | 
			
		||||
 | 
			
		||||
* 通用版:交互体验跟 ChatGPT 官方一致,聊天记录保存在客户端(浏览器)
 | 
			
		||||
* 角色版:内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
 | 
			
		||||
聊天记录保存在云端(可以配置是否保存聊天记录)
 | 
			
		||||
每个版本都有 PC 版和移动版,PC 版本的体验要略优于移动版。
 | 
			
		||||
 | 
			
		||||
**本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。如需商用建议联系作者登记,仅做统计使用,优秀项目我们将在项目首页为您展示。**
 | 
			
		||||
 | 
			
		||||
## 项目介绍
 | 
			
		||||
这一套完整的系统,包括两套前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供 ChatGPT 的服务。
 | 
			
		||||
 | 
			
		||||
项目的技术架构是
 | 
			
		||||
 | 
			
		||||
> Go + Vue3 + element-plus
 | 
			
		||||
 | 
			
		||||
后端采用的是 Go 语言开发的 Gin Web 框架。前端用的是 Vue3 + element-plus UI 框架
 | 
			
		||||
 | 
			
		||||
目前已经实现了以下功能:
 | 
			
		||||
1. 通用版的 ChatGPT 聊天界面和功能,聊天记录保存在客户端。
 | 
			
		||||
2. 口令机制:输入口令才可以访问,支持设置口令的对话次数,有效期。
 | 
			
		||||
3. 角色版的聊天界面和功能,角色设定,预设一些角色,比如程序员,客服,作家,老师,艺术家... 
 | 
			
		||||
4. 保存聊天记录,支持聊天上下文。 
 | 
			
		||||
5. OpenAI API 负载均衡,限制每个 API Key 每分钟之内调用次数不超过 15次,防止被封。 
 | 
			
		||||
6. 支持配置多个代理,保证高可用。 
 | 
			
		||||
7. 实现 markdown 语法解析和代码高亮,支持复制回复内容功能。 
 | 
			
		||||
8. 后台管理功能,实现系统的动态配置,用户和角色的动态管理。
 | 
			
		||||
* 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
 | 
			
		||||
* 聊天体验跟 ChatGPT 官方版本完全一致。
 | 
			
		||||
* 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
 | 
			
		||||
* 支持 MidJourney AI 绘画集成,开箱即用。
 | 
			
		||||
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持)
 | 
			
		||||
* 集成插件 API 功能,可结合 GPT 开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。
 | 
			
		||||
 | 
			
		||||
## 功能截图
 | 
			
		||||
 | 
			
		||||
### 1. 角色版PC端
 | 
			
		||||

 | 
			
		||||
### PC 端聊天界面
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 2. 角色版移动端
 | 
			
		||||

 | 
			
		||||
### 新版聊天界面
 | 
			
		||||
 | 
			
		||||
### 3. 通用版
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 4. 管理后台
 | 
			
		||||

 | 
			
		||||
### 自动调用函数插件
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 5. 体验地址
 | 
			
		||||
> 体验地址:[https://www.chat-plus.net/chat/#/free](https://www.chat-plus.net/chat/#/free) </br>
 | 
			
		||||
> 口令:GeekMaster
 | 
			
		||||
### 用户设置
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 登录页面
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 管理后台
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 移动端 Web 页面
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 7. 体验地址
 | 
			
		||||
 | 
			
		||||
> 免费体验地址:[https://ai.r9it.com/chat](https://ai.r9it.com/chat) <br/>
 | 
			
		||||
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
 | 
			
		||||
 | 
			
		||||
## 使用须知
 | 
			
		||||
 | 
			
		||||
1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
 | 
			
		||||
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
 | 
			
		||||
 | 
			
		||||
## 项目介绍
 | 
			
		||||
 | 
			
		||||
这一套完整的系统,包括前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供
 | 
			
		||||
ChatGPT 的服务。
 | 
			
		||||
 | 
			
		||||
### 项目的技术架构
 | 
			
		||||
 | 
			
		||||
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
 | 
			
		||||
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。
 | 
			
		||||
 | 
			
		||||
> Gin + fx + MySQL
 | 
			
		||||
 | 
			
		||||
3.0 版本之后会陆续添加其他语言的 API 实现,比如 PHP,Java 等。考虑到作者精力有限,api 目录已经添加了,有兴趣的同学自主去认领各自擅长的语言去实现。
 | 
			
		||||
 | 
			
		||||
前端的框架还是:
 | 
			
		||||
 | 
			
		||||
> Vue3 + Element-Plus
 | 
			
		||||
 | 
			
		||||
前后台的页面风格已经全部变了,几乎所有页面样式代码都重写了。逻辑代码还是沿用之前的,毕竟功能没有太大的变化。
 | 
			
		||||
 | 
			
		||||
此次重构改版主要是为了后面功能的扩展准备了。
 | 
			
		||||
 | 
			
		||||
新版本已经实现的功能如下:
 | 
			
		||||
 | 
			
		||||
1. 引入用户体系,新增用户注册和登录功能。
 | 
			
		||||
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
 | 
			
		||||
3. 创建会话的时候可以选择聊天角色和模型。
 | 
			
		||||
4. 新增聊天设置功能,用户可以导入自己的 API KEY
 | 
			
		||||
5. 保存聊天记录,支持聊天上下文。
 | 
			
		||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
 | 
			
		||||
7. 引入 ip2region 组件,记录用户的登录IP和地址。
 | 
			
		||||
8. 支持会话搜索过滤。
 | 
			
		||||
9. 支持微信支付充值
 | 
			
		||||
 | 
			
		||||
## 项目地址
 | 
			
		||||
 | 
			
		||||
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
 | 
			
		||||
* 码云地址:https://gitee.com/blackfox/chatgpt-plus
 | 
			
		||||
 | 
			
		||||
## TODOLIST
 | 
			
		||||
* [ ] 让用户配置自己的 API KEY,调用自己的 API Key,将不记 Token 的使用次数
 | 
			
		||||
* [ ] 嵌入 AI 绘画功能,支持根据描述词生成图片
 | 
			
		||||
* [ ] 接入自己训练的开源大语言模型
 | 
			
		||||
* [ ] 接入 Google 语音 API,支持语音聊天
 | 
			
		||||
 | 
			
		||||
## 本地部署
 | 
			
		||||
## 线上发布
 | 
			
		||||
* [x] 整合 Midjourney AI 绘画 API
 | 
			
		||||
* [x] 开发移动端聊天页面
 | 
			
		||||
* [x] 接入微信支付功能
 | 
			
		||||
* [x] 支持 ChatGPT 函数功能,通过函数实现插件
 | 
			
		||||
* [ ] 支持基于知识库的 AI 问答
 | 
			
		||||
* [ ] 开发桌面版应用
 | 
			
		||||
* [ ] 开发手机 App 客户端
 | 
			
		||||
 | 
			
		||||
## 注意事项
 | 
			
		||||
## Docker 快速部署
 | 
			
		||||
 | 
			
		||||
>
 | 
			
		||||
鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。
 | 
			
		||||
> 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。
 | 
			
		||||
 | 
			
		||||
V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。
 | 
			
		||||
 | 
			
		||||
### 1. 导入数据库
 | 
			
		||||
 | 
			
		||||
首先我们需要创建一个 MySQL 容器,并导入初始数据库。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd docker/mysql
 | 
			
		||||
# 创建 mysql 容器
 | 
			
		||||
docker-compose up -d
 | 
			
		||||
# 导入数据库
 | 
			
		||||
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus.sql
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
# 连接数据库
 | 
			
		||||
mysql -u username -p password
 | 
			
		||||
# 导入数据库
 | 
			
		||||
source database/chatgpt_plus.sql
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 2. 修改配置文档
 | 
			
		||||
 | 
			
		||||
修改配置文档 `docker/conf/config.toml` 配置文档,修改代理地址和管理员密码:
 | 
			
		||||
 | 
			
		||||
```toml
 | 
			
		||||
Listen = "0.0.0.0:5678"
 | 
			
		||||
ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理,如:http://127.0.0.1:7777
 | 
			
		||||
#ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了
 | 
			
		||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
 | 
			
		||||
 | 
			
		||||
[Session]
 | 
			
		||||
  SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
 | 
			
		||||
  MaxAge = 86400
 | 
			
		||||
 | 
			
		||||
[Manager]
 | 
			
		||||
  Username = "admin"
 | 
			
		||||
  Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
 | 
			
		||||
  
 | 
			
		||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
 | 
			
		||||
  ApiURL = "{URL}"
 | 
			
		||||
  AppId = "{APP_ID}"
 | 
			
		||||
  Token = "{TOKEN}"
 | 
			
		||||
 | 
			
		||||
[SmsConfig] # 阿里云短信服务配置
 | 
			
		||||
  AccessKey = "{YOUR_ACCESS_KEY}"
 | 
			
		||||
  AccessSecret = "{YOUR_SECRET_KEY}"
 | 
			
		||||
  Product = "Dysmsapi"
 | 
			
		||||
  Domain = "dysmsapi.aliyuncs.com"
 | 
			
		||||
 | 
			
		||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
 | 
			
		||||
  ApiURL = "插件扩展 API 地址"
 | 
			
		||||
  Token = "插件扩展 API Token" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行 
 | 
			
		||||
  
 | 
			
		||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
 | 
			
		||||
   Active = "local" # 默认使用本地文件存储引擎
 | 
			
		||||
   [OSS.Local]
 | 
			
		||||
     BasePath = "./static/upload" # 本地文件上传根路径
 | 
			
		||||
     BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
 | 
			
		||||
   [OSS.Minio]
 | 
			
		||||
     Endpoint = "IP:端口" # 如 172.22.11.200:9000
 | 
			
		||||
     AccessKey = "minio oss access key" # 自己去 Minio 控制台去创建一个 Access Key
 | 
			
		||||
     AccessSecret = "minio oss access secret"
 | 
			
		||||
     Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
 | 
			
		||||
     UseSSL = false
 | 
			
		||||
     Domain = "minio 文件公开访问地址" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
 | 
			
		||||
   [OSS.QiNiu] # 七牛云 OSS 配置
 | 
			
		||||
       Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
 | 
			
		||||
       AccessKey = "七牛云 OSS AccessKey"
 | 
			
		||||
       AccessSecret = "七牛云 OSS AccessSecret"
 | 
			
		||||
       Bucket = "七牛云 OSS Bucket"
 | 
			
		||||
       Domain = "OSS Bucket 所绑定的域名,如 https://img.r9it.com"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> 如果要启用微信收款服务和 MidJourney
 | 
			
		||||
> 绘画功能,请先部署扩展服务项目 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts)。
 | 
			
		||||
 | 
			
		||||
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
 # 这里配置后端 API 的转发
 | 
			
		||||
location /api/ {
 | 
			
		||||
       proxy_http_version 1.1;
 | 
			
		||||
       proxy_connect_timeout 300s;
 | 
			
		||||
       proxy_read_timeout 300s;
 | 
			
		||||
       proxy_send_timeout 12s;
 | 
			
		||||
       proxy_set_header Host $host;
 | 
			
		||||
       proxy_set_header X-Real-IP $remote_addr;
 | 
			
		||||
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
       proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
       proxy_set_header Connection $connection_upgrade;
 | 
			
		||||
       proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 3. 启动应用
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd docker
 | 
			
		||||
docker-compose up -d
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* 前端访问地址:http://localhost:8080/chat
 | 
			
		||||
* 后台管理地址:http://localhost:8080/admin
 | 
			
		||||
* 移动端地址:http://localhost:8080/mobile
 | 
			
		||||
 | 
			
		||||
> 注意:你得访问后台管理系统 http://localhost:8080/admin
 | 
			
		||||
> 输入你前面配置文档中设置的管理员用户名和密码登录。
 | 
			
		||||
> 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 才可以正常开启 AI 对话。
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
最后登录前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat)
 | 
			
		||||
你可以注册新用户,也可以使用系统默认有个账号:`geekmaster/12345678` 登录聊天。
 | 
			
		||||
 | 
			
		||||
祝你使用愉快!!!
 | 
			
		||||
 | 
			
		||||
## 本地开发调试
 | 
			
		||||
 | 
			
		||||
本地开发同样要分别运行前端和后端程序。
 | 
			
		||||
 | 
			
		||||
### 运行后端程序
 | 
			
		||||
 | 
			
		||||
1. 同样你首先要 [导入数据库](#1-导入数据库)
 | 
			
		||||
2. 然后 [修改配置文档](#2-修改配置文档)
 | 
			
		||||
3. 运行后端程序:
 | 
			
		||||
 | 
			
		||||
    ```shell
 | 
			
		||||
    cd api 
 | 
			
		||||
    # 1. 先下载依赖
 | 
			
		||||
    go mod tidy
 | 
			
		||||
    # 2. 运行程序
 | 
			
		||||
    go run main.go
 | 
			
		||||
    # 如果你安装了 fresh 可以使用 fresh 实现热启动
 | 
			
		||||
    fresh -c fresh.conf
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
### 运行前端程序
 | 
			
		||||
 | 
			
		||||
同样先拷贝配置文档:
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd web
 | 
			
		||||
cp .env.production .env.development
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
编辑 `.env.development` 文件,修改后端 API 的访问路径:
 | 
			
		||||
 | 
			
		||||
```ini
 | 
			
		||||
VUE_APP_API_HOST=http://localhost:5678
 | 
			
		||||
VUE_APP_WS_HOST=ws://localhost:5678
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
配置好了之后就可以运行前端应用了:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
# 安装依赖
 | 
			
		||||
npm install
 | 
			
		||||
# 运行
 | 
			
		||||
npm run dev
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* 前端页面:http://localhost:8888/chat
 | 
			
		||||
* 后台管理页面:http://localhost:8888/admin
 | 
			
		||||
 | 
			
		||||
## 项目打包
 | 
			
		||||
 | 
			
		||||
由于本项目是采用异构开发的方式,所项目打包分成两步:首先编译后端程序,然后再打包前端应用。
 | 
			
		||||
 | 
			
		||||
### 打包前端
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd web
 | 
			
		||||
npm run build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 打包后端
 | 
			
		||||
 | 
			
		||||
你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd api
 | 
			
		||||
# for all platforms
 | 
			
		||||
make clean all
 | 
			
		||||
# for linux only
 | 
			
		||||
make clean linux
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
打包后的可执行文件在 `bin` 目录下。
 | 
			
		||||
 | 
			
		||||
## 参与贡献
 | 
			
		||||
 | 
			
		||||
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
 | 
			
		||||
 | 
			
		||||
如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 特此声明:不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
 | 
			
		||||
 | 
			
		||||
### Commit 类型
 | 
			
		||||
 | 
			
		||||
* feat: 新特性或功能
 | 
			
		||||
* fix: 缺陷修复
 | 
			
		||||
* docs: 文档更新
 | 
			
		||||
* style: 代码风格或者组件样式更新
 | 
			
		||||
* refactor: 代码重构,不引入新功能和缺陷修复
 | 
			
		||||
* opt: 性能优化
 | 
			
		||||
* chore: 一些不涉及到功能变动的小提交,比如修改文字表述,修改注释等
 | 
			
		||||
 | 
			
		||||
## 打赏
 | 
			
		||||
 | 
			
		||||
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								api/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
.idea
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
tmp
 | 
			
		||||
bin
 | 
			
		||||
data
 | 
			
		||||
config.toml
 | 
			
		||||
static/upload 
 | 
			
		||||
storage.json
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
SHELL=/usr/bin/env bash
 | 
			
		||||
NAME := wechatGPT
 | 
			
		||||
NAME := chatgpt-plus
 | 
			
		||||
all: window linux darwin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -12,7 +12,7 @@ linux:
 | 
			
		||||
.PHONY: linux
 | 
			
		||||
 | 
			
		||||
darwin:
 | 
			
		||||
	CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
 | 
			
		||||
	CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
 | 
			
		||||
.PHONY: darwin
 | 
			
		||||
 | 
			
		||||
clean:
 | 
			
		||||
							
								
								
									
										5
									
								
								api/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								api/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# chatgpt-plus-go
 | 
			
		||||
 | 
			
		||||
chatgpt-plus 后端 API Go 语言实现。技术选型采用 Gin + Mysql 架构,依赖注入使用的是 fx 框架,ORM 采用的是 GORM 框架。
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										53
									
								
								api/config.sample.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								api/config.sample.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
Listen = "0.0.0.0:5678"
 | 
			
		||||
ProxyURL = "http://172.22.11.200:7777"
 | 
			
		||||
MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
 | 
			
		||||
StaticDir = "./static"
 | 
			
		||||
StaticUrl = "http://localhost:5678/static"
 | 
			
		||||
AesEncryptKey = "{YOUR_AES_KEY}"
 | 
			
		||||
 | 
			
		||||
[Session]
 | 
			
		||||
  SecretKey = "m0cjm3gsuw9jk73np1ni7r42koilybjcndlycjdmq7za3pbqn7w12fyok5pqh6q5"
 | 
			
		||||
  MaxAge = 86400
 | 
			
		||||
 | 
			
		||||
[Manager]
 | 
			
		||||
  Username = "admin"
 | 
			
		||||
  Password = "admin123"
 | 
			
		||||
 | 
			
		||||
[Redis]
 | 
			
		||||
  Host = "localhost"
 | 
			
		||||
  Port = 6379
 | 
			
		||||
  Password = ""
 | 
			
		||||
 | 
			
		||||
[ApiConfig]
 | 
			
		||||
  ApiURL = "{URL}"
 | 
			
		||||
  AppId = "{APP_ID}"
 | 
			
		||||
  Token = "{TOKEN}"
 | 
			
		||||
 | 
			
		||||
[SmsConfig]
 | 
			
		||||
  AccessKey = "{YOUR_ACCESS_KEY}"
 | 
			
		||||
  AccessSecret = "{YOUR_SECRET_KEY}"
 | 
			
		||||
  Product = "Dysmsapi"
 | 
			
		||||
  Domain = "dysmsapi.aliyuncs.com"
 | 
			
		||||
 | 
			
		||||
[ExtConfig]
 | 
			
		||||
  ApiURL = "插件扩展 API 地址"
 | 
			
		||||
  Token = "插件扩展 API Token"
 | 
			
		||||
 | 
			
		||||
 [OSS]
 | 
			
		||||
   Active = "local"
 | 
			
		||||
   [OSS.Local]
 | 
			
		||||
     BasePath = "./static/upload"
 | 
			
		||||
     BaseURL = "http://localhost:5678/static/upload"
 | 
			
		||||
   [OSS.Minio]
 | 
			
		||||
     Endpoint = "IP:端口"
 | 
			
		||||
     AccessKey = "minio oss access key"
 | 
			
		||||
     AccessSecret = "minio oss access secret"
 | 
			
		||||
     Bucket = "minio oss bucket"
 | 
			
		||||
     UseSSL = false
 | 
			
		||||
     Domain = "minio 文件公开访问地址"
 | 
			
		||||
   [OSS.QiNiu] # 七牛云 OSS 配置
 | 
			
		||||
       Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
 | 
			
		||||
       AccessKey = "七牛云 OSS AccessKey"
 | 
			
		||||
       AccessSecret = "七牛云 OSS AccessSecret"
 | 
			
		||||
       Bucket = "七牛云 OSS Bucket"
 | 
			
		||||
       Domain = "OSS Bucket 所绑定的域名,如 https://img.r9it.com"
 | 
			
		||||
							
								
								
									
										208
									
								
								api/core/app_server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								api/core/app_server.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,208 @@
 | 
			
		||||
package core
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/function"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"runtime/debug"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AppServer struct {
 | 
			
		||||
	Debug        bool
 | 
			
		||||
	Config       *types.AppConfig
 | 
			
		||||
	Engine       *gin.Engine
 | 
			
		||||
	ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message
 | 
			
		||||
 | 
			
		||||
	ChatConfig *types.ChatConfig   // chat config cache
 | 
			
		||||
	SysConfig  *types.SystemConfig // system config cache
 | 
			
		||||
 | 
			
		||||
	// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次
 | 
			
		||||
	// 防止第三方直接连接 socket 调用 OpenAI API
 | 
			
		||||
	ChatSession   *types.LMap[string, *types.ChatSession] //map[sessionId]UserId
 | 
			
		||||
	ChatClients   *types.LMap[string, *types.WsClient]    // map[sessionId]Websocket 连接集合
 | 
			
		||||
	ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
 | 
			
		||||
	Functions     map[string]function.Function
 | 
			
		||||
	MjTaskClients *types.LMap[string, *types.WsClient]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewServer(appConfig *types.AppConfig, functions map[string]function.Function) *AppServer {
 | 
			
		||||
	gin.SetMode(gin.ReleaseMode)
 | 
			
		||||
	gin.DefaultWriter = io.Discard
 | 
			
		||||
	return &AppServer{
 | 
			
		||||
		Debug:         false,
 | 
			
		||||
		Config:        appConfig,
 | 
			
		||||
		Engine:        gin.Default(),
 | 
			
		||||
		ChatContexts:  types.NewLMap[string, []interface{}](),
 | 
			
		||||
		ChatSession:   types.NewLMap[string, *types.ChatSession](),
 | 
			
		||||
		ChatClients:   types.NewLMap[string, *types.WsClient](),
 | 
			
		||||
		ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
 | 
			
		||||
		MjTaskClients: types.NewLMap[string, *types.WsClient](),
 | 
			
		||||
		Functions:     functions,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *AppServer) Init(debug bool, client *redis.Client) {
 | 
			
		||||
	if debug { // 调试模式允许跨域请求 API
 | 
			
		||||
		s.Debug = debug
 | 
			
		||||
		logger.Info("Enabled debug mode")
 | 
			
		||||
	}
 | 
			
		||||
	s.Engine.Use(corsMiddleware())
 | 
			
		||||
	s.Engine.Use(authorizeMiddleware(s, client))
 | 
			
		||||
	s.Engine.Use(errorHandler)
 | 
			
		||||
	// 添加静态资源访问
 | 
			
		||||
	s.Engine.Static("/static", s.Config.StaticDir)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *AppServer) Run(db *gorm.DB) error {
 | 
			
		||||
	// load chat config from database
 | 
			
		||||
	var chatConfig model.Config
 | 
			
		||||
	res := db.Where("marker", "chat").First(&chatConfig)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return res.Error
 | 
			
		||||
	}
 | 
			
		||||
	err := utils.JsonDecode(chatConfig.Config, &s.ChatConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// load system configs
 | 
			
		||||
	var sysConfig model.Config
 | 
			
		||||
	res = db.Where("marker", "system").First(&sysConfig)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return res.Error
 | 
			
		||||
	}
 | 
			
		||||
	err = utils.JsonDecode(sysConfig.Config, &s.SysConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	logger.Infof("http://%s", s.Config.Listen)
 | 
			
		||||
	return s.Engine.Run(s.Config.Listen)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 全局异常处理
 | 
			
		||||
func errorHandler(c *gin.Context) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			logger.Errorf("Handler Panic: %v", r)
 | 
			
		||||
			debug.PrintStack()
 | 
			
		||||
			c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
 | 
			
		||||
			c.Abort()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	//加载完 defer recover,继续后续接口调用
 | 
			
		||||
	c.Next()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 跨域中间件设置
 | 
			
		||||
func corsMiddleware() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		method := c.Request.Method
 | 
			
		||||
		origin := c.Request.Header.Get("Origin")
 | 
			
		||||
		if origin != "" {
 | 
			
		||||
			// 设置允许的请求源
 | 
			
		||||
			c.Header("Access-Control-Allow-Origin", origin)
 | 
			
		||||
			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
 | 
			
		||||
			//允许跨域设置可以返回其他子段,可以自定义字段
 | 
			
		||||
			c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Chat-Token, Admin-Authorization")
 | 
			
		||||
			// 允许浏览器(客户端)可以解析的头部 (重要)
 | 
			
		||||
			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
 | 
			
		||||
			//设置缓存时间
 | 
			
		||||
			c.Header("Access-Control-Max-Age", "172800")
 | 
			
		||||
			//允许客户端传递校验信息比如 cookie (重要)
 | 
			
		||||
			c.Header("Access-Control-Allow-Credentials", "true")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if method == http.MethodOptions {
 | 
			
		||||
			c.JSON(http.StatusOK, "ok!")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		defer func() {
 | 
			
		||||
			if err := recover(); err != nil {
 | 
			
		||||
				logger.Info("Panic info is: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		c.Next()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 用户授权验证
 | 
			
		||||
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		if c.Request.URL.Path == "/api/user/login" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/admin/login" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/user/register" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/reward/notify" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/mj/notify" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/chat/history" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/chat/detail" ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/static/") ||
 | 
			
		||||
			c.Request.URL.Path == "/api/admin/config/get" {
 | 
			
		||||
			c.Next()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var tokenString string
 | 
			
		||||
		if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
 | 
			
		||||
			tokenString = c.GetHeader(types.AdminAuthHeader)
 | 
			
		||||
		} else if c.Request.URL.Path == "/api/chat/new" {
 | 
			
		||||
			tokenString = c.Query("token")
 | 
			
		||||
		} else {
 | 
			
		||||
			tokenString = c.GetHeader(types.UserAuthHeader)
 | 
			
		||||
		}
 | 
			
		||||
		if tokenString == "" {
 | 
			
		||||
			resp.ERROR(c, "You should put Authorization in request headers")
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
 | 
			
		||||
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 | 
			
		||||
				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return []byte(s.Config.Session.SecretKey), nil
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err))
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		claims, ok := token.Claims.(jwt.MapClaims)
 | 
			
		||||
		if !ok || !token.Valid {
 | 
			
		||||
			resp.NotAuth(c, "Token is invalid")
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
 | 
			
		||||
		if expr > 0 && int64(expr) < time.Now().Unix() {
 | 
			
		||||
			resp.NotAuth(c, "Token is expired")
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		key := fmt.Sprintf("users/%v", claims["user_id"])
 | 
			
		||||
		if _, err := client.Get(context.Background(), key).Result(); err != nil {
 | 
			
		||||
			resp.NotAuth(c, "Token is not found in redis")
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		c.Set(types.LoginUserID, claims["user_id"])
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										71
									
								
								api/core/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								api/core/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
package core
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/BurntSushi/toml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
func NewDefaultConfig() *types.AppConfig {
 | 
			
		||||
	return &types.AppConfig{
 | 
			
		||||
		Listen:        "0.0.0.0:5678",
 | 
			
		||||
		ProxyURL:      "",
 | 
			
		||||
		Manager:       types.Manager{Username: "admin", Password: "admin123"},
 | 
			
		||||
		StaticDir:     "./static",
 | 
			
		||||
		StaticUrl:     "http://localhost/5678/static",
 | 
			
		||||
		Redis:         types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
 | 
			
		||||
		AesEncryptKey: utils.RandString(24),
 | 
			
		||||
		Session: types.Session{
 | 
			
		||||
			SecretKey: utils.RandString(64),
 | 
			
		||||
			MaxAge:    86400,
 | 
			
		||||
		},
 | 
			
		||||
		ApiConfig: types.ChatPlusApiConfig{},
 | 
			
		||||
		ExtConfig: types.ChatPlusExtConfig{Token: utils.RandString(32)},
 | 
			
		||||
		OSS: types.OSSConfig{
 | 
			
		||||
			Active: "local",
 | 
			
		||||
			Local: types.LocalStorageConfig{
 | 
			
		||||
				BaseURL:  "http://localhost/5678/static/upload",
 | 
			
		||||
				BasePath: "./static/upload",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LoadConfig(configFile string) (*types.AppConfig, error) {
 | 
			
		||||
	var config *types.AppConfig
 | 
			
		||||
	_, err := os.Stat(configFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Info("creating new config file: ", configFile)
 | 
			
		||||
		config = NewDefaultConfig()
 | 
			
		||||
		config.Path = configFile
 | 
			
		||||
		// save config
 | 
			
		||||
		err := SaveConfig(config)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return config, nil
 | 
			
		||||
	}
 | 
			
		||||
	_, err = toml.DecodeFile(configFile, &config)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return config, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SaveConfig(config *types.AppConfig) error {
 | 
			
		||||
	buf := new(bytes.Buffer)
 | 
			
		||||
	encoder := toml.NewEncoder(buf)
 | 
			
		||||
	if err := encoder.Encode(&config); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return os.WriteFile(config.Path, buf.Bytes(), 0644)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								api/core/types/chat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								api/core/types/chat.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
// ApiRequest API 请求实体
 | 
			
		||||
type ApiRequest struct {
 | 
			
		||||
	Model       string        `json:"model"`
 | 
			
		||||
	Temperature float32       `json:"temperature"`
 | 
			
		||||
	MaxTokens   int           `json:"max_tokens"`
 | 
			
		||||
	Stream      bool          `json:"stream"`
 | 
			
		||||
	Messages    []interface{} `json:"messages,omitempty"`
 | 
			
		||||
	Prompt      []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
 | 
			
		||||
	Functions   []Function    `json:"functions,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Message struct {
 | 
			
		||||
	Role    string `json:"role"`
 | 
			
		||||
	Content string `json:"content"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ApiResponse struct {
 | 
			
		||||
	Choices []ChoiceItem `json:"choices"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChoiceItem API 响应实体
 | 
			
		||||
type ChoiceItem struct {
 | 
			
		||||
	Delta        Delta  `json:"delta"`
 | 
			
		||||
	FinishReason string `json:"finish_reason"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Delta struct {
 | 
			
		||||
	Role         string       `json:"role"`
 | 
			
		||||
	Name         string       `json:"name"`
 | 
			
		||||
	Content      interface{}  `json:"content"`
 | 
			
		||||
	FunctionCall FunctionCall `json:"function_call,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChatSession 聊天会话对象
 | 
			
		||||
type ChatSession struct {
 | 
			
		||||
	SessionId string    `json:"session_id"`
 | 
			
		||||
	ClientIP  string    `json:"client_ip"` // 客户端 IP
 | 
			
		||||
	Username  string    `json:"username"`  // 当前登录的 username
 | 
			
		||||
	UserId    uint      `json:"user_id"`   // 当前登录的 user ID
 | 
			
		||||
	ChatId    string    `json:"chat_id"`   // 客户端聊天会话 ID, 多会话模式专用字段
 | 
			
		||||
	Model     ChatModel `json:"model"`     // GPT 模型
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatModel struct {
 | 
			
		||||
	Id       uint     `json:"id"`
 | 
			
		||||
	Platform Platform `json:"platform"`
 | 
			
		||||
	Value    string   `json:"value"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MjTask struct {
 | 
			
		||||
	ChatId      string
 | 
			
		||||
	MessageId   string
 | 
			
		||||
	MessageHash string
 | 
			
		||||
	UserId      uint
 | 
			
		||||
	RoleId      uint
 | 
			
		||||
	Icon        string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ApiError struct {
 | 
			
		||||
	Error struct {
 | 
			
		||||
		Message string
 | 
			
		||||
		Type    string
 | 
			
		||||
		Param   interface{}
 | 
			
		||||
		Code    string
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PromptMsg = "prompt" // prompt message
 | 
			
		||||
const ReplyMsg = "reply"   // reply message
 | 
			
		||||
const MjMsg = "mj"
 | 
			
		||||
 | 
			
		||||
var ModelToTokens = map[string]int{
 | 
			
		||||
	"gpt-3.5-turbo":     4096,
 | 
			
		||||
	"gpt-3.5-turbo-16k": 16384,
 | 
			
		||||
	"gpt-4":             8192,
 | 
			
		||||
	"gpt-4-32k":         32768,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TaskStorePrefix = "/tasks/"
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package server
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
@@ -6,18 +6,14 @@ import (
 | 
			
		||||
	"sync"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ErrConClosed = errors.New("connection closed")
 | 
			
		||||
 | 
			
		||||
type Client interface {
 | 
			
		||||
	Close()
 | 
			
		||||
}
 | 
			
		||||
var ErrConClosed = errors.New("connection Closed")
 | 
			
		||||
 | 
			
		||||
// WsClient websocket client
 | 
			
		||||
type WsClient struct {
 | 
			
		||||
	Conn   *websocket.Conn
 | 
			
		||||
	lock   sync.Mutex
 | 
			
		||||
	mt     int
 | 
			
		||||
	closed bool
 | 
			
		||||
	Closed bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewWsClient(conn *websocket.Conn) *WsClient {
 | 
			
		||||
@@ -25,7 +21,7 @@ func NewWsClient(conn *websocket.Conn) *WsClient {
 | 
			
		||||
		Conn:   conn,
 | 
			
		||||
		lock:   sync.Mutex{},
 | 
			
		||||
		mt:     2, // fixed bug for 'Invalid UTF-8 in text frame'
 | 
			
		||||
		closed: false,
 | 
			
		||||
		Closed: false,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +29,7 @@ func (wc *WsClient) Send(message []byte) error {
 | 
			
		||||
	wc.lock.Lock()
 | 
			
		||||
	defer wc.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	if wc.closed {
 | 
			
		||||
	if wc.Closed {
 | 
			
		||||
		return ErrConClosed
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -41,7 +37,7 @@ func (wc *WsClient) Send(message []byte) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (wc *WsClient) Receive() (int, []byte, error) {
 | 
			
		||||
	if wc.closed {
 | 
			
		||||
	if wc.Closed {
 | 
			
		||||
		return 0, nil, ErrConClosed
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -52,10 +48,10 @@ func (wc *WsClient) Close() {
 | 
			
		||||
	wc.lock.Lock()
 | 
			
		||||
	defer wc.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	if wc.closed {
 | 
			
		||||
	if wc.Closed {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_ = wc.Conn.Close()
 | 
			
		||||
	wc.closed = true
 | 
			
		||||
	wc.Closed = true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										127
									
								
								api/core/types/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								api/core/types/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AppConfig struct {
 | 
			
		||||
	Path          string `toml:"-"`
 | 
			
		||||
	Listen        string
 | 
			
		||||
	Session       Session
 | 
			
		||||
	ProxyURL      string
 | 
			
		||||
	MysqlDns      string            // mysql 连接地址
 | 
			
		||||
	Manager       Manager           // 后台管理员账户信息
 | 
			
		||||
	StaticDir     string            // 静态资源目录
 | 
			
		||||
	StaticUrl     string            // 静态资源 URL
 | 
			
		||||
	Redis         RedisConfig       // redis 连接信息
 | 
			
		||||
	ApiConfig     ChatPlusApiConfig // ChatPlus API authorization configs
 | 
			
		||||
	AesEncryptKey string
 | 
			
		||||
	SmsConfig     AliYunSmsConfig   // AliYun send message service config
 | 
			
		||||
	ExtConfig     ChatPlusExtConfig // ChatPlus extensions callback api config
 | 
			
		||||
 | 
			
		||||
	OSS OSSConfig // OSS config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatPlusApiConfig struct {
 | 
			
		||||
	ApiURL string
 | 
			
		||||
	AppId  string
 | 
			
		||||
	Token  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatPlusExtConfig struct {
 | 
			
		||||
	ApiURL string
 | 
			
		||||
	Token  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliYunSmsConfig struct {
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Product      string
 | 
			
		||||
	Domain       string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OSSConfig struct {
 | 
			
		||||
	Active string
 | 
			
		||||
	Local  LocalStorageConfig
 | 
			
		||||
	Minio  MinioConfig
 | 
			
		||||
	QiNiu  QiNiuConfig
 | 
			
		||||
}
 | 
			
		||||
type MinioConfig struct {
 | 
			
		||||
	Endpoint     string
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Bucket       string
 | 
			
		||||
	UseSSL       bool
 | 
			
		||||
	Domain       string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type QiNiuConfig struct {
 | 
			
		||||
	Zone         string
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Bucket       string
 | 
			
		||||
	Domain       string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LocalStorageConfig struct {
 | 
			
		||||
	BasePath string
 | 
			
		||||
	BaseURL  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RedisConfig struct {
 | 
			
		||||
	Host     string
 | 
			
		||||
	Port     int
 | 
			
		||||
	Password string
 | 
			
		||||
	DB       int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c RedisConfig) Url() string {
 | 
			
		||||
	return fmt.Sprintf("%s:%d", c.Host, c.Port)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Manager 管理员
 | 
			
		||||
type Manager struct {
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChatConfig 系统默认的聊天配置
 | 
			
		||||
type ChatConfig struct {
 | 
			
		||||
	OpenAI  ModelAPIConfig `json:"open_ai"`
 | 
			
		||||
	Azure   ModelAPIConfig `json:"azure"`
 | 
			
		||||
	ChatGML ModelAPIConfig `json:"chat_gml"`
 | 
			
		||||
 | 
			
		||||
	EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
 | 
			
		||||
	EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
 | 
			
		||||
	ContextDeep   int  `json:"context_deep"`   // 上下文深度
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Platform string
 | 
			
		||||
 | 
			
		||||
const OpenAI = Platform("OpenAI")
 | 
			
		||||
const Azure = Platform("Azure")
 | 
			
		||||
const ChatGLM = Platform("ChatGLM")
 | 
			
		||||
 | 
			
		||||
// UserChatConfig 用户的聊天配置
 | 
			
		||||
type UserChatConfig struct {
 | 
			
		||||
	ApiKeys map[Platform]string `json:"api_keys"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ModelAPIConfig struct {
 | 
			
		||||
	ApiURL      string  `json:"api_url,omitempty"`
 | 
			
		||||
	Temperature float32 `json:"temperature"`
 | 
			
		||||
	MaxTokens   int     `json:"max_tokens"`
 | 
			
		||||
	ApiKey      string  `json:"api_key"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SystemConfig struct {
 | 
			
		||||
	Title             string   `json:"title"`
 | 
			
		||||
	AdminTitle        string   `json:"admin_title"`
 | 
			
		||||
	Models            []string `json:"models"`
 | 
			
		||||
	UserInitCalls     int      `json:"user_init_calls"` // 新用户注册默认总送多少次调用
 | 
			
		||||
	InitImgCalls      int      `json:"init_img_calls"`
 | 
			
		||||
	VipMonthCalls     int      `json:"vip_month_calls"` // 会员每个赠送的调用次数
 | 
			
		||||
	EnabledRegister   bool     `json:"enabled_register"`
 | 
			
		||||
	EnabledMsgService bool     `json:"enabled_msg_service"`
 | 
			
		||||
	EnabledDraw       bool     `json:"enabled_draw"` // 启动 AI 绘画功能
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										104
									
								
								api/core/types/function.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								api/core/types/function.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
type FunctionCall struct {
 | 
			
		||||
	Name      string `json:"name"`
 | 
			
		||||
	Arguments string `json:"arguments"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Function struct {
 | 
			
		||||
	Name        string     `json:"name"`
 | 
			
		||||
	Description string     `json:"description"`
 | 
			
		||||
	Parameters  Parameters `json:"parameters"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Parameters struct {
 | 
			
		||||
	Type       string              `json:"type"`
 | 
			
		||||
	Required   []string            `json:"required"`
 | 
			
		||||
	Properties map[string]Property `json:"properties"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Property struct {
 | 
			
		||||
	Type        string `json:"type"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	FuncZaoBao     = "zao_bao"     // 每日早报
 | 
			
		||||
	FuncHeadLine   = "headline"    // 今日头条
 | 
			
		||||
	FuncWeibo      = "weibo_hot"   // 微博热搜
 | 
			
		||||
	FuncMidJourney = "mid_journey" // MJ 绘画
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var InnerFunctions = []Function{
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncZaoBao,
 | 
			
		||||
		Description: "每日早报,获取当天全球的热门新闻事件列表",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"text": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncWeibo,
 | 
			
		||||
		Description: "新浪微博热搜榜,微博当日热搜榜单",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"text": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncHeadLine,
 | 
			
		||||
		Description: "今日头条,给用户推荐当天的头条新闻,周榜热文",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"text": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncMidJourney,
 | 
			
		||||
		Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"prompt": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "绘画内容描述,提示词,如果该参数中有中文的话,则需要翻译成英文",
 | 
			
		||||
				},
 | 
			
		||||
				"ar": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "图片长宽比,默认值 16:9",
 | 
			
		||||
				},
 | 
			
		||||
				"niji": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "动漫模型版本,默认值空",
 | 
			
		||||
				},
 | 
			
		||||
				"v": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "模型版本,默认值: 5.2",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								api/core/types/locked_map.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/core/types/locked_map.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"sync"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MKey interface {
 | 
			
		||||
	string | int
 | 
			
		||||
}
 | 
			
		||||
type MValue interface {
 | 
			
		||||
	*WsClient | *ChatSession | context.CancelFunc | []interface{} | MjTask
 | 
			
		||||
}
 | 
			
		||||
type LMap[K MKey, T MValue] struct {
 | 
			
		||||
	lock sync.RWMutex
 | 
			
		||||
	data map[K]T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLMap[K MKey, T MValue]() *LMap[K, T] {
 | 
			
		||||
	return &LMap[K, T]{
 | 
			
		||||
		lock: sync.RWMutex{},
 | 
			
		||||
		data: make(map[K]T),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *LMap[K, T]) Put(key K, value T) {
 | 
			
		||||
	m.lock.Lock()
 | 
			
		||||
	defer m.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	m.data[key] = value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *LMap[K, T]) Get(key K) T {
 | 
			
		||||
	m.lock.RLock()
 | 
			
		||||
	defer m.lock.RUnlock()
 | 
			
		||||
 | 
			
		||||
	return m.data[key]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *LMap[K, T]) Has(key K) bool {
 | 
			
		||||
	m.lock.RLock()
 | 
			
		||||
	defer m.lock.RUnlock()
 | 
			
		||||
	_, ok := m.data[key]
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *LMap[K, T]) Delete(key K) {
 | 
			
		||||
	m.lock.Lock()
 | 
			
		||||
	defer m.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	delete(m.data, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *LMap[K, T]) ToList() []T {
 | 
			
		||||
	m.lock.Lock()
 | 
			
		||||
	defer m.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	var s = make([]T, 0)
 | 
			
		||||
	for _, v := range m.data {
 | 
			
		||||
		s = append(s, v)
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								api/core/types/session.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								api/core/types/session.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
const LoginUserID = "LOGIN_USER_ID"
 | 
			
		||||
const LoginUserCache = "LOGIN_USER_CACHE"
 | 
			
		||||
 | 
			
		||||
const UserAuthHeader = "Authorization"
 | 
			
		||||
const AdminAuthHeader = "Admin-Authorization"
 | 
			
		||||
const ChatTokenHeader = "Chat-Token"
 | 
			
		||||
 | 
			
		||||
// Session configs struct
 | 
			
		||||
type Session struct {
 | 
			
		||||
	SecretKey string
 | 
			
		||||
	MaxAge    int
 | 
			
		||||
}
 | 
			
		||||
@@ -12,9 +12,8 @@ type BizVo struct {
 | 
			
		||||
 | 
			
		||||
// WsMessage Websocket message
 | 
			
		||||
type WsMessage struct {
 | 
			
		||||
	Type       WsMsgType `json:"type"`         // 消息类别,start, end
 | 
			
		||||
	IsHelloMsg bool      `json:"is_hello_msg"` // 是否是打招呼的消息
 | 
			
		||||
	Content    string    `json:"content"`
 | 
			
		||||
	Type    WsMsgType   `json:"type"` // 消息类别,start, end, img
 | 
			
		||||
	Content interface{} `json:"content"`
 | 
			
		||||
}
 | 
			
		||||
type WsMsgType string
 | 
			
		||||
 | 
			
		||||
@@ -22,6 +21,7 @@ const (
 | 
			
		||||
	WsStart  = WsMsgType("start")
 | 
			
		||||
	WsMiddle = WsMsgType("middle")
 | 
			
		||||
	WsEnd    = WsMsgType("end")
 | 
			
		||||
	WsMjImg  = WsMsgType("mj")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BizCode int
 | 
			
		||||
@@ -29,12 +29,9 @@ type BizCode int
 | 
			
		||||
const (
 | 
			
		||||
	Success       = BizCode(0)
 | 
			
		||||
	Failed        = BizCode(1)
 | 
			
		||||
	InvalidParams = BizCode(101) // 非法参数
 | 
			
		||||
	NotAuthorized = BizCode(400) // 未授权
 | 
			
		||||
 | 
			
		||||
	OkMsg    = "Success"
 | 
			
		||||
	ErrorMsg = "系统开小差了"
 | 
			
		||||
	OkMsg       = "Success"
 | 
			
		||||
	ErrorMsg    = "系统开小差了"
 | 
			
		||||
	InvalidArgs = "非法参数或参数解析失败"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const TokenName = "ChatGPT-TOKEN"
 | 
			
		||||
const SessionKey = "WEB_SSH_SESSION"
 | 
			
		||||
							
								
								
									
										90
									
								
								api/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								api/go.mod
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
module chatplus
 | 
			
		||||
 | 
			
		||||
go 1.19
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/BurntSushi/toml v1.1.0
 | 
			
		||||
	github.com/aliyun/alibaba-cloud-sdk-go v1.62.405
 | 
			
		||||
	github.com/gin-gonic/gin v1.9.1
 | 
			
		||||
	github.com/go-redis/redis/v8 v8.11.5
 | 
			
		||||
	github.com/golang-jwt/jwt/v5 v5.0.0
 | 
			
		||||
	github.com/gorilla/websocket v1.5.0
 | 
			
		||||
	github.com/imroc/req/v3 v3.37.2
 | 
			
		||||
	github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0
 | 
			
		||||
	github.com/minio/minio-go/v7 v7.0.62
 | 
			
		||||
	github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
 | 
			
		||||
	github.com/qiniu/go-sdk/v7 v7.17.1
 | 
			
		||||
	github.com/syndtr/goleveldb v1.0.0
 | 
			
		||||
	go.uber.org/zap v1.23.0
 | 
			
		||||
	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 | 
			
		||||
	gorm.io/driver/mysql v1.4.7
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/andybalholm/brotli v1.0.4 // indirect
 | 
			
		||||
	github.com/bytedance/sonic v1.9.1 // indirect
 | 
			
		||||
	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 | 
			
		||||
	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
 | 
			
		||||
	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 | 
			
		||||
	github.com/dlclark/regexp2 v1.8.1 // indirect
 | 
			
		||||
	github.com/dustin/go-humanize v1.0.1 // indirect
 | 
			
		||||
	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
 | 
			
		||||
	github.com/gaukas/godicttls v0.0.3 // indirect
 | 
			
		||||
	github.com/go-sql-driver/mysql v1.7.0 // indirect
 | 
			
		||||
	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 | 
			
		||||
	github.com/goccy/go-json v0.10.2 // indirect
 | 
			
		||||
	github.com/golang/mock v1.6.0 // indirect
 | 
			
		||||
	github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
 | 
			
		||||
	github.com/google/uuid v1.3.0 // indirect
 | 
			
		||||
	github.com/hashicorp/errwrap v1.1.0 // indirect
 | 
			
		||||
	github.com/hashicorp/go-multierror v1.1.1 // indirect
 | 
			
		||||
	github.com/jinzhu/inflection v1.0.0 // indirect
 | 
			
		||||
	github.com/jinzhu/now v1.1.5 // indirect
 | 
			
		||||
	github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.16.7 // indirect
 | 
			
		||||
	github.com/klauspost/cpuid/v2 v2.2.5 // indirect
 | 
			
		||||
	github.com/minio/md5-simd v1.1.2 // indirect
 | 
			
		||||
	github.com/minio/sha256-simd v1.0.1 // indirect
 | 
			
		||||
	github.com/onsi/ginkgo/v2 v2.10.0 // indirect
 | 
			
		||||
	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
 | 
			
		||||
	github.com/quic-go/qpack v0.4.0 // indirect
 | 
			
		||||
	github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
 | 
			
		||||
	github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
 | 
			
		||||
	github.com/quic-go/quic-go v0.35.1 // indirect
 | 
			
		||||
	github.com/refraction-networking/utls v1.3.2 // indirect
 | 
			
		||||
	github.com/rs/xid v1.5.0 // indirect
 | 
			
		||||
	github.com/sirupsen/logrus v1.9.3 // indirect
 | 
			
		||||
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 | 
			
		||||
	go.uber.org/dig v1.16.1 // indirect
 | 
			
		||||
	golang.org/x/arch v0.3.0 // indirect
 | 
			
		||||
	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
 | 
			
		||||
	golang.org/x/mod v0.11.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.14.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.3.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.12.0 // indirect
 | 
			
		||||
	golang.org/x/tools v0.10.0 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.30.0 // indirect
 | 
			
		||||
	gopkg.in/ini.v1 v1.67.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/gin-contrib/sse v0.1.0 // indirect
 | 
			
		||||
	github.com/go-playground/locales v0.14.1 // indirect
 | 
			
		||||
	github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
			
		||||
	github.com/go-playground/validator/v10 v10.14.0 // indirect
 | 
			
		||||
	github.com/golang/snappy v0.0.1 // indirect
 | 
			
		||||
	github.com/json-iterator/go v1.1.12 // indirect
 | 
			
		||||
	github.com/leodido/go-urn v1.2.4 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.19 // indirect
 | 
			
		||||
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
			
		||||
	github.com/modern-go/reflect2 v1.0.2 // indirect
 | 
			
		||||
	github.com/ugorji/go/codec v1.2.11 // indirect
 | 
			
		||||
	go.uber.org/atomic v1.9.0 // indirect
 | 
			
		||||
	go.uber.org/fx v1.19.3
 | 
			
		||||
	go.uber.org/multierr v1.6.0 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.12.0
 | 
			
		||||
	golang.org/x/sys v0.11.0 // indirect
 | 
			
		||||
	gorm.io/gorm v1.25.1
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										299
									
								
								api/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								api/go.sum
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,299 @@
 | 
			
		||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
 | 
			
		||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 | 
			
		||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
 | 
			
		||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
 | 
			
		||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
 | 
			
		||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 | 
			
		||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
 | 
			
		||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 | 
			
		||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
 | 
			
		||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
			
		||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
 | 
			
		||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 | 
			
		||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
 | 
			
		||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 | 
			
		||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 | 
			
		||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
 | 
			
		||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 | 
			
		||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 | 
			
		||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 | 
			
		||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
 | 
			
		||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
 | 
			
		||||
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
 | 
			
		||||
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
 | 
			
		||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 | 
			
		||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 | 
			
		||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
 | 
			
		||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
 | 
			
		||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
 | 
			
		||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 | 
			
		||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 | 
			
		||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
 | 
			
		||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
 | 
			
		||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 | 
			
		||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 | 
			
		||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 | 
			
		||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
 | 
			
		||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 | 
			
		||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 | 
			
		||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
 | 
			
		||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 | 
			
		||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 | 
			
		||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 | 
			
		||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 | 
			
		||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 | 
			
		||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 | 
			
		||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
 | 
			
		||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 | 
			
		||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
 | 
			
		||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 | 
			
		||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
			
		||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 | 
			
		||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
			
		||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 | 
			
		||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
			
		||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 | 
			
		||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 | 
			
		||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
 | 
			
		||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
 | 
			
		||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 | 
			
		||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 | 
			
		||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 | 
			
		||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 | 
			
		||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 | 
			
		||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 | 
			
		||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 | 
			
		||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 | 
			
		||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 | 
			
		||||
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
 | 
			
		||||
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
 | 
			
		||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 | 
			
		||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 | 
			
		||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 | 
			
		||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 | 
			
		||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 | 
			
		||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 | 
			
		||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 | 
			
		||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 | 
			
		||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
 | 
			
		||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
 | 
			
		||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 | 
			
		||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 | 
			
		||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 | 
			
		||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 | 
			
		||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 | 
			
		||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
			
		||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
			
		||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 | 
			
		||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
 | 
			
		||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
 | 
			
		||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
 | 
			
		||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs=
 | 
			
		||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 | 
			
		||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 | 
			
		||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
 | 
			
		||||
github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc=
 | 
			
		||||
github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
 | 
			
		||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
 | 
			
		||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 | 
			
		||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 | 
			
		||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 | 
			
		||||
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
 | 
			
		||||
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
 | 
			
		||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 | 
			
		||||
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
 | 
			
		||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
 | 
			
		||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 | 
			
		||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 | 
			
		||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 | 
			
		||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
 | 
			
		||||
github.com/qiniu/go-sdk/v7 v7.17.1 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0=
 | 
			
		||||
github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=
 | 
			
		||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
 | 
			
		||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
 | 
			
		||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
 | 
			
		||||
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
 | 
			
		||||
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
 | 
			
		||||
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
 | 
			
		||||
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
 | 
			
		||||
github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
 | 
			
		||||
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
 | 
			
		||||
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
 | 
			
		||||
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 | 
			
		||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
 | 
			
		||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 | 
			
		||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 | 
			
		||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 | 
			
		||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 | 
			
		||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
 | 
			
		||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 | 
			
		||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
 | 
			
		||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 | 
			
		||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 | 
			
		||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 | 
			
		||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
 | 
			
		||||
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
 | 
			
		||||
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
 | 
			
		||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
 | 
			
		||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 | 
			
		||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 | 
			
		||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 | 
			
		||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 | 
			
		||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 | 
			
		||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 | 
			
		||||
go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8=
 | 
			
		||||
go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
 | 
			
		||||
go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA=
 | 
			
		||||
go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM=
 | 
			
		||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
 | 
			
		||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
 | 
			
		||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 | 
			
		||||
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
 | 
			
		||||
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
 | 
			
		||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 | 
			
		||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
 | 
			
		||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
			
		||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 | 
			
		||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
 | 
			
		||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 | 
			
		||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 | 
			
		||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
 | 
			
		||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 | 
			
		||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
			
		||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 | 
			
		||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
			
		||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 | 
			
		||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
 | 
			
		||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 | 
			
		||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
 | 
			
		||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
 | 
			
		||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
			
		||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
			
		||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
			
		||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
 | 
			
		||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
			
		||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 | 
			
		||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 | 
			
		||||
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
 | 
			
		||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 | 
			
		||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
 | 
			
		||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 | 
			
		||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 | 
			
		||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 | 
			
		||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 | 
			
		||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
 | 
			
		||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
 | 
			
		||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 | 
			
		||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
 | 
			
		||||
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
 | 
			
		||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 | 
			
		||||
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
 | 
			
		||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 | 
			
		||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 | 
			
		||||
							
								
								
									
										147
									
								
								api/handler/admin/admin_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								api/handler/admin/admin_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,147 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
type ManagerHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db    *gorm.DB
 | 
			
		||||
	redis *redis.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client) *ManagerHandler {
 | 
			
		||||
	h := ManagerHandler{db: db, redis: client}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Login 登录
 | 
			
		||||
func (h *ManagerHandler) Login(c *gin.Context) {
 | 
			
		||||
	var data types.Manager
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	manager := h.App.Config.Manager
 | 
			
		||||
	if data.Username == manager.Username && data.Password == manager.Password {
 | 
			
		||||
		// 创建 token
 | 
			
		||||
		token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
			"user_id": manager.Username,
 | 
			
		||||
			"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)),
 | 
			
		||||
		})
 | 
			
		||||
		tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, "Failed to generate token, "+err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// 保存到 redis
 | 
			
		||||
		if _, err := h.redis.Set(context.Background(), "users/"+manager.Username, tokenString, 0).Result(); err != nil {
 | 
			
		||||
			resp.ERROR(c, "error with save token: "+err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		resp.SUCCESS(c, tokenString)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.ERROR(c, "用户名或者密码错误")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Logout 注销
 | 
			
		||||
func (h *ManagerHandler) Logout(c *gin.Context) {
 | 
			
		||||
	token := c.GetHeader(types.AdminAuthHeader)
 | 
			
		||||
	if _, err := h.redis.Del(c, token).Result(); err != nil {
 | 
			
		||||
		logger.Error("error with delete session: ", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Session 会话检测
 | 
			
		||||
func (h *ManagerHandler) Session(c *gin.Context) {
 | 
			
		||||
	token := c.GetHeader(types.AdminAuthHeader)
 | 
			
		||||
	if token == "" {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Migrate 数据修正
 | 
			
		||||
func (h *ManagerHandler) Migrate(c *gin.Context) {
 | 
			
		||||
	opt := c.Query("opt")
 | 
			
		||||
	switch opt {
 | 
			
		||||
	case "user":
 | 
			
		||||
		// 将用户订阅角色的数据结构从 map 改成数组
 | 
			
		||||
		var users []model.User
 | 
			
		||||
		h.db.Find(&users)
 | 
			
		||||
		for _, u := range users {
 | 
			
		||||
			var m map[string]int
 | 
			
		||||
			var roleKeys = make([]string, 0)
 | 
			
		||||
			err := utils.JsonDecode(u.ChatRoles, &m)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for k := range m {
 | 
			
		||||
				roleKeys = append(roleKeys, k)
 | 
			
		||||
			}
 | 
			
		||||
			u.ChatRoles = utils.JsonEncode(roleKeys)
 | 
			
		||||
			h.db.Updates(&u)
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	case "role":
 | 
			
		||||
		// 修改角色图片,改成绝对路径
 | 
			
		||||
		var roles []model.ChatRole
 | 
			
		||||
		h.db.Find(&roles)
 | 
			
		||||
		for _, r := range roles {
 | 
			
		||||
			if !strings.HasPrefix(r.Icon, "/") {
 | 
			
		||||
				r.Icon = "/" + r.Icon
 | 
			
		||||
				h.db.Updates(&r)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	case "history":
 | 
			
		||||
		// 修改角色图片,改成绝对路径
 | 
			
		||||
		var message []model.HistoryMessage
 | 
			
		||||
		h.db.Find(&message)
 | 
			
		||||
		for _, r := range message {
 | 
			
		||||
			if !strings.HasPrefix(r.Icon, "/") {
 | 
			
		||||
				r.Icon = "/" + r.Icon
 | 
			
		||||
				h.db.Updates(&r)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
 | 
			
		||||
	case "avatar":
 | 
			
		||||
		// 更新用户的头像地址
 | 
			
		||||
		var users []model.User
 | 
			
		||||
		h.db.Find(&users)
 | 
			
		||||
		for _, u := range users {
 | 
			
		||||
			if !strings.HasPrefix(u.Avatar, "/") {
 | 
			
		||||
				u.Avatar = "/" + u.Avatar
 | 
			
		||||
				h.db.Updates(&u)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, "SUCCESS")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										92
									
								
								api/handler/admin/api_key_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								api/handler/admin/api_key_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ApiKeyHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
 | 
			
		||||
	h := ApiKeyHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ApiKeyHandler) Save(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id       uint   `json:"id"`
 | 
			
		||||
		Platform string `json:"platform"`
 | 
			
		||||
		Value    string `json:"value"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiKey := model.ApiKey{}
 | 
			
		||||
	if data.Id > 0 {
 | 
			
		||||
		h.db.Find(&apiKey)
 | 
			
		||||
	}
 | 
			
		||||
	apiKey.Platform = data.Platform
 | 
			
		||||
	apiKey.Value = data.Value
 | 
			
		||||
	res := h.db.Save(&apiKey)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var keyVo vo.ApiKey
 | 
			
		||||
	err := utils.CopyObject(apiKey, &keyVo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "数据拷贝失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	keyVo.Id = apiKey.Id
 | 
			
		||||
	keyVo.CreatedAt = apiKey.CreatedAt.Unix()
 | 
			
		||||
	resp.SUCCESS(c, keyVo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ApiKeyHandler) List(c *gin.Context) {
 | 
			
		||||
	var items []model.ApiKey
 | 
			
		||||
	var keys = make([]vo.ApiKey, 0)
 | 
			
		||||
	res := h.db.Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var key vo.ApiKey
 | 
			
		||||
			err := utils.CopyObject(item, &key)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				key.Id = item.Id
 | 
			
		||||
				key.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
				key.UpdatedAt = item.UpdatedAt.Unix()
 | 
			
		||||
				keys = append(keys, key)
 | 
			
		||||
			} else {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, keys)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ApiKeyHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
 | 
			
		||||
	if id > 0 {
 | 
			
		||||
		res := h.db.Where("id = ?", id).Delete(&model.ApiKey{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										143
									
								
								api/handler/admin/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								api/handler/admin/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ChatModelHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
 | 
			
		||||
	h := ChatModelHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatModelHandler) Save(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id        uint   `json:"id"`
 | 
			
		||||
		Name      string `json:"name"`
 | 
			
		||||
		Value     string `json:"value"`
 | 
			
		||||
		Enabled   bool   `json:"enabled"`
 | 
			
		||||
		SortNum   int    `json:"sort_num"`
 | 
			
		||||
		Platform  string `json:"platform"`
 | 
			
		||||
		CreatedAt int64  `json:"created_at"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	item := model.ChatModel{Platform: data.Platform, Name: data.Name, Value: data.Value, Enabled: data.Enabled}
 | 
			
		||||
	item.Id = data.Id
 | 
			
		||||
	if item.Id > 0 {
 | 
			
		||||
		item.CreatedAt = time.Unix(data.CreatedAt, 0)
 | 
			
		||||
	}
 | 
			
		||||
	res := h.db.Save(&item)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var itemVo vo.ChatModel
 | 
			
		||||
	err := utils.CopyObject(item, &itemVo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "数据拷贝失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	itemVo.Id = item.Id
 | 
			
		||||
	itemVo.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
	resp.SUCCESS(c, itemVo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List 模型列表
 | 
			
		||||
func (h *ChatModelHandler) List(c *gin.Context) {
 | 
			
		||||
	session := h.db.Session(&gorm.Session{})
 | 
			
		||||
	enable := h.GetBool(c, "enable")
 | 
			
		||||
	if enable {
 | 
			
		||||
		session = session.Where("enabled", enable)
 | 
			
		||||
	}
 | 
			
		||||
	var items []model.ChatModel
 | 
			
		||||
	var cms = make([]vo.ChatModel, 0)
 | 
			
		||||
	res := session.Order("sort_num ASC").Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var cm vo.ChatModel
 | 
			
		||||
			err := utils.CopyObject(item, &cm)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				cm.Id = item.Id
 | 
			
		||||
				cm.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
				cm.UpdatedAt = item.UpdatedAt.Unix()
 | 
			
		||||
				cms = append(cms, cm)
 | 
			
		||||
			} else {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, cms)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatModelHandler) Enable(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id      uint `json:"id"`
 | 
			
		||||
		Enabled bool `json:"enabled"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update("enabled", data.Enabled)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatModelHandler) Sort(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Ids   []uint `json:"ids"`
 | 
			
		||||
		Sorts []int  `json:"sorts"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for index, id := range data.Ids {
 | 
			
		||||
		res := h.db.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatModelHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
 | 
			
		||||
	if id > 0 {
 | 
			
		||||
		res := h.db.Where("id = ?", id).Delete(&model.ChatModel{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										114
									
								
								api/handler/admin/chat_role_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								api/handler/admin/chat_role_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ChatRoleHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
 | 
			
		||||
	h := ChatRoleHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Save 创建或者更新某个角色
 | 
			
		||||
func (h *ChatRoleHandler) Save(c *gin.Context) {
 | 
			
		||||
	var data vo.ChatRole
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var role model.ChatRole
 | 
			
		||||
	err := utils.CopyObject(data, &role)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	role.Id = data.Id
 | 
			
		||||
	if data.CreatedAt > 0 {
 | 
			
		||||
		role.CreatedAt = time.Unix(data.CreatedAt, 0)
 | 
			
		||||
	}
 | 
			
		||||
	res := h.db.Save(&role)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 填充 ID 数据
 | 
			
		||||
	data.Id = role.Id
 | 
			
		||||
	data.CreatedAt = role.CreatedAt.Unix()
 | 
			
		||||
	resp.SUCCESS(c, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatRoleHandler) List(c *gin.Context) {
 | 
			
		||||
	var items []model.ChatRole
 | 
			
		||||
	var roles = make([]vo.ChatRole, 0)
 | 
			
		||||
	res := h.db.Order("sort_num ASC").Find(&items)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "No data found")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, v := range items {
 | 
			
		||||
		var role vo.ChatRole
 | 
			
		||||
		err := utils.CopyObject(v, &role)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			role.Id = v.Id
 | 
			
		||||
			role.CreatedAt = v.CreatedAt.Unix()
 | 
			
		||||
			role.UpdatedAt = v.UpdatedAt.Unix()
 | 
			
		||||
			roles = append(roles, role)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, roles)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sort 更新角色排序
 | 
			
		||||
func (h *ChatRoleHandler) Sort(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Ids   []uint `json:"ids"`
 | 
			
		||||
		Sorts []int  `json:"sorts"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for index, id := range data.Ids {
 | 
			
		||||
		res := h.db.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatRoleHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	if id <= 0 {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Where("id = ?", id).Delete(&model.ChatRole{})
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "删除失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								api/handler/admin/config_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								api/handler/admin/config_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ConfigHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
 | 
			
		||||
	h := ConfigHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ConfigHandler) Update(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Key    string                 `json:"key"`
 | 
			
		||||
		Config map[string]interface{} `json:"config"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	str := utils.JsonEncode(&data.Config)
 | 
			
		||||
	config := model.Config{Key: data.Key, Config: str}
 | 
			
		||||
	res := h.db.FirstOrCreate(&config, model.Config{Key: data.Key})
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if config.Id > 0 {
 | 
			
		||||
		config.Config = str
 | 
			
		||||
		res := h.db.Updates(&config)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, res.Error.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// update config cache for AppServer
 | 
			
		||||
		var cfg model.Config
 | 
			
		||||
		h.db.Where("marker", data.Key).First(&cfg)
 | 
			
		||||
		var err error
 | 
			
		||||
		if data.Key == "system" {
 | 
			
		||||
			err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
 | 
			
		||||
		} else if data.Key == "chat" {
 | 
			
		||||
			err = utils.JsonDecode(cfg.Config, &h.App.ChatConfig)
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, "Failed to update config cache: "+err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		logger.Infof("Update AppServer's config successfully: %v", config.Config)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get 获取指定的系统配置
 | 
			
		||||
func (h *ConfigHandler) Get(c *gin.Context) {
 | 
			
		||||
	key := c.Query("key")
 | 
			
		||||
	var config model.Config
 | 
			
		||||
	res := h.db.Where("marker", key).First(&config)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var m map[string]interface{}
 | 
			
		||||
	err := utils.JsonDecode(config.Config, &m)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, m)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								api/handler/admin/dashboard_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/handler/admin/dashboard_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DashboardHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
 | 
			
		||||
	h := DashboardHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type statsVo struct {
 | 
			
		||||
	Users   int64   `json:"users"`
 | 
			
		||||
	Chats   int64   `json:"chats"`
 | 
			
		||||
	Tokens  int64   `json:"tokens"`
 | 
			
		||||
	Rewards float64 `json:"rewards"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *DashboardHandler) Stats(c *gin.Context) {
 | 
			
		||||
	stats := statsVo{}
 | 
			
		||||
	// new users statistic
 | 
			
		||||
	var userCount int64
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
 | 
			
		||||
	res := h.db.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&userCount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Users = userCount
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// new chats statistic
 | 
			
		||||
	var chatCount int64
 | 
			
		||||
	res = h.db.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Chats = chatCount
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// tokens took stats
 | 
			
		||||
	var tokenCount int64
 | 
			
		||||
	res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as total").Where("created_at > ?", zeroTime).Scan(&tokenCount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Tokens = tokenCount
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// reward revenue
 | 
			
		||||
	var amount float64
 | 
			
		||||
	res = h.db.Model(&model.Reward{}).Select("sum(amount) as total").Where("created_at > ?", zeroTime).Scan(&amount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Rewards = amount
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, stats)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								api/handler/admin/reward_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								api/handler/admin/reward_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RewardHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
 | 
			
		||||
	h := RewardHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *RewardHandler) List(c *gin.Context) {
 | 
			
		||||
	var items []model.Reward
 | 
			
		||||
	res := h.db.Order("id DESC").Find(&items)
 | 
			
		||||
	var rewards = make([]vo.Reward, 0)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		userIds := make([]uint, 0)
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			userIds = append(userIds, v.UserId)
 | 
			
		||||
		}
 | 
			
		||||
		var users []model.User
 | 
			
		||||
		h.db.Where("id IN ?", userIds).Find(&users)
 | 
			
		||||
		var userMap = make(map[uint]model.User)
 | 
			
		||||
		for _, u := range users {
 | 
			
		||||
			userMap[u.Id] = u
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			var r vo.Reward
 | 
			
		||||
			err := utils.CopyObject(v, &r)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			r.Id = v.Id
 | 
			
		||||
			r.Username = userMap[v.UserId].Mobile
 | 
			
		||||
			r.CreatedAt = v.CreatedAt.Unix()
 | 
			
		||||
			r.UpdatedAt = v.UpdatedAt.Unix()
 | 
			
		||||
			rewards = append(rewards, r)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, rewards)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										212
									
								
								api/handler/admin/user_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								api/handler/admin/user_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,212 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UserHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler {
 | 
			
		||||
	h := UserHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List 用户列表
 | 
			
		||||
func (h *UserHandler) List(c *gin.Context) {
 | 
			
		||||
	page := h.GetInt(c, "page", 1)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 20)
 | 
			
		||||
	mobile := h.GetTrim(c, "mobile")
 | 
			
		||||
 | 
			
		||||
	offset := (page - 1) * pageSize
 | 
			
		||||
	var items []model.User
 | 
			
		||||
	var users = make([]vo.User, 0)
 | 
			
		||||
	var total int64
 | 
			
		||||
 | 
			
		||||
	session := h.db.Session(&gorm.Session{})
 | 
			
		||||
	if mobile != "" {
 | 
			
		||||
		session = session.Where("mobile LIKE ?", "%"+mobile+"%")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session.Model(&model.User{}).Count(&total)
 | 
			
		||||
	res := session.Offset(offset).Limit(pageSize).Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var user vo.User
 | 
			
		||||
			err := utils.CopyObject(item, &user)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				user.Id = item.Id
 | 
			
		||||
				user.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
				user.UpdatedAt = item.UpdatedAt.Unix()
 | 
			
		||||
				users = append(users, user)
 | 
			
		||||
			} else {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	pageVo := vo.NewPage(total, page, pageSize, users)
 | 
			
		||||
	resp.SUCCESS(c, pageVo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UserHandler) Save(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id          uint     `json:"id"`
 | 
			
		||||
		Password    string   `json:"password"`
 | 
			
		||||
		Mobile      string   `json:"mobile"`
 | 
			
		||||
		Calls       int      `json:"calls"`
 | 
			
		||||
		ImgCalls    int      `json:"img_calls"`
 | 
			
		||||
		ChatRoles   []string `json:"chat_roles"`
 | 
			
		||||
		ExpiredTime string   `json:"expired_time"`
 | 
			
		||||
		Status      bool     `json:"status"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var user = model.User{}
 | 
			
		||||
	var res *gorm.DB
 | 
			
		||||
	var userVo vo.User
 | 
			
		||||
	if data.Id > 0 { // 更新
 | 
			
		||||
		user.Id = data.Id
 | 
			
		||||
		// 此处需要用 map 更新,用结构体无法更新 0 值
 | 
			
		||||
		res = h.db.Model(&user).Updates(map[string]interface{}{
 | 
			
		||||
			"mobile":          data.Mobile,
 | 
			
		||||
			"calls":           data.Calls,
 | 
			
		||||
			"img_calls":       data.ImgCalls,
 | 
			
		||||
			"status":          data.Status,
 | 
			
		||||
			"chat_roles_json": utils.JsonEncode(data.ChatRoles),
 | 
			
		||||
			"expired_time":    utils.Str2stamp(data.ExpiredTime),
 | 
			
		||||
		})
 | 
			
		||||
	} else {
 | 
			
		||||
		salt := utils.RandString(8)
 | 
			
		||||
		u := model.User{
 | 
			
		||||
			Mobile:      data.Mobile,
 | 
			
		||||
			Password:    utils.GenPassword(data.Password, salt),
 | 
			
		||||
			Avatar:      "/images/avatar/user.png",
 | 
			
		||||
			Salt:        salt,
 | 
			
		||||
			Status:      true,
 | 
			
		||||
			ChatRoles:   utils.JsonEncode(data.ChatRoles),
 | 
			
		||||
			ExpiredTime: utils.Str2stamp(data.ExpiredTime),
 | 
			
		||||
			ChatConfig: utils.JsonEncode(types.UserChatConfig{
 | 
			
		||||
				ApiKeys: map[types.Platform]string{
 | 
			
		||||
					types.OpenAI:  "",
 | 
			
		||||
					types.Azure:   "",
 | 
			
		||||
					types.ChatGLM: "",
 | 
			
		||||
				},
 | 
			
		||||
			}),
 | 
			
		||||
			Calls: h.App.SysConfig.UserInitCalls,
 | 
			
		||||
		}
 | 
			
		||||
		res = h.db.Create(&u)
 | 
			
		||||
		_ = utils.CopyObject(u, &userVo)
 | 
			
		||||
		userVo.Id = u.Id
 | 
			
		||||
		userVo.CreatedAt = u.CreatedAt.Unix()
 | 
			
		||||
		userVo.UpdatedAt = u.UpdatedAt.Unix()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, userVo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ResetPass 重置密码
 | 
			
		||||
func (h *UserHandler) ResetPass(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id       uint
 | 
			
		||||
		Password string
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var user model.User
 | 
			
		||||
	res := h.db.First(&user, data.Id)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "No user found")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	password := utils.GenPassword(data.Password, user.Salt)
 | 
			
		||||
	user.Password = password
 | 
			
		||||
	res = h.db.Updates(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UserHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	if id > 0 {
 | 
			
		||||
		tx := h.db.Begin()
 | 
			
		||||
		res := h.db.Where("id = ?", id).Delete(&model.User{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "删除失败")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// 删除聊天记录
 | 
			
		||||
		res = h.db.Where("user_id = ?", id).Delete(&model.ChatItem{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			resp.ERROR(c, "删除失败")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// 删除聊天历史记录
 | 
			
		||||
		res = h.db.Where("user_id = ?", id).Delete(&model.HistoryMessage{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			resp.ERROR(c, "删除失败")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// 删除登录日志
 | 
			
		||||
		res = h.db.Where("user_id = ?", id).Delete(&model.UserLoginLog{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			resp.ERROR(c, "删除失败")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		tx.Commit()
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UserHandler) LoginLog(c *gin.Context) {
 | 
			
		||||
	page := h.GetInt(c, "page", 1)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 20)
 | 
			
		||||
	var total int64
 | 
			
		||||
	h.db.Model(&model.UserLoginLog{}).Count(&total)
 | 
			
		||||
	offset := (page - 1) * pageSize
 | 
			
		||||
	var items []model.UserLoginLog
 | 
			
		||||
	res := h.db.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "获取数据失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var logs []vo.UserLoginLog
 | 
			
		||||
	for _, v := range items {
 | 
			
		||||
		var log vo.UserLoginLog
 | 
			
		||||
		err := utils.CopyObject(v, &log)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			log.Id = v.Id
 | 
			
		||||
			log.CreatedAt = v.CreatedAt.Unix()
 | 
			
		||||
			logs = append(logs, log)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										301
									
								
								api/handler/azure_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								api/handler/azure_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,301 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 将消息发送给 Azure API 并获取结果,通过 WebSocket 推送到客户端
 | 
			
		||||
func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
	chatCtx []interface{},
 | 
			
		||||
	req types.ApiRequest,
 | 
			
		||||
	userVo vo.User,
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	session *types.ChatSession,
 | 
			
		||||
	role model.ChatRole,
 | 
			
		||||
	prompt string,
 | 
			
		||||
	ws *types.WsClient) error {
 | 
			
		||||
	promptCreatedAt := time.Now() // 记录提问时间
 | 
			
		||||
	start := time.Now()
 | 
			
		||||
	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
 | 
			
		||||
	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
 | 
			
		||||
	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if strings.Contains(err.Error(), "context canceled") {
 | 
			
		||||
			logger.Info("用户取消了请求:", prompt)
 | 
			
		||||
			return nil
 | 
			
		||||
		} else if strings.Contains(err.Error(), "no available key") {
 | 
			
		||||
			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			logger.Error(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		utils.ReplyMessage(ws, ErrorMsg)
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		defer response.Body.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	contentType := response.Header.Get("Content-Type")
 | 
			
		||||
	if strings.Contains(contentType, "text/event-stream") {
 | 
			
		||||
		replyCreatedAt := time.Now() // 记录回复时间
 | 
			
		||||
		// 循环读取 Chunk 消息
 | 
			
		||||
		var message = types.Message{}
 | 
			
		||||
		var contents = make([]string, 0)
 | 
			
		||||
		var functionCall = false
 | 
			
		||||
		var functionName string
 | 
			
		||||
		var arguments = make([]string, 0)
 | 
			
		||||
		scanner := bufio.NewScanner(response.Body)
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			line := scanner.Text()
 | 
			
		||||
			if !strings.Contains(line, "data:") || len(line) < 30 {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var responseBody = types.ApiResponse{}
 | 
			
		||||
			err = json.Unmarshal([]byte(line[6:]), &responseBody)
 | 
			
		||||
			if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
 | 
			
		||||
				logger.Error(err, line)
 | 
			
		||||
				utils.ReplyMessage(ws, ErrorMsg)
 | 
			
		||||
				utils.ReplyMessage(ws, "")
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			fun := responseBody.Choices[0].Delta.FunctionCall
 | 
			
		||||
			if functionCall && fun.Name == "" {
 | 
			
		||||
				arguments = append(arguments, fun.Arguments)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if !utils.IsEmptyValue(fun) {
 | 
			
		||||
				functionName = fun.Name
 | 
			
		||||
				f := h.App.Functions[functionName]
 | 
			
		||||
				if f != nil {
 | 
			
		||||
					functionCall = true
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 初始化 role
 | 
			
		||||
			if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
 | 
			
		||||
				message.Role = responseBody.Choices[0].Delta.Role
 | 
			
		||||
				utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
 | 
			
		||||
				continue
 | 
			
		||||
			} else if responseBody.Choices[0].FinishReason != "" {
 | 
			
		||||
				break // 输出完成或者输出中断了
 | 
			
		||||
			} else {
 | 
			
		||||
				content := responseBody.Choices[0].Delta.Content
 | 
			
		||||
				contents = append(contents, utils.InterfaceToString(content))
 | 
			
		||||
				utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
					Type:    types.WsMiddle,
 | 
			
		||||
					Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		} // end for
 | 
			
		||||
 | 
			
		||||
		if err := scanner.Err(); err != nil {
 | 
			
		||||
			if strings.Contains(err.Error(), "context canceled") {
 | 
			
		||||
				logger.Info("用户取消了请求:", prompt)
 | 
			
		||||
			} else {
 | 
			
		||||
				logger.Error("信息读取出错:", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if functionCall { // 调用函数完成任务
 | 
			
		||||
			var params map[string]interface{}
 | 
			
		||||
			_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
 | 
			
		||||
			logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
 | 
			
		||||
 | 
			
		||||
			// for creating image, check if the user's img_calls > 0
 | 
			
		||||
			if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
 | 
			
		||||
				utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
 | 
			
		||||
				utils.ReplyMessage(ws, "")
 | 
			
		||||
			} else {
 | 
			
		||||
				f := h.App.Functions[functionName]
 | 
			
		||||
				data, err := f.Invoke(params)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					msg := "调用函数出错:" + err.Error()
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
						Type:    types.WsMiddle,
 | 
			
		||||
						Content: msg,
 | 
			
		||||
					})
 | 
			
		||||
					contents = append(contents, msg)
 | 
			
		||||
				} else {
 | 
			
		||||
					content := data
 | 
			
		||||
					if functionName == types.FuncMidJourney {
 | 
			
		||||
						key := utils.Sha256(data)
 | 
			
		||||
						logger.Debug(data, ",", key)
 | 
			
		||||
						// add task for MidJourney
 | 
			
		||||
						h.App.MjTaskClients.Put(key, ws)
 | 
			
		||||
						task := types.MjTask{
 | 
			
		||||
							UserId: userVo.Id,
 | 
			
		||||
							RoleId: role.Id,
 | 
			
		||||
							Icon:   "/images/avatar/mid_journey.png",
 | 
			
		||||
							ChatId: session.ChatId,
 | 
			
		||||
						}
 | 
			
		||||
						err := h.leveldb.Put(types.TaskStorePrefix+key, task)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							logger.Error("error with store MidJourney task: ", err)
 | 
			
		||||
						}
 | 
			
		||||
						content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
 | 
			
		||||
 | 
			
		||||
						// update user's img_calls
 | 
			
		||||
						h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
						Type:    types.WsMiddle,
 | 
			
		||||
						Content: content,
 | 
			
		||||
					})
 | 
			
		||||
					contents = append(contents, content)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 消息发送成功
 | 
			
		||||
		if len(contents) > 0 {
 | 
			
		||||
			// 更新用户的对话次数
 | 
			
		||||
			if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
 | 
			
		||||
				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if message.Role == "" {
 | 
			
		||||
				message.Role = "assistant"
 | 
			
		||||
			}
 | 
			
		||||
			message.Content = strings.Join(contents, "")
 | 
			
		||||
			useMsg := types.Message{Role: "user", Content: prompt}
 | 
			
		||||
 | 
			
		||||
			// 更新上下文消息,如果是调用函数则不需要更新上下文
 | 
			
		||||
			if h.App.ChatConfig.EnableContext && functionCall == false {
 | 
			
		||||
				chatCtx = append(chatCtx, useMsg)  // 提问消息
 | 
			
		||||
				chatCtx = append(chatCtx, message) // 回复消息
 | 
			
		||||
				h.App.ChatContexts.Put(session.ChatId, chatCtx)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 追加聊天记录
 | 
			
		||||
			if h.App.ChatConfig.EnableHistory {
 | 
			
		||||
				useContext := true
 | 
			
		||||
				if functionCall {
 | 
			
		||||
					useContext = false
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// for prompt
 | 
			
		||||
				promptToken, err := utils.CalcTokens(prompt, req.Model)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Error(err)
 | 
			
		||||
				}
 | 
			
		||||
				historyUserMsg := model.HistoryMessage{
 | 
			
		||||
					UserId:     userVo.Id,
 | 
			
		||||
					ChatId:     session.ChatId,
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.PromptMsg,
 | 
			
		||||
					Icon:       userVo.Avatar,
 | 
			
		||||
					Content:    prompt,
 | 
			
		||||
					Tokens:     promptToken,
 | 
			
		||||
					UseContext: useContext,
 | 
			
		||||
				}
 | 
			
		||||
				historyUserMsg.CreatedAt = promptCreatedAt
 | 
			
		||||
				historyUserMsg.UpdatedAt = promptCreatedAt
 | 
			
		||||
				res := h.db.Save(&historyUserMsg)
 | 
			
		||||
				if res.Error != nil {
 | 
			
		||||
					logger.Error("failed to save prompt history message: ", res.Error)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// for reply
 | 
			
		||||
				// 计算本次对话消耗的总 token 数量
 | 
			
		||||
				var replyToken = 0
 | 
			
		||||
				if functionCall { // 函数名 + 参数 token
 | 
			
		||||
					tokens, _ := utils.CalcTokens(functionName, req.Model)
 | 
			
		||||
					replyToken += tokens
 | 
			
		||||
					tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
 | 
			
		||||
					replyToken += tokens
 | 
			
		||||
				} else {
 | 
			
		||||
					replyToken, _ = utils.CalcTokens(message.Content, req.Model)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				historyReplyMsg := model.HistoryMessage{
 | 
			
		||||
					UserId:     userVo.Id,
 | 
			
		||||
					ChatId:     session.ChatId,
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.ReplyMsg,
 | 
			
		||||
					Icon:       role.Icon,
 | 
			
		||||
					Content:    message.Content,
 | 
			
		||||
					Tokens:     replyToken,
 | 
			
		||||
					UseContext: useContext,
 | 
			
		||||
				}
 | 
			
		||||
				historyReplyMsg.CreatedAt = replyCreatedAt
 | 
			
		||||
				historyReplyMsg.UpdatedAt = replyCreatedAt
 | 
			
		||||
				res = h.db.Create(&historyReplyMsg)
 | 
			
		||||
				if res.Error != nil {
 | 
			
		||||
					logger.Error("failed to save reply history message: ", res.Error)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 计算本次对话消耗的总 token 数量
 | 
			
		||||
				var totalTokens = 0
 | 
			
		||||
				if functionCall { // prompt + 函数名 + 参数 token
 | 
			
		||||
					totalTokens = promptToken + replyToken
 | 
			
		||||
				} else {
 | 
			
		||||
					totalTokens = replyToken + getTotalTokens(req)
 | 
			
		||||
				}
 | 
			
		||||
				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
 | 
			
		||||
					UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 保存当前会话
 | 
			
		||||
			var chatItem model.ChatItem
 | 
			
		||||
			res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
 | 
			
		||||
			if res.Error != nil {
 | 
			
		||||
				chatItem.ChatId = session.ChatId
 | 
			
		||||
				chatItem.UserId = session.UserId
 | 
			
		||||
				chatItem.RoleId = role.Id
 | 
			
		||||
				chatItem.ModelId = session.Model.Id
 | 
			
		||||
				if utf8.RuneCountInString(prompt) > 30 {
 | 
			
		||||
					chatItem.Title = string([]rune(prompt)[:30]) + "..."
 | 
			
		||||
				} else {
 | 
			
		||||
					chatItem.Title = prompt
 | 
			
		||||
				}
 | 
			
		||||
				h.db.Create(&chatItem)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		body, err := io.ReadAll(response.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error with reading response: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		var res types.ApiError
 | 
			
		||||
		err = json.Unmarshal(body, &res)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error with decode response: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if strings.Contains(res.Error.Message, "maximum context length") {
 | 
			
		||||
			logger.Error(res.Error.Message)
 | 
			
		||||
			utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
 | 
			
		||||
			h.App.ChatContexts.Delete(session.ChatId)
 | 
			
		||||
			return h.sendMessage(ctx, session, role, prompt, ws)
 | 
			
		||||
		} else {
 | 
			
		||||
			utils.ReplyMessage(ws, "请求 Azure API 失败:"+res.Error.Message)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								api/handler/base_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								api/handler/base_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
type BaseHandler struct {
 | 
			
		||||
	App *core.AppServer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) GetTrim(c *gin.Context, key string) string {
 | 
			
		||||
	return strings.TrimSpace(c.Query(key))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int {
 | 
			
		||||
	return utils.IntValue(c.PostForm(key), defaultValue)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int {
 | 
			
		||||
	return utils.IntValue(c.Query(key), defaultValue)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 {
 | 
			
		||||
	return utils.FloatValue(c.Query(key))
 | 
			
		||||
}
 | 
			
		||||
func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 {
 | 
			
		||||
	return utils.FloatValue(c.PostForm(key))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) GetBool(c *gin.Context, key string) bool {
 | 
			
		||||
	return utils.BoolValue(c.Query(key))
 | 
			
		||||
}
 | 
			
		||||
func (h *BaseHandler) PostBool(c *gin.Context, key string) bool {
 | 
			
		||||
	return utils.BoolValue(c.PostForm(key))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								api/handler/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								api/handler/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 今日头条函数实现
 | 
			
		||||
 | 
			
		||||
type CaptchaHandler struct {
 | 
			
		||||
	service *service.CaptchaService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler {
 | 
			
		||||
	return &CaptchaHandler{service: s}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CaptchaHandler) Get(c *gin.Context) {
 | 
			
		||||
	data, err := h.service.Get()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Check verify the captcha data
 | 
			
		||||
func (h *CaptchaHandler) Check(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Key  string `json:"key"`
 | 
			
		||||
		Dots string `json:"dots"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h.service.Check(data) {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.ERROR(c)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										398
									
								
								api/handler/chat_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								api/handler/chat_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,398 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
 | 
			
		||||
 | 
			
		||||
type ChatHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db      *gorm.DB
 | 
			
		||||
	leveldb *store.LevelDB
 | 
			
		||||
	redis   *redis.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, redis *redis.Client) *ChatHandler {
 | 
			
		||||
	handler := ChatHandler{db: db, leveldb: levelDB, redis: redis}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return &handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var chatConfig types.ChatConfig
 | 
			
		||||
 | 
			
		||||
// ChatHandle 处理聊天 WebSocket 请求
 | 
			
		||||
func (h *ChatHandler) ChatHandle(c *gin.Context) {
 | 
			
		||||
	ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sessionId := c.Query("session_id")
 | 
			
		||||
	roleId := h.GetInt(c, "role_id", 0)
 | 
			
		||||
	chatId := c.Query("chat_id")
 | 
			
		||||
	modelId := h.GetInt(c, "model_id", 0)
 | 
			
		||||
 | 
			
		||||
	client := types.NewWsClient(ws)
 | 
			
		||||
	// get model info
 | 
			
		||||
	var chatModel model.ChatModel
 | 
			
		||||
	res := h.db.First(&chatModel, modelId)
 | 
			
		||||
	if res.Error != nil || chatModel.Enabled == false {
 | 
			
		||||
		utils.ReplyMessage(client, "当前AI模型暂未启用,连接已关闭!!!")
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session := h.App.ChatSession.Get(sessionId)
 | 
			
		||||
	if session == nil {
 | 
			
		||||
		user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Info("用户未登录")
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		session = &types.ChatSession{
 | 
			
		||||
			SessionId: sessionId,
 | 
			
		||||
			ClientIP:  c.ClientIP(),
 | 
			
		||||
			Username:  user.Mobile,
 | 
			
		||||
			UserId:    user.Id,
 | 
			
		||||
		}
 | 
			
		||||
		h.App.ChatSession.Put(sessionId, session)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// use old chat data override the chat model and role ID
 | 
			
		||||
	var chat model.ChatItem
 | 
			
		||||
	res = h.db.Where("chat_id=?", chatId).First(&chat)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		chatModel.Id = chat.ModelId
 | 
			
		||||
		roleId = int(chat.RoleId)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session.ChatId = chatId
 | 
			
		||||
	session.Model = types.ChatModel{
 | 
			
		||||
		Id:       chatModel.Id,
 | 
			
		||||
		Value:    chatModel.Value,
 | 
			
		||||
		Platform: types.Platform(chatModel.Platform)}
 | 
			
		||||
	logger.Infof("New websocket connected, IP: %s, Username: %s", c.Request.RemoteAddr, session.Username)
 | 
			
		||||
	var chatRole model.ChatRole
 | 
			
		||||
	res = h.db.First(&chatRole, roleId)
 | 
			
		||||
	if res.Error != nil || !chatRole.Enable {
 | 
			
		||||
		utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 初始化聊天配置
 | 
			
		||||
	var config model.Config
 | 
			
		||||
	h.db.Where("marker", "chat").First(&config)
 | 
			
		||||
	err = utils.JsonDecode(config.Config, &chatConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!")
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 保存会话连接
 | 
			
		||||
	h.App.ChatClients.Put(sessionId, client)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			_, msg, err := client.Receive()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
				client.Close()
 | 
			
		||||
				h.App.ChatClients.Delete(sessionId)
 | 
			
		||||
				h.App.ReqCancelFunc.Delete(sessionId)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			message := string(msg)
 | 
			
		||||
			logger.Info("Receive a message: ", message)
 | 
			
		||||
			//utils.ReplyMessage(client, "这是一条测试消息!")
 | 
			
		||||
			ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
			h.App.ReqCancelFunc.Put(sessionId, cancel)
 | 
			
		||||
			// 回复消息
 | 
			
		||||
			err = h.sendMessage(ctx, session, chatRole, message, client)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
			} else {
 | 
			
		||||
				utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
 | 
			
		||||
				logger.Info("回答完毕: " + string(message))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			logger.Error("Recover message from error: ", r)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	var user model.User
 | 
			
		||||
	res := h.db.Model(&model.User{}).First(&user, session.UserId)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		utils.ReplyMessage(ws, "非法用户,请联系管理员!")
 | 
			
		||||
		return res.Error
 | 
			
		||||
	}
 | 
			
		||||
	var userVo vo.User
 | 
			
		||||
	err := utils.CopyObject(user, &userVo)
 | 
			
		||||
	userVo.Id = user.Id
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.New("User 对象转换失败," + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if userVo.Status == false {
 | 
			
		||||
		utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
 | 
			
		||||
		utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!")
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
 | 
			
		||||
		utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!")
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	var req = types.ApiRequest{
 | 
			
		||||
		Model:  session.Model.Value,
 | 
			
		||||
		Stream: true,
 | 
			
		||||
	}
 | 
			
		||||
	switch session.Model.Platform {
 | 
			
		||||
	case types.Azure:
 | 
			
		||||
		req.Temperature = h.App.ChatConfig.Azure.Temperature
 | 
			
		||||
		req.MaxTokens = h.App.ChatConfig.Azure.MaxTokens
 | 
			
		||||
		break
 | 
			
		||||
	case types.ChatGLM:
 | 
			
		||||
		req.Temperature = h.App.ChatConfig.ChatGML.Temperature
 | 
			
		||||
		req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens
 | 
			
		||||
		break
 | 
			
		||||
	default:
 | 
			
		||||
		req.Temperature = h.App.ChatConfig.OpenAI.Temperature
 | 
			
		||||
		req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens
 | 
			
		||||
		var functions = make([]types.Function, 0)
 | 
			
		||||
		for _, f := range types.InnerFunctions {
 | 
			
		||||
			if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			functions = append(functions, f)
 | 
			
		||||
		}
 | 
			
		||||
		req.Functions = functions
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 加载聊天上下文
 | 
			
		||||
	var chatCtx []interface{}
 | 
			
		||||
	if h.App.ChatConfig.EnableContext {
 | 
			
		||||
		if h.App.ChatContexts.Has(session.ChatId) {
 | 
			
		||||
			chatCtx = h.App.ChatContexts.Get(session.ChatId)
 | 
			
		||||
		} else {
 | 
			
		||||
			// calculate the tokens of current request, to prevent to exceeding the max tokens num
 | 
			
		||||
			tokens := req.MaxTokens
 | 
			
		||||
			for _, f := range types.InnerFunctions {
 | 
			
		||||
				tks, _ := utils.CalcTokens(utils.JsonEncode(f), req.Model)
 | 
			
		||||
				tokens += tks
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// loading the role context
 | 
			
		||||
			var messages []types.Message
 | 
			
		||||
			err := utils.JsonDecode(role.Context, &messages)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				for _, v := range messages {
 | 
			
		||||
					tks, _ := utils.CalcTokens(v.Content, req.Model)
 | 
			
		||||
					if tokens+tks >= types.ModelToTokens[req.Model] {
 | 
			
		||||
						break
 | 
			
		||||
					}
 | 
			
		||||
					tokens += tks
 | 
			
		||||
					chatCtx = append(chatCtx, v)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// loading recent chat history as chat context
 | 
			
		||||
			if chatConfig.ContextDeep > 0 {
 | 
			
		||||
				var historyMessages []model.HistoryMessage
 | 
			
		||||
				res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("created_at desc").Find(&historyMessages)
 | 
			
		||||
				if res.Error == nil {
 | 
			
		||||
					for _, msg := range historyMessages {
 | 
			
		||||
						if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] {
 | 
			
		||||
							break
 | 
			
		||||
						}
 | 
			
		||||
						tokens += msg.Tokens
 | 
			
		||||
						ms := types.Message{Role: "user", Content: msg.Content}
 | 
			
		||||
						if msg.Type == types.ReplyMsg {
 | 
			
		||||
							ms.Role = "assistant"
 | 
			
		||||
						}
 | 
			
		||||
						chatCtx = append(chatCtx, ms)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		logger.Debugf("聊天上下文:%+v", chatCtx)
 | 
			
		||||
	}
 | 
			
		||||
	reqMgs := make([]interface{}, 0)
 | 
			
		||||
	for _, m := range chatCtx {
 | 
			
		||||
		reqMgs = append(reqMgs, m)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Messages = append(reqMgs, map[string]interface{}{
 | 
			
		||||
		"role":    "user",
 | 
			
		||||
		"content": prompt,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	switch session.Model.Platform {
 | 
			
		||||
	case types.Azure:
 | 
			
		||||
		return h.sendAzureMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
 | 
			
		||||
	case types.OpenAI:
 | 
			
		||||
		return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
 | 
			
		||||
	case types.ChatGLM:
 | 
			
		||||
		return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Errorf("not supported platform: %s", session.Model.Platform)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Tokens 统计 token 数量
 | 
			
		||||
func (h *ChatHandler) Tokens(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Text  string `json:"text"`
 | 
			
		||||
		Model string `json:"model"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tokens, err := utils.CalcTokens(data.Text, data.Model)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, tokens)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getTotalTokens(req types.ApiRequest) int {
 | 
			
		||||
	encode := utils.JsonEncode(req.Messages)
 | 
			
		||||
	var items []map[string]interface{}
 | 
			
		||||
	err := utils.JsonDecode(encode, &items)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	tokens := 0
 | 
			
		||||
	for _, item := range items {
 | 
			
		||||
		content, ok := item["content"]
 | 
			
		||||
		if ok && !utils.IsEmptyValue(content) {
 | 
			
		||||
			t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				tokens += t
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return tokens
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StopGenerate 停止生成
 | 
			
		||||
func (h *ChatHandler) StopGenerate(c *gin.Context) {
 | 
			
		||||
	sessionId := c.Query("session_id")
 | 
			
		||||
	if h.App.ReqCancelFunc.Has(sessionId) {
 | 
			
		||||
		h.App.ReqCancelFunc.Get(sessionId)()
 | 
			
		||||
		h.App.ReqCancelFunc.Delete(sessionId)
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, types.OkMsg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 发送请求到 OpenAI 服务器
 | 
			
		||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
 | 
			
		||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platform types.Platform, apiKey *string) (*http.Response, error) {
 | 
			
		||||
 | 
			
		||||
	var apiURL string
 | 
			
		||||
	switch platform {
 | 
			
		||||
	case types.Azure:
 | 
			
		||||
		md := strings.Replace(req.Model, ".", "", 1)
 | 
			
		||||
		apiURL = strings.Replace(h.App.ChatConfig.Azure.ApiURL, "{model}", md, 1)
 | 
			
		||||
		break
 | 
			
		||||
	case types.ChatGLM:
 | 
			
		||||
		apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1)
 | 
			
		||||
		req.Prompt = req.Messages
 | 
			
		||||
		req.Messages = nil
 | 
			
		||||
		break
 | 
			
		||||
	default:
 | 
			
		||||
		apiURL = h.App.ChatConfig.OpenAI.ApiURL
 | 
			
		||||
	}
 | 
			
		||||
	// 创建 HttpClient 请求对象
 | 
			
		||||
	var client *http.Client
 | 
			
		||||
	requestBody, err := json.Marshal(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	request = request.WithContext(ctx)
 | 
			
		||||
	request.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	proxyURL := h.App.Config.ProxyURL
 | 
			
		||||
	if proxyURL != "" && platform == types.OpenAI { // 使用代理
 | 
			
		||||
		proxy, _ := url.Parse(proxyURL)
 | 
			
		||||
		client = &http.Client{
 | 
			
		||||
			Transport: &http.Transport{
 | 
			
		||||
				Proxy: http.ProxyURL(proxy),
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		client = http.DefaultClient
 | 
			
		||||
	}
 | 
			
		||||
	if *apiKey == "" {
 | 
			
		||||
		var key model.ApiKey
 | 
			
		||||
		res := h.db.Where("platform = ?", platform).Order("last_used_at ASC").First(&key)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			return nil, errors.New("no available key, please import key")
 | 
			
		||||
		}
 | 
			
		||||
		// 更新 API KEY 的最后使用时间
 | 
			
		||||
		h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
 | 
			
		||||
		*apiKey = key.Value
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model)
 | 
			
		||||
	switch platform {
 | 
			
		||||
	case types.Azure:
 | 
			
		||||
		request.Header.Set("api-key", *apiKey)
 | 
			
		||||
		break
 | 
			
		||||
	case types.ChatGLM:
 | 
			
		||||
		token, err := h.getChatGLMToken(*apiKey)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		logger.Info(token)
 | 
			
		||||
		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
 | 
			
		||||
		break
 | 
			
		||||
	default:
 | 
			
		||||
		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
 | 
			
		||||
	}
 | 
			
		||||
	return client.Do(request)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										103
									
								
								api/handler/chat_history_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								api/handler/chat_history_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Update 更新会话标题
 | 
			
		||||
func (h *ChatHandler) Update(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id    uint   `json:"id"`
 | 
			
		||||
		Title string `json:"title"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var m = model.ChatItem{}
 | 
			
		||||
	m.Id = data.Id
 | 
			
		||||
	res := h.db.Model(&m).UpdateColumn("title", data.Title)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "Failed to update database")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, types.OkMsg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// History 获取聊天历史记录
 | 
			
		||||
func (h *ChatHandler) History(c *gin.Context) {
 | 
			
		||||
	chatId := c.Query("chat_id") // 会话 ID
 | 
			
		||||
	var items []model.HistoryMessage
 | 
			
		||||
	var messages = make([]vo.HistoryMessage, 0)
 | 
			
		||||
	res := h.db.Where("chat_id = ?", chatId).Find(&items)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "No history message")
 | 
			
		||||
		return
 | 
			
		||||
	} else {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var v vo.HistoryMessage
 | 
			
		||||
			err := utils.CopyObject(item, &v)
 | 
			
		||||
			v.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
			v.UpdatedAt = item.UpdatedAt.Unix()
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				messages = append(messages, v)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, messages)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Clear 清空所有聊天记录
 | 
			
		||||
func (h *ChatHandler) Clear(c *gin.Context) {
 | 
			
		||||
	// 获取当前登录用户所有的聊天会话
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var chats []model.ChatItem
 | 
			
		||||
	res := h.db.Where("user_id = ?", user.Id).Find(&chats)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "No chats found")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var chatIds = make([]string, 0)
 | 
			
		||||
	for _, chat := range chats {
 | 
			
		||||
		chatIds = append(chatIds, chat.ChatId)
 | 
			
		||||
		// 清空会话上下文
 | 
			
		||||
		h.App.ChatContexts.Delete(chat.ChatId)
 | 
			
		||||
	}
 | 
			
		||||
	err = h.db.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		res := h.db.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			return res.Error
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res = h.db.Where("user_id = ? AND chat_id IN ?", user.Id, chatIds).Delete(&model.HistoryMessage{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			return res.Error
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorf("Error with delete chats: %+v", err)
 | 
			
		||||
		resp.ERROR(c, "Failed to remove chat from database.")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, types.OkMsg)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										105
									
								
								api/handler/chat_item_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								api/handler/chat_item_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// List 获取会话列表
 | 
			
		||||
func (h *ChatHandler) List(c *gin.Context) {
 | 
			
		||||
	userId := h.GetInt(c, "user_id", 0)
 | 
			
		||||
	if userId == 0 {
 | 
			
		||||
		resp.ERROR(c, "The parameter 'user_id' is needed.")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var items = make([]vo.ChatItem, 0)
 | 
			
		||||
	var chats []model.ChatItem
 | 
			
		||||
	res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		var roleIds = make([]uint, 0)
 | 
			
		||||
		for _, chat := range chats {
 | 
			
		||||
			roleIds = append(roleIds, chat.RoleId)
 | 
			
		||||
		}
 | 
			
		||||
		var roles []model.ChatRole
 | 
			
		||||
		res = h.db.Find(&roles, roleIds)
 | 
			
		||||
		if res.Error == nil {
 | 
			
		||||
			roleMap := make(map[uint]model.ChatRole)
 | 
			
		||||
			for _, role := range roles {
 | 
			
		||||
				roleMap[role.Id] = role
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, chat := range chats {
 | 
			
		||||
				var item vo.ChatItem
 | 
			
		||||
				err := utils.CopyObject(chat, &item)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					item.Id = chat.Id
 | 
			
		||||
					item.Icon = roleMap[chat.RoleId].Icon
 | 
			
		||||
					items = append(items, item)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, items)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Remove 删除会话
 | 
			
		||||
func (h *ChatHandler) Remove(c *gin.Context) {
 | 
			
		||||
	chatId := h.GetTrim(c, "chat_id")
 | 
			
		||||
	if chatId == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{})
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "Failed to update database")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 删除当前会话的聊天记录
 | 
			
		||||
	res = h.db.Where("user_id = ? AND chat_id =?", user.Id, chatId).Delete(&model.ChatItem{})
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "Failed to remove chat from database.")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
 | 
			
		||||
 | 
			
		||||
	// 清空会话上下文
 | 
			
		||||
	h.App.ChatContexts.Delete(chatId)
 | 
			
		||||
	resp.SUCCESS(c, types.OkMsg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatHandler) Detail(c *gin.Context) {
 | 
			
		||||
	chatId := h.GetTrim(c, "chat_id")
 | 
			
		||||
	if utils.IsEmptyValue(chatId) {
 | 
			
		||||
		resp.ERROR(c, "Invalid chatId")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var chatItem model.ChatItem
 | 
			
		||||
	res := h.db.Where("chat_id = ?", chatId).First(&chatItem)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "No chat found")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var chatItemVo vo.ChatItem
 | 
			
		||||
	err := utils.CopyObject(chatItem, &chatItemVo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, chatItemVo)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								api/handler/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								api/handler/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ChatModelHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
 | 
			
		||||
	h := ChatModelHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List 模型列表
 | 
			
		||||
func (h *ChatModelHandler) List(c *gin.Context) {
 | 
			
		||||
	var items []model.ChatModel
 | 
			
		||||
	var cms = make([]vo.ChatModel, 0)
 | 
			
		||||
	res := h.db.Where("enabled = ?", true).Order("sort_num ASC").Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var cm vo.ChatModel
 | 
			
		||||
			err := utils.CopyObject(item, &cm)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				cm.Id = item.Id
 | 
			
		||||
				cm.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
				cm.UpdatedAt = item.UpdatedAt.Unix()
 | 
			
		||||
				cms = append(cms, cm)
 | 
			
		||||
			} else {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, cms)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								api/handler/chat_role_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								api/handler/chat_role_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ChatRoleHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
 | 
			
		||||
	handler := &ChatRoleHandler{db: db}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List get user list
 | 
			
		||||
func (h *ChatRoleHandler) List(c *gin.Context) {
 | 
			
		||||
	var roles []model.ChatRole
 | 
			
		||||
	res := h.db.Where("enable", true).Order("sort_num ASC").Find(&roles)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "No roles found,"+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var roleKeys []string
 | 
			
		||||
	err = utils.JsonDecode(user.ChatRoles, &roleKeys)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "角色解析失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 转成 vo
 | 
			
		||||
	var roleVos = make([]vo.ChatRole, 0)
 | 
			
		||||
	for _, r := range roles {
 | 
			
		||||
		if !utils.ContainsStr(roleKeys, r.Key) {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		var v vo.ChatRole
 | 
			
		||||
		err := utils.CopyObject(r, &v)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			v.Id = r.Id
 | 
			
		||||
			roleVos = append(roleVos, v)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, roleVos)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										239
									
								
								api/handler/chatglm_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								api/handler/chatglm_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,239 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 将消息发送给 ChatGLM API 并获取结果,通过 WebSocket 推送到客户端
 | 
			
		||||
func (h *ChatHandler) sendChatGLMMessage(
 | 
			
		||||
	chatCtx []interface{},
 | 
			
		||||
	req types.ApiRequest,
 | 
			
		||||
	userVo vo.User,
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	session *types.ChatSession,
 | 
			
		||||
	role model.ChatRole,
 | 
			
		||||
	prompt string,
 | 
			
		||||
	ws *types.WsClient) error {
 | 
			
		||||
	promptCreatedAt := time.Now() // 记录提问时间
 | 
			
		||||
	start := time.Now()
 | 
			
		||||
	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
 | 
			
		||||
	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
 | 
			
		||||
	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if strings.Contains(err.Error(), "context canceled") {
 | 
			
		||||
			logger.Info("用户取消了请求:", prompt)
 | 
			
		||||
			return nil
 | 
			
		||||
		} else if strings.Contains(err.Error(), "no available key") {
 | 
			
		||||
			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			logger.Error(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		utils.ReplyMessage(ws, ErrorMsg)
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		defer response.Body.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	contentType := response.Header.Get("Content-Type")
 | 
			
		||||
	if strings.Contains(contentType, "text/event-stream") {
 | 
			
		||||
		replyCreatedAt := time.Now() // 记录回复时间
 | 
			
		||||
		// 循环读取 Chunk 消息
 | 
			
		||||
		var message = types.Message{}
 | 
			
		||||
		var contents = make([]string, 0)
 | 
			
		||||
		var event, content string
 | 
			
		||||
		scanner := bufio.NewScanner(response.Body)
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			line := scanner.Text()
 | 
			
		||||
			if len(line) < 5 || strings.HasPrefix(line, "id:") {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if strings.HasPrefix(line, "event:") {
 | 
			
		||||
				event = line[6:]
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if strings.HasPrefix(line, "data:") {
 | 
			
		||||
				content = line[5:]
 | 
			
		||||
			}
 | 
			
		||||
			switch event {
 | 
			
		||||
			case "add":
 | 
			
		||||
				if len(contents) == 0 {
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
 | 
			
		||||
				}
 | 
			
		||||
				utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
					Type:    types.WsMiddle,
 | 
			
		||||
					Content: utils.InterfaceToString(content),
 | 
			
		||||
				})
 | 
			
		||||
				contents = append(contents, content)
 | 
			
		||||
			case "finish":
 | 
			
		||||
				break
 | 
			
		||||
			case "error":
 | 
			
		||||
				utils.ReplyMessage(ws, fmt.Sprintf("**调用 ChatGLM API 出错:%s**", content))
 | 
			
		||||
				break
 | 
			
		||||
			case "interrupted":
 | 
			
		||||
				utils.ReplyMessage(ws, "**调用 ChatGLM API 出错,当前输出被中断!**")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		} // end for
 | 
			
		||||
 | 
			
		||||
		if err := scanner.Err(); err != nil {
 | 
			
		||||
			if strings.Contains(err.Error(), "context canceled") {
 | 
			
		||||
				logger.Info("用户取消了请求:", prompt)
 | 
			
		||||
			} else {
 | 
			
		||||
				logger.Error("信息读取出错:", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 消息发送成功
 | 
			
		||||
		if len(contents) > 0 {
 | 
			
		||||
			// 更新用户的对话次数
 | 
			
		||||
			if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
 | 
			
		||||
				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if message.Role == "" {
 | 
			
		||||
				message.Role = "assistant"
 | 
			
		||||
			}
 | 
			
		||||
			message.Content = strings.Join(contents, "")
 | 
			
		||||
			useMsg := types.Message{Role: "user", Content: prompt}
 | 
			
		||||
 | 
			
		||||
			// 更新上下文消息,如果是调用函数则不需要更新上下文
 | 
			
		||||
			if h.App.ChatConfig.EnableContext {
 | 
			
		||||
				chatCtx = append(chatCtx, useMsg)  // 提问消息
 | 
			
		||||
				chatCtx = append(chatCtx, message) // 回复消息
 | 
			
		||||
				h.App.ChatContexts.Put(session.ChatId, chatCtx)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 追加聊天记录
 | 
			
		||||
			if h.App.ChatConfig.EnableHistory {
 | 
			
		||||
				// for prompt
 | 
			
		||||
				promptToken, err := utils.CalcTokens(prompt, req.Model)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Error(err)
 | 
			
		||||
				}
 | 
			
		||||
				historyUserMsg := model.HistoryMessage{
 | 
			
		||||
					UserId:     userVo.Id,
 | 
			
		||||
					ChatId:     session.ChatId,
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.PromptMsg,
 | 
			
		||||
					Icon:       userVo.Avatar,
 | 
			
		||||
					Content:    prompt,
 | 
			
		||||
					Tokens:     promptToken,
 | 
			
		||||
					UseContext: true,
 | 
			
		||||
				}
 | 
			
		||||
				historyUserMsg.CreatedAt = promptCreatedAt
 | 
			
		||||
				historyUserMsg.UpdatedAt = promptCreatedAt
 | 
			
		||||
				res := h.db.Save(&historyUserMsg)
 | 
			
		||||
				if res.Error != nil {
 | 
			
		||||
					logger.Error("failed to save prompt history message: ", res.Error)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// for reply
 | 
			
		||||
				// 计算本次对话消耗的总 token 数量
 | 
			
		||||
				var replyToken = 0
 | 
			
		||||
				replyToken, _ = utils.CalcTokens(message.Content, req.Model)
 | 
			
		||||
 | 
			
		||||
				historyReplyMsg := model.HistoryMessage{
 | 
			
		||||
					UserId:     userVo.Id,
 | 
			
		||||
					ChatId:     session.ChatId,
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.ReplyMsg,
 | 
			
		||||
					Icon:       role.Icon,
 | 
			
		||||
					Content:    message.Content,
 | 
			
		||||
					Tokens:     replyToken,
 | 
			
		||||
					UseContext: true,
 | 
			
		||||
				}
 | 
			
		||||
				historyReplyMsg.CreatedAt = replyCreatedAt
 | 
			
		||||
				historyReplyMsg.UpdatedAt = replyCreatedAt
 | 
			
		||||
				res = h.db.Create(&historyReplyMsg)
 | 
			
		||||
				if res.Error != nil {
 | 
			
		||||
					logger.Error("failed to save reply history message: ", res.Error)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 计算本次对话消耗的总 token 数量
 | 
			
		||||
				var totalTokens = 0
 | 
			
		||||
				totalTokens = replyToken + getTotalTokens(req)
 | 
			
		||||
				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
 | 
			
		||||
					UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 保存当前会话
 | 
			
		||||
			var chatItem model.ChatItem
 | 
			
		||||
			res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
 | 
			
		||||
			if res.Error != nil {
 | 
			
		||||
				chatItem.ChatId = session.ChatId
 | 
			
		||||
				chatItem.UserId = session.UserId
 | 
			
		||||
				chatItem.RoleId = role.Id
 | 
			
		||||
				chatItem.ModelId = session.Model.Id
 | 
			
		||||
				if utf8.RuneCountInString(prompt) > 30 {
 | 
			
		||||
					chatItem.Title = string([]rune(prompt)[:30]) + "..."
 | 
			
		||||
				} else {
 | 
			
		||||
					chatItem.Title = prompt
 | 
			
		||||
				}
 | 
			
		||||
				h.db.Create(&chatItem)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		body, err := io.ReadAll(response.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error with reading response: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var res struct {
 | 
			
		||||
			Code    int    `json:"code"`
 | 
			
		||||
			Success bool   `json:"success"`
 | 
			
		||||
			Msg     string `json:"msg"`
 | 
			
		||||
		}
 | 
			
		||||
		err = json.Unmarshal(body, &res)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error with decode response: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		if !res.Success {
 | 
			
		||||
			utils.ReplyMessage(ws, "请求 ChatGLM 失败:"+res.Msg)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatHandler) getChatGLMToken(apiKey string) (string, error) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	tokenString, err := h.redis.Get(ctx, apiKey).Result()
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return tokenString, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expr := time.Hour * 2
 | 
			
		||||
	key := strings.Split(apiKey, ".")
 | 
			
		||||
	if len(key) != 2 {
 | 
			
		||||
		return "", fmt.Errorf("invalid api key: %s", apiKey)
 | 
			
		||||
	}
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
		"api_key":   key[0],
 | 
			
		||||
		"timestamp": time.Now().Unix(),
 | 
			
		||||
		"exp":       time.Now().Add(expr).Add(time.Second * 10).Unix(),
 | 
			
		||||
	})
 | 
			
		||||
	token.Header["alg"] = "HS256"
 | 
			
		||||
	token.Header["sign_type"] = "SIGN"
 | 
			
		||||
	delete(token.Header, "typ")
 | 
			
		||||
	// Sign and get the complete encoded token as a string using the secret
 | 
			
		||||
	tokenString, err = token.SignedString([]byte(key[1]))
 | 
			
		||||
	h.redis.Set(ctx, apiKey, tokenString, expr)
 | 
			
		||||
	return tokenString, err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										244
									
								
								api/handler/mj_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								api/handler/mj_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,244 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/function"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TaskStatus string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	Start    = TaskStatus("Started")
 | 
			
		||||
	Running  = TaskStatus("Running")
 | 
			
		||||
	Stopped  = TaskStatus("Stopped")
 | 
			
		||||
	Finished = TaskStatus("Finished")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Image struct {
 | 
			
		||||
	URL      string `json:"url"`
 | 
			
		||||
	ProxyURL string `json:"proxy_url"`
 | 
			
		||||
	Filename string `json:"filename"`
 | 
			
		||||
	Width    int    `json:"width"`
 | 
			
		||||
	Height   int    `json:"height"`
 | 
			
		||||
	Size     int    `json:"size"`
 | 
			
		||||
	Hash     string `json:"hash"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MidJourneyHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	leveldb         *store.LevelDB
 | 
			
		||||
	db              *gorm.DB
 | 
			
		||||
	mjFunc          function.FuncMidJourney
 | 
			
		||||
	uploaderManager *oss.UploaderManager
 | 
			
		||||
	lock            sync.Mutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMidJourneyHandler(
 | 
			
		||||
	app *core.AppServer,
 | 
			
		||||
	leveldb *store.LevelDB,
 | 
			
		||||
	db *gorm.DB,
 | 
			
		||||
	manager *oss.UploaderManager,
 | 
			
		||||
	functions map[string]function.Function) *MidJourneyHandler {
 | 
			
		||||
	h := MidJourneyHandler{
 | 
			
		||||
		leveldb:         leveldb,
 | 
			
		||||
		db:              db,
 | 
			
		||||
		uploaderManager: manager,
 | 
			
		||||
		lock:            sync.Mutex{},
 | 
			
		||||
		mjFunc:          functions[types.FuncMidJourney].(function.FuncMidJourney)}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *MidJourneyHandler) Notify(c *gin.Context) {
 | 
			
		||||
	token := c.GetHeader("Authorization")
 | 
			
		||||
	if token != h.App.Config.ExtConfig.Token {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var data struct {
 | 
			
		||||
		MessageId   string     `json:"message_id"`
 | 
			
		||||
		ReferenceId string     `json:"reference_id"`
 | 
			
		||||
		Image       Image      `json:"image"`
 | 
			
		||||
		Content     string     `json:"content"`
 | 
			
		||||
		Prompt      string     `json:"prompt"`
 | 
			
		||||
		Status      TaskStatus `json:"status"`
 | 
			
		||||
		Key         string     `json:"key"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Debugf("收到 MidJourney 回调请求:%+v", data)
 | 
			
		||||
	h.lock.Lock()
 | 
			
		||||
	defer h.lock.Unlock()
 | 
			
		||||
	
 | 
			
		||||
	// the job is saved
 | 
			
		||||
	var job model.MidJourneyJob
 | 
			
		||||
	res := h.db.Where("message_id = ?", data.MessageId).First(&job)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data.Key = utils.Sha256(data.Prompt)
 | 
			
		||||
	//logger.Info(data.Prompt, ",", key)
 | 
			
		||||
	if data.Status == Finished {
 | 
			
		||||
		var task types.MjTask
 | 
			
		||||
		err := h.leveldb.Get(types.TaskStorePrefix+data.Key, &task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("error with get MidJourney task: ", err)
 | 
			
		||||
			resp.SUCCESS(c)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// download image
 | 
			
		||||
		imgURL, err := h.uploaderManager.GetUploadHandler().PutImg(data.Image.URL)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("error with download image: ", err)
 | 
			
		||||
			resp.SUCCESS(c)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		data.Image.URL = imgURL
 | 
			
		||||
		message := model.HistoryMessage{
 | 
			
		||||
			UserId:     task.UserId,
 | 
			
		||||
			ChatId:     task.ChatId,
 | 
			
		||||
			RoleId:     task.RoleId,
 | 
			
		||||
			Type:       types.MjMsg,
 | 
			
		||||
			Icon:       task.Icon,
 | 
			
		||||
			Content:    utils.JsonEncode(data),
 | 
			
		||||
			Tokens:     0,
 | 
			
		||||
			UseContext: false,
 | 
			
		||||
		}
 | 
			
		||||
		res := h.db.Create(&message)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			logger.Error("error with save chat history message: ", res.Error)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// save the job
 | 
			
		||||
		job.UserId = task.UserId
 | 
			
		||||
		job.ChatId = task.ChatId
 | 
			
		||||
		job.MessageId = data.MessageId
 | 
			
		||||
		job.ReferenceId = data.ReferenceId
 | 
			
		||||
		job.Content = data.Content
 | 
			
		||||
		job.Prompt = data.Prompt
 | 
			
		||||
		job.Image = utils.JsonEncode(data.Image)
 | 
			
		||||
		job.Hash = data.Image.Hash
 | 
			
		||||
		job.CreatedAt = time.Now()
 | 
			
		||||
		res = h.db.Create(&job)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			logger.Error("error with save MidJourney Job: ", res.Error)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 推送消息到客户端
 | 
			
		||||
	wsClient := h.App.MjTaskClients.Get(data.Key)
 | 
			
		||||
	if wsClient == nil { // 客户端断线,则丢弃
 | 
			
		||||
		logger.Errorf("Client is offline: %+v", data)
 | 
			
		||||
		resp.SUCCESS(c, "Client is offline")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if data.Status == Finished {
 | 
			
		||||
		utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
 | 
			
		||||
		utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
 | 
			
		||||
		// delete client
 | 
			
		||||
		h.App.MjTaskClients.Delete(data.Key)
 | 
			
		||||
	} else {
 | 
			
		||||
		// 使用代理临时转发图片
 | 
			
		||||
		if data.Image.URL != "" {
 | 
			
		||||
			image, err := utils.DownloadImage(data.Image.URL, h.App.Config.ProxyURL)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				data.Image.URL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, "SUCCESS")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type reqVo struct {
 | 
			
		||||
	Index       int32  `json:"index"`
 | 
			
		||||
	MessageId   string `json:"message_id"`
 | 
			
		||||
	MessageHash string `json:"message_hash"`
 | 
			
		||||
	SessionId   string `json:"session_id"`
 | 
			
		||||
	Key         string `json:"key"`
 | 
			
		||||
	Prompt      string `json:"prompt"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Upscale send upscale command to MidJourney Bot
 | 
			
		||||
func (h *MidJourneyHandler) Upscale(c *gin.Context) {
 | 
			
		||||
	var data reqVo
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil ||
 | 
			
		||||
		data.SessionId == "" ||
 | 
			
		||||
		data.Key == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	wsClient := h.App.ChatClients.Get(data.SessionId)
 | 
			
		||||
	if wsClient == nil {
 | 
			
		||||
		resp.ERROR(c, "No Websocket client online")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := h.mjFunc.Upscale(function.MjUpscaleReq{
 | 
			
		||||
		Index:       data.Index,
 | 
			
		||||
		MessageId:   data.MessageId,
 | 
			
		||||
		MessageHash: data.MessageHash,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content := fmt.Sprintf("**%s** 已推送 Upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
 | 
			
		||||
	utils.ReplyMessage(wsClient, content)
 | 
			
		||||
	if h.App.MjTaskClients.Get(data.Key) == nil {
 | 
			
		||||
		h.App.MjTaskClients.Put(data.Key, wsClient)
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *MidJourneyHandler) Variation(c *gin.Context) {
 | 
			
		||||
	var data reqVo
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil ||
 | 
			
		||||
		data.SessionId == "" ||
 | 
			
		||||
		data.Key == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	wsClient := h.App.ChatClients.Get(data.SessionId)
 | 
			
		||||
	if wsClient == nil {
 | 
			
		||||
		resp.ERROR(c, "No Websocket client online")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := h.mjFunc.Variation(function.MjVariationReq{
 | 
			
		||||
		Index:       data.Index,
 | 
			
		||||
		MessageId:   data.MessageId,
 | 
			
		||||
		MessageHash: data.MessageHash,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	content := fmt.Sprintf("**%s** 已推送 Variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
 | 
			
		||||
	utils.ReplyMessage(wsClient, content)
 | 
			
		||||
	if h.App.MjTaskClients.Get(data.Key) == nil {
 | 
			
		||||
		h.App.MjTaskClients.Put(data.Key, wsClient)
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										308
									
								
								api/handler/openai_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								api/handler/openai_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,308 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 将消息发送给 OpenAI API 并获取结果,通过 WebSocket 推送到客户端
 | 
			
		||||
func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
	chatCtx []interface{},
 | 
			
		||||
	req types.ApiRequest,
 | 
			
		||||
	userVo vo.User,
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	session *types.ChatSession,
 | 
			
		||||
	role model.ChatRole,
 | 
			
		||||
	prompt string,
 | 
			
		||||
	ws *types.WsClient) error {
 | 
			
		||||
	promptCreatedAt := time.Now() // 记录提问时间
 | 
			
		||||
	start := time.Now()
 | 
			
		||||
	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
 | 
			
		||||
	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
 | 
			
		||||
	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if strings.Contains(err.Error(), "context canceled") {
 | 
			
		||||
			logger.Info("用户取消了请求:", prompt)
 | 
			
		||||
			return nil
 | 
			
		||||
		} else if strings.Contains(err.Error(), "no available key") {
 | 
			
		||||
			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			logger.Error(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		utils.ReplyMessage(ws, ErrorMsg)
 | 
			
		||||
		utils.ReplyMessage(ws, "")
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		defer response.Body.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	contentType := response.Header.Get("Content-Type")
 | 
			
		||||
	if strings.Contains(contentType, "text/event-stream") {
 | 
			
		||||
		replyCreatedAt := time.Now() // 记录回复时间
 | 
			
		||||
		// 循环读取 Chunk 消息
 | 
			
		||||
		var message = types.Message{}
 | 
			
		||||
		var contents = make([]string, 0)
 | 
			
		||||
		var functionCall = false
 | 
			
		||||
		var functionName string
 | 
			
		||||
		var arguments = make([]string, 0)
 | 
			
		||||
		scanner := bufio.NewScanner(response.Body)
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			line := scanner.Text()
 | 
			
		||||
			if !strings.Contains(line, "data:") || len(line) < 30 {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var responseBody = types.ApiResponse{}
 | 
			
		||||
			err = json.Unmarshal([]byte(line[6:]), &responseBody)
 | 
			
		||||
			if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
 | 
			
		||||
				logger.Error(err, line)
 | 
			
		||||
				utils.ReplyMessage(ws, ErrorMsg)
 | 
			
		||||
				utils.ReplyMessage(ws, "")
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			fun := responseBody.Choices[0].Delta.FunctionCall
 | 
			
		||||
			if functionCall && fun.Name == "" {
 | 
			
		||||
				arguments = append(arguments, fun.Arguments)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if !utils.IsEmptyValue(fun) {
 | 
			
		||||
				functionName = fun.Name
 | 
			
		||||
				f := h.App.Functions[functionName]
 | 
			
		||||
				if f != nil {
 | 
			
		||||
					functionCall = true
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
 | 
			
		||||
				}
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 初始化 role
 | 
			
		||||
			if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
 | 
			
		||||
				message.Role = responseBody.Choices[0].Delta.Role
 | 
			
		||||
				utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
 | 
			
		||||
				continue
 | 
			
		||||
			} else if responseBody.Choices[0].FinishReason != "" {
 | 
			
		||||
				break // 输出完成或者输出中断了
 | 
			
		||||
			} else {
 | 
			
		||||
				content := responseBody.Choices[0].Delta.Content
 | 
			
		||||
				contents = append(contents, utils.InterfaceToString(content))
 | 
			
		||||
				utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
					Type:    types.WsMiddle,
 | 
			
		||||
					Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		} // end for
 | 
			
		||||
 | 
			
		||||
		if err := scanner.Err(); err != nil {
 | 
			
		||||
			if strings.Contains(err.Error(), "context canceled") {
 | 
			
		||||
				logger.Info("用户取消了请求:", prompt)
 | 
			
		||||
			} else {
 | 
			
		||||
				logger.Error("信息读取出错:", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if functionCall { // 调用函数完成任务
 | 
			
		||||
			var params map[string]interface{}
 | 
			
		||||
			_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
 | 
			
		||||
			logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
 | 
			
		||||
 | 
			
		||||
			// for creating image, check if the user's img_calls > 0
 | 
			
		||||
			if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
 | 
			
		||||
				utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
 | 
			
		||||
				utils.ReplyMessage(ws, "")
 | 
			
		||||
			} else {
 | 
			
		||||
				f := h.App.Functions[functionName]
 | 
			
		||||
				data, err := f.Invoke(params)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					msg := "调用函数出错:" + err.Error()
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
						Type:    types.WsMiddle,
 | 
			
		||||
						Content: msg,
 | 
			
		||||
					})
 | 
			
		||||
					contents = append(contents, msg)
 | 
			
		||||
				} else {
 | 
			
		||||
					content := data
 | 
			
		||||
					if functionName == types.FuncMidJourney {
 | 
			
		||||
						key := utils.Sha256(data)
 | 
			
		||||
						logger.Debug(data, ",", key)
 | 
			
		||||
						// add task for MidJourney
 | 
			
		||||
						h.App.MjTaskClients.Put(key, ws)
 | 
			
		||||
						task := types.MjTask{
 | 
			
		||||
							UserId: userVo.Id,
 | 
			
		||||
							RoleId: role.Id,
 | 
			
		||||
							Icon:   "/images/avatar/mid_journey.png",
 | 
			
		||||
							ChatId: session.ChatId,
 | 
			
		||||
						}
 | 
			
		||||
						err := h.leveldb.Put(types.TaskStorePrefix+key, task)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							logger.Error("error with store MidJourney task: ", err)
 | 
			
		||||
						}
 | 
			
		||||
						content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
 | 
			
		||||
 | 
			
		||||
						// update user's img_calls
 | 
			
		||||
						h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
						Type:    types.WsMiddle,
 | 
			
		||||
						Content: content,
 | 
			
		||||
					})
 | 
			
		||||
					contents = append(contents, content)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 消息发送成功
 | 
			
		||||
		if len(contents) > 0 {
 | 
			
		||||
			// 更新用户的对话次数
 | 
			
		||||
			if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
 | 
			
		||||
				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if message.Role == "" {
 | 
			
		||||
				message.Role = "assistant"
 | 
			
		||||
			}
 | 
			
		||||
			message.Content = strings.Join(contents, "")
 | 
			
		||||
			useMsg := types.Message{Role: "user", Content: prompt}
 | 
			
		||||
 | 
			
		||||
			// 更新上下文消息,如果是调用函数则不需要更新上下文
 | 
			
		||||
			if h.App.ChatConfig.EnableContext && functionCall == false {
 | 
			
		||||
				chatCtx = append(chatCtx, useMsg)  // 提问消息
 | 
			
		||||
				chatCtx = append(chatCtx, message) // 回复消息
 | 
			
		||||
				h.App.ChatContexts.Put(session.ChatId, chatCtx)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 追加聊天记录
 | 
			
		||||
			if h.App.ChatConfig.EnableHistory {
 | 
			
		||||
				useContext := true
 | 
			
		||||
				if functionCall {
 | 
			
		||||
					useContext = false
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// for prompt
 | 
			
		||||
				promptToken, err := utils.CalcTokens(prompt, req.Model)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Error(err)
 | 
			
		||||
				}
 | 
			
		||||
				historyUserMsg := model.HistoryMessage{
 | 
			
		||||
					UserId:     userVo.Id,
 | 
			
		||||
					ChatId:     session.ChatId,
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.PromptMsg,
 | 
			
		||||
					Icon:       userVo.Avatar,
 | 
			
		||||
					Content:    prompt,
 | 
			
		||||
					Tokens:     promptToken,
 | 
			
		||||
					UseContext: useContext,
 | 
			
		||||
				}
 | 
			
		||||
				historyUserMsg.CreatedAt = promptCreatedAt
 | 
			
		||||
				historyUserMsg.UpdatedAt = promptCreatedAt
 | 
			
		||||
				res := h.db.Save(&historyUserMsg)
 | 
			
		||||
				if res.Error != nil {
 | 
			
		||||
					logger.Error("failed to save prompt history message: ", res.Error)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// for reply
 | 
			
		||||
				// 计算本次对话消耗的总 token 数量
 | 
			
		||||
				var replyToken = 0
 | 
			
		||||
				if functionCall { // 函数名 + 参数 token
 | 
			
		||||
					tokens, _ := utils.CalcTokens(functionName, req.Model)
 | 
			
		||||
					replyToken += tokens
 | 
			
		||||
					tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
 | 
			
		||||
					replyToken += tokens
 | 
			
		||||
				} else {
 | 
			
		||||
					replyToken, _ = utils.CalcTokens(message.Content, req.Model)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				historyReplyMsg := model.HistoryMessage{
 | 
			
		||||
					UserId:     userVo.Id,
 | 
			
		||||
					ChatId:     session.ChatId,
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.ReplyMsg,
 | 
			
		||||
					Icon:       role.Icon,
 | 
			
		||||
					Content:    message.Content,
 | 
			
		||||
					Tokens:     replyToken,
 | 
			
		||||
					UseContext: useContext,
 | 
			
		||||
				}
 | 
			
		||||
				historyReplyMsg.CreatedAt = replyCreatedAt
 | 
			
		||||
				historyReplyMsg.UpdatedAt = replyCreatedAt
 | 
			
		||||
				res = h.db.Create(&historyReplyMsg)
 | 
			
		||||
				if res.Error != nil {
 | 
			
		||||
					logger.Error("failed to save reply history message: ", res.Error)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 计算本次对话消耗的总 token 数量
 | 
			
		||||
				var totalTokens = 0
 | 
			
		||||
				if functionCall { // prompt + 函数名 + 参数 token
 | 
			
		||||
					totalTokens = promptToken + replyToken
 | 
			
		||||
				} else {
 | 
			
		||||
					totalTokens = replyToken + getTotalTokens(req)
 | 
			
		||||
				}
 | 
			
		||||
				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
 | 
			
		||||
					UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 保存当前会话
 | 
			
		||||
			var chatItem model.ChatItem
 | 
			
		||||
			res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
 | 
			
		||||
			if res.Error != nil {
 | 
			
		||||
				chatItem.ChatId = session.ChatId
 | 
			
		||||
				chatItem.UserId = session.UserId
 | 
			
		||||
				chatItem.RoleId = role.Id
 | 
			
		||||
				chatItem.ModelId = session.Model.Id
 | 
			
		||||
				if utf8.RuneCountInString(prompt) > 30 {
 | 
			
		||||
					chatItem.Title = string([]rune(prompt)[:30]) + "..."
 | 
			
		||||
				} else {
 | 
			
		||||
					chatItem.Title = prompt
 | 
			
		||||
				}
 | 
			
		||||
				h.db.Create(&chatItem)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		body, err := io.ReadAll(response.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error with reading response: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		var res types.ApiError
 | 
			
		||||
		err = json.Unmarshal(body, &res)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error with decode response: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// OpenAI API 调用异常处理
 | 
			
		||||
		if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") {
 | 
			
		||||
			utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 所关联的账户被禁用。")
 | 
			
		||||
			// 移除当前 API key
 | 
			
		||||
			h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{})
 | 
			
		||||
		} else if strings.Contains(res.Error.Message, "You exceeded your current quota") {
 | 
			
		||||
			utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。")
 | 
			
		||||
		} else if strings.Contains(res.Error.Message, "This model's maximum context length") {
 | 
			
		||||
			logger.Error(res.Error.Message)
 | 
			
		||||
			utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
 | 
			
		||||
			h.App.ChatContexts.Delete(session.ChatId)
 | 
			
		||||
			return h.sendMessage(ctx, session, role, prompt, ws)
 | 
			
		||||
		} else {
 | 
			
		||||
			utils.ReplyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								api/handler/reward_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								api/handler/reward_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RewardHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
 | 
			
		||||
	h := RewardHandler{db: db}
 | 
			
		||||
	h.App = server
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *RewardHandler) Notify(c *gin.Context) {
 | 
			
		||||
	token := c.GetHeader("Authorization")
 | 
			
		||||
	if token != h.App.Config.ExtConfig.Token {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var data struct {
 | 
			
		||||
		TransId string  `json:"trans_id"` // 微信转账交易 ID
 | 
			
		||||
		Amount  float64 `json:"amount"`   // 微信转账交易金额
 | 
			
		||||
		Remark  string  `json:"remark"`   // 转账备注
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if data.Amount <= 0 {
 | 
			
		||||
		resp.ERROR(c, "Amount should not be 0")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Infof("收到众筹收款信息: %+v", data)
 | 
			
		||||
	var item model.Reward
 | 
			
		||||
	res := h.db.Where("tx_id = ?", data.TransId).First(&item)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.ERROR(c, "当前交易 ID 己经存在!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res = h.db.Create(&model.Reward{
 | 
			
		||||
		TxId:   data.TransId,
 | 
			
		||||
		Amount: data.Amount,
 | 
			
		||||
		Remark: data.Remark,
 | 
			
		||||
		Status: false,
 | 
			
		||||
	})
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		logger.Errorf("交易保存失败: %v", res.Error)
 | 
			
		||||
		resp.ERROR(c, "交易保存失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Verify 打赏码核销
 | 
			
		||||
func (h *RewardHandler) Verify(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		TxId string `json:"tx_id"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 移除转账单号中间的空格,防止有人复制的时候多复制了空格
 | 
			
		||||
	data.TxId = strings.ReplaceAll(data.TxId, " ", "")
 | 
			
		||||
 | 
			
		||||
	var item model.Reward
 | 
			
		||||
	res := h.db.Where("tx_id = ?", data.TxId).First(&item)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "无效的众筹交易流水号!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if item.Status {
 | 
			
		||||
		resp.ERROR(c, "当前众筹交易流水号已经被核销,请不要重复核销!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.HACKER(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tx := h.db.Begin()
 | 
			
		||||
	calls := (item.Amount + 0.1) * 10
 | 
			
		||||
	res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls))
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新核销状态
 | 
			
		||||
	item.Status = true
 | 
			
		||||
	item.UserId = user.Id
 | 
			
		||||
	res = h.db.Updates(&item)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tx.Commit()
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								api/handler/sms_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								api/handler/sms_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const CodeStorePrefix = "/verify/codes/"
 | 
			
		||||
 | 
			
		||||
type SmsHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	leveldb *store.LevelDB
 | 
			
		||||
	sms     *service.AliYunSmsService
 | 
			
		||||
	captcha *service.CaptchaService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
 | 
			
		||||
	handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendCode 发送验证码短信
 | 
			
		||||
func (h *SmsHandler) SendCode(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile string `json:"mobile"`
 | 
			
		||||
		Key    string `json:"key"`
 | 
			
		||||
		Dots   string `json:"dots"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !h.captcha.Check(data) {
 | 
			
		||||
		resp.ERROR(c, "验证码错误,请先完人机验证")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	code := utils.RandomNumber(6)
 | 
			
		||||
	err := h.sms.SendVerifyCode(data.Mobile, code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 存储验证码,等待后面注册验证
 | 
			
		||||
	err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "验证码保存失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type statusVo struct {
 | 
			
		||||
	EnabledMsgService bool `json:"enabled_msg_service"`
 | 
			
		||||
	EnabledRegister   bool `json:"enabled_register"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Status check if the message service is enabled
 | 
			
		||||
func (h *SmsHandler) Status(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsgService, EnabledRegister: h.App.SysConfig.EnabledRegister})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								api/handler/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								api/handler/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UploadHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db              *gorm.DB
 | 
			
		||||
	uploaderManager *oss.UploaderManager
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
 | 
			
		||||
	handler := &UploadHandler{db: db, uploaderManager: manager}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UploadHandler) Upload(c *gin.Context) {
 | 
			
		||||
	fileURL, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, fileURL)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										352
									
								
								api/handler/user_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										352
									
								
								api/handler/user_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,352 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UserHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db       *gorm.DB
 | 
			
		||||
	searcher *xdb.Searcher
 | 
			
		||||
	leveldb  *store.LevelDB
 | 
			
		||||
	redis    *redis.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUserHandler(
 | 
			
		||||
	app *core.AppServer,
 | 
			
		||||
	db *gorm.DB,
 | 
			
		||||
	searcher *xdb.Searcher,
 | 
			
		||||
	levelDB *store.LevelDB,
 | 
			
		||||
	client *redis.Client) *UserHandler {
 | 
			
		||||
	handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, redis: client}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Register user register
 | 
			
		||||
func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
	// parameters process
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile   string `json:"mobile"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
		Code     int    `json:"code"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	data.Password = strings.TrimSpace(data.Password)
 | 
			
		||||
 | 
			
		||||
	if len(data.Mobile) < 10 {
 | 
			
		||||
		resp.ERROR(c, "请输入合法的手机号")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if len(data.Password) < 8 {
 | 
			
		||||
		resp.ERROR(c, "密码长度不能少于8个字符")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	key := CodeStorePrefix + data.Mobile
 | 
			
		||||
	if h.App.SysConfig.EnabledMsgService {
 | 
			
		||||
		var code int
 | 
			
		||||
		err := h.leveldb.Get(key, &code)
 | 
			
		||||
		if err != nil || code != data.Code {
 | 
			
		||||
			resp.ERROR(c, "短信验证码错误")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check if the username is exists
 | 
			
		||||
	var item model.User
 | 
			
		||||
	res := h.db.Where("mobile = ?", data.Mobile).First(&item)
 | 
			
		||||
	if res.RowsAffected > 0 {
 | 
			
		||||
		resp.ERROR(c, "该手机号码已经被注册,请更换其他手机号")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 默认订阅所有角色
 | 
			
		||||
	var chatRoles []model.ChatRole
 | 
			
		||||
	h.db.Find(&chatRoles)
 | 
			
		||||
	var roleKeys = make([]string, 0)
 | 
			
		||||
	for _, r := range chatRoles {
 | 
			
		||||
		roleKeys = append(roleKeys, r.Key)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	salt := utils.RandString(8)
 | 
			
		||||
	user := model.User{
 | 
			
		||||
		Password:  utils.GenPassword(data.Password, salt),
 | 
			
		||||
		Avatar:    "/images/avatar/user.png",
 | 
			
		||||
		Salt:      salt,
 | 
			
		||||
		Status:    true,
 | 
			
		||||
		Mobile:    data.Mobile,
 | 
			
		||||
		ChatRoles: utils.JsonEncode(roleKeys),
 | 
			
		||||
		ChatConfig: utils.JsonEncode(types.UserChatConfig{
 | 
			
		||||
			ApiKeys: map[types.Platform]string{
 | 
			
		||||
				types.OpenAI:  "",
 | 
			
		||||
				types.Azure:   "",
 | 
			
		||||
				types.ChatGLM: "",
 | 
			
		||||
			},
 | 
			
		||||
		}),
 | 
			
		||||
		Calls:    h.App.SysConfig.UserInitCalls,
 | 
			
		||||
		ImgCalls: h.App.SysConfig.InitImgCalls,
 | 
			
		||||
	}
 | 
			
		||||
	res = h.db.Create(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "保存数据失败")
 | 
			
		||||
		logger.Error(res.Error)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h.App.SysConfig.EnabledMsgService {
 | 
			
		||||
		_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, user)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Login 用户登录
 | 
			
		||||
func (h *UserHandler) Login(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile   string `json:"username"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var user model.User
 | 
			
		||||
	res := h.db.Where("mobile = ?", data.Mobile).First(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "用户名不存在")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	password := utils.GenPassword(data.Password, user.Salt)
 | 
			
		||||
	if password != user.Password {
 | 
			
		||||
		resp.ERROR(c, "用户名或密码错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.Status == false {
 | 
			
		||||
		resp.ERROR(c, "该用户已被禁止登录,请联系管理员")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新最后登录时间和IP
 | 
			
		||||
	user.LastLoginIp = c.ClientIP()
 | 
			
		||||
	user.LastLoginAt = time.Now().Unix()
 | 
			
		||||
	h.db.Model(&user).Updates(user)
 | 
			
		||||
 | 
			
		||||
	h.db.Create(&model.UserLoginLog{
 | 
			
		||||
		UserId:       user.Id,
 | 
			
		||||
		Username:     user.Mobile,
 | 
			
		||||
		LoginIp:      c.ClientIP(),
 | 
			
		||||
		LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// 创建 token
 | 
			
		||||
	expired := time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge))
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
		"user_id": user.Id,
 | 
			
		||||
		"expired": expired,
 | 
			
		||||
	})
 | 
			
		||||
	tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "Failed to generate token, "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 保存到 redis
 | 
			
		||||
	key := fmt.Sprintf("users/%d", user.Id)
 | 
			
		||||
	if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with save token: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, tokenString)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Logout 注 销
 | 
			
		||||
func (h *UserHandler) Logout(c *gin.Context) {
 | 
			
		||||
	sessionId := c.GetHeader(types.ChatTokenHeader)
 | 
			
		||||
	token := c.GetHeader(types.UserAuthHeader)
 | 
			
		||||
	if _, err := h.redis.Del(c, token).Result(); err != nil {
 | 
			
		||||
		logger.Error("error with delete session: ", err)
 | 
			
		||||
	}
 | 
			
		||||
	// 删除 websocket 会话列表
 | 
			
		||||
	h.App.ChatSession.Delete(sessionId)
 | 
			
		||||
	// 关闭 socket 连接
 | 
			
		||||
	client := h.App.ChatClients.Get(sessionId)
 | 
			
		||||
	if client != nil {
 | 
			
		||||
		client.Close()
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Session 获取/验证会话
 | 
			
		||||
func (h *UserHandler) Session(c *gin.Context) {
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		var userVo vo.User
 | 
			
		||||
		err := utils.CopyObject(user, &userVo)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c)
 | 
			
		||||
		}
 | 
			
		||||
		userVo.Id = user.Id
 | 
			
		||||
		resp.SUCCESS(c, userVo)
 | 
			
		||||
	} else {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type userProfile struct {
 | 
			
		||||
	Id          uint                 `json:"id"`
 | 
			
		||||
	Mobile      string               `json:"mobile"`
 | 
			
		||||
	Avatar      string               `json:"avatar"`
 | 
			
		||||
	ChatConfig  types.UserChatConfig `json:"chat_config"`
 | 
			
		||||
	Calls       int                  `json:"calls"`
 | 
			
		||||
	ImgCalls    int                  `json:"img_calls"`
 | 
			
		||||
	TotalTokens int64                `json:"total_tokens"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UserHandler) Profile(c *gin.Context) {
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.db.First(&user, user.Id)
 | 
			
		||||
	var profile userProfile
 | 
			
		||||
	err = utils.CopyObject(user, &profile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("对象拷贝失败:", err.Error())
 | 
			
		||||
		resp.ERROR(c, "获取用户信息失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	profile.Id = user.Id
 | 
			
		||||
	resp.SUCCESS(c, profile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UserHandler) ProfileUpdate(c *gin.Context) {
 | 
			
		||||
	var data userProfile
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	h.db.First(&user, user.Id)
 | 
			
		||||
	user.Avatar = data.Avatar
 | 
			
		||||
	user.ChatConfig = utils.JsonEncode(data.ChatConfig)
 | 
			
		||||
	res := h.db.Updates(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新用户信息失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Password 更新密码
 | 
			
		||||
func (h *UserHandler) Password(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		OldPass  string `json:"old_pass"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(data.Password) < 8 {
 | 
			
		||||
		resp.ERROR(c, "密码长度不能少于8个字符")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	password := utils.GenPassword(data.OldPass, user.Salt)
 | 
			
		||||
	logger.Info(user.Salt, ",", user.Password, ",", password, ",", data.OldPass)
 | 
			
		||||
	if password != user.Password {
 | 
			
		||||
		resp.ERROR(c, "原密码错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newPass := utils.GenPassword(data.Password, user.Salt)
 | 
			
		||||
	res := h.db.Model(&user).UpdateColumn("password", newPass)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		logger.Error("更新数据库失败: ", res.Error)
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BindMobile 绑定手机号
 | 
			
		||||
func (h *UserHandler) BindMobile(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile string `json:"mobile"`
 | 
			
		||||
		Code   int    `json:"code"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查手机号是否被其他账号绑定
 | 
			
		||||
	var item model.User
 | 
			
		||||
	res := h.db.Where("mobile = ?", data.Mobile).First(&item)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.ERROR(c, "该手机号已经被其他账号绑定")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	key := CodeStorePrefix + data.Mobile
 | 
			
		||||
	var code int
 | 
			
		||||
	err := h.leveldb.Get(key, &code)
 | 
			
		||||
	if err != nil || code != data.Code {
 | 
			
		||||
		resp.ERROR(c, "短信验证码错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res = h.db.Model(&user).UpdateColumn("mobile", data.Mobile)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_ = h.leveldb.Delete(key) // 删除短信验证码
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								api/logger/logger.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								api/logger/logger.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
package logger
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
	"go.uber.org/zap/zapcore"
 | 
			
		||||
	"gopkg.in/natefinch/lumberjack.v2"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var logger *zap.Logger
 | 
			
		||||
var sugarLogger *zap.SugaredLogger
 | 
			
		||||
 | 
			
		||||
func GetLogger() *zap.SugaredLogger {
 | 
			
		||||
	if sugarLogger != nil {
 | 
			
		||||
		return sugarLogger
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL")))
 | 
			
		||||
	encoder := getEncoder()
 | 
			
		||||
	writerSyncer := getLogWriter()
 | 
			
		||||
	fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
 | 
			
		||||
	consoleOutput := zapcore.Lock(os.Stdout)
 | 
			
		||||
	consoleCore := zapcore.NewCore(
 | 
			
		||||
		encoder,
 | 
			
		||||
		consoleOutput,
 | 
			
		||||
		logLevel,
 | 
			
		||||
	)
 | 
			
		||||
	core := zapcore.NewTee(fileCore, consoleCore)
 | 
			
		||||
	logger = zap.New(core, zap.AddCaller())
 | 
			
		||||
	sugarLogger = logger.Sugar()
 | 
			
		||||
	return sugarLogger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// core 三个参数之  编码
 | 
			
		||||
func getEncoder() zapcore.Encoder {
 | 
			
		||||
	encoderConfig := zapcore.EncoderConfig{
 | 
			
		||||
		TimeKey:        "time",
 | 
			
		||||
		LevelKey:       "level",
 | 
			
		||||
		NameKey:        "logger",
 | 
			
		||||
		CallerKey:      "caller",
 | 
			
		||||
		MessageKey:     "msg",
 | 
			
		||||
		StacktraceKey:  "stacktrace",
 | 
			
		||||
		EncodeTime:     zapcore.ISO8601TimeEncoder,
 | 
			
		||||
		EncodeDuration: zapcore.SecondsDurationEncoder,
 | 
			
		||||
		EncodeCaller:   zapcore.ShortCallerEncoder,
 | 
			
		||||
		EncodeLevel:    zapcore.CapitalLevelEncoder,
 | 
			
		||||
	}
 | 
			
		||||
	return zapcore.NewConsoleEncoder(encoderConfig)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getLogWriter() zapcore.WriteSyncer {
 | 
			
		||||
	lumberJackLogger := &lumberjack.Logger{
 | 
			
		||||
		Filename:   "logs/app.log",
 | 
			
		||||
		MaxSize:    10,
 | 
			
		||||
		MaxBackups: 5,
 | 
			
		||||
		MaxAge:     30,
 | 
			
		||||
		Compress:   false,
 | 
			
		||||
	}
 | 
			
		||||
	return zapcore.AddSync(lumberJackLogger)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getLogLevel(level string) zapcore.Level {
 | 
			
		||||
	switch strings.ToUpper(level) {
 | 
			
		||||
	case "DEBUG":
 | 
			
		||||
		return zapcore.DebugLevel
 | 
			
		||||
	case "WARN":
 | 
			
		||||
		return zapcore.WarnLevel
 | 
			
		||||
	case "ERROR":
 | 
			
		||||
		return zapcore.ErrorLevel
 | 
			
		||||
	default:
 | 
			
		||||
		return zapcore.InfoLevel
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										284
									
								
								api/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								api/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,284 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/handler/admin"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/service/function"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"context"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
//go:embed res/ip2region.xdb
 | 
			
		||||
var xdbFS embed.FS
 | 
			
		||||
 | 
			
		||||
// AppLifecycle 应用程序生命周期
 | 
			
		||||
type AppLifecycle struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OnStart 应用程序启动时执行
 | 
			
		||||
func (l *AppLifecycle) OnStart(context.Context) error {
 | 
			
		||||
	log.Println("AppLifecycle OnStart")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OnStop 应用程序停止时执行
 | 
			
		||||
func (l *AppLifecycle) OnStop(context.Context) error {
 | 
			
		||||
	log.Println("AppLifecycle OnStop")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	configFile := os.Getenv("CONFIG_FILE")
 | 
			
		||||
	if configFile == "" {
 | 
			
		||||
		configFile = "config.toml"
 | 
			
		||||
	}
 | 
			
		||||
	var debug bool
 | 
			
		||||
	debugEnv := os.Getenv("DEBUG")
 | 
			
		||||
	if debugEnv == "" {
 | 
			
		||||
		debug = true
 | 
			
		||||
	} else {
 | 
			
		||||
		debug, _ = strconv.ParseBool(os.Getenv("DEBUG"))
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Loading config file: ", configFile)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := recover(); err != nil {
 | 
			
		||||
			logger.Error("Panic Error:", err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	app := fx.New(
 | 
			
		||||
		// 初始化配置应用配置
 | 
			
		||||
		fx.Provide(func() *types.AppConfig {
 | 
			
		||||
			config, err := core.LoadConfig(configFile)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			config.Path = configFile
 | 
			
		||||
			if debug {
 | 
			
		||||
				_ = core.SaveConfig(config)
 | 
			
		||||
			}
 | 
			
		||||
			return config
 | 
			
		||||
		}),
 | 
			
		||||
		// 创建应用服务
 | 
			
		||||
		fx.Provide(core.NewServer),
 | 
			
		||||
		// 初始化
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, client *redis.Client) {
 | 
			
		||||
			s.Init(debug, client)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 初始化数据库
 | 
			
		||||
		fx.Provide(store.NewGormConfig),
 | 
			
		||||
		fx.Provide(store.NewMysql),
 | 
			
		||||
		fx.Provide(store.NewLevelDB),
 | 
			
		||||
		fx.Provide(store.NewRedisClient),
 | 
			
		||||
 | 
			
		||||
		// 创建 Ip2Region 查询对象
 | 
			
		||||
		fx.Provide(func() (*xdb.Searcher, error) {
 | 
			
		||||
			file, err := xdbFS.Open("res/ip2region.xdb")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			cBuff, err := io.ReadAll(file)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return xdb.NewWithBuffer(cBuff)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 创建函数
 | 
			
		||||
		fx.Provide(function.NewFunctions),
 | 
			
		||||
 | 
			
		||||
		// 创建控制器
 | 
			
		||||
		fx.Provide(handler.NewChatRoleHandler),
 | 
			
		||||
		fx.Provide(handler.NewUserHandler),
 | 
			
		||||
		fx.Provide(handler.NewChatHandler),
 | 
			
		||||
		fx.Provide(handler.NewUploadHandler),
 | 
			
		||||
		fx.Provide(handler.NewSmsHandler),
 | 
			
		||||
		fx.Provide(handler.NewRewardHandler),
 | 
			
		||||
		fx.Provide(handler.NewCaptchaHandler),
 | 
			
		||||
		fx.Provide(handler.NewMidJourneyHandler),
 | 
			
		||||
		fx.Provide(handler.NewChatModelHandler),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(admin.NewConfigHandler),
 | 
			
		||||
		fx.Provide(admin.NewAdminHandler),
 | 
			
		||||
		fx.Provide(admin.NewApiKeyHandler),
 | 
			
		||||
		fx.Provide(admin.NewUserHandler),
 | 
			
		||||
		fx.Provide(admin.NewChatRoleHandler),
 | 
			
		||||
		fx.Provide(admin.NewRewardHandler),
 | 
			
		||||
		fx.Provide(admin.NewDashboardHandler),
 | 
			
		||||
		fx.Provide(admin.NewChatModelHandler),
 | 
			
		||||
 | 
			
		||||
		// 创建服务
 | 
			
		||||
		fx.Provide(service.NewAliYunSmsService),
 | 
			
		||||
		fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
 | 
			
		||||
			return service.NewCaptchaService(config.ApiConfig)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(oss.NewUploaderManager),
 | 
			
		||||
 | 
			
		||||
		// 注册路由
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/role/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/user/")
 | 
			
		||||
			group.POST("register", h.Register)
 | 
			
		||||
			group.POST("login", h.Login)
 | 
			
		||||
			group.GET("logout", h.Logout)
 | 
			
		||||
			group.GET("session", h.Session)
 | 
			
		||||
			group.GET("profile", h.Profile)
 | 
			
		||||
			group.POST("profile/update", h.ProfileUpdate)
 | 
			
		||||
			group.POST("password", h.Password)
 | 
			
		||||
			group.POST("bind/mobile", h.BindMobile)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/chat/")
 | 
			
		||||
			group.Any("new", h.ChatHandle)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("detail", h.Detail)
 | 
			
		||||
			group.POST("update", h.Update)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("history", h.History)
 | 
			
		||||
			group.GET("clear", h.Clear)
 | 
			
		||||
			group.GET("tokens", h.Tokens)
 | 
			
		||||
			group.GET("stop", h.StopGenerate)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
 | 
			
		||||
			s.Engine.POST("/api/upload", h.Upload)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/sms/")
 | 
			
		||||
			group.GET("status", h.Status)
 | 
			
		||||
			group.POST("code", h.SendCode)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/captcha/")
 | 
			
		||||
			group.GET("get", h.Get)
 | 
			
		||||
			group.POST("check", h.Check)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/reward/")
 | 
			
		||||
			group.POST("notify", h.Notify)
 | 
			
		||||
			group.POST("verify", h.Verify)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
 | 
			
		||||
			s.Engine.POST("/api/mj/notify", h.Notify)
 | 
			
		||||
			s.Engine.POST("/api/mj/upscale", h.Upscale)
 | 
			
		||||
			s.Engine.POST("/api/mj/variation", h.Variation)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 管理后台控制器
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/config/")
 | 
			
		||||
			group.POST("update", h.Update)
 | 
			
		||||
			group.GET("get", h.Get)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/")
 | 
			
		||||
			group.POST("login", h.Login)
 | 
			
		||||
			group.GET("logout", h.Logout)
 | 
			
		||||
			group.GET("session", h.Session)
 | 
			
		||||
			group.GET("migrate", h.Migrate)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/apikey/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/user/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("loginLog", h.LoginLog)
 | 
			
		||||
			group.POST("resetPass", h.ResetPass)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ChatRoleHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/role/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/reward/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/dashboard/")
 | 
			
		||||
			group.GET("stats", h.Stats)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/model/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/model/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("enable", h.Enable)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
 | 
			
		||||
			err := s.Run(db)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 注册生命周期回调函数
 | 
			
		||||
		fx.Invoke(func(lifecycle fx.Lifecycle, lc *AppLifecycle) {
 | 
			
		||||
			lifecycle.Append(fx.Hook{
 | 
			
		||||
				OnStart: func(ctx context.Context) error {
 | 
			
		||||
					return lc.OnStart(ctx)
 | 
			
		||||
				},
 | 
			
		||||
				OnStop: func(ctx context.Context) error {
 | 
			
		||||
					return lc.OnStop(ctx)
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
		}),
 | 
			
		||||
	)
 | 
			
		||||
	// 启动应用程序
 | 
			
		||||
	go func() {
 | 
			
		||||
		if err := app.Start(context.Background()); err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// 监听退出信号
 | 
			
		||||
	quit := make(chan os.Signal, 1)
 | 
			
		||||
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
 | 
			
		||||
	<-quit
 | 
			
		||||
 | 
			
		||||
	// 关闭应用程序
 | 
			
		||||
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
	if err := app.Stop(ctx); err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								api/res/ip2region.xdb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								api/res/ip2region.xdb
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										54
									
								
								api/service/aliyun_sms_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								api/service/aliyun_sms_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AliYunSmsService struct {
 | 
			
		||||
	config *types.AppConfig
 | 
			
		||||
	db     *store.LevelDB
 | 
			
		||||
	client *dysmsapi.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSmsService, error) {
 | 
			
		||||
	// 创建阿里云短信客户端
 | 
			
		||||
	client, err := dysmsapi.NewClientWithAccessKey(
 | 
			
		||||
		"cn-hangzhou",
 | 
			
		||||
		config.SmsConfig.AccessKey,
 | 
			
		||||
		config.SmsConfig.AccessSecret)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to create client: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &AliYunSmsService{
 | 
			
		||||
		config: config,
 | 
			
		||||
		db:     db,
 | 
			
		||||
		client: client,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *AliYunSmsService) SendVerifyCode(mobile string, code int) error {
 | 
			
		||||
	// 创建短信请求并设置参数
 | 
			
		||||
	request := dysmsapi.CreateSendSmsRequest()
 | 
			
		||||
	request.Scheme = "https"
 | 
			
		||||
	request.Domain = s.config.SmsConfig.Domain
 | 
			
		||||
	request.PhoneNumbers = mobile
 | 
			
		||||
	request.SignName = "飞行的蜗牛"
 | 
			
		||||
	request.TemplateCode = "SMS_281460317"
 | 
			
		||||
	request.TemplateParam = fmt.Sprintf("{\"code\":\"%d\"}", code) // 短信模板中的参数
 | 
			
		||||
 | 
			
		||||
	// 发送短信
 | 
			
		||||
	response, err := s.client.SendSms(request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to send SMS:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if response.Code != "OK" {
 | 
			
		||||
		return fmt.Errorf("failed to send SMS:%v", response.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								api/service/captcha_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								api/service/captcha_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type CaptchaService struct {
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCaptchaService(config types.ChatPlusApiConfig) *CaptchaService {
 | 
			
		||||
	return &CaptchaService{
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *CaptchaService) Get() (interface{}, error) {
 | 
			
		||||
	if s.config.Token == "" {
 | 
			
		||||
		return nil, errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/captcha/get", s.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := s.client.R().
 | 
			
		||||
		SetHeader("AppId", s.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return nil, fmt.Errorf("请求 API 失败:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return nil, fmt.Errorf("请求 API 失败:%s", res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res.Data, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *CaptchaService) Check(data interface{}) bool {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := s.client.R().
 | 
			
		||||
		SetHeader("AppId", s.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
 | 
			
		||||
		SetBodyJsonMarshal(data).
 | 
			
		||||
		SetSuccessResult(&res).Post(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								api/service/function/function.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								api/service/function/function.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Function interface {
 | 
			
		||||
	Invoke(map[string]interface{}) (string, error)
 | 
			
		||||
	Name() string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
type resVo struct {
 | 
			
		||||
	Code    types.BizCode `json:"code"`
 | 
			
		||||
	Message string        `json:"message"`
 | 
			
		||||
	Data    struct {
 | 
			
		||||
		Title     string     `json:"title"`
 | 
			
		||||
		UpdatedAt string     `json:"updated_at"`
 | 
			
		||||
		Items     []dataItem `json:"items"`
 | 
			
		||||
	} `json:"data"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type dataItem struct {
 | 
			
		||||
	Title  string `json:"title"`
 | 
			
		||||
	Url    string `json:"url"`
 | 
			
		||||
	Remark string `json:"remark"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFunctions(config *types.AppConfig) map[string]Function {
 | 
			
		||||
	return map[string]Function{
 | 
			
		||||
		types.FuncZaoBao:     NewZaoBao(config.ApiConfig),
 | 
			
		||||
		types.FuncWeibo:      NewWeiboHot(config.ApiConfig),
 | 
			
		||||
		types.FuncHeadLine:   NewHeadLines(config.ApiConfig),
 | 
			
		||||
		types.FuncMidJourney: NewMidJourneyFunc(config.ExtConfig),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										117
									
								
								api/service/function/mid_journey.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								api/service/function/mid_journey.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AI 绘画函数
 | 
			
		||||
 | 
			
		||||
type FuncMidJourney struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusExtConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMidJourneyFunc(config types.ChatPlusExtConfig) FuncMidJourney {
 | 
			
		||||
	return FuncMidJourney{
 | 
			
		||||
		name:   "MidJourney AI 绘画",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(30 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Infof("MJ 绘画参数:%+v", params)
 | 
			
		||||
	prompt := utils.InterfaceToString(params["prompt"])
 | 
			
		||||
	if !utils.IsEmptyValue(params["ar"]) {
 | 
			
		||||
		prompt = fmt.Sprintf("%s --ar %s", prompt, params["ar"])
 | 
			
		||||
		delete(params, "--ar")
 | 
			
		||||
	}
 | 
			
		||||
	if !utils.IsEmptyValue(params["niji"]) {
 | 
			
		||||
		prompt = fmt.Sprintf("%s --niji %s", prompt, params["niji"])
 | 
			
		||||
		delete(params, "niji")
 | 
			
		||||
	} else {
 | 
			
		||||
		prompt = prompt + " --v 5.2"
 | 
			
		||||
	}
 | 
			
		||||
	params["prompt"] = prompt
 | 
			
		||||
	url := fmt.Sprintf("%s/api/mj/image", f.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("Authorization", f.config.Token).
 | 
			
		||||
		SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetBody(params).
 | 
			
		||||
		SetSuccessResult(&res).Post(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", r.String(), err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return prompt, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MjUpscaleReq struct {
 | 
			
		||||
	Index       int32  `json:"index"`
 | 
			
		||||
	MessageId   string `json:"message_id"`
 | 
			
		||||
	MessageHash string `json:"message_hash"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncMidJourney) Upscale(upReq MjUpscaleReq) error {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/mj/upscale", f.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("Authorization", f.config.Token).
 | 
			
		||||
		SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetBody(upReq).
 | 
			
		||||
		SetSuccessResult(&res).Post(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return fmt.Errorf("%v%v", r.String(), err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MjVariationReq struct {
 | 
			
		||||
	Index       int32  `json:"index"`
 | 
			
		||||
	MessageId   string `json:"message_id"`
 | 
			
		||||
	MessageHash string `json:"message_hash"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncMidJourney) Variation(upReq MjVariationReq) error {
 | 
			
		||||
	url := fmt.Sprintf("%s/api/mj/variation", f.config.ApiURL)
 | 
			
		||||
	var res types.BizVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("Authorization", f.config.Token).
 | 
			
		||||
		SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetBody(upReq).
 | 
			
		||||
		SetSuccessResult(&res).Post(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return fmt.Errorf("%v%v", r.String(), err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncMidJourney) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncMidJourney{}
 | 
			
		||||
							
								
								
									
										58
									
								
								api/service/function/tou_tiao.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								api/service/function/tou_tiao.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 今日头条函数实现
 | 
			
		||||
 | 
			
		||||
type FuncHeadlines struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines {
 | 
			
		||||
	return FuncHeadlines{
 | 
			
		||||
		name:   "今日头条",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncHeadlines) Invoke(map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/headline/fetch", f.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("AppId", f.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", err, r.Err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := make([]string, 0)
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
 | 
			
		||||
	for i, v := range res.Data.Items {
 | 
			
		||||
		builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [%s]", i+1, v.Title, v.Url, v.Remark))
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Join(builder, "\n\n"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncHeadlines) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncHeadlines{}
 | 
			
		||||
							
								
								
									
										58
									
								
								api/service/function/weibo_hot.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								api/service/function/weibo_hot.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 微博热搜函数实现
 | 
			
		||||
 | 
			
		||||
type FuncWeiboHot struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot {
 | 
			
		||||
	return FuncWeiboHot{
 | 
			
		||||
		name:   "微博热搜",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncWeiboHot) Invoke(map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/weibo/fetch", f.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("AppId", f.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", err, r.Err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := make([]string, 0)
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
 | 
			
		||||
	for i, v := range res.Data.Items {
 | 
			
		||||
		builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [热度:%s]", i+1, v.Title, v.Url, v.Remark))
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Join(builder, "\n\n"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncWeiboHot) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncWeiboHot{}
 | 
			
		||||
							
								
								
									
										59
									
								
								api/service/function/zao_bao.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								api/service/function/zao_bao.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
package function
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 每日早报函数实现
 | 
			
		||||
 | 
			
		||||
type FuncZaoBao struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao {
 | 
			
		||||
	return FuncZaoBao{
 | 
			
		||||
		name:   "每日早报",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncZaoBao) Invoke(map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/zaobao/fetch", f.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("AppId", f.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", err, r.Err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := make([]string, 0)
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt))
 | 
			
		||||
	for _, v := range res.Data.Items {
 | 
			
		||||
		builder = append(builder, v.Title)
 | 
			
		||||
	}
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("%s", res.Data.Title))
 | 
			
		||||
	return strings.Join(builder, "\n\n"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncZaoBao) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncZaoBao{}
 | 
			
		||||
							
								
								
									
										64
									
								
								api/service/oss/localstorage_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								api/service/oss/localstorage_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
package oss
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LocalStorageService struct {
 | 
			
		||||
	config   *types.LocalStorageConfig
 | 
			
		||||
	proxyURL string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLocalStorageService(config *types.AppConfig) LocalStorageService {
 | 
			
		||||
	return LocalStorageService{
 | 
			
		||||
		config:   &config.OSS.Local,
 | 
			
		||||
		proxyURL: config.ProxyURL,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorageService) PutFile(ctx *gin.Context, name string) (string, error) {
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with get form: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filePath, err := utils.GenUploadPath(s.config.BasePath, file.Filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with generate filename: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	// 将文件保存到指定路径
 | 
			
		||||
	err = ctx.SaveUploadedFile(file, filePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with save upload file: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorageService) PutImg(imageURL string) (string, error) {
 | 
			
		||||
	filename := filepath.Base(imageURL)
 | 
			
		||||
	filePath, err := utils.GenUploadPath(s.config.BasePath, filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with generate image dir: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = utils.DownloadFile(imageURL, filePath, s.proxyURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with download image: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorageService) Delete(fileURL string) error {
 | 
			
		||||
	filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1)
 | 
			
		||||
	return os.Remove(filePath)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Uploader = LocalStorageService{}
 | 
			
		||||
							
								
								
									
										83
									
								
								api/service/oss/minio_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								api/service/oss/minio_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
package oss
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/minio/minio-go/v7"
 | 
			
		||||
	"github.com/minio/minio-go/v7/pkg/credentials"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MinioService struct {
 | 
			
		||||
	config   *types.MinioConfig
 | 
			
		||||
	client   *minio.Client
 | 
			
		||||
	proxyURL string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMinioService(appConfig *types.AppConfig) (MinioService, error) {
 | 
			
		||||
	config := &appConfig.OSS.Minio
 | 
			
		||||
	minioClient, err := minio.New(config.Endpoint, &minio.Options{
 | 
			
		||||
		Creds:  credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""),
 | 
			
		||||
		Secure: config.UseSSL,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return MinioService{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return MinioService{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s MinioService) PutImg(imageURL string) (string, error) {
 | 
			
		||||
	imageData, err := utils.DownloadImage(imageURL, s.proxyURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with download image: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	fileExt := filepath.Ext(filepath.Base(imageURL))
 | 
			
		||||
	filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	info, err := s.client.PutObject(
 | 
			
		||||
		context.Background(),
 | 
			
		||||
		s.config.Bucket,
 | 
			
		||||
		filename,
 | 
			
		||||
		strings.NewReader(string(imageData)),
 | 
			
		||||
		int64(len(imageData)),
 | 
			
		||||
		minio.PutObjectOptions{ContentType: "image/png"})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s MinioService) PutFile(ctx *gin.Context, name string) (string, error) {
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with get form: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	// Open the uploaded file
 | 
			
		||||
	fileReader, err := file.Open()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error opening file: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer fileReader.Close()
 | 
			
		||||
 | 
			
		||||
	fileExt := filepath.Ext(file.Filename)
 | 
			
		||||
	filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
 | 
			
		||||
		ContentType: file.Header.Get("Content-Type"),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error uploading to MinIO: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s MinioService) Delete(fileURL string) error {
 | 
			
		||||
	objectName := filepath.Base(fileURL)
 | 
			
		||||
	return s.client.RemoveObject(context.Background(), s.config.Bucket, objectName, minio.RemoveObjectOptions{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Uploader = MinioService{}
 | 
			
		||||
							
								
								
									
										98
									
								
								api/service/oss/qiniu_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								api/service/oss/qiniu_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
			
		||||
package oss
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/qiniu/go-sdk/v7/auth/qbox"
 | 
			
		||||
	"github.com/qiniu/go-sdk/v7/storage"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type QiNiuService struct {
 | 
			
		||||
	config   *types.QiNiuConfig
 | 
			
		||||
	token    string
 | 
			
		||||
	uploader *storage.FormUploader
 | 
			
		||||
	manager  *storage.BucketManager
 | 
			
		||||
	proxyURL string
 | 
			
		||||
	dir      string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewQiNiuService(appConfig *types.AppConfig) QiNiuService {
 | 
			
		||||
	config := &appConfig.OSS.QiNiu
 | 
			
		||||
	// build storage uploader
 | 
			
		||||
	zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone))
 | 
			
		||||
	if !ok {
 | 
			
		||||
		zone = storage.ZoneHuanan
 | 
			
		||||
	}
 | 
			
		||||
	storeConfig := storage.Config{Zone: &zone}
 | 
			
		||||
	formUploader := storage.NewFormUploader(&storeConfig)
 | 
			
		||||
	// generate token
 | 
			
		||||
	mac := qbox.NewMac(config.AccessKey, config.AccessSecret)
 | 
			
		||||
	putPolicy := storage.PutPolicy{
 | 
			
		||||
		Scope: config.Bucket,
 | 
			
		||||
	}
 | 
			
		||||
	return QiNiuService{
 | 
			
		||||
		config:   config,
 | 
			
		||||
		token:    putPolicy.UploadToken(mac),
 | 
			
		||||
		uploader: formUploader,
 | 
			
		||||
		manager:  storage.NewBucketManager(mac, &storeConfig),
 | 
			
		||||
		proxyURL: appConfig.ProxyURL,
 | 
			
		||||
		dir:      "chatgpt-plus",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s QiNiuService) PutFile(ctx *gin.Context, name string) (string, error) {
 | 
			
		||||
	// 解析表单
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	// 打开上传文件
 | 
			
		||||
	src, err := file.Open()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	defer src.Close()
 | 
			
		||||
 | 
			
		||||
	fileExt := filepath.Ext(file.Filename)
 | 
			
		||||
	key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	// 上传文件
 | 
			
		||||
	ret := storage.PutRet{}
 | 
			
		||||
	extra := storage.PutExtra{}
 | 
			
		||||
	err = s.uploader.Put(ctx, &ret, s.token, key, src, file.Size, &extra)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s QiNiuService) PutImg(imageURL string) (string, error) {
 | 
			
		||||
	imageData, err := utils.DownloadImage(imageURL, s.proxyURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with download image: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	fileExt := filepath.Ext(filepath.Base(imageURL))
 | 
			
		||||
	key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	ret := storage.PutRet{}
 | 
			
		||||
	extra := storage.PutExtra{}
 | 
			
		||||
	// 上传文件字节数据
 | 
			
		||||
	err = s.uploader.Put(context.Background(), &ret, s.token, key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s QiNiuService) Delete(fileURL string) error {
 | 
			
		||||
	objectName := filepath.Base(fileURL)
 | 
			
		||||
	key := fmt.Sprintf("%s/%s", s.dir, objectName)
 | 
			
		||||
	return s.manager.Delete(s.config.Bucket, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Uploader = QiNiuService{}
 | 
			
		||||
							
								
								
									
										9
									
								
								api/service/oss/uploader.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/service/oss/uploader.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package oss
 | 
			
		||||
 | 
			
		||||
import "github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
type Uploader interface {
 | 
			
		||||
	PutFile(ctx *gin.Context, name string) (string, error)
 | 
			
		||||
	PutImg(imageURL string) (string, error)
 | 
			
		||||
	Delete(fileURL string) error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								api/service/oss/uploader_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								api/service/oss/uploader_manager.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
package oss
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UploaderManager struct {
 | 
			
		||||
	handler Uploader
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Local = "LOCAL"
 | 
			
		||||
const Minio = "MINIO"
 | 
			
		||||
const QiNiu = "QINIU"
 | 
			
		||||
 | 
			
		||||
func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) {
 | 
			
		||||
	active := Local
 | 
			
		||||
	if config.OSS.Active != "" {
 | 
			
		||||
		active = strings.ToUpper(config.OSS.Active)
 | 
			
		||||
	}
 | 
			
		||||
	var handler Uploader
 | 
			
		||||
	switch active {
 | 
			
		||||
	case Local:
 | 
			
		||||
		handler = NewLocalStorageService(config)
 | 
			
		||||
		break
 | 
			
		||||
	case Minio:
 | 
			
		||||
		service, err := NewMinioService(config)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		handler = service
 | 
			
		||||
		break
 | 
			
		||||
	case QiNiu:
 | 
			
		||||
		handler = NewQiNiuService(config)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &UploaderManager{handler: handler}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *UploaderManager) GetUploadHandler() Uploader {
 | 
			
		||||
	return m.handler
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								api/service/sms_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								api/service/sms_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
type SmsService interface {
 | 
			
		||||
	SendVerifyCode(mobile string, code int) error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								api/static/hello.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								api/static/hello.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
hello, world!
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
package utils
 | 
			
		||||
package store
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"github.com/syndtr/goleveldb/leveldb"
 | 
			
		||||
	"github.com/syndtr/goleveldb/leveldb/util"
 | 
			
		||||
)
 | 
			
		||||
@@ -11,8 +11,8 @@ type LevelDB struct {
 | 
			
		||||
	driver *leveldb.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLevelDB(path string) (*LevelDB, error) {
 | 
			
		||||
	db, err := leveldb.OpenFile(path, nil)
 | 
			
		||||
func NewLevelDB() (*LevelDB, error) {
 | 
			
		||||
	db, err := leveldb.OpenFile("data/leveldb", nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -29,13 +29,13 @@ func (db *LevelDB) Put(key string, value interface{}) error {
 | 
			
		||||
	return db.driver.Put([]byte(key), bytes, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *LevelDB) Get(key string) ([]byte, error) {
 | 
			
		||||
func (db *LevelDB) Get(key string, value interface{}) error {
 | 
			
		||||
	bytes, err := db.driver.Get([]byte(key), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return bytes, nil
 | 
			
		||||
	return json.Unmarshal(bytes, &value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *LevelDB) Search(prefix string) []string {
 | 
			
		||||
@@ -49,27 +49,19 @@ func (db *LevelDB) Search(prefix string) []string {
 | 
			
		||||
	return items
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PageVo struct {
 | 
			
		||||
	Items     []string
 | 
			
		||||
	Page      int
 | 
			
		||||
	PageSize  int
 | 
			
		||||
	Total     int
 | 
			
		||||
	TotalPage int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *PageVo {
 | 
			
		||||
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *vo.Page {
 | 
			
		||||
	var items = make([]string, 0)
 | 
			
		||||
	iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
 | 
			
		||||
	defer iter.Release()
 | 
			
		||||
 | 
			
		||||
	res := &PageVo{Page: page, PageSize: pageSize}
 | 
			
		||||
	res := &vo.Page{Page: page, PageSize: pageSize}
 | 
			
		||||
	// 计算数据总数和总页数
 | 
			
		||||
	total := 0
 | 
			
		||||
	for iter.Next() {
 | 
			
		||||
		total++
 | 
			
		||||
	}
 | 
			
		||||
	res.TotalPage = (total + pageSize - 1) / pageSize
 | 
			
		||||
	res.Total = total
 | 
			
		||||
	res.Total = int64(total)
 | 
			
		||||
 | 
			
		||||
	// 计算目标页码的起始和结束位置
 | 
			
		||||
	start := (page - 1) * pageSize
 | 
			
		||||
							
								
								
									
										9
									
								
								api/store/model/api_key.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/store/model/api_key.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
// ApiKey OpenAI API 模型
 | 
			
		||||
type ApiKey struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	Platform   string
 | 
			
		||||
	Value      string // API Key 的值
 | 
			
		||||
	LastUsedAt int64  // 最后使用时间
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								api/store/model/base.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/store/model/base.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
type BaseModel struct {
 | 
			
		||||
	Id        uint `gorm:"primarykey;column:id"`
 | 
			
		||||
	CreatedAt time.Time
 | 
			
		||||
	UpdatedAt time.Time
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								api/store/model/chat_history.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/store/model/chat_history.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import "gorm.io/gorm"
 | 
			
		||||
 | 
			
		||||
type HistoryMessage struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	ChatId     string // 会话 ID
 | 
			
		||||
	UserId     uint   // 用户 ID
 | 
			
		||||
	RoleId     uint   // 角色 ID
 | 
			
		||||
	Type       string
 | 
			
		||||
	Icon       string
 | 
			
		||||
	Tokens     int
 | 
			
		||||
	Content    string
 | 
			
		||||
	UseContext bool // 是否可以作为聊天上下文
 | 
			
		||||
	DeletedAt  gorm.DeletedAt
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (HistoryMessage) TableName() string {
 | 
			
		||||
	return "chatgpt_chat_history"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								api/store/model/chat_item.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								api/store/model/chat_item.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import "gorm.io/gorm"
 | 
			
		||||
 | 
			
		||||
type ChatItem struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	ChatId    string `gorm:"column:chat_id;unique"` // 会话 ID
 | 
			
		||||
	UserId    uint   // 用户 ID
 | 
			
		||||
	RoleId    uint   // 角色 ID
 | 
			
		||||
	ModelId   uint   // 会话模型
 | 
			
		||||
	Title     string // 会话标题
 | 
			
		||||
	DeletedAt gorm.DeletedAt
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								api/store/model/chat_model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								api/store/model/chat_model.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
type ChatModel struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	Platform string
 | 
			
		||||
	Name     string
 | 
			
		||||
	Value    string // API Key 的值
 | 
			
		||||
	SortNum  int
 | 
			
		||||
	Enabled  bool
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								api/store/model/chat_role.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								api/store/model/chat_role.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
type ChatRole struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	Key      string `gorm:"column:marker;unique"` // 角色唯一标识
 | 
			
		||||
	Name     string // 角色名称
 | 
			
		||||
	Context  string `gorm:"column:context_json"` // 角色语料信息 json
 | 
			
		||||
	HelloMsg string // 打招呼的消息
 | 
			
		||||
	Icon     string // 角色聊天图标
 | 
			
		||||
	Enable   bool   // 是否启用被启用
 | 
			
		||||
	SortNum  int    //排序数字
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								api/store/model/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								api/store/model/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Id     uint   `gorm:"primarykey;column:id"`
 | 
			
		||||
	Key    string `gorm:"column:marker;unique"`
 | 
			
		||||
	Config string `gorm:"column:config_json"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								api/store/model/mj_job.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/store/model/mj_job.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
type MidJourneyJob struct {
 | 
			
		||||
	Id          uint `gorm:"primarykey;column:id"`
 | 
			
		||||
	UserId      uint
 | 
			
		||||
	ChatId      string
 | 
			
		||||
	MessageId   string
 | 
			
		||||
	ReferenceId string
 | 
			
		||||
	Hash        string
 | 
			
		||||
	Content     string
 | 
			
		||||
	Prompt      string
 | 
			
		||||
	Image       string
 | 
			
		||||
	CreatedAt   time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (MidJourneyJob) TableName() string {
 | 
			
		||||
	return "chatgpt_mj_jobs"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								api/store/model/reward.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								api/store/model/reward.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
// 用户打赏
 | 
			
		||||
 | 
			
		||||
type Reward struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	UserId uint    // 用户 ID
 | 
			
		||||
	TxId   string  // 交易ID
 | 
			
		||||
	Amount float64 // 打赏金额
 | 
			
		||||
	Remark string  // 打赏备注
 | 
			
		||||
	Status bool    // 核销状态
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								api/store/model/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/store/model/user.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
type User struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	Mobile      string
 | 
			
		||||
	Password    string
 | 
			
		||||
	Avatar      string
 | 
			
		||||
	Salt        string // 密码盐
 | 
			
		||||
	TotalTokens int64  // 总消耗 tokens
 | 
			
		||||
	Calls       int    // 剩余对话次数
 | 
			
		||||
	ImgCalls    int    // 剩余绘图次数
 | 
			
		||||
	ChatConfig  string `gorm:"column:chat_config_json"` // 聊天配置 json
 | 
			
		||||
	ChatRoles   string `gorm:"column:chat_roles_json"`  // 聊天角色
 | 
			
		||||
	ExpiredTime int64  // 账户到期时间
 | 
			
		||||
	Status      bool   `gorm:"default:true"` // 当前状态
 | 
			
		||||
	LastLoginAt int64  // 最后登录时间
 | 
			
		||||
	LastLoginIp string // 最后登录 IP
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								api/store/model/user_login_log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/store/model/user_login_log.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
type UserLoginLog struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	UserId       uint
 | 
			
		||||
	Username     string
 | 
			
		||||
	LoginIp      string
 | 
			
		||||
	LoginAddress string
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								api/store/mysql.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								api/store/mysql.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
package store
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"gorm.io/driver/mysql"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"gorm.io/gorm/logger"
 | 
			
		||||
	"gorm.io/gorm/schema"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NewGormConfig() *gorm.Config {
 | 
			
		||||
	return &gorm.Config{
 | 
			
		||||
		Logger: logger.Default.LogMode(logger.Warn),
 | 
			
		||||
		NamingStrategy: schema.NamingStrategy{
 | 
			
		||||
			TablePrefix:   "chatgpt_", // 设置表前缀
 | 
			
		||||
			SingularTable: false,      // 使用单数表名形式
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) {
 | 
			
		||||
	db, err := gorm.Open(mysql.Open(appConfig.MysqlDns), config)
 | 
			
		||||
 | 
			
		||||
	sqlDB, err := db.DB()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	sqlDB.SetMaxIdleConns(32)
 | 
			
		||||
	sqlDB.SetMaxOpenConns(512)
 | 
			
		||||
	sqlDB.SetConnMaxLifetime(time.Hour)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return db, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								api/store/redis.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/store/redis.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
package store
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NewRedisClient(config *types.AppConfig) (*redis.Client, error) {
 | 
			
		||||
	client := redis.NewClient(&redis.Options{
 | 
			
		||||
		Addr:     config.Redis.Url(),
 | 
			
		||||
		Password: config.Redis.Password,
 | 
			
		||||
		DB:       config.Redis.DB,
 | 
			
		||||
	})
 | 
			
		||||
	_, err := client.Ping(context.Background()).Result()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return client, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								api/store/vo/api_key.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/store/vo/api_key.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
// ApiKey OpenAI API 模型
 | 
			
		||||
type ApiKey struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	Platform   string `json:"platform"`
 | 
			
		||||
	Value      string `json:"value"`        // API Key 的值
 | 
			
		||||
	LastUsedAt int64  `json:"last_used_at"` // 最后使用时间
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								api/store/vo/base.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								api/store/vo/base.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
type BaseVo struct {
 | 
			
		||||
	Id        uint  `json:"id"`
 | 
			
		||||
	CreatedAt int64 `json:"created_at"`
 | 
			
		||||
	UpdatedAt int64 `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								api/store/vo/chat_history.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								api/store/vo/chat_history.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
type HistoryMessage struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	ChatId     string `json:"chat_id"`
 | 
			
		||||
	UserId     uint   `json:"user_id"`
 | 
			
		||||
	RoleId     uint   `json:"role_id"`
 | 
			
		||||
	Type       string `json:"type"`
 | 
			
		||||
	Icon       string `json:"icon"`
 | 
			
		||||
	Tokens     int    `json:"tokens"`
 | 
			
		||||
	Content    string `json:"content"`
 | 
			
		||||
	UseContext bool   `json:"use_context"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								api/store/vo/chat_item.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/store/vo/chat_item.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
type ChatItem struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	UserId  uint   `json:"user_id"`
 | 
			
		||||
	Icon    string `json:"icon"`
 | 
			
		||||
	RoleId  uint   `json:"role_id"`
 | 
			
		||||
	ChatId  string `json:"chat_id"`
 | 
			
		||||
	ModelId uint   `json:"model_id"`
 | 
			
		||||
	Title   string `json:"title"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								api/store/vo/chat_model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/store/vo/chat_model.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
type ChatModel struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	Platform string `json:"platform"`
 | 
			
		||||
	Name     string `json:"name"`
 | 
			
		||||
	Value    string `json:"value"`
 | 
			
		||||
	Enabled  bool   `json:"enabled"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								api/store/vo/chat_role.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								api/store/vo/chat_role.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
import "chatplus/core/types"
 | 
			
		||||
 | 
			
		||||
type ChatRole struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	Key      string          `json:"key"`       // 角色唯一标识
 | 
			
		||||
	Name     string          `json:"name"`      // 角色名称
 | 
			
		||||
	Context  []types.Message `json:"context"`   // 角色语料信息
 | 
			
		||||
	HelloMsg string          `json:"hello_msg"` // 打招呼的消息
 | 
			
		||||
	Icon     string          `json:"icon"`      // 角色聊天图标
 | 
			
		||||
	Enable   bool            `json:"enable"`    // 是否启用被启用
 | 
			
		||||
	SortNum  int             `json:"sort"`      // 排序
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								api/store/vo/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								api/store/vo/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
import "chatplus/core/types"
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Id           uint               `json:"id"`
 | 
			
		||||
	Key          string             `json:"key"`
 | 
			
		||||
	ChatConfig   types.ChatConfig   `json:"chat_config"`
 | 
			
		||||
	SystemConfig types.SystemConfig `json:"system_config"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								api/store/vo/page.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								api/store/vo/page.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
import "math"
 | 
			
		||||
 | 
			
		||||
type Page struct {
 | 
			
		||||
	Items     interface{} `json:"items"`
 | 
			
		||||
	Page      int         `json:"page"`
 | 
			
		||||
	PageSize  int         `json:"page_size"`
 | 
			
		||||
	Total     int64       `json:"total"`
 | 
			
		||||
	TotalPage int         `json:"total_page"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPage(total int64, page int, pageSize int, items interface{}) Page {
 | 
			
		||||
	totalPage := math.Ceil(float64(total) / float64(pageSize))
 | 
			
		||||
	return Page{
 | 
			
		||||
		Items:     items,
 | 
			
		||||
		Page:      page,
 | 
			
		||||
		PageSize:  pageSize,
 | 
			
		||||
		Total:     total,
 | 
			
		||||
		TotalPage: int(totalPage),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								api/store/vo/reward.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/store/vo/reward.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
type Reward struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	UserId   uint    `json:"user_id"` // 用户 ID
 | 
			
		||||
	Username string  `json:"username"`
 | 
			
		||||
	TxId     string  `json:"tx_id"`  // 交易ID
 | 
			
		||||
	Amount   float64 `json:"amount"` // 打赏金额
 | 
			
		||||
	Remark   string  `json:"remark"` // 打赏备注
 | 
			
		||||
	Status   bool    `json:"status"` // 核销状态
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								api/store/vo/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								api/store/vo/user.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
import "chatplus/core/types"
 | 
			
		||||
 | 
			
		||||
type User struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	Mobile      string               `json:"mobile"`
 | 
			
		||||
	Avatar      string               `json:"avatar"`
 | 
			
		||||
	Salt        string               `json:"salt"`         // 密码盐
 | 
			
		||||
	TotalTokens int64                `json:"total_tokens"` // 总消耗tokens
 | 
			
		||||
	Calls       int                  `json:"calls"`        // 剩余对话次数
 | 
			
		||||
	ImgCalls    int                  `json:"img_calls"`
 | 
			
		||||
	ChatConfig  types.UserChatConfig `json:"chat_config"`   // 聊天配置
 | 
			
		||||
	ChatRoles   []string             `json:"chat_roles"`    // 聊天角色集合
 | 
			
		||||
	ExpiredTime int64                `json:"expired_time"`  // 账户到期时间
 | 
			
		||||
	Status      bool                 `json:"status"`        // 当前状态
 | 
			
		||||
	LastLoginAt int64                `json:"last_login_at"` // 最后登录时间
 | 
			
		||||
	LastLoginIp string               `json:"last_login_ip"` // 最后登录 IP
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								api/store/vo/user_login_log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/store/vo/user_login_log.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
type UserLoginLog struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	UserId       uint   `json:"user_id"`
 | 
			
		||||
	Username     string `json:"username"`
 | 
			
		||||
	LoginIp      string `json:"login_ip"`
 | 
			
		||||
	LoginAddress string `json:"login_address"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										199
									
								
								api/test/test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								api/test/test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,199 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
	"github.com/pkoukk/tiktoken-go"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	imageURL := "https://cdn.discordapp.com/attachments/1139552247693443184/1141619433752768572/lisamiller4099_A_beautiful_fairy_sister_from_Chinese_mythology__3162726e-5ee4-4f60-932b-6b78b375eaef.png"
 | 
			
		||||
 | 
			
		||||
	fmt.Println(filepath.Ext(filepath.Base(imageURL)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Http client 取消操作
 | 
			
		||||
func testHttpClient(ctx context.Context) {
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", "http://localhost:2345", nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req = req.WithContext(ctx)
 | 
			
		||||
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer func(Body io.ReadCloser) {
 | 
			
		||||
		err := Body.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}(resp.Body)
 | 
			
		||||
	_, err = io.ReadAll(resp.Body)
 | 
			
		||||
	for {
 | 
			
		||||
		time.Sleep(time.Second)
 | 
			
		||||
		fmt.Println(time.Now())
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			fmt.Println("取消退出")
 | 
			
		||||
			return
 | 
			
		||||
		default:
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testDate() {
 | 
			
		||||
	fmt.Println(time.Unix(1683336167, 0).Format("2006-01-02 15:04:05"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testIp2Region() {
 | 
			
		||||
	dbPath := "res/ip2region.xdb"
 | 
			
		||||
	// 1、从 dbPath 加载整个 xdb 到内存
 | 
			
		||||
	cBuff, err := xdb.LoadContentFromFile(dbPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 2、用全局的 cBuff 创建完全基于内存的查询对象。
 | 
			
		||||
	searcher, err := xdb.NewWithBuffer(cBuff)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Printf("failed to create searcher with content: %s\n", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	str, err := searcher.SearchByStr("103.88.46.85")
 | 
			
		||||
	fmt.Println(str)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	arr := strings.Split(str, "|")
 | 
			
		||||
	fmt.Println(arr[2], arr[3], arr[4])
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func calTokens() {
 | 
			
		||||
	text := "须知少年凌云志,曾许人间第一流"
 | 
			
		||||
	encoding := "cl100k_base"
 | 
			
		||||
 | 
			
		||||
	tke, err := tiktoken.GetEncoding(encoding)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		err = fmt.Errorf("getEncoding: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// encode
 | 
			
		||||
	token := tke.Encode(text, nil, nil)
 | 
			
		||||
 | 
			
		||||
	//tokens
 | 
			
		||||
	fmt.Println(token)
 | 
			
		||||
	// num_tokens
 | 
			
		||||
	fmt.Println(len(token))
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testAesEncrypt() {
 | 
			
		||||
	// 加密
 | 
			
		||||
	text := []byte("this is a secret text")
 | 
			
		||||
	key := utils.RandString(24)
 | 
			
		||||
	encrypt, err := utils.AesEncrypt(key, text)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Println("加密密文:", encrypt)
 | 
			
		||||
	// 解密
 | 
			
		||||
	decrypt, err := utils.AesDecrypt(key, encrypt)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Println("解密明文:", string(decrypt))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func extractFunction() error {
 | 
			
		||||
	open, err := os.Open("res/data.txt")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	reader := bufio.NewReader(open)
 | 
			
		||||
	var contents = make([]string, 0)
 | 
			
		||||
	var functionCall = false
 | 
			
		||||
	var functionName string
 | 
			
		||||
	for {
 | 
			
		||||
		line, err := reader.ReadString('\n')
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if !strings.Contains(line, "data:") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var responseBody = types.ApiResponse{}
 | 
			
		||||
		err = json.Unmarshal([]byte(line[6:]), &responseBody)
 | 
			
		||||
		if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function := responseBody.Choices[0].Delta.FunctionCall
 | 
			
		||||
		if functionCall && function.Name == "" {
 | 
			
		||||
			contents = append(contents, function.Arguments)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !utils.IsEmptyValue(function) {
 | 
			
		||||
			functionCall = true
 | 
			
		||||
			functionName = function.Name
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Println("函数名称: ", functionName)
 | 
			
		||||
	fmt.Println(strings.Join(contents, ""))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func minio() {
 | 
			
		||||
	config := core.NewDefaultConfig()
 | 
			
		||||
	config.ProxyURL = "http://localhost:7777"
 | 
			
		||||
	config.OSS.Minio = types.MinioConfig{
 | 
			
		||||
		Endpoint:     "localhost:9010",
 | 
			
		||||
		AccessKey:    "ObWIEyXaQUHOYU26L0oI",
 | 
			
		||||
		AccessSecret: "AJW3HHhlGrprfPcmiC7jSOSzVCyrlhX4AnOAUzqI",
 | 
			
		||||
		Bucket:       "chatgpt-plus",
 | 
			
		||||
		UseSSL:       false,
 | 
			
		||||
		Domain:       "http://localhost:9010",
 | 
			
		||||
	}
 | 
			
		||||
	minioService, err := oss.NewMinioService(config)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url, err := minioService.PutImg("https://cdn.discordapp.com/attachments/1139552247693443184/1141619433752768572/lisamiller4099_A_beautiful_fairy_sister_from_Chinese_mythology__3162726e-5ee4-4f60-932b-6b78b375eaef.png")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Println(url)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								api/utils/common.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								api/utils/common.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CopyObject 拷贝对象
 | 
			
		||||
func CopyObject(src interface{}, dst interface{}) error {
 | 
			
		||||
 | 
			
		||||
	srcType := reflect.TypeOf(src)
 | 
			
		||||
	srcValue := reflect.ValueOf(src)
 | 
			
		||||
	dstValue := reflect.ValueOf(dst).Elem()
 | 
			
		||||
	reflect.TypeOf(dst)
 | 
			
		||||
	for i := 0; i < srcType.NumField(); i++ {
 | 
			
		||||
		field := srcType.Field(i)
 | 
			
		||||
		value := dstValue.FieldByName(field.Name)
 | 
			
		||||
		if !value.IsValid() {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		// 数据类型相同,直接赋值
 | 
			
		||||
		v := srcValue.FieldByName(field.Name)
 | 
			
		||||
		if value.Type() == field.Type {
 | 
			
		||||
			value.Set(v)
 | 
			
		||||
		} else {
 | 
			
		||||
			// src data type is  string,dst data type is slice, map, struct
 | 
			
		||||
			// use json decode the data
 | 
			
		||||
			if field.Type.Kind() == reflect.String && (value.Type().Kind() == reflect.Struct ||
 | 
			
		||||
				value.Type().Kind() == reflect.Map ||
 | 
			
		||||
				value.Type().Kind() == reflect.Slice) {
 | 
			
		||||
				pType := reflect.New(value.Type())
 | 
			
		||||
				v2 := pType.Interface()
 | 
			
		||||
				err := json.Unmarshal([]byte(v.String()), &v2)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					value.Set(reflect.ValueOf(v2).Elem())
 | 
			
		||||
				}
 | 
			
		||||
				// map, struct, slice to string
 | 
			
		||||
			} else if (field.Type.Kind() == reflect.Struct ||
 | 
			
		||||
				field.Type.Kind() == reflect.Map ||
 | 
			
		||||
				field.Type.Kind() == reflect.Slice) && value.Type().Kind() == reflect.String {
 | 
			
		||||
				ba, err := json.Marshal(v.Interface())
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					val := string(ba)
 | 
			
		||||
					if strings.Contains(val, "{") {
 | 
			
		||||
						value.Set(reflect.ValueOf(string(ba)))
 | 
			
		||||
					} else {
 | 
			
		||||
						value.Set(reflect.ValueOf(""))
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} else { // 简单数据类型的强制类型转换
 | 
			
		||||
				switch value.Kind() {
 | 
			
		||||
				case reflect.Int:
 | 
			
		||||
				case reflect.Int8:
 | 
			
		||||
				case reflect.Int16:
 | 
			
		||||
				case reflect.Int32:
 | 
			
		||||
				case reflect.Int64:
 | 
			
		||||
					value.SetInt(v.Int())
 | 
			
		||||
					break
 | 
			
		||||
				case reflect.Float32:
 | 
			
		||||
				case reflect.Float64:
 | 
			
		||||
					value.SetFloat(v.Float())
 | 
			
		||||
					break
 | 
			
		||||
				case reflect.Bool:
 | 
			
		||||
					value.SetBool(v.Bool())
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Ip2Region(searcher *xdb.Searcher, ip string) string {
 | 
			
		||||
	str, err := searcher.SearchByStr(ip)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	arr := strings.Split(str, "|")
 | 
			
		||||
	if len(arr) < 3 {
 | 
			
		||||
		return arr[0]
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s-%s-%s", arr[0], arr[2], arr[3])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsEmptyValue(obj interface{}) bool {
 | 
			
		||||
	if obj == nil {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	v := reflect.ValueOf(obj)
 | 
			
		||||
	switch v.Kind() {
 | 
			
		||||
	case reflect.Ptr, reflect.Interface:
 | 
			
		||||
		return v.IsNil()
 | 
			
		||||
	case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
 | 
			
		||||
		return v.Len() == 0
 | 
			
		||||
	case reflect.Bool:
 | 
			
		||||
		return !v.Bool()
 | 
			
		||||
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 | 
			
		||||
		return v.Int() == 0
 | 
			
		||||
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
 | 
			
		||||
		return v.Uint() == 0
 | 
			
		||||
	case reflect.Float32, reflect.Float64:
 | 
			
		||||
		return v.Float() == 0
 | 
			
		||||
	case reflect.Complex64, reflect.Complex128:
 | 
			
		||||
		return v.Complex() == 0
 | 
			
		||||
	default:
 | 
			
		||||
		return reflect.DeepEqual(obj, reflect.Zero(reflect.TypeOf(obj)).Interface())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BoolValue(str string) bool {
 | 
			
		||||
	value, err := strconv.ParseBool(str)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FloatValue(str string) float64 {
 | 
			
		||||
	value, err := strconv.ParseFloat(str, 64)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IntValue(str string, defaultValue int) int {
 | 
			
		||||
	value, err := strconv.Atoi(str)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defaultValue
 | 
			
		||||
	}
 | 
			
		||||
	return value
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								api/utils/crypto.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								api/utils/crypto.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"crypto/aes"
 | 
			
		||||
	"crypto/cipher"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AesEncrypt 加密
 | 
			
		||||
func AesEncrypt(keyStr string, data []byte) (string, error) {
 | 
			
		||||
	//创建加密实例
 | 
			
		||||
	key := []byte(keyStr)
 | 
			
		||||
	block, err := aes.NewCipher(key)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	blockSize := block.BlockSize()
 | 
			
		||||
	encryptBytes := pkcs7Padding(data, blockSize)
 | 
			
		||||
	result := make([]byte, len(encryptBytes))
 | 
			
		||||
	//使用cbc加密模式
 | 
			
		||||
	blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
 | 
			
		||||
	//执行加密
 | 
			
		||||
	blockMode.CryptBlocks(result, encryptBytes)
 | 
			
		||||
	return base64.StdEncoding.EncodeToString(result), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AesDecrypt 解密
 | 
			
		||||
func AesDecrypt(keyStr string, dataStr string) ([]byte, error) {
 | 
			
		||||
	//创建实例
 | 
			
		||||
	key := []byte(keyStr)
 | 
			
		||||
	block, err := aes.NewCipher(key)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data, err := base64.StdEncoding.DecodeString(dataStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	blockSize := block.BlockSize()
 | 
			
		||||
	blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
 | 
			
		||||
	result := make([]byte, len(data))
 | 
			
		||||
	//执行解密
 | 
			
		||||
	blockMode.CryptBlocks(result, data)
 | 
			
		||||
	//去除填充
 | 
			
		||||
	result, err = pkcs7UnPadding(result)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func pkcs7Padding(data []byte, blockSize int) []byte {
 | 
			
		||||
	padding := blockSize - len(data)%blockSize
 | 
			
		||||
	padText := bytes.Repeat([]byte{byte(padding)}, padding)
 | 
			
		||||
	return append(data, padText...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func pkcs7UnPadding(data []byte) ([]byte, error) {
 | 
			
		||||
	length := len(data)
 | 
			
		||||
	if length == 0 {
 | 
			
		||||
		return nil, errors.New("empty encrypt data")
 | 
			
		||||
	}
 | 
			
		||||
	unPadding := int(data[length-1])
 | 
			
		||||
	return data[:(length - unPadding)], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Sha256(data string) string {
 | 
			
		||||
	hash := sha256.New()
 | 
			
		||||
	_, err := io.WriteString(hash, data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hashValue := hash.Sum(nil)
 | 
			
		||||
	return fmt.Sprintf("%x", hashValue)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								api/utils/net.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/utils/net.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
// ReplyChunkMessage 回复客户片段端消息
 | 
			
		||||
func ReplyChunkMessage(client *types.WsClient, message types.WsMessage) {
 | 
			
		||||
	msg, err := json.Marshal(message)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorf("Error for decoding json data: %v", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err = client.Send(msg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorf("Error for reply message: %v", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReplyMessage 回复客户端一条完整的消息
 | 
			
		||||
func ReplyMessage(ws *types.WsClient, message interface{}) {
 | 
			
		||||
	ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
 | 
			
		||||
	ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message})
 | 
			
		||||
	ReplyChunkMessage(ws, types.WsMessage{Type: types.WsEnd})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DownloadImage(imageURL string, proxy string) ([]byte, error) {
 | 
			
		||||
	var client *http.Client
 | 
			
		||||
	if proxy == "" {
 | 
			
		||||
		client = http.DefaultClient
 | 
			
		||||
	} else {
 | 
			
		||||
		proxyURL, _ := url.Parse(proxy)
 | 
			
		||||
		client = &http.Client{
 | 
			
		||||
			Transport: &http.Transport{
 | 
			
		||||
				Proxy: http.ProxyURL(proxyURL),
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	req, err := http.NewRequest("GET", imageURL, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	imageBytes, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return imageBytes, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								api/utils/openai.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/utils/openai.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/pkoukk/tiktoken-go"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func CalcTokens(text string, model string) (int, error) {
 | 
			
		||||
	encoding, ok := tiktoken.MODEL_TO_ENCODING[model]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		encoding = "cl100k_base"
 | 
			
		||||
	}
 | 
			
		||||
	tke, err := tiktoken.GetEncoding(encoding)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, fmt.Errorf("getEncoding: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	token := tke.Encode(text, nil, nil)
 | 
			
		||||
	return len(token), nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								api/utils/resp/response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								api/utils/resp/response.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
package resp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func SUCCESS(c *gin.Context, values ...interface{}) {
 | 
			
		||||
	if values != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: values[0]})
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ERROR(c *gin.Context, messages ...string) {
 | 
			
		||||
	if messages != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: messages[0]})
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(http.StatusOK, types.BizVo{Code: types.Failed})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func HACKER(c *gin.Context) {
 | 
			
		||||
	c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Hacker attempt!!!"})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NotAuth(c *gin.Context, messages ...string) {
 | 
			
		||||
	if messages != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: messages[0]})
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: "Not Authorized"})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,12 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/sha3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// RandString generate rand string with specified length
 | 
			
		||||
@@ -18,12 +21,20 @@ func RandString(length int) string {
 | 
			
		||||
	return string(result)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Long2IP(ipInt int64) string {
 | 
			
		||||
	b0 := strconv.FormatInt((ipInt>>24)&0xff, 10)
 | 
			
		||||
	b1 := strconv.FormatInt((ipInt>>16)&0xff, 10)
 | 
			
		||||
	b2 := strconv.FormatInt((ipInt>>8)&0xff, 10)
 | 
			
		||||
	b3 := strconv.FormatInt(ipInt&0xff, 10)
 | 
			
		||||
	return b0 + "." + b1 + "." + b2 + "." + b3
 | 
			
		||||
func RandomNumber(bit int) int {
 | 
			
		||||
	min := intPow(10, bit-1)
 | 
			
		||||
	max := intPow(10, bit) - 1
 | 
			
		||||
 | 
			
		||||
	rand.Seed(time.Now().UnixNano())
 | 
			
		||||
	return rand.Intn(max-min+1) + min
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func intPow(x, y int) int {
 | 
			
		||||
	result := 1
 | 
			
		||||
	for i := 0; i < y; i++ {
 | 
			
		||||
		result *= x
 | 
			
		||||
	}
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ContainsStr(slice []string, item string) bool {
 | 
			
		||||
@@ -45,6 +56,10 @@ func Stamp2str(timestamp int64) string {
 | 
			
		||||
 | 
			
		||||
// Str2stamp 字符串转时间戳
 | 
			
		||||
func Str2stamp(str string) int64 {
 | 
			
		||||
	if len(str) == 0 {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	layout := "2006-01-02 15:04:05"
 | 
			
		||||
	t, err := time.Parse(layout, str)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -52,3 +67,28 @@ func Str2stamp(str string) int64 {
 | 
			
		||||
	}
 | 
			
		||||
	return t.Unix()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GenPassword(pass string, salt string) string {
 | 
			
		||||
	data := []byte(pass + salt)
 | 
			
		||||
	hash := sha3.Sum256(data)
 | 
			
		||||
	return fmt.Sprintf("%x", hash)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func JsonEncode(value interface{}) string {
 | 
			
		||||
	bytes, err := json.Marshal(value)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	return string(bytes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func JsonDecode(src string, dest interface{}) error {
 | 
			
		||||
	return json.Unmarshal([]byte(src), dest)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func InterfaceToString(value interface{}) string {
 | 
			
		||||
	if str, ok := value.(string); ok {
 | 
			
		||||
		return str
 | 
			
		||||
	}
 | 
			
		||||
	return JsonEncode(value)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										68
									
								
								api/utils/upload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								api/utils/upload.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GenUploadPath 生成上传文件路径
 | 
			
		||||
func GenUploadPath(basePath, filename string) (string, error) {
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	dir := fmt.Sprintf("%s/%d/%d", basePath, now.Year(), now.Month())
 | 
			
		||||
	_, err := os.Stat(dir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		err = os.MkdirAll(dir, 0755)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", fmt.Errorf("error with create upload dir:%v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	fileExt := filepath.Ext(filename)
 | 
			
		||||
	return fmt.Sprintf("%s/%d%s", dir, now.UnixMicro(), fileExt), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GenUploadUrl 生成上传文件 URL
 | 
			
		||||
func GenUploadUrl(basePath, baseUrl string, filePath string) string {
 | 
			
		||||
	return strings.Replace(filePath, basePath, baseUrl, 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DownloadFile(fileURL string, filepath string, proxy string) error {
 | 
			
		||||
	var client *http.Client
 | 
			
		||||
	if proxy == "" {
 | 
			
		||||
		client = http.DefaultClient
 | 
			
		||||
	} else {
 | 
			
		||||
		proxyURL, _ := url.Parse(proxy)
 | 
			
		||||
		client = &http.Client{
 | 
			
		||||
			Transport: &http.Transport{
 | 
			
		||||
				Proxy: http.ProxyURL(proxyURL),
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	req, err := http.NewRequest("GET", fileURL, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	file, err := os.Create(filepath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(file, resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								api/utils/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								api/utils/user.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetLoginUser(c *gin.Context, db *gorm.DB) (model.User, error) {
 | 
			
		||||
	value, exists := c.Get(types.LoginUserCache)
 | 
			
		||||
	if exists {
 | 
			
		||||
		return value.(model.User), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userId, ok := c.Get(types.LoginUserID)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return model.User{}, errors.New("user not login")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var user model.User
 | 
			
		||||
	res := db.First(&user, userId)
 | 
			
		||||
	// 更新缓存
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		c.Set(types.LoginUserCache, user)
 | 
			
		||||
	}
 | 
			
		||||
	return user, res.Error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										360
									
								
								database/chatgpt_plus-v3.0.7.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								database/chatgpt_plus-v3.0.7.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,360 @@
 | 
			
		||||
-- phpMyAdmin SQL Dump
 | 
			
		||||
-- version 5.2.1
 | 
			
		||||
-- https://www.phpmyadmin.net/
 | 
			
		||||
--
 | 
			
		||||
-- 主机: localhost
 | 
			
		||||
-- 生成日期: 2023-08-15 14:51:13
 | 
			
		||||
-- 服务器版本: 8.0.27
 | 
			
		||||
-- PHP 版本: 8.1.18
 | 
			
		||||
 | 
			
		||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
 | 
			
		||||
START TRANSACTION;
 | 
			
		||||
SET time_zone = "+00:00";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
 | 
			
		||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
 | 
			
		||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
 | 
			
		||||
/*!40101 SET NAMES utf8mb4 */;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 数据库: `chatgpt_plus`
 | 
			
		||||
--
 | 
			
		||||
CREATE DATABASE IF NOT EXISTS `chatgpt_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
 | 
			
		||||
USE `chatgpt_plus`;
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_api_keys`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_api_keys`;
 | 
			
		||||
CREATE TABLE `chatgpt_api_keys` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `value` varchar(100) NOT NULL COMMENT 'API KEY value',
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
  `last_used_at` int NOT NULL COMMENT '最后使用时间',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API ';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_chat_history`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_chat_history`;
 | 
			
		||||
CREATE TABLE `chatgpt_chat_history` (
 | 
			
		||||
  `id` bigint NOT NULL,
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
  `chat_id` char(40) NOT NULL COMMENT '会话 ID',
 | 
			
		||||
  `type` varchar(10) NOT NULL COMMENT '类型:prompt|reply',
 | 
			
		||||
  `icon` varchar(100) NOT NULL COMMENT '角色图标',
 | 
			
		||||
  `role_id` int NOT NULL COMMENT '角色 ID',
 | 
			
		||||
  `content` text NOT NULL COMMENT '聊天内容',
 | 
			
		||||
  `tokens` smallint NOT NULL COMMENT '耗费 token 数量',
 | 
			
		||||
  `use_context` tinyint(1) DEFAULT NULL COMMENT '是否允许作为上下文语料',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_chat_items`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_chat_items`;
 | 
			
		||||
CREATE TABLE `chatgpt_chat_items` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `chat_id` char(40) NOT NULL COMMENT '会话 ID',
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
  `role_id` int NOT NULL COMMENT '角色 ID',
 | 
			
		||||
  `title` varchar(100) NOT NULL COMMENT '会话标题',
 | 
			
		||||
  `model` varchar(30) NOT NULL COMMENT '会话使用的 AI 模型',
 | 
			
		||||
  `created_at` datetime NOT NULL COMMENT '创建时间',
 | 
			
		||||
  `updated_at` datetime NOT NULL COMMENT '更新时间'
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_chat_roles`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_chat_roles`;
 | 
			
		||||
CREATE TABLE `chatgpt_chat_roles` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `name` varchar(30) NOT NULL COMMENT '角色名称',
 | 
			
		||||
  `marker` varchar(30) NOT NULL COMMENT '角色标识',
 | 
			
		||||
  `context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色语料 json',
 | 
			
		||||
  `hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息',
 | 
			
		||||
  `icon` varchar(255) NOT NULL COMMENT '角色图标',
 | 
			
		||||
  `enable` tinyint(1) NOT NULL COMMENT '是否被启用',
 | 
			
		||||
  `sort` smallint NOT NULL COMMENT '角色排序',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天角色表';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转存表中的数据 `chatgpt_chat_roles`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort`, `created_at`, `updated_at`) VALUES
 | 
			
		||||
(1, '通用AI助手', 'gpt', '', '您好,我是您的AI智能助手,我会尽力回答您的问题或提供有用的建议。', '/images/avatar/gpt.png', 1, 1, '2023-05-30 07:02:06', '2023-06-22 09:33:34'),
 | 
			
		||||
(24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 2, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 3, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 4, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '生命的意义在于成为你自己!', '/images/avatar/psychiatrist.jpg', 1, 5, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 6, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装:¥1188/箱,和系列 6 瓶装:¥2208/箱,贵系列 6 瓶装:¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 7, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(30, '英语陪练员', 'english_trainer', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。\"},{\"role\":\"assistant\",\"content\":\"Okay, let\'s start our conversation practice! What\'s your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)\"}]', 'Okay, let\'s start our conversation practice! What\'s your name?', '/images/avatar/english_trainer.jpg', 1, 8, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 9, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 10, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 11, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 12, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 13, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 14, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 15, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 16, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
 | 
			
		||||
(39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 17, '2023-05-30 14:10:24', '2023-06-22 09:31:20');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_configs`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_configs`;
 | 
			
		||||
CREATE TABLE `chatgpt_configs` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `marker` varchar(20) NOT NULL COMMENT '标识',
 | 
			
		||||
  `config_json` text NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转存表中的数据 `chatgpt_configs`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
 | 
			
		||||
(1, 'system', '{\"admin_title\":\"ChatGPT-控制台\",\"enabled_msg_service\":true,\"enabled_register\":true,\"init_calls\":1000,\"models\":[\"gpt-3.5-turbo\",\"gpt-3.5-turbo-16k\",\"gpt-4\",\"gpt-4-0613\",\"gpt-4-32k\"],\"title\":\"ChatGPT-智能助手V2\",\"user_init_calls\":10}'),
 | 
			
		||||
(2, 'chat', '{\"api_key\":\"\",\"api_url\":\"https://api.openai.com/v1/chat/completions\",\"context_deep\":0,\"enable_context\":true,\"enable_history\":true,\"history_level\":0,\"max_tokens\":2048,\"model\":\"gpt-3.5-turbo\",\"temperature\":1}');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_mj_jobs`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_mj_jobs`;
 | 
			
		||||
CREATE TABLE `chatgpt_mj_jobs` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
  `chat_id` char(40) NOT NULL COMMENT '聊天会话 ID',
 | 
			
		||||
  `message_id` char(40) NOT NULL COMMENT '消息 ID',
 | 
			
		||||
  `hash` char(40) NOT NULL COMMENT '图片哈希',
 | 
			
		||||
  `content` varchar(2000) NOT NULL COMMENT '消息内容',
 | 
			
		||||
  `prompt` varchar(2000) NOT NULL COMMENT '会话提示词',
 | 
			
		||||
  `image` text NOT NULL COMMENT '图片信息 json',
 | 
			
		||||
  `created_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_rewards`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_rewards`;
 | 
			
		||||
CREATE TABLE `chatgpt_rewards` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
  `tx_id` char(36) NOT NULL COMMENT '交易 ID',
 | 
			
		||||
  `amount` decimal(10,2) NOT NULL COMMENT '打赏金额',
 | 
			
		||||
  `remark` varchar(80) NOT NULL COMMENT '备注',
 | 
			
		||||
  `status` tinyint(1) NOT NULL COMMENT '核销状态,0:未核销,1:已核销',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户打赏';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_users`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_users`;
 | 
			
		||||
CREATE TABLE `chatgpt_users` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `username` varchar(30) NOT NULL COMMENT '用户名',
 | 
			
		||||
  `mobile` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '手机号码',
 | 
			
		||||
  `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
 | 
			
		||||
  `nickname` varchar(30) NOT NULL COMMENT '昵称',
 | 
			
		||||
  `avatar` varchar(100) NOT NULL COMMENT '头像',
 | 
			
		||||
  `salt` char(12) NOT NULL COMMENT '密码盐',
 | 
			
		||||
  `tokens` bigint NOT NULL DEFAULT '0' COMMENT '剩余 tokens',
 | 
			
		||||
  `calls` int NOT NULL DEFAULT '0' COMMENT '剩余调用次数',
 | 
			
		||||
  `expired_time` int NOT NULL COMMENT '用户过期时间',
 | 
			
		||||
  `status` tinyint(1) NOT NULL COMMENT '当前状态',
 | 
			
		||||
  `chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天配置json',
 | 
			
		||||
  `chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天角色 json',
 | 
			
		||||
  `last_login_at` int NOT NULL COMMENT '最后登录时间',
 | 
			
		||||
  `last_login_ip` char(16) NOT NULL COMMENT '最后登录 IP',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转存表中的数据 `chatgpt_users`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_users` (`id`, `username`, `mobile`, `password`, `nickname`, `avatar`, `salt`, `tokens`, `calls`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES
 | 
			
		||||
(4, 'geekmaster', '18575670125', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', '极客学长@104203', 'images/avatar/user.png', 'ueedue5l', 17632, 196, 0, 1, '{\"model\":\"gpt-3.5-turbo\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"\"}', '[\"gpt\",\"seller\",\"artist\",\"dou_yin\",\"translator\",\"kong_zi\",\"programmer\",\"psychiatrist\",\"red_book\",\"steve_jobs\",\"teacher\",\"elon_musk\",\"girl_friend\",\"lu_xun\",\"weekly_report\",\"english_trainer\",\"good_comment\"]', 1691927597, '::1', '2023-06-12 16:47:17', '2023-08-13 19:53:17');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_user_login_logs`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_user_login_logs`;
 | 
			
		||||
CREATE TABLE `chatgpt_user_login_logs` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户ID',
 | 
			
		||||
  `username` varchar(30) NOT NULL COMMENT '用户名',
 | 
			
		||||
  `login_ip` char(16) NOT NULL COMMENT '登录IP',
 | 
			
		||||
  `login_address` varchar(30) NOT NULL COMMENT '登录地址',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转储表的索引
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_api_keys`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_api_keys`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `value` (`value`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_chat_history`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_history`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD KEY `chat_id` (`chat_id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_chat_items`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_items`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `chat_id` (`chat_id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_chat_roles`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_roles`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `marker` (`marker`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_configs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_configs`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `marker` (`marker`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_mj_jobs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_mj_jobs`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `message_id` (`message_id`),
 | 
			
		||||
  ADD UNIQUE KEY `hash` (`hash`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_rewards`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_rewards`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `tx_id` (`tx_id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_users`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_users`
 | 
			
		||||
  ADD PRIMARY KEY (`id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_user_login_logs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_user_login_logs`
 | 
			
		||||
  ADD PRIMARY KEY (`id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 在导出的表使用AUTO_INCREMENT
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_api_keys`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_api_keys`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_history`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_history`
 | 
			
		||||
  MODIFY `id` bigint NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_items`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_items`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_roles`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_roles`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=125;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_configs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_configs`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_mj_jobs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_mj_jobs`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_rewards`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_rewards`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_users`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_users`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=17;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_user_login_logs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_user_login_logs`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
COMMIT;
 | 
			
		||||
 | 
			
		||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
 | 
			
		||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
 | 
			
		||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
 | 
			
		||||
							
								
								
									
										403
									
								
								database/chatgpt_plus-v3.1.0.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										403
									
								
								database/chatgpt_plus-v3.1.0.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,403 @@
 | 
			
		||||
-- phpMyAdmin SQL Dump
 | 
			
		||||
-- version 5.1.3
 | 
			
		||||
-- https://www.phpmyadmin.net/
 | 
			
		||||
--
 | 
			
		||||
-- 主机: localhost
 | 
			
		||||
-- 生成日期: 2023-09-05 13:56:27
 | 
			
		||||
-- 服务器版本: 8.0.33-0ubuntu0.22.04.2
 | 
			
		||||
-- PHP 版本: 8.1.18
 | 
			
		||||
 | 
			
		||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
 | 
			
		||||
START TRANSACTION;
 | 
			
		||||
SET time_zone = "+00:00";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
 | 
			
		||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
 | 
			
		||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
 | 
			
		||||
/*!40101 SET NAMES utf8mb4 */;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 数据库: `chatgpt_plus`
 | 
			
		||||
--
 | 
			
		||||
CREATE DATABASE IF NOT EXISTS `chatgpt_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
 | 
			
		||||
USE `chatgpt_plus`;
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_api_keys`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_api_keys`;
 | 
			
		||||
CREATE TABLE `chatgpt_api_keys` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `platform` char(20) DEFAULT NULL COMMENT '平台',
 | 
			
		||||
  `value` varchar(100) NOT NULL COMMENT 'API KEY value',
 | 
			
		||||
  `last_used_at` int NOT NULL COMMENT '最后使用时间',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API ';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_chat_history`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_chat_history`;
 | 
			
		||||
CREATE TABLE `chatgpt_chat_history` (
 | 
			
		||||
  `id` bigint NOT NULL,
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
  `chat_id` char(40) NOT NULL COMMENT '会话 ID',
 | 
			
		||||
  `type` varchar(10) NOT NULL COMMENT '类型:prompt|reply',
 | 
			
		||||
  `icon` varchar(100) NOT NULL COMMENT '角色图标',
 | 
			
		||||
  `role_id` int NOT NULL COMMENT '角色 ID',
 | 
			
		||||
  `content` text NOT NULL COMMENT '聊天内容',
 | 
			
		||||
  `tokens` smallint NOT NULL COMMENT '耗费 token 数量',
 | 
			
		||||
  `use_context` tinyint(1) NOT NULL COMMENT '是否允许作为上下文语料',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL,
 | 
			
		||||
  `deleted_at` datetime DEFAULT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_chat_items`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_chat_items`;
 | 
			
		||||
CREATE TABLE `chatgpt_chat_items` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `chat_id` char(40) NOT NULL COMMENT '会话 ID',
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
  `role_id` int NOT NULL COMMENT '角色 ID',
 | 
			
		||||
  `title` varchar(100) NOT NULL COMMENT '会话标题',
 | 
			
		||||
  `model_id` int NOT NULL DEFAULT '0' COMMENT '模型 ID',
 | 
			
		||||
  `created_at` datetime NOT NULL COMMENT '创建时间',
 | 
			
		||||
  `updated_at` datetime NOT NULL COMMENT '更新时间',
 | 
			
		||||
  `deleted_at` datetime DEFAULT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_chat_models`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_chat_models`;
 | 
			
		||||
CREATE TABLE `chatgpt_chat_models` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `platform` varchar(20) DEFAULT NULL COMMENT '模型平台',
 | 
			
		||||
  `name` varchar(50) NOT NULL COMMENT '模型名称',
 | 
			
		||||
  `value` varchar(50) NOT NULL COMMENT '模型值',
 | 
			
		||||
  `sort_num` tinyint(1) NOT NULL COMMENT '排序数字',
 | 
			
		||||
  `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用模型',
 | 
			
		||||
  `created_at` datetime DEFAULT NULL,
 | 
			
		||||
  `updated_at` datetime DEFAULT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 模型表';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转存表中的数据 `chatgpt_chat_models`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_chat_models` (`id`, `platform`, `name`, `value`, `sort_num`, `enabled`, `created_at`, `updated_at`) VALUES
 | 
			
		||||
(1, 'OpenAI', 'GPT-3.5', 'gpt-3.5-turbo-16k', 0, 1, '2023-08-23 12:06:36', '2023-09-05 09:53:12'),
 | 
			
		||||
(2, 'Azure', 'Azure-3.5', 'gpt-3.5-turbo', 0, 1, '2023-08-23 12:15:30', '2023-09-05 09:52:53'),
 | 
			
		||||
(3, 'ChatGLM', 'ChatGML-Pro', 'chatglm_pro', 0, 1, '2023-08-23 13:35:45', '2023-09-04 17:28:31'),
 | 
			
		||||
(5, 'ChatGLM', 'ChatGLM-Std', 'chatglm_std', 0, 1, '2023-08-24 15:05:38', '2023-09-04 17:28:27'),
 | 
			
		||||
(6, 'ChatGLM', 'ChatGLM-Lite', 'chatglm_lite', 0, 1, '2023-08-24 15:06:15', '2023-09-04 17:28:35');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_chat_roles`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_chat_roles`;
 | 
			
		||||
CREATE TABLE `chatgpt_chat_roles` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `name` varchar(30) NOT NULL COMMENT '角色名称',
 | 
			
		||||
  `marker` varchar(30) NOT NULL COMMENT '角色标识',
 | 
			
		||||
  `context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色语料 json',
 | 
			
		||||
  `hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息',
 | 
			
		||||
  `icon` varchar(255) NOT NULL COMMENT '角色图标',
 | 
			
		||||
  `enable` tinyint(1) NOT NULL COMMENT '是否被启用',
 | 
			
		||||
  `sort_num` smallint NOT NULL DEFAULT '0' COMMENT '角色排序',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天角色表';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转存表中的数据 `chatgpt_chat_roles`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort_num`, `created_at`, `updated_at`) VALUES
 | 
			
		||||
(1, '通用AI助手', 'gpt', '', '您好,我是您的AI智能助手,我会尽力回答您的问题或提供有用的建议。', '/images/avatar/gpt.png', 1, 0, '2023-05-30 07:02:06', '2023-09-04 15:45:56'),
 | 
			
		||||
(24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 3, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 2, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 4, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '生命的意义在于成为你自己!', '/images/avatar/psychiatrist.jpg', 1, 1, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 5, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装:¥1188/箱,和系列 6 瓶装:¥2208/箱,贵系列 6 瓶装:¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 9, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(30, '英语陪练员', 'english_trainer', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。\"},{\"role\":\"assistant\",\"content\":\"Okay, let\'s start our conversation practice! What\'s your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)\"}]', 'Okay, let\'s start our conversation practice! What\'s your name?', '/images/avatar/english_trainer.jpg', 1, 6, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 7, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 8, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 10, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 11, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 12, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 13, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 14, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 15, '2023-05-30 14:10:24', '2023-09-04 15:45:56'),
 | 
			
		||||
(39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 16, '2023-05-30 14:10:24', '2023-09-04 15:45:56');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_configs`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_configs`;
 | 
			
		||||
CREATE TABLE `chatgpt_configs` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `marker` varchar(20) NOT NULL COMMENT '标识',
 | 
			
		||||
  `config_json` text NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转存表中的数据 `chatgpt_configs`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
 | 
			
		||||
(1, 'system', '{\"admin_title\":\"ChatPlus 控制台\",\"enabled_draw\":true,\"enabled_msg_service\":true,\"enabled_register\":true,\"init_calls\":1000,\"init_img_calls\":0,\"models\":[\"gpt-3.5-turbo-16k\",\"gpt-3.5-turbo\",\"gpt-4\",\"gpt-4-32k\"],\"title\":\"ChatGPT-智能助手V3\",\"user_init_calls\":10}'),
 | 
			
		||||
(2, 'chat', '{\"azure\":{\"api_url\":\"https://chat-bot-api.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-05-15\",\"max_tokens\":1024,\"temperature\":1},\"chat_gml\":{\"api_url\":\"https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke\",\"max_tokens\":1024,\"temperature\":1},\"context_deep\":4,\"enable_context\":true,\"enable_history\":true,\"open_ai\":{\"api_url\":\"https://api.openai.com/v1/chat/completions\",\"max_tokens\":1024,\"temperature\":1}}');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_mj_jobs`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_mj_jobs`;
 | 
			
		||||
CREATE TABLE `chatgpt_mj_jobs` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
  `chat_id` char(40) NOT NULL COMMENT '聊天会话 ID',
 | 
			
		||||
  `message_id` char(40) NOT NULL COMMENT '消息 ID',
 | 
			
		||||
  `reference_id` char(40) DEFAULT NULL COMMENT '引用消息 ID',
 | 
			
		||||
  `hash` char(40) NOT NULL COMMENT '图片哈希',
 | 
			
		||||
  `content` varchar(2000) NOT NULL COMMENT '消息内容',
 | 
			
		||||
  `prompt` varchar(2000) NOT NULL COMMENT '会话提示词',
 | 
			
		||||
  `image` text NOT NULL COMMENT '图片信息 json',
 | 
			
		||||
  `created_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_rewards`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_rewards`;
 | 
			
		||||
CREATE TABLE `chatgpt_rewards` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
  `tx_id` char(36) NOT NULL COMMENT '交易 ID',
 | 
			
		||||
  `amount` decimal(10,2) NOT NULL COMMENT '打赏金额',
 | 
			
		||||
  `remark` varchar(80) NOT NULL COMMENT '备注',
 | 
			
		||||
  `status` tinyint(1) NOT NULL COMMENT '核销状态,0:未核销,1:已核销',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户打赏';
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_users`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_users`;
 | 
			
		||||
CREATE TABLE `chatgpt_users` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `mobile` char(11) NOT NULL COMMENT '手机号码',
 | 
			
		||||
  `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
 | 
			
		||||
  `avatar` varchar(100) NOT NULL COMMENT '头像',
 | 
			
		||||
  `salt` char(12) NOT NULL COMMENT '密码盐',
 | 
			
		||||
  `total_tokens` bigint NOT NULL DEFAULT '0' COMMENT '累计消耗 tokens',
 | 
			
		||||
  `calls` int NOT NULL DEFAULT '0' COMMENT '剩余调用次数',
 | 
			
		||||
  `img_calls` int NOT NULL DEFAULT '0' COMMENT '剩余绘图次数',
 | 
			
		||||
  `expired_time` int NOT NULL COMMENT '用户过期时间',
 | 
			
		||||
  `status` tinyint(1) NOT NULL COMMENT '当前状态',
 | 
			
		||||
  `chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天配置json',
 | 
			
		||||
  `chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天角色 json',
 | 
			
		||||
  `last_login_at` int NOT NULL COMMENT '最后登录时间',
 | 
			
		||||
  `last_login_ip` char(16) NOT NULL COMMENT '最后登录 IP',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转存表中的数据 `chatgpt_users`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_users` (`id`, `mobile`, `password`, `avatar`, `salt`, `total_tokens`, `calls`, `img_calls`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES
 | 
			
		||||
(4, '18575670125', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 'ueedue5l', 3479, 806, 99, 1727829036, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"elon_musk\",\"girl_friend\",\"lu_xun\",\"red_book\",\"psychiatrist\",\"teacher\",\"translator\",\"weekly_report\",\"artist\",\"dou_yin\",\"good_comment\",\"english_trainer\",\"gpt\",\"kong_zi\",\"programmer\",\"seller\",\"steve_jobs\"]', 1693882752, '::1', '2023-06-12 16:47:17', '2023-09-05 10:59:13');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的结构 `chatgpt_user_login_logs`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
DROP TABLE IF EXISTS `chatgpt_user_login_logs`;
 | 
			
		||||
CREATE TABLE `chatgpt_user_login_logs` (
 | 
			
		||||
  `id` int NOT NULL,
 | 
			
		||||
  `user_id` int NOT NULL COMMENT '用户ID',
 | 
			
		||||
  `username` varchar(30) NOT NULL COMMENT '用户名',
 | 
			
		||||
  `login_ip` char(16) NOT NULL COMMENT '登录IP',
 | 
			
		||||
  `login_address` varchar(30) NOT NULL COMMENT '登录地址',
 | 
			
		||||
  `created_at` datetime NOT NULL,
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转储表的索引
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_api_keys`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_api_keys`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `value` (`value`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_chat_history`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_history`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD KEY `chat_id` (`chat_id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_chat_items`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_items`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `chat_id` (`chat_id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_chat_models`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_models`
 | 
			
		||||
  ADD PRIMARY KEY (`id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_chat_roles`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_roles`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `marker` (`marker`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_configs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_configs`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `marker` (`marker`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_mj_jobs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_mj_jobs`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `message_id` (`message_id`),
 | 
			
		||||
  ADD UNIQUE KEY `hash` (`hash`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_rewards`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_rewards`
 | 
			
		||||
  ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `tx_id` (`tx_id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_users`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_users`
 | 
			
		||||
  ADD PRIMARY KEY (`id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_user_login_logs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_user_login_logs`
 | 
			
		||||
  ADD PRIMARY KEY (`id`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 在导出的表使用AUTO_INCREMENT
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_api_keys`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_api_keys`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_history`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_history`
 | 
			
		||||
  MODIFY `id` bigint NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_items`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_items`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_models`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_models`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_roles`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_roles`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=127;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_configs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_configs`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_mj_jobs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_mj_jobs`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_rewards`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_rewards`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_users`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_users`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=87;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_user_login_logs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_user_login_logs`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
COMMIT;
 | 
			
		||||
 | 
			
		||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
 | 
			
		||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
 | 
			
		||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
 | 
			
		||||
							
								
								
									
										22
									
								
								database/update-v3.0.6.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								database/update-v3.0.6.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
ALTER TABLE `chatgpt_chat_history` ADD `use_context` TINYINT(1) NULL DEFAULT NULL COMMENT '是否允许作为上下文语料' AFTER `tokens`;
 | 
			
		||||
ALTER TABLE `chatgpt_users` ADD `mobile` CHAR(11) NULL DEFAULT NULL COMMENT '手机号码' AFTER `username`;
 | 
			
		||||
 | 
			
		||||
CREATE TABLE `chatgpt_rewards` (
 | 
			
		||||
                                   `id` int NOT NULL,
 | 
			
		||||
                                   `tx_id` char(36) NOT NULL COMMENT '交易 ID',
 | 
			
		||||
                                   `amount` decimal(10,2) NOT NULL COMMENT '打赏金额',
 | 
			
		||||
                                   `remark` varchar(80) NOT NULL COMMENT '备注',
 | 
			
		||||
                                   `status` tinyint(1) NOT NULL COMMENT '核销状态,0:未核销,1:已核销',
 | 
			
		||||
                                   `created_at` datetime NOT NULL,
 | 
			
		||||
                                   `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户打赏';
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `chatgpt_rewards`
 | 
			
		||||
    ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `tx_id` (`tx_id`);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `chatgpt_rewards`
 | 
			
		||||
    MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `chatgpt_rewards` ADD `user_id` INT(11) NOT NULL COMMENT '用户 ID' AFTER `id`;
 | 
			
		||||
							
								
								
									
										37
									
								
								database/update-v3.0.7.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								database/update-v3.0.7.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
CREATE TABLE `chatgpt_mj_jobs` (
 | 
			
		||||
                                   `id` int NOT NULL,
 | 
			
		||||
                                   `user_id` int NOT NULL COMMENT '用户 ID',
 | 
			
		||||
                                   `chat_id` char(40) NOT NULL COMMENT '聊天会话 ID',
 | 
			
		||||
                                   `message_id` char(40) NOT NULL COMMENT '消息 ID',
 | 
			
		||||
                                   `hash` char(40) NOT NULL COMMENT '图片哈希',
 | 
			
		||||
                                   `content` varchar(2000) NOT NULL COMMENT '消息内容',
 | 
			
		||||
                                   `prompt` varchar(2000) NOT NULL COMMENT '会话提示词',
 | 
			
		||||
                                   `image` text NOT NULL COMMENT '图片信息 json',
 | 
			
		||||
                                   `created_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转储表的索引
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 表的索引 `chatgpt_mj_jobs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_mj_jobs`
 | 
			
		||||
    ADD PRIMARY KEY (`id`),
 | 
			
		||||
  ADD UNIQUE KEY `message_id` (`message_id`),
 | 
			
		||||
  ADD UNIQUE KEY `hash` (`hash`);
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 在导出的表使用AUTO_INCREMENT
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_mj_jobs`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_mj_jobs`
 | 
			
		||||
    MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `chatgpt_mj_jobs` ADD `reference_id` CHAR(40) NULL DEFAULT NULL COMMENT '引用消息 ID' AFTER `message_id`;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `chatgpt_users` ADD `img_calls` INT NOT NULL DEFAULT '0' COMMENT '剩余绘图次数' AFTER `calls`;
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user