Files
SubConverter-Extended/scripts/generate_schemes.go
Aethersailor 2047424d28 feat(phase-9.3): implement fully automated parameter compatibility system
- Auto-extract protocol list from mihomo converter.go switch statements
- Auto-extract protocol aliases from case declarations (hy2->hysteria2, etc)
- Auto-scan all protocol files and build protocol->file->struct mappings
- Auto-extract parameters from XXXOption structs via AST parsing
- Generate param_compat.h with complete compatibility database
- Integrate smart parameter application in subexport.cpp
- Add mihomo_schemes.h and param_compat.h generation to build process
- Support 15 protocols with zero hardcoded protocol lists
- Fix http/https protocol filtering (were incorrectly excluded)
- Add intelligent protocol name matching for abbreviations (ss->shadowsocks)

This achieves ~95% automation - only 3 naming convention mappings remain.
All protocol and alias information is now extracted directly from mihomo source code.
2025-12-29 01:41:22 +08:00

189 lines
4.4 KiB
Go

package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
)
const headerTemplate = `// Auto-generated by scripts/generate_schemes.go
// DO NOT EDIT MANUALLY
// Based on mihomo version: {{.Version}}
#pragma once
#include <vector>
#include <string>
namespace mihomo {
const std::vector<std::string> SUPPORTED_SCHEMES = {
{{- range .Schemes}}
"{{.}}",
{{- end}}
};
}
`
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run generate_schemes.go <output_path>")
os.Exit(1)
}
outputPath := os.Args[1]
// Determine module root
moduleRoot := "."
if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
// try specific known paths
if _, err := os.Stat("../bridge/go.mod"); err == nil {
moduleRoot = "../bridge"
} else if _, err := os.Stat("bridge/go.mod"); err == nil {
moduleRoot = "bridge"
}
}
fmt.Printf("Using module root: %s\n", moduleRoot)
// 1. Get mihomo source path
cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "github.com/metacubex/mihomo")
cmd.Dir = moduleRoot // execute in correct directory
// Capture stderr for debugging
var stderr strings.Builder
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil {
fmt.Printf("Error finding mihomo source: %v\nStderr: %s\n", err, stderr.String())
os.Exit(1)
}
mihomoPath := strings.TrimSpace(string(output))
if mihomoPath == "" {
fmt.Printf("Error: go list returned empty path! Stderr: %s\n", stderr.String())
// Fallback Debug: Get full JSON info
debugCmd := exec.Command("go", "list", "-m", "-json", "github.com/metacubex/mihomo")
debugCmd.Dir = moduleRoot
debugOut, _ := debugCmd.CombinedOutput()
fmt.Printf("Full go list JSON output:\n%s\n", string(debugOut))
// Try to fallback to GOPATH mode if we can guess it
if moduleRoot == "." || moduleRoot == "../bridge" {
// If we are in module root, maybe we can just find it?
// But if go list failed, probably finding it is hard.
}
os.Exit(1)
}
targetFile := filepath.Join(mihomoPath, "common/convert/converter.go")
fmt.Printf("Analyzing %s...\n", targetFile)
// 2. Parse AST
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, targetFile, nil, parser.ParseComments)
if err != nil {
fmt.Printf("Error parsing file: %v\n", err)
os.Exit(1)
}
schemes := make([]string, 0)
seen := make(map[string]bool)
// 3. Walk AST
ast.Inspect(node, func(n ast.Node) bool {
fn, ok := n.(*ast.FuncDecl)
if !ok || fn.Name.Name != "ConvertsV2Ray" {
return true
}
fmt.Println("Found ConvertsV2Ray function")
ast.Inspect(fn.Body, func(n ast.Node) bool {
switchStmt, ok := n.(*ast.SwitchStmt)
if !ok {
return true
}
// Optional: check Tag
if ident, ok := switchStmt.Tag.(*ast.Ident); ok {
if ident.Name != "scheme" {
return true
}
}
fmt.Printf("Found switch statement with %d cases\n", len(switchStmt.Body.List))
for _, caseClause := range switchStmt.Body.List {
cc, ok := caseClause.(*ast.CaseClause)
if !ok {
continue
}
// Optional Debug: fmt.Printf(" Case %d: %d expressions\n", i, len(cc.List))
for _, expr := range cc.List {
switch v := expr.(type) {
case *ast.BasicLit:
if v.Kind == token.STRING {
val := strings.Trim(v.Value, "\"")
// Filter out transport-layer protocols (not standalone proxy protocols)
// ws, grpc, h2, httpupgrade are transport layers for vmess/vless/trojan
transportProtocols := []string{"ws", "grpc", "h2", "httpupgrade"}
isTransport := false
for _, tp := range transportProtocols {
if val == tp {
isTransport = true
break
}
}
// Include all standalone proxy protocols (including http/https)
if !isTransport && !seen[val] {
schemes = append(schemes, val)
seen[val] = true
}
}
}
}
}
return true
})
return false
})
if len(schemes) == 0 {
fmt.Println("Warning: No schemes found!")
} else {
fmt.Printf("Total schemes found: %d\n", len(schemes))
}
// 4. Generate Output
data := struct {
Version string
Schemes []string
}{
Version: "latest",
Schemes: schemes,
}
f, err := os.Create(outputPath)
if err != nil {
fmt.Printf("Error creating output file: %v\n", err)
os.Exit(1)
}
defer f.Close()
tmpl := template.Must(template.New("header").Parse(headerTemplate))
tmpl.Execute(f, data)
fmt.Printf("Successfully generated %s\n", outputPath)
}