最近,chatgpt很吸引我,感觉很有意思,出于技术热情自己也搭建了一个
效果图如下:
使用前提:有openai账号,并且创建好api_key,注册事项可以参考 https://juejin.cn/post/7173447848292253704
配置文件说明
{
"api_key": "your api key",
"api_url": "",
"port": 8080,
"listen": "",
"bot_desc": "你是一个AI助手,我需要你模拟一名温柔贴心的女朋友来回答我的问题。",
"proxy": "http://host.docker.internal:10809",
"model": "gpt-3.5-turbo-0301",
"max_tokens": 512,
"temperature": 0.9,
"top_p": 1,
"frequency_penalty": 0.0,
"presence_penalty": 0.6,
"auth_user": "",
"auth_password": ""
}
api_key:openai api_key
api_url: openai api接口地址 不填使用默认 https://api.openai.com/v1
port: http服务端口
listen: http服务监听地址,不填默认监听0.0.0.0
proxy: openai请求代理,防墙。 例如 http://127.0.0.1:7890 socks5://127.0.0.1:7890
bot_desc:AI特征,非常重要,功能等同给与AI一个身份设定
max_tokens: GPT响应字符数,最大2048,默认值512。max_tokens会影响接口响应速度,字符越大响应越慢。
model: GPT选用模型,默认text-davinci-003,具体选项参考官网训练场
temperature: GPT热度,0到1,默认0.9。数字越大创造力越强,但更偏离训练事实,越低越接近训练事实
top_p: 使用温度采样的替代方法称为核心采样,其中模型考虑具有top_p概率质量的令牌的结果。因此,0.1 意味着只考虑包含前 10% 概率质量的代币。
frequency_penalty:
presence_penalty:
auth_user": http基本认证用户名(空表示不开启验证)
auth_password": http基本认证密码
代理服务器,如果不配置国内无法访问 https://api.openai.com/v1
代理服务器是一个中间服务器,可以使客户端在请求网络资源时间接地访问互联网。代理服务器常用于访问受限制的网站、匿名浏览和安全访问等场景。
在网络编程中,如果需要通过代理服务器进行 HTTP 请求,可以通过在 Transport 中指定一个代理地址,然后使用 http.ProxyURL() 函数将地址转换为 Proxy 对象。例如,如果代理地址为 “http://127.0.0.1:7890”,可以使用以下代码将其转换为 Proxy 对象:
proxyUrl, err := url.Parse("http://127.0.0.1:7890")
if err != nil {
log.Fatal(err)
}
proxy := http.ProxyURL(proxyUrl)
在创建 HTTP 客户端时,可以将这个代理对象设置到 Transport 中的 Proxy 字段中,如下所示:
client := &http.Client{
Transport: &http.Transport{
Proxy: proxy,
},
}
这样,在客户端进行 HTTP 请求时,就会使用指定的代理服务器。需要注意的是,代理地址的格式应该符合 URL 规范,例如上面的示例中,代理地址前面需要加上 “http://” 前缀。如果需要使用 socks5 协议的代理服务器,地址应该以 “socks5://” 开头。
关键代码如下:
package controllers
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/net/proxy"
"github.com/869413421/chatgpt-web/config"
"github.com/869413421/chatgpt-web/pkg/logger"
"github.com/gin-gonic/gin"
gogpt "github.com/sashabaranov/go-gpt3"
)
// ChatController 首页控制器
type ChatController struct {
BaseController
}
// NewChatController 创建控制器
func NewChatController() *ChatController {
return &ChatController{}
}
// Index 首页
func (c *ChatController) Index(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", gin.H{
"title": "Main website",
})
}
// Completion 回复
func (c *ChatController) Completion(ctx *gin.Context) {
var request gogpt.ChatCompletionRequest
err := ctx.BindJSON(&request)
if err != nil {
c.ResponseJson(ctx, http.StatusInternalServerError, err.Error(), nil)
return
}
logger.Info(request)
if len(request.Messages) == 0 {
c.ResponseJson(ctx, http.StatusBadRequest, "request messages required", nil)
return
}
cnf := config.LoadConfig()
gptConfig := gogpt.DefaultConfig(cnf.ApiKey)
if cnf.Proxy != "" {
transport := &http.Transport{}
if strings.HasPrefix(cnf.Proxy, "socks5h://") {
// 创建一个 DialContext 对象,并设置代理服务器
dialContext, err := newDialContext(cnf.Proxy[10:])
if err != nil {
panic(err)
}
transport.DialContext = dialContext
} else {
// 创建一个 HTTP Transport 对象,并设置代理服务器
proxyUrl, err := url.Parse(cnf.Proxy)
if err != nil {
panic(err)
}
transport.Proxy = http.ProxyURL(proxyUrl)
}
// 创建一个 HTTP 客户端,并将 Transport 对象设置为其 Transport 字段
gptConfig.HTTPClient = &http.Client{
Transport: transport,
}
}
// 自定义gptConfig.BaseURL
if cnf.ApiURL != "" {
gptConfig.BaseURL = cnf.ApiURL
}
client := gogpt.NewClientWithConfig(gptConfig)
if request.Messages[0].Role != "system" {
newMessage := append([]gogpt.ChatCompletionMessage{
{Role: "system", Content: cnf.BotDesc},
}, request.Messages...)
request.Messages = newMessage
logger.Info(request.Messages)
}
if cnf.Model == gogpt.GPT3Dot5Turbo0301 || cnf.Model == gogpt.GPT3Dot5Turbo {
request.Model = cnf.Model
resp, err := client.CreateChatCompletion(ctx, request)
if err != nil {
c.ResponseJson(ctx, http.StatusInternalServerError, err.Error(), nil)
return
}
c.ResponseJson(ctx, http.StatusOK, "", gin.H{
"reply": resp.Choices[0].Message.Content,
"messages": append(request.Messages, resp.Choices[0].Message),
})
} else {
prompt := ""
for _, item := range request.Messages {
prompt += item.Content + "/n"
}
prompt = strings.Trim(prompt, "/n")
logger.Info("request prompt is %s", prompt)
req := gogpt.CompletionRequest{
Model: cnf.Model,
MaxTokens: cnf.MaxTokens,
TopP: cnf.TopP,
FrequencyPenalty: cnf.FrequencyPenalty,
PresencePenalty: cnf.PresencePenalty,
Prompt: prompt,
}
resp, err := client.CreateCompletion(ctx, req)
if err != nil {
c.ResponseJson(ctx, http.StatusInternalServerError, err.Error(), nil)
return
}
c.ResponseJson(ctx, http.StatusOK, "", gin.H{
"reply": resp.Choices[0].Text,
"messages": append(request.Messages, gogpt.ChatCompletionMessage{
Role: "assistant",
Content: resp.Choices[0].Text,
}),
})
}
}
type dialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)
func newDialContext(socks5 string) (dialContextFunc, error) {
baseDialer := &net.Dialer{
Timeout: 60 * time.Second,
KeepAlive: 60 * time.Second,
}
if socks5 != "" {
// split socks5 proxy string [username:password@]host:port
var auth *proxy.Auth = nil
if strings.Contains(socks5, "@") {
proxyInfo := strings.SplitN(socks5, "@", 2)
proxyUser := strings.Split(proxyInfo[0], ":")
if len(proxyUser) == 2 {
auth = &proxy.Auth{
User: proxyUser[0],
Password: proxyUser[1],
}
}
socks5 = proxyInfo[1]
}
dialSocksProxy, err := proxy.SOCKS5("tcp", socks5, auth, baseDialer)
if err != nil {
return nil, err
}
contextDialer, ok := dialSocksProxy.(proxy.ContextDialer)
if !ok {
return nil, err
}
return contextDialer.DialContext, nil
} else {
return baseDialer.DialContext, nil
}
}