diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..d525fd0e95be4ecec3a0cb125fd7e323cc407e64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +logs +dist +doc +node_modules +.vscode +.git +.gitignore +README.md +*.tar.gz \ No newline at end of file diff --git a/.gitignore b/.gitignore index bc9ddb2fb07ed1babe95a6299513d7bbbed3f3c6..ada3e9270c7cb4a9a5fbb77f1a6830b46a1ae136 100644 --- a/.gitignore +++ b/.gitignore @@ -1,185 +1,4 @@ -### JetBrains template -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### Go template -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work - -### GoLand template -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -/logs/ -/.env -/target/ -/CHANGELOG.md +dist/ +node_modules/ +logs/ +.vercel diff --git a/Dockerfile b/Dockerfile index b6ae84c81107f21fb62e5bbe9960f52492324b16..77f6e7dce7fc4edb9aebb237ca5956eab0322c63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,21 @@ -FROM golang:1.21 AS builder - -ENV CGO_ENABLED=0 +FROM node:lts AS BUILD_IMAGE WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download +COPY . /app -COPY . . -RUN go build -o /app/free-gpt3.5-2api . +RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build -FROM alpine:latest +FROM node:lts-alpine -WORKDIR /app +COPY --from=BUILD_IMAGE /app/configs /app/configs +COPY --from=BUILD_IMAGE /app/package.json /app/package.json +COPY --from=BUILD_IMAGE /app/dist /app/dist +COPY --from=BUILD_IMAGE /app/public /app/public +COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules -COPY --from=builder /app/free-gpt3.5-2api /app/free-gpt3.5-2api +WORKDIR /app -EXPOSE 3040 +EXPOSE 8000 -CMD [ "./free-gpt3.5-2api" ] +CMD ["npm", "start"] \ No newline at end of file diff --git a/FreeGpt35/FreeGpt35.go b/FreeGpt35/FreeGpt35.go deleted file mode 100644 index 790720dd1ffa41c102ee3ee470974eba0480f2b4..0000000000000000000000000000000000000000 --- a/FreeGpt35/FreeGpt35.go +++ /dev/null @@ -1,231 +0,0 @@ -package FreeGpt35 - -import ( - "encoding/json" - "fmt" - ProofWork2 "free-gpt3.5-2api/ProofWork" - "free-gpt3.5-2api/ProxyPool" - "free-gpt3.5-2api/RequestClient" - "free-gpt3.5-2api/common" - "free-gpt3.5-2api/config" - "free-gpt3.5-2api/constant" - "github.com/aurorax-neo/go-logger" - fhttp "github.com/bogdanfinn/fhttp" - "github.com/google/uuid" - "io" -) - -var ( - BaseUrl = config.BaseUrl - ChatUrl = BaseUrl + "/backend-anon/conversation" - AuthUrl = BaseUrl + "/backend-anon/sentinel/chat-requirements" - OfficialBaseURLS = []string{"https://chat.openai.com", "https://chatgpt.com"} -) - -// NewFreeAuthType 定义一个枚举类型 -type NewFreeAuthType int - -const ( - NewFreeAuthNormal NewFreeAuthType = 0 //正常获取 - NewFreeAuthRefresh NewFreeAuthType = 1 // 刷新获取 -) - -type FreeGpt35 struct { - RequestClient RequestClient.RequestClient - Proxy *ProxyPool.Proxy - MaxUseCount int - ExpiresAt int64 - FreeAuth *freeAuth - Ua string - Cookies []*fhttp.Cookie -} - -type freeAuth struct { - OaiDeviceId string `json:"-"` - Persona string `json:"persona"` - Arkose arkose `json:"arkose"` - Turnstile turnstile `json:"turnstile"` - ProofWork ProofWork2.ProofWork `json:"proofofwork"` - Token string `json:"token"` -} - -type arkose struct { - Required bool `json:"required"` - Dx string `json:"dx"` -} - -type turnstile struct { - Required bool `json:"required"` -} - -// NewFreeGpt35 创建 FreeGpt35 实例 0 无论网络是否被标记限制都获取 1 在网络未标记时才能获取 -func NewFreeGpt35(newType NewFreeAuthType, maxUseCount int, expiresAt int64) *FreeGpt35 { - // 创建 FreeGpt35 实例 - freeGpt35 := &FreeGpt35{ - MaxUseCount: maxUseCount, - ExpiresAt: expiresAt, - FreeAuth: &freeAuth{}, - } - // 获取请求客户端 - err := freeGpt35.newRequestClient() - if err != nil { - logger.Logger.Debug(err.Error()) - return nil - } - // 获取并设置代理 - err = freeGpt35.getProxy(newType) - if err != nil { - logger.Logger.Debug(err.Error()) - return nil - } - // 获取cookies - if common.IsStrInArray(BaseUrl, OfficialBaseURLS) { - err = freeGpt35.getCookies() - if err != nil { - logger.Logger.Debug(err.Error()) - return nil - } - } - // 获取 FreeAuth - err = freeGpt35.newFreeAuth(newType) - if err != nil { - logger.Logger.Debug(err.Error()) - return nil - } - return freeGpt35 -} - -func (FG *FreeGpt35) NewRequest(method, url string, body io.Reader) (*fhttp.Request, error) { - request, err := RequestClient.NewRequest(method, url, body) - if err != nil { - return nil, err - } - request.Header.Set("accept", "*/*") - request.Header.Set("accept-language", "zh-CN,zh;q=0.9,zh-Hans;q=0.8,en;q=0.7") - for _, cookie := range FG.Cookies { - request.AddCookie(cookie) - } - request.Header.Set("oai-language", "en-US") - request.Header.Set("origin", common.GetOrigin(url)) - request.Header.Set("referer", common.GetOrigin(url)) - request.Header.Set("sec-ch-ua", `"Microsoft Edge";v="123", "Not:A-Brand";v="8", "Chromium";v="123"`) - request.Header.Set("sec-ch-ua-mobile", "?0") - request.Header.Set("sec-ch-ua-platform", `"Windows"`) - request.Header.Set("sec-fetch-dest", "empty") - request.Header.Set("sec-fetch-mode", "cors") - request.Header.Set("sec-fetch-site", "same-origin") - request.Header.Set("user-agent", FG.Ua) - return request, nil -} - -func (FG *FreeGpt35) newRequestClient() error { - // 请求客户端 - FG.RequestClient = RequestClient.NewTlsClient(300, constant.ClientProfile) - if FG.RequestClient == nil { - errStr := fmt.Sprint("RequestClient is nil") - logger.Logger.Debug(errStr) - return fmt.Errorf(errStr) - } - return nil -} - -func (FG *FreeGpt35) getProxy(newFreeAuthType NewFreeAuthType) error { - // 获取代理池 - ProxyPoolInstance := ProxyPool.GetProxyPoolInstance() - // 获取代理 - FG.Proxy = ProxyPoolInstance.GetProxy() - // 判断代理是否可用 - if FG.Proxy.CanUseAt > common.GetTimestampSecond(0) && newFreeAuthType == NewFreeAuthRefresh { - errStr := fmt.Sprint(FG.Proxy.Link, ": Proxy restricted, Reuse at ", FG.Proxy.CanUseAt) - return fmt.Errorf(errStr) - } - // Ua - FG.Ua = FG.Proxy.Ua - // 补全cookies - FG.Cookies = append(FG.Cookies, FG.Proxy.Cookies...) - // 设置代理 - err := FG.RequestClient.SetProxy(FG.Proxy.Link.String()) - if err != nil { - errStr := fmt.Sprint("SetProxy Error: ", err) - logger.Logger.Debug(errStr) - } - return nil -} - -func (FG *FreeGpt35) getCookies() error { - // 获取cookies - request, err := FG.NewRequest("GET", fmt.Sprint(BaseUrl, "/?oai-dm=1"), nil) - if err != nil { - return err - } - // 设置请求头 - request.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") - // 发送 GET 请求 - response, err := FG.RequestClient.Do(request) - if err != nil { - return err - } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(response.Body) - if response.StatusCode != 200 { - return fmt.Errorf("StatusCode: %d", response.StatusCode) - } - // 获取cookies - cookies := response.Cookies() - for i, cookie := range cookies { - if cookie.Name == "oai-did" { - FG.FreeAuth.OaiDeviceId = cookie.Value - cookies = append(cookies[:i], cookies[i+1:]...) - } - if cookie.Name == "__Secure-next-auth.callback-url" { - cookie.Value = BaseUrl - } - } - // 设置cookies - FG.Cookies = append(FG.Cookies, cookies...) - return nil -} - -func (FG *FreeGpt35) newFreeAuth(newFreeAuthType NewFreeAuthType) error { - // 生成新的设备 ID - if FG.FreeAuth.OaiDeviceId == "" { - FG.FreeAuth.OaiDeviceId = uuid.New().String() - } - // 创建请求 - request, err := FG.NewRequest("POST", AuthUrl, nil) - if err != nil { - return err - } - // 设置请求头 - request.Header.Set("Content-Type", "application/json") - request.Header.Set("oai-device-id", FG.FreeAuth.OaiDeviceId) - // 发送 POST 请求 - response, err := FG.RequestClient.Do(request) - if err != nil { - return err - } - if response.StatusCode != 200 { - logger.Logger.Debug(fmt.Sprint("newFreeAuth: StatusCode: ", response.StatusCode)) - if (response.StatusCode == 429 || response.StatusCode == 403) && newFreeAuthType == NewFreeAuthRefresh { - FG.Proxy.CanUseAt = common.GetTimestampSecond(300) - logger.Logger.Debug(fmt.Sprint("newFreeAuth: Proxy(", FG.Proxy.Link, ")restricted, Reuse at ", FG.Proxy.CanUseAt)) - } - return fmt.Errorf("StatusCode: %d", response.StatusCode) - } else if newFreeAuthType == 0 { - // 成功后更新代理的可用时间 - FG.Proxy.CanUseAt = common.GetTimestampSecond(0) - logger.Logger.Debug(fmt.Sprint("newFreeAuth: Proxy(", FG.Proxy.Link, ")Reuse at ", FG.Proxy.CanUseAt)) - } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(response.Body) - if err := json.NewDecoder(response.Body).Decode(&FG.FreeAuth); err != nil { - return err - } - // ProofWork - if FG.FreeAuth.ProofWork.Required { - FG.FreeAuth.ProofWork.Ospt = ProofWork2.CalcProofToken(FG.FreeAuth.ProofWork.Seed, FG.FreeAuth.ProofWork.Difficulty, request.Header.Get("User-Agent")) - } - return nil -} diff --git a/FreeGpt35Pool/FreeGpt35Pool.go b/FreeGpt35Pool/FreeGpt35Pool.go deleted file mode 100644 index 8d5c27d271f406e33fc9ec6dbcd2d002f9b1b9f7..0000000000000000000000000000000000000000 --- a/FreeGpt35Pool/FreeGpt35Pool.go +++ /dev/null @@ -1,128 +0,0 @@ -package FreeGpt35Pool - -import ( - "fmt" - "free-gpt3.5-2api/FreeGpt35" - "free-gpt3.5-2api/common" - "free-gpt3.5-2api/config" - "free-gpt3.5-2api/queue" - "github.com/aurorax-neo/go-logger" - "sync" - "time" -) - -var ( - instance *FreeGpt35Pool - once sync.Once -) - -type FreeGpt35Pool struct { - queue *queue.Queue - capacity int // 队列容量 -} - -func newFreeGpt35Pool(capacity int) *FreeGpt35Pool { - return &FreeGpt35Pool{ - queue: queue.New(), - capacity: capacity, - } -} - -func GetFreeGpt35PoolInstance() *FreeGpt35Pool { - once.Do(func() { - logger.Logger.Info(fmt.Sprint("Init FreeGpt35Pool...")) - // 初始化 FreeGpt35Pool - instance = newFreeGpt35Pool(config.PoolMaxCount) - // 定时刷新 FreeGpt35Pool - instance.refreshFreeGpt35Pool(time.Millisecond * 256) - // - logger.Logger.Info(fmt.Sprint("Init FreeGpt35Pool Success", ", PoolMaxCount: ", config.PoolMaxCount, ", AuthExpirationDate: ", config.AuthED)) - }) - return instance -} - -func (G *FreeGpt35Pool) refreshFreeGpt35Pool(sleep time.Duration) { - // 检测 FreeGpt35Pool 是否已满 - common.AsyncLoopTask(sleep, func() { - // 判断 FreeGpt35Pool 是否已满 - if G.IsFull() { - return - } - // 获取新 FreeGpt35 实例 - gpt35 := FreeGpt35.NewFreeGpt35(FreeGpt35.NewFreeAuthRefresh, 1, common.GetTimestampSecond(config.AuthED)) - // 判断 FreeGpt35 实例是否有效 - if G.isLiveGpt35(gpt35) { - // 入队新 FreeGpt35 实例 - G.AddFreeGpt35(gpt35) - } - }) - // 检测并移除无效 FreeGpt35 实例 - common.AsyncLoopTask(sleep, func() { - // 遍历队列中的所有元素 - G.queue.Traverse(func(n *queue.Node) { - // 判断是否为无效 FreeGpt35 实例 - if !G.isLiveGpt35(n.Value.(*FreeGpt35.FreeGpt35)) { - // 移除无效 FreeGpt35 实例 - G.queue.Remove(n) - } - }) - }) -} - -func (G *FreeGpt35Pool) isLiveGpt35(gpt35 *FreeGpt35.FreeGpt35) bool { - //判断是否为空 - if gpt35 == nil || - gpt35.MaxUseCount <= 0 || //无可用次数 - gpt35.ExpiresAt <= common.GetTimestampSecond(0) { - return false - } - return true -} - -func (G *FreeGpt35Pool) GetFreeGpt35(retry int) *FreeGpt35.FreeGpt35 { - // 获取 FreeGpt35 实例 - n := G.queue.Peek() - if n != nil { - gpt35 := n.Value.(*FreeGpt35.FreeGpt35) - if G.isLiveGpt35(gpt35) { //有缓存 - // 深拷贝 - gpt35_ := common.DeepCopyStruct(gpt35).(*FreeGpt35.FreeGpt35) - // 减少 FreeGpt35 实例的最大使用次数 - gpt35.MaxUseCount-- - // 判断 FreeGpt35 实例是否有效 无效则移除 - if !G.isLiveGpt35(gpt35) { - G.queue.Dequeue() - } - return gpt35_ - } else if retry > 0 { - time.Sleep(time.Millisecond * 128) - return G.GetFreeGpt35(retry - 1) - } - } - // 缓存内无可用 FreeGpt35 实例,返回新 FreeGpt35 实例 - return FreeGpt35.NewFreeGpt35(FreeGpt35.NewFreeAuthNormal, 1, common.GetTimestampSecond(config.AuthED)) -} - -// GetSize 获取队列当前元素个数 -func (G *FreeGpt35Pool) GetSize() int { - return G.queue.Len() -} - -// GetCapacity 获取队列容量 -func (G *FreeGpt35Pool) GetCapacity() int { - return G.capacity -} - -// IsFull 检查队列是否已满 -func (G *FreeGpt35Pool) IsFull() bool { - return G.GetSize() == G.capacity -} - -// AddFreeGpt35 入队 -func (G *FreeGpt35Pool) AddFreeGpt35(v *FreeGpt35.FreeGpt35) bool { - if G.IsFull() || v == nil { - return false - } - G.queue.Enqueue(v) - return true -} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/ProofWork/ProofWork.go b/ProofWork/ProofWork.go deleted file mode 100644 index 12d5cc363e1630f22cc3b3825b3abc8ea23b03e8..0000000000000000000000000000000000000000 --- a/ProofWork/ProofWork.go +++ /dev/null @@ -1,55 +0,0 @@ -package ProofWork - -import ( - "encoding/base64" - "encoding/hex" - "encoding/json" - "golang.org/x/crypto/sha3" - "math/rand" - "time" -) - -var ( - numberCollisions = 100000 - cores = []int{8, 12, 16, 24} - screens = []int{3000, 4000, 6000} - timeLayout = "Mon Jan 2 2006 15:04:05" -) - -type ProofWork struct { - Difficulty string `json:"difficulty,omitempty"` - Required bool `json:"required"` - Seed string `json:"seed,omitempty"` - Ospt string `json:"-"` -} - -func getParseTime() string { - now := time.Now() - return now.Format(timeLayout) + " GMT" + now.Format("-0700 MST (MST)") -} - -func getConfig(userAgent string) []interface{} { - rand.New(rand.NewSource(time.Now().UnixNano())) - core := cores[rand.Intn(4)] - rand.New(rand.NewSource(time.Now().UnixNano())) - screen := screens[rand.Intn(3)] - return []interface{}{core + screen, getParseTime(), int64(4294705152), 0, userAgent} - -} - -func CalcProofToken(seed string, diff string, userAgent string) string { - config := getConfig(userAgent) - hasher := sha3.New512() - for i := 0; i < numberCollisions; i++ { - config[3] = i - jsonStr, _ := json.Marshal(config) - base := base64.StdEncoding.EncodeToString(jsonStr) - hasher.Write([]byte(seed + base)) - hash := hasher.Sum(nil) - hasher.Reset() - if hex.EncodeToString(hash[:len(diff)]) <= diff { - return "gAAAAAB" + base - } - } - return "gAAAAABwQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + base64.StdEncoding.EncodeToString([]byte(`"`+seed+`"`)) -} diff --git a/ProxyPool/ProxyPool.go b/ProxyPool/ProxyPool.go deleted file mode 100644 index c512968654b22c9e069714ad4caca8640664149f..0000000000000000000000000000000000000000 --- a/ProxyPool/ProxyPool.go +++ /dev/null @@ -1,87 +0,0 @@ -package ProxyPool - -import ( - "fmt" - "free-gpt3.5-2api/common" - "free-gpt3.5-2api/config" - "free-gpt3.5-2api/constant" - "github.com/aurorax-neo/go-logger" - fhttp "github.com/bogdanfinn/fhttp" - "net/url" - "sync" - "time" -) - -var ( - Instance *ProxyPool - Once sync.Once -) - -type ProxyPool struct { - Proxies []*Proxy - Index int -} - -type Proxy struct { - Link *url.URL - CanUseAt int64 - Ua string - Cookies []*fhttp.Cookie -} - -func GetProxyPoolInstance() *ProxyPool { - Once.Do(func() { - logger.Logger.Info(fmt.Sprint("Init ProxyPool...")) - // 初始化 ProxyPool - Instance = NewProxyPool(nil) - // 遍历配置文件中的代理 添加到代理池 - for _, px := range config.Proxy { - proxy := NewProxy(px, common.GetTimestampSecond(0), constant.Ua) - _ = proxy.getCookies() - Instance.AddProxy(proxy) - } - //定时刷新代理cookies - common.AsyncLoopTask(1*time.Minute, func() { - for _, proxy := range Instance.Proxies { - _ = proxy.getCookies() - } - }) - logger.Logger.Info(fmt.Sprint("Init ProxyPool Success")) - }) - return Instance -} - -func NewProxyPool(proxies []*Proxy) *ProxyPool { - proxy := NewProxy("", common.GetTimestampSecond(0), constant.Ua) - _ = proxy.getCookies() - return &ProxyPool{ - Proxies: append([]*Proxy{proxy}, proxies...), - Index: 0, - } -} - -func (PP *ProxyPool) GetProxy() *Proxy { - PP.Index = (PP.Index + 1) % len(PP.Proxies) - // 如果配置了代理 不会使用无代理 - if PP.Index == 0 && len(PP.Proxies) > 1 { - PP.Index = 1 - } - // 返回代理 - return PP.Proxies[PP.Index] -} - -func (PP *ProxyPool) AddProxy(proxy *Proxy) { - PP.Proxies = append(PP.Proxies, proxy) -} - -func NewProxy(link string, cannotUseTime int64, ua string) *Proxy { - return &Proxy{ - Link: common.ParseUrl(link), - CanUseAt: cannotUseTime, - Ua: ua, - } -} - -func (P *Proxy) getCookies() error { - return nil -} diff --git a/README.md b/README.md index ff4a049f8f28a69e6000db27ee70a158a7ad76d3..d12761d7e9b9d95f36bc4115675adbc9b73148a8 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,529 @@ -# [free-gpt3.5-2api](https://github.com/aurorax-neo/free-gpt3.5-2api) +# GLM AI Free 服务 -## 接口 +[![](https://img.shields.io/github/license/llm-red-team/glm-free-api.svg)](LICENSE) +![](https://img.shields.io/github/stars/llm-red-team/glm-free-api.svg) +![](https://img.shields.io/github/forks/llm-red-team/glm-free-api.svg) +![](https://img.shields.io/docker/pulls/vinlic/glm-free-api.svg) -#### /v1/tokens +支持高速流式输出、支持多轮对话、支持智能体对话、支持AI绘图、支持联网搜索、支持长文档解读、支持图像解析,零配置部署,多路token支持,自动清理会话痕迹。 +与ChatGPT接口完全兼容。 + +还有以下七个free-api欢迎关注: + +Moonshot AI(Kimi.ai)接口转API [kimi-free-api](https://github.com/LLM-Red-Team/kimi-free-api) + +阶跃星辰 (跃问StepChat) 接口转API [step-free-api](https://github.com/LLM-Red-Team/step-free-api) + +阿里通义 (Qwen) 接口转API [qwen-free-api](https://github.com/LLM-Red-Team/qwen-free-api) + +秘塔AI (Metaso) 接口转API [metaso-free-api](https://github.com/LLM-Red-Team/metaso-free-api) + +讯飞星火(Spark)接口转API [spark-free-api](https://github.com/LLM-Red-Team/spark-free-api) + +MiniMax(海螺AI)接口转API [hailuo-free-api](https://github.com/LLM-Red-Team/hailuo-free-api) + +聆心智能 (Emohaa) 接口转API [emohaa-free-api](https://github.com/LLM-Red-Team/emohaa-free-api) + +## 目录 + +* [免责声明](#免责声明) +* [在线体验](#在线体验) +* [效果示例](#效果示例) +* [接入准备](#接入准备) + * [智能体接入](#智能体接入) + * [多账号接入](#多账号接入) +* [Docker部署](#Docker部署) + * [Docker-compose部署](#Docker-compose部署) +* [Render部署](#Render部署) +* [Vercel部署](#Vercel部署) +* [原生部署](#原生部署) +* [推荐使用客户端](#推荐使用客户端) +* [接口列表](#接口列表) + * [对话补全](#对话补全) + * [AI绘图](#AI绘图) + * [文档解读](#文档解读) + * [图像解析](#图像解析) + * [refresh_token存活检测](#refresh_token存活检测) +* [注意事项](#注意事项) + * [Nginx反代优化](#Nginx反代优化) + * [Token统计](#Token统计) +* [Star History](#star-history) + +## 免责声明 + +**逆向API是不稳定的,建议前往智谱AI官方 https://open.bigmodel.cn/ 付费使用API,避免封禁的风险。** + +**本组织和个人不接受任何资金捐助和交易,此项目是纯粹研究交流学习性质!** + +**仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!** + +**仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!** + +**仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!** + +## 在线体验 + +此链接仅临时测试功能,只有一路并发,如果遇到异常请稍后重试,建议自行部署使用。 + +https://udify.app/chat/Pe89TtaX3rKXM8NS + +## 效果示例 + +### 验明正身Demo + +![验明正身](./doc/example-1.png) + +### 智能体对话Demo + +对应智能体链接:[网抑云评论生成器](https://chatglm.cn/main/gdetail/65c046a531d3fcb034918abe) + +![智能体对话](./doc/example-9.png) + +### 结合Dify工作流Demo + +体验地址:https://udify.app/chat/m46YgeVLNzFh4zRs + +image + +### 多轮对话Demo + +![多轮对话](./doc/example-6.png) + +### AI绘图Demo + +![AI绘图](./doc/example-10.png) + +### 联网搜索Demo + +![联网搜索](./doc/example-2.png) + +### 长文档解读Demo + +![长文档解读](./doc/example-5.png) + +### 代码调用Demo + +![代码调用](./doc/example-12.png) + +### 图像解析Demo + +![图像解析](./doc/example-3.png) + +## 接入准备 + +从 [智谱清言](https://chatglm.cn/) 获取refresh_token + +进入智谱清言随便发起一个对话,然后F12打开开发者工具,从Application > Cookies中找到`chatglm_refresh_token`的值,这将作为Authorization的Bearer Token值:`Authorization: Bearer TOKEN` + +![example0](./doc/example-0.png) + +### 智能体接入 + +打开智能体的聊天界面,地址栏的一串ID就是智能体的ID,复制下来备用,这个值将用作调用时的 `model` 参数值。 + +![example11](./doc/example-11.png) + +### 多账号接入 + +目前似乎限制同个账号同时只能有*一路*输出,你可以通过提供多个账号的chatglm_refresh_token并使用`,`拼接提供: + +`Authorization: Bearer TOKEN1,TOKEN2,TOKEN3` + +每次请求服务会从中挑选一个。 + +## Docker部署 + +请准备一台具有公网IP的服务器并将8000端口开放。 + +拉取镜像并启动服务 + +```shell +docker run -it -d --init --name glm-free-api -p 8000:8000 -e TZ=Asia/Shanghai vinlic/glm-free-api:latest ``` -curl --location --request GET 'http://127.0.0.1:9846/v1/tokens' \ ---header 'Authorization: Bearer abc' + +查看服务实时日志 + +```shell +docker logs -f glm-free-api ``` -返回示例说明:`count`为授权池中可用授权数,如果`count` 为 `0`请检查`ip`是否支持 `openai` +重启服务 +```shell +docker restart glm-free-api ``` -{ - "count": 0 -} + +停止服务 + +```shell +docker stop glm-free-api ``` -#### /v1/chat/completions +### Docker-compose部署 -###### 支持返回stream和json +```yaml +version: '3' +services: + glm-free-api: + container_name: glm-free-api + image: vinlic/glm-free-api:latest + restart: always + ports: + - "8000:8000" + environment: + - TZ=Asia/Shanghai ``` -http://:/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不可统计,将以固定数字返回。 -[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](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 +[![Star History Chart](https://api.star-history.com/svg?repos=LLM-Red-Team/glm-free-api&type=Date)](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) + ? `![图像](${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) + ? `![图像](${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