chore: sub-store Go 重写项目初始化

This commit is contained in:
2026-07-27 14:38:13 +08:00
commit 90e53aa754
85 changed files with 15271 additions and 0 deletions
+53
View File
@@ -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
}
+198
View File
@@ -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
}
+128
View File
@@ -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:"-"`
}