chore: sub-store Go 重写项目初始化
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// APIResponse is the standard JSON envelope.
|
||||
type APIResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error *APIError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func SuccessResponse(data interface{}) APIResponse {
|
||||
return APIResponse{Status: "success", Data: data}
|
||||
}
|
||||
|
||||
func FailedResponse(message string, code int) APIResponse {
|
||||
return APIResponse{Status: "failed", Error: &APIError{Code: code, Message: message}}
|
||||
}
|
||||
|
||||
// UnmarshalFilterRules parses filters from JSON, tolerating nil.
|
||||
func UnmarshalFilterRules(data string) []FilterRule {
|
||||
if data == "" || data == "[]" {
|
||||
return []FilterRule{}
|
||||
}
|
||||
var rules []FilterRule
|
||||
if err := json.Unmarshal([]byte(data), &rules); err != nil {
|
||||
return []FilterRule{}
|
||||
}
|
||||
if rules == nil {
|
||||
return []FilterRule{}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// UnmarshalMeta parses meta from JSON, tolerating nil.
|
||||
func UnmarshalMeta(data string) map[string]any {
|
||||
if data == "" || data == "{}" {
|
||||
return map[string]any{}
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(data), &m); err != nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
if m == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package model
|
||||
|
||||
// SubscriptionTarget constants.
|
||||
const (
|
||||
TargetMihomo = "mihomo"
|
||||
TargetStash = "stash"
|
||||
TargetSurge = "surge"
|
||||
TargetSurgeMac = "surge-mac"
|
||||
TargetSurfboard = "surfboard"
|
||||
TargetLoon = "loon"
|
||||
TargetEgern = "egern"
|
||||
TargetShadowrocket = "shadowrocket"
|
||||
TargetQX = "qx"
|
||||
TargetSingBox = "sing-box"
|
||||
TargetV2ray = "v2ray"
|
||||
TargetURI = "uri"
|
||||
TargetJSON = "json"
|
||||
)
|
||||
|
||||
// FrontendVersion per review-resolution #39.
|
||||
const FrontendVersion = "1.0.0"
|
||||
|
||||
// TestURL per review-resolution #40.
|
||||
const TestURL = "https://www.gstatic.com/generate_204"
|
||||
|
||||
// targetAliases maps alias strings to canonical target names.
|
||||
var targetAliases = map[string]string{
|
||||
"clash": TargetMihomo,
|
||||
"clashmeta": TargetMihomo,
|
||||
"clash-meta": TargetMihomo,
|
||||
"meta": TargetMihomo,
|
||||
"mihomo": TargetMihomo,
|
||||
"stash": TargetStash,
|
||||
"surge": TargetSurge,
|
||||
"surgemac": TargetSurgeMac,
|
||||
"surge-mac": TargetSurgeMac,
|
||||
"surge_mac": TargetSurgeMac,
|
||||
"surfboard": TargetSurfboard,
|
||||
"loon": TargetLoon,
|
||||
"egern": TargetEgern,
|
||||
"shadowrocket": TargetShadowrocket,
|
||||
"sr": TargetShadowrocket,
|
||||
"qx": TargetQX,
|
||||
"quanx": TargetQX,
|
||||
"quantumult": TargetQX,
|
||||
"quantumultx": TargetQX,
|
||||
"quantumult-x": TargetQX,
|
||||
"singbox": TargetSingBox,
|
||||
"sing-box": TargetSingBox,
|
||||
"sfa": TargetSingBox,
|
||||
"karing": TargetSingBox,
|
||||
"v2ray": TargetV2ray,
|
||||
"v2rayn": TargetV2ray,
|
||||
"v2rayng": TargetV2ray,
|
||||
"base64": TargetV2ray,
|
||||
"uri": TargetURI,
|
||||
"uris": TargetURI,
|
||||
"plain": TargetURI,
|
||||
"text": TargetURI,
|
||||
"json": TargetJSON,
|
||||
"raw": TargetJSON,
|
||||
}
|
||||
|
||||
// NormalizeTargetAlias returns the canonical target for an alias, or "" if unknown.
|
||||
func NormalizeTargetAlias(input any) string {
|
||||
if input == nil {
|
||||
return ""
|
||||
}
|
||||
s := toString(input)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
if t, ok := targetAliases[s]; ok {
|
||||
return t
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// NormalizeTarget resolves a target from explicit input or User-Agent inference.
|
||||
// Per review-resolution #7 — full UA keyword mapping with priority order.
|
||||
func NormalizeTarget(target, ua string) string {
|
||||
v := toLowerString(target)
|
||||
if t, ok := targetAliases[v]; ok {
|
||||
return t
|
||||
}
|
||||
u := toLowerString(ua)
|
||||
if contains(u, "sing-box") || contains(u, "singbox") {
|
||||
return TargetSingBox
|
||||
}
|
||||
if contains(u, "v2ray") {
|
||||
return TargetV2ray
|
||||
}
|
||||
if contains(u, "surge") {
|
||||
if contains(u, "mac") {
|
||||
return TargetSurgeMac
|
||||
}
|
||||
return TargetSurge
|
||||
}
|
||||
if contains(u, "loon") {
|
||||
return TargetLoon
|
||||
}
|
||||
if contains(u, "egern") {
|
||||
return TargetEgern
|
||||
}
|
||||
if contains(u, "shadowrocket") {
|
||||
return TargetShadowrocket
|
||||
}
|
||||
if contains(u, "quantumult") {
|
||||
return TargetQX
|
||||
}
|
||||
if contains(u, "stash") {
|
||||
return TargetStash
|
||||
}
|
||||
return TargetMihomo
|
||||
}
|
||||
|
||||
// IsTargetCompatible checks whether a proxy type can be rendered for the given target.
|
||||
func IsTargetCompatible(proxyType, target string) bool {
|
||||
if target == TargetMihomo || target == TargetStash || target == TargetJSON {
|
||||
return true
|
||||
}
|
||||
commonUri := []string{"ss", "ssr", "vmess", "vless", "trojan", "hysteria", "hysteria2", "tuic", "anytls", "http", "socks5", "wireguard"}
|
||||
switch target {
|
||||
case TargetURI, TargetV2ray, TargetShadowrocket:
|
||||
return containsAny(proxyType, commonUri)
|
||||
case TargetSingBox:
|
||||
return containsAny(proxyType, []string{"ss", "vmess", "vless", "trojan", "hysteria", "hysteria2", "tuic", "anytls", "http", "socks5", "wireguard"})
|
||||
case TargetSurge:
|
||||
return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls", "snell"})
|
||||
case TargetSurgeMac:
|
||||
return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls", "snell", "ssh", "h2-connect"})
|
||||
case TargetSurfboard:
|
||||
return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5"})
|
||||
case TargetLoon:
|
||||
return containsAny(proxyType, []string{"ss", "ssr", "vmess", "vless", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls", "wireguard"})
|
||||
case TargetQX:
|
||||
return containsAny(proxyType, []string{"ss", "ssr", "vmess", "vless", "trojan", "http", "socks5", "anytls"})
|
||||
case TargetEgern:
|
||||
return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls"})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetTargetContentType returns the Content-Type for a target.
|
||||
func GetTargetContentType(target string) string {
|
||||
switch target {
|
||||
case TargetSingBox, TargetJSON:
|
||||
return "application/json; charset=utf-8"
|
||||
case TargetV2ray, TargetURI, TargetSurge, TargetSurgeMac, TargetSurfboard, TargetLoon, TargetShadowrocket, TargetQX:
|
||||
return "text/plain; charset=utf-8"
|
||||
default:
|
||||
return "text/yaml; charset=utf-8"
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func toString(v any) string {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func toLowerString(v any) string {
|
||||
s := toString(v)
|
||||
b := make([]byte, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
c += 32
|
||||
}
|
||||
b[i] = c
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || indexOf(s, substr) >= 0)
|
||||
}
|
||||
|
||||
func indexOf(s, substr string) int {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func containsAny(s string, list []string) bool {
|
||||
for _, item := range list {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package model
|
||||
|
||||
// ProxyNode is a dynamic proxy configuration node.
|
||||
// Defined as map[string]any per review-resolution #1 — proxy protocols have
|
||||
// many optional fields and unknown fields must be preserved through JSON/YAML round-trips.
|
||||
type ProxyNode map[string]any
|
||||
|
||||
// FilterRule represents one operation in the filter pipeline.
|
||||
type FilterRule struct {
|
||||
Type string `json:"type"`
|
||||
Field string `json:"field,omitempty"`
|
||||
Fields []string `json:"fields,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
Patterns []string `json:"patterns,omitempty"`
|
||||
Expressions []string `json:"expressions,omitempty"`
|
||||
Replacement string `json:"replacement,omitempty"`
|
||||
Direction string `json:"direction,omitempty"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Link string `json:"link,omitempty"`
|
||||
Position string `json:"position,omitempty"`
|
||||
Template string `json:"template,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
RecordType string `json:"recordType,omitempty"`
|
||||
Filter string `json:"filter,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Edns string `json:"edns,omitempty"`
|
||||
Concurrency any `json:"concurrency,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Tw string `json:"tw,omitempty"`
|
||||
Udp any `json:"udp,omitempty"`
|
||||
Tfo any `json:"tfo,omitempty"`
|
||||
Scert any `json:"scert,omitempty"`
|
||||
Useless any `json:"useless,omitempty"`
|
||||
Args map[string]any `json:"args,omitempty"`
|
||||
Rules []CustomRule `json:"rules,omitempty"`
|
||||
// Catch-all for unknown keys
|
||||
Extra map[string]any `json:"-"`
|
||||
}
|
||||
|
||||
// CustomRule is one atomic operation in a custom filter rule chain (review-resolution #37).
|
||||
type CustomRule struct {
|
||||
Action string `json:"action"`
|
||||
Field string `json:"field,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
Replacement string `json:"replacement,omitempty"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Template string `json:"template,omitempty"`
|
||||
When string `json:"when,omitempty"`
|
||||
}
|
||||
|
||||
// SourceRecord is the full DB record for a subscription source.
|
||||
type SourceRecord struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Content string `json:"content"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Filters []FilterRule `json:"filters"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// CollectionRecord is the full DB record for a subscription collection.
|
||||
type CollectionRecord struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
SourceIds []string `json:"sourceIds"`
|
||||
Filters []FilterRule `json:"filters"`
|
||||
TemplateId string `json:"templateId"`
|
||||
IgnoreFailed bool `json:"ignoreFailed"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// TemplateRecord is the full DB record for a routing template.
|
||||
type TemplateRecord struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Target string `json:"target"`
|
||||
Config map[string]any `json:"config"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// DownloadGrantRecord represents a scoped download share token.
|
||||
type DownloadGrantRecord struct {
|
||||
ID string `json:"id"`
|
||||
ResourceType string `json:"resourceType"`
|
||||
ResourceId string `json:"resourceId"`
|
||||
Target string `json:"target,omitempty"`
|
||||
ExpiresAt *int64 `json:"expiresAt,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// RecycleBinRecord represents a soft-deleted resource snapshot.
|
||||
type RecycleBinRecord struct {
|
||||
ID string `json:"id"`
|
||||
ResourceType string `json:"resourceType"`
|
||||
ResourceId string `json:"resourceId"`
|
||||
Snapshot map[string]any `json:"snapshot"`
|
||||
DeletedAt int64 `json:"deletedAt"`
|
||||
}
|
||||
|
||||
// AppConfig is the full export/import payload.
|
||||
type AppConfig struct {
|
||||
Sources []SourceRecord `json:"sources"`
|
||||
Collections []CollectionRecord `json:"collections"`
|
||||
Templates []TemplateRecord `json:"templates"`
|
||||
Settings map[string]any `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
// SubscriptionResponseMetadata holds headers/metadata for download responses.
|
||||
type SubscriptionResponseMetadata struct {
|
||||
SubscriptionUserinfo string `json:"-"`
|
||||
ProfileWebPageUrl string `json:"-"`
|
||||
ProfileUpdateInterval string `json:"-"`
|
||||
ContentDisposition string `json:"-"`
|
||||
Etag string `json:"-"`
|
||||
LastModified string `json:"-"`
|
||||
CacheStatus string `json:"-"`
|
||||
}
|
||||
Reference in New Issue
Block a user