Commit 13262a56 authored by yangjianbo's avatar yangjianbo
Browse files

feat(sora): 新增 Sora 平台支持并修复高危安全和性能问题



新增功能:
- 新增 Sora 账号管理和 OAuth 认证
- 新增 Sora 视频/图片生成 API 网关
- 新增 Sora 任务调度和缓存机制
- 新增 Sora 使用统计和计费支持
- 前端增加 Sora 平台配置界面

安全修复(代码审核):
- [SEC-001] 限制媒体下载响应体大小(图片 20MB、视频 200MB),防止 DoS 攻击
- [SEC-002] 限制 SDK API 响应大小(1MB),防止内存耗尽
- [SEC-003] 修复 SSRF 风险,添加 URL 验证并强制使用代理配置

BUG 修复(代码审核):
- [BUG-001] 修复 for 循环内 defer 累积导致的资源泄漏
- [BUG-002] 修复图片并发槽位获取失败时已持有锁未释放的永久泄漏

性能优化(代码审核):
- [PERF-001] 添加 Sentinel Token 缓存(3 分钟有效期),减少 PoW 计算开销

技术细节:
- 使用 io.LimitReader 限制所有外部输入的大小
- 添加 urlvalidator 验证防止 SSRF 攻击
- 使用 sync.Map 实现线程安全的包级缓存
- 优化并发槽位管理,添加 releaseAll 模式防止泄漏

影响范围:
- 后端:新增 Sora 相关数据模型、服务、网关和管理接口
- 前端:新增 Sora 平台配置、账号管理和监控界面
- 配置:新增 Sora 相关配置项和环境变量
Co-Authored-By: default avatarClaude Sonnet 4.5 <noreply@anthropic.com>
parent bece1b52
This diff is collapsed.
// Code generated by ent, DO NOT EDIT.
package ent
import (
"fmt"
"strings"
"time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"github.com/Wei-Shaw/sub2api/ent/soracachefile"
)
// SoraCacheFile is the model entity for the SoraCacheFile schema.
type SoraCacheFile struct {
config `json:"-"`
// ID of the ent.
ID int64 `json:"id,omitempty"`
// TaskID holds the value of the "task_id" field.
TaskID *string `json:"task_id,omitempty"`
// AccountID holds the value of the "account_id" field.
AccountID int64 `json:"account_id,omitempty"`
// UserID holds the value of the "user_id" field.
UserID int64 `json:"user_id,omitempty"`
// MediaType holds the value of the "media_type" field.
MediaType string `json:"media_type,omitempty"`
// OriginalURL holds the value of the "original_url" field.
OriginalURL string `json:"original_url,omitempty"`
// CachePath holds the value of the "cache_path" field.
CachePath string `json:"cache_path,omitempty"`
// CacheURL holds the value of the "cache_url" field.
CacheURL string `json:"cache_url,omitempty"`
// SizeBytes holds the value of the "size_bytes" field.
SizeBytes int64 `json:"size_bytes,omitempty"`
// CreatedAt holds the value of the "created_at" field.
CreatedAt time.Time `json:"created_at,omitempty"`
selectValues sql.SelectValues
}
// scanValues returns the types for scanning values from sql.Rows.
func (*SoraCacheFile) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case soracachefile.FieldID, soracachefile.FieldAccountID, soracachefile.FieldUserID, soracachefile.FieldSizeBytes:
values[i] = new(sql.NullInt64)
case soracachefile.FieldTaskID, soracachefile.FieldMediaType, soracachefile.FieldOriginalURL, soracachefile.FieldCachePath, soracachefile.FieldCacheURL:
values[i] = new(sql.NullString)
case soracachefile.FieldCreatedAt:
values[i] = new(sql.NullTime)
default:
values[i] = new(sql.UnknownType)
}
}
return values, nil
}
// assignValues assigns the values that were returned from sql.Rows (after scanning)
// to the SoraCacheFile fields.
func (_m *SoraCacheFile) assignValues(columns []string, values []any) error {
if m, n := len(values), len(columns); m < n {
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
}
for i := range columns {
switch columns[i] {
case soracachefile.FieldID:
value, ok := values[i].(*sql.NullInt64)
if !ok {
return fmt.Errorf("unexpected type %T for field id", value)
}
_m.ID = int64(value.Int64)
case soracachefile.FieldTaskID:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field task_id", values[i])
} else if value.Valid {
_m.TaskID = new(string)
*_m.TaskID = value.String
}
case soracachefile.FieldAccountID:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field account_id", values[i])
} else if value.Valid {
_m.AccountID = value.Int64
}
case soracachefile.FieldUserID:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field user_id", values[i])
} else if value.Valid {
_m.UserID = value.Int64
}
case soracachefile.FieldMediaType:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field media_type", values[i])
} else if value.Valid {
_m.MediaType = value.String
}
case soracachefile.FieldOriginalURL:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field original_url", values[i])
} else if value.Valid {
_m.OriginalURL = value.String
}
case soracachefile.FieldCachePath:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field cache_path", values[i])
} else if value.Valid {
_m.CachePath = value.String
}
case soracachefile.FieldCacheURL:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field cache_url", values[i])
} else if value.Valid {
_m.CacheURL = value.String
}
case soracachefile.FieldSizeBytes:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field size_bytes", values[i])
} else if value.Valid {
_m.SizeBytes = value.Int64
}
case soracachefile.FieldCreatedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field created_at", values[i])
} else if value.Valid {
_m.CreatedAt = value.Time
}
default:
_m.selectValues.Set(columns[i], values[i])
}
}
return nil
}
// Value returns the ent.Value that was dynamically selected and assigned to the SoraCacheFile.
// This includes values selected through modifiers, order, etc.
func (_m *SoraCacheFile) Value(name string) (ent.Value, error) {
return _m.selectValues.Get(name)
}
// Update returns a builder for updating this SoraCacheFile.
// Note that you need to call SoraCacheFile.Unwrap() before calling this method if this SoraCacheFile
// was returned from a transaction, and the transaction was committed or rolled back.
func (_m *SoraCacheFile) Update() *SoraCacheFileUpdateOne {
return NewSoraCacheFileClient(_m.config).UpdateOne(_m)
}
// Unwrap unwraps the SoraCacheFile entity that was returned from a transaction after it was closed,
// so that all future queries will be executed through the driver which created the transaction.
func (_m *SoraCacheFile) Unwrap() *SoraCacheFile {
_tx, ok := _m.config.driver.(*txDriver)
if !ok {
panic("ent: SoraCacheFile is not a transactional entity")
}
_m.config.driver = _tx.drv
return _m
}
// String implements the fmt.Stringer.
func (_m *SoraCacheFile) String() string {
var builder strings.Builder
builder.WriteString("SoraCacheFile(")
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
if v := _m.TaskID; v != nil {
builder.WriteString("task_id=")
builder.WriteString(*v)
}
builder.WriteString(", ")
builder.WriteString("account_id=")
builder.WriteString(fmt.Sprintf("%v", _m.AccountID))
builder.WriteString(", ")
builder.WriteString("user_id=")
builder.WriteString(fmt.Sprintf("%v", _m.UserID))
builder.WriteString(", ")
builder.WriteString("media_type=")
builder.WriteString(_m.MediaType)
builder.WriteString(", ")
builder.WriteString("original_url=")
builder.WriteString(_m.OriginalURL)
builder.WriteString(", ")
builder.WriteString("cache_path=")
builder.WriteString(_m.CachePath)
builder.WriteString(", ")
builder.WriteString("cache_url=")
builder.WriteString(_m.CacheURL)
builder.WriteString(", ")
builder.WriteString("size_bytes=")
builder.WriteString(fmt.Sprintf("%v", _m.SizeBytes))
builder.WriteString(", ")
builder.WriteString("created_at=")
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
builder.WriteByte(')')
return builder.String()
}
// SoraCacheFiles is a parsable slice of SoraCacheFile.
type SoraCacheFiles []*SoraCacheFile
// Code generated by ent, DO NOT EDIT.
package soracachefile
import (
"time"
"entgo.io/ent/dialect/sql"
)
const (
// Label holds the string label denoting the soracachefile type in the database.
Label = "sora_cache_file"
// FieldID holds the string denoting the id field in the database.
FieldID = "id"
// FieldTaskID holds the string denoting the task_id field in the database.
FieldTaskID = "task_id"
// FieldAccountID holds the string denoting the account_id field in the database.
FieldAccountID = "account_id"
// FieldUserID holds the string denoting the user_id field in the database.
FieldUserID = "user_id"
// FieldMediaType holds the string denoting the media_type field in the database.
FieldMediaType = "media_type"
// FieldOriginalURL holds the string denoting the original_url field in the database.
FieldOriginalURL = "original_url"
// FieldCachePath holds the string denoting the cache_path field in the database.
FieldCachePath = "cache_path"
// FieldCacheURL holds the string denoting the cache_url field in the database.
FieldCacheURL = "cache_url"
// FieldSizeBytes holds the string denoting the size_bytes field in the database.
FieldSizeBytes = "size_bytes"
// FieldCreatedAt holds the string denoting the created_at field in the database.
FieldCreatedAt = "created_at"
// Table holds the table name of the soracachefile in the database.
Table = "sora_cache_files"
)
// Columns holds all SQL columns for soracachefile fields.
var Columns = []string{
FieldID,
FieldTaskID,
FieldAccountID,
FieldUserID,
FieldMediaType,
FieldOriginalURL,
FieldCachePath,
FieldCacheURL,
FieldSizeBytes,
FieldCreatedAt,
}
// ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool {
for i := range Columns {
if column == Columns[i] {
return true
}
}
return false
}
var (
// TaskIDValidator is a validator for the "task_id" field. It is called by the builders before save.
TaskIDValidator func(string) error
// MediaTypeValidator is a validator for the "media_type" field. It is called by the builders before save.
MediaTypeValidator func(string) error
// DefaultSizeBytes holds the default value on creation for the "size_bytes" field.
DefaultSizeBytes int64
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
DefaultCreatedAt func() time.Time
)
// OrderOption defines the ordering options for the SoraCacheFile queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByTaskID orders the results by the task_id field.
func ByTaskID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldTaskID, opts...).ToFunc()
}
// ByAccountID orders the results by the account_id field.
func ByAccountID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAccountID, opts...).ToFunc()
}
// ByUserID orders the results by the user_id field.
func ByUserID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUserID, opts...).ToFunc()
}
// ByMediaType orders the results by the media_type field.
func ByMediaType(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldMediaType, opts...).ToFunc()
}
// ByOriginalURL orders the results by the original_url field.
func ByOriginalURL(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldOriginalURL, opts...).ToFunc()
}
// ByCachePath orders the results by the cache_path field.
func ByCachePath(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCachePath, opts...).ToFunc()
}
// ByCacheURL orders the results by the cache_url field.
func ByCacheURL(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCacheURL, opts...).ToFunc()
}
// BySizeBytes orders the results by the size_bytes field.
func BySizeBytes(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldSizeBytes, opts...).ToFunc()
}
// ByCreatedAt orders the results by the created_at field.
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
}
This diff is collapsed.
This diff is collapsed.
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/soracachefile"
)
// SoraCacheFileDelete is the builder for deleting a SoraCacheFile entity.
type SoraCacheFileDelete struct {
config
hooks []Hook
mutation *SoraCacheFileMutation
}
// Where appends a list predicates to the SoraCacheFileDelete builder.
func (_d *SoraCacheFileDelete) Where(ps ...predicate.SoraCacheFile) *SoraCacheFileDelete {
_d.mutation.Where(ps...)
return _d
}
// Exec executes the deletion query and returns how many vertices were deleted.
func (_d *SoraCacheFileDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
}
// ExecX is like Exec, but panics if an error occurs.
func (_d *SoraCacheFileDelete) ExecX(ctx context.Context) int {
n, err := _d.Exec(ctx)
if err != nil {
panic(err)
}
return n
}
func (_d *SoraCacheFileDelete) sqlExec(ctx context.Context) (int, error) {
_spec := sqlgraph.NewDeleteSpec(soracachefile.Table, sqlgraph.NewFieldSpec(soracachefile.FieldID, field.TypeInt64))
if ps := _d.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
if err != nil && sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
_d.mutation.done = true
return affected, err
}
// SoraCacheFileDeleteOne is the builder for deleting a single SoraCacheFile entity.
type SoraCacheFileDeleteOne struct {
_d *SoraCacheFileDelete
}
// Where appends a list predicates to the SoraCacheFileDelete builder.
func (_d *SoraCacheFileDeleteOne) Where(ps ...predicate.SoraCacheFile) *SoraCacheFileDeleteOne {
_d._d.mutation.Where(ps...)
return _d
}
// Exec executes the deletion query.
func (_d *SoraCacheFileDeleteOne) Exec(ctx context.Context) error {
n, err := _d._d.Exec(ctx)
switch {
case err != nil:
return err
case n == 0:
return &NotFoundError{soracachefile.Label}
default:
return nil
}
}
// ExecX is like Exec, but panics if an error occurs.
func (_d *SoraCacheFileDeleteOne) ExecX(ctx context.Context) {
if err := _d.Exec(ctx); err != nil {
panic(err)
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/soratask"
)
// SoraTaskDelete is the builder for deleting a SoraTask entity.
type SoraTaskDelete struct {
config
hooks []Hook
mutation *SoraTaskMutation
}
// Where appends a list predicates to the SoraTaskDelete builder.
func (_d *SoraTaskDelete) Where(ps ...predicate.SoraTask) *SoraTaskDelete {
_d.mutation.Where(ps...)
return _d
}
// Exec executes the deletion query and returns how many vertices were deleted.
func (_d *SoraTaskDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
}
// ExecX is like Exec, but panics if an error occurs.
func (_d *SoraTaskDelete) ExecX(ctx context.Context) int {
n, err := _d.Exec(ctx)
if err != nil {
panic(err)
}
return n
}
func (_d *SoraTaskDelete) sqlExec(ctx context.Context) (int, error) {
_spec := sqlgraph.NewDeleteSpec(soratask.Table, sqlgraph.NewFieldSpec(soratask.FieldID, field.TypeInt64))
if ps := _d.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
if err != nil && sqlgraph.IsConstraintError(err) {
err = &ConstraintError{msg: err.Error(), wrap: err}
}
_d.mutation.done = true
return affected, err
}
// SoraTaskDeleteOne is the builder for deleting a single SoraTask entity.
type SoraTaskDeleteOne struct {
_d *SoraTaskDelete
}
// Where appends a list predicates to the SoraTaskDelete builder.
func (_d *SoraTaskDeleteOne) Where(ps ...predicate.SoraTask) *SoraTaskDeleteOne {
_d._d.mutation.Where(ps...)
return _d
}
// Exec executes the deletion query.
func (_d *SoraTaskDeleteOne) Exec(ctx context.Context) error {
n, err := _d._d.Exec(ctx)
switch {
case err != nil:
return err
case n == 0:
return &NotFoundError{soratask.Label}
default:
return nil
}
}
// ExecX is like Exec, but panics if an error occurs.
func (_d *SoraTaskDeleteOne) ExecX(ctx context.Context) {
if err := _d.Exec(ctx); err != nil {
panic(err)
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment