Skip to content

Commit

Permalink
openai
Browse files Browse the repository at this point in the history
  • Loading branch information
aimerneige committed Jul 29, 2024
1 parent 3cfaedd commit ebcc6a3
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/FloatTech/AnimeAPI v1.6.1-0.20230724165034-a3cf504fab92
github.com/FloatTech/floatbox v0.0.0-20230331064925-9af336a84944
github.com/disintegration/imaging v1.6.2
github.com/google/uuid v1.3.0
github.com/notnil/chess v1.9.0
github.com/sirupsen/logrus v1.9.3
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ github.com/fumiama/go-simple-protobuf v0.1.0 h1:rLzJgNqB6LHNDVMl81yyNt6ZKziWtVfu
github.com/fumiama/go-simple-protobuf v0.1.0/go.mod h1:5yYNapXq1tQMOZg9bOIVhQlZk9pQqpuFIO4DZLbsdy4=
github.com/fumiama/gofastTEA v0.0.10 h1:JJJ+brWD4kie+mmK2TkspDXKzqq0IjXm89aGYfoGhhQ=
github.com/fumiama/gofastTEA v0.0.10/go.mod h1:RIdbYZyB4MbH6ZBlPymRaXn3cD6SedlCu5W/HHfMPBk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
Expand Down
57 changes: 57 additions & 0 deletions internal/plugin/openai/openai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package openai

import (
"fmt"
"os"

"github.com/aimerneige/yukichan-bot/internal/pkg/common"
"github.com/aimerneige/yukichan-bot/internal/plugin/openai/query"
"github.com/sirupsen/logrus"
zero "github.com/wdvxdr1123/ZeroBot"
"gopkg.in/yaml.v3"
)

const configFilePath = "./config/openai.yaml"

type OpenaiConfig struct {
Secret OpenaiSecret `yaml:"secret"`
}

type OpenaiSecret struct {
Account string `yaml:"account"`
AppId string `yaml:"appid"`
Token string `yaml:"token"`
AesKey string `yaml:"aeskey"`
}

func init() {
confData, err := os.ReadFile(configFilePath)
if err != nil {
logrus.Errorln("[openai]", "Fail to read config file", err)
return
}
var config OpenaiConfig
if err := yaml.Unmarshal(confData, &config); err != nil {
logrus.Errorln("[openai]", "Fail to unmarshal config data", err)
return
}
logrus.Debugln(config)

engine := zero.New()
engine.OnPrefix("//").
SetPriority(2).
SetBlock(true).Handle(func(ctx *zero.Ctx) {
userQuery := ctx.State["args"].(string)
secret := config.Secret
accessToken := query.GetToken(secret.Account, secret.AppId, secret.Token)
response := query.SendQueryRequest(query.ApiReq{
Query: userQuery,
Env: "online",
UserName: ctx.Event.Sender.NickName,
Avatar: fmt.Sprintf("https://q2.qlogo.cn/headimg_dl?dst_uin=%d&spec=100", ctx.Event.Sender.ID),
Userid: fmt.Sprintf("qq_%d", ctx.Event.Sender.ID),
}, accessToken, secret.Token, secret.AesKey)
ctx.Send(response)
})
engine.UseMidHandler(common.DefaultSpeedLimit)
}
93 changes: 93 additions & 0 deletions internal/plugin/openai/query/aes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package query

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
)

// 调用 aes 算法加密并编码
func Encrypt(encodingAesKey, msg string) (string, error) {
// aes 加密
encryptedMsg, err := aesCBCEncrypt(encodingAesKey, []byte(msg))
if err != nil {
return "", err
}
// base64 编码
base64Msg := make([]byte, base64.StdEncoding.EncodedLen(len(encryptedMsg)))
base64.StdEncoding.Encode(base64Msg, encryptedMsg)
return string(base64Msg), nil
}

// 解码后调用 aes 算法解密
func Decrypt(encodingAesKey, msg string) (string, error) {
// base64 解码
cipherText, err := base64.StdEncoding.DecodeString(msg)
if err != nil {
return "", err
}
// aes 解密
plainText, err := aesCBCDecrypt(encodingAesKey, []byte(cipherText))
if err != nil {
return "", err
}
return string(plainText), nil
}

// aesCBCEncrypt 使用给定的 aes 密钥对数据进行加密
// encodingKey 应为 base64 编码的密钥,并去掉结尾的 `=`
// 即 aes 密钥原文 1234567890123456
// base64 编码后得到 MTIzNDU2Nzg5MDEyMzQ1Ng==
// encodingKey 应为 MTIzNDU2Nzg5MDEyMzQ1Ng=
func aesCBCEncrypt(encodingKey string, plaintextMsg []byte) ([]byte, error) {
block, aesKey, err := decodeAesKey(encodingKey)
if err != nil {
return []byte{}, err
}
plaintextMsg = pkcs5Padding(plaintextMsg, block.BlockSize())
cipherText := make([]byte, len(plaintextMsg))
iv := aesKey[:aes.BlockSize]
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(cipherText, plaintextMsg)
return cipherText, nil
}

// aesCBCDecrypt 使用给定的 aes 密钥对给定的数据进行解密
// encodingKey 用法同 AesCBCEncrypt 的注释
func aesCBCDecrypt(encodingKey string, encryptedMsg []byte) ([]byte, error) {
block, aesKey, err := decodeAesKey(encodingKey)
if err != nil {
return []byte{}, err
}
iv := aesKey[:aes.BlockSize]
mode := cipher.NewCBCDecrypter(block, iv)
decryptedMsg := make([]byte, len(encryptedMsg))
mode.CryptBlocks(decryptedMsg, encryptedMsg)
decryptedMsg = pkcs5UnPadding(decryptedMsg)
return decryptedMsg, nil
}

func decodeAesKey(encodingAesKey string) (cipher.Block, []byte, error) {
aesKey, err := base64.StdEncoding.DecodeString(encodingAesKey + "=")
if err != nil {
return nil, aesKey, err
}
block, e := aes.NewCipher(aesKey)
if e != nil {
return nil, aesKey, err
}
return block, aesKey, nil
}

func pkcs5Padding(cipherText []byte, blockSize int) []byte {
padding := blockSize - len(cipherText)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(cipherText, padText...)
}

func pkcs5UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
104 changes: 104 additions & 0 deletions internal/plugin/openai/query/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package query

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

const QueryNonceLength = 32
const QueryApiAddress = "https://openaiapi.weixin.qq.com/v2/bot/query"

type ApiReq struct {
Query string `json:"query"`
Env string `json:"env"`
UserName string `json:"user_name"`
Avatar string `json:"avatar"`
Userid string `json:"userid"`
}

type ApiRsp struct {
Code int32 `json:"code"`
Msg string `json:"msg"`
Data ApiRspData `json:"data"`
RequestId string `json:"request_id"`
}

type ApiRspData struct {
Answer string `json:"answer"`
AnswerType string `json:"answer_type"`
IntentName string `json:"intent_name"`
MsgId string `json:"msg_id"`
Options []Option `json:"options"`
SkillName string `json:"skill_name"`
Slots []SlotDetail `json:"slots"`
Status string `json:"status"`
}

type Option struct {
AnsNodeName string `json:"ans_node_name"`
Title string `json:"title"`
Answer string `json:"answer"`
Confidence float64 `json:"confidence"`
}

type SlotDetail struct {
Name string `json:"name"`
Value string `json:"value"`
Norm string `json:"norm"`
}

func SendQueryRequest(apiReq ApiReq, accessToken, signToken, aeskey string) string {
data, err := json.Marshal(apiReq)
if err != nil {
panic(err)
}
reqJson := string(data)
cipher, err := Encrypt(aeskey, reqJson)
if err != nil {
panic(err)
}
response := apiCall(accessToken, signToken, aeskey, []byte(cipher))
return response
}

func apiCall(accessToken, signToken, aeskey string, body []byte) string {
timestamp := fmt.Sprintf("%d", time.Now().Unix())
nonce := GenRandomLetterString(QueryNonceLength)
sign := CalcSign(signToken, timestamp, nonce, body)

client := http.Client{}
request, err := http.NewRequest("POST", QueryApiAddress, bytes.NewBuffer(body))
if err != nil {
panic(err)
}
request.Header = http.Header{
"X-OPENAI-TOKEN": {accessToken},
"request_id": {GetUUid()},
"timestamp": {timestamp},
"nonce": {nonce},
"sign": {sign},
"Content-Type": {"text/plain"},
}
response, err := client.Do(request)
if err != nil {
panic(err)
}
respBody, err := io.ReadAll(response.Body)
if err != nil {
panic(err)
}
cipher := string(respBody)
plain, err := Decrypt(aeskey, cipher)
if err != nil {
panic(err)
}
apiResp := ApiRsp{}
if err := json.Unmarshal([]byte(plain), &apiResp); err != nil {
panic(err)
}
return apiResp.Data.Answer
}
58 changes: 58 additions & 0 deletions internal/plugin/openai/query/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package query

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

const TokenNonceLength = 32
const TokenApiAddress = "https://openaiapi.weixin.qq.com/v2/token"

type TokenResponse struct {
Code int `json:"code"`
Data struct {
AccessToken string `json:"access_token"`
} `json:"data"`
Msg string `json:"msg"`
RequestID string `json:"request_id"`
}

func GetToken(account, appId, token string) string {
timestamp := fmt.Sprintf("%d", time.Now().Unix())
nonce := GenRandomLetterString(TokenNonceLength)
bodyStr := fmt.Sprintf(`{"account":"%s"}`, account)
body := []byte(bodyStr)
sign := CalcSign(token, timestamp, nonce, body)

client := http.Client{}
request, err := http.NewRequest("POST", TokenApiAddress, bytes.NewBuffer(body))
if err != nil {
panic(err)
}
request.Header = http.Header{
"X-APPID": {appId},
"request_id": {GetUUid()},
"timestamp": {timestamp},
"nonce": {nonce},
"sign": {sign},
}
response, err := client.Do(request)
if err != nil {
panic(err)
}
respBody, err := io.ReadAll(response.Body)
if err != nil {
panic(err)
}
var tokenResp TokenResponse
err = json.Unmarshal(respBody, &tokenResp)
if err != nil {
fmt.Printf("request: %+v, respBody: %v, err: %v", request, string(respBody), err)
panic(err)
}
return tokenResp.Data.AccessToken
}
51 changes: 51 additions & 0 deletions internal/plugin/openai/query/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package query

import (
"crypto/md5"
"encoding/hex"
"math/rand"

"github.com/google/uuid"
)

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const letterNums = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func GenRandomLetterString(length int) string {
b := make([]byte, length)
for i := range b {
b[i] = letters[rand.Int63()%int64(len(letters))]
}
return string(b)
}

func GenRandomString(length int) string {
b := make([]byte, length)
for i := range b {
b[i] = letterNums[rand.Int63()%int64(len(letterNums))]
}
return string(b)
}

func CalcSign(token, timestamp, nonce string, body []byte) string {
if len(token) == 0 {
return ""
}
bodyMd5 := MD5(body)
return MD5([]byte(token + timestamp + nonce + bodyMd5))
}

func MD5(data []byte) string {
return hex.EncodeToString(MD5Byte(data))
}

func MD5Byte(data []byte) []byte {
hash := md5.New()
hash.Write(data)
return hash.Sum(nil)
}

func GetUUid() string {
random, _ := uuid.NewRandom()
return random.String()
}

0 comments on commit ebcc6a3

Please sign in to comment.