feat: 算力引擎 Go 后端核心(new-api)

- 统一 OpenAI 兼容 /v1 中继 + 渠道/额度/令牌/流水
- 各领域包:relay 模型网关、model 数据层、controller 管理 API
This commit is contained in:
2026-08-23 22:38:46 +08:00
commit 28e53d90fc
836 changed files with 165403 additions and 0 deletions
+86
View File
@@ -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
}
+65
View File
@@ -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
+31
View File
@@ -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)
}
+134
View File
@@ -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
}
+52
View File
@@ -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()
}