/v1/chat/completions
+
+### Render部署
+
+**注意:部分部署区域可能无法连接glm,如容器日志出现请求超时或无法连接,请切换其他区域部署!**
+**注意:免费账户的容器实例将在一段时间不活动时自动停止运行,这会导致下次请求时遇到50秒或更长的延迟,建议查看[Render容器保活](https://github.com/LLM-Red-Team/free-api-hub/#Render%E5%AE%B9%E5%99%A8%E4%BF%9D%E6%B4%BB)**
+
+1. fork本项目到你的github账号下。
+
+2. 访问 [Render](https://dashboard.render.com/) 并登录你的github账号。
+
+3. 构建你的 Web Service(New+ -> Build and deploy from a Git repository -> Connect你fork的项目 -> 选择部署区域 -> 选择实例类型为Free -> Create Web Service)。
+
+4. 等待构建完成后,复制分配的域名并拼接URL访问即可。
+
+### Vercel部署
+
+**注意:Vercel免费账户的请求响应超时时间为10秒,但接口响应通常较久,可能会遇到Vercel返回的504超时错误!**
+
+请先确保安装了Node.js环境。
+
+```shell
+npm i -g vercel --registry http://registry.npmmirror.com
+vercel login
+git clone https://github.com/LLM-Red-Team/glm-free-api
+cd glm-free-api
+vercel --prod
```
-##### 示例
+## 原生部署
+
+请准备一台具有公网IP的服务器并将8000端口开放。
+
+请先安装好Node.js环境并且配置好环境变量,确认node命令可用。
+安装依赖
+
+```shell
+npm i
```
-curl http://127.0.0.1:9846
+
+安装PM2进行进程守护
+
+```shell
+npm i -g pm2
```
+编译构建,看到dist目录就是构建完成
+
+```shell
+npm run build
```
-curl --location --request POST 'http://127.0.0.1:9846/v1/chat/completions' \
---header 'Authorization: Bearer abc' \
---header 'Content-Type: application/json' \
---data-raw '{
- "model": "gpt-3.5-turbo",
+
+启动服务
+
+```shell
+pm2 start dist/index.js --name "glm-free-api"
+```
+
+查看服务实时日志
+
+```shell
+pm2 logs glm-free-api
+```
+
+重启服务
+
+```shell
+pm2 reload glm-free-api
+```
+
+停止服务
+
+```shell
+pm2 stop glm-free-api
+```
+
+## 推荐使用客户端
+
+使用以下二次开发客户端接入free-api系列项目更快更简单,支持文档/图像上传!
+
+由 [Clivia](https://github.com/Yanyutin753/lobe-chat) 二次开发的LobeChat [https://github.com/Yanyutin753/lobe-chat](https://github.com/Yanyutin753/lobe-chat)
+
+由 [时光@](https://github.com/SuYxh) 二次开发的ChatGPT Web [https://github.com/SuYxh/chatgpt-web-sea](https://github.com/SuYxh/chatgpt-web-sea)
+
+## 接口列表
+
+目前支持与openai兼容的 `/v1/chat/completions` 接口,可自行使用与openai或其他兼容的客户端接入接口,或者使用 [dify](https://dify.ai/) 等线上服务接入使用。
+
+### 对话补全
+
+对话补全接口,与openai的 [chat-completions-api](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) 兼容。
+
+**POST /v1/chat/completions**
+
+header 需要设置 Authorization 头部:
+
+```
+Authorization: Bearer [refresh_token]
+```
+
+请求数据:
+```json
+{
+ // 如果使用智能体请填写智能体ID到此处,否则可以乱填
+ "model": "glm4",
+ // 目前多轮对话基于消息合并实现,某些场景可能导致能力下降且受单轮最大token数限制
+ // 如果您想获得原生的多轮对话体验,可以传入首轮消息获得的id,来接续上下文
+ // "conversation_id": "65f6c28546bae1f0fbb532de",
"messages": [
{
"role": "user",
- "content": "西红柿炒钢丝球怎么做?"
+ "content": "你叫什么?"
}
],
+ // 如果使用SSE流请设置为true,默认false
"stream": false
-}'
+}
```
-## 配置
+响应数据:
+```json
+{
+ // 如果想获得原生多轮对话体验,此id,你可以传入到下一轮对话的conversation_id来接续上下文
+ "id": "65f6c28546bae1f0fbb532de",
+ "model": "glm4",
+ "object": "chat.completion",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "我叫智谱清言,是基于智谱 AI 公司于 2023 年训练的 ChatGLM 开发的。我的任务是针对用户的问题和要求提供适当的答复和支持。"
+ },
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 1,
+ "completion_tokens": 1,
+ "total_tokens": 2
+ },
+ "created": 1710152062
+}
+```
+
+### AI绘图
+
+对话补全接口,与openai的 [images-create-api](https://platform.openai.com/docs/api-reference/images/create) 兼容。
-### 环境变量
+**POST /v1/images/generations**
+
+header 需要设置 Authorization 头部:
```
-LOG_LEVEL=info # debug, info, warn, error
-LOG_PATH= # 日志文件路径,默认为空(不生成日志文件)
-BIND=0.0.0.0 # 127.0.0.1
-PORT=3040
-PROXY= # http://127.0.0.1:7890,http://127.0.0.1:7890 已支持多个代理(英文 "," 分隔)
-AUTHORIZATIONS= # abc,bac (英文 "," 分隔)
-BASE_URL= # 默认:https://chat.openai.com
-POOL_MAX_COUNT=64 # max number of connections to keep in the pool 默认:64
-AUTH_ED=600 # expiration time for the authorization in seconds 默认:600
+Authorization: Bearer [refresh_token]
```
-###### 也可使用与程序同目录下 `.env` 文件配置上述字段
+请求数据:
+```json
+{
+ // 如果使用智能体请填写智能体ID到此处,否则可以乱填
+ "model": "cogview-3",
+ "prompt": "一只可爱的猫"
+}
+```
+响应数据:
+```json
+{
+ "created": 1711507449,
+ "data": [
+ {
+ "url": "https://sfile.chatglm.cn/testpath/5e56234b-34ae-593c-ba4e-3f7ba77b5768_0.png"
+ }
+ ]
+}
+```
+
+### 文档解读
+
+提供一个可访问的文件URL或者BASE64_URL进行解析。
-### docker部署
+**POST /v1/chat/completions**
-##### 1 .创建文件夹
+header 需要设置 Authorization 头部:
+
+```
+Authorization: Bearer [refresh_token]
+```
+请求数据:
+```json
+{
+ // 如果使用智能体请填写智能体ID到此处,否则可以乱填
+ "model": "glm4",
+ "messages": [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "file",
+ "file_url": {
+ "url": "https://mj101-1317487292.cos.ap-shanghai.myqcloud.com/ai/test.pdf"
+ }
+ },
+ {
+ "type": "text",
+ "text": "文档里说了什么?"
+ }
+ ]
+ }
+ ],
+ // 如果使用SSE流请设置为true,默认false
+ "stream": false
+}
```
-mkdir -p $PWD/free-gpt3.5-2api
+
+响应数据:
+```json
+{
+ "id": "cnmuo7mcp7f9hjcmihn0",
+ "model": "glm4",
+ "object": "chat.completion",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "根据文档内容,我总结如下:\n\n这是一份关于希腊罗马时期的魔法咒语和仪式的文本,包含几个魔法仪式:\n\n1. 一个涉及面包、仪式场所和特定咒语的仪式,用于使某人爱上你。\n\n2. 一个针对女神赫卡忒的召唤仪式,用来折磨某人直到她自愿来到你身边。\n\n3. 一个通过念诵爱神阿芙罗狄蒂的秘密名字,连续七天进行仪式,来赢得一个美丽女子的心。\n\n4. 一个通过燃烧没药并念诵咒语,让一个女子对你产生强烈欲望的仪式。\n\n这些仪式都带有魔法和迷信色彩,使用各种咒语和象征性行为来影响人的感情和意愿。"
+ },
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 1,
+ "completion_tokens": 1,
+ "total_tokens": 2
+ },
+ "created": 100920
+}
```
-##### 2.拉取镜像启动
+### 图像解析
+
+提供一个可访问的图像URL或者BASE64_URL进行解析。
+
+此格式兼容 [gpt-4-vision-preview](https://platform.openai.com/docs/guides/vision) API格式,您也可以用这个格式传送文档进行解析。
+
+**POST /v1/chat/completions**
+
+header 需要设置 Authorization 头部:
```
-docker run -itd --name=free-gpt3.5-2api -p 9846:3040 ghcr.io/aurorax-neo/free-gpt3.5-2api
+Authorization: Bearer [refresh_token]
```
-##### 3.更新容器
+请求数据:
+```json
+{
+ "model": "65c046a531d3fcb034918abe",
+ "messages": [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "image_url",
+ "image_url": {
+ "url": "http://1255881664.vod2.myqcloud.com/6a0cd388vodbj1255881664/7b97ce1d3270835009240537095/uSfDwh6ZpB0A.png"
+ }
+ },
+ {
+ "type": "text",
+ "text": "图像描述了什么?"
+ }
+ ]
+ }
+ ],
+ "stream": false
+}
+```
+响应数据:
+```json
+{
+ "id": "65f6c28546bae1f0fbb532de",
+ "model": "glm",
+ "object": "chat.completion",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "图片中展示的是一个蓝色背景下的logo,具体地,左边是一个由多个蓝色的圆点组成的圆形图案,右边是“智谱·AI”四个字,字体颜色为蓝色。"
+ },
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 1,
+ "completion_tokens": 1,
+ "total_tokens": 2
+ },
+ "created": 1710670469
+}
```
-docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR free-gpt3.5-2api --debug
+
+### refresh_token存活检测
+
+检测refresh_token是否存活,如果存活live未true,否则为false,请不要频繁(小于10分钟)调用此接口。
+
+**POST /token/check**
+
+请求数据:
+```json
+{
+ "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..."
+}
+```
+
+响应数据:
+```json
+{
+ "live": true
+}
+```
+
+## 注意事项
+
+### Nginx反代优化
+
+如果您正在使用Nginx反向代理glm-free-api,请添加以下配置项优化流的输出效果,优化体验感。
+
+```nginx
+# 关闭代理缓冲。当设置为off时,Nginx会立即将客户端请求发送到后端服务器,并立即将从后端服务器接收到的响应发送回客户端。
+proxy_buffering off;
+# 启用分块传输编码。分块传输编码允许服务器为动态生成的内容分块发送数据,而不需要预先知道内容的大小。
+chunked_transfer_encoding on;
+# 开启TCP_NOPUSH,这告诉Nginx在数据包发送到客户端之前,尽可能地发送数据。这通常在sendfile使用时配合使用,可以提高网络效率。
+tcp_nopush on;
+# 开启TCP_NODELAY,这告诉Nginx不延迟发送数据,立即发送小数据包。在某些情况下,这可以减少网络的延迟。
+tcp_nodelay on;
+# 设置保持连接的超时时间,这里设置为120秒。如果在这段时间内,客户端和服务器之间没有进一步的通信,连接将被关闭。
+keepalive_timeout 120;
```
-### Koyeb部署
+### Token统计
-###### 注意:`Regions`请选择支持`openai`免登的区域!!!
+由于推理侧不在glm-free-api,因此token不可统计,将以固定数字返回。
-[](https://app.koyeb.com/deploy?type=docker&name=free-gpt3-5-2api®ion=par&ports=3040;http;/&image=ghcr.io/aurorax-neo/free-gpt3.5-2api)
+## Star History
+[](https://star-history.com/#LLM-Red-Team/glm-free-api&Date)
diff --git a/RequestClient/RequestClient.go b/RequestClient/RequestClient.go
deleted file mode 100644
index 6038a6bdc16c62396a2bc434b36caaeaeec37e16..0000000000000000000000000000000000000000
--- a/RequestClient/RequestClient.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package RequestClient
-
-import (
- fhttp "github.com/bogdanfinn/fhttp"
-)
-
-type RequestClient interface {
- Do(req *fhttp.Request) (*fhttp.Response, error)
- SetProxy(link string) error
-}
diff --git a/RequestClient/TlsClient.go b/RequestClient/TlsClient.go
deleted file mode 100644
index c9662ced147072107af5a0804b9bf9f68238d605..0000000000000000000000000000000000000000
--- a/RequestClient/TlsClient.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package RequestClient
-
-import (
- fhttp "github.com/bogdanfinn/fhttp"
- tlsClient "github.com/bogdanfinn/tls-client"
- "github.com/bogdanfinn/tls-client/profiles"
- "io"
- "math/rand"
- "time"
-)
-
-type TlsClient struct {
- client tlsClient.HttpClient
-}
-
-func NewTlsClient(timeoutSeconds int, clientProfile profiles.ClientProfile) *TlsClient {
- jar := tlsClient.NewCookieJar()
- options := []tlsClient.HttpClientOption{
- tlsClient.WithTimeoutSeconds(timeoutSeconds),
- tlsClient.WithClientProfile(clientProfile),
- tlsClient.WithNotFollowRedirects(),
- tlsClient.WithCookieJar(jar),
- }
- client, err := tlsClient.NewHttpClient(tlsClient.NewNoopLogger(), options...)
- if err != nil {
- return nil
- }
- return &TlsClient{
- client: client,
- }
-}
-
-func RandomClientProfile() profiles.ClientProfile {
- // 初始化随机数生成器
- seed := time.Now().UnixNano()
- rng := rand.New(rand.NewSource(seed))
- clientProfiles := []profiles.ClientProfile{
- profiles.Firefox_102,
- profiles.Safari_15_6_1,
- profiles.Safari_16_0,
- profiles.Chrome_110,
- profiles.Okhttp4Android13,
- profiles.CloudflareCustom,
- profiles.Firefox_117,
- }
- // 随机选择一个
- randomIndex := rng.Intn(len(clientProfiles))
- return clientProfiles[randomIndex]
-}
-
-func NewRequest(method, url string, body io.Reader) (*fhttp.Request, error) {
- request, err := fhttp.NewRequest(method, url, body)
- if err != nil {
- return nil, err
- }
- return request, nil
-
-}
-
-func (T *TlsClient) Do(req *fhttp.Request) (*fhttp.Response, error) {
- response, err := T.client.Do(req)
- if err != nil {
- return nil, err
- }
- return response, nil
-}
-
-func (T *TlsClient) SetProxy(link string) error {
- if link == "" {
- return nil
- }
- err := T.client.SetProxy(link)
- if err != nil {
- return err
- }
- return nil
-}
diff --git a/common/common.go b/common/common.go
deleted file mode 100644
index 429b192fe274742fad1febe24bf9a4d3178497ef..0000000000000000000000000000000000000000
--- a/common/common.go
+++ /dev/null
@@ -1,285 +0,0 @@
-package common
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- fhttp "github.com/bogdanfinn/fhttp"
- "github.com/bogdanfinn/fhttp/httputil"
- "github.com/gin-gonic/gin"
- jsoniter "github.com/json-iterator/go"
- "math/rand"
- "net/url"
- "os"
- "path/filepath"
- "reflect"
- "strings"
- "time"
-)
-
-func ErrorResponse(c *gin.Context, code int, msg interface{}, err interface{}) {
- c.AbortWithStatusJSON(code, gin.H{
- "detail": struct {
- Code int `json:"code"`
- Msg interface{} `json:"msg"`
- Error interface{} `json:"error"`
- }{
- Code: code,
- Msg: msg,
- Error: err,
- },
- })
- return
-}
-
-// GetTimestampSecond 获取当前时间戳 + 指定 秒
-func GetTimestampSecond(second int) int64 {
- return time.Now().Add(time.Second * time.Duration(second)).Unix()
-}
-
-func ParseUrl(link string) *url.URL {
- if link == "" {
- return &url.URL{}
- }
- u, err := url.Parse(link)
- if err != nil {
- return &url.URL{}
- }
- return u
-}
-
-func GetOrigin(link string) string {
- u := ParseUrl(link)
- if u == nil {
- return ""
- }
- return u.Scheme + "://" + u.Host
-}
-
-func Struct2BytesBuffer(v interface{}) (*bytes.Buffer, error) {
- data := new(bytes.Buffer)
- err := json.NewEncoder(data).Encode(v)
- if err != nil {
- return nil, err
- }
- return data, nil
-}
-
-func Struct2Bytes(v interface{}) ([]byte, error) {
- // 创建一个jsonIter的Encoder
- configCompatibleWithStandardLibrary := jsoniter.ConfigCompatibleWithStandardLibrary
- // 将结构体转换为JSON文本并保持顺序
- bytes_, err := configCompatibleWithStandardLibrary.Marshal(v)
- if err != nil {
- return nil, err
- }
- return bytes_, nil
-}
-
-func SplitAndAddBearer(authTokens string) []string {
- var authTokenList []string
- for _, v := range strings.Split(authTokens, ",") {
- authTokenList = append(authTokenList, "Bearer "+v)
- }
- return authTokenList
-}
-
-func GetRand() rand.Rand {
- // 初始化随机数生成器
- seed := time.Now().UnixNano()
- rng := rand.New(rand.NewSource(seed))
- return *rng
-}
-
-func RandomLanguage() string {
- // 初始化随机数生成器
- rng := GetRand()
- // 语言列表
- languages := []string{"af", "am", "ar-sa", "as", "az-Latn", "be", "bg", "bn-BD", "bn-IN", "bs", "ca", "ca-ES-valencia", "cs", "cy", "da", "de", "de-de", "el", "en-GB", "en-US", "es", "es-ES", "es-US", "es-MX", "et", "eu", "fa", "fi", "fil-Latn", "fr", "fr-FR", "fr-CA", "ga", "gd-Latn", "gl", "gu", "ha-Latn", "he", "hi", "hr", "hu", "hy", "id", "ig-Latn", "is", "it", "it-it", "ja", "ka", "kk", "km", "kn", "ko", "kok", "ku-Arab", "ky-Cyrl", "lb", "lt", "lv", "mi-Latn", "mk", "ml", "mn-Cyrl", "mr", "ms", "mt", "nb", "ne", "nl", "nl-BE", "nn", "nso", "or", "pa", "pa-Arab", "pl", "prs-Arab", "pt-BR", "pt-PT", "qut-Latn", "quz", "ro", "ru", "rw", "sd-Arab", "si", "sk", "sl", "sq", "sr-Cyrl-BA", "sr-Cyrl-RS", "sr-Latn-RS", "sv", "sw", "ta", "te", "tg-Cyrl", "th", "ti", "tk-Latn", "tn", "tr", "tt-Cyrl", "ug-Arab", "uk", "ur", "uz-Latn", "vi", "wo", "xh", "yo-Latn", "zh-Hans", "zh-Hant", "zu"}
- // 随机选择一个语言
- randomIndex := rng.Intn(len(languages))
- return languages[randomIndex]
-}
-
-// GetAbsPathAndGenerate 获取绝对路径并生成文件或文件夹
-func GetAbsPathAndGenerate(path string, isFilePath bool, content string) string {
- // 获取绝对路径
- path = GetAbsPath(path)
- if isFilePath {
- // 判断文件是否存在
- if isExist := fileIsExistAndCreat(path, content); isExist {
- return path
- }
- } else {
- // 判断文件夹是否存在
- if isExist := dirIsExistAndMkdir(path, false); isExist {
- return path
- }
- }
- return path
-}
-
-// GetAbsPath 获取绝对路径
-func GetAbsPath(path string) string {
- if !filepath.IsAbs(path) {
- absPath, err := filepath.Abs(path)
- if err != nil {
- return ""
- }
- return absPath
- }
- return path
-}
-
-func dirIsExistAndMkdir(dirPath string, isFile bool) bool {
- // 判断路径是否存在
- _, err := os.Stat(dirPath)
- dir := dirPath
- if err != nil {
- if isFile {
- dir = filepath.Dir(dirPath)
- }
- // 创建路径
- err := os.MkdirAll(dir, os.ModePerm)
- if err != nil {
- return false
- }
- }
- return true
-}
-
-func fileIsExistAndCreat(filePath string, content string) bool {
- //判断文件是否存在
- _, err := os.Stat(filePath)
- if err != nil {
- // 判断文件夹是否存在
- if isExist := dirIsExistAndMkdir(filePath, true); !isExist {
- return false
- }
- // 创建文件
- _, err := os.Create(filePath)
- if err != nil {
- return false
- }
- if content != "" {
- // 写入content
- file, _ := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0777)
- _, _ = file.Write([]byte(content))
- defer func(file *os.File) {
- _ = file.Close()
- }(file)
- }
- }
- return true
-}
-
-// AsyncTimingTask 定时任务 参数含函数
-func AsyncTimingTask(nanosecond time.Duration, fun func()) {
- go func() {
- timerChan := time.After(nanosecond)
- // 使用for循环阻塞等待定时器的信号
- for {
- // 通过select语句监听定时器通道和其他事件
- select {
- case <-timerChan:
- fun()
- // 重新设置定时器,以便下一次执行
- timerChan = time.After(nanosecond)
- }
- time.Sleep(time.Millisecond * 100)
- }
- }()
-}
-
-// AsyncLoopTask AsyncTimingTask 定时任务 参数含函数
-func AsyncLoopTask(sleep time.Duration, fun func()) {
- go func() {
- for {
- fun()
- time.Sleep(sleep)
- }
- }()
-}
-
-// DeepCopyStruct 深拷贝函数
-func DeepCopyStruct(src interface{}) interface{} {
- // 获取源对象的类型信息
- srcType := reflect.TypeOf(src)
- // 创建目标对象
- dst := reflect.New(srcType).Elem()
-
- // 深拷贝过程
- deepCopyValue(reflect.ValueOf(src), dst)
-
- return dst.Interface()
-}
-
-// 递归进行深拷贝
-func deepCopyValue(src, dst reflect.Value) {
- switch src.Kind() {
- case reflect.Ptr:
- if src.IsNil() {
- dst.Set(src)
- return
- }
- // 递归处理指针指向的内容
- newDst := reflect.New(src.Elem().Type())
- deepCopyValue(src.Elem(), newDst.Elem())
- dst.Set(newDst)
- case reflect.Struct:
- for i := 0; i < src.NumField(); i++ {
- // 递归处理结构体的字段
- deepCopyValue(src.Field(i), dst.Field(i))
- }
- default:
- // 检查目标值是否支持设置
- if dst.CanSet() {
- // 处理基本类型和数组、切片、映射等
- dst.Set(src)
- }
- }
-}
-
-func RandomHexadecimalString() string {
- rng := GetRand()
- const charset = "0123456789abcdef"
- const length = 16 // The length of the string you want to generate
- b := make([]byte, length)
- for i := range b {
- b[i] = charset[rng.Intn(len(charset))]
- }
- return string(b)
-}
-
-// OutRequest 打印请求.
-func OutRequest(req *fhttp.Request) {
- dump, err := httputil.DumpRequestOut(req, true)
- if err != nil {
- fmt.Println("Error dumping request:", err)
- } else {
- fmt.Println(string(dump))
- }
-}
-
-// OutResponse 打印响应.
-func OutResponse(res *fhttp.Response) {
- dump, err := httputil.DumpResponse(res, true)
- if err != nil {
- fmt.Println("Error dumping response:", err)
- } else {
- fmt.Println(string(dump))
- }
-}
-
-func IsStrInArray(str string, strS []string) bool {
- // 如果 strS 为空,直接返回 true
- if len(strS) == 0 {
- return true
- }
- for _, v := range strS {
- if v == str {
- return true
- }
- }
- return false
-}
diff --git a/config/config.go b/config/config.go
deleted file mode 100644
index d55b16169cf5eaeca9bd42e25361b3f28e63d68b..0000000000000000000000000000000000000000
--- a/config/config.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package config
-
-import (
- "free-gpt3.5-2api/common"
- "github.com/joho/godotenv"
- "os"
- "strconv"
- "strings"
-)
-
-var (
- Bind string
- Port string
- Proxy []string
- AUTHORIZATIONS []string
- BaseUrl string
- PoolMaxCount int
- AuthED int
-)
-
-func init() {
- _ = godotenv.Load()
- // Bind
- Bind = os.Getenv("BIND")
- if Bind == "" {
- Bind = "0.0.0.0"
- }
- // PORT
- Port = os.Getenv("PORT")
- if Port == "" {
- Port = "3040"
- }
- // PROXY
- proxy := os.Getenv("PROXY")
- if proxy != "" {
- Proxy = strings.Split(proxy, ",")
- }
- // AUTH_TOKEN
- authorizations := os.Getenv("AUTHORIZATIONS")
- if authorizations == "" {
- AUTHORIZATIONS = []string{}
- } else {
- //以,分割 AUTH_TOKEN 并且为每个AUTH_TOKEN前面加上Bearer
- AUTHORIZATIONS = common.SplitAndAddBearer(authorizations)
- }
- // BASE_URL
- BaseUrl = os.Getenv("BASE_URL")
- if BaseUrl == "" {
- BaseUrl = "https://chatgpt.com"
- } else {
- BaseUrl = strings.TrimRight(BaseUrl, "/")
- }
- // POOL_MAX_COUNT
- poolMaxCount := os.Getenv("POOL_MAX_COUNT")
- var err error
- if poolMaxCount == "" {
- PoolMaxCount = 64
- } else {
- PoolMaxCount, err = strconv.Atoi(poolMaxCount)
- if err != nil {
- PoolMaxCount = 64
- }
- }
- // AUTH_ED
- authED := os.Getenv("AUTH_ED")
- if authED == "" {
- AuthED = 600
- } else {
- AuthED, err = strconv.Atoi(authED)
- if err != nil {
- AuthED = 600
- }
- }
-}
diff --git a/configs/dev/service.yml b/configs/dev/service.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c3b2e40f50b954aab34f97aa082d0bd75af40bbd
--- /dev/null
+++ b/configs/dev/service.yml
@@ -0,0 +1,6 @@
+# 服务名称
+name: glm-free-api
+# 服务绑定主机地址
+host: '0.0.0.0'
+# 服务绑定端口
+port: 8000
\ No newline at end of file
diff --git a/configs/dev/system.yml b/configs/dev/system.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dca6170becae22d950112402a8cfc28ab12d9a7d
--- /dev/null
+++ b/configs/dev/system.yml
@@ -0,0 +1,14 @@
+# 是否开启请求日志
+requestLog: true
+# 临时目录路径
+tmpDir: ./tmp
+# 日志目录路径
+logDir: ./logs
+# 日志写入间隔(毫秒)
+logWriteInterval: 200
+# 日志文件有效期(毫秒)
+logFileExpires: 2626560000
+# 公共目录路径
+publicDir: ./public
+# 临时文件有效期(毫秒)
+tmpFileExpires: 86400000
\ No newline at end of file
diff --git a/constant/constant.go b/constant/constant.go
deleted file mode 100644
index c48f636394f9d0dc75d351565950584dde8a0e7c..0000000000000000000000000000000000000000
--- a/constant/constant.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package constant
-
-import "github.com/bogdanfinn/tls-client/profiles"
-
-var (
- ClientProfile = profiles.Safari_15_6_1
-)
-
-const (
- Ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
-)
diff --git a/doc/example-0.png b/doc/example-0.png
new file mode 100644
index 0000000000000000000000000000000000000000..405050ef3b49f1674820d909b3c65888fd2a477a
Binary files /dev/null and b/doc/example-0.png differ
diff --git a/doc/example-1.png b/doc/example-1.png
new file mode 100644
index 0000000000000000000000000000000000000000..0fdb6b848bbc6e14ec0ba6ee6077b01915f637a0
Binary files /dev/null and b/doc/example-1.png differ
diff --git a/doc/example-10.png b/doc/example-10.png
new file mode 100644
index 0000000000000000000000000000000000000000..541509a84785d83fab92ae3c566853c91841c841
Binary files /dev/null and b/doc/example-10.png differ
diff --git a/doc/example-11.png b/doc/example-11.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a9a092762ec28ae945d0c6a09953b97c96494d6
Binary files /dev/null and b/doc/example-11.png differ
diff --git a/doc/example-12.png b/doc/example-12.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a5a90475a108ee7e1627b77248f7f691f5cf7b3
Binary files /dev/null and b/doc/example-12.png differ
diff --git a/doc/example-2.png b/doc/example-2.png
new file mode 100644
index 0000000000000000000000000000000000000000..397ba6778dc3da294134af8720de868666f00c3b
Binary files /dev/null and b/doc/example-2.png differ
diff --git a/doc/example-3.png b/doc/example-3.png
new file mode 100644
index 0000000000000000000000000000000000000000..e28f4ad6f8dda347e1090cea4ff65eb371a08387
Binary files /dev/null and b/doc/example-3.png differ
diff --git a/doc/example-5.png b/doc/example-5.png
new file mode 100644
index 0000000000000000000000000000000000000000..aacb21805bb6b835ced352d91e7dc67feba77afe
Binary files /dev/null and b/doc/example-5.png differ
diff --git a/doc/example-6.png b/doc/example-6.png
new file mode 100644
index 0000000000000000000000000000000000000000..61e701517d00a75314c83e55fa3f4f74ce13530f
Binary files /dev/null and b/doc/example-6.png differ
diff --git a/doc/example-9.png b/doc/example-9.png
new file mode 100644
index 0000000000000000000000000000000000000000..f52e1cb9ce747bd667f9bda5553b6979a6155b98
Binary files /dev/null and b/doc/example-9.png differ
diff --git a/env.template b/env.template
deleted file mode 100644
index 44eaccf392a457fcd400c55e53f6a020f7bbd8b9..0000000000000000000000000000000000000000
--- a/env.template
+++ /dev/null
@@ -1,8 +0,0 @@
-LOG_LEVEL=info # debug, info, warn, error
-BIND=127.0.0.1 #
-PORT=8080
-PROXY=
-AUTHORIZATIONS=
-POOL_MAX_COUNT=5 # max number of connections to keep in the pool
-AUTH_ED=180 # expiration time for the authorization in seconds
-AUTH_USE_COUNT=5 # number of times an authorization can be used
\ No newline at end of file
diff --git a/go.mod b/go.mod
deleted file mode 100644
index 34f61c1cbf861598141a55311139e5669a90391b..0000000000000000000000000000000000000000
--- a/go.mod
+++ /dev/null
@@ -1,50 +0,0 @@
-module free-gpt3.5-2api
-
-go 1.21
-
-require (
- github.com/aurorax-neo/go-logger v0.0.0-20240421094709-1eb4bda786d5
- github.com/bogdanfinn/fhttp v0.5.28
- github.com/bogdanfinn/tls-client v1.7.2
- github.com/gin-gonic/gin v1.9.1
- github.com/google/uuid v1.6.0
- github.com/joho/godotenv v1.5.1
- github.com/json-iterator/go v1.1.12
- github.com/launchdarkly/eventsource v1.7.1
- golang.org/x/crypto v0.21.0
-)
-
-require (
- github.com/andybalholm/brotli v1.0.5 // indirect
- github.com/bogdanfinn/utls v1.6.1 // indirect
- github.com/bytedance/sonic v1.9.1 // indirect
- github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
- github.com/cloudflare/circl v1.3.7 // indirect
- github.com/gabriel-vasile/mimetype v1.4.2 // indirect
- 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/goccy/go-json v0.10.2 // indirect
- github.com/klauspost/compress v1.16.7 // indirect
- github.com/klauspost/cpuid/v2 v2.2.4 // 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/pelletier/go-toml/v2 v2.0.8 // indirect
- github.com/quic-go/quic-go v0.42.0 // indirect
- github.com/rogpeppe/go-internal v1.12.0 // indirect
- github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
- github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
- github.com/ugorji/go/codec v1.2.11 // indirect
- go.uber.org/multierr v1.11.0 // indirect
- go.uber.org/zap v1.27.0 // indirect
- golang.org/x/arch v0.3.0 // indirect
- golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
- golang.org/x/net v0.22.0 // indirect
- golang.org/x/sys v0.18.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- google.golang.org/protobuf v1.33.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
-)
diff --git a/go.sum b/go.sum
deleted file mode 100644
index 47f6cdf813f5959f59c70c9a5ac9c6b5dd7fe86c..0000000000000000000000000000000000000000
--- a/go.sum
+++ /dev/null
@@ -1,139 +0,0 @@
-github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
-github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
-github.com/aurorax-neo/go-logger v0.0.0-20240421094709-1eb4bda786d5 h1:L1ei0BPLvE/ld4KAh4bKVAn5tDYOdJz0SuxlbuzfKzQ=
-github.com/aurorax-neo/go-logger v0.0.0-20240421094709-1eb4bda786d5/go.mod h1:BJsRG1ECcXTHwiz2zaMYxFkeXh+MpQVs6nWYphLT244=
-github.com/bogdanfinn/fhttp v0.5.28 h1:G6thT8s8v6z1IuvXMUsX9QKy3ZHseTQTzxuIhSiaaAw=
-github.com/bogdanfinn/fhttp v0.5.28/go.mod h1:oJiYPG3jQTKzk/VFmogH8jxjH5yiv2rrOH48Xso2lrE=
-github.com/bogdanfinn/tls-client v1.7.2 h1:vpL5qBYUfT9ueygEf1yLfymrXyUEZQatL25amfqGV8M=
-github.com/bogdanfinn/tls-client v1.7.2/go.mod h1:pOGa2euqTbEkGNqE5idx5jKKfs9ytlyn3fwEw8RSP+g=
-github.com/bogdanfinn/utls v1.6.1 h1:dKDYAcXEyFFJ3GaWaN89DEyjyRraD1qb4osdEK89ass=
-github.com/bogdanfinn/utls v1.6.1/go.mod h1:VXIbRZaiY/wHZc6Hu+DZ4O2CgTzjhjCg/Ou3V4r/39Y=
-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/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/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
-github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
-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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
-github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
-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-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
-github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-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.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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
-github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
-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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
-github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
-github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-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.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
-github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/launchdarkly/eventsource v1.7.1 h1:StoRQeiPyrcQIXjlQ7b5jWMzHW4p+GGczN2r2oBhujg=
-github.com/launchdarkly/eventsource v1.7.1/go.mod h1:LHxSeb4OnqznNZxCSXbFghxS/CjIQfzHovNoAqbO/Wk=
-github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs=
-github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw=
-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/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/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.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
-github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
-github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
-github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
-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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
-github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
-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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.0/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/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
-github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
-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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
-github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
-go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
-go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
-go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
-go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
-golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
-golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
-golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
-golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
-golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/libs.d.ts b/libs.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/main.go b/main.go
deleted file mode 100644
index 38b09a0413cb0bde2eef30dede5e502476aeb161..0000000000000000000000000000000000000000
--- a/main.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package main
-
-import (
- "fmt"
- "free-gpt3.5-2api/FreeGpt35Pool"
- "free-gpt3.5-2api/ProxyPool"
- "free-gpt3.5-2api/config"
- "free-gpt3.5-2api/router"
- "github.com/aurorax-neo/go-logger"
- "github.com/gin-gonic/gin"
-)
-
-func Init() {
- ProxyPool.GetProxyPoolInstance()
- FreeGpt35Pool.GetFreeGpt35PoolInstance()
-}
-
-func main() {
- // Init
- Init()
- // Initialize HTTP server
- gin.SetMode(gin.ReleaseMode)
- server := gin.New()
- server.Use(gin.Recovery())
- // 设置路由
- router.SetRouter(server)
- // 提示服务启动
- host := config.Bind
- if config.Bind == "0.0.0.0" {
- host = "127.0.0.1"
- }
- logger.Logger.Info(fmt.Sprint("Server started on http://", host, ":", config.Port))
- // 启动 HTTP 服务器
- _ = server.Run(fmt.Sprint(config.Bind, ":", config.Port))
-}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..64f8993b57d94451d8629350d3a1a8bb281f501d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "glm-free-api",
+ "version": "0.0.30",
+ "description": "GLM Free API Server",
+ "type": "module",
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "directories": {
+ "dist": "dist"
+ },
+ "files": [
+ "dist/"
+ ],
+ "scripts": {
+ "dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node dist/index.js\"",
+ "start": "node dist/index.js",
+ "build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public"
+ },
+ "author": "Vinlic",
+ "license": "ISC",
+ "dependencies": {
+ "axios": "^1.6.7",
+ "colors": "^1.4.0",
+ "crc-32": "^1.2.2",
+ "cron": "^3.1.6",
+ "date-fns": "^3.3.1",
+ "eventsource-parser": "^1.1.2",
+ "form-data": "^4.0.0",
+ "fs-extra": "^11.2.0",
+ "koa": "^2.15.0",
+ "koa-body": "^5.0.0",
+ "koa-bodyparser": "^4.4.1",
+ "koa-range": "^0.3.0",
+ "koa-router": "^12.0.1",
+ "koa2-cors": "^2.0.6",
+ "lodash": "^4.17.21",
+ "mime": "^4.0.1",
+ "minimist": "^1.2.8",
+ "randomstring": "^1.3.0",
+ "uuid": "^9.0.1",
+ "yaml": "^2.3.4"
+ },
+ "devDependencies": {
+ "@types/lodash": "^4.14.202",
+ "@types/mime": "^3.0.4",
+ "tsup": "^8.0.2",
+ "typescript": "^5.3.3"
+ }
+}
diff --git a/public/welcome.html b/public/welcome.html
new file mode 100644
index 0000000000000000000000000000000000000000..98d380ace3e7791e1d76f8694525f934f7c433c1
--- /dev/null
+++ b/public/welcome.html
@@ -0,0 +1,10 @@
+
+
+
+
+ 🚀 服务已启动
+
+
+ glm-free-api已启动!
请通过LobeChat / NextChat / Dify等客户端或OpenAI SDK接入!
+
+
\ No newline at end of file
diff --git a/queue/queue.go b/queue/queue.go
deleted file mode 100644
index 3278488734b16abfdd1ae43c0f33bd2a0cbdbf4d..0000000000000000000000000000000000000000
--- a/queue/queue.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package queue
-
-type (
- Queue struct {
- start, end *Node
- length int
- }
- Node struct {
- Value interface{}
- next *Node
- }
-)
-
-// New 新建一个队列
-func New() *Queue {
- return &Queue{nil, nil, 0}
-}
-
-// Dequeue 出队
-func (Q *Queue) Dequeue() *Node {
- if Q.length == 0 {
- return nil
- }
- n := Q.start
- if Q.length == 1 {
- Q.start = nil
- Q.end = nil
- } else {
- Q.start = Q.start.next
- }
- Q.length--
- return n
-}
-
-// Enqueue 入队
-func (Q *Queue) Enqueue(value interface{}) {
- n := &Node{value, nil}
- if Q.length == 0 {
- Q.start = n
- Q.end = n
- } else {
- Q.end.next = n
- Q.end = n
- }
- Q.length++
-}
-
-// Len 获取队列长度
-func (Q *Queue) Len() int {
- return Q.length
-}
-
-// Peek 返回队列的第一个元素
-func (Q *Queue) Peek() *Node {
- if Q.length == 0 {
- return nil
- }
- return Q.start
-}
-
-// Remove 移除指定节点
-func (Q *Queue) Remove(n *Node) {
- if Q.length == 0 || n == nil {
- return
- }
-
- // 如果移除的是队列的第一个元素
- if n == Q.start {
- Q.start = Q.start.next
- if Q.start == nil {
- // 如果移除后队列为空,则end也应该设置为nil
- Q.end = nil
- }
- Q.length--
- return
- }
-
- // 找到n的前一个节点
- prevNode := Q.start
- for prevNode != nil && prevNode.next != n {
- prevNode = prevNode.next
- }
-
- if prevNode == nil {
- // 没有找到n的前一个节点(n不在队列中)
- return
- }
-
- // 移除节点n
- prevNode.next = n.next
- // 如果移除的是最后一个元素,更新end指针
- if n.next == nil {
- Q.end = prevNode
- }
- Q.length--
-}
-
-// Traverse 遍历队列
-func (Q *Queue) Traverse(cb func(n *Node)) {
- if Q.length == 0 {
- return
- }
- for n := Q.start; n != nil; n = n.next {
- cb(n)
- }
-}
diff --git a/release.bat b/release.bat
deleted file mode 100644
index dc1200192f647440b9a60db8438b2cbfc5d08565..0000000000000000000000000000000000000000
--- a/release.bat
+++ /dev/null
@@ -1,56 +0,0 @@
-@echo off
-SETLOCAL
-
-REM 指定编码为 UTF-8
-chcp 65001
-
-REM 设置要生成的可执行文件的名称
-set OUTPUT_NAME=free-gpt3.5-2api
-
-REM 设置 Go 源文件的名称
-SET GOFILE=main.go
-
-REM 设置输出目录
-SET OUTPUTDIR=target
-
-REM 确保输出目录存在
-IF NOT EXIST %OUTPUTDIR% MKDIR %OUTPUTDIR%
-
-REM 编译为 Windows/amd64
-echo 开始编译 Windows/amd64
-SET GOOS=windows
-SET GOARCH=amd64
-go build -o %OUTPUTDIR%/%OUTPUT_NAME%_windows_amd64.exe %GOFILE%
-echo 编译完成 Windows/amd64
-
-REM 编译为 Windows/386
-echo 开始编译 Windows/386
-SET GOOS=windows
-SET GOARCH=386
-go build -o %OUTPUTDIR%/%OUTPUT_NAME%_windows_386.exe %GOFILE%
-echo 编译完成 Windows/386
-
-REM 编译为 Linux/amd64
-echo 开始编译 Linux/amd64
-SET GOOS=linux
-SET GOARCH=amd64
-go build -o %OUTPUTDIR%/%OUTPUT_NAME%_linux_amd64 %GOFILE%
-echo 编译完成 Linux/amd64
-
-REM 编译为 macOS/amd64
-echo 开始编译 macOS/amd64
-SET GOOS=darwin
-SET GOARCH=amd64
-go build -o %OUTPUTDIR%/%OUTPUT_NAME%_macos_amd64 %GOFILE%
-echo 编译完成 macOS/amd64
-
-REM 编译为 freebsd/amd64
-echo 开始编译 freebsd/amd64
-SET GOOS=freebsd
-SET GOARCH=amd64
-go build -o %OUTPUTDIR%/%OUTPUT_NAME%_freebsd_amd64 %GOFILE%
-echo 编译完成 freebsd/amd64
-
-REM 结束批处理脚本
-ENDLOCAL
-echo 编译完成!
diff --git a/router/middleware.go b/router/middleware.go
deleted file mode 100644
index 93c0e4e8b710f9ba03b605b12560a2df6c47b862..0000000000000000000000000000000000000000
--- a/router/middleware.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package router
-
-import (
- "fmt"
- "free-gpt3.5-2api/common"
- "free-gpt3.5-2api/config"
- "github.com/aurorax-neo/go-logger"
- "github.com/gin-gonic/gin"
-)
-
-// Ping 测试接口
-func Ping(c *gin.Context) {
- c.JSON(200, gin.H{
- "message": "pong",
- })
-}
-
-// V1Cors 跨域中间件
-func V1Cors(c *gin.Context) {
- // 允许跨域
- c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
- c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
- c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
- c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept")
- c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
- // 如果是OPTIONS请求,直接返回
- if c.Request.Method == "OPTIONS" {
- c.AbortWithStatus(204)
- return
- }
- c.Next()
-}
-
-// V1Request 请求中间件
-func V1Request(c *gin.Context) {
- // 打印请求摘要 方法 url ip - user-agent 格式化输出
- infoStr := fmt.Sprint(" -> ", c.Request.Method, " ", c.Request.URL.String(), " - ", c.ClientIP(), " - ", c.Request.Header.Get("User-Agent"))
- logger.Logger.Info(infoStr)
- c.Next()
-}
-
-// V1Auth 验证v1 api 的token
-func V1Auth(c *gin.Context) {
- authToken := c.Request.Header.Get("Authorization")
- if authToken == "" && len(config.AUTHORIZATIONS) > 0 {
- common.ErrorResponse(c, 401, "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY)", nil)
- return
- }
- // 判断 authToken 是否在 config.CONFIG.AUTHORIZATIONS 列表
- if !common.IsStrInArray(authToken, config.AUTHORIZATIONS) {
- common.ErrorResponse(c, 401, "Incorrect API key provided: sk-4yNZz***************************************6mjw.", nil)
- return
- }
- c.Next()
-}
-
-// V1Response 响应中间件
-func V1Response(c *gin.Context) {
- c.Next()
- // 打印响应摘要 方法 url 状态码
- infoStr := fmt.Sprint(" <- ", c.Request.Method, " ", c.Request.URL.String(), " - ", c.Writer.Status())
- logger.Logger.Info(infoStr)
-}
diff --git a/router/router.go b/router/router.go
deleted file mode 100644
index 9aff77691a9921828c63e6140e28ea0cc2f06dde..0000000000000000000000000000000000000000
--- a/router/router.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package router
-
-import (
- v1 "free-gpt3.5-2api/service/v1"
- "free-gpt3.5-2api/service/v1Chat"
- "github.com/gin-gonic/gin"
- "net/http"
-)
-
-func SetRouter(router *gin.Engine) {
- router.GET("/", Index)
- router.GET("/ping", Ping)
- v1Router := router.Group("/v1")
- v1Router.Use(V1Cors)
- v1Router.Use(V1Request)
- v1Router.Use(V1Response)
- v1Router.Use(V1Auth)
- v1Router.GET("/tokens", v1.Tokens)
- v1Router.OPTIONS("/chat/completions", nil)
- v1Router.POST("/chat/completions", v1Chat.Completions)
-}
-
-func Index(c *gin.Context) {
- c.String(http.StatusOK, "Hello,This is free-gpt3.5-2api.")
-}
diff --git a/service/v1/tokens.go b/service/v1/tokens.go
deleted file mode 100644
index b576c184401eabe468ea04dee1de63a4c8698423..0000000000000000000000000000000000000000
--- a/service/v1/tokens.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package v1
-
-import (
- "fmt"
- "free-gpt3.5-2api/FreeGpt35Pool"
- "github.com/aurorax-neo/go-logger"
- "github.com/gin-gonic/gin"
-)
-
-type TokensResp struct {
- Count int `json:"count"`
-}
-
-func Tokens(c *gin.Context) {
- resp := &TokensResp{
- Count: FreeGpt35Pool.GetFreeGpt35PoolInstance().GetSize(),
- }
- logger.Logger.Info(fmt.Sprint("FreeGpt35Pool Tokens: ", resp.Count))
- c.JSON(200, resp)
-}
diff --git a/service/v1/util.go b/service/v1/util.go
deleted file mode 100644
index 358e84e457c7137e344a27af7f96a697563475e7..0000000000000000000000000000000000000000
--- a/service/v1/util.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package v1
-
-import (
- "free-gpt3.5-2api/service/v1Chat/reqModel"
- "github.com/google/uuid"
- "math/rand"
-)
-
-func MappingModel(model string) string {
- var modelMapping = map[string]string{
- "gpt-3.5-turbo": "text-davinci-002-render-sha",
- "gpt-3.5-turbo-16k": "text-davinci-002-render-sha",
- "gpt-3.5-turbo-16k-0613": "text-davinci-002-render-sha",
- "gpt-3.5-turbo-0301": "text-davinci-002-render-sha",
- "gpt-3.5-turbo-0613": "text-davinci-002-render-sha",
- "gpt-3.5-turbo-1106": "text-davinci-002-render-sha",
- }
- if model == "" {
- return "text-davinci-002-render-sha"
- }
- if v, ok := modelMapping[model]; ok {
- return v
- }
- return "text-davinci-002-render-sha"
-}
-
-func GenerateID(length int) string {
- const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
- id := "chatcmpl-"
- for i := 0; i < length; i++ {
- id += string(charset[rand.Intn(len(charset))])
- }
- return id
-}
-
-func ApiReq2ChatReq35(apiReq *reqModel.ApiReq) (chatReq *reqModel.ChatReq35) {
- messages := make([]reqModel.ChatMessages, 0)
- for _, apiMessage := range apiReq.Messages {
- chatMessage := reqModel.ChatMessages{
- Author: reqModel.ChatAuthor{
- Role: apiMessage.Role,
- },
- Content: reqModel.ChatContent{
- ContentType: "text",
- Parts: []string{apiMessage.Content},
- },
- }
- messages = append(messages, chatMessage)
- }
-
- chatReq = &reqModel.ChatReq35{
- Action: "next",
- Messages: messages,
- ParentMessageId: uuid.New().String(),
- Model: MappingModel(apiReq.Model),
- TimeZoneOffsetMin: -180,
- Suggestions: make([]string, 0),
- HistoryAndTrainingDisabled: true,
- ConversationMode: reqModel.ChatConversationMode{
- Kind: "primary_assistant",
- },
- WebsocketRequestId: uuid.New().String(),
- }
- return chatReq
-}
diff --git a/service/v1Chat/completions.go b/service/v1Chat/completions.go
deleted file mode 100644
index 434834a6513424ed52d5632b0532c0ab47ad87fb..0000000000000000000000000000000000000000
--- a/service/v1Chat/completions.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package v1Chat
-
-import (
- "free-gpt3.5-2api/common"
- "free-gpt3.5-2api/service/v1Chat/reqModel"
- "github.com/gin-gonic/gin"
- "net/http"
-)
-
-func Completions(c *gin.Context) {
- // 从请求中获取参数
- apiReq := &reqModel.ApiReq{}
- err := c.BindJSON(apiReq)
- if err != nil {
- common.ErrorResponse(c, http.StatusBadRequest, "Invalid parameter", nil)
- return
- }
- Gpt35Completions(c, apiReq)
-}
diff --git a/service/v1Chat/gpt35Completions.go b/service/v1Chat/gpt35Completions.go
deleted file mode 100644
index 650cc0b5d37cc6cd8498ceb1dc2106acf250971e..0000000000000000000000000000000000000000
--- a/service/v1Chat/gpt35Completions.go
+++ /dev/null
@@ -1,231 +0,0 @@
-package v1Chat
-
-import (
- "encoding/json"
- "fmt"
- "free-gpt3.5-2api/FreeGpt35"
- "free-gpt3.5-2api/FreeGpt35Pool"
- "free-gpt3.5-2api/common"
- "free-gpt3.5-2api/service/v1"
- "free-gpt3.5-2api/service/v1Chat/reqModel"
- "free-gpt3.5-2api/service/v1Chat/respModel"
- "github.com/aurorax-neo/go-logger"
- fhttp "github.com/bogdanfinn/fhttp"
- "github.com/gin-gonic/gin"
- "github.com/launchdarkly/eventsource"
- "io"
- "net/http"
- "strings"
-)
-
-func Gpt35Completions(c *gin.Context, apiReq *reqModel.ApiReq) {
- // 获取 FreeGpt35 实例
- ChatGpt35 := FreeGpt35Pool.GetFreeGpt35PoolInstance().GetFreeGpt35(3)
- if ChatGpt35 == nil {
- errStr := "please restart the program、change the IP address、use a proxy to try again."
- logger.Logger.Error(errStr)
- common.ErrorResponse(c, http.StatusUnauthorized, errStr, nil)
- return
- }
- // 转换请求
- ChatReq35 := v1.ApiReq2ChatReq35(apiReq)
- // 请求参数
- body, err := common.Struct2BytesBuffer(ChatReq35)
- if err != nil {
- logger.Logger.Error(err.Error())
- common.ErrorResponse(c, http.StatusInternalServerError, "", err)
- return
-
- }
- // 生成请求
- request, err := ChatGpt35.NewRequest(fhttp.MethodPost, FreeGpt35.ChatUrl, body)
- if err != nil || request == nil {
- errStr := "Request is nil or error"
- logger.Logger.Error("Request is nil or error")
- common.ErrorResponse(c, http.StatusInternalServerError, errStr, err)
- return
- }
- // 设置请求头
- request.Header.Set("Content-Type", "application/json")
- request.Header.Set("oai-device-id", ChatGpt35.FreeAuth.OaiDeviceId)
- request.Header.Set("openai-sentinel-chat-requirements-token", ChatGpt35.FreeAuth.Token)
- if ChatGpt35.FreeAuth.ProofWork.Required {
- request.Header.Set("Openai-Sentinel-Proof-Token", ChatGpt35.FreeAuth.ProofWork.Ospt)
- }
- // 发送请求
- response, err := ChatGpt35.RequestClient.Do(request)
- if err != nil {
- errStr := "RequestClient Do error"
- logger.Logger.Error(fmt.Sprint(errStr, " ", err))
- common.ErrorResponse(c, http.StatusInternalServerError, errStr, err)
- return
- }
- defer func(Body io.ReadCloser) {
- _ = Body.Close()
- }(response.Body)
- if response.StatusCode != http.StatusOK {
- errStr := "Request error"
- logger.Logger.Error(fmt.Sprint(errStr, " ", response.StatusCode))
- common.ErrorResponse(c, response.StatusCode, errStr, nil)
- return
- }
- // 流式返回
- if apiReq.Stream {
- __CompletionsStream(c, apiReq, response)
- } else { // 非流式回应
- __CompletionsNoStream(c, apiReq, response)
- }
-}
-
-func __CompletionsStream(c *gin.Context, apiReq *reqModel.ApiReq, resp *fhttp.Response) {
- defer func(Body io.ReadCloser) {
- _ = Body.Close()
- }(resp.Body)
- messageTemp := ""
- decoder := eventsource.NewDecoder(resp.Body)
- // 响应id
- id := v1.GenerateID(29)
- handlingSigns := false
- for {
- event, err := decoder.Decode()
- if err != nil {
- logger.Logger.Error(err.Error())
- common.ErrorResponse(c, http.StatusInternalServerError, "", err)
- break
- }
- name := event.Event()
- data := event.Data()
- // 空白数据不处理
- if data == "" {
- continue
- }
- // 结束标志
- if data == "[DONE]" {
- // 生成响应 stream
- apiRespStream := respModel.NewApiRespStream(id, apiReq.Model, "", "stop")
- // 生成响应 bytes
- bytes, err := common.Struct2Bytes(apiRespStream)
- if err != nil {
- logger.Logger.Error(err.Error())
- continue
- }
- // 发送响应
- c.SSEvent(name, fmt.Sprint(" ", string(bytes)))
- // 结束
- c.SSEvent(name, " [DONE]")
- return
- }
- chatResp35 := &respModel.ChatResp35{}
- err = json.Unmarshal([]byte(data), chatResp35)
- if chatResp35.Error != nil && !handlingSigns {
- logger.Logger.Error(fmt.Sprint(chatResp35.Error))
- common.ErrorResponse(c, http.StatusInternalServerError, "", chatResp35.Error)
- return
- }
- // 脏数据不处理
- if err != nil {
- continue
- }
- // 被block
- if contentIsBlocked(chatResp35) {
- // 返回响应
- common.ErrorResponse(c, http.StatusBadRequest, "content is blocked.", "")
- return
- }
- // 仅处理assistant的消息
- if chatResp35.Message.Author.Role == "assistant" && (chatResp35.Message.Status == "in_progress" || handlingSigns) {
- // handlingSigns 置为 true
- handlingSigns = true
- // 仅处理第一个part
- parts := chatResp35.Message.Content.Parts[0]
- // 去除重复数据
- content := strings.Replace(parts, messageTemp, "", 1)
- messageTemp = parts
- // 空白数据不处理
- if content == "" {
- continue
- }
- // 生成响应 stream
- apiRespStream := respModel.NewApiRespStream(id, apiReq.Model, content, "")
- // 生成响应 bytes
- bytes, err := common.Struct2Bytes(apiRespStream)
- if err != nil {
- logger.Logger.Error(err.Error())
- continue
- }
- // 发送响应
- c.SSEvent(name, fmt.Sprint(" ", string(bytes)))
- // 继续
- continue
- }
- }
-}
-
-func __CompletionsNoStream(c *gin.Context, apiReq *reqModel.ApiReq, resp *fhttp.Response) {
- defer func(Body io.ReadCloser) {
- _ = Body.Close()
- }(resp.Body)
- content := ""
- decoder := eventsource.NewDecoder(resp.Body)
- handlingSigns := false
- for {
- event, err := decoder.Decode()
- if err != nil {
- logger.Logger.Error(err.Error())
- common.ErrorResponse(c, http.StatusInternalServerError, "", err)
- return
- }
- data := event.Data()
- // 空白数据不处理
- if data == "" {
- continue
- }
- // 结束标志
- if data == "[DONE]" {
- apiRespObj := respModel.NewApiRespJson(v1.GenerateID(29), apiReq.Model, content)
- // 返回响应
- c.JSON(http.StatusOK, apiRespObj)
- return
- }
- chatResp35 := &respModel.ChatResp35{}
- err = json.Unmarshal([]byte(data), chatResp35)
- if chatResp35.Error != nil && !handlingSigns {
- logger.Logger.Error(fmt.Sprint(chatResp35.Error))
- common.ErrorResponse(c, http.StatusInternalServerError, "", chatResp35.Error)
- return
- }
- // 被block
- if contentIsBlocked(chatResp35) {
- // 返回响应
- common.ErrorResponse(c, http.StatusBadRequest, "content is blocked.", "")
- return
- }
- // 脏数据不处理
- if err != nil {
- continue
- }
- // 仅处理assistant的消息
- if chatResp35.Message.Author.Role == "assistant" && (chatResp35.Message.Status == "in_progress" || handlingSigns) {
- // handlingSigns 置为 true
- handlingSigns = true
- // 如果不包含上一次的数据则不处理
- if !strings.Contains(chatResp35.Message.Content.Parts[0], content) {
- continue
- }
- // 仅处理第一个part
- content = chatResp35.Message.Content.Parts[0]
- // 空白数据不处理
- if content == "" {
- continue
- }
- continue
- }
- }
-}
-
-func contentIsBlocked(chatResp35 *respModel.ChatResp35) bool {
- if !chatResp35.IsCompletion && chatResp35.ModerationResponse.Blocked {
- return true
- }
- return false
-}
diff --git a/service/v1Chat/reqModel/apiReq.go b/service/v1Chat/reqModel/apiReq.go
deleted file mode 100644
index 3363f3638603fb56147f7fabeddab70d372912e4..0000000000000000000000000000000000000000
--- a/service/v1Chat/reqModel/apiReq.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package reqModel
-
-type ApiReq struct {
- Messages []ApiMessage `json:"messages"`
- Model string `json:"model"`
- Stream bool `json:"stream"`
- PluginIds []string `json:"plugin_ids"`
- NewMessages string `json:"-"`
-}
-
-type ApiMessage struct {
- Role string `json:"role"`
- Content string `json:"content"`
-}
diff --git a/service/v1Chat/reqModel/chatReq.go b/service/v1Chat/reqModel/chatReq.go
deleted file mode 100644
index 41e7c34df62d734d8dd76c0f1b43ab21548fdf55..0000000000000000000000000000000000000000
--- a/service/v1Chat/reqModel/chatReq.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package reqModel
-
-type ChatAuthor struct {
- Role string `json:"role"`
-}
-
-type ChatContent struct {
- ContentType string `json:"content_type"`
- Parts []string `json:"parts"`
-}
-
-type ChatMessages struct {
- Author ChatAuthor `json:"author"`
- Content ChatContent `json:"content"`
-}
-
-type ChatConversationMode struct {
- Kind string `json:"kind"`
-}
-
-type ChatReq35 struct {
- Action string `json:"action"`
- Messages []ChatMessages `json:"messages"`
- ParentMessageId string `json:"parent_message_id"`
- Model string `json:"model"`
- TimeZoneOffsetMin int `json:"timezone_offset_min"`
- Suggestions []string `json:"suggestions"`
- HistoryAndTrainingDisabled bool `json:"history_and_training_disabled"`
- ConversationMode ChatConversationMode `json:"conversation_mode"`
- WebsocketRequestId string `json:"websocket_request_id"`
-}
diff --git a/service/v1Chat/respModel/apiRespJson.go b/service/v1Chat/respModel/apiRespJson.go
deleted file mode 100644
index e6ca66afb59e7cd923ffb5885cb0a37274564992..0000000000000000000000000000000000000000
--- a/service/v1Chat/respModel/apiRespJson.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package respModel
-
-import "time"
-
-type ApiRespJson struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- Model string `json:"model"`
- Usage ApiRespJsonUsage `json:"usage"`
- Choices []ApiRespJsonChoice `json:"choices"`
-}
-
-type ApiRespJsonMessage struct {
- Role string `json:"role"`
- Content string `json:"content"`
-}
-
-type ApiRespJsonChoice struct {
- Message ApiRespJsonMessage `json:"message"`
- FinishReason string `json:"finish_reason"`
- Index int `json:"index"`
-}
-
-type ApiRespJsonUsage struct {
- PromptTokens int `json:"prompt_tokens"`
- CompletionTokens int `json:"completion_tokens"`
- TotalTokens int `json:"total_tokens"`
-}
-
-func NewApiRespJson(id string, model string, content string) *ApiRespJson {
- apiRespObj := &ApiRespJson{
- ID: id,
- Created: time.Now().Unix(),
- Object: "chat.completion",
- Model: model,
- Usage: ApiRespJsonUsage{
- PromptTokens: 0,
- CompletionTokens: 0,
- TotalTokens: 0,
- },
- Choices: []ApiRespJsonChoice{
- {
- Message: ApiRespJsonMessage{
- Role: "assistant",
- Content: content,
- },
- FinishReason: "stop",
- Index: 0,
- },
- },
- }
- return apiRespObj
-}
\ No newline at end of file
diff --git a/service/v1Chat/respModel/apiRespStream.go b/service/v1Chat/respModel/apiRespStream.go
deleted file mode 100644
index fad990741116fbedf3c76d9b990b04453db35808..0000000000000000000000000000000000000000
--- a/service/v1Chat/respModel/apiRespStream.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package respModel
-
-import "time"
-
-// ApiRespStream represents the JSON structure
-type ApiRespStream struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- Model string `json:"model"`
- Choices []ApiStreamChoice `json:"choices"`
-}
-
-// ApiStreamChoice represents the nested "choices" object in the JSON
-type ApiStreamChoice struct {
- Delta ApiStreamDelta `json:"delta"`
- Index int `json:"index"`
- FinishReason string `json:"finish_reason"`
-}
-
-// ApiStreamDelta represents the nested "delta" object in the JSON
-type ApiStreamDelta struct {
- Content string `json:"content"`
-}
-
-func NewApiRespStream(id string, model string, content string, finishReason string) *ApiRespStream {
- // 生成响应 model
- apiRespStream := &ApiRespStream{
- ID: id,
- Created: time.Now().Unix(),
- Object: "chat.completion.chunk",
- Model: model,
- Choices: []ApiStreamChoice{
- {
- Delta: ApiStreamDelta{
- Content: content,
- },
- FinishReason: finishReason,
- },
- },
- }
- return apiRespStream
-}
diff --git a/service/v1Chat/respModel/chatResp.go b/service/v1Chat/respModel/chatResp.go
deleted file mode 100644
index b54ccd01e79b3ba87968e7db9b46ea2142ff5ff5..0000000000000000000000000000000000000000
--- a/service/v1Chat/respModel/chatResp.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package respModel
-
-type ChatResp35 struct {
- Message struct {
- Id string `json:"id"`
- Author struct {
- Role string `json:"role"`
- Name interface{} `json:"name"`
- Metadata struct {
- } `json:"metadata"`
- } `json:"author"`
- CreateTime float64 `json:"create_time"`
- UpdateTime interface{} `json:"update_time"`
- Content struct {
- ContentType string `json:"content_type"`
- Parts []string `json:"parts"`
- } `json:"content"`
- Status string `json:"status"`
- EndTurn interface{} `json:"end_turn"`
- Weight float64 `json:"weight"`
- Metadata struct {
- Citations []interface{} `json:"citations"`
- GizmoId interface{} `json:"gizmo_id"`
- MessageType string `json:"message_type"`
- ModelSlug string `json:"model_slug"`
- DefaultModelSlug string `json:"default_model_slug"`
- Pad string `json:"pad"`
- ParentId string `json:"parent_id"`
- } `json:"metadata"`
- Recipient string `json:"recipient"`
- } `json:"message"`
- ConversationId string `json:"conversation_id"`
- Error interface{} `json:"error"`
- // 审核
- Type string `json:"type"`
- MessageId string `json:"message_id"`
- IsCompletion bool `json:"is_completion"`
- ModerationResponse struct {
- Flagged bool `json:"flagged"`
- Disclaimers []interface{} `json:"disclaimers"`
- Blocked bool `json:"blocked"`
- ModerationId string `json:"moderation_id"`
- } `json:"moderation_response"`
-}
diff --git a/src/api/consts/exceptions.ts b/src/api/consts/exceptions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a54a4a58050035d2c361d371f1594ff3740d05fe
--- /dev/null
+++ b/src/api/consts/exceptions.ts
@@ -0,0 +1,11 @@
+export default {
+ API_TEST: [-9999, 'API异常错误'],
+ API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'],
+ API_REQUEST_FAILED: [-2001, '请求失败'],
+ API_TOKEN_EXPIRES: [-2002, 'Token已失效'],
+ API_FILE_URL_INVALID: [-2003, '远程文件URL非法'],
+ API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'],
+ API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'],
+ API_CONTENT_FILTERED: [-2006, '内容由于合规问题已被阻止生成'],
+ API_IMAGE_GENERATION_FAILED: [-2007, '图像生成失败']
+}
\ No newline at end of file
diff --git a/src/api/controllers/chat.ts b/src/api/controllers/chat.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a6b98e9784d5a64a293f22a14075e37ab3e28996
--- /dev/null
+++ b/src/api/controllers/chat.ts
@@ -0,0 +1,1183 @@
+import { PassThrough } from "stream";
+import path from "path";
+import _ from "lodash";
+import mime from "mime";
+import FormData from "form-data";
+import axios, { AxiosResponse } from "axios";
+
+import APIException from "@/lib/exceptions/APIException.ts";
+import EX from "@/api/consts/exceptions.ts";
+import { createParser } from "eventsource-parser";
+import logger from "@/lib/logger.ts";
+import util from "@/lib/util.ts";
+
+// 模型名称
+const MODEL_NAME = "glm";
+// 默认的智能体ID,GLM4
+const DEFAULT_ASSISTANT_ID = "65940acff94777010aa6b796";
+// access_token有效期
+const ACCESS_TOKEN_EXPIRES = 3600;
+// 最大重试次数
+const MAX_RETRY_COUNT = 3;
+// 重试延迟
+const RETRY_DELAY = 5000;
+// 伪装headers
+const FAKE_HEADERS = {
+ Accept: "*/*",
+ "App-Name": "chatglm",
+ Platform: "pc",
+ Origin: "https://chatglm.cn",
+ "Sec-Ch-Ua":
+ '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
+ "Sec-Ch-Ua-Mobile": "?0",
+ "Sec-Ch-Ua-Platform": '"Windows"',
+ "User-Agent":
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
+ Version: "0.0.1",
+};
+// 文件最大大小
+const FILE_MAX_SIZE = 100 * 1024 * 1024;
+// access_token映射
+const accessTokenMap = new Map();
+// access_token请求队列映射
+const accessTokenRequestQueueMap: Record = {};
+
+/**
+ * 请求access_token
+ *
+ * 使用refresh_token去刷新获得access_token
+ *
+ * @param refreshToken 用于刷新access_token的refresh_token
+ */
+async function requestToken(refreshToken: string) {
+ if (accessTokenRequestQueueMap[refreshToken])
+ return new Promise((resolve) =>
+ accessTokenRequestQueueMap[refreshToken].push(resolve)
+ );
+ accessTokenRequestQueueMap[refreshToken] = [];
+ logger.info(`Refresh token: ${refreshToken}`);
+ const result = await (async () => {
+ const result = await axios.post(
+ "https://chatglm.cn/chatglm/backend-api/v1/user/refresh",
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${refreshToken}`,
+ Referer: "https://chatglm.cn/main/alltoolsdetail",
+ "X-Device-Id": util.uuid(false),
+ "X-Request-Id": util.uuid(false),
+ ...FAKE_HEADERS,
+ },
+ timeout: 15000,
+ validateStatus: () => true,
+ }
+ );
+ const { result: _result } = checkResult(result, refreshToken);
+ const { accessToken } = _result;
+ return {
+ accessToken,
+ refreshToken,
+ refreshTime: util.unixTimestamp() + ACCESS_TOKEN_EXPIRES,
+ };
+ })()
+ .then((result) => {
+ if (accessTokenRequestQueueMap[refreshToken]) {
+ accessTokenRequestQueueMap[refreshToken].forEach((resolve) =>
+ resolve(result)
+ );
+ delete accessTokenRequestQueueMap[refreshToken];
+ }
+ logger.success(`Refresh successful`);
+ return result;
+ })
+ .catch((err) => {
+ if (accessTokenRequestQueueMap[refreshToken]) {
+ accessTokenRequestQueueMap[refreshToken].forEach((resolve) =>
+ resolve(err)
+ );
+ delete accessTokenRequestQueueMap[refreshToken];
+ }
+ return err;
+ });
+ if (_.isError(result)) throw result;
+ return result;
+}
+
+/**
+ * 获取缓存中的access_token
+ *
+ * 避免短时间大量刷新token,未加锁,如果有并发要求还需加锁
+ *
+ * @param refreshToken 用于刷新access_token的refresh_token
+ */
+async function acquireToken(refreshToken: string): Promise {
+ let result = accessTokenMap.get(refreshToken);
+ if (!result) {
+ result = await requestToken(refreshToken);
+ accessTokenMap.set(refreshToken, result);
+ }
+ if (util.unixTimestamp() > result.refreshTime) {
+ result = await requestToken(refreshToken);
+ accessTokenMap.set(refreshToken, result);
+ }
+ return result.accessToken;
+}
+
+/**
+ * 移除会话
+ *
+ * 在对话流传输完毕后移除会话,避免创建的会话出现在用户的对话列表中
+ *
+ * @param refreshToken 用于刷新access_token的refresh_token
+ */
+async function removeConversation(
+ convId: string,
+ refreshToken: string,
+ assistantId = DEFAULT_ASSISTANT_ID
+) {
+ const token = await acquireToken(refreshToken);
+
+ const result = await axios.post(
+ "https://chatglm.cn/chatglm/backend-api/assistant/conversation/delete",
+ {
+ assistant_id: assistantId,
+ conversation_id: convId,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ Referer: `https://chatglm.cn/main/alltoolsdetail`,
+ "X-Device-Id": util.uuid(false),
+ "X-Request-Id": util.uuid(false),
+ ...FAKE_HEADERS,
+ },
+ timeout: 15000,
+ validateStatus: () => true,
+ }
+ );
+ checkResult(result, refreshToken);
+}
+
+/**
+ * 同步对话补全
+ *
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
+ * @param refreshToken 用于刷新access_token的refresh_token
+ * @param assistantId 智能体ID,默认使用GLM4原版
+ * @param retryCount 重试次数
+ */
+async function createCompletion(
+ messages: any[],
+ refreshToken: string,
+ assistantId = DEFAULT_ASSISTANT_ID,
+ refConvId = '',
+ retryCount = 0
+) {
+ return (async () => {
+ logger.info(messages);
+
+ // 提取引用文件URL并上传获得引用的文件ID列表
+ const refFileUrls = extractRefFileUrls(messages);
+ const refs = refFileUrls.length
+ ? await Promise.all(
+ refFileUrls.map((fileUrl) => uploadFile(fileUrl, refreshToken))
+ )
+ : [];
+
+ // 如果引用对话ID不正确则重置引用
+ if (!/[0-9a-zA-Z]{24}/.test(refConvId))
+ refConvId = '';
+
+ // 请求流
+ const token = await acquireToken(refreshToken);
+ const result = await axios.post(
+ "https://chatglm.cn/chatglm/backend-api/assistant/stream",
+ {
+ assistant_id: assistantId,
+ conversation_id: refConvId,
+ messages: messagesPrepare(messages, refs, !!refConvId),
+ meta_data: {
+ channel: "",
+ draft_id: "",
+ input_question_type: "xxxx",
+ is_test: false,
+ },
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ Referer:
+ assistantId == DEFAULT_ASSISTANT_ID
+ ? "https://chatglm.cn/main/alltoolsdetail"
+ : `https://chatglm.cn/main/gdetail/${assistantId}`,
+ "X-Device-Id": util.uuid(false),
+ "X-Request-Id": util.uuid(false),
+ ...FAKE_HEADERS,
+ },
+ // 120秒超时
+ timeout: 120000,
+ validateStatus: () => true,
+ responseType: "stream",
+ }
+ );
+ if (result.headers["content-type"].indexOf("text/event-stream") == -1) {
+ result.data.on("data", buffer => logger.error(buffer.toString()));
+ throw new APIException(
+ EX.API_REQUEST_FAILED,
+ `Stream response Content-Type invalid: ${result.headers["content-type"]}`
+ );
+ }
+
+ const streamStartTime = util.timestamp();
+ // 接收流为输出文本
+ const answer = await receiveStream(result.data);
+ logger.success(
+ `Stream has completed transfer ${util.timestamp() - streamStartTime}ms`
+ );
+
+ // 异步移除会话
+ removeConversation(answer.id, refreshToken, assistantId).catch((err) =>
+ !refConvId && console.error(err)
+ );
+
+ return answer;
+ })().catch((err) => {
+ if (retryCount < MAX_RETRY_COUNT) {
+ logger.error(`Stream response error: ${err.stack}`);
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
+ return (async () => {
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
+ return createCompletion(
+ messages,
+ refreshToken,
+ assistantId,
+ refConvId,
+ retryCount + 1
+ );
+ })();
+ }
+ throw err;
+ });
+}
+
+/**
+ * 流式对话补全
+ *
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
+ * @param refreshToken 用于刷新access_token的refresh_token
+ * @param assistantId 智能体ID,默认使用GLM4原版
+ * @param retryCount 重试次数
+ */
+async function createCompletionStream(
+ messages: any[],
+ refreshToken: string,
+ assistantId = DEFAULT_ASSISTANT_ID,
+ refConvId = '',
+ retryCount = 0
+) {
+ return (async () => {
+ logger.info(messages);
+
+ // 提取引用文件URL并上传获得引用的文件ID列表
+ const refFileUrls = extractRefFileUrls(messages);
+ const refs = refFileUrls.length
+ ? await Promise.all(
+ refFileUrls.map((fileUrl) => uploadFile(fileUrl, refreshToken))
+ )
+ : [];
+
+ // 如果引用对话ID不正确则重置引用
+ if (!/[0-9a-zA-Z]{24}/.test(refConvId))
+ refConvId = '';
+
+ // 请求流
+ const token = await acquireToken(refreshToken);
+ const result = await axios.post(
+ `https://chatglm.cn/chatglm/backend-api/assistant/stream`,
+ {
+ assistant_id: assistantId,
+ conversation_id: refConvId,
+ messages: messagesPrepare(messages, refs, !!refConvId),
+ meta_data: {
+ channel: "",
+ draft_id: "",
+ input_question_type: "xxxx",
+ is_test: false,
+ },
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ Referer:
+ assistantId == DEFAULT_ASSISTANT_ID
+ ? "https://chatglm.cn/main/alltoolsdetail"
+ : `https://chatglm.cn/main/gdetail/${assistantId}`,
+ "X-Device-Id": util.uuid(false),
+ "X-Request-Id": util.uuid(false),
+ ...FAKE_HEADERS,
+ },
+ // 120秒超时
+ timeout: 120000,
+ validateStatus: () => true,
+ responseType: "stream",
+ }
+ );
+
+ if (result.headers["content-type"].indexOf("text/event-stream") == -1) {
+ logger.error(
+ `Invalid response Content-Type:`,
+ result.headers["content-type"]
+ );
+ result.data.on("data", buffer => logger.error(buffer.toString()));
+ const transStream = new PassThrough();
+ transStream.end(
+ `data: ${JSON.stringify({
+ id: "",
+ model: MODEL_NAME,
+ object: "chat.completion.chunk",
+ choices: [
+ {
+ index: 0,
+ delta: {
+ role: "assistant",
+ content: "服务暂时不可用,第三方响应错误",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
+ created: util.unixTimestamp(),
+ })}\n\n`
+ );
+ return transStream;
+ }
+
+ const streamStartTime = util.timestamp();
+ // 创建转换流将消息格式转换为gpt兼容格式
+ return createTransStream(result.data, (convId: string) => {
+ logger.success(
+ `Stream has completed transfer ${util.timestamp() - streamStartTime}ms`
+ );
+ // 流传输结束后异步移除会话
+ removeConversation(convId, refreshToken, assistantId).catch((err) =>
+ !refConvId && console.error(err)
+ );
+ });
+ })().catch((err) => {
+ if (retryCount < MAX_RETRY_COUNT) {
+ logger.error(`Stream response error: ${err.stack}`);
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
+ return (async () => {
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
+ return createCompletionStream(
+ messages,
+ refreshToken,
+ assistantId,
+ refConvId,
+ retryCount + 1
+ );
+ })();
+ }
+ throw err;
+ });
+}
+
+async function generateImages(
+ model = "65a232c082ff90a2ad2f15e2",
+ prompt: string,
+ refreshToken: string,
+ retryCount = 0
+) {
+ return (async () => {
+ logger.info(prompt);
+ const messages = [
+ { role: "user", content: prompt.indexOf('画') == -1 ? `请画:${prompt}` : prompt },
+ ];
+ // 请求流
+ const token = await acquireToken(refreshToken);
+ const result = await axios.post(
+ "https://chatglm.cn/chatglm/backend-api/assistant/stream",
+ {
+ assistant_id: model,
+ conversation_id: "",
+ messages: messagesPrepare(messages, []),
+ meta_data: {
+ channel: "",
+ draft_id: "",
+ input_question_type: "xxxx",
+ is_test: false,
+ },
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ Referer: `https://chatglm.cn/main/gdetail/${model}`,
+ "X-Device-Id": util.uuid(false),
+ "X-Request-Id": util.uuid(false),
+ ...FAKE_HEADERS,
+ },
+ // 120秒超时
+ timeout: 120000,
+ validateStatus: () => true,
+ responseType: "stream",
+ }
+ );
+
+ if (result.headers["content-type"].indexOf("text/event-stream") == -1)
+ throw new APIException(
+ EX.API_REQUEST_FAILED,
+ `Stream response Content-Type invalid: ${result.headers["content-type"]}`
+ );
+
+ const streamStartTime = util.timestamp();
+ // 接收流为输出文本
+ const { convId, imageUrls } = await receiveImages(result.data);
+ logger.success(
+ `Stream has completed transfer ${util.timestamp() - streamStartTime}ms`
+ );
+
+ // 异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略
+ removeConversation(convId, refreshToken, model).catch((err) =>
+ console.error(err)
+ );
+
+ if (imageUrls.length == 0)
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED);
+
+ return imageUrls;
+ })().catch((err) => {
+ if (retryCount < MAX_RETRY_COUNT) {
+ logger.error(`Stream response error: ${err.message}`);
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
+ return (async () => {
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
+ return generateImages(model, prompt, refreshToken, retryCount + 1);
+ })();
+ }
+ throw err;
+ });
+}
+
+/**
+ * 提取消息中引用的文件URL
+ *
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
+ */
+function extractRefFileUrls(messages: any[]) {
+ const urls = [];
+ // 如果没有消息,则返回[]
+ if (!messages.length) {
+ return urls;
+ }
+ // 只获取最新的消息
+ const lastMessage = messages[messages.length - 1];
+ if (_.isArray(lastMessage.content)) {
+ lastMessage.content.forEach((v) => {
+ if (!_.isObject(v) || !["file", "image_url"].includes(v["type"])) return;
+ // glm-free-api支持格式
+ if (
+ v["type"] == "file" &&
+ _.isObject(v["file_url"]) &&
+ _.isString(v["file_url"]["url"])
+ )
+ urls.push(v["file_url"]["url"]);
+ // 兼容gpt-4-vision-preview API格式
+ else if (
+ v["type"] == "image_url" &&
+ _.isObject(v["image_url"]) &&
+ _.isString(v["image_url"]["url"])
+ )
+ urls.push(v["image_url"]["url"]);
+ });
+ }
+ logger.info("本次请求上传:" + urls.length + "个文件");
+ return urls;
+}
+
+/**
+ * 消息预处理
+ *
+ * 由于接口只取第一条消息,此处会将多条消息合并为一条,实现多轮对话效果
+ *
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
+ * @param refs 参考文件列表
+ * @param isRefConv 是否为引用会话
+ */
+function messagesPrepare(messages: any[], refs: any[], isRefConv = false) {
+ let content;
+ if (isRefConv || messages.length < 2) {
+ content = messages.reduce((content, message) => {
+ if (_.isArray(message.content)) {
+ return (
+ message.content.reduce((_content, v) => {
+ if (!_.isObject(v) || v["type"] != "text") return _content;
+ return _content + (v["text"] || "") + "\n";
+ }, content)
+ );
+ }
+ return content + `${message.content}\n`;
+ }, "");
+ logger.info("\n透传内容:\n" + content);
+ }
+ else {
+ // 检查最新消息是否含有"type": "image_url"或"type": "file",如果有则注入消息
+ let latestMessage = messages[messages.length - 1];
+ let hasFileOrImage =
+ Array.isArray(latestMessage.content) &&
+ latestMessage.content.some(
+ (v) => typeof v === "object" && ["file", "image_url"].includes(v["type"])
+ );
+ if (hasFileOrImage) {
+ let newFileMessage = {
+ content: "关注用户最新发送文件和消息",
+ role: "system",
+ };
+ messages.splice(messages.length - 1, 0, newFileMessage);
+ logger.info("注入提升尾部文件注意力system prompt");
+ } else {
+ // 由于注入会导致设定污染,暂时注释
+ // let newTextMessage = {
+ // content: "关注用户最新的消息",
+ // role: "system",
+ // };
+ // messages.splice(messages.length - 1, 0, newTextMessage);
+ // logger.info("注入提升尾部消息注意力system prompt");
+ }
+ content = (
+ messages.reduce((content, message) => {
+ const role = message.role
+ .replace("system", "<|sytstem|>")
+ .replace("assistant", "<|assistant|>")
+ .replace("user", "<|user|>");
+ if (_.isArray(message.content)) {
+ return (
+ message.content.reduce((_content, v) => {
+ if (!_.isObject(v) || v["type"] != "text") return _content;
+ return _content + (`${role}\n` + v["text"] || "") + "\n";
+ }, content)
+ );
+ }
+ return (content += `${role}\n${message.content}\n`);
+ }, "") + "<|assistant|>\n"
+ )
+ // 移除MD图像URL避免幻觉
+ .replace(/\!\[.+\]\(.+\)/g, "")
+ // 移除临时路径避免在新会话引发幻觉
+ .replace(/\/mnt\/data\/.+/g, "");
+ logger.info("\n对话合并:\n" + content);
+ }
+
+ const fileRefs = refs.filter((ref) => !ref.width && !ref.height);
+ const imageRefs = refs
+ .filter((ref) => ref.width || ref.height)
+ .map((ref) => {
+ ref.image_url = ref.file_url;
+ return ref;
+ });
+ return [
+ {
+ role: "user",
+ content: [
+ { type: "text", text: content },
+ ...(fileRefs.length == 0
+ ? []
+ : [
+ {
+ type: "file",
+ file: fileRefs,
+ },
+ ]),
+ ...(imageRefs.length == 0
+ ? []
+ : [
+ {
+ type: "image",
+ image: imageRefs,
+ },
+ ]),
+ ],
+ },
+ ];
+}
+
+/**
+ * 预检查文件URL有效性
+ *
+ * @param fileUrl 文件URL
+ */
+async function checkFileUrl(fileUrl: string) {
+ if (util.isBASE64Data(fileUrl)) return;
+ const result = await axios.head(fileUrl, {
+ timeout: 15000,
+ validateStatus: () => true,
+ });
+ if (result.status >= 400)
+ throw new APIException(
+ EX.API_FILE_URL_INVALID,
+ `File ${fileUrl} is not valid: [${result.status}] ${result.statusText}`
+ );
+ // 检查文件大小
+ if (result.headers && result.headers["content-length"]) {
+ const fileSize = parseInt(result.headers["content-length"], 10);
+ if (fileSize > FILE_MAX_SIZE)
+ throw new APIException(
+ EX.API_FILE_EXECEEDS_SIZE,
+ `File ${fileUrl} is not valid`
+ );
+ }
+}
+
+/**
+ * 上传文件
+ *
+ * @param fileUrl 文件URL
+ * @param refreshToken 用于刷新access_token的refresh_token
+ */
+async function uploadFile(fileUrl: string, refreshToken: string) {
+ // 预检查远程文件URL可用性
+ await checkFileUrl(fileUrl);
+
+ let filename, fileData, mimeType;
+ // 如果是BASE64数据则直接转换为Buffer
+ if (util.isBASE64Data(fileUrl)) {
+ mimeType = util.extractBASE64DataFormat(fileUrl);
+ const ext = mime.getExtension(mimeType);
+ filename = `${util.uuid()}.${ext}`;
+ fileData = Buffer.from(util.removeBASE64DataHeader(fileUrl), "base64");
+ }
+ // 下载文件到内存,如果您的服务器内存很小,建议考虑改造为流直传到下一个接口上,避免停留占用内存
+ else {
+ filename = path.basename(fileUrl);
+ ({ data: fileData } = await axios.get(fileUrl, {
+ responseType: "arraybuffer",
+ // 100M限制
+ maxContentLength: FILE_MAX_SIZE,
+ // 60秒超时
+ timeout: 60000,
+ }));
+ }
+
+ // 获取文件的MIME类型
+ mimeType = mimeType || mime.getType(filename);
+
+ const formData = new FormData();
+ formData.append("file", fileData, {
+ filename,
+ contentType: mimeType,
+ });
+
+ // 上传文件到目标OSS
+ const token = await acquireToken(refreshToken);
+ let result = await axios.request({
+ method: "POST",
+ url: "https://chatglm.cn/chatglm/backend-api/assistant/file_upload",
+ data: formData,
+ // 100M限制
+ maxBodyLength: FILE_MAX_SIZE,
+ // 60秒超时
+ timeout: 60000,
+ headers: {
+ Authorization: `Bearer ${token}`,
+ Referer: `https://chatglm.cn/`,
+ ...FAKE_HEADERS,
+ ...formData.getHeaders(),
+ },
+ validateStatus: () => true,
+ });
+ const { result: uploadResult } = checkResult(result, refreshToken);
+
+ return uploadResult;
+}
+
+/**
+ * 检查请求结果
+ *
+ * @param result 结果
+ */
+function checkResult(result: AxiosResponse, refreshToken: string) {
+ if (!result.data) return null;
+ const { code, status, message } = result.data;
+ if (!_.isFinite(code) && !_.isFinite(status)) return result.data;
+ if (code === 0 || status === 0) return result.data;
+ if (code == 401) accessTokenMap.delete(refreshToken);
+ throw new APIException(EX.API_REQUEST_FAILED, `[请求glm失败]: ${message}`);
+}
+
+/**
+ * 从流接收完整的消息内容
+ *
+ * @param stream 消息流
+ */
+async function receiveStream(stream: any): Promise {
+ return new Promise((resolve, reject) => {
+ // 消息初始化
+ const data = {
+ id: "",
+ model: MODEL_NAME,
+ object: "chat.completion",
+ choices: [
+ {
+ index: 0,
+ message: { role: "assistant", content: "" },
+ finish_reason: "stop",
+ },
+ ],
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
+ created: util.unixTimestamp(),
+ };
+ let toolCall = false;
+ let codeGenerating = false;
+ let textChunkLength = 0;
+ let codeTemp = "";
+ let lastExecutionOutput = "";
+ let textOffset = 0;
+ let refContent = '';
+ const parser = createParser((event) => {
+ try {
+ if (event.type !== "event") return;
+ // 解析JSON
+ const result = _.attempt(() => JSON.parse(event.data));
+ if (_.isError(result))
+ throw new Error(`Stream response invalid: ${event.data}`);
+ if (!data.id && result.conversation_id)
+ data.id = result.conversation_id;
+ if (result.status != "finish") {
+ const text = result.parts.reduce((str, part) => {
+ const { status, content, meta_data } = part;
+ if (!_.isArray(content)) return str;
+ const partText = content.reduce((innerStr, value) => {
+ const {
+ status: partStatus,
+ type,
+ text,
+ image,
+ code,
+ content,
+ } = value;
+ if (partStatus == "init" && textChunkLength > 0) {
+ textOffset += textChunkLength + 1;
+ textChunkLength = 0;
+ innerStr += "\n";
+ }
+ if (type == "text") {
+ if (toolCall) {
+ innerStr += "\n";
+ textOffset++;
+ toolCall = false;
+ }
+ if (partStatus == "finish") textChunkLength = text.length;
+ return innerStr + text;
+ } else if (
+ type == "quote_result" &&
+ status == "finish" &&
+ meta_data &&
+ _.isArray(meta_data.metadata_list)
+ ) {
+ refContent = meta_data.metadata_list.reduce((meta, v) => {
+ return meta + `${v.title} - ${v.url}\n`;
+ }, refContent);
+ } else if (
+ type == "image" &&
+ _.isArray(image) &&
+ status == "finish"
+ ) {
+ const imageText =
+ image.reduce(
+ (imgs, v) =>
+ imgs +
+ (/^(http|https):\/\//.test(v.image_url)
+ ? ``
+ : ""),
+ ""
+ ) + "\n";
+ textOffset += imageText.length;
+ toolCall = true;
+ return innerStr + imageText;
+ } else if (type == "code" && partStatus == "init") {
+ let codeHead = "";
+ if (!codeGenerating) {
+ codeGenerating = true;
+ codeHead = "```python\n";
+ }
+ const chunk = code.substring(codeTemp.length, code.length);
+ codeTemp += chunk;
+ textOffset += codeHead.length + chunk.length;
+ return innerStr + codeHead + chunk;
+ } else if (
+ type == "code" &&
+ partStatus == "finish" &&
+ codeGenerating
+ ) {
+ const codeFooter = "\n```\n";
+ codeGenerating = false;
+ codeTemp = "";
+ textOffset += codeFooter.length;
+ return innerStr + codeFooter;
+ } else if (
+ type == "execution_output" &&
+ _.isString(content) &&
+ partStatus == "done" &&
+ lastExecutionOutput != content
+ ) {
+ lastExecutionOutput = content;
+ const _content = content.replace(/^\n/, "");
+ textOffset += _content.length + 1;
+ return innerStr + _content + "\n";
+ }
+ return innerStr;
+ }, "");
+ return str + partText;
+ }, "");
+ const chunk = text.substring(
+ data.choices[0].message.content.length - textOffset,
+ text.length
+ );
+ data.choices[0].message.content += chunk;
+ } else {
+ data.choices[0].message.content =
+ data.choices[0].message.content.replace(/【\d+†(来源|source)】/g, "") + (refContent ? `\n\n搜索结果来自:\n${refContent.replace(/\n$/, '')}` : '');
+ resolve(data);
+ }
+ } catch (err) {
+ logger.error(err);
+ reject(err);
+ }
+ });
+ // 将流数据喂给SSE转换器
+ stream.on("data", (buffer) => parser.feed(buffer.toString()));
+ stream.once("error", (err) => reject(err));
+ stream.once("close", () => resolve(data));
+ });
+}
+
+/**
+ * 创建转换流
+ *
+ * 将流格式转换为gpt兼容流格式
+ *
+ * @param stream 消息流
+ * @param endCallback 传输结束回调
+ */
+function createTransStream(stream: any, endCallback?: Function) {
+ // 消息创建时间
+ const created = util.unixTimestamp();
+ // 创建转换流
+ const transStream = new PassThrough();
+ let content = "";
+ let toolCall = false;
+ let codeGenerating = false;
+ let textChunkLength = 0;
+ let codeTemp = "";
+ let lastExecutionOutput = "";
+ let textOffset = 0;
+ !transStream.closed &&
+ transStream.write(
+ `data: ${JSON.stringify({
+ id: "",
+ model: MODEL_NAME,
+ object: "chat.completion.chunk",
+ choices: [
+ {
+ index: 0,
+ delta: { role: "assistant", content: "" },
+ finish_reason: null,
+ },
+ ],
+ created,
+ })}\n\n`
+ );
+ const parser = createParser((event) => {
+ try {
+ if (event.type !== "event") return;
+ // 解析JSON
+ const result = _.attempt(() => JSON.parse(event.data));
+ if (_.isError(result))
+ throw new Error(`Stream response invalid: ${event.data}`);
+ if (result.status != "finish" && result.status != "intervene") {
+ const text = result.parts.reduce((str, part) => {
+ const { status, content, meta_data } = part;
+ if (!_.isArray(content)) return str;
+ const partText = content.reduce((innerStr, value) => {
+ const {
+ status: partStatus,
+ type,
+ text,
+ image,
+ code,
+ content,
+ } = value;
+ if (partStatus == "init" && textChunkLength > 0) {
+ textOffset += textChunkLength + 1;
+ textChunkLength = 0;
+ innerStr += "\n";
+ }
+ if (type == "text") {
+ if (toolCall) {
+ innerStr += "\n";
+ textOffset++;
+ toolCall = false;
+ }
+ if (partStatus == "finish") textChunkLength = text.length;
+ return innerStr + text;
+ } else if (
+ type == "quote_result" &&
+ status == "finish" &&
+ meta_data &&
+ _.isArray(meta_data.metadata_list)
+ ) {
+ const searchText =
+ meta_data.metadata_list.reduce(
+ (meta, v) => meta + `检索 ${v.title}(${v.url}) ...`,
+ ""
+ ) + "\n";
+ textOffset += searchText.length;
+ toolCall = true;
+ return innerStr + searchText;
+ } else if (
+ type == "image" &&
+ _.isArray(image) &&
+ status == "finish"
+ ) {
+ const imageText =
+ image.reduce(
+ (imgs, v) =>
+ imgs +
+ (/^(http|https):\/\//.test(v.image_url)
+ ? ``
+ : ""),
+ ""
+ ) + "\n";
+ textOffset += imageText.length;
+ toolCall = true;
+ return innerStr + imageText;
+ } else if (type == "code" && partStatus == "init") {
+ let codeHead = "";
+ if (!codeGenerating) {
+ codeGenerating = true;
+ codeHead = "```python\n";
+ }
+ const chunk = code.substring(codeTemp.length, code.length);
+ codeTemp += chunk;
+ textOffset += codeHead.length + chunk.length;
+ return innerStr + codeHead + chunk;
+ } else if (
+ type == "code" &&
+ partStatus == "finish" &&
+ codeGenerating
+ ) {
+ const codeFooter = "\n```\n";
+ codeGenerating = false;
+ codeTemp = "";
+ textOffset += codeFooter.length;
+ return innerStr + codeFooter;
+ } else if (
+ type == "execution_output" &&
+ _.isString(content) &&
+ partStatus == "done" &&
+ lastExecutionOutput != content
+ ) {
+ lastExecutionOutput = content;
+ textOffset += content.length + 1;
+ return innerStr + content + "\n";
+ }
+ return innerStr;
+ }, "");
+ return str + partText;
+ }, "");
+ const chunk = text.substring(content.length - textOffset, text.length);
+ if (chunk) {
+ content += chunk;
+ const data = `data: ${JSON.stringify({
+ id: result.conversation_id,
+ model: MODEL_NAME,
+ object: "chat.completion.chunk",
+ choices: [
+ { index: 0, delta: { content: chunk }, finish_reason: null },
+ ],
+ created,
+ })}\n\n`;
+ !transStream.closed && transStream.write(data);
+ }
+ } else {
+ const data = `data: ${JSON.stringify({
+ id: result.conversation_id,
+ model: MODEL_NAME,
+ object: "chat.completion.chunk",
+ choices: [
+ {
+ index: 0,
+ delta:
+ result.status == "intervene" &&
+ result.last_error &&
+ result.last_error.intervene_text
+ ? { content: `\n\n${result.last_error.intervene_text}` }
+ : {},
+ finish_reason: "stop",
+ },
+ ],
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
+ created,
+ })}\n\n`;
+ !transStream.closed && transStream.write(data);
+ !transStream.closed && transStream.end("data: [DONE]\n\n");
+ content = "";
+ endCallback && endCallback(result.conversation_id);
+ }
+ } catch (err) {
+ logger.error(err);
+ !transStream.closed && transStream.end("\n\n");
+ }
+ });
+ // 将流数据喂给SSE转换器
+ stream.on("data", (buffer) => parser.feed(buffer.toString()));
+ stream.once(
+ "error",
+ () => !transStream.closed && transStream.end("data: [DONE]\n\n")
+ );
+ stream.once(
+ "close",
+ () => !transStream.closed && transStream.end("data: [DONE]\n\n")
+ );
+ return transStream;
+}
+
+/**
+ * 从流接收图像
+ *
+ * @param stream 消息流
+ */
+async function receiveImages(
+ stream: any
+): Promise<{ convId: string; imageUrls: string[] }> {
+ return new Promise((resolve, reject) => {
+ let convId = "";
+ const imageUrls = [];
+ const parser = createParser((event) => {
+ try {
+ if (event.type !== "event") return;
+ // 解析JSON
+ const result = _.attempt(() => JSON.parse(event.data));
+ if (_.isError(result))
+ throw new Error(`Stream response invalid: ${event.data}`);
+ if (!convId && result.conversation_id) convId = result.conversation_id;
+ if (result.status == "intervene")
+ throw new APIException(EX.API_CONTENT_FILTERED);
+ if (result.status != "finish") {
+ result.parts.forEach((part) => {
+ const { content } = part;
+ if (!_.isArray(content)) return;
+ content.forEach((value) => {
+ const { status: partStatus, type, image, text } = value;
+ if (
+ type == "image" &&
+ _.isArray(image) &&
+ partStatus == "finish"
+ ) {
+ image.forEach((value) => {
+ if (
+ !/^(http|https):\/\//.test(value.image_url) ||
+ imageUrls.indexOf(value.image_url) != -1
+ )
+ return;
+ imageUrls.push(value.image_url);
+ });
+ }
+ if (
+ type == "text" &&
+ partStatus == "finish"
+ ) {
+ const urlPattern = /\((https?:\/\/\S+)\)/g;
+ let match;
+ while ((match = urlPattern.exec(text)) !== null) {
+ const url = match[1];
+ if (imageUrls.indexOf(url) == -1)
+ imageUrls.push(url);
+ }
+ }
+ });
+ });
+ }
+ } catch (err) {
+ logger.error(err);
+ reject(err);
+ }
+ });
+ // 将流数据喂给SSE转换器
+ stream.on("data", (buffer) => parser.feed(buffer.toString()));
+ stream.once("error", (err) => reject(err));
+ stream.once("close", () =>
+ resolve({
+ convId,
+ imageUrls,
+ })
+ );
+ });
+}
+
+/**
+ * Token切分
+ *
+ * @param authorization 认证字符串
+ */
+function tokenSplit(authorization: string) {
+ return authorization.replace("Bearer ", "").split(",");
+}
+
+/**
+ * 备用生成cookie
+ *
+ * 暂时还不需要
+ *
+ * @param refreshToken
+ * @param token
+ */
+function generateCookie(refreshToken: string, token: string) {
+ const timestamp = util.unixTimestamp();
+ const gsTimestamp = timestamp - Math.round(Math.random() * 2592000);
+ return {
+ chatglm_refresh_token: refreshToken,
+ // chatglm_user_id: '',
+ _ga_PMD05MS2V9: `GS1.1.${gsTimestamp}.18.0.${gsTimestamp}.0.0.0`,
+ chatglm_token: token,
+ chatglm_token_expires: util.getDateString("yyyy-MM-dd HH:mm:ss"),
+ abtestid: "a",
+ // acw_tc: ''
+ };
+}
+
+/**
+ * 获取Token存活状态
+ */
+async function getTokenLiveStatus(refreshToken: string) {
+ const result = await axios.post(
+ "https://chatglm.cn/chatglm/backend-api/v1/user/refresh",
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${refreshToken}`,
+ Referer: "https://chatglm.cn/main/alltoolsdetail",
+ "X-Device-Id": util.uuid(false),
+ "X-Request-Id": util.uuid(false),
+ ...FAKE_HEADERS,
+ },
+ timeout: 15000,
+ validateStatus: () => true,
+ }
+ );
+ try {
+ const { result: _result } = checkResult(result, refreshToken);
+ const { accessToken } = _result;
+ return !!accessToken;
+ }
+ catch (err) {
+ return false;
+ }
+}
+
+export default {
+ createCompletion,
+ createCompletionStream,
+ generateImages,
+ getTokenLiveStatus,
+ tokenSplit,
+};
diff --git a/src/api/routes/chat.ts b/src/api/routes/chat.ts
new file mode 100644
index 0000000000000000000000000000000000000000..12782c04e6a22db8bebef96462e00244270a0262
--- /dev/null
+++ b/src/api/routes/chat.ts
@@ -0,0 +1,37 @@
+import _ from 'lodash';
+
+import Request from '@/lib/request/Request.ts';
+import Response from '@/lib/response/Response.ts';
+import chat from '@/api/controllers/chat.ts';
+import logger from '@/lib/logger.ts';
+
+export default {
+
+ prefix: '/v1/chat',
+
+ post: {
+
+ '/completions': async (request: Request) => {
+ request
+ .validate('body.conversation_id', v => _.isUndefined(v) || _.isString(v))
+ .validate('body.messages', _.isArray)
+ .validate('headers.authorization', _.isString)
+ // refresh_token切分
+ const tokens = chat.tokenSplit(request.headers.authorization);
+ // 随机挑选一个refresh_token
+ const token = _.sample(tokens);
+ const { model, conversation_id: convId, messages, stream } = request.body;
+ const assistantId = /^[a-z0-9]{24,}$/.test(model) ? model : undefined
+ if (stream) {
+ const stream = await chat.createCompletionStream(messages, token, assistantId, convId);
+ return new Response(stream, {
+ type: "text/event-stream"
+ });
+ }
+ else
+ return await chat.createCompletion(messages, token, assistantId, convId);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/api/routes/images.ts b/src/api/routes/images.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2118b89048781951785a5234ed2bc41055a4fada
--- /dev/null
+++ b/src/api/routes/images.ts
@@ -0,0 +1,39 @@
+import _ from "lodash";
+
+import Request from "@/lib/request/Request.ts";
+import chat from "@/api/controllers/chat.ts";
+import util from "@/lib/util.ts";
+
+export default {
+ prefix: "/v1/images",
+
+ post: {
+ "/generations": async (request: Request) => {
+ request
+ .validate("body.prompt", _.isString)
+ .validate("headers.authorization", _.isString);
+ // refresh_token切分
+ const tokens = chat.tokenSplit(request.headers.authorization);
+ // 随机挑选一个refresh_token
+ const token = _.sample(tokens);
+ const prompt = request.body.prompt;
+ const responseFormat = _.defaultTo(request.body.response_format, "url");
+ const assistantId = /^[a-z0-9]{24,}$/.test(request.body.model) ? request.body.model : undefined
+ const imageUrls = await chat.generateImages(assistantId, prompt, token);
+ let data = [];
+ if (responseFormat == "b64_json") {
+ data = (
+ await Promise.all(imageUrls.map((url) => util.fetchFileBASE64(url)))
+ ).map((b64) => ({ b64_json: b64 }));
+ } else {
+ data = imageUrls.map((url) => ({
+ url,
+ }));
+ }
+ return {
+ created: util.unixTimestamp(),
+ data,
+ };
+ },
+ },
+};
diff --git a/src/api/routes/index.ts b/src/api/routes/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..16738dde4008f045623c18fa30c281a011704ce0
--- /dev/null
+++ b/src/api/routes/index.ts
@@ -0,0 +1,29 @@
+import fs from 'fs-extra';
+
+import Response from '@/lib/response/Response.ts';
+import chat from "./chat.ts";
+import images from "./images.ts";
+import ping from "./ping.ts";
+import token from './token.js';
+import models from './models.ts';
+
+export default [
+ {
+ get: {
+ '/': async () => {
+ const content = await fs.readFile('public/welcome.html');
+ return new Response(content, {
+ type: 'html',
+ headers: {
+ Expires: '-1'
+ }
+ });
+ }
+ }
+ },
+ chat,
+ images,
+ ping,
+ token,
+ models
+];
\ No newline at end of file
diff --git a/src/api/routes/models.ts b/src/api/routes/models.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f776cb0d2b0f3f99519cd8496b993de34b6a823e
--- /dev/null
+++ b/src/api/routes/models.ts
@@ -0,0 +1,41 @@
+import _ from 'lodash';
+
+export default {
+
+ prefix: '/v1',
+
+ get: {
+ '/models': async () => {
+ return {
+ "data": [
+ {
+ "id": "glm-3-turbo",
+ "object": "model",
+ "owned_by": "glm-free-api"
+ },
+ {
+ "id": "glm-4",
+ "object": "model",
+ "owned_by": "glm-free-api"
+ },
+ {
+ "id": "glm-4v",
+ "object": "model",
+ "owned_by": "glm-free-api"
+ },
+ {
+ "id": "glm-v1",
+ "object": "model",
+ "owned_by": "glm-free-api"
+ },
+ {
+ "id": "glm-v1-vision",
+ "object": "model",
+ "owned_by": "glm-free-api"
+ }
+ ]
+ };
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/api/routes/ping.ts b/src/api/routes/ping.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dc9af728ac4f9a9d05c4e0118919a16f1aadbc68
--- /dev/null
+++ b/src/api/routes/ping.ts
@@ -0,0 +1,6 @@
+export default {
+ prefix: '/ping',
+ get: {
+ '': async () => "pong"
+ }
+}
\ No newline at end of file
diff --git a/src/api/routes/token.ts b/src/api/routes/token.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c69c512df3a49005efeb4711925be586473c6456
--- /dev/null
+++ b/src/api/routes/token.ts
@@ -0,0 +1,25 @@
+import _ from 'lodash';
+
+import Request from '@/lib/request/Request.ts';
+import Response from '@/lib/response/Response.ts';
+import chat from '@/api/controllers/chat.ts';
+import logger from '@/lib/logger.ts';
+
+export default {
+
+ prefix: '/token',
+
+ post: {
+
+ '/check': async (request: Request) => {
+ request
+ .validate('body.token', _.isString)
+ const live = await chat.getTokenLiveStatus(request.body.token);
+ return {
+ live
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/daemon.ts b/src/daemon.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0c5fe69c7334512887a1cde53dbf18f68829f3ea
--- /dev/null
+++ b/src/daemon.ts
@@ -0,0 +1,82 @@
+/**
+ * 守护进程
+ */
+
+import process from 'process';
+import path from 'path';
+import { spawn } from 'child_process';
+
+import fs from 'fs-extra';
+import { format as dateFormat } from 'date-fns';
+import 'colors';
+
+const CRASH_RESTART_LIMIT = 600; //进程崩溃重启次数限制
+const CRASH_RESTART_DELAY = 5000; //进程崩溃重启延迟
+const LOG_PATH = path.resolve("./logs/daemon.log"); //守护进程日志路径
+let crashCount = 0; //进程崩溃次数
+let currentProcess; //当前运行进程
+
+/**
+ * 写入守护进程日志
+ */
+function daemonLog(value, color?: string) {
+ try {
+ const head = `[daemon][${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")}] `;
+ value = head + value;
+ console.log(color ? value[color] : value);
+ fs.ensureDirSync(path.dirname(LOG_PATH));
+ fs.appendFileSync(LOG_PATH, value + "\n");
+ }
+ catch(err) {
+ console.error("daemon log write error:", err);
+ }
+}
+
+daemonLog(`daemon pid: ${process.pid}`);
+
+function createProcess() {
+ const childProcess = spawn("node", ["index.js", ...process.argv.slice(2)]); //启动子进程
+ childProcess.stdout.pipe(process.stdout, { end: false }); //将子进程输出管道到当前进程输出
+ childProcess.stderr.pipe(process.stderr, { end: false }); //将子进程错误输出管道到当前进程输出
+ currentProcess = childProcess; //更新当前进程
+ daemonLog(`process(${childProcess.pid}) has started`);
+ childProcess.on("error", err => daemonLog(`process(${childProcess.pid}) error: ${err.stack}`, "red"));
+ childProcess.on("close", code => {
+ if(code === 0) //进程正常退出
+ daemonLog(`process(${childProcess.pid}) has exited`);
+ else if(code === 2) //进程已被杀死
+ daemonLog(`process(${childProcess.pid}) has been killed!`, "bgYellow");
+ else if(code === 3) { //进程主动重启
+ daemonLog(`process(${childProcess.pid}) has restart`, "yellow");
+ createProcess(); //重新创建进程
+ }
+ else { //进程发生崩溃
+ if(crashCount++ < CRASH_RESTART_LIMIT) { //进程崩溃次数未达重启次数上限前尝试重启
+ daemonLog(`process(${childProcess.pid}) has crashed! delay ${CRASH_RESTART_DELAY}ms try restarting...(${crashCount})`, "bgRed");
+ setTimeout(() => createProcess(), CRASH_RESTART_DELAY); //延迟指定时长后再重启
+ }
+ else //进程已崩溃,且无法重启
+ daemonLog(`process(${childProcess.pid}) has crashed! unable to restart`, "bgRed");
+ }
+ }); //子进程关闭监听
+}
+
+process.on("exit", code => {
+ if(code === 0)
+ daemonLog("daemon process exited");
+ else if(code === 2)
+ daemonLog("daemon process has been killed!");
+}); //守护进程退出事件
+
+process.on("SIGTERM", () => {
+ daemonLog("received kill signal", "yellow");
+ currentProcess && currentProcess.kill("SIGINT");
+ process.exit(2);
+}); //kill退出守护进程
+
+process.on("SIGINT", () => {
+ currentProcess && currentProcess.kill("SIGINT");
+ process.exit(0);
+}); //主动退出守护进程
+
+createProcess(); //创建进程
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..60a0e9115b98bbb3df085e22409179bd2a347a8c
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,32 @@
+"use strict";
+
+import environment from "@/lib/environment.ts";
+import config from "@/lib/config.ts";
+import "@/lib/initialize.ts";
+import server from "@/lib/server.ts";
+import routes from "@/api/routes/index.ts";
+import logger from "@/lib/logger.ts";
+
+const startupTime = performance.now();
+
+(async () => {
+ logger.header();
+
+ logger.info("<<<< glm free server >>>>");
+ logger.info("Version:", environment.package.version);
+ logger.info("Process id:", process.pid);
+ logger.info("Environment:", environment.env);
+ logger.info("Service name:", config.service.name);
+
+ server.attachRoutes(routes);
+ await server.listen();
+
+ config.service.bindAddress &&
+ logger.success("Service bind address:", config.service.bindAddress);
+})()
+ .then(() =>
+ logger.success(
+ `Service startup completed (${Math.floor(performance.now() - startupTime)}ms)`
+ )
+ )
+ .catch((err) => console.error(err));
diff --git a/src/lib/config.ts b/src/lib/config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b6072d220ae2fa4f2943e4994f998b397faaa9c4
--- /dev/null
+++ b/src/lib/config.ts
@@ -0,0 +1,14 @@
+import serviceConfig from "./configs/service-config.ts";
+import systemConfig from "./configs/system-config.ts";
+
+class Config {
+
+ /** 服务配置 */
+ service = serviceConfig;
+
+ /** 系统配置 */
+ system = systemConfig;
+
+}
+
+export default new Config();
\ No newline at end of file
diff --git a/src/lib/configs/service-config.ts b/src/lib/configs/service-config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9618d4217fcf1c7d6b2078a4263b5d56ba67607a
--- /dev/null
+++ b/src/lib/configs/service-config.ts
@@ -0,0 +1,68 @@
+import path from 'path';
+
+import fs from 'fs-extra';
+import yaml from 'yaml';
+import _ from 'lodash';
+
+import environment from '../environment.ts';
+import util from '../util.ts';
+
+const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/service.yml");
+
+/**
+ * 服务配置
+ */
+export class ServiceConfig {
+
+ /** 服务名称 */
+ name: string;
+ /** @type {string} 服务绑定主机地址 */
+ host;
+ /** @type {number} 服务绑定端口 */
+ port;
+ /** @type {string} 服务路由前缀 */
+ urlPrefix;
+ /** @type {string} 服务绑定地址(外部访问地址) */
+ bindAddress;
+
+ constructor(options?: any) {
+ const { name, host, port, urlPrefix, bindAddress } = options || {};
+ this.name = _.defaultTo(name, 'glm-free-api');
+ this.host = _.defaultTo(host, '0.0.0.0');
+ this.port = _.defaultTo(port, 5566);
+ this.urlPrefix = _.defaultTo(urlPrefix, '');
+ this.bindAddress = bindAddress;
+ }
+
+ get addressHost() {
+ if(this.bindAddress) return this.bindAddress;
+ const ipAddresses = util.getIPAddressesByIPv4();
+ for(let ipAddress of ipAddresses) {
+ if(ipAddress === this.host)
+ return ipAddress;
+ }
+ return ipAddresses[0] || "127.0.0.1";
+ }
+
+ get address() {
+ return `${this.addressHost}:${this.port}`;
+ }
+
+ get pageDirUrl() {
+ return `http://127.0.0.1:${this.port}/page`;
+ }
+
+ get publicDirUrl() {
+ return `http://127.0.0.1:${this.port}/public`;
+ }
+
+ static load() {
+ const external = _.pickBy(environment, (v, k) => ["name", "host", "port"].includes(k) && !_.isUndefined(v));
+ if(!fs.pathExistsSync(CONFIG_PATH)) return new ServiceConfig(external);
+ const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
+ return new ServiceConfig({ ...data, ...external });
+ }
+
+}
+
+export default ServiceConfig.load();
\ No newline at end of file
diff --git a/src/lib/configs/system-config.ts b/src/lib/configs/system-config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7c589a691a7b9d39ec5107c1426d4ae052c47377
--- /dev/null
+++ b/src/lib/configs/system-config.ts
@@ -0,0 +1,84 @@
+import path from 'path';
+
+import fs from 'fs-extra';
+import yaml from 'yaml';
+import _ from 'lodash';
+
+import environment from '../environment.ts';
+
+const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/system.yml");
+
+/**
+ * 系统配置
+ */
+export class SystemConfig {
+
+ /** 是否开启请求日志 */
+ requestLog: boolean;
+ /** 临时目录路径 */
+ tmpDir: string;
+ /** 日志目录路径 */
+ logDir: string;
+ /** 日志写入间隔(毫秒) */
+ logWriteInterval: number;
+ /** 日志文件有效期(毫秒) */
+ logFileExpires: number;
+ /** 公共目录路径 */
+ publicDir: string;
+ /** 临时文件有效期(毫秒) */
+ tmpFileExpires: number;
+ /** 请求体配置 */
+ requestBody: any;
+ /** 是否调试模式 */
+ debug: boolean;
+
+ constructor(options?: any) {
+ const { requestLog, tmpDir, logDir, logWriteInterval, logFileExpires, publicDir, tmpFileExpires, requestBody, debug } = options || {};
+ this.requestLog = _.defaultTo(requestLog, false);
+ this.tmpDir = _.defaultTo(tmpDir, './tmp');
+ this.logDir = _.defaultTo(logDir, './logs');
+ this.logWriteInterval = _.defaultTo(logWriteInterval, 200);
+ this.logFileExpires = _.defaultTo(logFileExpires, 2626560000);
+ this.publicDir = _.defaultTo(publicDir, './public');
+ this.tmpFileExpires = _.defaultTo(tmpFileExpires, 86400000);
+ this.requestBody = Object.assign(requestBody || {}, {
+ enableTypes: ['json', 'form', 'text', 'xml'],
+ encoding: 'utf-8',
+ formLimit: '100mb',
+ jsonLimit: '100mb',
+ textLimit: '100mb',
+ xmlLimit: '100mb',
+ formidable: {
+ maxFileSize: '100mb'
+ },
+ multipart: true,
+ parsedMethods: ['POST', 'PUT', 'PATCH']
+ });
+ this.debug = _.defaultTo(debug, true);
+ }
+
+ get rootDirPath() {
+ return path.resolve();
+ }
+
+ get tmpDirPath() {
+ return path.resolve(this.tmpDir);
+ }
+
+ get logDirPath() {
+ return path.resolve(this.logDir);
+ }
+
+ get publicDirPath() {
+ return path.resolve(this.publicDir);
+ }
+
+ static load() {
+ if (!fs.pathExistsSync(CONFIG_PATH)) return new SystemConfig();
+ const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
+ return new SystemConfig(data);
+ }
+
+}
+
+export default SystemConfig.load();
\ No newline at end of file
diff --git a/src/lib/consts/exceptions.ts b/src/lib/consts/exceptions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7a9b7889a0c8891bafd5c7751f4b85ae5917509e
--- /dev/null
+++ b/src/lib/consts/exceptions.ts
@@ -0,0 +1,5 @@
+export default {
+ SYSTEM_ERROR: [-1000, '系统异常'],
+ SYSTEM_REQUEST_VALIDATION_ERROR: [-1001, '请求参数校验错误'],
+ SYSTEM_NOT_ROUTE_MATCHING: [-1002, '无匹配的路由']
+} as Record
\ No newline at end of file
diff --git a/src/lib/environment.ts b/src/lib/environment.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6e52a849b6fe7646c6912c84f075f4d180c615bb
--- /dev/null
+++ b/src/lib/environment.ts
@@ -0,0 +1,44 @@
+import path from 'path';
+
+import fs from 'fs-extra';
+import minimist from 'minimist';
+import _ from 'lodash';
+
+const cmdArgs = minimist(process.argv.slice(2)); //获取命令行参数
+const envVars = process.env; //获取环境变量
+
+class Environment {
+
+ /** 命令行参数 */
+ cmdArgs: any;
+ /** 环境变量 */
+ envVars: any;
+ /** 环境名称 */
+ env?: string;
+ /** 服务名称 */
+ name?: string;
+ /** 服务地址 */
+ host?: string;
+ /** 服务端口 */
+ port?: number;
+ /** 包参数 */
+ package: any;
+
+ constructor(options: any = {}) {
+ const { cmdArgs, envVars, package: _package } = options;
+ this.cmdArgs = cmdArgs;
+ this.envVars = envVars;
+ this.env = _.defaultTo(cmdArgs.env || envVars.SERVER_ENV, 'dev');
+ this.name = cmdArgs.name || envVars.SERVER_NAME || undefined;
+ this.host = cmdArgs.host || envVars.SERVER_HOST || undefined;
+ this.port = Number(cmdArgs.port || envVars.SERVER_PORT) ? Number(cmdArgs.port || envVars.SERVER_PORT) : undefined;
+ this.package = _package;
+ }
+
+}
+
+export default new Environment({
+ cmdArgs,
+ envVars,
+ package: JSON.parse(fs.readFileSync(path.join(path.resolve(), "package.json")).toString())
+});
\ No newline at end of file
diff --git a/src/lib/exceptions/APIException.ts b/src/lib/exceptions/APIException.ts
new file mode 100644
index 0000000000000000000000000000000000000000..515c8067a44d585e744d777bfc3aab6253c37612
--- /dev/null
+++ b/src/lib/exceptions/APIException.ts
@@ -0,0 +1,14 @@
+import Exception from './Exception.js';
+
+export default class APIException extends Exception {
+
+ /**
+ * 构造异常
+ *
+ * @param {[number, string]} exception 异常
+ */
+ constructor(exception: (string | number)[], errmsg?: string) {
+ super(exception, errmsg);
+ }
+
+}
\ No newline at end of file
diff --git a/src/lib/exceptions/Exception.ts b/src/lib/exceptions/Exception.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ef0372f1d926c801ab4f6201ff09bc95f99f760e
--- /dev/null
+++ b/src/lib/exceptions/Exception.ts
@@ -0,0 +1,47 @@
+import assert from 'assert';
+
+import _ from 'lodash';
+
+export default class Exception extends Error {
+
+ /** 错误码 */
+ errcode: number;
+ /** 错误消息 */
+ errmsg: string;
+ /** 数据 */
+ data: any;
+ /** HTTP状态码 */
+ httpStatusCode: number;
+
+ /**
+ * 构造异常
+ *
+ * @param exception 异常
+ * @param _errmsg 异常消息
+ */
+ constructor(exception: (string | number)[], _errmsg?: string) {
+ assert(_.isArray(exception), 'Exception must be Array');
+ const [errcode, errmsg] = exception as [number, string];
+ assert(_.isFinite(errcode), 'Exception errcode invalid');
+ assert(_.isString(errmsg), 'Exception errmsg invalid');
+ super(_errmsg || errmsg);
+ this.errcode = errcode;
+ this.errmsg = _errmsg || errmsg;
+ }
+
+ compare(exception: (string | number)[]) {
+ const [errcode] = exception as [number, string];
+ return this.errcode == errcode;
+ }
+
+ setHTTPStatusCode(value: number) {
+ this.httpStatusCode = value;
+ return this;
+ }
+
+ setData(value: any) {
+ this.data = _.defaultTo(value, null);
+ return this;
+ }
+
+}
\ No newline at end of file
diff --git a/src/lib/http-status-codes.ts b/src/lib/http-status-codes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cc0c571403022aef36802cc861304acc795b82ea
--- /dev/null
+++ b/src/lib/http-status-codes.ts
@@ -0,0 +1,61 @@
+export default {
+
+ CONTINUE: 100, //客户端应当继续发送请求。这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应
+ SWITCHING_PROTOCOLS: 101, //服务器已经理解了客户端的请求,并将通过Upgrade 消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade 消息头中定义的那些协议。只有在切换新的协议更有好处的时候才应该采取类似措施。例如,切换到新的HTTP 版本比旧版本更有优势,或者切换到一个实时且同步的协议以传送利用此类特性的资源
+ PROCESSING: 102, //处理将被继续执行
+
+ OK: 200, //请求已成功,请求所希望的响应头或数据体将随此响应返回
+ CREATED: 201, //请求已经被实现,而且有一个新的资源已经依据请求的需要而建立,且其 URI 已经随Location 头信息返回。假如需要的资源无法及时建立的话,应当返回 '202 Accepted'
+ ACCEPTED: 202, //服务器已接受请求,但尚未处理。正如它可能被拒绝一样,最终该请求可能会也可能不会被执行。在异步操作的场合下,没有比发送这个状态码更方便的做法了。返回202状态码的响应的目的是允许服务器接受其他过程的请求(例如某个每天只执行一次的基于批处理的操作),而不必让客户端一直保持与服务器的连接直到批处理操作全部完成。在接受请求处理并返回202状态码的响应应当在返回的实体中包含一些指示处理当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便用户能够估计操作是否已经完成
+ NON_AUTHORITATIVE_INFO: 203, //服务器已成功处理了请求,但返回的实体头部元信息不是在原始服务器上有效的确定集合,而是来自本地或者第三方的拷贝。当前的信息可能是原始版本的子集或者超集。例如,包含资源的元数据可能导致原始服务器知道元信息的超级。使用此状态码不是必须的,而且只有在响应不使用此状态码便会返回200 OK的情况下才是合适的
+ NO_CONTENT: 204, //服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾
+ RESET_CONTENT: 205, //服务器成功处理了请求,且没有返回任何内容。但是与204响应不同,返回此状态码的响应要求请求者重置文档视图。该响应主要是被用于接受用户输入后,立即重置表单,以便用户能够轻松地开始另一次输入。与204响应一样,该响应也被禁止包含任何消息体,且以消息头后的第一个空行结束
+ PARTIAL_CONTENT: 206, //服务器已经成功处理了部分 GET 请求。类似于FlashGet或者迅雷这类的HTTP下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。该请求必须包含 Range 头信息来指示客户端希望得到的内容范围,并且可能包含 If-Range 来作为请求条件。响应必须包含如下的头部域:Content-Range 用以指示本次响应中返回的内容的范围;如果是Content-Type为multipart/byteranges的多段下载,则每一段multipart中都应包含Content-Range域用以指示本段的内容范围。假如响应中包含Content-Length,那么它的数值必须匹配它返回的内容范围的真实字节数。Date和ETag或Content-Location,假如同样的请求本应该返回200响应。Expires, Cache-Control,和/或 Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。假如本响应请求使用了 If-Range 强缓存验证,那么本次响应不应该包含其他实体头;假如本响应的请求使用了 If-Range 弱缓存验证,那么本次响应禁止包含其他实体头;这避免了缓存的实体内容和更新了的实体头信息之间的不一致。否则,本响应就应当包含所有本应该返回200响应中应当返回的所有实体头部域。假如 ETag 或 Latest-Modified 头部不能精确匹配的话,则客户端缓存应禁止将206响应返回的内容与之前任何缓存过的内容组合在一起。任何不支持 Range 以及 Content-Range 头的缓存都禁止缓存206响应返回的内容
+ MULTIPLE_STATUS: 207, //代表之后的消息体将是一个XML消息,并且可能依照之前子请求数量的不同,包含一系列独立的响应代码
+
+ MULTIPLE_CHOICES: 300, //被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。除非这是一个HEAD请求,否则该响应应当包括一个资源特性及地址的列表的实体,以便用户或浏览器从中选择最合适的重定向地址。这个实体的格式由Content-Type定义的格式所决定。浏览器可能根据响应的格式以及浏览器自身能力,自动作出最合适的选择。当然,RFC 2616规范并没有规定这样的自动选择该如何进行。如果服务器本身已经有了首选的回馈选择,那么在Location中应当指明这个回馈的 URI;浏览器可能会将这个 Location 值作为自动重定向的地址。此外,除非额外指定,否则这个响应也是可缓存的
+ MOVED_PERMANENTLY: 301, //被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。新的永久性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。如果这不是一个GET或者HEAD请求,因此浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。注意:对于某些使用 HTTP/1.0 协议的浏览器,当它们发送的POST请求得到了一个301响应的话,接下来的重定向请求将会变成GET方式
+ FOUND: 302, //请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。新的临时性的URI应当在响应的 Location 域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。注意:虽然RFC 1945和RFC 2068规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将302响应视作为303响应,并且使用GET方式访问在Location中规定的URI,而无视原先请求的方法。状态码303和307被添加了进来,用以明确服务器期待客户端进行何种反应
+ SEE_OTHER: 303, //对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用 GET 的方式访问那个资源。这个方法的存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源。这个新的 URI 不是原始资源的替代引用。同时,303响应禁止被缓存。当然,第二个请求(重定向)可能被缓存。新的 URI 应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。注意:许多 HTTP/1.1 版以前的浏览器不能正确理解303状态。如果需要考虑与这些浏览器之间的互动,302状态码应该可以胜任,因为大多数的浏览器处理302响应时的方式恰恰就是上述规范要求客户端处理303响应时应当做的
+ NOT_MODIFIED: 304, //如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。304响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。该响应必须包含以下的头信息:Date,除非这个服务器没有时钟。假如没有时钟的服务器也遵守这些规则,那么代理服务器以及客户端可以自行将Date字段添加到接收到的响应头中去(正如RFC 2068中规定的一样),缓存机制将会正常工作。ETag或 Content-Location,假如同样的请求本应返回200响应。Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。假如本响应请求使用了强缓存验证,那么本次响应不应该包含其他实体头;否则(例如,某个带条件的 GET 请求使用了弱缓存验证),本次响应禁止包含其他实体头;这避免了缓存了的实体内容和更新了的实体头信息之间的不一致。假如某个304响应指明了当前某个实体没有缓存,那么缓存系统必须忽视这个响应,并且重复发送不包含限制条件的请求。假如接收到一个要求更新某个缓存条目的304响应,那么缓存系统必须更新整个条目以反映所有在响应中被更新的字段的值
+ USE_PROXY: 305, //被请求的资源必须通过指定的代理才能被访问。Location域中将给出指定的代理所在的URI信息,接收者需要重复发送一个单独的请求,通过这个代理才能访问相应资源。只有原始服务器才能建立305响应。注意:RFC 2068中没有明确305响应是为了重定向一个单独的请求,而且只能被原始服务器建立。忽视这些限制可能导致严重的安全后果
+ UNUSED: 306, //在最新版的规范中,306状态码已经不再被使用
+ TEMPORARY_REDIRECT: 307, //请求的资源现在临时从不同的URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。新的临时性的URI 应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI 的超链接及简短说明。因为部分浏览器不能识别307响应,因此需要添加上述必要信息以便用户能够理解并向新的 URI 发出访问请求。如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化
+
+ BAD_REQUEST: 400, //1.语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求 2.请求参数有误
+ UNAUTHORIZED: 401, //当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。参见RFC 2617
+ PAYMENT_REQUIRED: 402, //该状态码是为了将来可能的需求而预留的
+ FORBIDDEN: 403, //服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个HEAD请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个404响应,假如它不希望让客户端获得任何信息
+ NOT_FOUND: 404, //请求失败,请求所希望得到的资源未被在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下
+ METHOD_NOT_ALLOWED: 405, //请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow 头信息用以表示出当前资源能够接受的请求方法的列表。鉴于PUT,DELETE方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回405错误
+ NO_ACCEPTABLE: 406, //请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体。除非这是一个 HEAD 请求,否则该响应就应当返回一个包含可以让用户或者浏览器从中选择最合适的实体特性以及地址列表的实体。实体的格式由Content-Type头中定义的媒体类型决定。浏览器可以根据格式及自身能力自行作出最佳选择。但是,规范中并没有定义任何作出此类自动选择的标准
+ PROXY_AUTHENTICATION_REQUIRED: 407, //与401响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个Proxy-Authenticate用以进行身份询问。客户端可以返回一个Proxy-Authorization信息头用以验证。参见RFC 2617
+ REQUEST_TIMEOUT: 408, //请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改
+ CONFLICT: 409, //由于和被请求的资源的当前状态之间存在冲突,请求无法完成。这个代码只允许用在这样的情况下才能被使用:用户被认为能够解决冲突,并且会重新提交新的请求。该响应应当包含足够的信息以便用户发现冲突的源头。冲突通常发生于对PUT请求的处理中。例如,在采用版本检查的环境下,某次PUT提交的对特定资源的修改请求所附带的版本信息与之前的某个(第三方)请求向冲突,那么此时服务器就应该返回一个409错误,告知用户请求无法完成。此时,响应实体中很可能会包含两个冲突版本之间的差异比较,以便用户重新提交归并以后的新版本
+ GONE: 410, //被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的状况应当被认为是永久性的。如果可能,拥有链接编辑功能的客户端应当在获得用户许可后删除所有指向这个地址的引用。如果服务器不知道或者无法确定这个状况是否是永久的,那么就应该使用404状态码。除非额外说明,否则这个响应是可缓存的。410响应的目的主要是帮助网站管理员维护网站,通知用户该资源已经不再可用,并且服务器拥有者希望所有指向这个资源的远端连接也被删除。这类事件在限时、增值服务中很普遍。同样,410响应也被用于通知客户端在当前服务器站点上,原本属于某个个人的资源已经不再可用。当然,是否需要把所有永久不可用的资源标记为'410 Gone',以及是否需要保持此标记多长时间,完全取决于服务器拥有者
+ LENGTH_REQUIRED: 411, //服务器拒绝在没有定义Content-Length头的情况下接受请求。在添加了表明请求消息体长度的有效Content-Length头之后,客户端可以再次提交该请求
+ PRECONDITION_FAILED: 412, //服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。这个状态码允许客户端在获取资源时在请求的元信息(请求头字段数据)中设置先决条件,以此避免该请求方法被应用到其希望的内容以外的资源上
+ REQUEST_ENTITY_TOO_LARGE: 413, //服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。此种情况下,服务器可以关闭连接以免客户端继续发送此请求。如果这个状况是临时的,服务器应当返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试
+ REQUEST_URI_TOO_LONG: 414, //请求的URI长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。这比较少见,通常的情况包括:本应使用POST方法的表单提交变成了GET方法,导致查询字符串(Query String)过长。重定向URI “黑洞”,例如每次重定向把旧的URI作为新的URI的一部分,导致在若干次重定向后URI超长。客户端正在尝试利用某些服务器中存在的安全漏洞攻击服务器。这类服务器使用固定长度的缓冲读取或操作请求的URI,当GET后的参数超过某个数值后,可能会产生缓冲区溢出,导致任意代码被执行[1]。没有此类漏洞的服务器,应当返回414状态码
+ UNSUPPORTED_MEDIA_TYPE: 415, //对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝
+ REQUESTED_RANGE_NOT_SATISFIABLE: 416, //如果请求中包含了Range请求头,并且Range中指定的任何数据范围都与当前资源的可用范围不重合,同时请求中又没有定义If-Range请求头,那么服务器就应当返回416状态码。假如Range使用的是字节范围,那么这种情况就是指请求指定的所有数据范围的首字节位置都超过了当前资源的长度。服务器也应当在返回416状态码的同时,包含一个Content-Range实体头,用以指明当前资源的长度。这个响应也被禁止使用multipart/byteranges作为其 Content-Type
+ EXPECTION_FAILED: 417, //在请求头Expect中指定的预期内容无法被服务器满足,或者这个服务器是一个代理服务器,它有明显的证据证明在当前路由的下一个节点上,Expect的内容无法被满足
+ TOO_MANY_CONNECTIONS: 421, //从当前客户端所在的IP地址到服务器的连接数超过了服务器许可的最大范围。通常,这里的IP地址指的是从服务器上看到的客户端地址(比如用户的网关或者代理服务器地址)。在这种情况下,连接数的计算可能涉及到不止一个终端用户
+ UNPROCESSABLE_ENTITY: 422, //请求格式正确,但是由于含有语义错误,无法响应
+ FAILED_DEPENDENCY: 424, //由于之前的某个请求发生的错误,导致当前请求失败,例如PROPPATCH
+ UNORDERED_COLLECTION: 425, //在WebDav Advanced Collections 草案中定义,但是未出现在《WebDAV 顺序集协议》(RFC 3658)中
+ UPGRADE_REQUIRED: 426, //客户端应当切换到TLS/1.0
+ RETRY_WITH: 449, //由微软扩展,代表请求应当在执行完适当的操作后进行重试
+
+ INTERNAL_SERVER_ERROR: 500, //服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题都会在服务器的程序码出错时出现
+ NOT_IMPLEMENTED: 501, //服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求
+ BAD_GATEWAY: 502, //作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
+ SERVICE_UNAVAILABLE: 503, //由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个 Retry-After 头用以标明这个延迟时间。如果没有给出这个 Retry-After 信息,那么客户端应当以处理500响应的方式处理它。注意:503状态码的存在并不意味着服务器在过载的时候必须使用它。某些服务器只不过是希望拒绝客户端的连接
+ GATEWAY_TIMEOUT: 504, //作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。注意:某些代理服务器在DNS查询超时时会返回400或者500错误
+ HTTP_VERSION_NOT_SUPPORTED: 505, //服务器不支持,或者拒绝支持在请求中使用的HTTP版本。这暗示着服务器不能或不愿使用与客户端相同的版本。响应中应当包含一个描述了为何版本不被支持以及服务器支持哪些协议的实体
+ VARIANT_ALSO_NEGOTIATES: 506, //服务器存在内部配置错误:被请求的协商变元资源被配置为在透明内容协商中使用自己,因此在一个协商处理中不是一个合适的重点
+ INSUFFICIENT_STORAGE: 507, //服务器无法存储完成请求所必须的内容。这个状况被认为是临时的
+ BANDWIDTH_LIMIT_EXCEEDED: 509, //服务器达到带宽限制。这不是一个官方的状态码,但是仍被广泛使用
+ NOT_EXTENDED: 510 //获取资源所需要的策略并没有没满足
+
+};
\ No newline at end of file
diff --git a/src/lib/initialize.ts b/src/lib/initialize.ts
new file mode 100644
index 0000000000000000000000000000000000000000..953d224f123d6e39d4edbe71ec92c3bb20b05515
--- /dev/null
+++ b/src/lib/initialize.ts
@@ -0,0 +1,28 @@
+import logger from './logger.js';
+
+// 允许无限量的监听器
+process.setMaxListeners(Infinity);
+// 输出未捕获异常
+process.on("uncaughtException", (err, origin) => {
+ logger.error(`An unhandled error occurred: ${origin}`, err);
+});
+// 输出未处理的Promise.reject
+process.on("unhandledRejection", (_, promise) => {
+ promise.catch(err => logger.error("An unhandled rejection occurred:", err));
+});
+// 输出系统警告信息
+process.on("warning", warning => logger.warn("System warning: ", warning));
+// 进程退出监听
+process.on("exit", () => {
+ logger.info("Service exit");
+ logger.footer();
+});
+// 进程被kill
+process.on("SIGTERM", () => {
+ logger.warn("received kill signal");
+ process.exit(2);
+});
+// Ctrl-C进程退出
+process.on("SIGINT", () => {
+ process.exit(0);
+});
\ No newline at end of file
diff --git a/src/lib/interfaces/ICompletionMessage.ts b/src/lib/interfaces/ICompletionMessage.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5aad3454b3722c031c56761325ab6dce1bbc3ce5
--- /dev/null
+++ b/src/lib/interfaces/ICompletionMessage.ts
@@ -0,0 +1,4 @@
+export default interface ICompletionMessage {
+ role: 'system' | 'assistant' | 'user' | 'function';
+ content: string;
+}
\ No newline at end of file
diff --git a/src/lib/logger.ts b/src/lib/logger.ts
new file mode 100644
index 0000000000000000000000000000000000000000..32cb3a6201e323cf8b3dfbe62c19f9f32de8a91b
--- /dev/null
+++ b/src/lib/logger.ts
@@ -0,0 +1,184 @@
+import path from 'path';
+import _util from 'util';
+
+import 'colors';
+import _ from 'lodash';
+import fs from 'fs-extra';
+import { format as dateFormat } from 'date-fns';
+
+import config from './config.ts';
+import util from './util.ts';
+
+const isVercelEnv = process.env.VERCEL;
+
+class LogWriter {
+
+ #buffers = [];
+
+ constructor() {
+ !isVercelEnv && fs.ensureDirSync(config.system.logDirPath);
+ !isVercelEnv && this.work();
+ }
+
+ push(content) {
+ const buffer = Buffer.from(content);
+ this.#buffers.push(buffer);
+ }
+
+ writeSync(buffer) {
+ !isVercelEnv && fs.appendFileSync(path.join(config.system.logDirPath, `/${util.getDateString()}.log`), buffer);
+ }
+
+ async write(buffer) {
+ !isVercelEnv && await fs.appendFile(path.join(config.system.logDirPath, `/${util.getDateString()}.log`), buffer);
+ }
+
+ flush() {
+ if(!this.#buffers.length) return;
+ !isVercelEnv && fs.appendFileSync(path.join(config.system.logDirPath, `/${util.getDateString()}.log`), Buffer.concat(this.#buffers));
+ }
+
+ work() {
+ if (!this.#buffers.length) return setTimeout(this.work.bind(this), config.system.logWriteInterval);
+ const buffer = Buffer.concat(this.#buffers);
+ this.#buffers = [];
+ this.write(buffer)
+ .finally(() => setTimeout(this.work.bind(this), config.system.logWriteInterval))
+ .catch(err => console.error("Log write error:", err));
+ }
+
+}
+
+class LogText {
+
+ /** @type {string} 日志级别 */
+ level;
+ /** @type {string} 日志文本 */
+ text;
+ /** @type {string} 日志来源 */
+ source;
+ /** @type {Date} 日志发生时间 */
+ time = new Date();
+
+ constructor(level, ...params) {
+ this.level = level;
+ this.text = _util.format.apply(null, params);
+ this.source = this.#getStackTopCodeInfo();
+ }
+
+ #getStackTopCodeInfo() {
+ const unknownInfo = { name: "unknown", codeLine: 0, codeColumn: 0 };
+ const stackArray = new Error().stack.split("\n");
+ const text = stackArray[4];
+ if (!text)
+ return unknownInfo;
+ const match = text.match(/at (.+) \((.+)\)/) || text.match(/at (.+)/);
+ if (!match || !_.isString(match[2] || match[1]))
+ return unknownInfo;
+ const temp = match[2] || match[1];
+ const _match = temp.match(/([a-zA-Z0-9_\-\.]+)\:(\d+)\:(\d+)$/);
+ if (!_match)
+ return unknownInfo;
+ const [, scriptPath, codeLine, codeColumn] = _match as any;
+ return {
+ name: scriptPath ? scriptPath.replace(/.js$/, "") : "unknown",
+ path: scriptPath || null,
+ codeLine: parseInt(codeLine || 0),
+ codeColumn: parseInt(codeColumn || 0)
+ };
+ }
+
+ toString() {
+ return `[${dateFormat(this.time, "yyyy-MM-dd HH:mm:ss.SSS")}][${this.level}][${this.source.name}<${this.source.codeLine},${this.source.codeColumn}>] ${this.text}`;
+ }
+
+}
+
+class Logger {
+
+ /** @type {Object} 系统配置 */
+ config = {};
+ /** @type {Object} 日志级别映射 */
+ static Level = {
+ Success: "success",
+ Info: "info",
+ Log: "log",
+ Debug: "debug",
+ Warning: "warning",
+ Error: "error",
+ Fatal: "fatal"
+ };
+ /** @type {Object} 日志级别文本颜色樱色 */
+ static LevelColor = {
+ [Logger.Level.Success]: "green",
+ [Logger.Level.Info]: "brightCyan",
+ [Logger.Level.Debug]: "white",
+ [Logger.Level.Warning]: "brightYellow",
+ [Logger.Level.Error]: "brightRed",
+ [Logger.Level.Fatal]: "red"
+ };
+ #writer;
+
+ constructor() {
+ this.#writer = new LogWriter();
+ }
+
+ header() {
+ this.#writer.writeSync(Buffer.from(`\n\n===================== LOG START ${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")} =====================\n\n`));
+ }
+
+ footer() {
+ this.#writer.flush(); //将未写入文件的日志缓存写入
+ this.#writer.writeSync(Buffer.from(`\n\n===================== LOG END ${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")} =====================\n\n`));
+ }
+
+ success(...params) {
+ const content = new LogText(Logger.Level.Success, ...params).toString();
+ console.info(content[Logger.LevelColor[Logger.Level.Success]]);
+ this.#writer.push(content + "\n");
+ }
+
+ info(...params) {
+ const content = new LogText(Logger.Level.Info, ...params).toString();
+ console.info(content[Logger.LevelColor[Logger.Level.Info]]);
+ this.#writer.push(content + "\n");
+ }
+
+ log(...params) {
+ const content = new LogText(Logger.Level.Log, ...params).toString();
+ console.log(content[Logger.LevelColor[Logger.Level.Log]]);
+ this.#writer.push(content + "\n");
+ }
+
+ debug(...params) {
+ if(!config.system.debug) return; //非调试模式忽略debug
+ const content = new LogText(Logger.Level.Debug, ...params).toString();
+ console.debug(content[Logger.LevelColor[Logger.Level.Debug]]);
+ this.#writer.push(content + "\n");
+ }
+
+ warn(...params) {
+ const content = new LogText(Logger.Level.Warning, ...params).toString();
+ console.warn(content[Logger.LevelColor[Logger.Level.Warning]]);
+ this.#writer.push(content + "\n");
+ }
+
+ error(...params) {
+ const content = new LogText(Logger.Level.Error, ...params).toString();
+ console.error(content[Logger.LevelColor[Logger.Level.Error]]);
+ this.#writer.push(content);
+ }
+
+ fatal(...params) {
+ const content = new LogText(Logger.Level.Fatal, ...params).toString();
+ console.error(content[Logger.LevelColor[Logger.Level.Fatal]]);
+ this.#writer.push(content);
+ }
+
+ destory() {
+ this.#writer.destory();
+ }
+
+}
+
+export default new Logger();
\ No newline at end of file
diff --git a/src/lib/request/Request.ts b/src/lib/request/Request.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fce604528f85ef9eb0d068863e7fa0972d85f708
--- /dev/null
+++ b/src/lib/request/Request.ts
@@ -0,0 +1,72 @@
+import _ from 'lodash';
+
+import APIException from '@/lib/exceptions/APIException.ts';
+import EX from '@/api/consts/exceptions.ts';
+import logger from '@/lib/logger.ts';
+import util from '@/lib/util.ts';
+
+export interface RequestOptions {
+ time?: number;
+}
+
+export default class Request {
+
+ /** 请求方法 */
+ method: string;
+ /** 请求URL */
+ url: string;
+ /** 请求路径 */
+ path: string;
+ /** 请求载荷类型 */
+ type: string;
+ /** 请求headers */
+ headers: any;
+ /** 请求原始查询字符串 */
+ search: string;
+ /** 请求查询参数 */
+ query: any;
+ /** 请求URL参数 */
+ params: any;
+ /** 请求载荷 */
+ body: any;
+ /** 上传的文件 */
+ files: any[];
+ /** 客户端IP地址 */
+ remoteIP: string | null;
+ /** 请求接受时间戳(毫秒) */
+ time: number;
+
+ constructor(ctx, options: RequestOptions = {}) {
+ const { time } = options;
+ this.method = ctx.request.method;
+ this.url = ctx.request.url;
+ this.path = ctx.request.path;
+ this.type = ctx.request.type;
+ this.headers = ctx.request.headers || {};
+ this.search = ctx.request.search;
+ this.query = ctx.query || {};
+ this.params = ctx.params || {};
+ this.body = ctx.request.body || {};
+ this.files = ctx.request.files || {};
+ this.remoteIP = this.headers["X-Real-IP"] || this.headers["x-real-ip"] || this.headers["X-Forwarded-For"] || this.headers["x-forwarded-for"] || ctx.ip || null;
+ this.time = Number(_.defaultTo(time, util.timestamp()));
+ }
+
+ validate(key: string, fn?: Function) {
+ try {
+ const value = _.get(this, key);
+ if (fn) {
+ if (fn(value) === false)
+ throw `[Mismatch] -> ${fn}`;
+ }
+ else if (_.isUndefined(value))
+ throw '[Undefined]';
+ }
+ catch (err) {
+ logger.warn(`Params ${key} invalid:`, err);
+ throw new APIException(EX.API_REQUEST_PARAMS_INVALID, `Params ${key} invalid`);
+ }
+ return this;
+ }
+
+}
\ No newline at end of file
diff --git a/src/lib/response/Body.ts b/src/lib/response/Body.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9cf857444bd2f42401d9c8c97a8c7a24b421496c
--- /dev/null
+++ b/src/lib/response/Body.ts
@@ -0,0 +1,41 @@
+import _ from 'lodash';
+
+export interface BodyOptions {
+ code?: number;
+ message?: string;
+ data?: any;
+ statusCode?: number;
+}
+
+export default class Body {
+
+ /** 状态码 */
+ code: number;
+ /** 状态消息 */
+ message: string;
+ /** 载荷 */
+ data: any;
+ /** HTTP状态码 */
+ statusCode: number;
+
+ constructor(options: BodyOptions = {}) {
+ const { code, message, data, statusCode } = options;
+ this.code = Number(_.defaultTo(code, 0));
+ this.message = _.defaultTo(message, 'OK');
+ this.data = _.defaultTo(data, null);
+ this.statusCode = Number(_.defaultTo(statusCode, 200));
+ }
+
+ toObject() {
+ return {
+ code: this.code,
+ message: this.message,
+ data: this.data
+ };
+ }
+
+ static isInstance(value) {
+ return value instanceof Body;
+ }
+
+}
\ No newline at end of file
diff --git a/src/lib/response/FailureBody.ts b/src/lib/response/FailureBody.ts
new file mode 100644
index 0000000000000000000000000000000000000000..33d7fb9bc80d63061b5eb0afd9c7c035376c5960
--- /dev/null
+++ b/src/lib/response/FailureBody.ts
@@ -0,0 +1,31 @@
+import _ from 'lodash';
+
+import Body from './Body.ts';
+import Exception from '../exceptions/Exception.ts';
+import APIException from '../exceptions/APIException.ts';
+import EX from '../consts/exceptions.ts';
+import HTTP_STATUS_CODES from '../http-status-codes.ts';
+
+export default class FailureBody extends Body {
+
+ constructor(error: APIException | Exception | Error, _data?: any) {
+ let errcode, errmsg, data = _data, httpStatusCode = HTTP_STATUS_CODES.OK;;
+ if(_.isString(error))
+ error = new Exception(EX.SYSTEM_ERROR, error);
+ else if(error instanceof APIException || error instanceof Exception)
+ ({ errcode, errmsg, data, httpStatusCode } = error);
+ else if(_.isError(error))
+ ({ errcode, errmsg, data, httpStatusCode } = new Exception(EX.SYSTEM_ERROR, error.message));
+ super({
+ code: errcode || -1,
+ message: errmsg || 'Internal error',
+ data,
+ statusCode: httpStatusCode
+ });
+ }
+
+ static isInstance(value) {
+ return value instanceof FailureBody;
+ }
+
+}
\ No newline at end of file
diff --git a/src/lib/response/Response.ts b/src/lib/response/Response.ts
new file mode 100644
index 0000000000000000000000000000000000000000..816397d14cea2e6e5125f2c44241d585e95e19a5
--- /dev/null
+++ b/src/lib/response/Response.ts
@@ -0,0 +1,63 @@
+import mime from 'mime';
+import _ from 'lodash';
+
+import Body from './Body.ts';
+import util from '../util.ts';
+
+export interface ResponseOptions {
+ statusCode?: number;
+ type?: string;
+ headers?: Record;
+ redirect?: string;
+ body?: any;
+ size?: number;
+ time?: number;
+}
+
+export default class Response {
+
+ /** 响应HTTP状态码 */
+ statusCode: number;
+ /** 响应内容类型 */
+ type: string;
+ /** 响应headers */
+ headers: Record;
+ /** 重定向目标 */
+ redirect: string;
+ /** 响应载荷 */
+ body: any;
+ /** 响应载荷大小 */
+ size: number;
+ /** 响应时间戳 */
+ time: number;
+
+ constructor(body: any, options: ResponseOptions = {}) {
+ const { statusCode, type, headers, redirect, size, time } = options;
+ this.statusCode = Number(_.defaultTo(statusCode, Body.isInstance(body) ? body.statusCode : undefined))
+ this.type = type;
+ this.headers = headers;
+ this.redirect = redirect;
+ this.size = size;
+ this.time = Number(_.defaultTo(time, util.timestamp()));
+ this.body = body;
+ }
+
+ injectTo(ctx) {
+ this.redirect && ctx.redirect(this.redirect);
+ this.statusCode && (ctx.status = this.statusCode);
+ this.type && (ctx.type = mime.getType(this.type) || this.type);
+ const headers = this.headers || {};
+ if(this.size && !headers["Content-Length"] && !headers["content-length"])
+ headers["Content-Length"] = this.size;
+ ctx.set(headers);
+ if(Body.isInstance(this.body))
+ ctx.body = this.body.toObject();
+ else
+ ctx.body = this.body;
+ }
+
+ static isInstance(value) {
+ return value instanceof Response;
+ }
+
+}
\ No newline at end of file
diff --git a/src/lib/response/SuccessfulBody.ts b/src/lib/response/SuccessfulBody.ts
new file mode 100644
index 0000000000000000000000000000000000000000..639d0d89781ff4c5f46abd39f32787d624e626dd
--- /dev/null
+++ b/src/lib/response/SuccessfulBody.ts
@@ -0,0 +1,19 @@
+import _ from 'lodash';
+
+import Body from './Body.ts';
+
+export default class SuccessfulBody extends Body {
+
+ constructor(data: any, message?: string) {
+ super({
+ code: 0,
+ message: _.defaultTo(message, "OK"),
+ data
+ });
+ }
+
+ static isInstance(value) {
+ return value instanceof SuccessfulBody;
+ }
+
+}
\ No newline at end of file
diff --git a/src/lib/server.ts b/src/lib/server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8c0e46a7002a0ef35c38069d8f5e49fb5c982f36
--- /dev/null
+++ b/src/lib/server.ts
@@ -0,0 +1,173 @@
+import Koa from 'koa';
+import KoaRouter from 'koa-router';
+import koaRange from 'koa-range';
+import koaCors from "koa2-cors";
+import koaBody from 'koa-body';
+import _ from 'lodash';
+
+import Exception from './exceptions/Exception.ts';
+import Request from './request/Request.ts';
+import Response from './response/Response.js';
+import FailureBody from './response/FailureBody.ts';
+import EX from './consts/exceptions.ts';
+import logger from './logger.ts';
+import config from './config.ts';
+
+class Server {
+
+ app;
+ router;
+
+ constructor() {
+ this.app = new Koa();
+ this.app.use(koaCors());
+ // 范围请求支持
+ this.app.use(koaRange);
+ this.router = new KoaRouter({ prefix: config.service.urlPrefix });
+ // 前置处理异常拦截
+ this.app.use(async (ctx: any, next: Function) => {
+ if(ctx.request.type === "application/xml" || ctx.request.type === "application/ssml+xml")
+ ctx.req.headers["content-type"] = "text/xml";
+ try { await next() }
+ catch (err) {
+ logger.error(err);
+ const failureBody = new FailureBody(err);
+ new Response(failureBody).injectTo(ctx);
+ }
+ });
+ // 载荷解析器支持
+ this.app.use(koaBody(_.clone(config.system.requestBody)));
+ this.app.on("error", (err: any) => {
+ // 忽略连接重试、中断、管道、取消错误
+ if (["ECONNRESET", "ECONNABORTED", "EPIPE", "ECANCELED"].includes(err.code)) return;
+ logger.error(err);
+ });
+ logger.success("Server initialized");
+ }
+
+ /**
+ * 附加路由
+ *
+ * @param routes 路由列表
+ */
+ attachRoutes(routes: any[]) {
+ routes.forEach((route: any) => {
+ const prefix = route.prefix || "";
+ for (let method in route) {
+ if(method === "prefix") continue;
+ if (!_.isObject(route[method])) {
+ logger.warn(`Router ${prefix} ${method} invalid`);
+ continue;
+ }
+ for (let uri in route[method]) {
+ this.router[method](`${prefix}${uri}`, async ctx => {
+ const { request, response } = await this.#requestProcessing(ctx, route[method][uri]);
+ if(response != null && config.system.requestLog)
+ logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`);
+ });
+ }
+ }
+ logger.info(`Route ${config.service.urlPrefix || ""}${prefix} attached`);
+ });
+ this.app.use(this.router.routes());
+ this.app.use((ctx: any) => {
+ const request = new Request(ctx);
+ logger.debug(`-> ${ctx.request.method} ${ctx.request.url} request is not supported - ${request.remoteIP || "unknown"}`);
+ // const failureBody = new FailureBody(new Exception(EX.SYSTEM_NOT_ROUTE_MATCHING, "Request is not supported"));
+ // const response = new Response(failureBody);
+ const message = `[请求有误]: 正确请求为 POST -> /v1/chat/completions,当前请求为 ${ctx.request.method} -> ${ctx.request.url} 请纠正`;
+ logger.warn(message);
+ const failureBody = new FailureBody(new Error(message));
+ const response = new Response(failureBody);
+ response.injectTo(ctx);
+ if(config.system.requestLog)
+ logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`);
+ });
+ }
+
+ /**
+ * 请求处理
+ *
+ * @param ctx 上下文
+ * @param routeFn 路由方法
+ */
+ #requestProcessing(ctx: any, routeFn: Function): Promise {
+ return new Promise(resolve => {
+ const request = new Request(ctx);
+ try {
+ if(config.system.requestLog)
+ logger.info(`-> ${request.method} ${request.url}`);
+ routeFn(request)
+ .then(response => {
+ try {
+ if(!Response.isInstance(response)) {
+ const _response = new Response(response);
+ _response.injectTo(ctx);
+ return resolve({ request, response: _response });
+ }
+ response.injectTo(ctx);
+ resolve({ request, response });
+ }
+ catch(err) {
+ logger.error(err);
+ const failureBody = new FailureBody(err);
+ const response = new Response(failureBody);
+ response.injectTo(ctx);
+ resolve({ request, response });
+ }
+ })
+ .catch(err => {
+ try {
+ logger.error(err);
+ const failureBody = new FailureBody(err);
+ const response = new Response(failureBody);
+ response.injectTo(ctx);
+ resolve({ request, response });
+ }
+ catch(err) {
+ logger.error(err);
+ const failureBody = new FailureBody(err);
+ const response = new Response(failureBody);
+ response.injectTo(ctx);
+ resolve({ request, response });
+ }
+ });
+ }
+ catch(err) {
+ logger.error(err);
+ const failureBody = new FailureBody(err);
+ const response = new Response(failureBody);
+ response.injectTo(ctx);
+ resolve({ request, response });
+ }
+ });
+ }
+
+ /**
+ * 监听端口
+ */
+ async listen() {
+ const host = config.service.host;
+ const port = config.service.port;
+ await Promise.all([
+ new Promise((resolve, reject) => {
+ if(host === "0.0.0.0" || host === "localhost" || host === "127.0.0.1")
+ return resolve(null);
+ this.app.listen(port, "localhost", err => {
+ if(err) return reject(err);
+ resolve(null);
+ });
+ }),
+ new Promise((resolve, reject) => {
+ this.app.listen(port, host, err => {
+ if(err) return reject(err);
+ resolve(null);
+ });
+ })
+ ]);
+ logger.success(`Server listening on port ${port} (${host})`);
+ }
+
+}
+
+export default new Server();
\ No newline at end of file
diff --git a/src/lib/util.ts b/src/lib/util.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0f3fd1661a2a9861ac344a36599f9bf5ac12b1d8
--- /dev/null
+++ b/src/lib/util.ts
@@ -0,0 +1,307 @@
+import os from "os";
+import path from "path";
+import crypto from "crypto";
+import { Readable, Writable } from "stream";
+
+import "colors";
+import mime from "mime";
+import axios from "axios";
+import fs from "fs-extra";
+import { v1 as uuid } from "uuid";
+import { format as dateFormat } from "date-fns";
+import CRC32 from "crc-32";
+import randomstring from "randomstring";
+import _ from "lodash";
+import { CronJob } from "cron";
+
+import HTTP_STATUS_CODE from "./http-status-codes.ts";
+
+const autoIdMap = new Map();
+
+const util = {
+ is2DArrays(value: any) {
+ return (
+ _.isArray(value) &&
+ (!value[0] || (_.isArray(value[0]) && _.isArray(value[value.length - 1])))
+ );
+ },
+
+ uuid: (separator = true) => (separator ? uuid() : uuid().replace(/\-/g, "")),
+
+ autoId: (prefix = "") => {
+ let index = autoIdMap.get(prefix);
+ if (index > 999999) index = 0; //超过最大数字则重置为0
+ autoIdMap.set(prefix, (index || 0) + 1);
+ return `${prefix}${index || 1}`;
+ },
+
+ ignoreJSONParse(value: string) {
+ const result = _.attempt(() => JSON.parse(value));
+ if (_.isError(result)) return null;
+ return result;
+ },
+
+ generateRandomString(options: any): string {
+ return randomstring.generate(options);
+ },
+
+ getResponseContentType(value: any): string | null {
+ return value.headers
+ ? value.headers["content-type"] || value.headers["Content-Type"]
+ : null;
+ },
+
+ mimeToExtension(value: string) {
+ let extension = mime.getExtension(value);
+ if (extension == "mpga") return "mp3";
+ return extension;
+ },
+
+ extractURLExtension(value: string) {
+ const extname = path.extname(new URL(value).pathname);
+ return extname.substring(1).toLowerCase();
+ },
+
+ createCronJob(cronPatterns: any, callback?: Function) {
+ if (!_.isFunction(callback))
+ throw new Error("callback must be an Function");
+ return new CronJob(
+ cronPatterns,
+ () => callback(),
+ null,
+ false,
+ "Asia/Shanghai"
+ );
+ },
+
+ getDateString(format = "yyyy-MM-dd", date = new Date()) {
+ return dateFormat(date, format);
+ },
+
+ getIPAddressesByIPv4(): string[] {
+ const interfaces = os.networkInterfaces();
+ const addresses = [];
+ for (let name in interfaces) {
+ const networks = interfaces[name];
+ const results = networks.filter(
+ (network) =>
+ network.family === "IPv4" &&
+ network.address !== "127.0.0.1" &&
+ !network.internal
+ );
+ if (results[0] && results[0].address) addresses.push(results[0].address);
+ }
+ return addresses;
+ },
+
+ getMACAddressesByIPv4(): string[] {
+ const interfaces = os.networkInterfaces();
+ const addresses = [];
+ for (let name in interfaces) {
+ const networks = interfaces[name];
+ const results = networks.filter(
+ (network) =>
+ network.family === "IPv4" &&
+ network.address !== "127.0.0.1" &&
+ !network.internal
+ );
+ if (results[0] && results[0].mac) addresses.push(results[0].mac);
+ }
+ return addresses;
+ },
+
+ generateSSEData(event?: string, data?: string, retry?: number) {
+ return `event: ${event || "message"}\ndata: ${(data || "")
+ .replace(/\n/g, "\\n")
+ .replace(/\s/g, "\\s")}\nretry: ${retry || 3000}\n\n`;
+ },
+
+ buildDataBASE64(type, ext, buffer) {
+ return `data:${type}/${ext.replace("jpg", "jpeg")};base64,${buffer.toString(
+ "base64"
+ )}`;
+ },
+
+ isLinux() {
+ return os.platform() !== "win32";
+ },
+
+ isIPAddress(value) {
+ return (
+ _.isString(value) &&
+ (/^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/.test(
+ value
+ ) ||
+ /\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*/.test(
+ value
+ ))
+ );
+ },
+
+ isPort(value) {
+ return _.isNumber(value) && value > 0 && value < 65536;
+ },
+
+ isReadStream(value): boolean {
+ return (
+ value &&
+ (value instanceof Readable || "readable" in value || value.readable)
+ );
+ },
+
+ isWriteStream(value): boolean {
+ return (
+ value &&
+ (value instanceof Writable || "writable" in value || value.writable)
+ );
+ },
+
+ isHttpStatusCode(value) {
+ return _.isNumber(value) && Object.values(HTTP_STATUS_CODE).includes(value);
+ },
+
+ isURL(value) {
+ return !_.isUndefined(value) && /^(http|https)/.test(value);
+ },
+
+ isSrc(value) {
+ return !_.isUndefined(value) && /^\/.+\.[0-9a-zA-Z]+(\?.+)?$/.test(value);
+ },
+
+ isBASE64(value) {
+ return !_.isUndefined(value) && /^[a-zA-Z0-9\/\+]+(=?)+$/.test(value);
+ },
+
+ isBASE64Data(value) {
+ return /^data:/.test(value);
+ },
+
+ extractBASE64DataFormat(value): string | null {
+ const match = value.trim().match(/^data:(.+);base64,/);
+ if (!match) return null;
+ return match[1];
+ },
+
+ removeBASE64DataHeader(value): string {
+ return value.replace(/^data:(.+);base64,/, "");
+ },
+
+ isDataString(value): boolean {
+ return /^(base64|json):/.test(value);
+ },
+
+ isStringNumber(value) {
+ return _.isFinite(Number(value));
+ },
+
+ isUnixTimestamp(value) {
+ return /^[0-9]{10}$/.test(`${value}`);
+ },
+
+ isTimestamp(value) {
+ return /^[0-9]{13}$/.test(`${value}`);
+ },
+
+ isEmail(value) {
+ return /^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/.test(
+ value
+ );
+ },
+
+ isAsyncFunction(value) {
+ return Object.prototype.toString.call(value) === "[object AsyncFunction]";
+ },
+
+ async isAPNG(filePath) {
+ let head;
+ const readStream = fs.createReadStream(filePath, { start: 37, end: 40 });
+ const readPromise = new Promise((resolve, reject) => {
+ readStream.once("end", resolve);
+ readStream.once("error", reject);
+ });
+ readStream.once("data", (data) => (head = data));
+ await readPromise;
+ return head.compare(Buffer.from([0x61, 0x63, 0x54, 0x4c])) === 0;
+ },
+
+ unixTimestamp() {
+ return parseInt(`${Date.now() / 1000}`);
+ },
+
+ timestamp() {
+ return Date.now();
+ },
+
+ urlJoin(...values) {
+ let url = "";
+ for (let i = 0; i < values.length; i++)
+ url += `${i > 0 ? "/" : ""}${values[i]
+ .replace(/^\/*/, "")
+ .replace(/\/*$/, "")}`;
+ return url;
+ },
+
+ millisecondsToHmss(milliseconds) {
+ if (_.isString(milliseconds)) return milliseconds;
+ milliseconds = parseInt(milliseconds);
+ const sec = Math.floor(milliseconds / 1000);
+ const hours = Math.floor(sec / 3600);
+ const minutes = Math.floor((sec - hours * 3600) / 60);
+ const seconds = sec - hours * 3600 - minutes * 60;
+ const ms = (milliseconds % 60000) - seconds * 1000;
+ return `${hours > 9 ? hours : "0" + hours}:${
+ minutes > 9 ? minutes : "0" + minutes
+ }:${seconds > 9 ? seconds : "0" + seconds}.${ms}`;
+ },
+
+ millisecondsToTimeString(milliseconds) {
+ if (milliseconds < 1000) return `${milliseconds}ms`;
+ if (milliseconds < 60000)
+ return `${parseFloat((milliseconds / 1000).toFixed(2))}s`;
+ return `${Math.floor(milliseconds / 1000 / 60)}m${Math.floor(
+ (milliseconds / 1000) % 60
+ )}s`;
+ },
+
+ rgbToHex(r, g, b): string {
+ return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
+ },
+
+ hexToRgb(hex) {
+ const value = parseInt(hex.replace(/^#/, ""), 16);
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
+ },
+
+ md5(value) {
+ return crypto.createHash("md5").update(value).digest("hex");
+ },
+
+ crc32(value) {
+ return _.isBuffer(value) ? CRC32.buf(value) : CRC32.str(value);
+ },
+
+ arrayParse(value): any[] {
+ return _.isArray(value) ? value : [value];
+ },
+
+ booleanParse(value) {
+ return value === "true" || value === true ? true : false;
+ },
+
+ encodeBASE64(value) {
+ return Buffer.from(value).toString("base64");
+ },
+
+ decodeBASE64(value) {
+ return Buffer.from(value, "base64").toString();
+ },
+
+ async fetchFileBASE64(url: string) {
+ const result = await axios.get(url, {
+ responseType: "arraybuffer",
+ });
+ return result.data.toString("base64");
+ },
+};
+
+export default util;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..b6477c382e915c5d97abb5c5e03501122b29b0b8
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "allowImportingTsExtensions": true,
+ "allowSyntheticDefaultImports": true,
+ "noEmit": true,
+ "paths": {
+ "@/*": ["src/*"]
+ },
+ "outDir": "./dist"
+ },
+ "include": ["src/**/*", "libs.d.ts"],
+ "exclude": ["node_modules", "dist"]
+}
\ No newline at end of file
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000000000000000000000000000000000000..74f98bcf40cdd3125abfaf57aec12cc52fcc7c9a
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,27 @@
+{
+ "builds": [
+ {
+ "src": "./dist/*.html",
+ "use": "@vercel/static"
+ },
+ {
+ "src": "./dist/index.js",
+ "use": "@vercel/node"
+ }
+ ],
+ "routes": [
+ {
+ "src": "/",
+ "dest": "/dist/welcome.html"
+ },
+ {
+ "src": "/(.*)",
+ "dest": "/dist",
+ "headers": {
+ "Access-Control-Allow-Credentials": "true",
+ "Access-Control-Allow-Methods": "GET,OPTIONS,PATCH,DELETE,POST,PUT",
+ "Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, Authorization"
+ }
+ }
+ ]
+}
\ No newline at end of file