1
This commit is contained in:
commit
209ef78919
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.*
|
||||
!.gitignore
|
||||
go.sum
|
||||
env.yml
|
||||
build
|
||||
release
|
||||
node_modules
|
||||
package.json
|
||||
829
api.go
Normal file
829
api.go
Normal file
@ -0,0 +1,829 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
"apigo.cc/gojs/goja"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/ssgo/config"
|
||||
"github.com/ssgo/httpclient"
|
||||
"github.com/ssgo/log"
|
||||
"github.com/ssgo/u"
|
||||
)
|
||||
|
||||
type Action struct {
|
||||
Desc string
|
||||
Config map[string]any
|
||||
Method string
|
||||
Path string
|
||||
RequestFormat string
|
||||
ResponseFormat string
|
||||
Timeout int
|
||||
RequestParams []*RequestParam
|
||||
RequestHeaders []*HeaderParam
|
||||
Response []*ResponseParam
|
||||
SuccessBy string
|
||||
FailedMessageBy string
|
||||
}
|
||||
|
||||
type FixedAction struct {
|
||||
Action
|
||||
RequiredRequestParams []*RequestParam
|
||||
NotRequiredRequestParams []*RequestParam
|
||||
FuncParams string
|
||||
}
|
||||
|
||||
type RequestParam struct {
|
||||
Name string
|
||||
Type string
|
||||
Required bool
|
||||
Value any
|
||||
Desc string
|
||||
Sub []*RequestParam
|
||||
}
|
||||
|
||||
type HeaderParam struct {
|
||||
Name string
|
||||
Value string
|
||||
Desc string
|
||||
}
|
||||
|
||||
type ResponseParam struct {
|
||||
Name string
|
||||
Type string
|
||||
Desc string
|
||||
Sub []*ResponseParam
|
||||
}
|
||||
|
||||
type CloudConfig struct {
|
||||
Desc string
|
||||
Config map[string]any
|
||||
RequestFormat string
|
||||
ResponseFormat string
|
||||
Timeout int
|
||||
RequestParams []*RequestParam
|
||||
RequestHeaders []*HeaderParam
|
||||
Response []*ResponseParam
|
||||
Api map[string]*Action
|
||||
}
|
||||
|
||||
// var encryptdMatcher = regexp.MustCompile(`<\*\*([\w-=]+)\*\*>`)
|
||||
var encryptdMatcher = regexp.MustCompile(`^[\w-=]+$`)
|
||||
|
||||
var confAes = u.NewAes([]byte("?GQ$0K0GgLdO=f+~L68PLm$uhKr4'=tV"), []byte("VFs7@sK61cj^f?HZ"))
|
||||
var keysIsSet = false
|
||||
|
||||
func SetSSKey(key, iv []byte) {
|
||||
if !keysIsSet {
|
||||
confAes = u.NewAes(key, iv)
|
||||
keysIsSet = true
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed export.ts
|
||||
var exportTS string
|
||||
|
||||
var cloudConfigs map[string]*CloudConfig
|
||||
var fixedCloudConfigs map[string]map[string]*FixedAction
|
||||
var httpClients = map[string]map[int]*httpclient.ClientPool{}
|
||||
var httpClientsLock = sync.RWMutex{}
|
||||
|
||||
func init() {
|
||||
gojs.Register("apigo.cc/cloud", gojs.Module{
|
||||
ObjectMaker: func(vm *goja.Runtime) gojs.Map {
|
||||
logger := gojs.GetLogger(vm)
|
||||
makeConfig(logger)
|
||||
gojsObj := gojs.Map{}
|
||||
|
||||
for cloudName, actions := range fixedCloudConfigs {
|
||||
httpClients[cloudName] = map[int]*httpclient.ClientPool{}
|
||||
cloudObj := map[string]any{}
|
||||
for actionName, action := range actions {
|
||||
if _, ok := httpClients[cloudName][action.Timeout]; !ok {
|
||||
httpClients[cloudName][action.Timeout] = httpclient.GetClient(time.Duration(action.Timeout) * time.Millisecond)
|
||||
}
|
||||
cloudObj[actionName] = makeAction(cloudName, action)
|
||||
}
|
||||
gojsObj[cloudName] = cloudObj
|
||||
}
|
||||
|
||||
return gojsObj
|
||||
},
|
||||
TsCodeMaker: func() string {
|
||||
makeConfig(log.DefaultLogger)
|
||||
var err error
|
||||
tsCode := ""
|
||||
tpl := template.New("export")
|
||||
tpl = tpl.Funcs(template.FuncMap{
|
||||
"makeMap": func(args ...any) map[string]any {
|
||||
out := map[string]any{}
|
||||
for i := 0; i < len(args); i += 2 {
|
||||
out[u.String(args[i])] = args[i+1]
|
||||
}
|
||||
// fmt.Println(u.BGreen(u.JsonP(out)))
|
||||
return out
|
||||
},
|
||||
})
|
||||
if tpl, err = tpl.Parse(strings.ReplaceAll(exportTS, "//----", "")); err == nil {
|
||||
buf := bytes.NewBuffer(make([]byte, 0))
|
||||
if err = tpl.Execute(buf, fixedCloudConfigs); err == nil {
|
||||
tsCode = buf.String()
|
||||
} else {
|
||||
fmt.Println(u.BRed(err.Error()))
|
||||
}
|
||||
} else {
|
||||
fmt.Println(u.BRed(err.Error()))
|
||||
}
|
||||
return tsCode
|
||||
},
|
||||
SetSSKey: SetSSKey,
|
||||
})
|
||||
}
|
||||
|
||||
func getHttpClient(cloudName string, timeout int) *httpclient.ClientPool {
|
||||
httpClientsLock.RLock()
|
||||
defer httpClientsLock.RUnlock()
|
||||
return httpClients[cloudName][timeout]
|
||||
}
|
||||
|
||||
var expressionMatcher = regexp.MustCompile(`(?ms){{.+?}}`)
|
||||
var funcNameExcludesMatcher = regexp.MustCompile(`[^\w]`)
|
||||
|
||||
func makeAction(cloudName string, action *FixedAction) any {
|
||||
return func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||
// 解析参数
|
||||
requiredCount := len(action.RequiredRequestParams)
|
||||
args := gojs.MakeArgs(&argsIn, vm).Check(requiredCount)
|
||||
params := args.Map(requiredCount + 1)
|
||||
headers := args.Map(requiredCount + 2)
|
||||
|
||||
// 合并请求参数
|
||||
for i, param := range action.RequiredRequestParams {
|
||||
params[param.Name] = args.Any(i)
|
||||
}
|
||||
|
||||
// 处理默认值
|
||||
for _, param := range action.RequestParams {
|
||||
if param.Value != nil && params[param.Name] == nil {
|
||||
if strValue, ok := param.Value.(string); ok {
|
||||
if !strings.Contains(strValue, "{{") {
|
||||
params[param.Name] = param.Value
|
||||
}
|
||||
} else {
|
||||
params[param.Name] = param.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, header := range action.RequestHeaders {
|
||||
if header.Value != "" && headers[header.Name] == nil {
|
||||
if !strings.Contains(header.Value, "{{") {
|
||||
headers[header.Name] = header.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成request信息
|
||||
requestUrl, err := url.Parse(u.String(action.Config["endpoint"]) + action.Path)
|
||||
if err != nil {
|
||||
panic(vm.NewGoError(err))
|
||||
}
|
||||
paramsSortJoin := func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||
args := gojs.MakeArgs(&argsIn, vm)
|
||||
joinTag := args.Str(0)
|
||||
linkTag := args.Str(1)
|
||||
if joinTag == "" {
|
||||
joinTag = "&"
|
||||
}
|
||||
if linkTag == "" {
|
||||
linkTag = "="
|
||||
}
|
||||
a := make([]string, 0)
|
||||
for k, v := range params {
|
||||
a = append(a, k+linkTag+u.String(v))
|
||||
}
|
||||
slices.Sort(a)
|
||||
return vm.ToValue(strings.Join(a, joinTag))
|
||||
}
|
||||
if requestUrl.RawPath == "" {
|
||||
requestUrl.RawPath = requestUrl.Path
|
||||
}
|
||||
requestInfo := map[string]any{
|
||||
"method": action.Method,
|
||||
"scheme": requestUrl.Scheme,
|
||||
"host": requestUrl.Host,
|
||||
"path": requestUrl.Path,
|
||||
"rawPath": requestUrl.RawPath,
|
||||
"rawQuery": requestUrl.RawQuery,
|
||||
"fragment": requestUrl.Fragment,
|
||||
"rawFragment": requestUrl.RawFragment,
|
||||
"fullPath": requestUrl.RequestURI(),
|
||||
"url": requestUrl.String(),
|
||||
"sortJoin": paramsSortJoin,
|
||||
}
|
||||
requestValue := vm.ToValue(requestInfo)
|
||||
|
||||
// 计算请求参数中的动态值
|
||||
configValue := vm.ToValue(action.Config)
|
||||
actionValue := vm.ToValue(gojs.MakeMap(action))
|
||||
var paramsValue goja.Value
|
||||
var headersValue goja.Value
|
||||
var dataValue goja.Value
|
||||
headersValue = vm.ToValue(headers)
|
||||
|
||||
// 处理基本信息中的动态值
|
||||
if action.Path != "" && strings.Contains(action.Path, "{{") {
|
||||
action.Path = expressionMatcher.ReplaceAllStringFunc(action.Path, func(s string) string {
|
||||
fnCode := s[2 : len(s)-2]
|
||||
newValue := runFn(fnCode, vm, "action, request, config, params, headers", actionValue, requestValue, configValue, paramsValue, headersValue)
|
||||
return u.String(newValue)
|
||||
})
|
||||
requestUrl, err = url.Parse(u.String(action.Config["endpoint"]) + action.Path)
|
||||
if err != nil {
|
||||
panic(vm.NewGoError(err))
|
||||
}
|
||||
if requestUrl.RawPath == "" {
|
||||
requestUrl.RawPath = requestUrl.Path
|
||||
}
|
||||
requestInfo = map[string]any{
|
||||
"method": action.Method,
|
||||
"scheme": requestUrl.Scheme,
|
||||
"host": requestUrl.Host,
|
||||
"path": requestUrl.Path,
|
||||
"rawPath": requestUrl.RawPath,
|
||||
"rawQuery": requestUrl.RawQuery,
|
||||
"fragment": requestUrl.Fragment,
|
||||
"rawFragment": requestUrl.RawFragment,
|
||||
"fullPath": requestUrl.RequestURI(),
|
||||
"url": requestUrl.String(),
|
||||
"sortJoin": paramsSortJoin,
|
||||
}
|
||||
requestValue = vm.ToValue(requestInfo)
|
||||
}
|
||||
|
||||
// 处理请求参数中的动态值
|
||||
for _, param := range action.RequestParams {
|
||||
if param.Value != nil && params[param.Name] == nil {
|
||||
if strValue, ok := param.Value.(string); ok && strings.Contains(strValue, "{{") {
|
||||
isTest := strings.Contains(param.Name, "TEST__")
|
||||
if isTest {
|
||||
fmt.Println("make param:", u.Cyan(param.Name))
|
||||
}
|
||||
|
||||
// 将数据拼接到URL中(部分API在请求中需要计算签名)
|
||||
if action.RequestFormat == "query" || action.RequestFormat == "binary" || action.Method == "GET" || action.Method == "WS" {
|
||||
q := requestUrl.Query()
|
||||
for k, v := range params {
|
||||
q.Set(k, u.String(v))
|
||||
}
|
||||
requestUrl.RawQuery = q.Encode()
|
||||
requestInfo["rawQuery"] = requestUrl.RawQuery
|
||||
requestInfo["fullPath"] = requestUrl.RequestURI()
|
||||
requestInfo["url"] = requestUrl.String()
|
||||
requestValue = vm.ToValue(requestInfo)
|
||||
}
|
||||
|
||||
// 计算动态值
|
||||
var lastRealValue any
|
||||
replaceTimes := 0
|
||||
newValue := expressionMatcher.ReplaceAllStringFunc(strValue, func(s string) string {
|
||||
replaceTimes++
|
||||
fnCode := s[2 : len(s)-2]
|
||||
// fnName := "_CLOUD_FN_" + funcNameExcludesMatcher.ReplaceAllString(fnCode, "_")
|
||||
// fnValue := vm.Get(fnName)
|
||||
// if fnValue == nil {
|
||||
// if _, err := vm.RunString("function " + fnName + "(action, request, config, params, headers){return " + fnCode + "}"); err != nil {
|
||||
// panic(vm.NewGoError(err))
|
||||
// }
|
||||
// fnValue = vm.Get(fnName)
|
||||
// }
|
||||
// if fn, ok := goja.AssertFunction(fnValue); ok {
|
||||
// if r, err := fn(nil, actionValue, requestValue, configValue, vm.ToValue(params), vm.ToValue(headers)); err != nil {
|
||||
// panic(vm.NewGoError(err))
|
||||
// } else {
|
||||
// lastRealValue = r.Export()
|
||||
// if isTest {
|
||||
// fmt.Println(" >", u.BMagenta(s), u.BCyan(u.JsonP(lastRealValue)), ".")
|
||||
// }
|
||||
// return u.String(r.Export())
|
||||
// }
|
||||
// } else {
|
||||
// panic(vm.NewGoError(errors.New("failed to make function " + fnName)))
|
||||
// }
|
||||
paramsValue = vm.ToValue(params)
|
||||
lastRealValue = runFn(fnCode, vm, "action, request, config, params, headers", actionValue, requestValue, configValue, paramsValue, headersValue)
|
||||
if isTest {
|
||||
fmt.Println(" >", u.BMagenta(s), u.BCyan(lastRealValue), ".")
|
||||
}
|
||||
return u.String(lastRealValue)
|
||||
})
|
||||
|
||||
if replaceTimes == 1 && newValue == u.String(lastRealValue) {
|
||||
// 如果是单一公式,直接使用最后一次替换真实类型的结果
|
||||
if !isTest {
|
||||
params[param.Name] = lastRealValue
|
||||
}
|
||||
} else {
|
||||
// 替换为计算的结果
|
||||
if !isTest {
|
||||
params[param.Name] = newValue
|
||||
} else {
|
||||
// 打印调试信息
|
||||
fmt.Println(" >", u.Cyan(u.JsonP(newValue)), ".")
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
paramsValue = vm.ToValue(params)
|
||||
|
||||
// 根据RequestFormat处理请求数据
|
||||
if action.RequestFormat == "query" || action.RequestFormat == "binary" || action.Method == "GET" || action.Method == "WS" {
|
||||
// 将数据拼接到URL中
|
||||
q := requestUrl.Query()
|
||||
for k, v := range params {
|
||||
q.Set(k, u.String(v))
|
||||
}
|
||||
requestUrl.RawQuery = q.Encode()
|
||||
requestInfo["rawQuery"] = requestUrl.RawQuery
|
||||
requestInfo["fullPath"] = requestUrl.RequestURI()
|
||||
requestInfo["url"] = requestUrl.String()
|
||||
requestValue = vm.ToValue(requestInfo)
|
||||
}
|
||||
|
||||
var data any
|
||||
switch action.RequestFormat {
|
||||
case "query":
|
||||
// 将数据拼接到URL中
|
||||
case "binary":
|
||||
// 将数据以二进制形式POST,参数拼接到URL中
|
||||
args.Check(requiredCount + 1)
|
||||
data = args.Bytes(requiredCount)
|
||||
case "form", "upload":
|
||||
// 将数据以表单类型提交
|
||||
args.Check(requiredCount + 1)
|
||||
form := make(map[string]string, 0)
|
||||
for k, v := range params {
|
||||
form[k] = u.String(v)
|
||||
}
|
||||
data = form
|
||||
case "json":
|
||||
// 将数据以JSON格式提交
|
||||
data = u.Json(params)
|
||||
default:
|
||||
if action.Method == "GET" {
|
||||
// 将数据拼接到URL中
|
||||
} else {
|
||||
// 将数据以JSON格式提交
|
||||
data = params
|
||||
}
|
||||
}
|
||||
// data = `{"EngSerViceType":"8k_zh","SourceType":1,"VoiceFormat":"mp3","Data":"AA","DataLen":0,"WordInfo":0,"FilterDirty":0,"FilterModal":0,"FilterPunc":0,"ConvertNumMode":1,"CustomizationId":"","InputSampleRate":0}`
|
||||
|
||||
dataValue = vm.ToValue(data)
|
||||
|
||||
// 计算请求头中的动态值
|
||||
for _, header := range action.RequestHeaders {
|
||||
if header.Value != "" && headers[header.Name] == nil {
|
||||
if strings.Contains(header.Value, "{{") {
|
||||
isTest := strings.Contains(header.Name, "TEST__")
|
||||
if isTest {
|
||||
fmt.Println("make header:", u.Cyan(header.Name))
|
||||
}
|
||||
var lastRealValue any
|
||||
replaceTimes := 0
|
||||
newValue := expressionMatcher.ReplaceAllStringFunc(header.Value, func(s string) string {
|
||||
replaceTimes++
|
||||
fnCode := s[2 : len(s)-2]
|
||||
// fnName := "_CLOUD_FN_" + funcNameExcludesMatcher.ReplaceAllString(fnCode, "_")
|
||||
// fnValue := vm.Get(fnName)
|
||||
// if fnValue == nil {
|
||||
// if _, err := vm.RunString("function " + fnName + "(action, request, config, params, headers, data){return " + fnCode + "}"); err != nil {
|
||||
// panic(vm.NewGoError(err))
|
||||
// }
|
||||
// fnValue = vm.Get(fnName)
|
||||
// }
|
||||
// if fn, ok := goja.AssertFunction(fnValue); ok {
|
||||
// if r, err := fn(nil, actionValue, requestValue, configValue, vm.ToValue(params), vm.ToValue(headers), vm.ToValue(data)); err != nil {
|
||||
// panic(vm.NewGoError(err))
|
||||
// } else {
|
||||
// lastRealValue = r.Export()
|
||||
// if isTest {
|
||||
// fmt.Println(" >", u.BMagenta(s), u.BCyan(u.JsonP(lastRealValue)), ".")
|
||||
// }
|
||||
// return u.String(lastRealValue)
|
||||
// }
|
||||
// } else {
|
||||
// panic(vm.NewGoError(errors.New("failed to make function " + fnName)))
|
||||
// }
|
||||
headersValue = vm.ToValue(headers)
|
||||
lastRealValue = runFn(fnCode, vm, "action, request, config, params, headers, data", actionValue, requestValue, configValue, paramsValue, headersValue, dataValue)
|
||||
if isTest {
|
||||
fmt.Println(" >", u.BMagenta(s), u.BCyan(lastRealValue), ".")
|
||||
}
|
||||
// if header.Name == "TEST__待签名" {
|
||||
// fmt.Println(u.BGreen(u.Json(lastRealValue)), ".")
|
||||
// fmt.Println(u.BGreen(hex.EncodeToString(u.Sha256([]byte(u.String(lastRealValue))))))
|
||||
// }
|
||||
return u.String(lastRealValue)
|
||||
})
|
||||
|
||||
if replaceTimes == 1 && newValue == u.String(lastRealValue) {
|
||||
// 如果是单一公式,直接使用最后一次替换真实类型的结果
|
||||
if !isTest {
|
||||
headers[header.Name] = lastRealValue
|
||||
}
|
||||
} else {
|
||||
// 替换为计算的结果
|
||||
if !isTest {
|
||||
headers[header.Name] = newValue
|
||||
} else {
|
||||
// 打印调试信息
|
||||
fmt.Println(" >", u.Cyan(u.JsonP(newValue)), ".")
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO 支持SSE
|
||||
// TODO 支持Download(异步回调)
|
||||
|
||||
// 处理请求头
|
||||
headersArr := make([]string, 0)
|
||||
for k, v := range headers {
|
||||
headersArr = append(headersArr, k, u.String(v))
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
hc := getHttpClient(cloudName, action.Timeout)
|
||||
var httpResult *httpclient.Result
|
||||
var otherResult any
|
||||
t1 := time.Now().UnixMilli()
|
||||
if action.RequestFormat == "upload" {
|
||||
// 处理上传文件
|
||||
files := args.Map(requiredCount)
|
||||
httpResult, _ = hc.MPost(requestUrl.String(), data.(map[string]string), files, headersArr...)
|
||||
} else if action.Method == "WS" {
|
||||
// 处理WS请求
|
||||
args.Check(requiredCount + 1)
|
||||
callback := args.Func(requiredCount)
|
||||
reqHeader := http.Header{}
|
||||
for k, v := range headers {
|
||||
reqHeader.Set(k, u.String(v))
|
||||
}
|
||||
if conn, resp, err := websocket.DefaultDialer.Dial(strings.Replace(requestUrl.String(), "http", "ws", 1), reqHeader); err == nil {
|
||||
for {
|
||||
if conn != nil {
|
||||
var data any
|
||||
var msgType string
|
||||
if typ, buf, err := conn.ReadMessage(); err == nil {
|
||||
switch typ {
|
||||
case websocket.TextMessage:
|
||||
msgType = "text"
|
||||
if action.ResponseFormat == "json" {
|
||||
obj := map[string]any{}
|
||||
if err := json.Unmarshal(buf, &obj); err == nil {
|
||||
msgType = "json"
|
||||
data = obj
|
||||
} else {
|
||||
data = string(buf)
|
||||
}
|
||||
}
|
||||
case websocket.BinaryMessage:
|
||||
msgType = "binary"
|
||||
data = buf
|
||||
default:
|
||||
msgType = "unknown"
|
||||
data = string(buf)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
if msgType == action.ResponseFormat {
|
||||
// 最后一个类型相同的消息作为输出
|
||||
otherResult = data
|
||||
}
|
||||
if callback != nil {
|
||||
if r, err := callback(nil, vm.ToValue(data), vm.ToValue(msgType)); err != nil {
|
||||
conn.Close()
|
||||
panic(vm.NewGoError(err))
|
||||
} else if u.Bool(r.Export()) {
|
||||
fmt.Println(222, r.Export())
|
||||
conn.Close()
|
||||
break
|
||||
} else {
|
||||
fmt.Println(333, r.Export())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
httpResult = &httpclient.Result{
|
||||
Response: resp,
|
||||
Error: err,
|
||||
}
|
||||
} else {
|
||||
panic(vm.NewGoError(err))
|
||||
}
|
||||
|
||||
} else {
|
||||
// fmt.Println("request", action.Method, requestUrl.String(), u.Json(headersArr), "...")
|
||||
httpResult = hc.Do(action.Method, requestUrl.String(), data, headersArr...)
|
||||
// fmt.Println("response", result.Response.StatusCode, result.Response.Status, result.String(), "...")
|
||||
}
|
||||
if httpResult.Error != nil {
|
||||
panic(vm.NewGoError(httpResult.Error))
|
||||
}
|
||||
|
||||
usedTime := time.Now().UnixMilli() - t1
|
||||
outHeaders := map[string]string{}
|
||||
for k, v := range httpResult.Response.Header {
|
||||
outHeaders[k] = v[0]
|
||||
}
|
||||
gojsResult := gojs.Map{
|
||||
"statusCode": httpResult.Response.StatusCode,
|
||||
"status": httpResult.Response.Status,
|
||||
"headers": outHeaders,
|
||||
"usedTime": usedTime,
|
||||
}
|
||||
|
||||
var result any
|
||||
if action.Method == "WS" {
|
||||
result = otherResult
|
||||
} else {
|
||||
switch action.ResponseFormat {
|
||||
case "json":
|
||||
result = httpResult.Map()
|
||||
case "binary":
|
||||
gojsResult["result"] = httpResult.Bytes()
|
||||
default:
|
||||
gojsResult["result"] = httpResult.String()
|
||||
}
|
||||
}
|
||||
var responseValue goja.Value
|
||||
var resultValue goja.Value
|
||||
|
||||
if action.SuccessBy != "" || action.FailedMessageBy != "" {
|
||||
responseValue = vm.ToValue(gojsResult)
|
||||
resultValue = vm.ToValue(result)
|
||||
}
|
||||
|
||||
if action.SuccessBy != "" {
|
||||
fnResult := runFn(action.SuccessBy, vm, "action, request, config, params, headers, data, response, result", actionValue, requestValue, configValue, paramsValue, headersValue, dataValue, responseValue, resultValue)
|
||||
gojsResult["success"] = u.Bool(fnResult)
|
||||
} else {
|
||||
gojsResult["success"] = httpResult.Response.StatusCode == 200
|
||||
}
|
||||
|
||||
if gojsResult["success"] == true {
|
||||
gojsResult["failedMessage"] = ""
|
||||
} else {
|
||||
if action.FailedMessageBy != "" {
|
||||
fnResult := runFn(action.FailedMessageBy, vm, "action, request, config, params, headers, data, response, result", actionValue, requestValue, configValue, paramsValue, headersValue, dataValue, responseValue, resultValue)
|
||||
gojsResult["failedMessage"] = u.String(fnResult)
|
||||
} else {
|
||||
gojsResult["failedMessage"] = httpResult.Response.Status
|
||||
}
|
||||
}
|
||||
|
||||
gojsResult["result"] = result
|
||||
return vm.ToValue(gojsResult)
|
||||
}
|
||||
}
|
||||
|
||||
func runFn(fnCode string, vm *goja.Runtime, argsParams string, args ...goja.Value) any {
|
||||
fnName := "_CLOUD_FN_" + funcNameExcludesMatcher.ReplaceAllString(fnCode, "_")
|
||||
fnValue := vm.Get(fnName)
|
||||
if fnValue == nil {
|
||||
if _, err := vm.RunString("function " + fnName + "(" + argsParams + "){return " + fnCode + "}"); err != nil {
|
||||
panic(vm.NewGoError(err))
|
||||
}
|
||||
fnValue = vm.Get(fnName)
|
||||
}
|
||||
if fn, ok := goja.AssertFunction(fnValue); ok {
|
||||
if r, err := fn(nil, args...); err != nil {
|
||||
panic(vm.NewGoError(err))
|
||||
} else {
|
||||
return r.Export()
|
||||
}
|
||||
} else {
|
||||
panic(vm.NewGoError(errors.New("failed to make function " + fnName)))
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfig(logger *log.Logger) {
|
||||
if cloudConfigs != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 读取所有的配置文件
|
||||
cloudConfigs = map[string]*CloudConfig{}
|
||||
fixedCloudConfigs = map[string]map[string]*FixedAction{}
|
||||
for _, f := range u.ReadDirN("api") {
|
||||
if strings.HasSuffix(f.Name, ".yml") {
|
||||
apiName := f.Name[:len(f.Name)-4]
|
||||
cloudConf := CloudConfig{}
|
||||
if err := u.LoadX(f.FullName, &cloudConf); err != nil {
|
||||
logger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
cloudConfigs[apiName] = &cloudConf
|
||||
}
|
||||
}
|
||||
// 载入补充的配置
|
||||
config.LoadConfig("cloud", &cloudConfigs)
|
||||
|
||||
// 根据配置生成gojs对象
|
||||
for cloudName, cloudConf := range cloudConfigs {
|
||||
// 解密 & 合并配置
|
||||
if cloudConf.Config == nil {
|
||||
cloudConf.Config = map[string]any{}
|
||||
}
|
||||
decryptConfig(cloudConf.Config)
|
||||
fixedActions := map[string]*FixedAction{}
|
||||
for actionName, apiConf := range cloudConf.Api {
|
||||
// var newConf map[string]any
|
||||
// if v, ok := cloudConf.ApiConfig[actionName]; ok && v != nil {
|
||||
// newConf = *v
|
||||
// }
|
||||
// if newConf == nil {
|
||||
// newConf = map[string]any{}
|
||||
// }
|
||||
// decryptConfig(newConf)
|
||||
// for k, v := range cloudConf.Config {
|
||||
// if _, ok := newConf[k]; !ok {
|
||||
// newConf[k] = v
|
||||
// }
|
||||
// }
|
||||
decryptConfig(apiConf.Config)
|
||||
for k, v := range cloudConf.Config {
|
||||
if _, ok := apiConf.Config[k]; !ok {
|
||||
apiConf.Config[k] = v
|
||||
}
|
||||
}
|
||||
fixedActions[actionName] = &FixedAction{
|
||||
Action: *apiConf,
|
||||
// Config: newConf,
|
||||
RequiredRequestParams: make([]*RequestParam, 0),
|
||||
NotRequiredRequestParams: make([]*RequestParam, 0),
|
||||
}
|
||||
}
|
||||
for actionName, apiConf := range fixedActions {
|
||||
// 合并请求参数
|
||||
existsRequestParams := map[string]bool{}
|
||||
for _, param := range apiConf.RequestParams {
|
||||
existsRequestParams[param.Name] = true
|
||||
}
|
||||
for _, param := range cloudConf.RequestParams {
|
||||
if !existsRequestParams[param.Name] {
|
||||
existsRequestParams[param.Name] = true
|
||||
apiConf.RequestParams = append(apiConf.RequestParams, param)
|
||||
}
|
||||
}
|
||||
for _, param := range apiConf.RequestParams {
|
||||
if param.Value == nil {
|
||||
if param.Required {
|
||||
apiConf.RequiredRequestParams = append(apiConf.RequiredRequestParams, param)
|
||||
} else {
|
||||
apiConf.NotRequiredRequestParams = append(apiConf.NotRequiredRequestParams, param)
|
||||
}
|
||||
} else {
|
||||
apiConf.NotRequiredRequestParams = append(apiConf.NotRequiredRequestParams, param)
|
||||
}
|
||||
}
|
||||
fixedActions[actionName].RequestParams = apiConf.RequestParams
|
||||
fixedActions[actionName].RequiredRequestParams = apiConf.RequiredRequestParams
|
||||
fixedActions[actionName].NotRequiredRequestParams = apiConf.NotRequiredRequestParams
|
||||
|
||||
// 合并请求头
|
||||
if apiConf.RequestHeaders == nil {
|
||||
apiConf.RequestHeaders = make([]*HeaderParam, 0)
|
||||
}
|
||||
existsRequestHeaders := map[string]bool{}
|
||||
for _, header := range apiConf.RequestHeaders {
|
||||
existsRequestHeaders[header.Name] = true
|
||||
}
|
||||
for _, header := range cloudConf.RequestHeaders {
|
||||
if !existsRequestHeaders[header.Name] {
|
||||
existsRequestHeaders[header.Name] = true
|
||||
apiConf.RequestHeaders = append(apiConf.RequestHeaders, header)
|
||||
}
|
||||
}
|
||||
fixedActions[actionName].RequestHeaders = apiConf.RequestHeaders
|
||||
|
||||
// 合并响应参数
|
||||
if apiConf.Response == nil {
|
||||
apiConf.Response = make([]*ResponseParam, 0)
|
||||
}
|
||||
existsResponse := map[string]bool{}
|
||||
for _, resp := range apiConf.Response {
|
||||
existsResponse[resp.Name] = true
|
||||
}
|
||||
for _, resp := range cloudConf.Response {
|
||||
if !existsResponse[resp.Name] {
|
||||
existsResponse[resp.Name] = true
|
||||
apiConf.Response = append(apiConf.Response, resp)
|
||||
}
|
||||
}
|
||||
fixedActions[actionName].Response = apiConf.Response
|
||||
|
||||
// 处理Sub类型
|
||||
makeRequestType(cloudName, actionName, fixedActions[actionName].RequestParams)
|
||||
makeResponseType(cloudName, actionName, fixedActions[actionName].Response)
|
||||
|
||||
// 合并其他
|
||||
apiConf.Method = strings.ToUpper(apiConf.Method)
|
||||
if apiConf.Method == "" {
|
||||
apiConf.Method = "POST"
|
||||
}
|
||||
apiConf.RequestFormat = strings.ToLower(apiConf.RequestFormat)
|
||||
if apiConf.RequestFormat == "" {
|
||||
apiConf.RequestFormat = cloudConf.RequestFormat
|
||||
}
|
||||
apiConf.ResponseFormat = strings.ToLower(apiConf.ResponseFormat)
|
||||
if apiConf.ResponseFormat == "" {
|
||||
apiConf.ResponseFormat = cloudConf.ResponseFormat
|
||||
}
|
||||
if apiConf.Timeout == 0 {
|
||||
apiConf.Timeout = cloudConf.Timeout
|
||||
}
|
||||
|
||||
// 生成函数参数
|
||||
funcParams := make([]string, 0)
|
||||
for _, param := range apiConf.RequiredRequestParams {
|
||||
if len(param.Sub) > 0 {
|
||||
funcParams = append(funcParams, fmt.Sprintf("%s:%s", param.Name, param.Type))
|
||||
} else {
|
||||
funcParams = append(funcParams, fmt.Sprintf("%s:%s", param.Name, param.Type))
|
||||
}
|
||||
}
|
||||
if apiConf.RequestFormat == "binary" {
|
||||
funcParams = append(funcParams, "data:any")
|
||||
}
|
||||
if apiConf.Method == "WS" {
|
||||
funcParams = append(funcParams, "callback:(data:any,type:string)=>boolean")
|
||||
}
|
||||
if len(apiConf.NotRequiredRequestParams) > 0 {
|
||||
funcParams = append(funcParams, fmt.Sprintf("options?:%s_%sParams", cloudName, actionName))
|
||||
}
|
||||
if len(apiConf.RequestHeaders) > 0 {
|
||||
funcParams = append(funcParams, fmt.Sprintf("headers?:%s_%sHeaders", cloudName, actionName))
|
||||
}
|
||||
apiConf.FuncParams = strings.Join(funcParams, ", ")
|
||||
fixedActions[actionName].FuncParams = apiConf.FuncParams
|
||||
}
|
||||
fixedCloudConfigs[cloudName] = fixedActions
|
||||
}
|
||||
}
|
||||
|
||||
func makeRequestType(cloudName, actionName string, params []*RequestParam) {
|
||||
for i, param := range params {
|
||||
if len(param.Sub) > 0 {
|
||||
subType := fmt.Sprintf("%s_%sParams%s", cloudName, actionName, param.Name)
|
||||
if param.Type == "Array" {
|
||||
params[i].Type = fmt.Sprintf("Array<%s>", subType)
|
||||
} else {
|
||||
params[i].Type = subType
|
||||
}
|
||||
makeRequestType(cloudName, actionName, param.Sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeResponseType(cloudName, actionName string, params []*ResponseParam) {
|
||||
for i, param := range params {
|
||||
if len(param.Sub) > 0 {
|
||||
subType := fmt.Sprintf("%s_%sResult%s", cloudName, actionName, param.Name)
|
||||
if param.Type == "Array" {
|
||||
params[i].Type = fmt.Sprintf("Array<%s>", subType)
|
||||
} else {
|
||||
params[i].Type = subType
|
||||
}
|
||||
makeResponseType(cloudName, actionName, param.Sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decryptConfig(conf map[string]any) {
|
||||
for k, v := range conf {
|
||||
if vStr, ok := v.(string); ok && encryptdMatcher.MatchString(vStr) {
|
||||
conf[k] = confAes.DecryptUrlBase64ToString(vStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
80
export.ts
Normal file
80
export.ts
Normal file
@ -0,0 +1,80 @@
|
||||
//----{{- define "subType" -}}
|
||||
//----{{$cloudName:=.cloudName}}{{$actionName:=.actionName}}{{$paramType:=.paramType}}
|
||||
//---- {{- range $k, $v := .list }}
|
||||
//---- {{- if $v.Sub }}
|
||||
//----{{- template "subType" (makeMap "list" $v.Sub "cloudName" $cloudName "actionName" $actionName "paramType" $paramType) }}
|
||||
//----interface {{ $cloudName }}_{{ $actionName }}{{ $paramType }}{{ $v.Name }} {
|
||||
//---- {{- range $k2, $v2 := $v.Sub }}
|
||||
//---- {{ $v2.Name }}: {{ $v2.Type }} // {{ $v2.Desc }}
|
||||
//---- {{- end }}
|
||||
//----}
|
||||
//---- {{- end }}
|
||||
//---- {{- end }}
|
||||
//----{{- end }}
|
||||
|
||||
//----{{- range $cloudName, $cloudConf := . -}}
|
||||
//----interface {{ $cloudName }}Cloud {
|
||||
//----{{- range $actionName, $api := $cloudConf }}
|
||||
//---- /**
|
||||
//---- * {{ $actionName }} {{ $api.Desc }}
|
||||
//---- {{- range $k, $v := $api.RequiredRequestParams }}
|
||||
//---- * @param {{ $v.Name }} {{ $v.Desc }}
|
||||
//---- {{- end }}
|
||||
//---- {{- if eq $api.RequestFormat "binary" }}
|
||||
//---- * @param data binary data
|
||||
//---- {{- end }}
|
||||
//---- */
|
||||
//---- {{ $actionName }}: ({{ $api.FuncParams }}) => {{ $cloudName }}_{{ $actionName }}Response
|
||||
//----{{- end }}
|
||||
//----}
|
||||
//----{{ range $actionName, $api := $cloudConf }}
|
||||
//----// {{ $actionName }} {{ $api.Desc }}
|
||||
//----{{- template "subType" (makeMap "list" $api.RequestParams "cloudName" $cloudName "actionName" $actionName "paramType" "Params") }}
|
||||
//---- {{- if $api.NotRequiredRequestParams }}
|
||||
//----interface {{ $cloudName }}_{{ $actionName }}Params {
|
||||
//---- {{- range $k, $v := $api.NotRequiredRequestParams }}
|
||||
//---- {{ $v.Name }}: {{ $v.Type }} // {{ $v.Desc }}
|
||||
//---- {{- end }}
|
||||
//----}
|
||||
//---- {{- end }}
|
||||
//---- {{- if $api.RequestHeaders }}
|
||||
//----interface {{ $cloudName }}_{{ $actionName }}Headers {
|
||||
//---- {{- range $k, $v := $api.RequestHeaders }}
|
||||
//---- "{{ $v.Name }}": string // {{ $v.Desc }}
|
||||
//---- {{- end }}
|
||||
//----}
|
||||
//---- {{- end }}
|
||||
//---- {{- if eq $api.ResponseFormat "json" }}
|
||||
//----{{- template "subType" (makeMap "list" $api.Response "cloudName" $cloudName "actionName" $actionName "paramType" "Result") }}
|
||||
//----interface {{ $cloudName }}_{{ $actionName }}Result {
|
||||
//---- {{- range $k, $v := $api.Response }}
|
||||
//---- {{ $v.Name }}: {{ $v.Type }} // {{ $v.Desc }}
|
||||
//---- {{- end }}
|
||||
//----}
|
||||
//---- {{- end }}
|
||||
//----interface {{ $cloudName }}_{{ $actionName }}Response {
|
||||
//---- success: boolean
|
||||
//---- failedMessage: string
|
||||
//---- statusCode: number
|
||||
//---- status: string
|
||||
//---- headers: Object
|
||||
//---- usedTime: number
|
||||
//---- {{- if eq $api.ResponseFormat "json" }}
|
||||
//---- result: {{ $cloudName }}_{{ $actionName }}Result
|
||||
//---- {{- else if eq $api.ResponseFormat "binary" }}
|
||||
//---- result: any
|
||||
//---- {{- else }}
|
||||
//---- result: string
|
||||
//---- {{- end }}
|
||||
//----}
|
||||
//----{{- end }}
|
||||
//----
|
||||
//----{{ end }}
|
||||
//----{{- range $cloudName, $cloudConf := . }}
|
||||
//----let {{ $cloudName }}: {{ $cloudName }}Cloud = null as any
|
||||
//----{{- end }}
|
||||
export {
|
||||
//----{{- range $cloudName, $cloudConf := . }}
|
||||
//----{{ $cloudName }},
|
||||
//----{{- end }}
|
||||
}
|
||||
25
go.mod
Normal file
25
go.mod
Normal file
@ -0,0 +1,25 @@
|
||||
module apigo.cc/cloud
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
apigo.cc/gojs v0.0.4
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/ssgo/config v1.7.9
|
||||
github.com/ssgo/httpclient v1.7.8
|
||||
github.com/ssgo/log v1.7.7
|
||||
github.com/ssgo/u v1.7.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
|
||||
github.com/ssgo/standard v1.7.7 // indirect
|
||||
github.com/ssgo/tool v0.4.27 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
10
tests/.gitignore
vendored
Normal file
10
tests/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
.*
|
||||
!.gitignore
|
||||
go.sum
|
||||
build
|
||||
release
|
||||
node_modules
|
||||
package.json
|
||||
env.yml
|
||||
*.mp3
|
||||
*.wav
|
||||
450
tests/api/tencent.yml
Normal file
450
tests/api/tencent.yml
Normal file
@ -0,0 +1,450 @@
|
||||
config:
|
||||
requestHeaders:
|
||||
api:
|
||||
fastAsr:
|
||||
config:
|
||||
service: asr
|
||||
endpoint: https://asr.tencentcloudapi.com
|
||||
desc: 本接口用于对60秒之内的短音频文件进行识别
|
||||
method: POST
|
||||
path: /
|
||||
requestFormat: json
|
||||
responseFormat: json
|
||||
requestHeaders:
|
||||
- name: Content-Type
|
||||
value: application/json
|
||||
desc: 请求体格式
|
||||
- name: X-TC-Action
|
||||
value: SentenceRecognition
|
||||
desc: 公共参数,本接口取值:SentenceRecognition
|
||||
- name: X-TC-Version
|
||||
value: "2019-06-14"
|
||||
desc: 公共参数,本接口取值:2019-06-14
|
||||
- name: X-TC-Region
|
||||
value: ap-shanghai
|
||||
desc: HTTP 请求头:X-TC-Region。地域参数,用来标识希望操作哪个地域的数据。取值参考接口文档中输入参数章节关于公共参数 Region 的说明。注意:某些接口不需要传递该参数,接口文档中会对此特别说明,此时即使传递该参数也不会生效。
|
||||
- name: X-TC-Timestamp
|
||||
value: "{{util.timestamp()}}"
|
||||
desc: HTTP 请求头:X-TC-Timestamp。当前 UNIX 时间戳,可记录发起 API 请求的时间。例如 1529223702。注意:如果与服务器时间相差超过5分钟,会引起签名过期错误。
|
||||
# 接口文档:https://cloud.tencent.com/document/product/1093/35646
|
||||
# 签名文档:https://cloud.tencent.com/document/product/1093/35641
|
||||
# 签名调试:https://console.cloud.tencent.com/api/explorer?Product=asr&Version=2019-06-14&Action=SentenceRecognition&SignVersion=api3v3
|
||||
# - name: TEST__请求主体
|
||||
# value: "{{ data }}"
|
||||
# - name: TEST__请求主体哈希
|
||||
# value: "{{ util.hex(util.sha256(data)) }}"
|
||||
# - name: TEST__待签名
|
||||
# value: "{{request.method+'\\n'+request.path+'\\n'+'\\n'+'content-type:'+headers['Content-Type']+'\\nhost:'+request.host+'\\nx-tc-action:'+headers['X-TC-Action'].toLowerCase()+'\\n'+'\\n'+'content-type;host;x-tc-action'+'\\n'+util.hex(util.sha256(data))}}"
|
||||
# - name: TEST__签名1
|
||||
# value: "{{util.hex(util.sha256(request.method+'\\n'+request.path+'\\n'+'\\n'+'content-type:'+headers['Content-Type']+'\\nhost:'+request.host+'\\nx-tc-action:'+headers['X-TC-Action'].toLowerCase()+'\\n'+'\\n'+'content-type;host;x-tc-action'+'\\n'+util.hex(util.sha256(data))))}}"
|
||||
# - name: TEST__拼接
|
||||
# value: "{{'TC3-HMAC-SHA256\\n'+headers['X-TC-Timestamp']+'\\n'+util.toDate(headers['X-TC-Timestamp'])+'/'+config.service+'/tc3_request\\n'+util.hex(util.sha256(request.method+'\\n'+request.path+'\\n'+'\\n'+'content-type:'+headers['Content-Type']+'\\nhost:'+request.host+'\\nx-tc-action:'+headers['X-TC-Action'].toLowerCase()+'\\n'+'\\n'+'content-type;host;x-tc-action'+'\\n'+util.hex(util.sha256(data))))}}"
|
||||
# - name: TEST__签名参数1_Key
|
||||
# value: "{{ 'TC3'+config.secretKey }}"
|
||||
# - name: TEST__签名参数2_Date
|
||||
# value: "{{ util.toDate(headers['X-TC-Timestamp']) }}"
|
||||
# - name: TEST__签名参数3_service
|
||||
# value: "{{ config.service }}"
|
||||
# - name: TEST__签名参数4_tc3_request
|
||||
# value: "{{ 'tc3_request' }}"
|
||||
# - name: TEST__计算签名
|
||||
# value: "{{ util.hex( util.hmacSHA256( util.hmacSHA256(util.hmacSHA256(util.hmacSHA256('TC3'+config.secretKey, util.toDate(headers['X-TC-Timestamp'])), config.service), 'tc3_request'), 'TC3-HMAC-SHA256\\n'+headers['X-TC-Timestamp']+'\\n'+util.toDate(headers['X-TC-Timestamp'])+'/'+config.service+'/tc3_request\\n'+util.hex(util.sha256(request.method+'\\n'+request.path+'\\n'+'\\n'+'content-type:'+headers['Content-Type']+'\\nhost:'+request.host+'\\nx-tc-action:'+headers['X-TC-Action'].toLowerCase()+'\\n'+'\\n'+'content-type;host;x-tc-action'+'\\n'+util.hex(util.sha256(data)))) ) ) }}"
|
||||
- name: Authorization
|
||||
value: "TC3-HMAC-SHA256 Credential={{config.secretId}}/{{util.toDate(headers['X-TC-Timestamp'])}}/{{config.service}}/tc3_request, SignedHeaders=content-type;host;x-tc-action, Signature={{ util.hex( util.hmacSHA256( util.hmacSHA256(util.hmacSHA256(util.hmacSHA256('TC3'+config.secretKey, util.toDate(headers['X-TC-Timestamp'])), config.service), 'tc3_request'), 'TC3-HMAC-SHA256\\n'+headers['X-TC-Timestamp']+'\\n'+util.toDate(headers['X-TC-Timestamp'])+'/'+config.service+'/tc3_request\\n'+util.hex(util.sha256(request.method+'\\n'+request.path+'\\n'+'\\n'+'content-type:'+headers['Content-Type']+'\\nhost:'+request.host+'\\nx-tc-action:'+headers['X-TC-Action'].toLowerCase()+'\\n'+'\\n'+'content-type;host;x-tc-action'+'\\n'+util.hex(util.sha256(data)) )) ) ) }}"
|
||||
desc: 签名认证信息
|
||||
requestParams:
|
||||
- name: EngSerViceType
|
||||
type: string
|
||||
required: true
|
||||
value: 8k_zh
|
||||
desc: 引擎模型类型。电话场景:8k_zh(中文电话通用)、8k_en(英文电话通用);非电话场景:16k_zh(中文通用)、16k_zh-PY(中英粤)、16k_zh_medical(中文医疗)、16k_en(英语)、16k_yue(粤语)、16k_ja(日语)、16k_ko(韩语)、16k_vi(越南语)、16k_ms(马来语)、16k_id(印度尼西亚语)、16k_fil(菲律宾语)、16k_th(泰语)、16k_pt(葡萄牙语)、16k_tr(土耳其语)、16k_ar(阿拉伯语)、16k_es(西班牙语)、16k_hi(印地语)、16k_fr(法语)、16k_de(德语)、16k_zh_dialect(多方言,支持23种方言)
|
||||
- name: SourceType
|
||||
type: number
|
||||
required: true
|
||||
value: 1
|
||||
desc: 语音数据来源。0:语音URL;1:语音数据(post body)
|
||||
- name: VoiceFormat
|
||||
type: string
|
||||
required: true
|
||||
desc: 识别音频的音频格式,支持wav、pcm、ogg-opus、speex、silk、mp3、m4a、aac、amr
|
||||
- name: Url
|
||||
type: string
|
||||
required: false
|
||||
desc: 语音的URL地址,需要公网环境浏览器可下载。当SourceType值为0时须填写该字段,为1时不填。音频时长不能超过60s,音频文件大小不能超过3MB。推荐使用腾讯云COS来存储音频、生成URL并提交请求
|
||||
- name: Data
|
||||
type: string
|
||||
required: true
|
||||
desc: 语音数据,当SourceType值为1(本地语音数据上传)时必须填写,当SourceType值为0(语音URL上传)可不写。要使用base64编码(采用python语言时注意读取文件应该为string而不是byte,以byte格式读取后要decode()。编码后的数据不可带有回车换行符)。音频时长不能超过60s,音频文件大小不能超过3MB(Base64后)
|
||||
- name: DataLen
|
||||
type: number
|
||||
required: false
|
||||
value: "{{util.unBase64(params.Data).length}}"
|
||||
desc: 数据长度,单位为字节。当SourceType值为1(本地语音数据上传)时必须填写,当SourceType值为0(语音URL上传)可不写(此数据长度为数据未进行base64编码时的数据长度)
|
||||
- name: WordInfo
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 是否显示词级别时间戳。0:不显示;1:显示,不包含标点时间戳,2:显示,包含标点时间戳。默认值为0
|
||||
- name: FilterDirty
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 是否过滤脏词(目前支持中文普通话引擎)。0:不过滤脏词;1:过滤脏词;2:将脏词替换为*。默认值为0
|
||||
- name: FilterModal
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 是否过滤语气词(目前支持中文普通话引擎)。0:不过滤语气词;1:部分过滤;2:严格过滤。默认值为0
|
||||
- name: FilterPunc
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 是否过滤标点符号(目前支持中文普通话引擎)。0:不过滤,1:过滤句末标点,2:过滤所有标点。默认值为0
|
||||
- name: ConvertNumMode
|
||||
type: number
|
||||
required: false
|
||||
value: 1
|
||||
desc: 是否进行阿拉伯数字智能转换。0:不转换,直接输出中文数字,1:根据场景智能转换为阿拉伯数字。默认值为1
|
||||
- name: HotwordId
|
||||
type: string
|
||||
required: false
|
||||
desc: 热词id。用于调用对应的热词表,如果在调用语音识别服务时,不进行单独的热词id设置,自动生效默认热词;如果进行了单独的热词id设置,那么将生效单独设置的热词id
|
||||
- name: CustomizationId
|
||||
type: string
|
||||
required: false
|
||||
value: ""
|
||||
desc: 自学习模型id。如设置了该参数,将生效对应的自学习模型
|
||||
- name: HotwordList
|
||||
type: string
|
||||
required: false
|
||||
desc: 临时热词表:该参数用于提升识别准确率。单个热词限制:"热词|权重",单个热词不超过30个字符(最多10个汉字),权重1-11或者100;临时热词表限制:多个热词用英文逗号分割,最多支持128个热词。参数hotword_list(临时热词表)与hotword_id(热词表)区别:hotword_id:热词表。需要先在控制台或接口创建热词表,获得对应hotword_id传入参数来使用热词功能;hotword_list:临时热词表。每次请求时直接传入临时热词表来使用热词功能,云端不保留临时热词表。适用于有极大量热词需求的用户。如果同时传入了hotword_id和hotword_list,会优先使用hotword_list
|
||||
- name: InputSampleRate
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 支持pcm格式的8k音频在与引擎采样率不匹配的情况下升采样到16k后识别,能有效提升识别准确率。仅支持:8000。如:传入8000,则pcm音频采样率为8k,当引擎选用16k_zh,那么该8k采样率的pcm音频可以在16k_zh引擎下正常识别。注:此参数仅适用于pcm格式音频,不传入值将维持默认状态,即默认调用的引擎采样率等于pcm音频采样率
|
||||
response:
|
||||
- name: Response
|
||||
type: Object
|
||||
desc: 识别结果
|
||||
sub:
|
||||
- name: Result
|
||||
type: string
|
||||
desc: 识别结果
|
||||
- name: AudioDuration
|
||||
type: number
|
||||
desc: 请求的音频时长,单位为ms
|
||||
- name: WordSize
|
||||
type: number
|
||||
desc: 词时间戳列表的长度
|
||||
- name: WordList
|
||||
type: string[]
|
||||
desc: 词时间戳列表
|
||||
- name: RequestId
|
||||
type: string
|
||||
desc: 唯一请求ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得RequestId)。定位问题时需要提供该次请求的RequestId
|
||||
successBy: "!result.Response.Error"
|
||||
failedMessageBy: result.Response.Error.Message
|
||||
longAsr:
|
||||
config:
|
||||
endpoint: https://asr.cloud.tencent.com
|
||||
desc: 本接口支持使用者通过HTTPS POST方式上传一段音频并在极短时间内同步返回识别结果(通常30分钟音频可在10秒内完成识别),可满足音视频字幕、准实时质检等场景下对语音文件识别时效性的要求。
|
||||
method: POST
|
||||
path: /asr/flash/v1/{{config.appId}}
|
||||
requestFormat: binary
|
||||
responseFormat: json
|
||||
requestHeaders:
|
||||
- name: Content-Type
|
||||
value: application/octet-stream
|
||||
desc: 固定值为application/octet-stream
|
||||
# - name: TEST__签名参数
|
||||
# value: "{{request.method+request.host+request.fullPath}}"
|
||||
# - name: TEST__生成签名
|
||||
# value: "{{ util.base64(util.hmacSHA1(config.secretKey, request.method+request.host+request.fullPath)) }}"
|
||||
- name: Authorization
|
||||
value: "{{ util.base64(util.hmacSHA1(config.secretKey, request.method+request.host+request.fullPath)) }}"
|
||||
desc: 用户的签名字符串,用于鉴权
|
||||
requestParams:
|
||||
- name: secretid
|
||||
type: string
|
||||
required: true
|
||||
value: "{{config.secretId}}"
|
||||
desc: 用户的secretId,用于鉴权
|
||||
- name: engine_type
|
||||
type: string
|
||||
required: true
|
||||
value: 16k_zh
|
||||
desc: 引擎模型类型,16k_zh:中文通用;16k_zh_large:普方英大模型引擎;16k_multi_lang:多语种大模型引擎;16k_zh_dialect:多方言等;详见:https://cloud.tencent.com/document/product/1093/52097#sign
|
||||
- name: voice_format
|
||||
type: string
|
||||
required: true
|
||||
value:
|
||||
desc: 音频格式,支持wav、pcm、ogg-opus、speex、silk、mp3、m4a、aac、amr
|
||||
- name: timestamp
|
||||
type: number
|
||||
required: true
|
||||
value: "{{util.timestamp()}}"
|
||||
desc: 当前UNIX时间戳,如果与当前时间相差超过3分钟,会报签名失败错误
|
||||
- name: speaker_diarization
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 是否开启说话人分离(目前支持中文普通话引擎),默认为0,0:不开启,1:开启
|
||||
- name: hotword_id
|
||||
type: string
|
||||
required: false
|
||||
value:
|
||||
desc: 热词表id。如不设置该参数,自动生效默认热词表;如设置了该参数,那么将生效对应的热词表
|
||||
- name: customization_id
|
||||
type: string
|
||||
required: false
|
||||
value:
|
||||
desc: 自学习模型id。如设置了该参数,将生效对应的自学习模型
|
||||
- name: filter_dirty
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 是否过滤脏词(目前支持中文普通话引擎),默认为0。0:不过滤脏词;1:过滤脏词;2:将脏词替换为*
|
||||
- name: filter_modal
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 是否过滤语气词(目前支持中文普通话引擎),默认为0。0:不过滤语气词;1:部分过滤;2:严格过滤
|
||||
- name: filter_punc
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 是否过滤标点符号(目前支持中文普通话引擎),默认为0。0:不过滤,1:过滤句末标点,2:过滤所有标点
|
||||
- name: convert_num_mode
|
||||
type: number
|
||||
required: false
|
||||
value: 1
|
||||
desc: 是否进行阿拉伯数字智能转换,默认为1。0:全部转为中文数字;1:根据场景智能转换为阿拉伯数字
|
||||
- name: word_info
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 是否显示词级别时间戳,默认为0。0:不显示;1:显示词时间戳,不包含标点;2:显示词时间戳,包含标点;3:标点符号分段,包含每段时间戳,特别适用于字幕场景(包含词级时间、标点、语速值)
|
||||
- name: first_channel_only
|
||||
type: number
|
||||
required: false
|
||||
value: 1
|
||||
desc: 是否只识别首个声道,默认为1。0:识别所有声道;1:识别首个声道
|
||||
- name: sentence_max_length
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 单标点最多字数,取值范围:[6,40]。默认为0,不开启该功能。该参数可用于字幕生成场景,控制单行字幕最大字数。仅支持8k_zh、16k_zh引擎
|
||||
- name: hotword_list
|
||||
type: string
|
||||
required: false
|
||||
value:
|
||||
desc: 临时热词表:该参数用于提升识别准确率。单个热词限制:"热词|权重",单个热词不超过30个字符(最多10个汉字),权重[1-11]或者100,如:“腾讯云|5”或“ASR|11”;临时热词表限制:多个热词用英文逗号分割,最多支持128个热词,如:“腾讯云|10,语音识别|5,ASR|11”
|
||||
- name: input_sample_rate
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 支持pcm格式的8k音频在与引擎采样率不匹配的情况下升采样到16k后识别,能有效提升识别准确率。仅支持:8000
|
||||
response:
|
||||
- name: code
|
||||
type: number
|
||||
desc: 0:正常,其他,发生错误
|
||||
- name: message
|
||||
type: string
|
||||
desc: code非0时,message中会有错误消息
|
||||
- name: request_id
|
||||
type: string
|
||||
desc: 请求唯一标识,请您记录该值,以便排查错误
|
||||
- name: audio_duration
|
||||
type: number
|
||||
desc: 音频时长,单位为毫秒
|
||||
- name: flash_result
|
||||
type: Array
|
||||
desc: 声道识别结果列表
|
||||
sub:
|
||||
- name: channel_id
|
||||
type: number
|
||||
desc: 声道标识,从0开始,对应音频声道数
|
||||
- name: text
|
||||
type: string
|
||||
desc: 声道音频完整识别结果
|
||||
- name: sentence_list
|
||||
type: Array
|
||||
desc: 句子/段落级别的识别结果列表
|
||||
sub:
|
||||
- name: text
|
||||
type: string
|
||||
desc: 句子/段落的识别结果
|
||||
- name: start_time
|
||||
type: number
|
||||
desc: 句子/段落的起始时间
|
||||
- name: end_time
|
||||
type: number
|
||||
desc: 句子/段落的结束时间
|
||||
- name: speaker_id
|
||||
type: number
|
||||
desc: 说话人 Id(请求中如果设置了 speaker_diarization,可以按照 speaker_id 来区分说话人)
|
||||
- name: word_list
|
||||
type: Array
|
||||
desc: 词级别的识别结果列表
|
||||
sub:
|
||||
- name: word
|
||||
type: string
|
||||
desc: 词的识别结果
|
||||
- name: start_time
|
||||
type: number
|
||||
desc: 词的起始时间
|
||||
- name: end_time
|
||||
type: number
|
||||
desc: 词的结束时间
|
||||
successBy: result.code == 0
|
||||
failedMessageBy: result.message
|
||||
tts:
|
||||
config:
|
||||
endpoint: wss://tts.cloud.tencent.com
|
||||
desc: 将请求文本合成为音频,同步返回合成音频数据及相关文本信息,达到“边合成边播放”的效果
|
||||
method: WS
|
||||
path: /stream_ws
|
||||
requestFormat: query
|
||||
responseFormat: json
|
||||
requestParams:
|
||||
- name: Action
|
||||
type: string
|
||||
required: true
|
||||
value: TextToStreamAudioWS
|
||||
desc: 调用接口名,取值为:TextToStreamAudioWS
|
||||
- name: AppId
|
||||
type: number
|
||||
required: true
|
||||
value: "{{config.appId}}"
|
||||
desc: 账号AppId(请确保该字段数据类型为整型int)
|
||||
- name: SecretId
|
||||
type: string
|
||||
required: true
|
||||
value: "{{config.secretId}}"
|
||||
desc: 腾讯云注册账号的密钥SecretId
|
||||
- name: Timestamp
|
||||
type: number
|
||||
required: true
|
||||
value: "{{util.timestamp()}}"
|
||||
desc: 当前UNIX时间戳,可记录发起API请求的时间
|
||||
- name: Expired
|
||||
type: number
|
||||
required: true
|
||||
value: "{{util.timestamp()+86400}}"
|
||||
desc: 签名的有效期,是一个符合UNIX Epoch时间戳规范的数值,单位为秒;Expired必须大于Timestamp且Expired-Timestamp小于90天
|
||||
- name: SessionId
|
||||
type: string
|
||||
required: true
|
||||
value: "{{util.uniqueId()}}"
|
||||
desc: 语音合成全局唯一标识,一个websocket连接对应一个,用户自己生成(推荐使用uuid),最长128位
|
||||
- name: Text
|
||||
type: string
|
||||
required: true
|
||||
value:
|
||||
desc: 合成语音的源文本,按UTF-8编码统一计算。中文最大支持600个汉字(全角标点符号算一个汉字);英文最大支持1800个字母(半角标点符号算一个字母)
|
||||
- name: VoiceType
|
||||
type: number
|
||||
required: true
|
||||
value:
|
||||
desc: 音色ID,包括标准音色与精品音色,精品音色拟真度更高,价格不同于标准音色,请参见购买指南。完整的音色ID列表请参见音色列表 https://cloud.tencent.com/document/product/1073/92668#55924b56-1a73-4663-a7a1-a8dd82d6e823
|
||||
- name: Volume
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 音量大小,范围[-10,10],对应音量大小。默认为0,代表正常音量,值越大音量越高
|
||||
- name: Speed
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 语速,范围:[-2,6],分别对应不同语速
|
||||
- name: SampleRate
|
||||
type: number
|
||||
required: false
|
||||
value: 16000
|
||||
desc: 音频采样率
|
||||
- name: Codec
|
||||
type: string
|
||||
required: false
|
||||
value: mp3
|
||||
desc: 返回音频格式
|
||||
- name: EnableSubtitle
|
||||
type: boolean
|
||||
required: false
|
||||
value: true
|
||||
desc: 是否开启时间戳功能,默认为false
|
||||
- name: EmotionCategory
|
||||
type: string
|
||||
required: false
|
||||
value: neutral
|
||||
desc: 控制合成音频的情感,仅支持多情感音色使用
|
||||
- name: EmotionIntensity
|
||||
type: number
|
||||
required: false
|
||||
value: 100
|
||||
desc: 控制合成音频情感程度,取值范围为 [50,200],默认为100;只有EmotionCategory不为空时生效
|
||||
- name: SegmentRate
|
||||
type: number
|
||||
required: false
|
||||
value: 0
|
||||
desc: 断句敏感阈值,取值范围:[0,1,2],默认值:0
|
||||
- name: FastVoiceType
|
||||
type: string
|
||||
required: false
|
||||
value:
|
||||
desc: 一句话版声音复刻音色ID,使用一句话版声音复刻音色时需填写
|
||||
# - name: TEST__签名参数
|
||||
# value: "{{'GET'+request.host+request.rawPath+'?'+request.sortJoin()}}"
|
||||
# - name: TEST__生成签名
|
||||
# value: "{{ util.base64(util.hmacSHA1(config.secretKey, 'GET'+request.host+request.rawPath+'?'+request.sortJoin())) }}"
|
||||
- name: Signature
|
||||
type: string
|
||||
value: "{{ util.base64(util.hmacSHA1(config.secretKey, 'GET'+request.host+request.rawPath+'?'+request.sortJoin())) }}"
|
||||
desc: 接口签名参数
|
||||
response:
|
||||
- name: code
|
||||
type: number
|
||||
desc: 状态码,0代表正常,非0值表示发生错误
|
||||
- name: message
|
||||
type: string
|
||||
desc: 错误说明,发生错误时显示这个错误发生的具体原因,随着业务发展或体验优化,此文本可能会经常保持变更或更新
|
||||
- name: session_id
|
||||
type: string
|
||||
desc: 音频流唯一id,由客户端在握手阶段生成并赋值在调用参数中
|
||||
- name: request_id
|
||||
type: string
|
||||
desc: 音频流唯一id,由服务端在握手阶段自动生成
|
||||
- name: message_id
|
||||
type: string
|
||||
desc: 本message唯一id
|
||||
- name: final
|
||||
type: number
|
||||
desc: 该字段返回1时表示文本全部合成结束,客户端收到后需主动关闭websocket连接
|
||||
- name: result
|
||||
type: Array
|
||||
desc: 最新语音合成文本结果
|
||||
sub:
|
||||
- name: subtitles
|
||||
type: array
|
||||
desc: 当前一段话的词列表
|
||||
sub:
|
||||
- name: Text
|
||||
type: string
|
||||
desc: 该字的内容
|
||||
- name: BeginTime
|
||||
type: number
|
||||
desc: 该字在整个音频流中的起始时间
|
||||
- name: EndTime
|
||||
type: number
|
||||
desc: 该字在整个音频流中的结束时间
|
||||
- name: BeginIndex
|
||||
type: number
|
||||
desc: 该字在整个文本中的开始位置,从0开始
|
||||
- name: EndIndex
|
||||
type: number
|
||||
desc: 该字在整个文本中的结束位置,从0开始
|
||||
- name: Phoneme
|
||||
type: string
|
||||
desc: 该字的音素(注意:此字段可能返回null,表示取不到有效值)
|
||||
successBy: result.code == 0
|
||||
failedMessageBy: result.message
|
||||
200
tests/api/xunfei.yml
Normal file
200
tests/api/xunfei.yml
Normal file
@ -0,0 +1,200 @@
|
||||
config:
|
||||
endpoint: https://raasr.xfyun.cn/v2/api
|
||||
appId:
|
||||
secretKey:
|
||||
apiConfig:
|
||||
requestParams:
|
||||
- name: appId
|
||||
type: string
|
||||
required: true
|
||||
value: "{{config.appId}}"
|
||||
desc: 应用ID,从配置中获取
|
||||
- name: ts
|
||||
type: string
|
||||
required: true
|
||||
value: "{{Math.round(new Date().getTime()/1000)}}"
|
||||
desc: 时间戳,单位为秒
|
||||
- name: signa
|
||||
type: string
|
||||
required: true
|
||||
value: "{{util.base64(util.hmacSHA1(config.secretKey,util.hex(util.md5(config.appId+params.ts))))}}"
|
||||
desc: 时间戳,单位为秒
|
||||
api:
|
||||
- name: upload
|
||||
desc: 首先调用文件上传接口,上传待转写音频文件的基本信息(文件名、大小等)和相关的可配置参数。调用成功,返回订单ID(orderId,用于查询结果或者联调排查问题时使用)
|
||||
method: POST
|
||||
path: /upload
|
||||
timeout: 60000
|
||||
requestFormat: binary
|
||||
responseFormat: json
|
||||
requestParams:
|
||||
- name: fileName
|
||||
type: string
|
||||
required: true
|
||||
value:
|
||||
desc: 音频文件名称,最好携带音频真实的后缀名,避免影响转码
|
||||
- name: fileSize
|
||||
type: number
|
||||
required: true
|
||||
value:
|
||||
desc: 音频文件大小(字节数),当前只针对本地文件流方式校验,使用url外链方式不校验,可随机传一个数字。传递真实的音频文件大小,音频流模式服务端会根据这个参数和实际获取到的进行比较,不一致可能是文件丢失直接导致创建订单失败
|
||||
- name: duration
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 音频真实时长.当前未验证,可随机传一个数字
|
||||
- name: language
|
||||
type: string
|
||||
required: false
|
||||
value: cn
|
||||
desc: 语种类型:默认cn。cn:中文,通用方言(包括普通话、天津、河北、东北、甘肃、山东、太原);en:英文;ja:日语;ko:韩语;ru:俄语;fr:法语;es:西班牙语;vi:越南语;ar:阿拉伯语;cn_xinanese:西南官话(包括四川、重庆、云南、贵州);cn_cantonese:粤语;cn_henanese;河南;cn_uyghur:维吾尔语;cn_tibetan:藏语;ar:阿拉伯语;de:德语;it:意大利语
|
||||
- name: callbackUrl
|
||||
type: string
|
||||
required: false
|
||||
value:
|
||||
desc: "回调地址,订单完成时回调该地址通知完成支持get 请求,我们会在回调地址中拼接参数,长度限制512: http://{ip}/{port}?xxx&OrderId=xxxx&status=1,参数:orderId为订单号、status为订单状态: 1(转写识别成功) 、-1(转写识别失败)"
|
||||
- name: hotWord
|
||||
type: string
|
||||
required: false
|
||||
value:
|
||||
desc: 热词,用以提升专业词汇的识别率。格式:热词1|热词2|热词3。单个热词长度: [2,16] ,格式要求正则:[\uD800\uDC00-\uDBFF\uDFFF\uD800-\uDFFF],热词个数限制200个
|
||||
- name: candidate
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 多候选开关。0:关闭 (默认);1:打开
|
||||
- name: roleType
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 是否开启角色分离。0:不开启角色分离(默认);1:通用角色分离
|
||||
- name: roleNum
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 说话人数,取值范围0-10,默认为0进行盲分。注:该字段只有在开通了角色分离功能的前提下才会生效,正确传入该参数后角色分离效果会有所提升
|
||||
- name: pd
|
||||
type: string
|
||||
required: false
|
||||
value:
|
||||
desc: 领域个性化参数。court:法律;edu:教育;finance:金融;medical:医疗;tech:科技;culture:人文历史;isp:运营商;sport:体育;gov:政府;game:游戏;ecom:电商;mil:军事;com:企业;life:生活;ent:娱乐;car:汽车
|
||||
- name: audioMode
|
||||
type: string
|
||||
required: false
|
||||
value: fileStream
|
||||
desc: 转写音频上传方式。fileStream:文件流 (默认);urlLink:音频url外链
|
||||
- name: audioUrl
|
||||
type: string
|
||||
required: false
|
||||
value:
|
||||
desc: 音频url外链地址。当audioMode为urlLink时该值必传;如果url中包含特殊字符,audioUrl需要UrlEncode(不包含签名时需要的 UrlEncode),长度限制512
|
||||
- name: standardWav
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 是否标准pcm/wav(16k/16bit/单声道)。0:非标准 wav (默认);1:标准pcm/wav
|
||||
- name: languageType
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 语言识别模式选择,支持的语言识别模式选择如下:language为cn时:1:自动中英文模式 (默认);2:中文模式(可能包含少量英文);4:品中文模式(不包含英文)
|
||||
- name: trackMode
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 按声道分轨转写模式(支持语种:cn、en):1-不分轨模式 (默认);2-双声道分轨模式。备注:如果转写任务使用双声道分轨模式,角色分离(roleType)功能失效。转写结果字段请参考getResult接口协议定义字段
|
||||
- name: transLanguage
|
||||
type: string
|
||||
required: false
|
||||
value:
|
||||
desc: 需要翻译的语种(转写语种和翻译语种不能相同)。支持的语种请参考语种支持
|
||||
- name: transMode
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 翻译模式(默认2:按段落进行翻译,目前只支持按段落进行翻译),使用翻译能力时该字段生效。1:按VAD进行翻译;2:品段落进行翻译;3:按整篇进行翻译
|
||||
- name: eng_seg_max
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 控制分段的最大字数,取值范围[0-500],不传使用引擎默认值
|
||||
- name: eng_seg_min
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 控制分段的最小字数,取值范围[0-50],不传使用引擎默认值
|
||||
- name: eng_seg_weight
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 控制分段字数的权重,权重比越高,表示引擎分段逻辑采用字数控制分段的比重越高。取值(0-0.05)不传即不采用字数来控制分段,采用引擎默认分段逻辑
|
||||
- name: eng_smoothproc
|
||||
type: boolean
|
||||
required: false
|
||||
value:
|
||||
desc: 顺滑开关。true:表示开启 (默认);false:表示关闭
|
||||
- name: eng_colloqproc
|
||||
type: boolean
|
||||
required: false
|
||||
value:
|
||||
desc: 口语规整开关,口语规整是顺滑的升级版本。true:表示开启;false:表示关闭 (默认)
|
||||
- name: eng_vad_mdn
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 远近场模式。1:远场模式 (默认);2:近场模式
|
||||
- name: eng_vad_margin
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 首尾是否带静音信息。0:不显示;1:显示 (默认)
|
||||
- name: eng_rlang
|
||||
type: number
|
||||
required: false
|
||||
value:
|
||||
desc: 针对粤语转写后的字体转换。0:输出简体;1:输出繁体 (默认)
|
||||
requestHeaders:
|
||||
- name: Content-Type
|
||||
value: application/octet-stream
|
||||
desc:
|
||||
response:
|
||||
- name: code
|
||||
type: string
|
||||
desc: 响应状态码
|
||||
- name: descInfo
|
||||
type: string
|
||||
desc: 响应描述信息
|
||||
- name: content
|
||||
type: Object
|
||||
desc: 包含订单ID和任务预估时间等信息
|
||||
successBy: response.code == '000000'
|
||||
failedMessageBy: response.descInfo
|
||||
- name: getUploadResult
|
||||
desc: 根据实际应用场景该接口调用分两种场景:正常场景:转写订单按时完成会返回订单号和对应的状态,如果成功,集成方在接收到我们回调成功的请求后调用该接口查询转写结果。异常场景:即我们回调集成方超时或失败后(我们有重试几次的机制)或者服务订单有积压,按照转写耗时的评估表在订单创建成功后超过预计的转写耗时后可主动调用该接口查询结果,可以考虑采用分梯度间 隔时间查询(避免一直轮询导致其他正常订单结果的查询,因为我们对接口查询频率有限制)
|
||||
method: GET
|
||||
path: /getResult
|
||||
requestFormat: json
|
||||
responseFormat: json
|
||||
requestParams:
|
||||
- name: orderId
|
||||
type: string
|
||||
required: true
|
||||
value:
|
||||
desc: 非实时转写订单号
|
||||
- name: resultType
|
||||
type: string
|
||||
required: false
|
||||
value: transfer
|
||||
desc: 查询结果类型:默认返回转写结果。转写结果:transfer;翻译结果:translate;质检结果:predict;组合结果查询:多个类型结果使用”,”隔开,目前只支持转写和质检结果一起返回,不家支持转写和翻译结果一起返回(如果任务有失败则只返回处理成功的成果)
|
||||
response:
|
||||
- name: code
|
||||
type: string
|
||||
desc: 响应状态码
|
||||
- name: descInfo
|
||||
type: string
|
||||
desc: 响应描述信息
|
||||
- name: content
|
||||
type: Object
|
||||
desc: 包含订单信息、订单结果、任务预估时间等信息
|
||||
successBy: response.code == '000000'
|
||||
failedMessageBy: response.descInfo
|
||||
28
tests/apigo.yml
Normal file
28
tests/apigo.yml
Normal file
@ -0,0 +1,28 @@
|
||||
name:
|
||||
version: v0.0.1
|
||||
main: main.js
|
||||
target:
|
||||
darwin: amd64 arm64
|
||||
linux: amd64 arm64
|
||||
windows: amd64
|
||||
module:
|
||||
apigo.cc/gojs/console: latest
|
||||
apigo.cc/gojs/util: latest
|
||||
apigo.cc/gojs/log: latest
|
||||
apigo.cc/gojs/file: latest
|
||||
apigo.cc/gojs/http: latest
|
||||
apigo.cc/gojs/db: latest
|
||||
modulealias:
|
||||
extraimport:
|
||||
cachefile:
|
||||
sskey:
|
||||
cgoenable: false
|
||||
buildfrom:
|
||||
buildenv:
|
||||
linux:
|
||||
darwin:
|
||||
windows:
|
||||
buildldflags:
|
||||
linux:
|
||||
darwin:
|
||||
windows:
|
||||
43
tests/go.mod
Normal file
43
tests/go.mod
Normal file
@ -0,0 +1,43 @@
|
||||
module tests
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
apigo.cc/cloud v0.0.0
|
||||
apigo.cc/gojs v0.0.4
|
||||
apigo.cc/gojs/console v0.0.2
|
||||
apigo.cc/gojs/db v0.0.3
|
||||
apigo.cc/gojs/file v0.0.2
|
||||
apigo.cc/gojs/http v0.0.3
|
||||
apigo.cc/gojs/log v0.0.1
|
||||
apigo.cc/gojs/util v0.0.5
|
||||
github.com/ssgo/log v1.7.7
|
||||
github.com/ssgo/u v1.7.11
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/ZZMarquis/gm v1.3.2 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/emmansun/gmsm v0.29.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
|
||||
github.com/ssgo/config v1.7.9 // indirect
|
||||
github.com/ssgo/dao v0.1.5 // indirect
|
||||
github.com/ssgo/db v1.7.11 // indirect
|
||||
github.com/ssgo/httpclient v1.7.8 // indirect
|
||||
github.com/ssgo/standard v1.7.7 // indirect
|
||||
github.com/ssgo/tool v0.4.27 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace apigo.cc/cloud v0.0.0 => ../
|
||||
280
tests/main.go
Normal file
280
tests/main.go
Normal file
@ -0,0 +1,280 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
|
||||
_ "apigo.cc/cloud"
|
||||
_ "apigo.cc/gojs/console"
|
||||
_ "apigo.cc/gojs/db"
|
||||
_ "apigo.cc/gojs/file"
|
||||
_ "apigo.cc/gojs/http"
|
||||
_ "apigo.cc/gojs/log"
|
||||
_ "apigo.cc/gojs/util"
|
||||
|
||||
"github.com/ssgo/log"
|
||||
"github.com/ssgo/u"
|
||||
)
|
||||
|
||||
var _mainFile = "main.js"
|
||||
var appName = "tests"
|
||||
var appVersion = "v0.0.1"
|
||||
|
||||
func init() {
|
||||
gojs.Alias("cloud", "apigo.cc/cloud")
|
||||
gojs.Alias("console", "apigo.cc/gojs/console")
|
||||
gojs.Alias("db", "apigo.cc/gojs/db")
|
||||
gojs.Alias("file", "apigo.cc/gojs/file")
|
||||
gojs.Alias("http", "apigo.cc/gojs/http")
|
||||
gojs.Alias("log", "apigo.cc/gojs/log")
|
||||
gojs.Alias("util", "apigo.cc/gojs/util")
|
||||
}
|
||||
|
||||
var idFixMatcher = regexp.MustCompile(`[^a-zA-Z0-9_]`)
|
||||
var testCaseOnStartMatcher = regexp.MustCompile(`(?m)^\s*function\s+onStart\s*\(`)
|
||||
var testCaseOnStopMatcher = regexp.MustCompile(`(?m)^\s*function\s+onStop\s*\(`)
|
||||
var testCaseMatcher = regexp.MustCompile(`(?m)^\s*function\s+test_(\w+)\s*\(`)
|
||||
|
||||
func setSSKey(key, iv []byte) {
|
||||
gojs.SetSSKey(key, iv)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if appName == "" {
|
||||
appName = filepath.Base(os.Args[0])
|
||||
appName = strings.TrimSuffix(appName, ".exe")
|
||||
}
|
||||
|
||||
if appVersion == "" {
|
||||
appVersion = "0.0.1"
|
||||
}
|
||||
cmd := ""
|
||||
id := ""
|
||||
mainFile := _mainFile
|
||||
isWatch := false
|
||||
startArgs := make([]string, 0)
|
||||
mainArgs := make([]any, 0)
|
||||
for i := 1; i < len(os.Args); i++ {
|
||||
arg := os.Args[i]
|
||||
switch arg {
|
||||
case "--help", "-help", "-h":
|
||||
printUsage()
|
||||
return
|
||||
case "--version", "-version", "-v":
|
||||
fmt.Println(appName, appVersion)
|
||||
return
|
||||
case "--export", "-export":
|
||||
fmt.Println(u.Cyan(gojs.ExportForDev()))
|
||||
return
|
||||
case "start", "stop", "restart", "reload", "test":
|
||||
if len(mainArgs) == 0 && cmd == "" {
|
||||
cmd = arg
|
||||
} else {
|
||||
mainArgs = append(mainArgs, arg)
|
||||
}
|
||||
case "--env", "-env", "-e":
|
||||
i++
|
||||
a := strings.SplitN(os.Args[i], "=", 2)
|
||||
os.Setenv(a[0], a[1])
|
||||
case "-id":
|
||||
i++
|
||||
id = strings.TrimSpace(os.Args[i])
|
||||
case "-main":
|
||||
i++
|
||||
mainFile = strings.TrimSpace(os.Args[i])
|
||||
startArgs = append(startArgs, "-main", mainFile)
|
||||
case "-watch", "-w":
|
||||
isWatch = true
|
||||
startArgs = append(startArgs, arg)
|
||||
default:
|
||||
mainArgs = append(mainArgs, arg)
|
||||
startArgs = append(startArgs, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if cmd != "test" {
|
||||
if !u.FileExists(mainFile) {
|
||||
log.DefaultLogger.Error("main file not found", "mainFile", mainFile)
|
||||
return
|
||||
}
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("%s_%s_%s", appName, mainFile, appVersion)
|
||||
}
|
||||
id = idFixMatcher.ReplaceAllString(id, "_")
|
||||
}
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
pidFile := filepath.Join(homeDir, ".pids", id+".pid")
|
||||
pid := u.Int(u.ReadFileN(pidFile))
|
||||
switch cmd {
|
||||
case "start":
|
||||
if pid > 0 && processIsExists(pid) {
|
||||
log.DefaultLogger.Info("process already started", "pid", pid)
|
||||
} else {
|
||||
startProcess(pidFile, startArgs)
|
||||
}
|
||||
case "stop":
|
||||
if pid > 0 {
|
||||
killProcess(pid, mainFile)
|
||||
_ = os.Remove(pidFile)
|
||||
}
|
||||
case "reload":
|
||||
if pid > 0 {
|
||||
kill(pid, syscall.Signal(0x1e))
|
||||
}
|
||||
case "restart":
|
||||
if pid > 0 {
|
||||
killProcess(pid, mainFile)
|
||||
_ = os.Remove(pidFile)
|
||||
}
|
||||
startProcess(pidFile, startArgs)
|
||||
case "test":
|
||||
testPath := "."
|
||||
if u.FileExists("tests") {
|
||||
testPath = "tests"
|
||||
}
|
||||
if isWatch {
|
||||
var wr *gojs.WatchRunner
|
||||
wr, _ = gojs.Watch([]string{testPath}, []string{"js", "yml", "json"}, func(changes []string) {
|
||||
if len(changes) == 1 && strings.HasSuffix(changes[0], "_test.js") {
|
||||
runTest(changes[0], wr)
|
||||
} else {
|
||||
runAllTest(testPath, wr)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
runAllTest(testPath, nil)
|
||||
}
|
||||
gojs.WaitAll()
|
||||
default:
|
||||
defer os.Remove(pidFile)
|
||||
if isWatch {
|
||||
gojs.WatchRun(mainFile, mainArgs...)
|
||||
} else {
|
||||
if r, err := gojs.RunFile(mainFile, mainArgs...); err != nil {
|
||||
log.DefaultLogger.Error("run failed for "+mainFile, "err", err.Error())
|
||||
} else if r != nil {
|
||||
log.DefaultLogger.Error("run "+mainFile, "result", r)
|
||||
}
|
||||
}
|
||||
gojs.WaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
func runAllTest(testPath string, wr *gojs.WatchRunner) {
|
||||
for _, f := range u.ReadDirN(testPath) {
|
||||
if !f.IsDir && strings.HasSuffix(f.Name, "_test.js") {
|
||||
runTest(f.Name, wr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runTest(filename string, wr *gojs.WatchRunner) {
|
||||
vm := gojs.New()
|
||||
if wr != nil {
|
||||
wr.SetModuleLoader(vm)
|
||||
}
|
||||
if _, err := vm.RunFile(filename); err != nil {
|
||||
fmt.Println(u.BRed("run "+filename), u.Red(err.Error()))
|
||||
return
|
||||
}
|
||||
code := u.ReadFileN(filename)
|
||||
if testCaseOnStartMatcher.MatchString(code) {
|
||||
if _, err := vm.RunCode(`onStart()`); err != nil {
|
||||
fmt.Println(u.Red(filename), u.BRed("onStart"), u.Red(err.Error()))
|
||||
return
|
||||
} else {
|
||||
fmt.Println(u.Green(filename), u.BGreen("onStart"), u.Green("success"))
|
||||
}
|
||||
}
|
||||
for _, m := range testCaseMatcher.FindAllStringSubmatch(code, 1000) {
|
||||
testCaseName := m[1]
|
||||
if r, err := vm.RunCode(testCaseName + "()"); err != nil {
|
||||
fmt.Println(u.Red(filename), u.BRed(testCaseName), u.Red(err.Error()))
|
||||
} else if r == true {
|
||||
fmt.Println(u.Green(filename), u.BGreen(testCaseName), u.Green("pass"))
|
||||
} else {
|
||||
fmt.Println(u.Red(filename), u.BRed(testCaseName), u.Red(u.StringP(r)))
|
||||
}
|
||||
}
|
||||
if testCaseOnStopMatcher.MatchString(code) {
|
||||
if _, err := vm.RunCode(`onStop()`); err != nil {
|
||||
fmt.Println(u.Red(filename), u.BRed("onStop"), u.Red(err.Error()))
|
||||
} else {
|
||||
fmt.Println(u.Green(filename), u.BGreen("onStop"), u.Green("success"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startProcess(pidFile string, startArgs []string) {
|
||||
cmd := exec.Command(os.Args[0], startArgs...)
|
||||
// cmd.Stdout = os.Stdout
|
||||
// cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err == nil {
|
||||
u.WriteFile(pidFile, u.String(cmd.Process.Pid))
|
||||
log.DefaultLogger.Info("started", "appName", appName, "appVersion", appVersion, "startArgs", startArgs, "pid", cmd.Process.Pid)
|
||||
} else {
|
||||
log.DefaultLogger.Error("start failed", "appName", appName, "appVersion", appVersion, "startArgs", startArgs, "err", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func kill(pid int, sig os.Signal) error {
|
||||
if proc, err := os.FindProcess(pid); err == nil {
|
||||
return proc.Signal(sig)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func killProcess(pid int, mainFile string) {
|
||||
if err := kill(pid, syscall.SIGTERM); err == nil {
|
||||
log.DefaultLogger.Info("killing", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid)
|
||||
t1 := time.Now().UnixMilli()
|
||||
for i := 0; i < 100; i++ {
|
||||
if !processIsExists(pid) {
|
||||
log.DefaultLogger.Info("killed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid, "usedTime", (time.Duration(time.Now().UnixMilli()-t1) * time.Millisecond).String())
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
err := kill(pid, syscall.SIGKILL)
|
||||
if processIsExists(pid) {
|
||||
log.DefaultLogger.Error("fource kill failed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid, "err", err)
|
||||
} else {
|
||||
log.DefaultLogger.Info("fource killed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processIsExists(pid int) bool {
|
||||
if proc, err := os.FindProcess(pid); err == nil {
|
||||
if err2 := proc.Signal(syscall.Signal(0)); err2 == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println(appName, appVersion)
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" ", appName, "[-main mainFile] ...", "run script")
|
||||
fmt.Println(" ", appName, "-version|-v", "show version")
|
||||
fmt.Println(" ", appName, "-export", "export for development")
|
||||
fmt.Println(" ", appName, "-env|-e", "set env")
|
||||
fmt.Println(" ", appName, "start [-id id] [-main mainFile]", "start server")
|
||||
fmt.Println(" ", appName, "stop [-id id]", "stop server")
|
||||
fmt.Println(" ", appName, "restart [-id id] [-main mainFile]", "restart server")
|
||||
fmt.Println(" ", appName, "reload [-id id] [-main mainFile]", "reload config")
|
||||
fmt.Println(" ", appName, "test", "test *_test.js")
|
||||
fmt.Println(" ", appName, "help", "- show help")
|
||||
}
|
||||
30
tests/main.js
Normal file
30
tests/main.js
Normal file
@ -0,0 +1,30 @@
|
||||
import co from 'console'
|
||||
import file from 'file'
|
||||
import util from 'util'
|
||||
import { tencent } from 'apigo.cc/cloud'
|
||||
|
||||
function main() {
|
||||
// let i = 0
|
||||
// let bufArr = []
|
||||
// let r = tencent.tts('小语,1+1=2 对吗?', 301029, (data, type) => {
|
||||
// co.info(i, type, data.length)
|
||||
// if (type == 'binary') {
|
||||
// i++
|
||||
// // file.write(i + '.mp3', new Uint8Array(data))
|
||||
// bufArr = bufArr.concat(data)
|
||||
// } else if (type == 'json') {
|
||||
// co.info(data)
|
||||
// if (data.final == 1) return true
|
||||
// } else {
|
||||
// co.warn(data)
|
||||
// }
|
||||
// })
|
||||
// file.write('out.mp3', new Uint8Array(bufArr))
|
||||
// util.shell('afplay', 'out.mp3')
|
||||
|
||||
let r = tencent.fastAsr('mp3', util.base64(file.readBytes('out.mp3')))
|
||||
|
||||
co.info(co.bGreen(r.success), co.bRed(r.failedMessage))
|
||||
co.info(r)
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user