Files
subconverter-go/internal/conversion/request.go
Rogee 7fcabe0225
Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
first commit
2025-09-28 10:05:07 +08:00

826 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package conversion
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
)
// ConversionRequest 表示转换请求的参数和配置
// 该结构体包含从HTTP请求解析出的所有转换参数
type ConversionRequest struct {
// 基本参数
Target string `json:"target"` // 目标格式clash, surge, quanx, loon, surfboard, v2ray
URL string `json:"url"` // 源订阅URL
RawURL string `json:"-"` // 原始请求中的URL参数可能包含多源
Sources []string `json:"-"` // 解析后的订阅源列表
// 请求上下文
UserAgent string `json:"-"` // 客户端传入的 User-Agent用于远程拉取订阅时复用
// 可选参数
ConfigURL string `json:"config_url,omitempty"` // 自定义配置文件URL
Emoji bool `json:"emoji,omitempty"` // 是否启用emoji
UDP bool `json:"udp,omitempty"` // 是否启用UDP支持
IPv6 bool `json:"ipv6,omitempty"` // 是否启用IPv6支持
Group string `json:"group,omitempty"` // 节点分组名称
Insert bool `json:"insert,omitempty"` // 是否插入URL测试
Strict bool `json:"strict,omitempty"` // 是否严格模式
Compatible bool `json:"compatible,omitempty"` // 是否兼容模式
AppendType bool `json:"append_type,omitempty"`
TFO bool `json:"tfo,omitempty"`
Script bool `json:"script,omitempty"`
SkipCert bool `json:"skip_cert_verify,omitempty"`
FilterDeprecated bool `json:"filter_deprecated,omitempty"`
ExpandRules bool `json:"expand_rules,omitempty"`
AppendInfo bool `json:"append_info,omitempty"`
Prepend bool `json:"prepend,omitempty"`
Classic bool `json:"classic,omitempty"`
TLS13 bool `json:"tls13,omitempty"`
AddEmoji bool `json:"add_emoji,omitempty"`
RemoveEmoji bool `json:"remove_emoji,omitempty"`
Upload bool `json:"upload,omitempty"`
// 过滤参数
Include []string `json:"include,omitempty"` // 包含的关键词
Exclude []string `json:"exclude,omitempty"` // 排除的关键词
Regex string `json:"regex,omitempty"` // 正则表达式过滤
Location string `json:"location,omitempty"` // 地区过滤
RenameRules []string `json:"rename_rules,omitempty"` // 重命名规则
// 高级参数
Test bool `json:"test,omitempty"` // 是否启用测试
List bool `json:"list,omitempty"` // 是否列表模式
Filename string `json:"filename,omitempty"` // 输出文件名
UploadPath string `json:"upload_path,omitempty"`
FilterScript string `json:"filter_script,omitempty"`
DeviceID string `json:"device_id,omitempty"`
Interval string `json:"interval,omitempty"`
Groups []string `json:"groups,omitempty"`
Rulesets []string `json:"rulesets,omitempty"`
BasePath string `json:"-"`
Providers []string `json:"providers,omitempty"`
EmojiRules []string `json:"emoji_rules,omitempty"`
}
// NewConversionRequest 创建新的转换请求
// 返回包含默认值的ConversionRequest结构体
func NewConversionRequest() *ConversionRequest {
return &ConversionRequest{
Target: "",
URL: "",
RawURL: "",
Sources: []string{},
UserAgent: "",
ConfigURL: "",
Emoji: false,
UDP: false,
IPv6: false,
Group: "",
Insert: false,
Strict: false,
Compatible: true, // 默认启用兼容模式
Include: []string{},
Exclude: []string{},
Regex: "",
Location: "",
RenameRules: []string{},
Test: false,
List: false,
Filename: "",
UploadPath: "",
FilterScript: "",
DeviceID: "",
Interval: "",
Groups: []string{},
Rulesets: []string{},
BasePath: "",
Providers: []string{},
EmojiRules: []string{},
}
}
// SetURL 设置订阅URL支持多源
func (r *ConversionRequest) SetURL(raw string) error {
trimmed := strings.TrimSpace(raw)
r.RawURL = trimmed
if trimmed == "" {
r.URL = ""
r.Sources = nil
return nil
}
sources, err := parseSourcesFromRaw(trimmed)
if err != nil {
return err
}
r.Sources = sources
if len(sources) > 0 {
r.URL = sources[0]
} else {
r.URL = ""
}
return nil
}
// GetSources 返回订阅源列表(拷贝)
func (r *ConversionRequest) GetSources() []string {
if len(r.Sources) > 0 {
out := make([]string, len(r.Sources))
copy(out, r.Sources)
return out
}
if strings.TrimSpace(r.URL) != "" {
return []string{strings.TrimSpace(r.URL)}
}
return nil
}
// SourceSummary 返回用于记录/展示的源URL字符串
func (r *ConversionRequest) SourceSummary() string {
if strings.TrimSpace(r.RawURL) != "" {
return r.RawURL
}
sources := r.GetSources()
if len(sources) == 0 {
return ""
}
return strings.Join(sources, "|")
}
// Validate 验证转换请求的有效性
// 返回error如果请求无效nil表示请求有效
func (r *ConversionRequest) Validate() error {
// 验证必需字段
if r.Target == "" {
return fmt.Errorf("target format cannot be empty")
}
sources := r.GetSources()
if len(sources) == 0 && strings.TrimSpace(r.URL) != "" {
if err := r.SetURL(r.URL); err != nil {
return err
}
sources = r.GetSources()
}
if len(sources) == 0 {
return fmt.Errorf("source URL cannot be empty")
}
// 验证目标格式
target := strings.ToLower(r.Target)
if target == "auto" {
return nil
}
validTargets := map[string]bool{
"clash": true,
"clashr": true,
"surge": true,
"quanx": true,
"loon": true,
"surfboard": true,
"v2ray": true,
}
if !validTargets[target] {
return fmt.Errorf("unsupported target format: %s, must be one of: clash, clashr, surge, quanx, loon, surfboard, v2ray", r.Target)
}
// 验证URL格式
for _, src := range sources {
if err := r.validateURL(src, "source URL"); err != nil {
return err
}
}
// 验证配置URL如果提供
if r.ConfigURL != "" {
if err := r.validateURL(r.ConfigURL, "config URL"); err != nil {
return err
}
}
// 验证正则表达式
if r.Regex != "" {
if _, err := regexp.Compile(r.Regex); err != nil {
return fmt.Errorf("invalid regex pattern: %v", err)
}
}
// 验证文件名
if r.Filename != "" {
if len(r.Filename) > 255 {
return fmt.Errorf("filename too long (max 255 characters)")
}
if strings.ContainsAny(r.Filename, "\\/:*?\"<>|") {
return fmt.Errorf("filename contains invalid characters")
}
}
return nil
}
// validateURL 验证URL格式
func (r *ConversionRequest) validateURL(rawURL, fieldName string) error {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("invalid %s: %v", fieldName, err)
}
if parsedURL.Scheme == "" {
return fmt.Errorf("%s must have a scheme (http:// or https://)", fieldName)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return fmt.Errorf("%s scheme must be http or https, got: %s", fieldName, parsedURL.Scheme)
}
if parsedURL.Host == "" {
return fmt.Errorf("%s must have a host", fieldName)
}
return nil
}
// FromQuery 从HTTP查询参数解析转换请求
// 根据查询参数自动填充ConversionRequest字段
func (r *ConversionRequest) FromQuery(query string) error {
values, err := url.ParseQuery(query)
if err != nil {
return fmt.Errorf("failed to parse query: %v", err)
}
splitAndTrim := func(input, sep string) []string {
parts := strings.Split(input, sep)
out := make([]string, 0, len(parts))
for _, part := range parts {
if trimmed := strings.TrimSpace(part); trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
decodeBase64List := func(key, sep string) ([]string, error) {
raw := values.Get(key)
if raw == "" {
return nil, nil
}
decoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, fmt.Errorf("invalid %s parameter: %v", key, err)
}
decodedStr := strings.TrimSpace(string(decoded))
if decodedStr == "" {
return []string{}, nil
}
return splitAndTrim(decodedStr, sep), nil
}
parseBool := func(key string, setter func(bool)) error {
raw := values.Get(key)
if raw == "" {
return nil
}
parsed, err := strconv.ParseBool(raw)
if err != nil {
return fmt.Errorf("invalid %s parameter: %v", key, err)
}
setter(parsed)
return nil
}
// 基本参数
if target := values.Get("target"); target != "" {
r.Target = target
}
if urlStr := values.Get("url"); urlStr != "" {
if err := r.SetURL(urlStr); err != nil {
return err
}
}
// 可选参数
if configURL := values.Get("config"); configURL != "" {
r.ConfigURL = configURL
}
if err := parseBool("emoji", func(v bool) { r.Emoji = v }); err != nil {
return err
}
if err := parseBool("udp", func(v bool) { r.UDP = v }); err != nil {
return err
}
if err := parseBool("ipv6", func(v bool) { r.IPv6 = v }); err != nil {
return err
}
if group := values.Get("group"); group != "" {
r.Group = group
}
if err := parseBool("insert", func(v bool) { r.Insert = v }); err != nil {
return err
}
if err := parseBool("strict", func(v bool) { r.Strict = v }); err != nil {
return err
}
if err := parseBool("compatible", func(v bool) { r.Compatible = v }); err != nil {
return err
}
if err := parseBool("append_type", func(v bool) { r.AppendType = v }); err != nil {
return err
}
if err := parseBool("tfo", func(v bool) { r.TFO = v }); err != nil {
return err
}
if err := parseBool("script", func(v bool) { r.Script = v }); err != nil {
return err
}
if err := parseBool("scv", func(v bool) { r.SkipCert = v }); err != nil {
return err
}
if err := parseBool("fdn", func(v bool) { r.FilterDeprecated = v }); err != nil {
return err
}
if err := parseBool("expand", func(v bool) { r.ExpandRules = v }); err != nil {
return err
}
if err := parseBool("append_info", func(v bool) { r.AppendInfo = v }); err != nil {
return err
}
if err := parseBool("prepend", func(v bool) { r.Prepend = v }); err != nil {
return err
}
if err := parseBool("classic", func(v bool) { r.Classic = v }); err != nil {
return err
}
if err := parseBool("tls13", func(v bool) { r.TLS13 = v }); err != nil {
return err
}
if err := parseBool("add_emoji", func(v bool) { r.AddEmoji = v }); err != nil {
return err
}
if err := parseBool("remove_emoji", func(v bool) { r.RemoveEmoji = v }); err != nil {
return err
}
if err := parseBool("upload", func(v bool) { r.Upload = v }); err != nil {
return err
}
// 过滤参数
if include := values.Get("include"); include != "" {
r.Include = splitAndTrim(include, ",")
}
if exclude := values.Get("exclude"); exclude != "" {
r.Exclude = splitAndTrim(exclude, ",")
}
if regex := values.Get("regex"); regex != "" {
r.Regex = regex
}
if location := values.Get("location"); location != "" {
r.Location = location
}
// 高级参数
if err := parseBool("test", func(v bool) { r.Test = v }); err != nil {
return err
}
if err := parseBool("list", func(v bool) { r.List = v }); err != nil {
return err
}
if filename := values.Get("filename"); filename != "" {
r.Filename = filename
}
if uploadPath := values.Get("upload_path"); uploadPath != "" {
r.UploadPath = uploadPath
}
if rename := values.Get("rename"); rename != "" {
r.RenameRules = splitAndTrim(rename, "`")
}
if filterScript := values.Get("filter_script"); filterScript != "" {
r.FilterScript = filterScript
}
if deviceID := values.Get("dev_id"); deviceID != "" {
r.DeviceID = deviceID
}
if interval := values.Get("interval"); interval != "" {
r.Interval = interval
}
if groups, err := decodeBase64List("groups", "@"); err != nil {
return err
} else if groups != nil {
r.Groups = groups
}
if rulesets, err := decodeBase64List("ruleset", "@"); err != nil {
return err
} else if rulesets != nil {
r.Rulesets = rulesets
}
if providers, err := decodeBase64List("providers", "@"); err != nil {
return err
} else if providers != nil {
r.Providers = providers
}
if emojiRules := values.Get("emoji_rule"); emojiRules != "" {
r.EmojiRules = splitAndTrim(emojiRules, "`")
}
if len(r.EmojiRules) == 0 {
if emojiRules := values.Get("emoji_rules"); emojiRules != "" {
r.EmojiRules = splitAndTrim(emojiRules, "`")
}
}
return r.Validate()
}
// FromHTTPRequest 从HTTP请求解析转换请求
// 自动从URL查询参数和请求头中提取转换参数
func (r *ConversionRequest) FromHTTPRequest(req *http.Request) error {
// 从查询参数解析
if err := r.FromQuery(req.URL.RawQuery); err != nil {
return err
}
// 从请求头中获取额外的信息
if userAgent := req.Header.Get("User-Agent"); userAgent != "" {
// 可以根据User-Agent进行特殊处理
}
if accept := req.Header.Get("Accept"); accept != "" {
// 可以根据Accept头调整输出格式
}
return nil
}
// ToQuery 将转换请求转换为查询字符串
// 返回URL编码的查询字符串
func (r *ConversionRequest) ToQuery() string {
values := url.Values{}
// 基本参数
values.Set("target", r.Target)
values.Set("url", r.SourceSummary())
// 可选参数
if r.ConfigURL != "" {
values.Set("config", r.ConfigURL)
}
values.Set("emoji", strconv.FormatBool(r.Emoji))
values.Set("udp", strconv.FormatBool(r.UDP))
values.Set("ipv6", strconv.FormatBool(r.IPv6))
if r.Group != "" {
values.Set("group", r.Group)
}
values.Set("insert", strconv.FormatBool(r.Insert))
values.Set("strict", strconv.FormatBool(r.Strict))
values.Set("compatible", strconv.FormatBool(r.Compatible))
// 过滤参数
if len(r.Include) > 0 {
values.Set("include", strings.Join(r.Include, ","))
}
if len(r.Exclude) > 0 {
values.Set("exclude", strings.Join(r.Exclude, ","))
}
if r.Regex != "" {
values.Set("regex", r.Regex)
}
if r.Location != "" {
values.Set("location", r.Location)
}
// 高级参数
values.Set("test", strconv.FormatBool(r.Test))
values.Set("list", strconv.FormatBool(r.List))
if r.Filename != "" {
values.Set("filename", r.Filename)
}
return values.Encode()
}
// Clone 创建转换请求的副本
// 返回新的ConversionRequest实例
func (r *ConversionRequest) Clone() *ConversionRequest {
return &ConversionRequest{
Target: r.Target,
URL: r.URL,
RawURL: r.RawURL,
Sources: append([]string{}, r.Sources...),
ConfigURL: r.ConfigURL,
Emoji: r.Emoji,
UDP: r.UDP,
Group: r.Group,
Insert: r.Insert,
Strict: r.Strict,
Compatible: r.Compatible,
Include: append([]string{}, r.Include...),
Exclude: append([]string{}, r.Exclude...),
Regex: r.Regex,
Location: r.Location,
Test: r.Test,
List: r.List,
Filename: r.Filename,
Providers: append([]string{}, r.Providers...),
EmojiRules: append([]string{}, r.EmojiRules...),
}
}
// CacheSignature returns a canonical representation of the request for caching.
func (r *ConversionRequest) CacheSignature() string {
sources := r.GetSources()
signature := struct {
Target string `json:"target"`
Sources []string `json:"sources"`
ConfigURL string `json:"config_url"`
Emoji bool `json:"emoji"`
UDP bool `json:"udp"`
IPv6 bool `json:"ipv6"`
Group string `json:"group"`
Insert bool `json:"insert"`
Strict bool `json:"strict"`
Compatible bool `json:"compatible"`
AppendType bool `json:"append_type"`
TFO bool `json:"tfo"`
Script bool `json:"script"`
SkipCert bool `json:"skip_cert"`
FilterDeprecated bool `json:"filter_deprecated"`
ExpandRules bool `json:"expand_rules"`
AppendInfo bool `json:"append_info"`
Prepend bool `json:"prepend"`
Classic bool `json:"classic"`
TLS13 bool `json:"tls13"`
AddEmoji bool `json:"add_emoji"`
RemoveEmoji bool `json:"remove_emoji"`
Upload bool `json:"upload"`
UploadPath string `json:"upload_path"`
Filename string `json:"filename"`
Test bool `json:"test"`
List bool `json:"list"`
DeviceID string `json:"device_id"`
Interval string `json:"interval"`
Include []string `json:"include"`
Exclude []string `json:"exclude"`
Regex string `json:"regex"`
Location string `json:"location"`
RenameRules []string `json:"rename_rules"`
Groups []string `json:"groups"`
Rulesets []string `json:"rulesets"`
Providers []string `json:"providers"`
EmojiRules []string `json:"emoji_rules"`
FilterScript string `json:"filter_script"`
BasePath string `json:"base_path"`
}{
Target: strings.ToLower(r.Target),
Sources: append([]string{}, sources...),
ConfigURL: r.ConfigURL,
Emoji: r.Emoji,
UDP: r.UDP,
IPv6: r.IPv6,
Group: r.Group,
Insert: r.Insert,
Strict: r.Strict,
Compatible: r.Compatible,
AppendType: r.AppendType,
TFO: r.TFO,
Script: r.Script,
SkipCert: r.SkipCert,
FilterDeprecated: r.FilterDeprecated,
ExpandRules: r.ExpandRules,
AppendInfo: r.AppendInfo,
Prepend: r.Prepend,
Classic: r.Classic,
TLS13: r.TLS13,
AddEmoji: r.AddEmoji,
RemoveEmoji: r.RemoveEmoji,
Upload: r.Upload,
UploadPath: r.UploadPath,
Filename: r.Filename,
Test: r.Test,
List: r.List,
DeviceID: r.DeviceID,
Interval: r.Interval,
Include: append([]string{}, r.Include...),
Exclude: append([]string{}, r.Exclude...),
Regex: r.Regex,
Location: r.Location,
RenameRules: append([]string{}, r.RenameRules...),
Groups: append([]string{}, r.Groups...),
Rulesets: append([]string{}, r.Rulesets...),
Providers: append([]string{}, r.Providers...),
EmojiRules: append([]string{}, r.EmojiRules...),
FilterScript: r.FilterScript,
BasePath: r.BasePath,
}
data, err := json.Marshal(signature)
if err != nil {
return fmt.Sprintf("%s|%s|%t|%t|%t", signature.Target, r.SourceSummary(), r.AddEmoji, r.RemoveEmoji, r.Upload)
}
return string(data)
}
func parseSourcesFromRaw(raw string) ([]string, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return nil, nil
}
sources := splitSources(trimmed)
if len(sources) <= 1 {
if decoded, ok := decodeURLListFromBase64(trimmed); ok {
sources = splitSources(decoded)
}
}
if len(sources) == 0 && trimmed != "" {
sources = []string{trimmed}
}
return sources, nil
}
func splitSources(input string) []string {
if input == "" {
return nil
}
normalized := strings.ReplaceAll(input, "\r\n", "\n")
normalized = strings.ReplaceAll(normalized, "\r", "\n")
parts := strings.FieldsFunc(normalized, func(r rune) bool {
switch r {
case '|', '\n', ',', ';':
return true
default:
return false
}
})
result := make([]string, 0, len(parts))
for _, part := range parts {
if trimmed := strings.TrimSpace(part); trimmed != "" {
result = append(result, trimmed)
}
}
if len(result) == 0 {
if trimmed := strings.TrimSpace(input); trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
func decodeURLListFromBase64(raw string) (string, bool) {
data, ok := decodeBase64String(raw)
if !ok || len(data) == 0 {
return "", false
}
decoded := strings.TrimSpace(string(data))
lower := strings.ToLower(decoded)
if !strings.Contains(lower, "http://") && !strings.Contains(lower, "https://") {
return "", false
}
return decoded, true
}
func decodeBase64String(raw string) ([]byte, bool) {
s := strings.TrimSpace(raw)
if s == "" {
return nil, false
}
s = strings.ReplaceAll(s, "\n", "")
s = strings.ReplaceAll(s, "\r", "")
if rem := len(s) % 4; rem != 0 {
s += strings.Repeat("=", 4-rem)
}
for _, enc := range []*base64.Encoding{base64.StdEncoding, base64.URLEncoding} {
decoded, err := enc.DecodeString(s)
if err == nil && len(decoded) > 0 {
return decoded, true
}
}
return nil, false
}
// MatchesFilter 检查代理是否匹配过滤条件
// 返回true如果代理匹配所有过滤条件
func (r *ConversionRequest) MatchesFilter(proxyName, proxyLocation string) bool {
// 检查包含关键词
for _, include := range r.Include {
if !strings.Contains(strings.ToLower(proxyName), strings.ToLower(include)) {
return false
}
}
// 检查排除关键词
for _, exclude := range r.Exclude {
if strings.Contains(strings.ToLower(proxyName), strings.ToLower(exclude)) {
return false
}
}
// 检查正则表达式
if r.Regex != "" {
if matched, err := regexp.MatchString(r.Regex, proxyName); err != nil || !matched {
return false
}
}
// 检查地区
if r.Location != "" && proxyLocation != "" {
if !strings.EqualFold(proxyLocation, r.Location) {
return false
}
}
return true
}
// GetOutputFilename 获取输出文件名
// 根据配置生成合适的输出文件名
func (r *ConversionRequest) GetOutputFilename() string {
if r.Filename != "" {
// 确保有正确的扩展名
if !strings.HasSuffix(r.Filename, "."+r.GetFileExtension()) {
return r.Filename + "." + r.GetFileExtension()
}
return r.Filename
}
// 生成默认文件名
defaultName := "converted"
if r.Group != "" {
defaultName = r.Group
}
return fmt.Sprintf("%s.%s", defaultName, r.GetFileExtension())
}
// GetFileExtension 获取目标格式的文件扩展名
// 返回对应格式的文件扩展名
func (r *ConversionRequest) GetFileExtension() string {
switch strings.ToLower(r.Target) {
case "clash":
return "yaml"
case "surge":
return "conf"
case "quanx":
return "conf"
case "loon":
return "conf"
case "surfboard":
return "conf"
case "v2ray":
return "json"
default:
return "txt"
}
}
// GetContentType 获取响应的Content-Type
// 返回适合目标格式的MIME类型
func (r *ConversionRequest) GetContentType() string {
if r.List {
return "text/plain; charset=utf-8"
}
switch strings.ToLower(r.Target) {
case "clash":
return "text/yaml; charset=utf-8"
case "surge", "quanx", "loon", "surfboard":
return "text/plain; charset=utf-8"
case "v2ray":
return "application/json; charset=utf-8"
default:
return "text/plain; charset=utf-8"
}
}
// IsCompatible 检查是否与C++版本兼容
// 返回true如果使用兼容模式
func (r *ConversionRequest) IsCompatible() bool {
return r.Compatible
}
// ShouldIncludeTests 检查是否应该包含测试信息
// 返回true如果启用了测试模式
func (r *ConversionRequest) ShouldIncludeTests() bool {
return r.Test
}
// ShouldShowList 检查是否应该显示列表
// 返回true如果是列表模式
func (r *ConversionRequest) ShouldShowList() bool {
return r.List
}
// ShouldInsertURLTest 检查是否应该插入URL测试
// 返回true如果启用了插入URL测试
func (r *ConversionRequest) ShouldInsertURLTest() bool {
return r.Insert
}
// IsStrictMode 检查是否是严格模式
// 返回true如果启用了严格模式
func (r *ConversionRequest) IsStrictMode() bool {
return r.Strict
}