First web useful
This commit is contained in:
commit
ddaba6ce62
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
# Binary files
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# Go build artifacts
|
||||
/_build/
|
||||
/_build/
|
||||
/dist/
|
||||
/bin/
|
||||
/tmp/
|
||||
|
||||
# Ignore log files and debugging output
|
||||
*.log
|
||||
*.trace
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
Gopkg.lock
|
||||
Gopkg.toml
|
||||
|
||||
# Go modules
|
||||
go.sum
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# Test output
|
||||
*.coverprofile
|
||||
*.test
|
||||
test-results/
|
||||
|
||||
# OS-specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Coverage files
|
||||
coverage.out
|
||||
|
||||
# Other temporary files
|
||||
*.bak
|
||||
*.tmp
|
||||
*.old
|
||||
*.backup
|
||||
!.keep
|
||||
src/proxies/*
|
||||
src/rules/*
|
||||
acl4ssr_repo
|
||||
lufxy.yaml
|
||||
result.yaml
|
||||
55
go.mod
Normal file
55
go.mod
Normal file
@ -0,0 +1,55 @@
|
||||
module proxyrules
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/go-git/go-git/v5 v5.12.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/bytedance/sonic v1.12.5 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.10.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.23.0 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
78
src/aclgit/manager.go
Normal file
78
src/aclgit/manager.go
Normal file
@ -0,0 +1,78 @@
|
||||
package aclgit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// AclGitManager 封装了克隆、分支切换和文件读取功能
|
||||
type AclGitManager struct {
|
||||
RepoURL string
|
||||
TargetDir string
|
||||
Repository *git.Repository
|
||||
}
|
||||
|
||||
// NewAclGitManager 创建一个新的 AclGitManager 实例
|
||||
func NewAclGitManager(repoURL, targetDir string) *AclGitManager {
|
||||
return &AclGitManager{
|
||||
RepoURL: repoURL,
|
||||
TargetDir: targetDir,
|
||||
}
|
||||
}
|
||||
|
||||
// Clone 克隆远程仓库到本地的 rules 文件夹,并默认指定分支
|
||||
func (a *AclGitManager) Clone(branchName string) error {
|
||||
// 确定克隆到 rules 文件夹
|
||||
cloneDir := filepath.Join(a.TargetDir, "rules")
|
||||
|
||||
// 如果目标目录已存在,删除它
|
||||
if _, err := os.Stat(cloneDir); err == nil {
|
||||
if err := os.RemoveAll(cloneDir); err != nil {
|
||||
return fmt.Errorf("failed to cleanup existing directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 克隆仓库,并指定分支
|
||||
repo, err := git.PlainClone(cloneDir, false, &git.CloneOptions{
|
||||
URL: a.RepoURL,
|
||||
Progress: os.Stdout,
|
||||
ReferenceName: plumbing.NewBranchReferenceName(branchName), // 指定克隆的分支
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clone repository: %v", err)
|
||||
}
|
||||
|
||||
a.Repository = repo
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckoutBranch 切换到指定分支
|
||||
func (a *AclGitManager) CheckoutBranch(branchName string) error {
|
||||
if a.Repository == nil {
|
||||
return fmt.Errorf("repository is not initialized, call Clone() first")
|
||||
}
|
||||
|
||||
worktree, err := a.Repository.Worktree()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get worktree: %v", err)
|
||||
}
|
||||
|
||||
// 确保分支存在
|
||||
ref := plumbing.NewBranchReferenceName(branchName)
|
||||
if _, err := a.Repository.Reference(ref, true); err != nil {
|
||||
return fmt.Errorf("branch '%s' does not exist: %v", branchName, err)
|
||||
}
|
||||
|
||||
// 切换到指定分支
|
||||
err = worktree.Checkout(&git.CheckoutOptions{
|
||||
Branch: ref,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to checkout branch '%s': %v", branchName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
src/entity/clash_config.go
Normal file
16
src/entity/clash_config.go
Normal file
@ -0,0 +1,16 @@
|
||||
package entity
|
||||
|
||||
// ClashConfig represents the main structure of the Clash configuration file
|
||||
type ClashConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
SocksPort int `yaml:"socks-port"`
|
||||
AllowLan bool `yaml:"allow-lan"`
|
||||
Mode string `yaml:"mode"`
|
||||
LogLevel string `yaml:"log-level"`
|
||||
ExternalController string `yaml:"external-controller"`
|
||||
Experimental ExperimentalOptions `yaml:"experimental"`
|
||||
Hosts map[string]string `yaml:"hosts"`
|
||||
Proxies []string `yaml:"proxies"`
|
||||
ProxyGroups []ProxyGroup `yaml:"proxy-groups"`
|
||||
Rules []string `yaml:"rules"`
|
||||
}
|
||||
6
src/entity/experimental.go
Normal file
6
src/entity/experimental.go
Normal file
@ -0,0 +1,6 @@
|
||||
package entity
|
||||
|
||||
// ExperimentalOptions represents the experimental section in the Clash config
|
||||
type ExperimentalOptions struct {
|
||||
IgnoreResolveFail bool `yaml:"ignore-resolve-fail"`
|
||||
}
|
||||
10
src/entity/proxy_group.go
Normal file
10
src/entity/proxy_group.go
Normal file
@ -0,0 +1,10 @@
|
||||
package entity
|
||||
|
||||
// ProxyGroup represents a proxy group in the Clash configuration
|
||||
type ProxyGroup struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Proxies []string `yaml:"proxies"`
|
||||
URL string `yaml:"url,omitempty"`
|
||||
Interval int `yaml:"interval,omitempty"`
|
||||
}
|
||||
77
src/example/example_config.go
Normal file
77
src/example/example_config.go
Normal file
@ -0,0 +1,77 @@
|
||||
package example
|
||||
|
||||
import (
|
||||
"proxyrules/src/entity"
|
||||
)
|
||||
|
||||
// GetExampleConfig generates a sample ClashConfig instance
|
||||
func GetExampleConfig() entity.ClashConfig {
|
||||
return entity.ClashConfig{
|
||||
Port: 7890,
|
||||
SocksPort: 7891,
|
||||
AllowLan: false,
|
||||
Mode: "Rule",
|
||||
LogLevel: "info",
|
||||
ExternalController: "127.0.0.1:9090",
|
||||
Experimental: entity.ExperimentalOptions{
|
||||
IgnoreResolveFail: true,
|
||||
},
|
||||
Hosts: map[string]string{
|
||||
"mtalk.google.com": "108.177.125.188",
|
||||
},
|
||||
Proxies: nil,
|
||||
ProxyGroups: []entity.ProxyGroup{
|
||||
{
|
||||
Name: "自动选择快速节点",
|
||||
Type: "url-test",
|
||||
Proxies: []string{"自由扩散", "协助扩散", "freebsd"},
|
||||
URL: "http://www.gstatic.com/generate_204",
|
||||
Interval: 300,
|
||||
},
|
||||
{
|
||||
Name: "PROXY",
|
||||
Type: "select",
|
||||
Proxies: []string{"自动选择快速节点", "自由扩散", "协助扩散", "freebsd", "Tokyo-该节点流量计费!", "Dallas", "Neon", "Neon-v6", "HK-v6", "保加利亚", "保加利亚-v6", "FR-hy2-v6", "FR-v2ray-v6", "FR-v2ray-cdn"},
|
||||
},
|
||||
{
|
||||
Name: "Final",
|
||||
Type: "select",
|
||||
Proxies: []string{"DIRECT", "PROXY", "freebsd"},
|
||||
},
|
||||
{
|
||||
Name: "Apple",
|
||||
Type: "select",
|
||||
Proxies: []string{"DIRECT", "PROXY", "freebsd"},
|
||||
},
|
||||
{
|
||||
Name: "GlobalMedia",
|
||||
Type: "select",
|
||||
Proxies: []string{"自由扩散", "协助扩散", "freebsd"},
|
||||
},
|
||||
{
|
||||
Name: "HKMTMedia",
|
||||
Type: "select",
|
||||
Proxies: []string{"DIRECT", "自由扩散", "协助扩散", "freebsd"},
|
||||
},
|
||||
},
|
||||
Rules: []string{
|
||||
"IP-CIDR,11.11.0.0/16,DIRECT",
|
||||
"DOMAIN-SUFFIX,*.hit.edu.cn,DIRECT",
|
||||
"DOMAIN-SUFFIX,hit.edu.cn,DIRECT",
|
||||
"DOMAIN-SUFFIX,*.fr.cherr.cc,DIRECT",
|
||||
"DOMAIN-SUFFIX,fr.cherr.cc,DIRECT",
|
||||
"DOMAIN-SUFFIX,aaronhu.cn,DIRECT",
|
||||
"DOMAIN-SUFFIX,*.aaronhu.cn,DIRECT",
|
||||
"DOMAIN-SUFFIX,metacubex.one,PROXY",
|
||||
"DOMAIN-SUFFIX,*.metacubex.one,PROXY",
|
||||
"DOMAIN-SUFFIX,cmliussss.com,PROXY",
|
||||
"DOMAIN-SUFFIX,*.cmliussss.com,PROXY",
|
||||
"DOMAIN-SUFFIX,hysteria.network,PROXY",
|
||||
"DOMAIN-SUFFIX,*.hysteria.network,PROXY",
|
||||
"DOMAIN-SUFFIX,googletraveladservices.com,DIRECT",
|
||||
"DOMAIN,dl.google.com,DIRECT",
|
||||
"DOMAIN,mtalk.google.com,DIRECT",
|
||||
"DOMAIN-SUFFIX,17gouwuba.com,REJECT",
|
||||
},
|
||||
}
|
||||
}
|
||||
255
src/main/main.go
Normal file
255
src/main/main.go
Normal file
@ -0,0 +1,255 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"proxyrules/src/service"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const htmlTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Edit Default List</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
button {
|
||||
margin-left: 10px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
#clipboard-message {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* 设置页面的样式 */
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 背景容器 */
|
||||
.background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('static/background.jpg') no-repeat center center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* 半透明白色覆盖层 */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.5); /* 半透明白色 */
|
||||
}
|
||||
|
||||
/* 页面内容 */
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1; /* 确保内容在覆盖层之上 */
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
color: black; /* 内容颜色 */
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
color: black; /* 内容颜色 */
|
||||
}
|
||||
.form-container {
|
||||
position: relative;
|
||||
max-width: 600px; /* 表单容器的最大宽度 */
|
||||
margin: 50px auto; /* 居中 */
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.7); /* 半透明白色背景 */
|
||||
border-radius: 15px; /* 圆角 */
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); /* 添加阴影 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="background"></div>
|
||||
|
||||
<!-- 覆盖层 -->
|
||||
<div class="overlay"></div>
|
||||
<div class="content">
|
||||
|
||||
<h1>Welcome to 404 Clash Config Site !~~</h1>
|
||||
<div class="form-container">
|
||||
<form method="POST" action="/update">
|
||||
<ul id="list">
|
||||
{{range $index, $line := .lines}}
|
||||
<li>
|
||||
<input type="text" name="lines" value="{{$line}}" />
|
||||
<button type="button" onclick="removeLine(this)">Remove</button>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<button type="button" onclick="addLine()">Add Line</button>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
<br>
|
||||
<button onclick="downloadClashConfig()">Download Clash Config</button>
|
||||
<button onclick="copyToClipboard()">Copy Clash Config to Clipboard</button>
|
||||
</div>
|
||||
<p id="clipboard-message" style="display: none;">Clash configuration copied to clipboard!</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function addLine() {
|
||||
const list = document.getElementById('list');
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = '<input type="text" name="lines" value="" /><button type="button" onclick="removeLine(this)">Remove</button>';
|
||||
list.appendChild(li);
|
||||
}
|
||||
|
||||
function removeLine(button) {
|
||||
const li = button.parentElement;
|
||||
li.remove();
|
||||
}
|
||||
|
||||
function downloadClashConfig() {
|
||||
window.location.href = "/download";
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
fetch('/clipboard')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
navigator.clipboard.writeText(data.content).then(() => {
|
||||
const message = document.getElementById('clipboard-message');
|
||||
message.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
message.style.display = 'none';
|
||||
}, 2000);
|
||||
});
|
||||
})
|
||||
.catch(err => console.error('Failed to copy:', err));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// 文件路径
|
||||
const defaultFilePath = "src/proxies/default.list"
|
||||
const clashConfigPath = "lufxy.yaml"
|
||||
|
||||
// 读取默认列表文件
|
||||
func readDefaultList() ([]string, error) {
|
||||
content, err := ioutil.ReadFile(defaultFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
// 去除空行
|
||||
var result []string
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 写入到默认列表文件
|
||||
func writeDefaultList(lines []string) error {
|
||||
content := strings.Join(lines, "\n")
|
||||
return ioutil.WriteFile(defaultFilePath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// 读取 Clash 配置文件
|
||||
func readClashConfig() (string, error) {
|
||||
content, err := ioutil.ReadFile(clashConfigPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
service.GenerateConfig()
|
||||
r := gin.Default()
|
||||
r.Static("/static", "./static") // 将 static 目录映射到 /static 路径
|
||||
|
||||
// 加载 HTML 模版
|
||||
r.SetHTMLTemplate(template.Must(template.New("index").Parse(htmlTemplate)))
|
||||
|
||||
// 显示文件内容和操作界面
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
lines, err := readDefaultList()
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("Error reading file: %v", err))
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "index", gin.H{"lines": lines})
|
||||
})
|
||||
|
||||
// 提交更新后的文件内容
|
||||
r.POST("/update", func(c *gin.Context) {
|
||||
formLines := c.PostFormArray("lines")
|
||||
if err := writeDefaultList(formLines); err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("Error writing file: %v", err))
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
})
|
||||
|
||||
// 下载 Clash 配置
|
||||
r.GET("/download", func(c *gin.Context) {
|
||||
if _, err := os.Stat(clashConfigPath); os.IsNotExist(err) {
|
||||
c.String(http.StatusNotFound, "Clash configuration file not found")
|
||||
return
|
||||
}
|
||||
c.FileAttachment(clashConfigPath, "lufxy.yaml")
|
||||
})
|
||||
|
||||
// 将 Clash 配置放入剪贴板
|
||||
r.GET("/clipboard", func(c *gin.Context) {
|
||||
config, err := readClashConfig()
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("Error reading Clash config: %v", err))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"content": config})
|
||||
})
|
||||
|
||||
// 启动服务
|
||||
r.Run(":8080") // 在 http://localhost:8080 提供服务
|
||||
}
|
||||
351
src/service/generate_config.go
Normal file
351
src/service/generate_config.go
Normal file
@ -0,0 +1,351 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"proxyrules/src/aclgit"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3" // YAML 序列化库
|
||||
"proxyrules/src/entity"
|
||||
"proxyrules/src/example"
|
||||
"proxyrules/src/util"
|
||||
)
|
||||
|
||||
func GenerateConfig() {
|
||||
repoManager := aclgit.NewAclGitManager("https://github.com/ACL4SSR/ACL4SSR.git", "./src/rules")
|
||||
|
||||
// 最大尝试次数
|
||||
maxAttempts := 3
|
||||
var err error
|
||||
|
||||
// 尝试克隆仓库
|
||||
for attempts := 1; attempts <= maxAttempts; attempts++ {
|
||||
fmt.Printf("Attempt %d to clone repository...\n", attempts)
|
||||
err = repoManager.Clone("master")
|
||||
if err == nil {
|
||||
fmt.Println("Clone completed successfully!")
|
||||
break
|
||||
}
|
||||
fmt.Printf("Clone attempt %d failed: %v\n", attempts, err)
|
||||
|
||||
// 如果还没到最大尝试次数,等待一段时间后再尝试
|
||||
if attempts < maxAttempts {
|
||||
fmt.Println("Retrying in 3 seconds...")
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果克隆失败,打印错误日志,但继续执行后续操作
|
||||
if err != nil {
|
||||
log.Printf("Failed to clone repository after %d attempts: %v", maxAttempts, err)
|
||||
} else {
|
||||
fmt.Println("Repository cloned successfully after retries!")
|
||||
}
|
||||
|
||||
// 无论克隆是否成功,继续执行后续操作
|
||||
fmt.Println("Proceeding with the next steps...")
|
||||
// 2. proxies阶段,读proxies,读取 proxies 文件夹中的 .list 文件
|
||||
proxyDir := filepath.Join("./src", "proxies")
|
||||
proxies, _, err := util.ReadAllListFiles(proxyDir, false)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read .list files from rules: %v", err)
|
||||
}
|
||||
|
||||
// 将所有 Proxies 的值(每个文件的内容)合并为一个切片
|
||||
var proxyList []string
|
||||
for _, values := range proxies {
|
||||
proxyList = append(proxyList, values...)
|
||||
}
|
||||
// 将所有 Proxies 的值(每个文件的内容)合并为一个切片
|
||||
// 打印合并后的 Proxies 切片
|
||||
fmt.Println("Proxies:", proxies)
|
||||
|
||||
// 3. rules阶段,读rules文件夹下的所有.list文件
|
||||
rulesDir := filepath.Join("./src", "rules")
|
||||
rules, requireChoseList, err := util.ReadAllListFiles(rulesDir, true)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read .list files from rules: %v", err)
|
||||
}
|
||||
|
||||
// 将所有 Rules 的值(每个文件的内容)合并为一个切片
|
||||
var rulesList []string
|
||||
for _, values := range rules {
|
||||
rulesList = append(rulesList, values...)
|
||||
}
|
||||
rulesList = dealNoResolve(rulesList)
|
||||
rulesList = dealUserAgent(rulesList)
|
||||
|
||||
// 打印合并后的 Rules 切片
|
||||
//fmt.Println("Rules:", rulesList)
|
||||
|
||||
// 使用 example 包中的示例配置
|
||||
config := example.GetExampleConfig()
|
||||
config.Proxies = proxyList
|
||||
// 获取 Rules
|
||||
// 创建 ProxyGroup
|
||||
var proxyGroups []entity.ProxyGroup
|
||||
proxyNameList := ExtractProxyNames(proxyList)
|
||||
proxyGroups = append(proxyGroups, entity.ProxyGroup{
|
||||
Name: "AutoSelect",
|
||||
Type: "url-test",
|
||||
Proxies: proxyNameList,
|
||||
URL: "http://www.gstatic.com/generate_204",
|
||||
Interval: 300,
|
||||
})
|
||||
|
||||
proxyGroups = append(proxyGroups, entity.ProxyGroup{
|
||||
Name: "PROXY",
|
||||
Type: "select",
|
||||
Proxies: append([]string{"AutoSelect"}, proxyNameList...),
|
||||
})
|
||||
proxyGroups = append(proxyGroups, entity.ProxyGroup{
|
||||
Name: "Final",
|
||||
Type: "select",
|
||||
Proxies: []string{"DIRECT", "PROXY"},
|
||||
})
|
||||
defaultDirect := []string{"Apple", "ChinaCompanyIp", "ChinaDomain", "ChinaIp", "ChinaIpV6", "ChinaMedia", "Download", "GoogleCN", "LocalAreaNetwork", "Microsoft", "OneDrive", "UnBan", "Xbox", "360", "4399", "58", "AccelerateDirectSites", "Alibaba", "Baidu", "Bilibili", "ByteDance", "CCTV", "CN", "ChinaDNS", "ChinaNet", "ChinaOneKeyLogin", "DiDi", "Douyu", "GoogleCNProxyIP", "Heytap", "HuaWei", "Iflytek", "Iqiyi", "JD", "Kingsoft", "Kuaishou", "Letv", "MGTVTV", "MI", "MOO", "Marketing", "Meitu", "NetEase", "NetEaseMusic", "PDD", "PPTV", "PrivateTracker", "PublicDirectCDN", "Samsung", "RemoteDesktop", "Sina", "SohuSogo", "SteamCN", "SteamRegionCheck", "Tencent", "TencentLolm", "TencentVideo", "Vip", "Wechat", "Ximalaya", "Xunlei", "Youku", "CherrDirect", "VPN"}
|
||||
defaultBAN := []string{"BanAD", "BanEasyList", "BanEasyListChina", "BanEasyPrivacy", "BanProgramAD", "MIUIPrivacy"}
|
||||
addedDirectProxyList := append([]string{"DIRECT", "PROXY"}, proxyNameList...)
|
||||
addedProxyList := append([]string{"PROXY", "DIRECT"}, proxyNameList...)
|
||||
banProxyList := append([]string{"REJECT", "PROXY", "DIRECT"}, proxyNameList...)
|
||||
// 添加 "DIRECT" 和 "PROXY"
|
||||
|
||||
requireChoseList = RemoveDuplicates(requireChoseList)
|
||||
for _, requirement := range requireChoseList {
|
||||
var proxyGroup entity.ProxyGroup
|
||||
if Contains(defaultDirect, requirement) {
|
||||
proxyGroup = entity.ProxyGroup{
|
||||
Name: requirement,
|
||||
Type: "select",
|
||||
Proxies: addedDirectProxyList,
|
||||
}
|
||||
} else if Contains(defaultBAN, requirement) {
|
||||
proxyGroup = entity.ProxyGroup{
|
||||
Name: requirement,
|
||||
Type: "select",
|
||||
Proxies: banProxyList,
|
||||
}
|
||||
} else {
|
||||
proxyGroup = entity.ProxyGroup{
|
||||
Name: requirement,
|
||||
Type: "select",
|
||||
Proxies: addedProxyList,
|
||||
}
|
||||
}
|
||||
proxyGroups = append(proxyGroups, proxyGroup)
|
||||
}
|
||||
orderedGroupNames := []string{"AutoSelect", "PROXY", "Final", "OpenAi", "Claude", "ClaudeAI", "Gemini", "Telegram", "Bilibili", "BilibiliHMT", "Steam", "SteamCN", "SteamRegionCheck", "Porn", "Pornhub", "Pixiv", "JetBrains", "PrivateTracker", "Microsoft", "Bing", "Apple", "AppleNews", "AppleTV", "Github", "Google", "GoogleCNProxyIP", "GoogleEarth", "GoogleFCM", "Youtube", "YoutubeMusic", "Tiktok", "Instagram", "Line", "LineTV", "Wikipedia", "Zoom", "Epic", "MIUIPrivacy", "MI"}
|
||||
proxyGroups = ReorderProxyGroups(proxyGroups, orderedGroupNames)
|
||||
// 更新配置中的 ProxyGroups
|
||||
config.ProxyGroups = proxyGroups
|
||||
rulesList = append(rulesList, "GEOIP,CN,DIRECT")
|
||||
rulesList = append(rulesList, "MATCH,Final")
|
||||
config.Rules = rulesList
|
||||
|
||||
// 打印生成的 ClashConfig(包括 ProxyGroups)
|
||||
//fmt.Println("Generated ClashConfig:")
|
||||
//printClashConfigAsYAML(config) // 打印 YAML
|
||||
// 将结果写入 result.yaml
|
||||
err = writeConfigToFile(config, "lufxy.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write config to file: %v", err)
|
||||
}
|
||||
fmt.Println("Configuration written to lufxy.yaml successfully!")
|
||||
}
|
||||
|
||||
// splitRule 分割规则字符串,例如 "RULE-TYPE,pattern,action" -> ["RULE-TYPE", "pattern", "action"]
|
||||
func splitRule(rule string) []string {
|
||||
return strings.Split(rule, ",")
|
||||
}
|
||||
|
||||
// printClashConfigAsYAML 序列化 ClashConfig 并打印为 YAML 格式
|
||||
func printClashConfigAsYAML(config entity.ClashConfig) {
|
||||
data, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to serialize ClashConfig to YAML: %v", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
func writeConfigToFile(config entity.ClashConfig, fileName string) error {
|
||||
data, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize config to YAML: %w", err)
|
||||
}
|
||||
|
||||
// 打开文件(如果文件不存在则创建)
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file %s: %w", fileName, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 写入 YAML 数据到文件
|
||||
_, err = file.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write data to file %s: %w", fileName, err)
|
||||
}
|
||||
ProcessFile(fileName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProcessFile(filePath string) error {
|
||||
// 打开文件(只读模式)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法打开文件: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 正则表达式,匹配单引号包裹的大括号内容
|
||||
re := regexp.MustCompile(`'(\{.*?\})'`)
|
||||
|
||||
// 用于存储处理后的文件内容
|
||||
var processedLines []string
|
||||
|
||||
// 逐行读取文件
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
// 替换匹配的内容,去掉两边的单引号
|
||||
processedLine := re.ReplaceAllString(line, `$1`)
|
||||
processedLines = append(processedLines, processedLine)
|
||||
}
|
||||
|
||||
// 检查读取过程中是否发生错误
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("读取文件时发生错误: %v", err)
|
||||
}
|
||||
|
||||
// 关闭当前文件以便后续写入
|
||||
file.Close()
|
||||
|
||||
// 打开同一个文件(写模式,覆盖内容)
|
||||
outputFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法写入文件: %v", err)
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
// 将处理后的内容写回文件
|
||||
writer := bufio.NewWriter(outputFile)
|
||||
for _, line := range processedLines {
|
||||
_, err := writer.WriteString(line + "\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入文件时发生错误: %v", err)
|
||||
}
|
||||
}
|
||||
writer.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExtractProxyNames(data []string) []string {
|
||||
// 定义正则表达式,匹配 name: "value"
|
||||
re := regexp.MustCompile(`name:\s*"(.*?)"`)
|
||||
var names []string
|
||||
|
||||
// 遍历每一行数据并匹配
|
||||
for _, line := range data {
|
||||
matches := re.FindStringSubmatch(line)
|
||||
if len(matches) > 1 { // matches[1] 是第一个捕获组
|
||||
names = append(names, matches[1])
|
||||
}
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func RemoveDuplicates(input []string) []string {
|
||||
// 使用 map 来记录已经存在的字符串
|
||||
seen := make(map[string]bool)
|
||||
var result []string
|
||||
|
||||
for _, value := range input {
|
||||
// 如果字符串没有出现过,则添加到结果切片
|
||||
if !seen[value] {
|
||||
result = append(result, value)
|
||||
seen[value] = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// dealNoResolve 对输入的字符串切片处理,将倒数第二项是 "no-resolve" 的项与倒数第一项调换位置
|
||||
func dealNoResolve(input []string) []string {
|
||||
// 遍历每一个字符串
|
||||
for i, str := range input {
|
||||
// 按逗号分隔,并去除每一项的前后空格
|
||||
parts := strings.Split(str, ",")
|
||||
for j := range parts {
|
||||
parts[j] = strings.TrimSpace(parts[j])
|
||||
}
|
||||
|
||||
// 检查是否至少有两项,且倒数第二项为 "no-resolve"
|
||||
if len(parts) >= 2 && parts[len(parts)-2] == "no-resolve" {
|
||||
// 调换倒数第二项和倒数第一项的位置
|
||||
parts[len(parts)-2], parts[len(parts)-1] = parts[len(parts)-1], parts[len(parts)-2]
|
||||
// 将修改后的字符串重新组合,并替换原来的字符串
|
||||
input[i] = strings.Join(parts, ",")
|
||||
}
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
func dealUserAgent(input []string) []string {
|
||||
output := []string{}
|
||||
// 遍历每一个字符串
|
||||
for i, str := range input {
|
||||
// 按逗号分隔,并去除每一项的前后空格
|
||||
parts := strings.Split(str, ",")
|
||||
for j := range parts {
|
||||
parts[j] = strings.TrimSpace(parts[j])
|
||||
}
|
||||
|
||||
if len(parts) >= 1 && parts[0] != "USER-AGENT" && parts[0] != "URL-REGEX" {
|
||||
output = append(output, input[i])
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
func Contains(slice []string, item string) bool {
|
||||
for _, element := range slice {
|
||||
if element == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func ReorderProxyGroups(proxyGroups []entity.ProxyGroup, nameOrder []string) []entity.ProxyGroup {
|
||||
// 创建一个用于存储新的 ProxyGroup 切片
|
||||
var orderedGroups []entity.ProxyGroup
|
||||
// 使用 map 记录已经处理过的 ProxyGroup 名称
|
||||
processed := make(map[string]bool)
|
||||
|
||||
// 按照 nameOrder 的顺序将对应的 ProxyGroup 添加到 orderedGroups
|
||||
for _, name := range nameOrder {
|
||||
for _, group := range proxyGroups {
|
||||
if group.Name == name {
|
||||
orderedGroups = append(orderedGroups, group)
|
||||
processed[name] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将剩余未处理的 ProxyGroup 添加到 orderedGroups
|
||||
for _, group := range proxyGroups {
|
||||
if !processed[group.Name] {
|
||||
orderedGroups = append(orderedGroups, group)
|
||||
}
|
||||
}
|
||||
|
||||
return orderedGroups
|
||||
}
|
||||
59
src/util/fileutils.go
Normal file
59
src/util/fileutils.go
Normal file
@ -0,0 +1,59 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReadFileAndAppendFileName 读取文件内容并将文件名追加到每行末尾
|
||||
func ReadFileAndAppendFileName(path string, fileName string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
// 获取文件名(去掉扩展名部分)
|
||||
fileBaseName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
// 如果行不为空,添加文件名
|
||||
if strings.TrimSpace(line) != "" {
|
||||
lines = append(lines, fmt.Sprintf("%s,%s", line, fileBaseName))
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read file content: %v", err)
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// ReadFile 读取文件内容并返回每一行的字符串切片
|
||||
func ReadFile(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read file content: %v", err)
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
79
src/util/filewalkers.go
Normal file
79
src/util/filewalkers.go
Normal file
@ -0,0 +1,79 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReadAllListFiles 遍历指定根目录下的所有 .list 文件,将文件内容按行读取并返回。
|
||||
// 每行会追加文件名(去掉扩展名),忽略以 # 开头的行和空行。
|
||||
func ReadAllListFiles(rootDir string, addFileName bool) (map[string][]string, []string, error) {
|
||||
listFiles := make(map[string][]string)
|
||||
fileNames := []string{}
|
||||
|
||||
// 遍历根目录下的所有文件
|
||||
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 只处理 .list 文件
|
||||
if !info.IsDir() && strings.HasSuffix(info.Name(), ".list") {
|
||||
content, err := readFilteredFile(path, info.Name(), addFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file '%s': %v", path, err)
|
||||
}
|
||||
|
||||
// 将文件内容存入 map,键为文件名,值为过滤后的文件内容
|
||||
listFiles[info.Name()] = content
|
||||
|
||||
// 保存文件名到切片(去掉扩展名部分)
|
||||
fileNames = append(fileNames, strings.TrimSuffix(info.Name(), filepath.Ext(info.Name())))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to walk directory: %v", err)
|
||||
}
|
||||
|
||||
return listFiles, fileNames, nil
|
||||
}
|
||||
|
||||
// readFilteredFile 读取文件内容,忽略以 # 开头的行和空行,并将文件名追加到每行末尾
|
||||
func readFilteredFile(filePath string, fileName string, addFilename bool) ([]string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
// 获取文件名(去掉扩展名部分)
|
||||
fileBaseName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text()) // 去除前后空格
|
||||
|
||||
// 忽略空行和以 # 开头的行
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if addFilename {
|
||||
// 将文件名追加到行末尾
|
||||
lines = append(lines, fmt.Sprintf("%s,%s", line, fileBaseName))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("%s", line))
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read file content: %v", err)
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
84
src/util/read_proxies.go
Normal file
84
src/util/read_proxies.go
Normal file
@ -0,0 +1,84 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReadProxies 读取并处理 proxies 文件夹下的所有 .list 文件。
|
||||
// 返回一个 map[string]interface{},键为文件名,值为解析后的内容。
|
||||
func ReadProxies(proxiesDir string) (map[string]interface{}, error) {
|
||||
// 调用 ReadAllListFiles,addFileName 设置为 false
|
||||
filesContent, _, err := ReadAllListFiles(proxiesDir, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read proxies files: %v", err)
|
||||
}
|
||||
|
||||
// 用于存储最终的 proxies 数据
|
||||
proxiesMap := make(map[string]interface{})
|
||||
|
||||
// 遍历每个文件的内容
|
||||
for fileName, lines := range filesContent {
|
||||
var fileProxies []map[string]interface{}
|
||||
|
||||
for _, line := range lines {
|
||||
// 将行内容解析为 JSON 格式
|
||||
var proxy map[string]interface{}
|
||||
err := parseProxyConfig(line, &proxy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse line in file '%s': %v", fileName, err)
|
||||
}
|
||||
|
||||
// 将解析后的数据添加到当前文件的代理列表中
|
||||
fileProxies = append(fileProxies, proxy)
|
||||
}
|
||||
|
||||
// 将文件的代理列表添加到最终结果中
|
||||
proxiesMap[fileName] = fileProxies
|
||||
}
|
||||
|
||||
return proxiesMap, nil
|
||||
}
|
||||
|
||||
// parseProxyConfig 将字符串解析为 map[string]interface{}
|
||||
func parseProxyConfig(proxyStr string, out *map[string]interface{}) error {
|
||||
// 检查字符串是否以 "{" 开头和以 "}" 结尾
|
||||
proxyStr = strings.TrimSpace(proxyStr)
|
||||
if !strings.HasPrefix(proxyStr, "{") || !strings.HasSuffix(proxyStr, "}") {
|
||||
return errors.New("invalid proxy string format, must start with '{' and end with '}'")
|
||||
}
|
||||
|
||||
// 使用正则表达式处理键值对
|
||||
re := regexp.MustCompile(`(\w+):\s*([^,{}]+|{[^}]*})`)
|
||||
matches := re.FindAllStringSubmatch(proxyStr, -1)
|
||||
|
||||
if matches == nil {
|
||||
return errors.New("failed to parse proxy string, no matches found")
|
||||
}
|
||||
|
||||
// 构造 map
|
||||
result := make(map[string]interface{})
|
||||
for _, match := range matches {
|
||||
key := match[1]
|
||||
value := strings.TrimSpace(match[2])
|
||||
|
||||
// 如果值是嵌套对象(以 { 开头和以 } 结尾),递归解析
|
||||
if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
|
||||
var nested map[string]interface{}
|
||||
err := parseProxyConfig(value, &nested)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse nested object for key '%s': %v", key, err)
|
||||
}
|
||||
result[key] = nested
|
||||
} else {
|
||||
// 如果值是字符串,移除可能的多余引号
|
||||
value = strings.Trim(value, `"`)
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
*out = result
|
||||
return nil
|
||||
}
|
||||
BIN
static/background.jpg
Normal file
BIN
static/background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
Loading…
x
Reference in New Issue
Block a user