第一版
This commit is contained in:
commit
30a821c217
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.*
|
||||||
|
!.gitignore
|
||||||
|
go.sum
|
||||||
|
node_modules
|
||||||
|
package.json
|
||||||
|
env.yml
|
591
API.go
Normal file
591
API.go
Normal file
@ -0,0 +1,591 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
xml2json "github.com/basgys/goxml2json"
|
||||||
|
"github.com/ssgo/httpclient"
|
||||||
|
"github.com/ssgo/u"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Signers = map[string]func(req *Request, cfg *SignerConfig) error{
|
||||||
|
// 基础认证
|
||||||
|
"basic": makeBasicAuthSign,
|
||||||
|
"bearer": makeBearerSign,
|
||||||
|
"jwt": makeJWTSign,
|
||||||
|
// "tc3": makeTC3Sign,
|
||||||
|
// "cos": makeCOSSign,
|
||||||
|
// "hmac": makeHmacSign,
|
||||||
|
}
|
||||||
|
var signersLock = sync.RWMutex{}
|
||||||
|
|
||||||
|
func GetSigner(name string) func(req *Request, cfg *SignerConfig) error {
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
signersLock.RLock()
|
||||||
|
defer signersLock.RUnlock()
|
||||||
|
return Signers[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterSigner(name string, f func(req *Request, cfg *SignerConfig) error) {
|
||||||
|
signersLock.Lock()
|
||||||
|
defer signersLock.Unlock()
|
||||||
|
Signers[name] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignerConfig struct {
|
||||||
|
data map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
var signerConfig = map[string]*map[string]any{}
|
||||||
|
var confLock = sync.RWMutex{}
|
||||||
|
|
||||||
|
func GetSignerConfig(signer string) *SignerConfig {
|
||||||
|
confLock.RLock()
|
||||||
|
cfg := signerConfig[signer]
|
||||||
|
confLock.RUnlock()
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = &map[string]any{}
|
||||||
|
}
|
||||||
|
return &SignerConfig{data: *cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (cfg *SignerConfig) Signer() string {
|
||||||
|
// return u.String(cfg.data["signer"])
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (cfg *SignerConfig) Get(k string, defaultValue any) any {
|
||||||
|
if v, ok := cfg.data[k]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *SignerConfig) Set(k string, v any) {
|
||||||
|
cfg.data[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *SignerConfig) String(k string, defaultValue string) string {
|
||||||
|
return u.String(cfg.Get(k, defaultValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *SignerConfig) Int(k string, defaultValue int64) int64 {
|
||||||
|
return u.Int64(cfg.Get(k, defaultValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *SignerConfig) Float(k string, defaultValue float64) float64 {
|
||||||
|
return u.Float64(cfg.Get(k, defaultValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *SignerConfig) Bool(k string, defaultValue bool) bool {
|
||||||
|
return u.Bool(cfg.Get(k, defaultValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *SignerConfig) To(k string, to any) {
|
||||||
|
from := cfg.Get(k, nil)
|
||||||
|
if from != nil {
|
||||||
|
u.Convert(from, to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *SignerConfig) Map(k string) map[string]any {
|
||||||
|
m := map[string]any{}
|
||||||
|
cfg.To(k, &m)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigValue(cfg map[string]any, k string) string {
|
||||||
|
a := u.SplitWithoutNoneN(k, ".", 2)
|
||||||
|
if len(a) > 1 {
|
||||||
|
m := map[string]any{}
|
||||||
|
u.Convert(cfg[a[0]], &m)
|
||||||
|
return u.String(m[a[1]])
|
||||||
|
} else if len(a) == 1 {
|
||||||
|
return u.String(cfg[a[0]])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (cfg *SignerConfig) Value(k string) string {
|
||||||
|
// a := u.SplitWithoutNoneN(k, ".", 2)
|
||||||
|
// if len(a) > 1 {
|
||||||
|
// m := cfg.Map(a[0])
|
||||||
|
// return u.String(m[a[1]])
|
||||||
|
// } else if len(a) == 1 {
|
||||||
|
// return cfg.String(a[0], "")
|
||||||
|
// }
|
||||||
|
// return ""
|
||||||
|
// }
|
||||||
|
|
||||||
|
var httpclients = map[uint]*httpclient.ClientPool{}
|
||||||
|
var httpclientsLock = sync.RWMutex{}
|
||||||
|
|
||||||
|
func GetHttpClient(timeout uint) *httpclient.ClientPool {
|
||||||
|
httpclientsLock.RLock()
|
||||||
|
c := httpclients[timeout]
|
||||||
|
httpclientsLock.RUnlock()
|
||||||
|
if c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
c = httpclient.GetClient(time.Duration(timeout) * time.Millisecond)
|
||||||
|
httpclientsLock.Lock()
|
||||||
|
httpclients[timeout] = c
|
||||||
|
httpclientsLock.Unlock()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
var RequestType = struct {
|
||||||
|
Json string // JSON请求体(默认)
|
||||||
|
Form string // 表单请求体
|
||||||
|
Multi string // 多部分请求体
|
||||||
|
Text string // 文本请求体
|
||||||
|
Binary string // 二进制请求体
|
||||||
|
}{
|
||||||
|
Json: "json",
|
||||||
|
Form: "form",
|
||||||
|
Multi: "multi",
|
||||||
|
Text: "text",
|
||||||
|
Binary: "binary",
|
||||||
|
}
|
||||||
|
|
||||||
|
var ResponseType = struct {
|
||||||
|
Json string // JSON响应体(默认)
|
||||||
|
Text string // 文本响应体
|
||||||
|
Binary string // 二进制响应体
|
||||||
|
}{
|
||||||
|
Json: "json",
|
||||||
|
Text: "text",
|
||||||
|
Binary: "binary",
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Url string
|
||||||
|
Method string
|
||||||
|
Config map[string]string // 配置信息,可以被 {{}} 引用
|
||||||
|
Headers map[string]string // 请求头
|
||||||
|
Query map[string]string // URL中的参数
|
||||||
|
Data map[string]any // 用JSON发送的请求主体对象
|
||||||
|
Form map[string]string // 用表单发送的主体内容
|
||||||
|
File map[string]any // 用multi-form发送的文件对象
|
||||||
|
Text string // 纯文本格式的主体内容
|
||||||
|
Binary []byte // 二进制格式的主体内容
|
||||||
|
RequestType string
|
||||||
|
ResponseType string
|
||||||
|
Callback func(data any)
|
||||||
|
Timeout uint // 单位毫秒,默认0表示不超时
|
||||||
|
FinalUrl string // 提前处理好的完整URL
|
||||||
|
FinalHost string // 主机名
|
||||||
|
FinalPath string // URL路径
|
||||||
|
FinalQuery string // 查询字符串
|
||||||
|
FinalBody []byte // 提前处理好的请求主体
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) SetUrl(v string) {
|
||||||
|
req.Url = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) GetHost() string {
|
||||||
|
return req.FinalHost
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) SetMethod(v string) {
|
||||||
|
req.Method = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) SetQuery(k string, v string) {
|
||||||
|
req.Query[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) SetForm(k string, v string) {
|
||||||
|
req.Form[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) SetText(v string) {
|
||||||
|
req.Text = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) SetBinary(v []byte) {
|
||||||
|
req.Binary = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) SetFile(k string, v any) {
|
||||||
|
req.File[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) SetData(k string, v any) {
|
||||||
|
req.Data[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) SetHeader(key, value string) {
|
||||||
|
req.Headers[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) MakeQuery() {
|
||||||
|
if urlInfo, err := url.Parse(req.Url); err == nil {
|
||||||
|
q := url.Values{}
|
||||||
|
for k, v := range req.Query {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
urlInfo.RawQuery = q.Encode()
|
||||||
|
req.FinalUrl = urlInfo.String()
|
||||||
|
req.FinalHost = urlInfo.Host
|
||||||
|
req.FinalPath = urlInfo.Path
|
||||||
|
req.FinalQuery = urlInfo.RawQuery
|
||||||
|
if req.FinalPath == "" {
|
||||||
|
req.FinalPath = "/"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
req.FinalUrl = req.Url
|
||||||
|
req.FinalHost = ""
|
||||||
|
req.FinalPath = "/"
|
||||||
|
req.FinalQuery = ""
|
||||||
|
}
|
||||||
|
// fmt.Println(">>>a", req.Url, req.FinalUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) makeConfig(str string) string {
|
||||||
|
if strings.Contains(str, "{{config.") {
|
||||||
|
if m := configMatcher.FindAllStringSubmatch(str, 100); m != nil {
|
||||||
|
for _, m1 := range m {
|
||||||
|
if v1, ok := req.Config[m1[1]]; ok && v1 != "" {
|
||||||
|
str = strings.ReplaceAll(str, m1[0], v1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(str, "{{/") {
|
||||||
|
if m := fnMatcher.FindAllStringSubmatch(str, 100); m != nil {
|
||||||
|
for _, m1 := range m {
|
||||||
|
fn := m1[1]
|
||||||
|
args := m1[2]
|
||||||
|
switch fn {
|
||||||
|
case "date":
|
||||||
|
str = strings.ReplaceAll(str, m1[0], time.Now().Format(args))
|
||||||
|
case "timestamp":
|
||||||
|
str = strings.ReplaceAll(str, m1[0], u.String(time.Now().Unix()))
|
||||||
|
case "timestampMilli":
|
||||||
|
str = strings.ReplaceAll(str, m1[0], u.String(time.Now().UnixMilli()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *Request) MakeBody() {
|
||||||
|
switch req.RequestType {
|
||||||
|
case RequestType.Json:
|
||||||
|
data := map[string]any{}
|
||||||
|
if req.Data != nil {
|
||||||
|
for k, v := range req.Data {
|
||||||
|
if str, ok := v.(string); ok {
|
||||||
|
data[k] = req.makeConfig(str)
|
||||||
|
} else {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.FinalBody = u.JsonBytesP(data)
|
||||||
|
case RequestType.Form:
|
||||||
|
formData := url.Values{}
|
||||||
|
for k, v := range req.Form {
|
||||||
|
formData.Set(k, req.makeConfig(v))
|
||||||
|
}
|
||||||
|
req.FinalBody = []byte(formData.Encode())
|
||||||
|
case RequestType.Multi:
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
if len(req.Form) > 0 {
|
||||||
|
for k, v := range req.Form {
|
||||||
|
writer.WriteField(k, req.makeConfig(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(req.File) > 0 {
|
||||||
|
for k, v := range req.File {
|
||||||
|
if filename, ok := v.(string); ok && u.FileExists(filename) {
|
||||||
|
if fp, err := os.Open(filename); err == nil {
|
||||||
|
if part, err := writer.CreateFormFile(k, filepath.Base(filename)); err == nil {
|
||||||
|
io.Copy(part, fp)
|
||||||
|
}
|
||||||
|
_ = fp.Close()
|
||||||
|
}
|
||||||
|
} else if dataUrl, ok := v.(string); ok && strings.HasPrefix(dataUrl, "data:") && strings.ContainsRune(dataUrl, ',') {
|
||||||
|
parts := strings.SplitN(dataUrl, ",", 2)
|
||||||
|
metaPart := strings.TrimSuffix(parts[0], "data:")
|
||||||
|
metaParts := strings.SplitN(metaPart, ";", 2)
|
||||||
|
mimeType := "application/octet-stream"
|
||||||
|
if metaParts[0] != "" {
|
||||||
|
mimeType = metaParts[0]
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
if len(metaParts) > 1 && metaParts[1] == "base64" {
|
||||||
|
data = u.UnBase64(parts[1])
|
||||||
|
} else {
|
||||||
|
data = []byte(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
h := make(textproto.MIMEHeader)
|
||||||
|
filename := k
|
||||||
|
if exts, err := mime.ExtensionsByType(mimeType); err == nil && len(exts) > 0 {
|
||||||
|
filename = k + exts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, k, filename))
|
||||||
|
h.Set("Content-Type", mimeType)
|
||||||
|
if part, err := writer.CreatePart(h); err == nil {
|
||||||
|
part.Write(data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h := make(textproto.MIMEHeader)
|
||||||
|
buf := u.Bytes(v)
|
||||||
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, k, k))
|
||||||
|
mt := mime.TypeByExtension(filepath.Ext(k))
|
||||||
|
if mt == "" {
|
||||||
|
mt = "application/octet-stream"
|
||||||
|
}
|
||||||
|
h.Set("Content-Type", mt)
|
||||||
|
if part, err := writer.CreatePart(h); err == nil {
|
||||||
|
part.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.Close()
|
||||||
|
req.FinalBody = body.Bytes()
|
||||||
|
case RequestType.Binary:
|
||||||
|
req.FinalBody = req.Binary
|
||||||
|
default:
|
||||||
|
req.FinalBody = []byte(req.makeConfig(req.Text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeSign(cfg *SignerConfig, req *Request) error {
|
||||||
|
if req.RequestType == "" {
|
||||||
|
if req.Data != nil {
|
||||||
|
req.RequestType = RequestType.Json
|
||||||
|
} else if req.Form != nil {
|
||||||
|
req.RequestType = RequestType.Form
|
||||||
|
} else if req.File != nil {
|
||||||
|
req.RequestType = RequestType.Multi
|
||||||
|
} else if req.Binary != nil {
|
||||||
|
req.RequestType = RequestType.Binary
|
||||||
|
} else {
|
||||||
|
req.RequestType = RequestType.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Method == "" {
|
||||||
|
req.Method = "POST"
|
||||||
|
}
|
||||||
|
req.Method = strings.ToUpper(req.Method)
|
||||||
|
if req.Headers == nil {
|
||||||
|
req.Headers = make(map[string]string)
|
||||||
|
}
|
||||||
|
if req.Query == nil {
|
||||||
|
req.Query = make(map[string]string)
|
||||||
|
}
|
||||||
|
// 自动将URL中的参数放入req.Query
|
||||||
|
if strings.ContainsRune(req.Url, '?') {
|
||||||
|
if urlInfo, err := url.Parse(req.Url); err == nil {
|
||||||
|
for k, v := range urlInfo.Query() {
|
||||||
|
if len(v) > 0 {
|
||||||
|
req.Query[k] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
urlInfo.RawQuery = ""
|
||||||
|
req.Url = urlInfo.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (req.Method == "POST" || req.Method == "PUT") && req.Headers["Content-Type"] == "" {
|
||||||
|
switch req.RequestType {
|
||||||
|
case RequestType.Json:
|
||||||
|
req.Headers["Content-Type"] = "application/json"
|
||||||
|
case RequestType.Form:
|
||||||
|
req.Headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||||
|
case RequestType.Multi:
|
||||||
|
req.Headers["Content-Type"] = "multipart/form-data"
|
||||||
|
case RequestType.Binary:
|
||||||
|
req.Headers["Content-Type"] = "application/octet-stream"
|
||||||
|
default:
|
||||||
|
req.Headers["Content-Type"] = "text/plain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = &SignerConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从signer中设置header
|
||||||
|
for k, v := range cfg.Map("headers") {
|
||||||
|
req.Headers[k] = req.makeConfig(u.String(v))
|
||||||
|
// fmt.Println(u.BCyan(k), req.Headers[k])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从signer中设置query
|
||||||
|
for k, v := range cfg.Map("query") {
|
||||||
|
req.Query[k] = req.makeConfig(u.String(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从signer中设置data
|
||||||
|
for k, v := range cfg.Map("data") {
|
||||||
|
req.Data[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从signer中设置form
|
||||||
|
for k, v := range cfg.Map("form") {
|
||||||
|
req.Form[k] = u.String(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.MakeQuery()
|
||||||
|
req.MakeBody()
|
||||||
|
|
||||||
|
if signer := GetSigner(u.String(cfg.data["signer"])); signer != nil {
|
||||||
|
// fmt.Println(111, req.Method, 111)
|
||||||
|
if err := signer(req, cfg); err != nil {
|
||||||
|
// fmt.Println(333, err)
|
||||||
|
return err
|
||||||
|
// } else {
|
||||||
|
// fmt.Println(333, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 拼接生成完整的URL
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
||||||
|
req.Method = "GET"
|
||||||
|
return Do(cfg, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Post(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
||||||
|
req.Method = "POST"
|
||||||
|
return Do(cfg, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Put(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
||||||
|
req.Method = "PUT"
|
||||||
|
return Do(cfg, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Delete(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
||||||
|
req.Method = "DELETE"
|
||||||
|
return Do(cfg, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Head(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
||||||
|
req.Method = "HEAD"
|
||||||
|
return Do(cfg, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Options(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
||||||
|
req.Method = "OPTIONS"
|
||||||
|
return Do(cfg, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Do(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
||||||
|
err := MakeSign(cfg, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
// fmt.Println(u.BMagenta(u.JsonP(req.Headers)))
|
||||||
|
// return nil, nil, err
|
||||||
|
|
||||||
|
headers := make([]string, 0)
|
||||||
|
for k, v := range req.Headers {
|
||||||
|
headers = append(headers, k, v)
|
||||||
|
}
|
||||||
|
c := GetHttpClient(req.Timeout)
|
||||||
|
// c.Debug = true
|
||||||
|
|
||||||
|
// fmt.Println(req.Url, req.finalUrl, u.BMagenta(string(req.finalBody)), "===")
|
||||||
|
// fmt.Println(u.BCyan(u.JsonP(req.Headers)), "===")
|
||||||
|
|
||||||
|
var r *httpclient.Result
|
||||||
|
var lastData any
|
||||||
|
if req.Callback != nil {
|
||||||
|
r = c.ManualDo(req.Method, req.FinalUrl, req.FinalBody, headers...)
|
||||||
|
if r.Error != nil {
|
||||||
|
return nil, r.Response, r.Error
|
||||||
|
}
|
||||||
|
respType := req.ResponseType
|
||||||
|
if respType == "" {
|
||||||
|
respType = strings.Split(r.Response.Header.Get("Content-Type"), ";")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Response.Body != nil {
|
||||||
|
func() {
|
||||||
|
reader := bufio.NewScanner(r.Response.Body)
|
||||||
|
defer r.Response.Body.Close()
|
||||||
|
for reader.Scan() {
|
||||||
|
str := strings.TrimSpace(reader.Text())
|
||||||
|
if str == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch respType {
|
||||||
|
case "text/event-stream":
|
||||||
|
if strings.HasPrefix(str, "data:") {
|
||||||
|
str = strings.TrimPrefix(str, "data:")
|
||||||
|
str = strings.TrimSpace(str)
|
||||||
|
}
|
||||||
|
var d any
|
||||||
|
if str == "[DONE]" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d = u.UnJson(str, nil)
|
||||||
|
req.Callback(d)
|
||||||
|
if d != nil {
|
||||||
|
lastData = d
|
||||||
|
}
|
||||||
|
case "application/json":
|
||||||
|
req.Callback(u.UnJson(str, nil))
|
||||||
|
case "application/xml":
|
||||||
|
if jData, err := xml2json.Convert(strings.NewReader(str)); err == nil {
|
||||||
|
req.Callback(u.UnJsonBytes(jData.Bytes(), nil))
|
||||||
|
}
|
||||||
|
req.Callback(str)
|
||||||
|
default:
|
||||||
|
req.Callback(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return lastData, r.Response, nil
|
||||||
|
} else {
|
||||||
|
r = c.Do(req.Method, req.FinalUrl, req.FinalBody, headers...)
|
||||||
|
if r.Error != nil {
|
||||||
|
return nil, r.Response, r.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
respType := req.ResponseType
|
||||||
|
if respType == "" {
|
||||||
|
respType = strings.Split(r.Response.Header.Get("Content-Type"), ";")[0]
|
||||||
|
}
|
||||||
|
switch respType {
|
||||||
|
case "application/json":
|
||||||
|
return u.UnJsonBytes(r.Bytes(), nil), r.Response, nil
|
||||||
|
case "application/xml":
|
||||||
|
if jData, err := xml2json.Convert(strings.NewReader(string(r.Bytes()))); err == nil {
|
||||||
|
return u.UnJsonBytes(jData.Bytes(), nil), r.Response, nil
|
||||||
|
}
|
||||||
|
return string(r.Bytes()), r.Response, nil
|
||||||
|
case "application/octet-stream":
|
||||||
|
return r.Bytes(), r.Response, nil
|
||||||
|
default:
|
||||||
|
return r.String(), r.Response, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
ai_test.js
Normal file
97
ai_test.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import co from 'apigo.cc/gojs/console'
|
||||||
|
import u from 'apigo.cc/gojs/util'
|
||||||
|
import api from 'apigo.cc/gojs/api'
|
||||||
|
|
||||||
|
// 注册腾讯云TC3签名器
|
||||||
|
api.registerSigner("tc3", (req, cfg) => {
|
||||||
|
let action = cfg.string("action", "")
|
||||||
|
let service = cfg.string("service", "")
|
||||||
|
let timestamp = u.timestamp()
|
||||||
|
let contentType = "application/json; charset=utf-8"
|
||||||
|
let canonicalHeaders = "content-type:" + contentType + "\nhost:" + req.finalHost + "\nx-tc-action:" + action.toLowerCase() + "\n"
|
||||||
|
let signedHeaders = "content-type;host;x-tc-action"
|
||||||
|
let hashedRequestPayload = u.hex(u.sha256(req.finalBody))
|
||||||
|
let canonicalRequest = req.method + "\n" + req.finalPath + "\n" + req.finalQuery + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload
|
||||||
|
let date = u.formatDate("2006-01-02", timestamp)
|
||||||
|
let credentialScope = date + "/" + service + "/tc3_request"
|
||||||
|
let string2sign = "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + u.hex(u.sha256(canonicalRequest))
|
||||||
|
let secretDate = u.hmacSHA256("TC3" + cfg.string("secretKey", ""), date)
|
||||||
|
let secretService = u.hmacSHA256(secretDate, service)
|
||||||
|
let secretSigning = u.hmacSHA256(secretService, "tc3_request")
|
||||||
|
let signature = u.hex(u.hmacSHA256(secretSigning, string2sign))
|
||||||
|
let authorization = "TC3-HMAC-SHA256 Credential=" + cfg.string("secretId", "") + "/" + credentialScope + ", SignedHeaders=" + signedHeaders + ", Signature=" + signature
|
||||||
|
req.setHeader("Content-Type", contentType)
|
||||||
|
req.setHeader("X-TC-Action", action)
|
||||||
|
req.setHeader("X-TC-Timestamp", u.string(timestamp))
|
||||||
|
req.setHeader("X-TC-Version", cfg.string("version", ""))
|
||||||
|
req.setHeader("X-TC-Region", cfg.string("region", ""))
|
||||||
|
req.setHeader("Authorization", authorization)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 腾讯云COS签名
|
||||||
|
api.registerSigner("cos", (req, cfg) => {
|
||||||
|
let startTimestamp = u.timestamp()
|
||||||
|
let keyTime = startTimestamp + ";" + startTimestamp + cfg.int("expiredTime", 600)
|
||||||
|
let signKey = u.hex(u.hmacSHA1(cfg.string("secretKey", ""), keyTime))
|
||||||
|
let [urlParamList, httpParameters] = api.sortParams(req.query)
|
||||||
|
let [headerList, httpHeaders] = api.sortParams(req.headers)
|
||||||
|
let hashedHttpString = u.hex(u.sha1(req.method.toLowerCase() + "\n" + req.finalPath + "\n" + httpParameters + "\n" + httpHeaders + "\n"))
|
||||||
|
let signature = u.hex(u.hmacSHA1(signKey, "sha1\n" + keyTime + "\n" + hashedHttpString + "\n"))
|
||||||
|
let authorization = "q-sign-algorithm=sha1&q-ak=" + cfg.string("secretId", "") + "&q-sign-time=" + keyTime + "&q-key-time=" + keyTime + "&q-header-list=" + headerList + "&q-url-param-list=" + urlParamList + "&q-signature=" + signature
|
||||||
|
req.setHeader("Authorization", authorization)
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
// let r = api.tencent.smsPackagesStatistics({ data: { SmsSdkAppId: '1400624676', BeginTime: '2025010100', EndTime: '2045010100' } })
|
||||||
|
// let r = api.tencent.getBucketList()
|
||||||
|
// let r = api.laoCos.get({ url: "/?max-keys=5" })
|
||||||
|
// let r = api.laoCos.get({ url: "user/9VKQpY2RH7Ks/avatar.jpg", responseType: 'text/plain' })
|
||||||
|
// let r = api.laoCos.put({ url: "test/aaa.txt", text: 'hello world' })
|
||||||
|
// let r = api.laoCos.get({ url: "test/aaa.txt" })
|
||||||
|
// let r = api.tencent.getCosToken({ config: { path: '/aaa/111' } })
|
||||||
|
// co.info(r)
|
||||||
|
|
||||||
|
// 测试llm接口
|
||||||
|
// let r = api.zhipujwt.do({ url: "https://open.bigmodel.cn/api/paas/v4/chat/completions", data: { model: 'GLM-4.5-Flash', messages: [{ role: 'user', content: '你好' }], thinking: { type: 'disabled' } } })
|
||||||
|
// test('智谱(jwt):简单对话(.do)', r.statusCode == 200 && !!r.data, r?.data?.choices[0]?.message?.content, r)
|
||||||
|
|
||||||
|
// testChat('PPIO(openai)', api.openai)
|
||||||
|
// testChat('智谱', api.zhipu)
|
||||||
|
testChat('豆包', api.doubao)
|
||||||
|
|
||||||
|
// let r = api.zhipu.image({ data: { prompt: '一张猫在沙发上喝咖啡' } })
|
||||||
|
// test('智谱:图片生成', r.statusCode == 200 && !!r.data, r?.data?.data[0]?.url, r)
|
||||||
|
// let img = r?.data?.data[0]?.url
|
||||||
|
|
||||||
|
// let img = 'https://aigc-files.bigmodel.cn/api/cogview/20250816103236c2331606d04c460a_0.png'
|
||||||
|
// let r = api.zhipu.chat({ data: { model: 'GLM-4V-Flash', messages: [{ role: 'user', content: [{ type: 'text', text: '看图说话,写一个简短生动的狗血爱情故事' }, { type: 'image_url', image_url: { url: img } }] }] } })
|
||||||
|
// test('智谱:看图说话', r.statusCode == 200 && !!r.data, r?.data?.choices[0]?.message?.content, r)
|
||||||
|
|
||||||
|
|
||||||
|
// 测试文件扫描
|
||||||
|
// let r = api.textin.scan({ binary: fs.readBytes('testRes/table.jpeg') })
|
||||||
|
// test('Textin:表格识别', r.statusCode == 200 && !!r.data, r?.data, r)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (ex) {
|
||||||
|
return ex.message
|
||||||
|
}
|
||||||
|
|
||||||
|
function testChat(title, llm) {
|
||||||
|
let r = llm.chat({ data: { messages: [{ role: 'user', content: '你好' }] } })
|
||||||
|
test(title + ':简单对话', r.statusCode == 200 && !!r.data, r?.data?.choices[0]?.message?.content, r)
|
||||||
|
|
||||||
|
r = llm.chat({ callback: v => { co.println(co.yellow(v.choices[0].delta.content) + ' ') }, data: { messages: [{ role: 'user', content: '你好' }], stream: true } })
|
||||||
|
test(title + ':流式对话', r.statusCode == 200 && !!r.data, r?.data?.usage?.total_tokens, r)
|
||||||
|
|
||||||
|
// r = llm.embeddings({ data: { input: '今天天气真好' } })
|
||||||
|
// test(title + ':向量化', r.statusCode == 200 && !!r.data, r?.data?.data[0]?.embedding?.length, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
function test(title, condition, successMessage, failedMessage) {
|
||||||
|
if (!condition) {
|
||||||
|
co.info(title, co.bRed('失败'), co.red(u.jsonP(failedMessage)))
|
||||||
|
throw new Error(title + '失败')
|
||||||
|
}
|
||||||
|
co.info(title, co.bGreen('通过'), co.yellow(u.json(successMessage)))
|
||||||
|
}
|
127
api.sample.yml
Normal file
127
api.sample.yml
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
api:
|
||||||
|
openai:
|
||||||
|
url: https://api.ppinfra.com/v3/openai
|
||||||
|
key: xxxx
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{.key}}
|
||||||
|
actions:
|
||||||
|
chat:
|
||||||
|
url: /chat/completions
|
||||||
|
data:
|
||||||
|
model: baidu/ernie-4.5-0.3b
|
||||||
|
zhipu:
|
||||||
|
url: https://open.bigmodel.cn/api/paas/v4
|
||||||
|
key: xxxx
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{.key}}
|
||||||
|
actions:
|
||||||
|
chat:
|
||||||
|
url: /chat/completions
|
||||||
|
data:
|
||||||
|
model: GLM-4.5-Flash
|
||||||
|
thinking:
|
||||||
|
type: disabled
|
||||||
|
embeddings:
|
||||||
|
url: /embeddings
|
||||||
|
data:
|
||||||
|
model: Embedding-3
|
||||||
|
image:
|
||||||
|
url: /images/generations
|
||||||
|
data:
|
||||||
|
model: CogView-4
|
||||||
|
# model: CogView-3-Flash
|
||||||
|
video:
|
||||||
|
url: /videos/generations
|
||||||
|
data:
|
||||||
|
model: CogVideoX-Flash
|
||||||
|
|
||||||
|
zhipujwt:
|
||||||
|
signer: jwt
|
||||||
|
url: https://open.bigmodel.cn/api/paas/v4
|
||||||
|
secret: xxxx
|
||||||
|
claims:
|
||||||
|
api_key: xxxx
|
||||||
|
jwtHeader:
|
||||||
|
sign_type: SIGN
|
||||||
|
|
||||||
|
tencent:
|
||||||
|
signer: tc3
|
||||||
|
region: ap-guangzhou
|
||||||
|
secretId: xxxx
|
||||||
|
secretKey: xxxx
|
||||||
|
url: https://{{.service}}.tencentcloudapi.com
|
||||||
|
actions:
|
||||||
|
smsPackagesStatistics:
|
||||||
|
service: sms
|
||||||
|
version: "2021-01-11"
|
||||||
|
action: SendStatusStatistics
|
||||||
|
data:
|
||||||
|
Limit: 10
|
||||||
|
Offset: 0
|
||||||
|
getBucketList:
|
||||||
|
service: cfs
|
||||||
|
version: "2019-07-19"
|
||||||
|
action: DescribeBucketList
|
||||||
|
data:
|
||||||
|
SrcSecretId: "{{.secretId}}"
|
||||||
|
SrcSecretKey: "{{.secretKey}}"
|
||||||
|
SrcService: COS
|
||||||
|
getObject:
|
||||||
|
service: cfs
|
||||||
|
version: "2019-07-19"
|
||||||
|
action: DescribeBucketList
|
||||||
|
region: ap-shanghai
|
||||||
|
data:
|
||||||
|
SrcSecretId: "{{.secretId}}"
|
||||||
|
SrcSecretKey: "{{.secretKey}}"
|
||||||
|
SrcService: COS
|
||||||
|
getCosToken:
|
||||||
|
service: sts
|
||||||
|
version: "2018-08-13"
|
||||||
|
action: GetFederationToken
|
||||||
|
config:
|
||||||
|
actions: '["cos:GetObject","cos:PutObject"]'
|
||||||
|
appId: 1257995425
|
||||||
|
bucket: lao
|
||||||
|
path: "/*"
|
||||||
|
data:
|
||||||
|
Name: ""
|
||||||
|
DurationSeconds: 60
|
||||||
|
Policy: |
|
||||||
|
{
|
||||||
|
"version": "2.0",
|
||||||
|
"statement": [
|
||||||
|
{
|
||||||
|
"action": {{config.actions}},
|
||||||
|
"effect": "allow",
|
||||||
|
"resource": ["qcs::cos:ap-shanghai:uid/{{config.appId}}:{{config.bucket}}-{{config.appId}}{{config.path}}"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
laoCos:
|
||||||
|
signer: cos
|
||||||
|
url: https://lao-1257995425.cos.ap-shanghai.myqcloud.com
|
||||||
|
secretId: xxxx
|
||||||
|
secretKey: xxxx
|
||||||
|
textin:
|
||||||
|
url: https://api.textin.com/ai/service/v1
|
||||||
|
appId: xxxx
|
||||||
|
secret: xxxx
|
||||||
|
headers:
|
||||||
|
x-ti-app-id: "{{.appId}}"
|
||||||
|
x-ti-secret-code: "{{.secret}}"
|
||||||
|
actions:
|
||||||
|
scan:
|
||||||
|
url: /pdf_to_markdown
|
||||||
|
requestType: binary
|
||||||
|
|
||||||
|
doubao:
|
||||||
|
url: https://ark.cn-beijing.volces.com/api/v3
|
||||||
|
key: xxxx
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{.key}}
|
||||||
|
actions:
|
||||||
|
chat:
|
||||||
|
url: /chat/completions
|
||||||
|
data:
|
||||||
|
model: doubao-1-5-lite-32k-250115
|
140
cloudSigner.go
Normal file
140
cloudSigner.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "fmt"
|
||||||
|
// "strings"
|
||||||
|
// "time"
|
||||||
|
|
||||||
|
// "github.com/ssgo/u"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// // 腾讯云TC3签名
|
||||||
|
// func makeTC3Sign(req *Request, cfg *SignerConfig) error {
|
||||||
|
// fmt.Println(u.JsonP(cfg), 111)
|
||||||
|
// action := cfg.String("action", "")
|
||||||
|
// service := cfg.String("service", "")
|
||||||
|
// version := cfg.String("version", "")
|
||||||
|
// region := cfg.String("region", "")
|
||||||
|
// timestamp := time.Now().Unix()
|
||||||
|
// if req.Url == "" {
|
||||||
|
// req.Url = "https://" + service + ".tencentcloudapi.com"
|
||||||
|
// req.MakeQuery()
|
||||||
|
// }
|
||||||
|
// algorithm := "TC3-HMAC-SHA256"
|
||||||
|
|
||||||
|
// contentType := "application/json; charset=utf-8"
|
||||||
|
// canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\nx-tc-action:%s\n",
|
||||||
|
// contentType, req.FinalHost, strings.ToLower(action))
|
||||||
|
// signedHeaders := "content-type;host;x-tc-action"
|
||||||
|
// hashedRequestPayload := u.Hex(u.Sha256(req.FinalBody))
|
||||||
|
// canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
|
||||||
|
// req.Method,
|
||||||
|
// req.FinalPath,
|
||||||
|
// req.FinalQuery,
|
||||||
|
// canonicalHeaders,
|
||||||
|
// signedHeaders,
|
||||||
|
// hashedRequestPayload)
|
||||||
|
// // fmt.Println(canonicalRequest)
|
||||||
|
|
||||||
|
// date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
|
||||||
|
// credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, service)
|
||||||
|
// hashedCanonicalRequest := u.Sha256String(canonicalRequest)
|
||||||
|
// string2sign := fmt.Sprintf("%s\n%d\n%s\n%s",
|
||||||
|
// algorithm,
|
||||||
|
// timestamp,
|
||||||
|
// credentialScope,
|
||||||
|
// hashedCanonicalRequest)
|
||||||
|
// // fmt.Println(string2sign)
|
||||||
|
|
||||||
|
// // ************* 步骤 3:计算签名 *************
|
||||||
|
// secretDate := u.HmacSha256([]byte("TC3"+cfg.String("secretKey", "")), []byte(date))
|
||||||
|
// secretService := u.HmacSha256(secretDate, []byte(service))
|
||||||
|
// secretSigning := u.HmacSha256(secretService, []byte("tc3_request"))
|
||||||
|
// signature := u.Hex(u.HmacSha256(secretSigning, []byte(string2sign)))
|
||||||
|
// // fmt.Println(signature)
|
||||||
|
|
||||||
|
// // ************* 步骤 4:拼接 Authorization *************
|
||||||
|
// authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
|
||||||
|
// algorithm,
|
||||||
|
// cfg.String("secretId", ""),
|
||||||
|
// credentialScope,
|
||||||
|
// signedHeaders,
|
||||||
|
// signature)
|
||||||
|
// // fmt.Println(u.BCyan(authorization))
|
||||||
|
// // fmt.Println(u.BCyan(string(req.finalBody)))
|
||||||
|
|
||||||
|
// req.Headers["Host"] = req.FinalHost
|
||||||
|
// req.Headers["Content-Type"] = contentType
|
||||||
|
// req.Headers["X-TC-Action"] = action
|
||||||
|
// req.Headers["X-TC-Timestamp"] = u.String(timestamp)
|
||||||
|
// req.Headers["X-TC-Version"] = version
|
||||||
|
// req.Headers["X-TC-Region"] = region
|
||||||
|
// req.Headers["Authorization"] = authorization
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 腾讯云COS签名
|
||||||
|
// func makeCOSSign(req *Request, cfg *SignerConfig) error {
|
||||||
|
// // 获取配置参数
|
||||||
|
// secretId := cfg.String("secretId", "")
|
||||||
|
// secretKey := cfg.String("secretKey", "")
|
||||||
|
// token := cfg.String("token", "") // 可选,用于临时安全凭证
|
||||||
|
|
||||||
|
// // 计算KeyTime(签名有效时间范围)
|
||||||
|
// startTimestamp := time.Now().Unix()
|
||||||
|
// expiredTime := cfg.Int("expiredTime", 600) // 默认10分钟
|
||||||
|
// endTimestamp := startTimestamp + expiredTime
|
||||||
|
// keyTime := fmt.Sprintf("%d;%d", startTimestamp, endTimestamp)
|
||||||
|
|
||||||
|
// // 步骤1:生成SignKey
|
||||||
|
// signKey := u.Hex(u.HmacSha1([]byte(secretKey), []byte(keyTime)))
|
||||||
|
|
||||||
|
// // 步骤2:生成HttpString
|
||||||
|
// // 处理HTTP方法
|
||||||
|
// httpMethod := strings.ToLower(req.Method)
|
||||||
|
|
||||||
|
// // 处理URI路径(需要URL解码?根据COS文档,可能需要原始路径)
|
||||||
|
// uriPathname := req.FinalPath
|
||||||
|
|
||||||
|
// // 处理查询参数(HttpParameters)
|
||||||
|
// queryParams := req.Query
|
||||||
|
// urlParamList, httpParameters := SortParams(queryParams, nil, nil)
|
||||||
|
|
||||||
|
// // 处理请求头(HttpHeaders)
|
||||||
|
// req.Headers["Host"] = req.FinalHost
|
||||||
|
// // 如果有安全令牌,添加到Header
|
||||||
|
// if token != "" {
|
||||||
|
// req.Headers["x-cos-security-token"] = token
|
||||||
|
// }
|
||||||
|
// headerList, httpHeaders := SortParams(req.Headers, nil, nil)
|
||||||
|
// // fmt.Println(u.BMagenta(httpHeaders))
|
||||||
|
|
||||||
|
// // 构建HttpString
|
||||||
|
// httpString := fmt.Sprintf("%s\n%s\n%s\n%s\n",
|
||||||
|
// httpMethod,
|
||||||
|
// uriPathname,
|
||||||
|
// httpParameters,
|
||||||
|
// httpHeaders)
|
||||||
|
|
||||||
|
// // 步骤3:生成StringToSign
|
||||||
|
// hashedHttpString := u.Sha1String(httpString)
|
||||||
|
// stringToSign := fmt.Sprintf("sha1\n%s\n%s\n", keyTime, hashedHttpString)
|
||||||
|
|
||||||
|
// // 步骤4:生成Signature
|
||||||
|
// signature := u.Hex(u.HmacSha1([]byte(signKey), []byte(stringToSign)))
|
||||||
|
|
||||||
|
// // 步骤5:组装签名
|
||||||
|
// authorization := fmt.Sprintf("q-sign-algorithm=sha1&q-ak=%s&q-sign-time=%s&q-key-time=%s&q-header-list=%s&q-url-param-list=%s&q-signature=%s",
|
||||||
|
// secretId,
|
||||||
|
// keyTime,
|
||||||
|
// keyTime,
|
||||||
|
// headerList,
|
||||||
|
// urlParamList,
|
||||||
|
// signature)
|
||||||
|
|
||||||
|
// // 将签名添加到Authorization头
|
||||||
|
// req.Headers["Authorization"] = authorization
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
217
commonSigner.go
Normal file
217
commonSigner.go
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 基本认证签名器
|
||||||
|
func makeBasicAuthSign(req *Request, cfg *SignerConfig) error {
|
||||||
|
// 1. 获取用户名和密码
|
||||||
|
username := cfg.String("username", "")
|
||||||
|
password := cfg.String("password", "")
|
||||||
|
|
||||||
|
// 2. 构造认证字符串
|
||||||
|
auth := username + ":" + password
|
||||||
|
encoded := base64.StdEncoding.EncodeToString([]byte(auth))
|
||||||
|
|
||||||
|
// 3. 添加认证头
|
||||||
|
req.Headers["Authorization"] = "Basic " + encoded
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bearer Token认证
|
||||||
|
func makeBearerSign(req *Request, cfg *SignerConfig) error {
|
||||||
|
req.Headers["Authorization"] = "Bearer " + cfg.String("key", "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用JWT签名器
|
||||||
|
func makeJWTSign(req *Request, cfg *SignerConfig) error {
|
||||||
|
// 1. 获取JWT配置
|
||||||
|
secret := cfg.String("secret", "")
|
||||||
|
algorithm := cfg.String("algorithm", "HS256")
|
||||||
|
expires := cfg.Int("expires", 3600) // 默认1小时有效期
|
||||||
|
timeuint := cfg.String("timeuint", "s") // 时间戳单位
|
||||||
|
|
||||||
|
// 2. 创建Claims
|
||||||
|
var tm, exp int64
|
||||||
|
if timeuint == "ms" {
|
||||||
|
tm = time.Now().UnixMilli()
|
||||||
|
exp = tm + expires*1000
|
||||||
|
} else {
|
||||||
|
tm = time.Now().Unix()
|
||||||
|
exp = tm + expires
|
||||||
|
}
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"exp": exp,
|
||||||
|
"iat": tm,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 添加自定义声明
|
||||||
|
for k, v := range cfg.Map("claims") {
|
||||||
|
if v == "{{timestamp}}" {
|
||||||
|
claims[k] = tm
|
||||||
|
} else {
|
||||||
|
claims[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Println(u.BMagenta(u.JsonP(claims)), algorithm)
|
||||||
|
|
||||||
|
// 4. 创建Token
|
||||||
|
var token *jwt.Token
|
||||||
|
switch algorithm {
|
||||||
|
case "HS256":
|
||||||
|
token = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
case "HS384":
|
||||||
|
token = jwt.NewWithClaims(jwt.SigningMethodHS384, claims)
|
||||||
|
case "HS512":
|
||||||
|
token = jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||||
|
case "RS256":
|
||||||
|
token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
|
default:
|
||||||
|
// 默认使用HS256
|
||||||
|
token = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
token.Header["alg"] = algorithm
|
||||||
|
for k, v := range cfg.Map("jwtHeader") {
|
||||||
|
token.Header[k] = v
|
||||||
|
}
|
||||||
|
// fmt.Println(u.BYellow(u.JsonP(token.Header)), "===")
|
||||||
|
|
||||||
|
// 5. 签名Token
|
||||||
|
var signedString string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if strings.HasPrefix(algorithm, "HS") {
|
||||||
|
signedString, err = token.SignedString([]byte(secret))
|
||||||
|
} else if strings.HasPrefix(algorithm, "RS") {
|
||||||
|
// 处理RSA私钥
|
||||||
|
if privateKey := cfg.String("privateKey", ""); privateKey != "" {
|
||||||
|
var prikey *rsa.PrivateKey
|
||||||
|
prikey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey))
|
||||||
|
if err == nil {
|
||||||
|
signedString, err = token.SignedString(prikey)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = errors.New("privateKey is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// 处理错误
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 添加认证头
|
||||||
|
prefix := cfg.String("prefix", "Bearer ")
|
||||||
|
req.Headers["Authorization"] = prefix + signedString
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// // 通用HMAC-SHA256签名器
|
||||||
|
// func makeHmacSign(req *Request, cfg *SignerConfig) error {
|
||||||
|
// hashMethod := strings.ToUpper(cfg.String("hashMethod", "SHA256"))
|
||||||
|
|
||||||
|
// // 1. 获取待签名数据
|
||||||
|
// dataToSign := cfg.String("signData", "")
|
||||||
|
// if dataToSign == "" {
|
||||||
|
// // 默认使用HTTP方法+URL+当前时间戳
|
||||||
|
// timestamp := time.Now().Format(time.RFC3339)
|
||||||
|
// dataToSign = req.Method + " " + req.Url + "\n" + timestamp
|
||||||
|
// cfg.Set("timestamp", timestamp)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 2. 计算签名
|
||||||
|
// key := []byte(cfg.String("secret", ""))
|
||||||
|
// var h hash.Hash
|
||||||
|
// switch hashMethod {
|
||||||
|
// case "SHA1":
|
||||||
|
// h = hmac.New(sha1.New, key)
|
||||||
|
// case "SHA256":
|
||||||
|
// h = hmac.New(sha256.New, key)
|
||||||
|
// case "SHA384":
|
||||||
|
// h = hmac.New(sha512.New384, key)
|
||||||
|
// case "SHA512":
|
||||||
|
// h = hmac.New(sha512.New, key)
|
||||||
|
// default:
|
||||||
|
// h = hmac.New(sha256.New, key)
|
||||||
|
// }
|
||||||
|
// h.Write([]byte(dataToSign))
|
||||||
|
// signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
|
||||||
|
// // 3. 添加到请求
|
||||||
|
// headerName := cfg.String("headerName", "X-Signature")
|
||||||
|
// req.Headers[headerName] = signature
|
||||||
|
|
||||||
|
// // 4. 可选: 添加时间戳
|
||||||
|
// timestamp := cfg.String("timestamp", "")
|
||||||
|
// if timestamp != "" {
|
||||||
|
// tsHeader := cfg.String("tsHeader", "X-Timestamp")
|
||||||
|
// req.Headers[tsHeader] = timestamp
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
func SortParams(params map[string]string, allow *[]string, deny *[]string) (paramsList string, paramsPairs string) {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取排序的key
|
||||||
|
keys := make([]string, 0)
|
||||||
|
for k := range params {
|
||||||
|
// 按白名单过滤
|
||||||
|
if allow != nil {
|
||||||
|
isAllow := false
|
||||||
|
for _, a := range *allow {
|
||||||
|
if a == k || (strings.HasSuffix(a, "*") && strings.HasPrefix(k, a[:len(a)-1])) {
|
||||||
|
isAllow = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isAllow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按黑名单过滤
|
||||||
|
if deny != nil {
|
||||||
|
isDeny := false
|
||||||
|
for _, d := range *deny {
|
||||||
|
if d == k || (strings.HasSuffix(d, "*") && strings.HasPrefix(k, d[:len(d)-1])) {
|
||||||
|
isDeny = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isDeny {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
// 构建urlParamList和httpParameters
|
||||||
|
paramList := make([]string, 0, len(keys))
|
||||||
|
paramPairs := make([]string, 0, len(keys))
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
paramList = append(paramList, strings.ToLower(url.QueryEscape(k)))
|
||||||
|
paramPairs = append(paramPairs, fmt.Sprintf("%s=%s",
|
||||||
|
strings.ToLower(url.QueryEscape(k)),
|
||||||
|
url.QueryEscape(params[k])))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(paramList, ";"), strings.Join(paramPairs, "&")
|
||||||
|
}
|
40
go.mod
Normal file
40
go.mod
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
module apigo.cc/gojs/api
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
apigo.cc/gojs v0.0.26
|
||||||
|
apigo.cc/gojs/console v0.0.2
|
||||||
|
apigo.cc/gojs/file v0.0.5
|
||||||
|
apigo.cc/gojs/runtime v0.0.3
|
||||||
|
apigo.cc/gojs/util v0.0.14
|
||||||
|
github.com/basgys/goxml2json v1.1.0
|
||||||
|
github.com/ssgo/config v1.7.10
|
||||||
|
github.com/ssgo/httpclient v1.7.8
|
||||||
|
github.com/ssgo/u v1.7.23
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bitly/go-simplejson v0.5.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ZZMarquis/gm v1.3.2 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
|
github.com/emmansun/gmsm v0.32.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
github.com/google/pprof v0.0.0-20250903194437-c28834ac2320 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
|
||||||
|
github.com/ssgo/log v1.7.9 // indirect
|
||||||
|
github.com/ssgo/standard v1.7.7 // indirect
|
||||||
|
github.com/ssgo/tool v0.4.29 // indirect
|
||||||
|
golang.org/x/crypto v0.42.0 // indirect
|
||||||
|
golang.org/x/net v0.44.0 // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
golang.org/x/text v0.29.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
309
plugin.go
Normal file
309
plugin.go
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"apigo.cc/gojs"
|
||||||
|
"apigo.cc/gojs/goja"
|
||||||
|
"github.com/ssgo/config"
|
||||||
|
"github.com/ssgo/u"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pluginName = "api"
|
||||||
|
|
||||||
|
var confAes *u.Aes = u.NewAes([]byte("?GQ$0K0GgLdO=f+~L68PLm$uhKr4'=tV"), []byte("VFs7@sK61cj^f?HZ"))
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
StatusCode int
|
||||||
|
Status string
|
||||||
|
Headers map[string]string
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
config.LoadConfig(pluginName, &signerConfig)
|
||||||
|
Config(signerConfig)
|
||||||
|
|
||||||
|
obj := map[string]any{
|
||||||
|
"config": Config,
|
||||||
|
"registerSigner": RegisterSigner,
|
||||||
|
"sortParams": SortParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
for signer := range signerConfig {
|
||||||
|
cfg := signerConfig[signer]
|
||||||
|
o := map[string]any{}
|
||||||
|
o["do"] = MakeAction(signer, nil)
|
||||||
|
o["get"] = MakeAction(signer, map[string]any{
|
||||||
|
"method": "GET",
|
||||||
|
})
|
||||||
|
o["post"] = MakeAction(signer, map[string]any{
|
||||||
|
"method": "POST",
|
||||||
|
})
|
||||||
|
o["put"] = MakeAction(signer, map[string]any{
|
||||||
|
"method": "PUT",
|
||||||
|
})
|
||||||
|
o["delete"] = MakeAction(signer, map[string]any{
|
||||||
|
"method": "DELETE",
|
||||||
|
})
|
||||||
|
o["head"] = MakeAction(signer, map[string]any{
|
||||||
|
"method": "HEAD",
|
||||||
|
})
|
||||||
|
o["options"] = MakeAction(signer, map[string]any{
|
||||||
|
"method": "OPTIONS",
|
||||||
|
})
|
||||||
|
|
||||||
|
actions := map[string]map[string]any{}
|
||||||
|
u.Convert((*cfg)["actions"], &actions)
|
||||||
|
for k, v := range actions {
|
||||||
|
o[k] = MakeAction(signer, v)
|
||||||
|
}
|
||||||
|
obj[signer] = o
|
||||||
|
}
|
||||||
|
|
||||||
|
tsCode := gojs.MakeTSCode(obj)
|
||||||
|
mappedObj := gojs.ToMap(obj)
|
||||||
|
gojs.Register("apigo.cc/gojs/"+pluginName, gojs.Module{
|
||||||
|
ObjectMaker: func(vm *goja.Runtime) gojs.Map {
|
||||||
|
return mappedObj
|
||||||
|
},
|
||||||
|
TsCode: tsCode,
|
||||||
|
Desc: pluginName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeAction(signer string, actionCfg map[string]any) func(req *Request) (*Result, error) {
|
||||||
|
cfg := &SignerConfig{data: map[string]any{}}
|
||||||
|
if c := signerConfig[signer]; c != nil {
|
||||||
|
u.Convert(c, cfg.data)
|
||||||
|
delete(cfg.data, "actions")
|
||||||
|
}
|
||||||
|
defaultUrl := cfg.String("url", "")
|
||||||
|
|
||||||
|
reqSet := &Request{}
|
||||||
|
u.Convert(actionCfg, reqSet)
|
||||||
|
|
||||||
|
delete(actionCfg, "headers")
|
||||||
|
delete(actionCfg, "query")
|
||||||
|
delete(actionCfg, "data")
|
||||||
|
delete(actionCfg, "form")
|
||||||
|
delete(actionCfg, "file")
|
||||||
|
delete(actionCfg, "text")
|
||||||
|
delete(actionCfg, "binary")
|
||||||
|
delete(actionCfg, "requestType")
|
||||||
|
delete(actionCfg, "callback")
|
||||||
|
delete(actionCfg, "timeout")
|
||||||
|
u.Convert(actionCfg, cfg.data)
|
||||||
|
|
||||||
|
makeConfigVar(cfg.data, reflect.ValueOf(cfg.data))
|
||||||
|
// 没有在外层配置url,或者url中有待处理的变量
|
||||||
|
if defaultUrl == "" || strings.Contains(defaultUrl, "{{.") {
|
||||||
|
defaultUrl = cfg.String("url", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(req *Request) (*Result, error) {
|
||||||
|
if req == nil {
|
||||||
|
req = &Request{}
|
||||||
|
}
|
||||||
|
var req1 *Request
|
||||||
|
if reqSet != nil {
|
||||||
|
req1v := *reqSet
|
||||||
|
req1 = &req1v
|
||||||
|
if req.Url != "" {
|
||||||
|
req1.Url = req.Url
|
||||||
|
}
|
||||||
|
if req.Method != "" {
|
||||||
|
req1.Method = req.Method
|
||||||
|
}
|
||||||
|
if req.Config != nil {
|
||||||
|
if req1.Config == nil {
|
||||||
|
req1.Config = map[string]string{}
|
||||||
|
}
|
||||||
|
for k, v := range req.Config {
|
||||||
|
req1.Config[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Headers != nil {
|
||||||
|
if req1.Headers == nil {
|
||||||
|
req1.Headers = map[string]string{}
|
||||||
|
}
|
||||||
|
for k, v := range req.Headers {
|
||||||
|
req1.Headers[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Query != nil {
|
||||||
|
if req1.Query == nil {
|
||||||
|
req1.Query = map[string]string{}
|
||||||
|
}
|
||||||
|
for k, v := range req.Query {
|
||||||
|
req1.Query[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Data != nil {
|
||||||
|
if req1.Data == nil {
|
||||||
|
req1.Data = map[string]any{}
|
||||||
|
}
|
||||||
|
for k, v := range req.Data {
|
||||||
|
req1.Data[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Form != nil {
|
||||||
|
if req1.Form == nil {
|
||||||
|
req1.Form = map[string]string{}
|
||||||
|
}
|
||||||
|
for k, v := range req.Form {
|
||||||
|
req1.Form[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.File != nil {
|
||||||
|
if req1.File == nil {
|
||||||
|
req1.File = map[string]any{}
|
||||||
|
}
|
||||||
|
for k, v := range req.File {
|
||||||
|
req1.File[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Text != "" {
|
||||||
|
req1.Text = req.Text
|
||||||
|
}
|
||||||
|
if req.Binary != nil {
|
||||||
|
req1.Binary = req.Binary
|
||||||
|
}
|
||||||
|
if req.RequestType != "" {
|
||||||
|
req1.RequestType = req.RequestType
|
||||||
|
}
|
||||||
|
if req.ResponseType != "" {
|
||||||
|
req1.ResponseType = req.ResponseType
|
||||||
|
}
|
||||||
|
if req.Callback != nil {
|
||||||
|
req1.Callback = req.Callback
|
||||||
|
}
|
||||||
|
if req.Timeout == 0 {
|
||||||
|
req1.Timeout = req.Timeout
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
req1 = req
|
||||||
|
}
|
||||||
|
if !strings.Contains(req1.Url, "://") && defaultUrl != "" {
|
||||||
|
has1 := strings.HasSuffix(defaultUrl, "/")
|
||||||
|
has2 := strings.HasPrefix(req1.Url, "/")
|
||||||
|
if !has1 && !has2 {
|
||||||
|
req1.Url = defaultUrl + "/" + req1.Url
|
||||||
|
} else if has1 && has2 {
|
||||||
|
req1.Url = defaultUrl + req1.Url[1:]
|
||||||
|
} else {
|
||||||
|
req1.Url = defaultUrl + req1.Url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, resp, err := Do(cfg, req1)
|
||||||
|
headers := map[string]string{}
|
||||||
|
statusCode := 0
|
||||||
|
status := ""
|
||||||
|
if resp != nil {
|
||||||
|
statusCode = resp.StatusCode
|
||||||
|
status = resp.Status
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
headers[k] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Result{
|
||||||
|
Status: status,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
Headers: headers,
|
||||||
|
Data: data,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Config(cfg map[string]*map[string]any) {
|
||||||
|
for k, v := range cfg {
|
||||||
|
if u.String((*v)["signer"]) == "" {
|
||||||
|
(*v)["signer"] = k
|
||||||
|
}
|
||||||
|
// 尝试解密配置
|
||||||
|
decryptConfig(reflect.ValueOf(v))
|
||||||
|
makeConfigVar((*v), reflect.ValueOf(v))
|
||||||
|
|
||||||
|
confLock.Lock()
|
||||||
|
signerConfig[k] = v
|
||||||
|
confLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptConfig(v reflect.Value) {
|
||||||
|
v = u.FinalValue(v)
|
||||||
|
if v.Kind() == reflect.Map {
|
||||||
|
for _, k := range v.MapKeys() {
|
||||||
|
v2 := u.FinalValue(v.MapIndex(k))
|
||||||
|
if v2.Kind() == reflect.String {
|
||||||
|
if b64, err := base64.URLEncoding.DecodeString(v2.String()); err == nil {
|
||||||
|
if dec, err := confAes.DecryptBytes(b64); err == nil {
|
||||||
|
v.SetMapIndex(k, reflect.ValueOf(string(dec)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if v2.Kind() == reflect.Map || v.Kind() == reflect.Slice {
|
||||||
|
decryptConfig(v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if v.Kind() == reflect.Slice {
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
v2 := u.FinalValue(v.Index(i))
|
||||||
|
if v2.Kind() == reflect.String {
|
||||||
|
if b64, err := base64.URLEncoding.DecodeString(v2.String()); err == nil {
|
||||||
|
if dec, err := confAes.DecryptBytes(b64); err == nil {
|
||||||
|
v.Index(i).Set(reflect.ValueOf(string(dec)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if v2.Kind() == reflect.Map || v.Kind() == reflect.Slice {
|
||||||
|
decryptConfig(v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var varMatcher = regexp.MustCompile(`{{\.([\w\.\-]+)}}`)
|
||||||
|
var configMatcher = regexp.MustCompile(`{{config\.([\w\.\-]+)}}`)
|
||||||
|
var fnMatcher = regexp.MustCompile(`{{/(\w+)\s*(.*?)}}`)
|
||||||
|
|
||||||
|
func makeConfigVar(cfg map[string]any, v reflect.Value) {
|
||||||
|
v = u.FinalValue(v)
|
||||||
|
if v.Kind() == reflect.Map {
|
||||||
|
for _, k := range v.MapKeys() {
|
||||||
|
v2 := u.FinalValue(v.MapIndex(k))
|
||||||
|
if v2.Kind() == reflect.String {
|
||||||
|
str := v2.String()
|
||||||
|
if m := varMatcher.FindAllStringSubmatch(str, 100); m != nil {
|
||||||
|
for _, m1 := range m {
|
||||||
|
if v1 := getConfigValue(cfg, m1[1]); v1 != "" {
|
||||||
|
str = strings.ReplaceAll(str, m1[0], v1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.SetMapIndex(k, reflect.ValueOf(str))
|
||||||
|
}
|
||||||
|
} else if v2.Kind() == reflect.Map || v.Kind() == reflect.Slice {
|
||||||
|
makeConfigVar(cfg, v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if v.Kind() == reflect.Slice {
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
v2 := u.FinalValue(v.Index(i))
|
||||||
|
if v2.Kind() == reflect.String {
|
||||||
|
str := v2.String()
|
||||||
|
if m := varMatcher.FindAllStringSubmatch(str, 100); m != nil {
|
||||||
|
for _, m1 := range m {
|
||||||
|
if v1 := getConfigValue(cfg, m1[1]); v1 != "" {
|
||||||
|
str = strings.ReplaceAll(str, m1[0], v1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.Index(i).Set(reflect.ValueOf(str))
|
||||||
|
}
|
||||||
|
} else if v2.Kind() == reflect.Map || v.Kind() == reflect.Slice {
|
||||||
|
makeConfigVar(cfg, v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
plugin_test.go
Normal file
30
plugin_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package plugin_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"apigo.cc/gojs"
|
||||||
|
_ "apigo.cc/gojs/console"
|
||||||
|
_ "apigo.cc/gojs/file"
|
||||||
|
_ "apigo.cc/gojs/runtime"
|
||||||
|
_ "apigo.cc/gojs/util"
|
||||||
|
"github.com/ssgo/u"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlugin(t *testing.T) {
|
||||||
|
gojs.ExportForDev()
|
||||||
|
for _, f := range u.ReadDirN(".") {
|
||||||
|
if strings.HasSuffix(f.Name, "_test.js") {
|
||||||
|
r, err := gojs.RunFile(f.Name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(u.Red(f.Name), u.BRed(err.Error()))
|
||||||
|
} else if r != true {
|
||||||
|
t.Fatal(u.Red(f.Name), u.BRed(u.JsonP(r)))
|
||||||
|
} else {
|
||||||
|
fmt.Println(u.Green(f.Name), u.BGreen("test succeess"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
testRes/table.jpeg
Normal file
BIN
testRes/table.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
BIN
testRes/table.xls
Normal file
BIN
testRes/table.xls
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user