From 30a821c21746fee3b6587bce7677fd5aeadbac23 Mon Sep 17 00:00:00 2001 From: Star Date: Thu, 11 Sep 2025 23:42:22 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + API.go | 591 +++++++++++++++++++++++++++++++++++++++++++++ ai_test.js | 97 ++++++++ api.sample.yml | 127 ++++++++++ cloudSigner.go | 140 +++++++++++ commonSigner.go | 217 +++++++++++++++++ go.mod | 40 +++ plugin.go | 309 ++++++++++++++++++++++++ plugin_test.go | 30 +++ testRes/table.jpeg | Bin 0 -> 52101 bytes testRes/table.xls | Bin 0 -> 6135 bytes 11 files changed, 1557 insertions(+) create mode 100644 .gitignore create mode 100644 API.go create mode 100644 ai_test.js create mode 100644 api.sample.yml create mode 100644 cloudSigner.go create mode 100644 commonSigner.go create mode 100644 go.mod create mode 100644 plugin.go create mode 100644 plugin_test.go create mode 100644 testRes/table.jpeg create mode 100644 testRes/table.xls diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47904c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.* +!.gitignore +go.sum +node_modules +package.json +env.yml diff --git a/API.go b/API.go new file mode 100644 index 0000000..0c0b664 --- /dev/null +++ b/API.go @@ -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 + } + } +} diff --git a/ai_test.js b/ai_test.js new file mode 100644 index 0000000..d80b309 --- /dev/null +++ b/ai_test.js @@ -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))) +} diff --git a/api.sample.yml b/api.sample.yml new file mode 100644 index 0000000..8ab9d19 --- /dev/null +++ b/api.sample.yml @@ -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 diff --git a/cloudSigner.go b/cloudSigner.go new file mode 100644 index 0000000..00e3fc1 --- /dev/null +++ b/cloudSigner.go @@ -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 +// } diff --git a/commonSigner.go b/commonSigner.go new file mode 100644 index 0000000..c479900 --- /dev/null +++ b/commonSigner.go @@ -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, "&") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..db12962 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..5d2ce29 --- /dev/null +++ b/plugin.go @@ -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) + } + } + } +} diff --git a/plugin_test.go b/plugin_test.go new file mode 100644 index 0000000..2988946 --- /dev/null +++ b/plugin_test.go @@ -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")) + } + } + } +} diff --git a/testRes/table.jpeg b/testRes/table.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b2dd900befd364427ba51f636d0ee5910b2d36f8 GIT binary patch literal 52101 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 a5<9^J5M&MD{rDh?A^ng~q!2+7U;o)hzx0U! literal 0 HcmV?d00001 diff --git a/testRes/table.xls b/testRes/table.xls new file mode 100644 index 0000000000000000000000000000000000000000..a91447ba87cd3294787d8f7108f520c3401f2ced GIT binary patch literal 6135 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 esoDRxl>R&K!Ri=DJ^=t+