许多修改
This commit is contained in:
parent
30a821c217
commit
275512e492
197
API.go
197
API.go
@ -16,12 +16,22 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"apigo.cc/gojs"
|
||||||
|
"apigo.cc/gojs/goja"
|
||||||
xml2json "github.com/basgys/goxml2json"
|
xml2json "github.com/basgys/goxml2json"
|
||||||
"github.com/ssgo/httpclient"
|
"github.com/ssgo/httpclient"
|
||||||
"github.com/ssgo/u"
|
"github.com/ssgo/u"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Signers = map[string]func(req *Request, cfg *SignerConfig) error{
|
type SignerCode struct {
|
||||||
|
Code string
|
||||||
|
Version uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsSigners = map[string]*SignerCode{}
|
||||||
|
var actionConfigs = map[string]*ApiConfig{}
|
||||||
|
|
||||||
|
var goSigners = map[string]func(req *Request, cfg *ApiConfig) error{
|
||||||
// 基础认证
|
// 基础认证
|
||||||
"basic": makeBasicAuthSign,
|
"basic": makeBasicAuthSign,
|
||||||
"bearer": makeBearerSign,
|
"bearer": makeBearerSign,
|
||||||
@ -30,79 +40,134 @@ var Signers = map[string]func(req *Request, cfg *SignerConfig) error{
|
|||||||
// "cos": makeCOSSign,
|
// "cos": makeCOSSign,
|
||||||
// "hmac": makeHmacSign,
|
// "hmac": makeHmacSign,
|
||||||
}
|
}
|
||||||
var signersLock = sync.RWMutex{}
|
var actionsLock = sync.RWMutex{}
|
||||||
|
|
||||||
func GetSigner(name string) func(req *Request, cfg *SignerConfig) error {
|
type ApiConfig struct {
|
||||||
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
|
data map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
var signerConfig = map[string]*map[string]any{}
|
var apiConfigs = map[string]*map[string]any{}
|
||||||
var confLock = sync.RWMutex{}
|
var confLock = sync.RWMutex{}
|
||||||
|
|
||||||
func GetSignerConfig(signer string) *SignerConfig {
|
func GetSigner(name string, vm *goja.Runtime) func(req *Request, cfg *ApiConfig) error {
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var jsi *SignerCode
|
||||||
|
actionsLock.RLock()
|
||||||
|
si := goSigners[name]
|
||||||
|
if si == nil {
|
||||||
|
jsi = jsSigners[name]
|
||||||
|
}
|
||||||
|
actionsLock.RUnlock()
|
||||||
|
|
||||||
|
// 获取当前VM的Signer
|
||||||
|
if jsi != nil {
|
||||||
|
if jsi.Version != u.Uint64(vm.GetData("__api_signer_version_"+name)) {
|
||||||
|
// 从代码更新
|
||||||
|
vm.SetData("__api_signer_version_"+name, jsi.Version)
|
||||||
|
if fnV, err := vm.RunString("(" + jsi.Code + ")"); err == nil {
|
||||||
|
if fn, ok := goja.AssertFunction(fnV); ok {
|
||||||
|
si = func(req *Request, cfg *ApiConfig) error {
|
||||||
|
_, err := fn(nil, vm.ToValue(gojs.ToMap(req)), vm.ToValue(gojs.ToMap(cfg)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
vm.SetData("__signer_"+name, si)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 从vm中直接获取
|
||||||
|
if si1, ok := vm.GetData("__signer_" + name).(func(req *Request, cfg *ApiConfig) error); ok {
|
||||||
|
si = si1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return si
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSignerVersion(name string) uint64 {
|
||||||
|
actionsLock.Lock()
|
||||||
|
defer actionsLock.Unlock()
|
||||||
|
si := jsSigners[name]
|
||||||
|
if si == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return si.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
// func RegisterSigner(name string, f func(req *Request, cfg *SignerConfig) error) {
|
||||||
|
func RegisterSigner(name string, fnCode string, fnVersion *uint64, vm *goja.Runtime) error {
|
||||||
|
version := u.Uint64(fnVersion)
|
||||||
|
if version == 0 {
|
||||||
|
version = 1
|
||||||
|
}
|
||||||
|
// fmt.Println(name, fnCode, version)
|
||||||
|
if fnV, err := vm.RunString("(" + fnCode + ")"); err == nil {
|
||||||
|
if _, ok := goja.AssertFunction(fnV); !ok {
|
||||||
|
return gojs.Err("signer must be a function")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return gojs.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsLock.Lock()
|
||||||
|
defer actionsLock.Unlock()
|
||||||
|
jsSigners[name] = &SignerCode{
|
||||||
|
Code: fnCode,
|
||||||
|
Version: version,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSignerConfig(signer string) *ApiConfig {
|
||||||
confLock.RLock()
|
confLock.RLock()
|
||||||
cfg := signerConfig[signer]
|
cfg := apiConfigs[signer]
|
||||||
confLock.RUnlock()
|
confLock.RUnlock()
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = &map[string]any{}
|
cfg = &map[string]any{}
|
||||||
}
|
}
|
||||||
return &SignerConfig{data: *cfg}
|
return &ApiConfig{data: *cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (cfg *SignerConfig) Signer() string {
|
// func (cfg *SignerConfig) Signer() string {
|
||||||
// return u.String(cfg.data["signer"])
|
// return u.String(cfg.data["signer"])
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func (cfg *SignerConfig) Get(k string, defaultValue any) any {
|
func (cfg *ApiConfig) Get(k string, defaultValue any) any {
|
||||||
if v, ok := cfg.data[k]; ok {
|
if v, ok := cfg.data[k]; ok {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *SignerConfig) Set(k string, v any) {
|
func (cfg *ApiConfig) Set(k string, v any) {
|
||||||
cfg.data[k] = v
|
cfg.data[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *SignerConfig) String(k string, defaultValue string) string {
|
func (cfg *ApiConfig) String(k string, defaultValue string) string {
|
||||||
return u.String(cfg.Get(k, defaultValue))
|
return u.String(cfg.Get(k, defaultValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *SignerConfig) Int(k string, defaultValue int64) int64 {
|
func (cfg *ApiConfig) Int(k string, defaultValue int64) int64 {
|
||||||
return u.Int64(cfg.Get(k, defaultValue))
|
return u.Int64(cfg.Get(k, defaultValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *SignerConfig) Float(k string, defaultValue float64) float64 {
|
func (cfg *ApiConfig) Float(k string, defaultValue float64) float64 {
|
||||||
return u.Float64(cfg.Get(k, defaultValue))
|
return u.Float64(cfg.Get(k, defaultValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *SignerConfig) Bool(k string, defaultValue bool) bool {
|
func (cfg *ApiConfig) Bool(k string, defaultValue bool) bool {
|
||||||
return u.Bool(cfg.Get(k, defaultValue))
|
return u.Bool(cfg.Get(k, defaultValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *SignerConfig) To(k string, to any) {
|
func (cfg *ApiConfig) To(k string, to any) {
|
||||||
from := cfg.Get(k, nil)
|
from := cfg.Get(k, nil)
|
||||||
if from != nil {
|
if from != nil {
|
||||||
u.Convert(from, to)
|
u.Convert(from, to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *SignerConfig) Map(k string) map[string]any {
|
func (cfg *ApiConfig) Map(k string) map[string]any {
|
||||||
m := map[string]any{}
|
m := map[string]any{}
|
||||||
cfg.To(k, &m)
|
cfg.To(k, &m)
|
||||||
return m
|
return m
|
||||||
@ -374,7 +439,7 @@ func (req *Request) MakeBody() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeSign(cfg *SignerConfig, req *Request) error {
|
func MakeSign(cfg *ApiConfig, req *Request, vm *goja.Runtime) error {
|
||||||
if req.RequestType == "" {
|
if req.RequestType == "" {
|
||||||
if req.Data != nil {
|
if req.Data != nil {
|
||||||
req.RequestType = RequestType.Json
|
req.RequestType = RequestType.Json
|
||||||
@ -426,7 +491,7 @@ func MakeSign(cfg *SignerConfig, req *Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = &SignerConfig{}
|
cfg = &ApiConfig{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从signer中设置header
|
// 从signer中设置header
|
||||||
@ -453,7 +518,7 @@ func MakeSign(cfg *SignerConfig, req *Request) error {
|
|||||||
req.MakeQuery()
|
req.MakeQuery()
|
||||||
req.MakeBody()
|
req.MakeBody()
|
||||||
|
|
||||||
if signer := GetSigner(u.String(cfg.data["signer"])); signer != nil {
|
if signer := GetSigner(u.String(cfg.data["signer"]), vm); signer != nil {
|
||||||
// fmt.Println(111, req.Method, 111)
|
// fmt.Println(111, req.Method, 111)
|
||||||
if err := signer(req, cfg); err != nil {
|
if err := signer(req, cfg); err != nil {
|
||||||
// fmt.Println(333, err)
|
// fmt.Println(333, err)
|
||||||
@ -466,38 +531,52 @@ func MakeSign(cfg *SignerConfig, req *Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
// func Get(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
|
||||||
req.Method = "GET"
|
// req.Method = "GET"
|
||||||
return Do(cfg, req)
|
// return Do(cfg, req, vm)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func Post(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
// func Post(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
|
||||||
req.Method = "POST"
|
// req.Method = "POST"
|
||||||
return Do(cfg, req)
|
// return Do(cfg, req, vm)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func Put(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
// func Put(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
|
||||||
req.Method = "PUT"
|
// req.Method = "PUT"
|
||||||
return Do(cfg, req)
|
// return Do(cfg, req, vm)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func Delete(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
// func Delete(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
|
||||||
req.Method = "DELETE"
|
// req.Method = "DELETE"
|
||||||
return Do(cfg, req)
|
// return Do(cfg, req, vm)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func Head(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
// func Head(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
|
||||||
req.Method = "HEAD"
|
// req.Method = "HEAD"
|
||||||
return Do(cfg, req)
|
// return Do(cfg, req, vm)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func Options(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
// func Options(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
|
||||||
req.Method = "OPTIONS"
|
// req.Method = "OPTIONS"
|
||||||
return Do(cfg, req)
|
// return Do(cfg, req, vm)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func Do(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
|
func Do(action string, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
|
||||||
err := MakeSign(cfg, req)
|
var cfg *ApiConfig
|
||||||
|
if action != "" {
|
||||||
|
action1 := u.SplitTrimN(action, ".", 2)[0]
|
||||||
|
actionsLock.RLock()
|
||||||
|
cfg = actionConfigs[action]
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = actionConfigs[action1]
|
||||||
|
}
|
||||||
|
actionsLock.RUnlock()
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = &ApiConfig{data: map[string]any{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := MakeSign(cfg, req, vm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
174
ai_test.js
174
ai_test.js
@ -1,97 +1,129 @@
|
|||||||
import co from 'apigo.cc/gojs/console'
|
import co from 'apigo.cc/gojs/console'
|
||||||
import u from 'apigo.cc/gojs/util'
|
import u from 'apigo.cc/gojs/util'
|
||||||
import api from 'apigo.cc/gojs/api'
|
import api from 'apigo.cc/gojs/api'
|
||||||
|
import file from 'apigo.cc/gojs/file'
|
||||||
|
|
||||||
// 注册腾讯云TC3签名器
|
// 注册腾讯云TC3签名器
|
||||||
api.registerSigner("tc3", (req, cfg) => {
|
api.registerSigner("tc3", ((req, cfg) => {
|
||||||
let action = cfg.string("action", "")
|
import u from 'apigo.cc/gojs/util'
|
||||||
let service = cfg.string("service", "")
|
let action = cfg.string("action", "")
|
||||||
let timestamp = u.timestamp()
|
let service = cfg.string("service", "")
|
||||||
let contentType = "application/json; charset=utf-8"
|
let timestamp = u.timestamp()
|
||||||
let canonicalHeaders = "content-type:" + contentType + "\nhost:" + req.finalHost + "\nx-tc-action:" + action.toLowerCase() + "\n"
|
let contentType = "application/json; charset=utf-8"
|
||||||
let signedHeaders = "content-type;host;x-tc-action"
|
let canonicalHeaders = "content-type:" + contentType + "\nhost:" + req.finalHost + "\nx-tc-action:" + action.toLowerCase() + "\n"
|
||||||
let hashedRequestPayload = u.hex(u.sha256(req.finalBody))
|
let signedHeaders = "content-type;host;x-tc-action"
|
||||||
let canonicalRequest = req.method + "\n" + req.finalPath + "\n" + req.finalQuery + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload
|
let hashedRequestPayload = u.hex(u.sha256(req.finalBody))
|
||||||
let date = u.formatDate("2006-01-02", timestamp)
|
let canonicalRequest = req.method + "\n" + req.finalPath + "\n" + req.finalQuery + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload
|
||||||
let credentialScope = date + "/" + service + "/tc3_request"
|
let date = u.formatDate("2006-01-02", timestamp)
|
||||||
let string2sign = "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + u.hex(u.sha256(canonicalRequest))
|
let credentialScope = date + "/" + service + "/tc3_request"
|
||||||
let secretDate = u.hmacSHA256("TC3" + cfg.string("secretKey", ""), date)
|
let string2sign = "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + u.hex(u.sha256(canonicalRequest))
|
||||||
let secretService = u.hmacSHA256(secretDate, service)
|
let secretDate = u.hmacSHA256("TC3" + cfg.string("secretKey", ""), date)
|
||||||
let secretSigning = u.hmacSHA256(secretService, "tc3_request")
|
let secretService = u.hmacSHA256(secretDate, service)
|
||||||
let signature = u.hex(u.hmacSHA256(secretSigning, string2sign))
|
let secretSigning = u.hmacSHA256(secretService, "tc3_request")
|
||||||
let authorization = "TC3-HMAC-SHA256 Credential=" + cfg.string("secretId", "") + "/" + credentialScope + ", SignedHeaders=" + signedHeaders + ", Signature=" + signature
|
let signature = u.hex(u.hmacSHA256(secretSigning, string2sign))
|
||||||
req.setHeader("Content-Type", contentType)
|
let authorization = "TC3-HMAC-SHA256 Credential=" + cfg.string("secretId", "") + "/" + credentialScope + ", SignedHeaders=" + signedHeaders + ", Signature=" + signature
|
||||||
req.setHeader("X-TC-Action", action)
|
req.setHeader("Content-Type", contentType)
|
||||||
req.setHeader("X-TC-Timestamp", u.string(timestamp))
|
req.setHeader("X-TC-Action", action)
|
||||||
req.setHeader("X-TC-Version", cfg.string("version", ""))
|
req.setHeader("X-TC-Timestamp", u.string(timestamp))
|
||||||
req.setHeader("X-TC-Region", cfg.string("region", ""))
|
req.setHeader("X-TC-Version", cfg.string("version", ""))
|
||||||
req.setHeader("Authorization", authorization)
|
req.setHeader("X-TC-Region", cfg.string("region", ""))
|
||||||
})
|
req.setHeader("Authorization", authorization)
|
||||||
|
}).toString())
|
||||||
|
|
||||||
// 腾讯云COS签名
|
// 腾讯云COS签名
|
||||||
api.registerSigner("cos", (req, cfg) => {
|
api.registerSigner("cos", ((req, cfg) => {
|
||||||
let startTimestamp = u.timestamp()
|
import u from 'apigo.cc/gojs/util'
|
||||||
let keyTime = startTimestamp + ";" + startTimestamp + cfg.int("expiredTime", 600)
|
let startTimestamp = u.timestamp()
|
||||||
let signKey = u.hex(u.hmacSHA1(cfg.string("secretKey", ""), keyTime))
|
let keyTime = startTimestamp + ";" + startTimestamp + cfg.int("expiredTime", 600)
|
||||||
let [urlParamList, httpParameters] = api.sortParams(req.query)
|
let signKey = u.hex(u.hmacSHA1(cfg.string("secretKey", ""), keyTime))
|
||||||
let [headerList, httpHeaders] = api.sortParams(req.headers)
|
let [urlParamList, httpParameters] = api.sortParams(req.query)
|
||||||
let hashedHttpString = u.hex(u.sha1(req.method.toLowerCase() + "\n" + req.finalPath + "\n" + httpParameters + "\n" + httpHeaders + "\n"))
|
let [headerList, httpHeaders] = api.sortParams(req.headers)
|
||||||
let signature = u.hex(u.hmacSHA1(signKey, "sha1\n" + keyTime + "\n" + hashedHttpString + "\n"))
|
let hashedHttpString = u.hex(u.sha1(req.method.toLowerCase() + "\n" + req.finalPath + "\n" + httpParameters + "\n" + httpHeaders + "\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
|
let signature = u.hex(u.hmacSHA1(signKey, "sha1\n" + keyTime + "\n" + hashedHttpString + "\n"))
|
||||||
req.setHeader("Authorization", authorization)
|
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)
|
||||||
|
}).toString())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// let r = api.tencent.smsPackagesStatistics({ data: { SmsSdkAppId: '1400624676', BeginTime: '2025010100', EndTime: '2045010100' } })
|
// let r = api.tencent.smsPackagesStatistics({ data: { SmsSdkAppId: '1400624676', BeginTime: '2025010100', EndTime: '2045010100' } })
|
||||||
// let r = api.tencent.getBucketList()
|
let r = api.tencent.getBucketList()
|
||||||
// let r = api.laoCos.get({ url: "/?max-keys=5" })
|
// 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.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.put({ url: "test/aaa.txt", text: 'hello world' })
|
||||||
// let r = api.laoCos.get({ url: "test/aaa.txt" })
|
// let r = api.laoCos.get({ url: "test/aaa.txt" })
|
||||||
// let r = api.tencent.getCosToken({ config: { path: '/aaa/111' } })
|
// let r = api.tencent.getCosToken({ config: { path: '/aaa/111' } })
|
||||||
// co.info(r)
|
co.info(r)
|
||||||
|
|
||||||
// 测试llm接口
|
// 测试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' } } })
|
// 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)
|
// test('智谱(jwt):简单对话(.do)', r.statusCode == 200 && !!r.data, r?.data?.choices[0]?.message?.content, r)
|
||||||
|
|
||||||
// testChat('PPIO(openai)', api.openai)
|
testChat('PPIO(openai)', api.openai)
|
||||||
// testChat('智谱', api.zhipu)
|
// testChat('智谱', api.zhipu)
|
||||||
testChat('豆包', api.doubao)
|
// testChat('豆包', api.doubao)
|
||||||
|
|
||||||
// let r = api.zhipu.image({ data: { prompt: '一张猫在沙发上喝咖啡' } })
|
// let r = api.zhipu.image({ data: { prompt: '一张猫在沙发上喝咖啡' } })
|
||||||
// test('智谱:图片生成', r.statusCode == 200 && !!r.data, r?.data?.data[0]?.url, r)
|
// test('智谱:图片生成', r.statusCode == 200 && !!r.data, r?.data?.data[0]?.url, r)
|
||||||
// let img = r?.data?.data[0]?.url
|
// let img = r?.data?.data[0]?.url
|
||||||
|
|
||||||
// let img = 'https://aigc-files.bigmodel.cn/api/cogview/20250816103236c2331606d04c460a_0.png'
|
// 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 } }] }] } })
|
// 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)
|
// test('智谱:看图说话', r.statusCode == 200 && !!r.data, r?.data?.choices[0]?.message?.content, r)
|
||||||
|
|
||||||
|
|
||||||
// 测试文件扫描
|
// 测试文件扫描
|
||||||
// let r = api.textin.scan({ binary: fs.readBytes('testRes/table.jpeg') })
|
// let r = api.textin.scan({ binary: fs.readBytes('testRes/table.jpeg') })
|
||||||
// test('Textin:表格识别', r.statusCode == 200 && !!r.data, r?.data, r)
|
// test('Textin:表格识别', r.statusCode == 200 && !!r.data, r?.data, r)
|
||||||
|
|
||||||
return true
|
|
||||||
|
// 测试动态配置
|
||||||
|
let zhipuKey = file.read('env.yml').match(/zhipu:.*?key: (.*?)\n/s)[1]
|
||||||
|
let zp = {
|
||||||
|
url: 'https://open.bigmodel.cn/api/paas/v4',
|
||||||
|
key: zhipuKey,
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer {{.key}}',
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
chat: {
|
||||||
|
url: '/chat/completions',
|
||||||
|
data: {
|
||||||
|
model: 'GLM-4.5-Flash',
|
||||||
|
thinking: {
|
||||||
|
type: 'disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
api.config('zp', zp)
|
||||||
|
let r1 = api.zp.chat({ callback: v => { co.println(co.yellow(v.choices[0].delta.content) + ' ') }, data: { messages: [{ role: 'user', content: '1+1=多少?说人话简单点' }], stream: true } })
|
||||||
|
test('动态配置(v1)', r1.statusCode == 200 && !!r1.data, r1?.data?.choices[0]?.message?.content, r1)
|
||||||
|
zp.actions.chat.data.thinking.type = 'enabled'
|
||||||
|
api.config('zp', zp, 2)
|
||||||
|
r1 = api.zp.chat({ callback: v => { co.println(co.yellow(v.choices[0]?.delta?.reasoning_content || v.choices[0]?.delta?.content) + ' ') }, data: { messages: [{ role: 'user', content: '1+2=多少?说人话简单点' }], stream: true } })
|
||||||
|
test('动态配置(v2)', r1.statusCode == 200 && !!r1.data, r1?.data?.choices[0]?.message, r1)
|
||||||
|
|
||||||
|
return true
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
return ex.message
|
return ex.message
|
||||||
}
|
}
|
||||||
|
|
||||||
function testChat(title, llm) {
|
function testChat(title, llm) {
|
||||||
let r = llm.chat({ data: { messages: [{ role: 'user', content: '你好' }] } })
|
let r = llm.chat({ data: { messages: [{ role: 'user', content: '你好' }] } })
|
||||||
test(title + ':简单对话', r.statusCode == 200 && !!r.data, r?.data?.choices[0]?.message?.content, r)
|
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 } })
|
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)
|
test(title + ':流式对话', r.statusCode == 200 && !!r.data, r?.data?.usage?.total_tokens, r)
|
||||||
|
|
||||||
// r = llm.embeddings({ data: { input: '今天天气真好' } })
|
// r = llm.embeddings({ data: { input: '今天天气真好' } })
|
||||||
// test(title + ':向量化', r.statusCode == 200 && !!r.data, r?.data?.data[0]?.embedding?.length, r)
|
// test(title + ':向量化', r.statusCode == 200 && !!r.data, r?.data?.data[0]?.embedding?.length, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
function test(title, condition, successMessage, failedMessage) {
|
function test(title, condition, successMessage, failedMessage) {
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
co.info(title, co.bRed('失败'), co.red(u.jsonP(failedMessage)))
|
co.info(title, co.bRed('失败'), co.red(u.jsonP(failedMessage)))
|
||||||
throw new Error(title + '失败')
|
throw new Error(title + '失败')
|
||||||
}
|
}
|
||||||
co.info(title, co.bGreen('通过'), co.yellow(u.json(successMessage)))
|
co.info(title, co.bGreen('通过'), co.yellow(u.json(successMessage)))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 基本认证签名器
|
// 基本认证签名器
|
||||||
func makeBasicAuthSign(req *Request, cfg *SignerConfig) error {
|
func makeBasicAuthSign(req *Request, cfg *ApiConfig) error {
|
||||||
// 1. 获取用户名和密码
|
// 1. 获取用户名和密码
|
||||||
username := cfg.String("username", "")
|
username := cfg.String("username", "")
|
||||||
password := cfg.String("password", "")
|
password := cfg.String("password", "")
|
||||||
@ -30,13 +30,13 @@ func makeBasicAuthSign(req *Request, cfg *SignerConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bearer Token认证
|
// Bearer Token认证
|
||||||
func makeBearerSign(req *Request, cfg *SignerConfig) error {
|
func makeBearerSign(req *Request, cfg *ApiConfig) error {
|
||||||
req.Headers["Authorization"] = "Bearer " + cfg.String("key", "")
|
req.Headers["Authorization"] = "Bearer " + cfg.String("key", "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用JWT签名器
|
// 通用JWT签名器
|
||||||
func makeJWTSign(req *Request, cfg *SignerConfig) error {
|
func makeJWTSign(req *Request, cfg *ApiConfig) error {
|
||||||
// 1. 获取JWT配置
|
// 1. 获取JWT配置
|
||||||
secret := cfg.String("secret", "")
|
secret := cfg.String("secret", "")
|
||||||
algorithm := cfg.String("algorithm", "HS256")
|
algorithm := cfg.String("algorithm", "HS256")
|
||||||
|
|||||||
21
go.mod
21
go.mod
@ -3,11 +3,11 @@ module apigo.cc/gojs/api
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/gojs v0.0.26
|
apigo.cc/gojs v0.0.30
|
||||||
apigo.cc/gojs/console v0.0.2
|
apigo.cc/gojs/console v0.0.4
|
||||||
apigo.cc/gojs/file v0.0.5
|
apigo.cc/gojs/file v0.0.7
|
||||||
apigo.cc/gojs/runtime v0.0.3
|
apigo.cc/gojs/runtime v0.0.4
|
||||||
apigo.cc/gojs/util v0.0.14
|
apigo.cc/gojs/util v0.0.16
|
||||||
github.com/basgys/goxml2json v1.1.0
|
github.com/basgys/goxml2json v1.1.0
|
||||||
github.com/ssgo/config v1.7.10
|
github.com/ssgo/config v1.7.10
|
||||||
github.com/ssgo/httpclient v1.7.8
|
github.com/ssgo/httpclient v1.7.8
|
||||||
@ -22,19 +22,18 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/ZZMarquis/gm v1.3.2 // indirect
|
github.com/ZZMarquis/gm v1.3.2 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/emmansun/gmsm v0.32.0 // indirect
|
github.com/emmansun/gmsm v0.40.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/pprof v0.0.0-20250903194437-c28834ac2320 // indirect
|
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/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
|
||||||
github.com/ssgo/log v1.7.9 // indirect
|
github.com/ssgo/log v1.7.9 // indirect
|
||||||
github.com/ssgo/standard v1.7.7 // indirect
|
github.com/ssgo/standard v1.7.7 // indirect
|
||||||
github.com/ssgo/tool v0.4.29 // indirect
|
github.com/ssgo/tool v0.4.29 // indirect
|
||||||
golang.org/x/crypto v0.42.0 // indirect
|
golang.org/x/crypto v0.44.0 // indirect
|
||||||
golang.org/x/net v0.44.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
193
plugin.go
193
plugin.go
@ -24,44 +24,51 @@ type Result struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
config.LoadConfig(pluginName, &signerConfig)
|
config.LoadConfig(pluginName, &apiConfigs)
|
||||||
Config(signerConfig)
|
for k, v := range apiConfigs {
|
||||||
|
Config(k, *v, nil)
|
||||||
obj := map[string]any{
|
|
||||||
"config": Config,
|
|
||||||
"registerSigner": RegisterSigner,
|
|
||||||
"sortParams": SortParams,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for signer := range signerConfig {
|
obj := map[string]any{
|
||||||
cfg := signerConfig[signer]
|
"config": Config,
|
||||||
o := map[string]any{}
|
"getConfigVersion": GetConfigVersion,
|
||||||
o["do"] = MakeAction(signer, nil)
|
"makeApi": MakeApi,
|
||||||
o["get"] = MakeAction(signer, map[string]any{
|
"registerSigner": RegisterSigner,
|
||||||
"method": "GET",
|
"getSignerVersion": GetSignerVersion,
|
||||||
})
|
"sortParams": SortParams,
|
||||||
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{}
|
// TODO 动态更新配置时无法重新生成obj,预处理配置生成Action,增加通用的Call方法
|
||||||
u.Convert((*cfg)["actions"], &actions)
|
for apiName := range apiConfigs {
|
||||||
for k, v := range actions {
|
// cfg := signerConfig[signer]
|
||||||
o[k] = MakeAction(signer, v)
|
// o := map[string]any{}
|
||||||
}
|
// o["do"] = MakeAction(signer, nil)
|
||||||
obj[signer] = o
|
// 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",
|
||||||
|
// })
|
||||||
|
// o["version"] = (*cfg)["_version"]
|
||||||
|
|
||||||
|
// actions := map[string]map[string]any{}
|
||||||
|
// u.Convert((*cfg)["actions"], &actions)
|
||||||
|
// for k, v := range actions {
|
||||||
|
// o[k] = MakeAction(signer, v)
|
||||||
|
// }
|
||||||
|
obj[apiName] = MakeApi(apiName)
|
||||||
}
|
}
|
||||||
|
|
||||||
tsCode := gojs.MakeTSCode(obj)
|
tsCode := gojs.MakeTSCode(obj)
|
||||||
@ -72,12 +79,61 @@ func init() {
|
|||||||
},
|
},
|
||||||
TsCode: tsCode,
|
TsCode: tsCode,
|
||||||
Desc: pluginName,
|
Desc: pluginName,
|
||||||
|
JsCode: `
|
||||||
|
let $MOD$_obj = $MOD$
|
||||||
|
$MOD$ = new Proxy($MOD$_obj, {
|
||||||
|
get(target, prop) {
|
||||||
|
if(['config', 'getConfigVersion', 'registerSigner', 'getSignerVersion', 'sortParams'].indexOf(prop) >= 0) {
|
||||||
|
return target[prop]
|
||||||
|
}
|
||||||
|
let o = target[prop]
|
||||||
|
let curVer = o && o.version || 0
|
||||||
|
if(curVer == 0 || curVer != $MOD$_obj.getConfigVersion(prop)){
|
||||||
|
o = $MOD$_obj.makeApi(prop)
|
||||||
|
target[prop] = o
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
})
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeAction(signer string, actionCfg map[string]any) func(req *Request) (*Result, error) {
|
func MakeApi(apiName string) map[string]any {
|
||||||
cfg := &SignerConfig{data: map[string]any{}}
|
cfg := apiConfigs[apiName]
|
||||||
if c := signerConfig[signer]; c != nil {
|
o := map[string]any{}
|
||||||
|
o["do"] = MakeAction(apiName, "", nil)
|
||||||
|
o["get"] = MakeAction(apiName, "get", map[string]any{
|
||||||
|
"method": "GET",
|
||||||
|
})
|
||||||
|
o["post"] = MakeAction(apiName, "post", map[string]any{
|
||||||
|
"method": "POST",
|
||||||
|
})
|
||||||
|
o["put"] = MakeAction(apiName, "put", map[string]any{
|
||||||
|
"method": "PUT",
|
||||||
|
})
|
||||||
|
o["delete"] = MakeAction(apiName, "delete", map[string]any{
|
||||||
|
"method": "DELETE",
|
||||||
|
})
|
||||||
|
o["head"] = MakeAction(apiName, "head", map[string]any{
|
||||||
|
"method": "HEAD",
|
||||||
|
})
|
||||||
|
o["options"] = MakeAction(apiName, "options", map[string]any{
|
||||||
|
"method": "OPTIONS",
|
||||||
|
})
|
||||||
|
o["version"] = (*cfg)["_version"]
|
||||||
|
|
||||||
|
actions := map[string]map[string]any{}
|
||||||
|
u.Convert((*cfg)["actions"], &actions)
|
||||||
|
for k, v := range actions {
|
||||||
|
o[k] = MakeAction(apiName, k, v)
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeAction(apiName, actionName string, actionCfg map[string]any) func(req *Request, vm *goja.Runtime) (*Result, error) {
|
||||||
|
cfg := &ApiConfig{data: map[string]any{}}
|
||||||
|
if c := apiConfigs[apiName]; c != nil {
|
||||||
u.Convert(c, cfg.data)
|
u.Convert(c, cfg.data)
|
||||||
delete(cfg.data, "actions")
|
delete(cfg.data, "actions")
|
||||||
}
|
}
|
||||||
@ -104,7 +160,15 @@ func MakeAction(signer string, actionCfg map[string]any) func(req *Request) (*Re
|
|||||||
defaultUrl = cfg.String("url", "")
|
defaultUrl = cfg.String("url", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(req *Request) (*Result, error) {
|
actionFullName := apiName
|
||||||
|
if actionName != "" {
|
||||||
|
actionFullName += "." + actionName
|
||||||
|
}
|
||||||
|
actionsLock.Lock()
|
||||||
|
actionConfigs[actionFullName] = cfg
|
||||||
|
actionsLock.Unlock()
|
||||||
|
|
||||||
|
return func(req *Request, vm *goja.Runtime) (*Result, error) {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
req = &Request{}
|
req = &Request{}
|
||||||
}
|
}
|
||||||
@ -199,7 +263,7 @@ func MakeAction(signer string, actionCfg map[string]any) func(req *Request) (*Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data, resp, err := Do(cfg, req1)
|
data, resp, err := Do(actionFullName, req1, vm)
|
||||||
headers := map[string]string{}
|
headers := map[string]string{}
|
||||||
statusCode := 0
|
statusCode := 0
|
||||||
status := ""
|
status := ""
|
||||||
@ -219,19 +283,46 @@ func MakeAction(signer string, actionCfg map[string]any) func(req *Request) (*Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Config(cfg map[string]*map[string]any) {
|
// func Config(cfg map[string]*map[string]any) {
|
||||||
for k, v := range cfg {
|
// for k, v := range cfg {
|
||||||
if u.String((*v)["signer"]) == "" {
|
// if u.String((*v)["signer"]) == "" {
|
||||||
(*v)["signer"] = k
|
// (*v)["signer"] = k
|
||||||
}
|
// }
|
||||||
// 尝试解密配置
|
// // 尝试解密配置
|
||||||
decryptConfig(reflect.ValueOf(v))
|
// decryptConfig(reflect.ValueOf(v))
|
||||||
makeConfigVar((*v), reflect.ValueOf(v))
|
// makeConfigVar((*v), reflect.ValueOf(v))
|
||||||
|
|
||||||
confLock.Lock()
|
// confLock.Lock()
|
||||||
signerConfig[k] = v
|
// signerConfig[k] = v
|
||||||
confLock.Unlock()
|
// confLock.Unlock()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
func Config(name string, cfg map[string]any, version *uint64) {
|
||||||
|
ver := u.Uint64(version)
|
||||||
|
if ver == 0 {
|
||||||
|
ver = 1
|
||||||
}
|
}
|
||||||
|
cfg["_version"] = ver
|
||||||
|
|
||||||
|
// 尝试解密配置
|
||||||
|
cfgV := reflect.ValueOf(cfg)
|
||||||
|
decryptConfig(cfgV)
|
||||||
|
makeConfigVar(cfg, cfgV)
|
||||||
|
|
||||||
|
confLock.Lock()
|
||||||
|
apiConfigs[name] = &cfg
|
||||||
|
confLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigVersion(name string) uint64 {
|
||||||
|
confLock.RLock()
|
||||||
|
defer confLock.RUnlock()
|
||||||
|
cfg := apiConfigs[name]
|
||||||
|
if cfg != nil {
|
||||||
|
return u.Uint64((*cfg)["_version"])
|
||||||
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func decryptConfig(v reflect.Value) {
|
func decryptConfig(v reflect.Value) {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestPlugin(t *testing.T) {
|
func TestPlugin(t *testing.T) {
|
||||||
|
// fmt.Println(filepath.Join("1", "2", "", "", "3", "", "4"))
|
||||||
gojs.ExportForDev()
|
gojs.ExportForDev()
|
||||||
for _, f := range u.ReadDirN(".") {
|
for _, f := range u.ReadDirN(".") {
|
||||||
if strings.HasSuffix(f.Name, "_test.js") {
|
if strings.HasSuffix(f.Name, "_test.js") {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user