Skip to content

Commit

Permalink
抽象AI服务,添加支持通义千问
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinDai committed May 21, 2024
1 parent a6ad420 commit 7d8d224
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 118 deletions.
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

## 项目介绍

本项目是一个微信公众号项目,需配合微信公众号使用,在微信公众号配置本项目运行的服务器域名,用户关注公众号后,向公众号发送任意信息,公众号会根据用户发送的内容自动回复。
本项目是一个微信公众号项目,需配合微信公众号使用,在微信公众号配置本项目运行的服务器域名,用户关注公众号后,向公众号发送任意信息,公众号会根据用户发送的内容自动回复。

## 第三方依赖

Expand All @@ -20,18 +20,31 @@
## 支持的功能

+ [x] 自定义关键字回复内容
+ [x] 调用ChatGPT接口回复内容(需配置环境变量:`OPENAI_API_KEY`
+ [x] 调用OpenAI接口回复内容(需配置环境变量:`OPENAI_API_KEY`
+ [x] 调用通义千问接口回复内容(需配置环境变量:`DASHSCOPE_API_KEY`
+ [x] 调用图灵机器人(V2)接口回复内容(需配置环境变量:`TULING_API_KEY`

## 使用说明

1. 使用之前需要有微信公众号的帐号,没有的请戳[微信公众号申请](https://mp.weixin.qq.com/cgi-bin/readtemplate?t=register/step1_tmpl&lang=zh_CN)
2. 如果需要使用图灵机器人的回复内容则需要[注册图灵机器人帐号](http://tuling123.com/register/email.jhtml)获取相应的ApiKey并配置在环境变量中
3. 如果需要使用ChatGPT的回复内容则需要[创建OpenAI的API Key](https://platform.openai.com/account/api-keys)并配置在环境变量中
4. 可以通过配置环境变量`OPENAI_BASE_DOMAIN`更换访问OpenAI的域名
5. 可以通过配置环境变量`OPENAI_PROXY`使用代理服务访问OpenAI
6. 内容响应来源的优先级`自定义关键 > ChatGPT > 图灵机器人`
7. 在微信公众号后台配置回调URL为<https://wechatrobot.doodl6.com/weChat/receiveMessage>,其中`wechatrobot.doodl6.com`是你自己的域名,token与`config.yml`里面配置的保持一致即可
需要有微信公众号的帐号,没有的请戳[微信公众号申请](https://mp.weixin.qq.com/cgi-bin/readtemplate?t=register/step1_tmpl&lang=zh_CN)

内容响应来源的优先级`自定义关键字 > OpenAI > 通义千问 > 图灵机器人`

在微信公众号后台配置回调URL为<https://<your.domain>/weChat/receiveMessage>,其中`<your.domain>`替换成你自己的域名,token与`config.yml`里面配置的保持一致即可

### OpenAI

1. 如果需要使用OpenAI的回复内容则需要[创建OpenAI的API Key](https://platform.openai.com/account/api-keys)并配置在启动参数或者环境变量中
2. 可以通过配置启动参数或者环境变量`OPENAI_SERVER_URL`指定访问OpenAI服务的baseUrl
3. 可以通过配置启动参数或者环境变量`OPENAI_PROXY`使用代理服务访问OpenAI

### 通义千问

如果需要使用通义千问的回复内容则需要[创建通义千问的API Key](https://bailian.console.aliyun.com/#/api_key)并配置在启动参数或者环境变量中

### 图灵机器人

如果需要使用图灵机器人的回复内容则需要[注册图灵机器人帐号](http://tuling123.com/register/email.jhtml)获取相应的ApiKey并配置在启动参数或者环境变量中

## 本地开发

Expand Down
27 changes: 1 addition & 26 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ import (
"weChatRobot-go/pkg/model"
"weChatRobot-go/pkg/provider"
"weChatRobot-go/pkg/service"
"weChatRobot-go/pkg/third-party/chatgpt"
"weChatRobot-go/pkg/third-party/tuling"
"weChatRobot-go/pkg/util"
)

//go:embed static/templates
Expand Down Expand Up @@ -111,29 +108,7 @@ func setupRouter(config *model.Config) *gin.Engine {

router.GET("/", controller.IndexHandler)

openaiApiKey := os.Getenv("OPENAI_API_KEY")
var chatGPT *chatgpt.ChatGPT
if openaiApiKey != "" {
openaiBaseDomain := os.Getenv("OPENAI_BASE_DOMAIN")
if openaiBaseDomain != "" && !util.ValidateAddress(openaiBaseDomain) {
logger.Fatal("OPENAI_BASE_DOMAIN is not valid", "openaiBaseDomain", openaiBaseDomain)
}

openaiProxy := os.Getenv("OPENAI_PROXY")
if openaiProxy != "" && !util.ValidateAddress(openaiProxy) {
logger.Fatal("OPENAI_PROXY is not valid", "openaiBaseDomain", openaiBaseDomain)
}

chatGPT = chatgpt.NewChatGPT(openaiApiKey, openaiBaseDomain, openaiProxy)
}

var tl *tuling.Tuling
tulingApiKey := os.Getenv("TULING_API_KEY")
if tulingApiKey != "" {
tl = tuling.NewTuling(tulingApiKey)
}

ws := controller.NewMessageController(&config.WechatConfig, chatGPT, tl)
ws := controller.NewMessageController(&config.WechatConfig)
weChatGroup := router.Group("weChat")
{
//签名回调
Expand Down
6 changes: 2 additions & 4 deletions pkg/controller/index_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"weChatRobot-go/pkg/logger"
"weChatRobot-go/pkg/model"
"weChatRobot-go/pkg/service"
"weChatRobot-go/pkg/third-party/chatgpt"
"weChatRobot-go/pkg/third-party/tuling"
)

func IndexHandler(c *gin.Context) {
Expand All @@ -19,8 +17,8 @@ type MessageController struct {
wechatService *service.WechatService
}

func NewMessageController(wc *model.WechatConfig, chatGPT *chatgpt.ChatGPT, tuling *tuling.Tuling) *MessageController {
wechatService := service.NewWechatService(wc, chatGPT, tuling)
func NewMessageController(wc *model.WechatConfig) *MessageController {
wechatService := service.NewWechatService(wc)
return &MessageController{
wechatService: wechatService,
}
Expand Down
32 changes: 17 additions & 15 deletions pkg/service/wechat_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,26 @@ import (
"strings"
"weChatRobot-go/pkg/logger"
"weChatRobot-go/pkg/model"
"weChatRobot-go/pkg/third-party/chatgpt"
third_party "weChatRobot-go/pkg/third-party"
"weChatRobot-go/pkg/third-party/dashscope"
"weChatRobot-go/pkg/third-party/openai"
"weChatRobot-go/pkg/third-party/tuling"
"weChatRobot-go/pkg/util"
)

type WechatService struct {
config *model.WechatConfig
chatGPT *chatgpt.ChatGPT
tuling *tuling.Tuling
config *model.WechatConfig
assistants []third_party.AssistantService
}

func NewWechatService(wc *model.WechatConfig, chatGPT *chatgpt.ChatGPT, tuling *tuling.Tuling) *WechatService {
func NewWechatService(wc *model.WechatConfig) *WechatService {
return &WechatService{
config: wc,
chatGPT: chatGPT,
tuling: tuling,
config: wc,
assistants: []third_party.AssistantService{
openai.NewOpenAI(),
dashscope.NewDashscope(),
tuling.NewTuling(),
},
}
}

Expand Down Expand Up @@ -53,13 +57,11 @@ func (ws *WechatService) GetResponseMessage(reqMessage model.ReqMessage) string
} else if reqMessage.MsgType == model.MsgTypeText {
respMessage = getRespMessageByKeyword(reqMessage.ToUserName, reqMessage.FromUserName, reqMessage.Content)

//优先使用ChatGPT响应
if respMessage == nil && ws.chatGPT != nil {
respMessage = ws.chatGPT.GetRespMessage(reqMessage.ToUserName, reqMessage.FromUserName, reqMessage.Content)
}

if respMessage == nil && ws.tuling != nil {
respMessage = ws.tuling.GetRespMessage(reqMessage.ToUserName, reqMessage.FromUserName, reqMessage.Content)
for _, ai := range ws.assistants {
respMessage = ai.ProcessText(reqMessage.ToUserName, reqMessage.FromUserName, reqMessage.Content)
if respMessage != nil {
break
}
}
} else {
respMessage = util.BuildRespTextMessage(reqMessage.ToUserName, reqMessage.FromUserName, "我只对文字感兴趣[悠闲]")
Expand Down
5 changes: 5 additions & 0 deletions pkg/third-party/assistant_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package third_party

type AssistantService interface {
ProcessText(fromUserName, toUserName, content string) interface{}
}
48 changes: 0 additions & 48 deletions pkg/third-party/chatgpt/chatgpt.go

This file was deleted.

13 changes: 0 additions & 13 deletions pkg/third-party/chatgpt/chatgpt_test.go

This file was deleted.

89 changes: 89 additions & 0 deletions pkg/third-party/dashscope/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package dashscope

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"weChatRobot-go/pkg/logger"
)

const generationUrl = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"

type Client struct {
apiKey string
httpClient *http.Client
}

type GenerationParam struct {
Model string `json:"model"`
Input Input `json:"input"`
}

type Input struct {
Messages []Message `json:"messages"`
}

type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}

type GenerationResult struct {
Output struct {
Text string `json:"text"`
FinishReason string `json:"finish_reason"`
} `json:"output"`
}

func (client *Client) call(param *GenerationParam) (*GenerationResult, error) {
// 转换为JSON格式
payload, err := json.Marshal(param)
if err != nil {
return nil, err
}

// 构建请求
req, err := http.NewRequest("POST", generationUrl, bytes.NewBuffer(payload))
if err != nil {
return nil, err
}

// 设置请求头部
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.apiKey))
req.Header.Set("Content-Type", "application/json")

resp, err := client.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
var errorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}

err = json.NewDecoder(resp.Body).Decode(&errorResponse)
if err != nil {
return nil, err
}

return nil, fmt.Errorf("API error: %s - %s", errorResponse.Code, errorResponse.Message)
}

result := &GenerationResult{}
var respBytes []byte
if respBytes, err = io.ReadAll(resp.Body); err != nil {
logger.Error(err, "读取通义千问响应内容报错")
return nil, nil
}
err = json.Unmarshal(respBytes, &result)
if err != nil {
return nil, err
}

return result, nil
}
52 changes: 52 additions & 0 deletions pkg/third-party/dashscope/dashscope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package dashscope

import (
"net/http"
"os"
"weChatRobot-go/pkg/logger"
"weChatRobot-go/pkg/util"
)

type Dashscope struct {
client *Client
}

func NewDashscope() *Dashscope {
apiKey := os.Getenv("DASHSCOPE_API_KEY")
var client *Client
if apiKey != "" {
client = &Client{
apiKey: apiKey,
httpClient: &http.Client{},
}
}

return &Dashscope{
client: client,
}
}

func (d *Dashscope) ProcessText(fromUserName, toUserName, content string) interface{} {
if d.client == nil {
return nil
}

param := &GenerationParam{
Model: "qwen-turbo",
Input: Input{
Messages: []Message{
{Role: "system", Content: "你是一个AI助手,保持回复内容尽量简短"},
{Role: "user", Content: content},
},
},
}

var result *GenerationResult
var err error
if result, err = d.client.call(param); err != nil {
logger.Error(err, "dashscope generation error")
return nil
}

return util.BuildRespTextMessage(fromUserName, toUserName, result.Output.Text)
}
13 changes: 13 additions & 0 deletions pkg/third-party/dashscope/dashscope_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dashscope

import (
"fmt"
"testing"
"weChatRobot-go/pkg/util"
)

func TestGetRespMessage(t *testing.T) {
dashscope := NewDashscope()
var respMessage = dashscope.ProcessText("aaa", "bbb", "什么是GPT")
fmt.Printf("respMessage:%v", util.ToJsonString(respMessage))
}
Loading

0 comments on commit 7d8d224

Please sign in to comment.