初始化导入 new-api 源码

This commit is contained in:
OpenClaw Task Bot
2026-04-09 22:31:14 +08:00
commit 75bda2e845
994 changed files with 250292 additions and 0 deletions

21
types/channel_error.go Normal file
View File

@@ -0,0 +1,21 @@
package types
type ChannelError struct {
ChannelId int `json:"channel_id"`
ChannelType int `json:"channel_type"`
ChannelName string `json:"channel_name"`
IsMultiKey bool `json:"is_multi_key"`
AutoBan bool `json:"auto_ban"`
UsingKey string `json:"using_key"`
}
func NewChannelError(channelId int, channelType int, channelName string, isMultiKey bool, usingKey string, autoBan bool) *ChannelError {
return &ChannelError{
ChannelId: channelId,
ChannelType: channelType,
ChannelName: channelName,
IsMultiKey: isMultiKey,
AutoBan: autoBan,
UsingKey: usingKey,
}
}

411
types/error.go Normal file
View File

@@ -0,0 +1,411 @@
package types
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"github.com/QuantumNous/new-api/common"
)
type OpenAIError struct {
Message string `json:"message"`
Type string `json:"type"`
Param string `json:"param"`
Code any `json:"code"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
type ClaudeError struct {
Type string `json:"type,omitempty"`
Message string `json:"message,omitempty"`
}
type ErrorType string
const (
ErrorTypeNewAPIError ErrorType = "new_api_error"
ErrorTypeOpenAIError ErrorType = "openai_error"
ErrorTypeClaudeError ErrorType = "claude_error"
ErrorTypeMidjourneyError ErrorType = "midjourney_error"
ErrorTypeGeminiError ErrorType = "gemini_error"
ErrorTypeRerankError ErrorType = "rerank_error"
ErrorTypeUpstreamError ErrorType = "upstream_error"
)
type ErrorCode string
const (
ErrorCodeInvalidRequest ErrorCode = "invalid_request"
ErrorCodeSensitiveWordsDetected ErrorCode = "sensitive_words_detected"
ErrorCodeViolationFeeGrokCSAM ErrorCode = "violation_fee.grok.csam"
// new api error
ErrorCodeCountTokenFailed ErrorCode = "count_token_failed"
ErrorCodeModelPriceError ErrorCode = "model_price_error"
ErrorCodeInvalidApiType ErrorCode = "invalid_api_type"
ErrorCodeJsonMarshalFailed ErrorCode = "json_marshal_failed"
ErrorCodeDoRequestFailed ErrorCode = "do_request_failed"
ErrorCodeGetChannelFailed ErrorCode = "get_channel_failed"
ErrorCodeGenRelayInfoFailed ErrorCode = "gen_relay_info_failed"
// channel error
ErrorCodeChannelNoAvailableKey ErrorCode = "channel:no_available_key"
ErrorCodeChannelParamOverrideInvalid ErrorCode = "channel:param_override_invalid"
ErrorCodeChannelHeaderOverrideInvalid ErrorCode = "channel:header_override_invalid"
ErrorCodeChannelModelMappedError ErrorCode = "channel:model_mapped_error"
ErrorCodeChannelAwsClientError ErrorCode = "channel:aws_client_error"
ErrorCodeChannelInvalidKey ErrorCode = "channel:invalid_key"
ErrorCodeChannelResponseTimeExceeded ErrorCode = "channel:response_time_exceeded"
// client request error
ErrorCodeReadRequestBodyFailed ErrorCode = "read_request_body_failed"
ErrorCodeConvertRequestFailed ErrorCode = "convert_request_failed"
ErrorCodeAccessDenied ErrorCode = "access_denied"
// request error
ErrorCodeBadRequestBody ErrorCode = "bad_request_body"
// response error
ErrorCodeReadResponseBodyFailed ErrorCode = "read_response_body_failed"
ErrorCodeBadResponseStatusCode ErrorCode = "bad_response_status_code"
ErrorCodeBadResponse ErrorCode = "bad_response"
ErrorCodeBadResponseBody ErrorCode = "bad_response_body"
ErrorCodeEmptyResponse ErrorCode = "empty_response"
ErrorCodeAwsInvokeError ErrorCode = "aws_invoke_error"
ErrorCodeModelNotFound ErrorCode = "model_not_found"
ErrorCodePromptBlocked ErrorCode = "prompt_blocked"
// sql error
ErrorCodeQueryDataError ErrorCode = "query_data_error"
ErrorCodeUpdateDataError ErrorCode = "update_data_error"
// quota error
ErrorCodeInsufficientUserQuota ErrorCode = "insufficient_user_quota"
ErrorCodePreConsumeTokenQuotaFailed ErrorCode = "pre_consume_token_quota_failed"
)
type NewAPIError struct {
Err error
RelayError any
skipRetry bool
recordErrorLog *bool
errorType ErrorType
errorCode ErrorCode
StatusCode int
Metadata json.RawMessage
}
// Unwrap enables errors.Is / errors.As to work with NewAPIError by exposing the underlying error.
func (e *NewAPIError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
func (e *NewAPIError) GetErrorCode() ErrorCode {
if e == nil {
return ""
}
return e.errorCode
}
func (e *NewAPIError) GetErrorType() ErrorType {
if e == nil {
return ""
}
return e.errorType
}
func (e *NewAPIError) Error() string {
if e == nil {
return ""
}
if e.Err == nil {
// fallback message when underlying error is missing
return string(e.errorCode)
}
return e.Err.Error()
}
func (e *NewAPIError) ErrorWithStatusCode() string {
if e == nil {
return ""
}
msg := e.Error()
if e.StatusCode == 0 {
return msg
}
if msg == "" {
return fmt.Sprintf("status_code=%d", e.StatusCode)
}
return fmt.Sprintf("status_code=%d, %s", e.StatusCode, msg)
}
func (e *NewAPIError) MaskSensitiveError() string {
if e == nil {
return ""
}
if e.Err == nil {
return string(e.errorCode)
}
errStr := e.Err.Error()
if e.errorCode == ErrorCodeCountTokenFailed {
return errStr
}
return common.MaskSensitiveInfo(errStr)
}
func (e *NewAPIError) MaskSensitiveErrorWithStatusCode() string {
if e == nil {
return ""
}
msg := e.MaskSensitiveError()
if e.StatusCode == 0 {
return msg
}
if msg == "" {
return fmt.Sprintf("status_code=%d", e.StatusCode)
}
return fmt.Sprintf("status_code=%d, %s", e.StatusCode, msg)
}
func (e *NewAPIError) SetMessage(message string) {
e.Err = errors.New(message)
}
func (e *NewAPIError) ToOpenAIError() OpenAIError {
var result OpenAIError
switch e.errorType {
case ErrorTypeOpenAIError:
if openAIError, ok := e.RelayError.(OpenAIError); ok {
result = openAIError
}
case ErrorTypeClaudeError:
if claudeError, ok := e.RelayError.(ClaudeError); ok {
result = OpenAIError{
Message: e.Error(),
Type: claudeError.Type,
Param: "",
Code: e.errorCode,
}
}
default:
result = OpenAIError{
Message: e.Error(),
Type: string(e.errorType),
Param: "",
Code: e.errorCode,
}
}
if e.errorCode != ErrorCodeCountTokenFailed {
result.Message = common.MaskSensitiveInfo(result.Message)
}
if result.Message == "" {
result.Message = string(e.errorType)
}
return result
}
func (e *NewAPIError) ToClaudeError() ClaudeError {
var result ClaudeError
switch e.errorType {
case ErrorTypeOpenAIError:
if openAIError, ok := e.RelayError.(OpenAIError); ok {
result = ClaudeError{
Message: e.Error(),
Type: fmt.Sprintf("%v", openAIError.Code),
}
}
case ErrorTypeClaudeError:
if claudeError, ok := e.RelayError.(ClaudeError); ok {
result = claudeError
}
default:
result = ClaudeError{
Message: e.Error(),
Type: string(e.errorType),
}
}
if e.errorCode != ErrorCodeCountTokenFailed {
result.Message = common.MaskSensitiveInfo(result.Message)
}
if result.Message == "" {
result.Message = string(e.errorType)
}
return result
}
type NewAPIErrorOptions func(*NewAPIError)
func NewError(err error, errorCode ErrorCode, ops ...NewAPIErrorOptions) *NewAPIError {
var newErr *NewAPIError
// 保留深层传递的 new err
if errors.As(err, &newErr) {
for _, op := range ops {
op(newErr)
}
return newErr
}
e := &NewAPIError{
Err: err,
RelayError: nil,
errorType: ErrorTypeNewAPIError,
StatusCode: http.StatusInternalServerError,
errorCode: errorCode,
}
for _, op := range ops {
op(e)
}
return e
}
func NewOpenAIError(err error, errorCode ErrorCode, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
var newErr *NewAPIError
// 保留深层传递的 new err
if errors.As(err, &newErr) {
if newErr.RelayError == nil {
openaiError := OpenAIError{
Message: newErr.Error(),
Type: string(errorCode),
Code: errorCode,
}
newErr.RelayError = openaiError
}
for _, op := range ops {
op(newErr)
}
return newErr
}
openaiError := OpenAIError{
Message: err.Error(),
Type: string(errorCode),
Code: errorCode,
}
return WithOpenAIError(openaiError, statusCode, ops...)
}
func InitOpenAIError(errorCode ErrorCode, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
openaiError := OpenAIError{
Type: string(errorCode),
Code: errorCode,
}
return WithOpenAIError(openaiError, statusCode, ops...)
}
func NewErrorWithStatusCode(err error, errorCode ErrorCode, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
e := &NewAPIError{
Err: err,
RelayError: OpenAIError{
Message: err.Error(),
Type: string(errorCode),
},
errorType: ErrorTypeNewAPIError,
StatusCode: statusCode,
errorCode: errorCode,
}
for _, op := range ops {
op(e)
}
return e
}
func WithOpenAIError(openAIError OpenAIError, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
code, ok := openAIError.Code.(string)
if !ok {
if openAIError.Code != nil {
code = fmt.Sprintf("%v", openAIError.Code)
} else {
code = "unknown_error"
}
}
if openAIError.Type == "" {
openAIError.Type = "upstream_error"
}
e := &NewAPIError{
RelayError: openAIError,
errorType: ErrorTypeOpenAIError,
StatusCode: statusCode,
Err: errors.New(openAIError.Message),
errorCode: ErrorCode(code),
}
// OpenRouter
if len(openAIError.Metadata) > 0 {
openAIError.Message = fmt.Sprintf("%s (%s)", openAIError.Message, openAIError.Metadata)
e.Metadata = openAIError.Metadata
e.RelayError = openAIError
e.Err = errors.New(openAIError.Message)
}
for _, op := range ops {
op(e)
}
return e
}
func WithClaudeError(claudeError ClaudeError, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
if claudeError.Type == "" {
claudeError.Type = "upstream_error"
}
e := &NewAPIError{
RelayError: claudeError,
errorType: ErrorTypeClaudeError,
StatusCode: statusCode,
Err: errors.New(claudeError.Message),
errorCode: ErrorCode(claudeError.Type),
}
for _, op := range ops {
op(e)
}
return e
}
func IsChannelError(err *NewAPIError) bool {
if err == nil {
return false
}
return strings.HasPrefix(string(err.errorCode), "channel:")
}
func IsSkipRetryError(err *NewAPIError) bool {
if err == nil {
return false
}
return err.skipRetry
}
func ErrOptionWithSkipRetry() NewAPIErrorOptions {
return func(e *NewAPIError) {
e.skipRetry = true
}
}
func ErrOptionWithNoRecordErrorLog() NewAPIErrorOptions {
return func(e *NewAPIError) {
e.recordErrorLog = common.GetPointer(false)
}
}
func ErrOptionWithHideErrMsg(replaceStr string) NewAPIErrorOptions {
return func(e *NewAPIError) {
if common.DebugEnabled {
fmt.Printf("ErrOptionWithHideErrMsg: %s, origin error: %s", replaceStr, e.Err)
}
e.Err = errors.New(replaceStr)
}
}
func IsRecordErrorLog(e *NewAPIError) bool {
if e == nil {
return false
}
if e.recordErrorLog == nil {
// default to true if not set
return true
}
return *e.recordErrorLog
}

8
types/file_data.go Normal file
View File

@@ -0,0 +1,8 @@
package types
type LocalFileData struct {
MimeType string
Base64Data string
Url string
Size int64
}

231
types/file_source.go Normal file
View File

@@ -0,0 +1,231 @@
package types
import (
"fmt"
"image"
"os"
"sync"
)
// FileSourceType 文件来源类型
type FileSourceType string
const (
FileSourceTypeURL FileSourceType = "url" // URL 来源
FileSourceTypeBase64 FileSourceType = "base64" // Base64 内联数据
)
// FileSource 统一的文件来源抽象
// 支持 URL 和 base64 两种来源,提供懒加载和缓存机制
type FileSource struct {
Type FileSourceType `json:"type"` // 来源类型
URL string `json:"url,omitempty"` // URL当 Type 为 url 时)
Base64Data string `json:"base64_data,omitempty"` // Base64 数据(当 Type 为 base64 时)
MimeType string `json:"mime_type,omitempty"` // MIME 类型(可选,会自动检测)
// 内部缓存(不导出,不序列化)
cachedData *CachedFileData
cacheLoaded bool
registered bool // 是否已注册到清理列表
mu sync.Mutex // 保护加载过程
}
// Mu 获取内部锁
func (f *FileSource) Mu() *sync.Mutex {
return &f.mu
}
// CachedFileData 缓存的文件数据
// 支持内存缓存和磁盘缓存两种模式
type CachedFileData struct {
base64Data string // 内存中的 base64 数据(小文件)
MimeType string // MIME 类型
Size int64 // 文件大小(字节)
DiskSize int64 // 磁盘缓存实际占用大小(字节,通常是 base64 长度)
ImageConfig *image.Config // 图片配置(如果是图片)
ImageFormat string // 图片格式(如果是图片)
// 磁盘缓存相关
diskPath string // 磁盘缓存文件路径(大文件)
isDisk bool // 是否使用磁盘缓存
diskMu sync.Mutex // 磁盘操作锁(保护磁盘文件的读取和删除)
diskClosed bool // 是否已关闭/清理
statDecremented bool // 是否已扣减统计
// 统计回调,避免循环依赖
OnClose func(size int64)
}
// NewMemoryCachedData 创建内存缓存的数据
func NewMemoryCachedData(base64Data string, mimeType string, size int64) *CachedFileData {
return &CachedFileData{
base64Data: base64Data,
MimeType: mimeType,
Size: size,
isDisk: false,
}
}
// NewDiskCachedData 创建磁盘缓存的数据
func NewDiskCachedData(diskPath string, mimeType string, size int64) *CachedFileData {
return &CachedFileData{
diskPath: diskPath,
MimeType: mimeType,
Size: size,
isDisk: true,
}
}
// GetBase64Data 获取 base64 数据(自动处理内存/磁盘)
func (c *CachedFileData) GetBase64Data() (string, error) {
if !c.isDisk {
return c.base64Data, nil
}
c.diskMu.Lock()
defer c.diskMu.Unlock()
if c.diskClosed {
return "", fmt.Errorf("disk cache already closed")
}
// 从磁盘读取
data, err := os.ReadFile(c.diskPath)
if err != nil {
return "", fmt.Errorf("failed to read from disk cache: %w", err)
}
return string(data), nil
}
// SetBase64Data 设置 base64 数据(仅用于内存模式)
func (c *CachedFileData) SetBase64Data(data string) {
if !c.isDisk {
c.base64Data = data
}
}
// IsDisk 是否使用磁盘缓存
func (c *CachedFileData) IsDisk() bool {
return c.isDisk
}
// Close 关闭并清理资源
func (c *CachedFileData) Close() error {
if !c.isDisk {
c.base64Data = "" // 释放内存
return nil
}
c.diskMu.Lock()
defer c.diskMu.Unlock()
if c.diskClosed {
return nil
}
c.diskClosed = true
if c.diskPath != "" {
err := os.Remove(c.diskPath)
// 只有在删除成功且未扣减过统计时,才执行回调
if err == nil && !c.statDecremented && c.OnClose != nil {
c.OnClose(c.DiskSize)
c.statDecremented = true
}
return err
}
return nil
}
// NewURLFileSource 创建 URL 来源的 FileSource
func NewURLFileSource(url string) *FileSource {
return &FileSource{
Type: FileSourceTypeURL,
URL: url,
}
}
// NewBase64FileSource 创建 base64 来源的 FileSource
func NewBase64FileSource(base64Data string, mimeType string) *FileSource {
return &FileSource{
Type: FileSourceTypeBase64,
Base64Data: base64Data,
MimeType: mimeType,
}
}
// IsURL 判断是否是 URL 来源
func (f *FileSource) IsURL() bool {
return f.Type == FileSourceTypeURL
}
// IsBase64 判断是否是 base64 来源
func (f *FileSource) IsBase64() bool {
return f.Type == FileSourceTypeBase64
}
// GetIdentifier 获取文件标识符(用于日志和错误追踪)
func (f *FileSource) GetIdentifier() string {
if f.IsURL() {
if len(f.URL) > 100 {
return f.URL[:100] + "..."
}
return f.URL
}
if len(f.Base64Data) > 50 {
return "base64:" + f.Base64Data[:50] + "..."
}
return "base64:" + f.Base64Data
}
// GetRawData 获取原始数据URL 或完整的 base64 字符串)
func (f *FileSource) GetRawData() string {
if f.IsURL() {
return f.URL
}
return f.Base64Data
}
// SetCache 设置缓存数据
func (f *FileSource) SetCache(data *CachedFileData) {
f.cachedData = data
f.cacheLoaded = true
}
// IsRegistered 是否已注册到清理列表
func (f *FileSource) IsRegistered() bool {
return f.registered
}
// SetRegistered 设置注册状态
func (f *FileSource) SetRegistered(registered bool) {
f.registered = registered
}
// GetCache 获取缓存数据
func (f *FileSource) GetCache() *CachedFileData {
return f.cachedData
}
// HasCache 是否有缓存
func (f *FileSource) HasCache() bool {
return f.cacheLoaded && f.cachedData != nil
}
// ClearCache 清除缓存,释放内存和磁盘文件
func (f *FileSource) ClearCache() {
// 如果有缓存数据,先关闭它(会清理磁盘文件)
if f.cachedData != nil {
f.cachedData.Close()
}
f.cachedData = nil
f.cacheLoaded = false
}
// ClearRawData 清除原始数据,只保留必要的元信息
// 用于在处理完成后释放大文件的内存
func (f *FileSource) ClearRawData() {
// 保留 URL通常很短只清除大的 base64 数据
if f.IsBase64() && len(f.Base64Data) > 1024 {
f.Base64Data = ""
}
}

42
types/price_data.go Normal file
View File

@@ -0,0 +1,42 @@
package types
import "fmt"
type GroupRatioInfo struct {
GroupRatio float64
GroupSpecialRatio float64
HasSpecialRatio bool
}
type PriceData struct {
FreeModel bool
ModelPrice float64
ModelRatio float64
CompletionRatio float64
CacheRatio float64
CacheCreationRatio float64
CacheCreation5mRatio float64
CacheCreation1hRatio float64
ImageRatio float64
AudioRatio float64
AudioCompletionRatio float64
OtherRatios map[string]float64
UsePrice bool
Quota int // 按次计费的最终额度MJ / Task
QuotaToPreConsume int // 按量计费的预消耗额度
GroupRatioInfo GroupRatioInfo
}
func (p *PriceData) AddOtherRatio(key string, ratio float64) {
if p.OtherRatios == nil {
p.OtherRatios = make(map[string]float64)
}
if ratio <= 0 {
return
}
p.OtherRatios[key] = ratio
}
func (p *PriceData) ToSetting() string {
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, CacheCreation5mRatio: %f, CacheCreation1hRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.CacheCreation5mRatio, p.CacheCreation1hRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio)
}

19
types/relay_format.go Normal file
View File

@@ -0,0 +1,19 @@
package types
type RelayFormat string
const (
RelayFormatOpenAI RelayFormat = "openai"
RelayFormatClaude = "claude"
RelayFormatGemini = "gemini"
RelayFormatOpenAIResponses = "openai_responses"
RelayFormatOpenAIResponsesCompaction = "openai_responses_compaction"
RelayFormatOpenAIAudio = "openai_audio"
RelayFormatOpenAIImage = "openai_image"
RelayFormatOpenAIRealtime = "openai_realtime"
RelayFormatRerank = "rerank"
RelayFormatEmbedding = "embedding"
RelayFormatTask = "task"
RelayFormatMjProxy = "mj_proxy"
)

84
types/request_meta.go Normal file
View File

@@ -0,0 +1,84 @@
package types
type FileType string
const (
FileTypeImage FileType = "image" // Image file type
FileTypeAudio FileType = "audio" // Audio file type
FileTypeVideo FileType = "video" // Video file type
FileTypeFile FileType = "file" // Generic file type
)
type TokenType string
const (
TokenTypeTextNumber TokenType = "text_number" // Text or number tokens
TokenTypeTokenizer TokenType = "tokenizer" // Tokenizer tokens
TokenTypeImage TokenType = "image" // Image tokens
)
type TokenCountMeta struct {
TokenType TokenType `json:"token_type,omitempty"` // Type of tokens used in the request
CombineText string `json:"combine_text,omitempty"` // Combined text from all messages
ToolsCount int `json:"tools_count,omitempty"` // Number of tools used
NameCount int `json:"name_count,omitempty"` // Number of names in the request
MessagesCount int `json:"messages_count,omitempty"` // Number of messages in the request
Files []*FileMeta `json:"files,omitempty"` // List of files, each with type and content
MaxTokens int `json:"max_tokens,omitempty"` // Maximum tokens allowed in the request
ImagePriceRatio float64 `json:"image_ratio,omitempty"` // Ratio for image size, if applicable
//IsStreaming bool `json:"is_streaming,omitempty"` // Indicates if the request is streaming
}
type FileMeta struct {
FileType
MimeType string
Source *FileSource // 统一的文件来源URL 或 base64
Detail string // 图片细节级别low/high/auto
}
// NewFileMeta 创建新的 FileMeta
func NewFileMeta(fileType FileType, source *FileSource) *FileMeta {
return &FileMeta{
FileType: fileType,
Source: source,
}
}
// NewImageFileMeta 创建图片类型的 FileMeta
func NewImageFileMeta(source *FileSource, detail string) *FileMeta {
return &FileMeta{
FileType: FileTypeImage,
Source: source,
Detail: detail,
}
}
// GetIdentifier 获取文件标识符(用于日志)
func (f *FileMeta) GetIdentifier() string {
if f.Source != nil {
return f.Source.GetIdentifier()
}
return "unknown"
}
// IsURL 判断是否是 URL 来源
func (f *FileMeta) IsURL() bool {
return f.Source != nil && f.Source.IsURL()
}
// GetRawData 获取原始数据(兼容旧代码)
// Deprecated: 请使用 Source.GetRawData()
func (f *FileMeta) GetRawData() string {
if f.Source != nil {
return f.Source.GetRawData()
}
return ""
}
type RequestMeta struct {
OriginalModelName string `json:"original_model_name"`
UserUsingGroup string `json:"user_using_group"`
PromptTokens int `json:"prompt_tokens"`
PreConsumedQuota int `json:"pre_consumed_quota"`
}

103
types/rw_map.go Normal file
View File

@@ -0,0 +1,103 @@
package types
import (
"sync"
"github.com/QuantumNous/new-api/common"
)
type RWMap[K comparable, V any] struct {
data map[K]V
mutex sync.RWMutex
}
func (m *RWMap[K, V]) UnmarshalJSON(b []byte) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.data = make(map[K]V)
return common.Unmarshal(b, &m.data)
}
func (m *RWMap[K, V]) MarshalJSON() ([]byte, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()
return common.Marshal(m.data)
}
func NewRWMap[K comparable, V any]() *RWMap[K, V] {
return &RWMap[K, V]{
data: make(map[K]V),
}
}
func (m *RWMap[K, V]) Get(key K) (V, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
value, exists := m.data[key]
return value, exists
}
func (m *RWMap[K, V]) Set(key K, value V) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.data[key] = value
}
func (m *RWMap[K, V]) AddAll(other map[K]V) {
m.mutex.Lock()
defer m.mutex.Unlock()
for k, v := range other {
m.data[k] = v
}
}
func (m *RWMap[K, V]) Clear() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.data = make(map[K]V)
}
// ReadAll returns a copy of the entire map.
func (m *RWMap[K, V]) ReadAll() map[K]V {
m.mutex.RLock()
defer m.mutex.RUnlock()
copiedMap := make(map[K]V)
for k, v := range m.data {
copiedMap[k] = v
}
return copiedMap
}
func (m *RWMap[K, V]) Len() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.data)
}
func LoadFromJsonString[K comparable, V any](m *RWMap[K, V], jsonStr string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.data = make(map[K]V)
return common.Unmarshal([]byte(jsonStr), &m.data)
}
// LoadFromJsonStringWithCallback loads a JSON string into the RWMap and calls the callback on success.
func LoadFromJsonStringWithCallback[K comparable, V any](m *RWMap[K, V], jsonStr string, onSuccess func()) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.data = make(map[K]V)
err := common.Unmarshal([]byte(jsonStr), &m.data)
if err == nil && onSuccess != nil {
onSuccess()
}
return err
}
// MarshalJSONString returns the JSON string representation of the RWMap.
func (m *RWMap[K, V]) MarshalJSONString() string {
bytes, err := m.MarshalJSON()
if err != nil {
return "{}"
}
return string(bytes)
}

42
types/set.go Normal file
View File

@@ -0,0 +1,42 @@
package types
type Set[T comparable] struct {
items map[T]struct{}
}
// NewSet 创建并返回一个新的 Set
func NewSet[T comparable]() *Set[T] {
return &Set[T]{
items: make(map[T]struct{}),
}
}
func (s *Set[T]) Add(item T) {
s.items[item] = struct{}{}
}
// Remove 从 Set 中移除一个元素
func (s *Set[T]) Remove(item T) {
delete(s.items, item)
}
// Contains 检查 Set 是否包含某个元素
func (s *Set[T]) Contains(item T) bool {
_, exists := s.items[item]
return exists
}
// Len 返回 Set 中元素的数量
func (s *Set[T]) Len() int {
return len(s.items)
}
// Items 返回 Set 中所有元素组成的切片
// 注意:由于 map 的无序性,返回的切片元素顺序是随机的
func (s *Set[T]) Items() []T {
items := make([]T, 0, s.Len())
for item := range s.items {
items = append(items, item)
}
return items
}