feat: 算力引擎 Go 后端核心(new-api)
- 统一 OpenAI 兼容 /v1 中继 + 渠道/额度/令牌/流水 - 各领域包:relay 模型网关、model 数据层、controller 管理 API
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// Package kitutil holds the dependency-free helpers shared by the conversion
|
||||
// kit packages (dto, types, relayconvert). It moved out of the host's common
|
||||
// package as part of the relaykit extraction; common re-exports these for
|
||||
// host code.
|
||||
package kitutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func UnmarshalJsonStr(data string, v any) error {
|
||||
return json.Unmarshal(StringToByteSlice(data), v)
|
||||
}
|
||||
|
||||
func DecodeJson(reader io.Reader, v any) error {
|
||||
return json.NewDecoder(reader).Decode(v)
|
||||
}
|
||||
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func GetJsonType(data json.RawMessage) string {
|
||||
trimmed := bytes.TrimSpace(data)
|
||||
if len(trimmed) == 0 {
|
||||
return "unknown"
|
||||
}
|
||||
firstChar := trimmed[0]
|
||||
switch firstChar {
|
||||
case '{':
|
||||
return "object"
|
||||
case '[':
|
||||
return "array"
|
||||
case '"':
|
||||
return "string"
|
||||
case 't', 'f':
|
||||
return "boolean"
|
||||
case 'n':
|
||||
return "null"
|
||||
default:
|
||||
return "number"
|
||||
}
|
||||
}
|
||||
|
||||
// JsonRawMessageToString returns JSON strings as their decoded value and other JSON values as raw text.
|
||||
func JsonRawMessageToString(data json.RawMessage) string {
|
||||
trimmed := bytes.TrimSpace(data)
|
||||
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
||||
return ""
|
||||
}
|
||||
if trimmed[0] != '"' {
|
||||
return string(trimmed)
|
||||
}
|
||||
var value string
|
||||
if err := Unmarshal(trimmed, &value); err != nil {
|
||||
return string(trimmed)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func StringToByteSlice(s string) []byte {
|
||||
tmp1 := (*[2]uintptr)(unsafe.Pointer(&s))
|
||||
tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]}
|
||||
return *(*[]byte)(unsafe.Pointer(&tmp2))
|
||||
}
|
||||
|
||||
func Any2Type[T any](data any) (T, error) {
|
||||
var zero T
|
||||
bytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
var res T
|
||||
err = json.Unmarshal(bytes, &res)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package kitutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Kit packages log rare data-shape anomalies through these hooks. The host
|
||||
// redirects them into its logging system at startup; standalone relaykit users
|
||||
// get stderr defaults.
|
||||
|
||||
type LogFunc func(message string)
|
||||
|
||||
var (
|
||||
logInfo atomic.Pointer[LogFunc]
|
||||
logError atomic.Pointer[LogFunc]
|
||||
logSystemError atomic.Pointer[LogFunc]
|
||||
)
|
||||
|
||||
func SetLogging(info LogFunc, errorFn LogFunc) {
|
||||
if info != nil {
|
||||
logInfo.Store(&info)
|
||||
}
|
||||
if errorFn != nil {
|
||||
logError.Store(&errorFn)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSystemErrorLogging configures the hook for internal converter failures.
|
||||
func SetSystemErrorLogging(errorFn LogFunc) {
|
||||
if errorFn != nil {
|
||||
logSystemError.Store(&errorFn)
|
||||
}
|
||||
}
|
||||
|
||||
func LogInfo(message string) {
|
||||
if fn := logInfo.Load(); fn != nil {
|
||||
(*fn)(message)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[relaykit] %s\n", message)
|
||||
}
|
||||
|
||||
func LogError(message string) {
|
||||
if fn := logError.Load(); fn != nil {
|
||||
(*fn)(message)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[relaykit] ERROR %s\n", message)
|
||||
}
|
||||
|
||||
// LogSystemError reports an internal converter failure through its dedicated
|
||||
// hook, keeping it distinct from malformed request-data diagnostics.
|
||||
func LogSystemError(message string) {
|
||||
if fn := logSystemError.Load(); fn != nil {
|
||||
(*fn)(message)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[relaykit] SYSTEM ERROR %s\n", message)
|
||||
}
|
||||
|
||||
// Debug reports whether verbose kit diagnostics are enabled. The host sets
|
||||
// this once at startup (new-api mirrors common.DebugEnabled into it).
|
||||
var Debug atomic.Bool
|
||||
@@ -0,0 +1,31 @@
|
||||
package kitutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrorHooksRemainDistinct(t *testing.T) {
|
||||
previousError := logError.Load()
|
||||
previousSystemError := logSystemError.Load()
|
||||
t.Cleanup(func() {
|
||||
logError.Store(previousError)
|
||||
logSystemError.Store(previousSystemError)
|
||||
})
|
||||
|
||||
var ordinaryMessages []string
|
||||
var systemMessages []string
|
||||
SetLogging(nil, func(message string) {
|
||||
ordinaryMessages = append(ordinaryMessages, message)
|
||||
})
|
||||
SetSystemErrorLogging(func(message string) {
|
||||
systemMessages = append(systemMessages, message)
|
||||
})
|
||||
|
||||
LogError("invalid dto")
|
||||
LogSystemError("converter failure")
|
||||
|
||||
assert.Equal(t, []string{"invalid dto"}, ordinaryMessages)
|
||||
assert.Equal(t, []string{"converter failure"}, systemMessages)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package kitutil
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
maskURLPattern = regexp.MustCompile(`(http|https)://[^\s/$.?#].[^\s]*`)
|
||||
maskDomainPattern = regexp.MustCompile(`\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b`)
|
||||
maskIPPattern = regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`)
|
||||
// maskApiKeyPattern matches patterns like 'api_key:xxx' or "api_key:xxx" to mask the API key value
|
||||
maskApiKeyPattern = regexp.MustCompile(`(['"]?)api_key:([^\s'"]+)(['"]?)`)
|
||||
)
|
||||
|
||||
// maskHostTail returns the tail parts of a domain/host that should be preserved.
|
||||
// It keeps 2 parts for likely country-code TLDs (e.g., co.uk, com.cn), otherwise keeps only the TLD.
|
||||
func maskHostTail(parts []string) []string {
|
||||
if len(parts) < 2 {
|
||||
return parts
|
||||
}
|
||||
lastPart := parts[len(parts)-1]
|
||||
secondLastPart := parts[len(parts)-2]
|
||||
if len(lastPart) == 2 && len(secondLastPart) <= 3 {
|
||||
// Likely country code TLD like co.uk, com.cn
|
||||
return []string{secondLastPart, lastPart}
|
||||
}
|
||||
return []string{lastPart}
|
||||
}
|
||||
|
||||
// maskHostForURL collapses subdomains and keeps only masked prefix + preserved tail.
|
||||
// Example: api.openai.com -> ***.com, sub.domain.co.uk -> ***.co.uk
|
||||
func maskHostForURL(host string) string {
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) < 2 {
|
||||
return "***"
|
||||
}
|
||||
tail := maskHostTail(parts)
|
||||
return "***." + strings.Join(tail, ".")
|
||||
}
|
||||
|
||||
// maskHostForPlainDomain masks a plain domain and reflects subdomain depth with multiple ***.
|
||||
// Example: openai.com -> ***.com, api.openai.com -> ***.***.com, sub.domain.co.uk -> ***.***.co.uk
|
||||
func maskHostForPlainDomain(domain string) string {
|
||||
parts := strings.Split(domain, ".")
|
||||
if len(parts) < 2 {
|
||||
return domain
|
||||
}
|
||||
tail := maskHostTail(parts)
|
||||
numStars := len(parts) - len(tail)
|
||||
if numStars < 1 {
|
||||
numStars = 1
|
||||
}
|
||||
stars := strings.TrimSuffix(strings.Repeat("***.", numStars), ".")
|
||||
return stars + "." + strings.Join(tail, ".")
|
||||
}
|
||||
|
||||
// MaskSensitiveInfo masks sensitive information like URLs, IPs, and domain names in a string
|
||||
// Example:
|
||||
// http://example.com -> http://***.com
|
||||
// https://api.test.org/v1/users/123?key=secret -> https://***.org/***/***/?key=***
|
||||
// https://sub.domain.co.uk/path/to/resource -> https://***.co.uk/***/***
|
||||
// 192.168.1.1 -> ***.***.***.***
|
||||
// openai.com -> ***.com
|
||||
// www.openai.com -> ***.***.com
|
||||
// api.openai.com -> ***.***.com
|
||||
func MaskSensitiveInfo(str string) string {
|
||||
// Mask URLs
|
||||
str = maskURLPattern.ReplaceAllStringFunc(str, func(urlStr string) string {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return urlStr
|
||||
}
|
||||
|
||||
host := u.Host
|
||||
if host == "" {
|
||||
return urlStr
|
||||
}
|
||||
|
||||
// Mask host with unified logic
|
||||
maskedHost := maskHostForURL(host)
|
||||
|
||||
result := u.Scheme + "://" + maskedHost
|
||||
|
||||
// Mask path
|
||||
if u.Path != "" && u.Path != "/" {
|
||||
pathParts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
maskedPathParts := make([]string, len(pathParts))
|
||||
for i := range pathParts {
|
||||
if pathParts[i] != "" {
|
||||
maskedPathParts[i] = "***"
|
||||
}
|
||||
}
|
||||
if len(maskedPathParts) > 0 {
|
||||
result += "/" + strings.Join(maskedPathParts, "/")
|
||||
}
|
||||
} else if u.Path == "/" {
|
||||
result += "/"
|
||||
}
|
||||
|
||||
// Mask query parameters
|
||||
if u.RawQuery != "" {
|
||||
values, err := url.ParseQuery(u.RawQuery)
|
||||
if err != nil {
|
||||
// If can't parse query, just mask the whole query string
|
||||
result += "?***"
|
||||
} else {
|
||||
maskedParams := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
maskedParams = append(maskedParams, key+"=***")
|
||||
}
|
||||
if len(maskedParams) > 0 {
|
||||
result += "?" + strings.Join(maskedParams, "&")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// Mask domain names without protocol (like openai.com, www.openai.com)
|
||||
str = maskDomainPattern.ReplaceAllStringFunc(str, func(domain string) string {
|
||||
return maskHostForPlainDomain(domain)
|
||||
})
|
||||
|
||||
// Mask IP addresses
|
||||
str = maskIPPattern.ReplaceAllString(str, "***.***.***.***")
|
||||
|
||||
// Mask API keys (e.g., "api_key:AIzaSyAAAaUooTUni8AdaOkSRMda30n_Q4vrV70" -> "api_key:***")
|
||||
str = maskApiKeyPattern.ReplaceAllString(str, "${1}api_key:***${3}")
|
||||
|
||||
return str
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package kitutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func GetPointer[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func Interface2String(inter interface{}) string {
|
||||
switch inter.(type) {
|
||||
case string:
|
||||
return inter.(string)
|
||||
case int:
|
||||
return fmt.Sprintf("%d", inter.(int))
|
||||
case float64:
|
||||
return strconv.FormatFloat(inter.(float64), 'f', -1, 64)
|
||||
case bool:
|
||||
if inter.(bool) {
|
||||
return "true"
|
||||
} else {
|
||||
return "false"
|
||||
}
|
||||
case nil:
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%v", inter)
|
||||
}
|
||||
|
||||
func String2Int(str string) int {
|
||||
num, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
func GetUUID() string {
|
||||
code := uuid.New().String()
|
||||
code = strings.Replace(code, "-", "", -1)
|
||||
return code
|
||||
}
|
||||
|
||||
func GetTimestamp() int64 {
|
||||
return time.Now().Unix()
|
||||
}
|
||||
Reference in New Issue
Block a user