-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3cfaedd
commit ebcc6a3
Showing
7 changed files
with
366 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |