1
0
mirror of https://github.com/nitezs/sub2clash.git synced 2024-12-23 11:14:41 -05:00

Merge pull request #23 from nitezs/dev

Dev
This commit is contained in:
Nite07 2024-03-13 17:34:19 +08:00 committed by GitHub
commit 822793b085
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 10857 additions and 10603 deletions

View File

@ -1,15 +1,15 @@
name: Build and Push to GHCR
name: Build and Push Docker
on:
push:
branches:
- dev
tags:
- '*'
- "*"
workflow_dispatch:
jobs:
build:
build-and-push:
runs-on: ubuntu-latest
steps:
@ -17,53 +17,50 @@ jobs:
uses: actions/checkout@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set tag name
id: set_tag
run: |
if [[ $GITHUB_REF == refs/heads/* ]]; then
echo "::set-output name=tag::$(echo $GITHUB_REF | cut -d'/' -f3)"
else
echo "::set-output name=tag::${{ github.ref_name }}"
fi
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Check if triggered by tag
id: check_tag
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "::set-output name=triggered_by_tag::true"
else
echo "::set-output name=triggered_by_tag::false"
fi
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/sub2clash
ghcr.io/${{ github.repository }}
- name: Build and push Docker image for dev branch
if: steps.check_tag.outputs.triggered_by_tag == 'false'
uses: docker/build-push-action@v2
- name: Prepare args
id: prep
run: |
if [[ "${{ github.event_name }}" == 'workflow_dispatch' ]]; then
VERSION="${{ github.sha }}"
fi
if [[ "$GITHUB_REF" == 'refs/heads/dev' ]]; then
VERSION="${{ github.sha }}"
fi
if [[ "$GITHUB_REF" == 'refs/tags/'* ]]; then
VERSION=$TAG_NAME
fi
echo "version=$VERSION" >> $GITHUB_ENV
- name: Set up Docker buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image to GHCR and Docker Hub
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
build-args: |
dev=true
version=${{ github.sha }}
build-args: version=${{ env.version }}
push: true
tags: ghcr.io/${{ github.repository }}:${{ steps.set_tag.outputs.tag }}
- name: Build and push Docker image for tags
if: steps.check_tag.outputs.triggered_by_tag == 'true'
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
build-args: |
dev=false
version=${{ steps.set_tag.outputs.tag }}
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ steps.set_tag.outputs.tag }}
ghcr.io/${{ github.repository }}:latest
platforms: linux/amd64,linux/arm,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,110 +0,0 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Build
run: |
LDFLAGS="-s -w -X config.Version=${{ github.ref_name }}"
# Linux
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-amd64 main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm64 main.go
# Darwin
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-amd64 main.go
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-arm64 main.go
# Windows
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-amd64.exe main.go
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-arm64.exe main.go
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
draft: false
prerelease: false
- name: Upload Release Asset (Linux amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-amd64
asset_name: sub2clash-linux-amd64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Linux arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-arm64
asset_name: sub2clash-linux-arm64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Darwin amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-darwin-amd64
asset_name: sub2clash-darwin-amd64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Darwin arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-darwin-arm64
asset_name: sub2clash-darwin-arm64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-windows-amd64.exe
asset_name: sub2clash-windows-amd64.exe
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-windows-arm64.exe
asset_name: sub2clash-windows-arm64.exe
asset_content_type: application/octet-stream

31
.github/workflows/goreleaser.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: Build and Release
on:
push:
tags:
- "*"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.21.5
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --rm-dist --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ test
logs
sub2clash.db
.env
.vscode/settings.json

View File

@ -1,17 +1,18 @@
#before:
# hooks:
# - go mod tidy
project_name: sub2clash
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- linux
- darwin
goarch:
- amd64
- arm64
- "386"
ldflags:
- -s -w -X sub2clash/config.Version={{ .Version }}
no_unique_dist_dir: true
binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
archives:
- format: binary

2
.vscode/launch.json vendored
View File

@ -7,7 +7,7 @@
"mode": "debug",
"program": "${workspaceFolder}/main.go",
"output": "${workspaceFolder}/dist/main.exe",
"buildFlags": "-ldflags '-X sub2clash/config.Dev=true -X sub2clash/config.Version=dev'"
"buildFlags": "-ldflags '-X sub2clash/config.Version=dev'"
}
]
}

View File

@ -1,37 +1,47 @@
# `/clash`, `/meta`
# `GET /clash`, `GET /meta`
获取 Clash/Clash.Meta 配置链接
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|--------------|--------|-------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接,可以在链接结尾加上`#名称`,来给订阅中的节点加上统一前缀(可以输入多个,用 `,` 分隔) |
| proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) |
| refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
| template | string | 否 | - | 外部模板链接或内部模板名称 |
| ruleProvider | string | 否 | - | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集使用的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到 MATCH 规则之前) |
| rule | string | 否 | - | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到 MATCH 规则之前) |
| autoTest | bool | 否 | `false` | 国家策略组是否自动测速 |
| lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
| sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc` |
| replace | string | 否 | - | 通过正则表达式重命名节点,格式 `[<ReplaceKey>,<ReplaceTo>],[<ReplaceKey>,<ReplaceTo>]...` |
| remove | string | 否 | - | 通过正则表达式删除节点 |
| nodeList | bool | 否 | `false` | 只输出节点 |
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
| ------------ | ------ | ------------------------ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接,可以在链接结尾加上`#名称`,来给订阅中的节点加上统一前缀(可以输入多个,用 `,` 分隔) |
| proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) |
| refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
| template | string | 否 | - | 外部模板链接或内部模板名称 |
| ruleProvider | string | 否 | - | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集使用的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到 MATCH 规则之前) |
| rule | string | 否 | - | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到 MATCH 规则之前) |
| autoTest | bool | 否 | `false` | 国家策略组是否自动测速 |
| lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
| sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc` |
| replace | string | 否 | - | 通过正则表达式重命名节点,格式 `[<ReplaceKey>,<ReplaceTo>],[<ReplaceKey>,<ReplaceTo>]...` |
| remove | string | 否 | - | 通过正则表达式删除节点 |
| nodeList | bool | 否 | `false` | 只输出节点 |
# `/short`
# `POST /short`
获取短链Content-Type 为 `application/json`
具体参考使用可以参考 [api\templates\index.html](api/static/index.html)
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|----------|--------|------|-----|------------------|
| url | string | 是 | - | 需要转换的 Query 参数部分 |
| password | string | 否 | - | 短链密码 |
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
| --------- | ------ | -------- | ------ | ------------------------- |
| url | string | 是 | - | 需要转换的 Query 参数部分 |
| password | string | 否 | - | 短链密码 |
# `/s/:hash`
# `GET /s/:hash`
短链跳转
`hash` 为动态路由参数,可以通过 `/short` 接口获取
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|----------|--------|------|-----|------|
| password | string | 否 | - | 短链密码 |
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
| ---------- | ------ | -------- | ------ | -------- |
| password | string | 否 | - | 短链密码 |
# `PUT /short`
更新短链Content-Type 为 `application/json`
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
| --------- | ------ | -------- | ------ | ------------------------- |
| url | string | 是 | - | 需要转换的 Query 参数部分 |
| password | string | 否 | - | 短链密码 |
| hash | string | 是 | - | 短链 hash |

View File

@ -11,10 +11,9 @@ RUN go mod download
# 获取参数
ARG version
ARG dev
# 使用 -ldflags 参数进行编译
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X sub2clash/config.Version=${version} -X sub2clash/config.Dev=${dev}" -o sub2clash main.go
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X sub2clash/config.Version=${version}" -o sub2clash main.go
FROM alpine:latest

View File

@ -5,40 +5,40 @@
## 特性
- 开箱即用的规则、策略组配置
- 自动根据节点名称按国家划分策略组
- 支持多订阅合并
- 支持添加自定义 Rule Provider、Rule
- 支持多种协议
- Shadowsocks
- ShadowsocksR
- Vmess
- Vless Clash.Meta
- Trojan
- Hysteria Clash.Meta
- Hysteria2 Clash.Meta
- 开箱即用的规则、策略组配置
- 自动根据节点名称按国家划分策略组
- 支持多订阅合并
- 支持添加自定义 Rule Provider、Rule
- 支持多种协议
- Shadowsocks
- ShadowsocksR
- Vmess
- Vless Clash.Meta
- Trojan
- Hysteria Clash.Meta
- Hysteria2 Clash.Meta
## 使用
### 运行
- [docker compose](./docker-compose.yml)
- 运行[二进制文件](https://github.com/nitezs/sub2clash/releases/latest)
- [docker compose](./docker-compose.yml)
- 运行[二进制文件](https://github.com/nitezs/sub2clash/releases/latest)
### 配置
可以通过编辑 .env 文件来修改默认配置docker 直接添加环境变量
| 变量名 | 说明 | 默认值 |
|-----------------------|----------------------------------------|-----------------------|
| PORT | 端口 | `8011` |
| META_TEMPLATE | 默认 meta 模板文件名 | `template_meta.yaml` |
| CLASH_TEMPLATE | 默认 clash 模板文件名 | `template_clash.yaml` |
| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` |
| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小byte | `1048576` |
| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
| 变量名 | 说明 | 默认值 |
| --------------------- | ---------------------------------------------- | --------------------- |
| PORT | 端口 | `8011` |
| META_TEMPLATE | 默认 meta 模板文件名 | `template_meta.yaml` |
| CLASH_TEMPLATE | 默认 clash 模板文件名 | `template_clash.yaml` |
| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` |
| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小byte | `1048576` |
| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` |
| SHORT_LINK_LENGTH | 短链长度 | `6` |
| SHORT_LINK_LENGTH | 短链长度 | `6` |
### API
@ -49,14 +49,14 @@
可以通过变量自定义模板中的策略组代理节点
解释的不太清楚,可以参考下方默认模板
- `<all>` 为添加所有节点
- `<countries>` 为添加所有国家策略组
- `<地区二位字母代码>` 为添加指定地区所有节点,例如 `<hk>` 将添加所有香港节点
- `<all>` 为添加所有节点
- `<countries>` 为添加所有国家策略组
- `<地区二位字母代码>` 为添加指定地区所有节点,例如 `<hk>` 将添加所有香港节点
#### 默认模板
- [Clash](./templates/template_clash.yaml)
- [Clash.Meta](./templates/template_meta.yaml)
- [Clash](./templates/template_clash.yaml)
- [Clash.Meta](./templates/template_meta.yaml)
## 已知问题

View File

@ -1,4 +1,4 @@
package controller
package handler
import (
"net/http"

View File

@ -1,4 +1,4 @@
package controller
package handler
import (
"crypto/sha256"
@ -74,7 +74,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
}
// 解析订阅
err = yaml.Unmarshal(data, &sub)
newProxies := make([]model.Proxy, 0)
var newProxies []model.Proxy
if err != nil {
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria)://")
if reg.Match(data) {
@ -157,10 +157,6 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
for i := range proxyList {
// 重命名匹配到的节点
for j, v := range replaceRegs {
if err != nil {
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
return nil, errors.New("replaceName 参数非法: " + err.Error())
}
if v.MatchString(proxyList[i].Name) {
proxyList[i].Name = v.ReplaceAllString(
proxyList[i].Name, query.ReplaceTo[j],
@ -200,7 +196,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
}
// 合并新节点和模板
MergeSubAndTemplate(temp, t)
MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
// 处理自定义规则
for _, v := range query.Rules {
if v.Prepend {
@ -233,7 +229,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
return temp, nil
}
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
// 只合并节点、策略组
// 统计所有国家策略组名称
var countryGroupNames []string
@ -284,5 +280,7 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
}
temp.ProxyGroups[i].Proxies = newProxies
}
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
if !igcg {
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
}
}

View File

@ -1,4 +1,4 @@
package controller
package handler
import (
_ "embed"

View File

@ -1,4 +1,4 @@
package controller
package handler
import (
"errors"
@ -70,7 +70,30 @@ func ShortLinkGenHandler(c *gin.Context) {
c.String(200, hash)
}
func ShortLinkGetHandler(c *gin.Context) {
func ShortLinkGetUrlHandler(c *gin.Context) {
var params validator.ShortLinkGetValidator
if err := c.ShouldBindQuery(&params); err != nil {
c.String(400, "参数错误: "+err.Error())
return
}
if strings.TrimSpace(params.Hash) == "" {
c.String(400, "参数错误")
return
}
var shortLink model.ShortLink
result := database.FindShortLinkByHash(params.Hash, &shortLink)
if result.Error != nil {
c.String(404, "未找到短链接")
return
}
if shortLink.Password != "" && shortLink.Password != params.Password {
c.String(403, "密码错误")
return
}
c.String(200, shortLink.Url)
}
func ShortLinkGetConfigHandler(c *gin.Context) {
// 获取动态路由
hash := c.Param("hash")
password := c.Query("password")
@ -107,3 +130,31 @@ func ShortLinkGetHandler(c *gin.Context) {
}
c.String(http.StatusOK, string(all))
}
func ShortLinkUpdateHandler(c *gin.Context) {
var params validator.ShortLinkUpdateValidator
if err := c.ShouldBind(&params); err != nil {
c.String(400, "参数错误: "+err.Error())
}
if strings.TrimSpace(params.Url) == "" {
c.String(400, "参数错误")
return
}
var shortLink model.ShortLink
result := database.FindShortLinkByHash(params.Hash, &shortLink)
if result.Error != nil {
c.String(404, "未找到短链接")
return
}
if shortLink.Password == "" {
c.String(403, "无法修改无密码短链接")
return
}
if shortLink.Password != params.Password {
c.String(403, "密码错误")
return
}
shortLink.Url = params.Url
database.SaveShortLink(&shortLink)
c.String(200, "更新成功")
}

View File

@ -5,7 +5,7 @@ import (
"html/template"
"log"
"net/http"
"sub2clash/api/controller"
"sub2clash/api/handler"
"sub2clash/config"
"sub2clash/middleware"
@ -45,22 +45,31 @@ func SetRoute(r *gin.Engine) {
)
r.GET(
"/clash", func(c *gin.Context) {
controller.SubmodHandler(c)
handler.SubmodHandler(c)
},
)
r.GET(
"/meta", func(c *gin.Context) {
controller.SubHandler(c)
},
)
r.POST(
"/short", func(c *gin.Context) {
controller.ShortLinkGenHandler(c)
handler.SubHandler(c)
},
)
r.GET(
"/s/:hash", func(c *gin.Context) {
controller.ShortLinkGetHandler(c)
handler.ShortLinkGetConfigHandler(c)
},
)
r.GET(
"/short", func(c *gin.Context) {
handler.ShortLinkGetUrlHandler(c)
})
r.POST(
"/short", func(c *gin.Context) {
handler.ShortLinkGenHandler(c)
},
)
r.PUT(
"/short", func(c *gin.Context) {
handler.ShortLinkUpdateHandler(c)
},
)
}

View File

@ -1,285 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>sub2clash</title>
<!-- Bootstrap CSS -->
<link
crossorigin="anonymous"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
rel="stylesheet"
/>
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" rel="stylesheet" />
<!-- Bootstrap JS -->
<script
crossorigin="anonymous"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
></script>
<script crossorigin="anonymous" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js"></script>
<script src="./static/index.js"></script>
<style>
.container {
max-width: 800px;
}
.btn-xs {
padding: 2px 2px; /* 调整内边距以减小按钮大小 */
font-size: 10px; /* 设置字体大小 */
line-height: 1.2; /* 调整行高 */
border-radius: 3px; /* 可选的边框半径调整 */
padding: 2px 2px;
/* 调整内边距以减小按钮大小 */
font-size: 10px;
/* 设置字体大小 */
line-height: 1.2;
/* 调整行高 */
border-radius: 3px;
/* 可选的边框半径调整 */
height: 25px;
width: 25px;
}
</style>
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="mb-4">
<h2>sub2clash</h2>
<span class="text-muted fst-italic"
>通用订阅链接转 Clash(Meta) 配置工具
<a
href="https://github.com/nitezs/sub2clash#clash-meta"
target="_blank"
>使用文档</a
></span
><br><span class="text-muted fst-italic"
>注意:本程序非纯前端程序,输入的订阅将被后端缓存,请确保您信任当前站点</span
>
</div>
<div class="container mt-5">
<div class="mb-4">
<h2>sub2clash</h2>
<span class="text-muted fst-italic">通用订阅链接转 Clash(Meta) 配置工具
<a href="https://github.com/nitezs/sub2clash#clash-meta" target="_blank">使用文档</a></span><br /><span
class="text-muted fst-italic">注意:本程序非纯前端程序,输入的订阅将被后端缓存,请确保您信任当前站点</span>
</div>
<!-- Input URL -->
<div class="form-group mb-5">
<label for="apiLink">解析链接:</label>
<div class="input-group mb-2">
<input
class="form-control"
id="urlInput"
type="text"
placeholder="通过生成的链接重新填写下方设置"
/>
<button
class="btn btn-primary"
onclick="parseInputURL()"
type="button"
>
解析
<!-- Input URL -->
<div class="form-group mb-5">
<label for="apiLink">解析链接:</label>
<div class="input-group mb-2">
<input class="form-control" id="urlInput" type="text" placeholder="通过生成的链接重新填写下方设置" />
<button class="btn btn-primary" onclick="parseInputURL()" type="button">
解析
</button>
</div>
</div>
<!-- API Endpoint -->
<div class="form-group mb-3">
<label for="endpoint">客户端类型:</label>
<select class="form-control" id="endpoint" name="endpoint">
<option value="clash">Clash</option>
<option value="meta">Clash.Meta</option>
</select>
</div>
<!-- Template -->
<div class="form-group mb-3">
<label for="template">模板链接或名称:</label>
<input class="form-control" id="template" name="template" placeholder="输入外部模板链接或内部模板名称(可选)" type="text" />
</div>
<!-- Subscription Link -->
<div class="form-group mb-3">
<label for="sub">订阅链接:</label>
<textarea class="form-control" id="sub" name="sub" placeholder="每行输入一个订阅链接" rows="5"></textarea>
</div>
<!-- Proxy Link -->
<div class="form-group mb-3">
<label for="proxy">节点分享链接:</label>
<textarea class="form-control" id="proxy" name="proxy" placeholder="每行输入一个节点分享链接" rows="5"></textarea>
</div>
<!-- Refresh -->
<div class="form-check mb-3">
<input class="form-check-input" id="refresh" name="refresh" type="checkbox" />
<label class="form-check-label" for="refresh">强制重新获取订阅</label>
</div>
<!-- Node List -->
<div class="form-check mb-3">
<input class="form-check-input" id="nodeList" name="nodeList" type="checkbox" />
<label class="form-check-label" for="nodeList">输出为 Node List</label>
</div>
<!-- Auto Test -->
<div class="form-check mb-3">
<input class="form-check-input" id="autoTest" name="autoTest" type="checkbox" />
<label class="form-check-label" for="autoTest">国家策略组自动测速</label>
</div>
<!-- Lazy -->
<div class="form-check mb-3">
<input class="form-check-input" id="lazy" name="lazy" type="checkbox" />
<label class="form-check-label" for="lazy">自动测速启用 lazy 模式</label>
</div>
<!-- IgnoreCountryGroup -->
<div class="form-check mb-3">
<input class="form-check-input" id="igcg" name="igcg" type="checkbox" />
<label class="form-check-label" for="igcg">不输出国家策略组</label>
</div>
<!-- Rule Provider -->
<div class="form-group mb-3" id="ruleProviderGroup">
<label>Rule Provider:</label>
<button class="btn btn-primary mb-1 btn-xs" onclick="addRuleProvider()" type="button">
+
</button>
</div>
<!-- Rule -->
<div class="form-group mb-3" id="ruleGroup">
<label>规则:</label>
<button class="btn btn-primary mb-1 btn-xs" onclick="addRule()" type="button">
+
</button>
</div>
<!-- Sort -->
<div class="form-group mb-3">
<label for="sort">国家策略组排序规则:</label>
<select class="form-control" id="sort" name="sort">
<option value="nameasc">名称(升序)</option>
<option value="namedesc">名称(降序)</option>
<option value="sizeasc">节点数量(升序)</option>
<option value="sizedesc">节点数量(降序)</option>
</select>
</div>
<!-- Remove -->
<div class="form-group mb-3">
<label for="remove">排除节点:</label>
<input class="form-control" type="text" name="remove" id="remove" placeholder="正则表达式" />
</div>
<!-- Rename -->
<div class="form-group mb-3" id="replaceGroup">
<label>节点名称替换:</label>
<button class="btn btn-primary mb-1 btn-xs" onclick="addReplace()" type="button">
+
</button>
</div>
</div>
<!-- API Endpoint -->
<div class="form-group mb-3">
<label for="endpoint">客户端类型:</label>
<select class="form-control" id="endpoint" name="endpoint">
<option value="clash">Clash</option>
<option value="meta">Clash.Meta</option>
</select>
</div>
<!-- Template -->
<div class="form-group mb-3">
<label for="template">模板链接或名称:</label>
<input
class="form-control"
id="template"
name="template"
placeholder="输入外部模板链接或内部模板名称(可选)"
type="text"
/>
</div>
<!-- Subscription Link -->
<div class="form-group mb-3">
<label for="sub">订阅链接:</label>
<textarea
class="form-control"
id="sub"
name="sub"
placeholder="每行输入一个订阅链接"
rows="5"
></textarea>
</div>
<!-- Proxy Link -->
<div class="form-group mb-3">
<label for="proxy">节点分享链接:</label>
<textarea
class="form-control"
id="proxy"
name="proxy"
placeholder="每行输入一个节点分享链接"
rows="5"
></textarea>
</div>
<!-- Refresh -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="refresh"
name="refresh"
type="checkbox"
/>
<label class="form-check-label" for="refresh"
>强制重新获取订阅</label
>
</div>
<!-- Node List -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="nodeList"
name="nodeList"
type="checkbox"
/>
<label class="form-check-label" for="nodeList"
>输出为 Node List</label
>
</div>
<!-- Auto Test -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="autoTest"
name="autoTest"
type="checkbox"
/>
<label class="form-check-label" for="autoTest"
>国家策略组自动测速</label
>
</div>
<!-- Lazy -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="lazy"
name="lazy"
type="checkbox"
/>
<label class="form-check-label" for="lazy"
>自动测速启用 lazy 模式</label
>
</div>
<!-- Rule Provider -->
<div class="form-group mb-3" id="ruleProviderGroup">
<label>Rule Provider:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addRuleProvider()"
type="button"
>
+
</button>
</div>
<!-- Rule -->
<div class="form-group mb-3" id="ruleGroup">
<label>规则:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addRule()"
type="button"
>
+
</button>
</div>
<!-- Sort -->
<div class="form-group mb-3">
<label for="sort">国家策略组排序规则:</label>
<select class="form-control" id="sort" name="sort">
<option value="nameasc">名称(升序)</option>
<option value="namedesc">名称(降序)</option>
<option value="sizeasc">节点数量(升序)</option>
<option value="sizedesc">节点数量(降序)</option>
</select>
</div>
<!-- Remove -->
<div class="form-group mb-3">
<label for="remove">排除节点:</label>
<input
class="form-control"
type="text"
name="remove"
id="remove"
placeholder="正则表达式"
/>
</div>
<!-- Rename -->
<div class="form-group mb-3" id="replaceGroup">
<label>节点名称替换:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addReplace()"
type="button"
>
+
</button>
</div>
<!-- Display the API Link -->
<div class="form-group mb-5">
<label for="apiLink">配置链接:</label>
<div class="input-group mb-2">
<input
class="form-control"
id="apiLink"
readonly
type="text"
/>
<button
class="btn btn-primary"
onclick="generateURL()"
type="button"
>
生成链接
</button>
<button
class="btn btn-primary"
onclick="copyToClipboard('apiLink',this)"
type="button"
>
复制链接
</button>
</div>
<div class="input-group">
<input
class="form-control"
id="apiShortLink"
readonly
type="text"
/>
<input
class="form-control"
id="password"
type="text"
placeholder="密码"
/>
<button
class="btn btn-primary"
onclick="generateShortLink()"
type="button"
>
生成短链
</button>
<button
class="btn btn-primary"
onclick="copyToClipboard('apiShortLink',this)"
type="button"
>
复制短链
</button>
<!-- Display the API Link -->
<div class="form-group mb-5">
<label for="apiLink">配置链接:</label>
<div class="input-group mb-2">
<input class="form-control" id="apiLink" type="text" placeholder="链接" />
<button class="btn btn-primary" onclick="copyToClipboard('apiLink',this)" type="button">
复制链接
</button>
</div>
<div class="input-group">
<input class="form-control" id="apiShortLink" type="text" placeholder="链接" />
<input class="form-control" id="password" type="text" placeholder="密码" />
<button class="btn btn-primary" onclick="generateShortLink()" type="button">
生成短链
</button>
<button class="btn btn-primary" onclick="updateShortLink()" type="button">
更新短链
</button>
<button class="btn btn-primary" onclick="copyToClipboard('apiShortLink',this)" type="button">
复制短链
</button>
</div>
</div>
<!-- footer-->
<footer>
<p class="text-center">
Powered by
<a class="link-primary" href="https://github.com/nitezs/sub2clash">sub2clash</a>
</p>
<p class="text-center">Version {{.Version}}</p>
</footer>
</div>
<!-- footer-->
<footer>
<p class="text-center">
Powered by
<a
class="link-primary"
href="https://github.com/nitezs/sub2clash"
>sub2clash</a
>
</p>
<p class="text-center">Version {{.Version}}</p>
</footer>
</div>
</body>
</html>
<script src="./static/index.js"></script>
</html>

View File

@ -1,335 +1,344 @@
function clearExistingValues() {
// 清除简单输入框和复选框的值
document.getElementById("endpoint").value = "clash";
document.getElementById("sub").value = "";
document.getElementById("proxy").value = "";
document.getElementById("refresh").checked = false;
document.getElementById("autoTest").checked = false;
document.getElementById("lazy").checked = false;
document.getElementById("template").value = "";
document.getElementById("sort").value = "nameasc";
document.getElementById("remove").value = "";
document.getElementById("apiLink").value = "";
document.getElementById("apiShortLink").value = "";
document.getElementById("password").value = "";
document.getElementById("nodeList").checked = false;
// 清除简单输入框和复选框的值
document.getElementById("endpoint").value = "clash";
document.getElementById("sub").value = "";
document.getElementById("proxy").value = "";
document.getElementById("refresh").checked = false;
document.getElementById("autoTest").checked = false;
document.getElementById("lazy").checked = false;
document.getElementById("igcg").checked = false;
document.getElementById("template").value = "";
document.getElementById("sort").value = "nameasc";
document.getElementById("remove").value = "";
document.getElementById("apiLink").value = "";
document.getElementById("apiShortLink").value = "";
document.getElementById("password").value = "";
document.getElementById("nodeList").checked = false;
// 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组
clearInputGroup("ruleProviderGroup");
clearInputGroup("replaceGroup");
clearInputGroup("ruleGroup");
// 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组
clearInputGroup("ruleProviderGroup");
clearInputGroup("replaceGroup");
clearInputGroup("ruleGroup");
}
function generateURI() {
const queryParams = [];
const queryParams = [];
// 获取 API Endpoint
const endpoint = document.getElementById("endpoint").value;
// 获取 API Endpoint
const endpoint = document.getElementById("endpoint").value;
// 获取并组合订阅链接
let subLines = document
.getElementById("sub")
.value.split("\n")
.filter((line) => line.trim() !== "");
let noSub = false;
// 去除 subLines 中空元素
subLines = subLines.map((item) => {
if (item !== "") {
return item;
}
});
if (subLines.length > 0) {
queryParams.push(`sub=${encodeURIComponent(subLines.join(","))}`);
} else {
noSub = true;
// 获取并组合订阅链接
let subLines = document
.getElementById("sub")
.value.split("\n")
.filter((line) => line.trim() !== "");
let noSub = false;
// 去除 subLines 中空元素
subLines = subLines.map((item) => {
if (item !== "") {
return item;
}
});
if (subLines.length > 0) {
queryParams.push(`sub=${encodeURIComponent(subLines.join(","))}`);
} else {
noSub = true;
}
// 获取并组合节点分享链接
let proxyLines = document
.getElementById("proxy")
.value.split("\n")
.filter((line) => line.trim() !== "");
let noProxy = false;
// 去除 proxyLines 中空元素
proxyLines = proxyLines.map((item) => {
if (item !== "") {
return item;
}
});
if (proxyLines.length > 0) {
queryParams.push(`proxy=${encodeURIComponent(proxyLines.join(","))}`);
} else {
noProxy = true;
// 获取并组合节点分享链接
let proxyLines = document
.getElementById("proxy")
.value.split("\n")
.filter((line) => line.trim() !== "");
let noProxy = false;
// 去除 proxyLines 中空元素
proxyLines = proxyLines.map((item) => {
if (item !== "") {
return item;
}
if (noSub && noProxy) {
alert("订阅链接和节点分享链接不能同时为空!");
});
if (proxyLines.length > 0) {
queryParams.push(`proxy=${encodeURIComponent(proxyLines.join(","))}`);
} else {
noProxy = true;
}
if (noSub && noProxy) {
// alert("订阅链接和节点分享链接不能同时为空!");
return "";
}
// 获取复选框的值
const refresh = document.getElementById("refresh").checked;
queryParams.push(`refresh=${refresh ? "true" : "false"}`);
const autoTest = document.getElementById("autoTest").checked;
queryParams.push(`autoTest=${autoTest ? "true" : "false"}`);
const lazy = document.getElementById("lazy").checked;
queryParams.push(`lazy=${lazy ? "true" : "false"}`);
const nodeList = document.getElementById("nodeList").checked;
queryParams.push(`nodeList=${nodeList ? "true" : "false"}`);
const igcg = document.getElementById("igcg").checked;
queryParams.push(`ignoreCountryGroup=${igcg ? "true" : "false"}`);
// 获取模板链接或名称(如果存在)
const template = document.getElementById("template").value;
if (template.trim() !== "") {
queryParams.push(`template=${encodeURIComponent(template)}`);
}
// 获取Rule Provider和规则
const ruleProviders = document.getElementsByName("ruleProvider");
const rules = document.getElementsByName("rule");
let providers = [];
for (let i = 0; i < ruleProviders.length / 5; i++) {
let baseIndex = i * 5;
let behavior = ruleProviders[baseIndex].value;
let url = ruleProviders[baseIndex + 1].value;
let group = ruleProviders[baseIndex + 2].value;
let prepend = ruleProviders[baseIndex + 3].value;
let name = ruleProviders[baseIndex + 4].value;
// 是否存在空值
if (
behavior.trim() === "" ||
url.trim() === "" ||
group.trim() === "" ||
prepend.trim() === "" ||
name.trim() === ""
) {
// alert("Rule Provider 中存在空值,请检查后重试!");
return "";
}
providers.push(`[${behavior},${url},${group},${prepend},${name}]`);
}
queryParams.push(`ruleProvider=${encodeURIComponent(providers.join(","))}`);
let ruleList = [];
for (let i = 0; i < rules.length / 3; i++) {
if (rules[i * 3].value.trim() !== "") {
let rule = rules[i * 3].value;
let prepend = rules[i * 3 + 1].value;
let group = rules[i * 3 + 2].value;
// 是否存在空值
if (rule.trim() === "" || prepend.trim() === "" || group.trim() === "") {
// alert("Rule 中存在空值,请检查后重试!");
return "";
}
ruleList.push(`[${rule},${prepend},${group}]`);
}
// 获取复选框的值
const refresh = document.getElementById("refresh").checked;
queryParams.push(`refresh=${refresh ? "true" : "false"}`);
const autoTest = document.getElementById("autoTest").checked;
queryParams.push(`autoTest=${autoTest ? "true" : "false"}`);
const lazy = document.getElementById("lazy").checked;
queryParams.push(`lazy=${lazy ? "true" : "false"}`);
const nodeList = document.getElementById("nodeList").checked;
queryParams.push(`nodeList=${nodeList ? "true" : "false"}`);
}
queryParams.push(`rule=${encodeURIComponent(ruleList.join(","))}`);
// 获取模板链接或名称(如果存在)
const template = document.getElementById("template").value;
if (template.trim() !== "") {
queryParams.push(`template=${encodeURIComponent(template)}`);
// 获取排序策略
const sort = document.getElementById("sort").value;
queryParams.push(`sort=${sort}`);
// 获取删除节点的正则表达式
const remove = document.getElementById("remove").value;
if (remove.trim() !== "") {
queryParams.push(`remove=${encodeURIComponent(remove)}`);
}
// 获取替换节点名称的正则表达式
let replaceList = [];
const replaces = document.getElementsByName("replace");
for (let i = 0; i < replaces.length / 2; i++) {
let replaceStr = `<${replaces[i * 2].value}>`;
let replaceTo = `<${replaces[i * 2 + 1].value}>`;
if (replaceStr.trim() === "") {
// alert("重命名设置中存在空值,请检查后重试!");
return "";
}
replaceList.push(`[${replaceStr},${replaceTo}]`);
}
queryParams.push(`replace=${encodeURIComponent(replaceList.join(","))}`);
// 获取Rule Provider和规则
const ruleProviders = document.getElementsByName("ruleProvider");
const rules = document.getElementsByName("rule");
let providers = [];
for (let i = 0; i < ruleProviders.length / 5; i++) {
let baseIndex = i * 5;
let behavior = ruleProviders[baseIndex].value;
let url = ruleProviders[baseIndex + 1].value;
let group = ruleProviders[baseIndex + 2].value;
let prepend = ruleProviders[baseIndex + 3].value;
let name = ruleProviders[baseIndex + 4].value;
// 是否存在空值
if (
behavior.trim() === "" ||
url.trim() === "" ||
group.trim() === "" ||
prepend.trim() === "" ||
name.trim() === ""
) {
alert("Rule Provider 中存在空值,请检查后重试!");
return "";
}
providers.push(`[${behavior},${url},${group},${prepend},${name}]`);
}
queryParams.push(`ruleProvider=${encodeURIComponent(providers.join(","))}`);
let ruleList = [];
for (let i = 0; i < rules.length / 3; i++) {
if (rules[i * 3].value.trim() !== "") {
let rule = rules[i * 3].value;
let prepend = rules[i * 3 + 1].value;
let group = rules[i * 3 + 2].value;
// 是否存在空值
if (
rule.trim() === "" ||
prepend.trim() === "" ||
group.trim() === ""
) {
alert("Rule 中存在空值,请检查后重试!");
return "";
}
ruleList.push(`[${rule},${prepend},${group}]`);
}
}
queryParams.push(`rule=${encodeURIComponent(ruleList.join(","))}`);
// 获取排序策略
const sort = document.getElementById("sort").value;
queryParams.push(`sort=${sort}`);
// 获取删除节点的正则表达式
const remove = document.getElementById("remove").value;
if (remove.trim() !== "") {
queryParams.push(`remove=${encodeURIComponent(remove)}`);
}
// 获取替换节点名称的正则表达式
let replaceList = [];
const replaces = document.getElementsByName("replace");
for (let i = 0; i < replaces.length / 2; i++) {
let replaceStr = `<${replaces[i * 2].value}>`;
let replaceTo = `<${replaces[i * 2 + 1].value}>`;
if (replaceStr.trim() === "") {
alert("重命名设置中存在空值,请检查后重试!");
return "";
}
replaceList.push(`[${replaceStr},${replaceTo}]`);
}
queryParams.push(`replace=${encodeURIComponent(replaceList.join(","))}`);
return `${endpoint}?${queryParams.join("&")}`;
return `${endpoint}?${queryParams.join("&")}`;
}
// 将输入框中的 URL 解析为参数
function parseInputURL() {
// 获取输入框中的 URL
const inputURL = document.getElementById("urlInput").value;
async function parseInputURL() {
// 获取输入框中的 URL
const inputURL = document.getElementById("urlInput").value;
// 清除现有的输入框值
clearExistingValues();
if (!inputURL) {
alert("请输入有效的链接!");
return;
}
if (!inputURL) {
alert("请输入有效的链接!");
return;
}
let url;
let url;
try {
url = new URL(inputURL);
} catch (_) {
alert("无效的链接!");
return;
}
if (url.pathname.includes("/s/")) {
let hash = url.pathname.substring(url.pathname.lastIndexOf("/s/") + 3);
let q = new URLSearchParams();
let password = url.searchParams.get("password");
q.append("hash", hash);
q.append("password", password);
try {
url = new URL(inputURL);
} catch (_) {
alert("无效的链接!");
return;
const response = await axios.get("./short?" + q.toString());
url = new URL(window.location.href + response.data);
document.querySelector("#apiShortLink").value = inputURL;
document.querySelector("#password").value = password;
} catch (error) {
console.log(error);
alert("获取短链失败,请检查密码!");
}
}
let params = new URLSearchParams(url.search);
// 清除现有的输入框值
clearExistingValues();
// 分配值到对应的输入框
const pathSections = url.pathname.split("/");
const lastSection = pathSections[pathSections.length - 1];
const clientTypeSelect = document.getElementById("endpoint");
switch (lastSection.toLowerCase()) {
case "meta":
clientTypeSelect.value = "meta";
break;
case "clash":
default:
clientTypeSelect.value = "clash";
break;
}
// 获取查询参数
const params = new URLSearchParams(url.search);
if (params.has("sub")) {
document.getElementById("sub").value = decodeURIComponent(params.get("sub"))
.split(",")
.join("\n");
}
// 分配值到对应的输入框
const pathSections = url.pathname.split("/");
const lastSection = pathSections[pathSections.length - 1];
const clientTypeSelect = document.getElementById("endpoint");
switch (lastSection.toLowerCase()) {
case "meta":
clientTypeSelect.value = "meta";
break;
case "clash":
default:
clientTypeSelect.value = "clash";
break;
}
if (params.has("proxy")) {
document.getElementById("proxy").value = decodeURIComponent(
params.get("proxy")
)
.split(",")
.join("\n");
}
if (params.has("sub")) {
document.getElementById("sub").value = decodeURIComponent(
params.get("sub")
)
.split(",")
.join("\n");
}
if (params.has("refresh")) {
document.getElementById("refresh").checked =
params.get("refresh") === "true";
}
if (params.has("proxy")) {
document.getElementById("proxy").value = decodeURIComponent(
params.get("proxy")
)
.split(",")
.join("\n");
}
if (params.has("autoTest")) {
document.getElementById("autoTest").checked =
params.get("autoTest") === "true";
}
if (params.has("refresh")) {
document.getElementById("refresh").checked =
params.get("refresh") === "true";
}
if (params.has("lazy")) {
document.getElementById("lazy").checked = params.get("lazy") === "true";
}
if (params.has("autoTest")) {
document.getElementById("autoTest").checked =
params.get("autoTest") === "true";
}
if (params.has("template")) {
document.getElementById("template").value = decodeURIComponent(
params.get("template")
);
}
if (params.has("lazy")) {
document.getElementById("lazy").checked = params.get("lazy") === "true";
}
if (params.has("sort")) {
document.getElementById("sort").value = params.get("sort");
}
if (params.has("template")) {
document.getElementById("template").value = decodeURIComponent(
params.get("template")
);
}
if (params.has("remove")) {
document.getElementById("remove").value = decodeURIComponent(
params.get("remove")
);
}
if (params.has("sort")) {
document.getElementById("sort").value = params.get("sort");
}
if (params.has("replace")) {
parseAndFillReplaceParams(decodeURIComponent(params.get("replace")));
}
if (params.has("remove")) {
document.getElementById("remove").value = decodeURIComponent(
params.get("remove")
);
}
if (params.has("ruleProvider")) {
parseAndFillRuleProviderParams(
decodeURIComponent(params.get("ruleProvider"))
);
}
if (params.has("replace")) {
parseAndFillReplaceParams(decodeURIComponent(params.get("replace")));
}
if (params.has("rule")) {
parseAndFillRuleParams(decodeURIComponent(params.get("rule")));
}
if (params.has("ruleProvider")) {
parseAndFillRuleProviderParams(
decodeURIComponent(params.get("ruleProvider"))
);
}
if (params.has("rule")) {
parseAndFillRuleParams(decodeURIComponent(params.get("rule")));
}
if (params.has("nodeList")) {
document.getElementById("nodeList").checked =
params.get("nodeList") === "true";
}
if (params.has("nodeList")) {
document.getElementById("nodeList").checked =
params.get("nodeList") === "true";
}
}
function clearInputGroup(groupId) {
// 清空第二个之后的child
const group = document.getElementById(groupId);
while (group.children.length > 2) {
group.removeChild(group.lastChild);
}
// 清空第二个之后的child
const group = document.getElementById(groupId);
while (group.children.length > 2) {
group.removeChild(group.lastChild);
}
}
function parseAndFillReplaceParams(replaceParams) {
const replaceGroup = document.getElementById("replaceGroup");
let matches;
const regex = /\[(<.*?>),(<.*?>)\]/g;
const str = decodeURIComponent(replaceParams);
while ((matches = regex.exec(str)) !== null) {
const div = createReplace();
const original = matches[1].slice(1, -1); // Remove < and >
const replacement = matches[2].slice(1, -1); // Remove < and >
const replaceGroup = document.getElementById("replaceGroup");
let matches;
const regex = /\[(<.*?>),(<.*?>)\]/g;
const str = decodeURIComponent(replaceParams);
while ((matches = regex.exec(str)) !== null) {
const div = createReplace();
const original = matches[1].slice(1, -1); // Remove < and >
const replacement = matches[2].slice(1, -1); // Remove < and >
div.children[0].value = original;
div.children[1].value = replacement;
replaceGroup.appendChild(div);
}
div.children[0].value = original;
div.children[1].value = replacement;
replaceGroup.appendChild(div);
}
}
function parseAndFillRuleProviderParams(ruleProviderParams) {
const ruleProviderGroup = document.getElementById("ruleProviderGroup");
let matches;
const regex = /\[(.*?),(.*?),(.*?),(.*?),(.*?)\]/g;
const str = decodeURIComponent(ruleProviderParams);
while ((matches = regex.exec(str)) !== null) {
const div = createRuleProvider();
div.children[0].value = matches[1];
div.children[1].value = matches[2];
div.children[2].value = matches[3];
div.children[3].value = matches[4];
div.children[4].value = matches[5];
ruleProviderGroup.appendChild(div);
}
const ruleProviderGroup = document.getElementById("ruleProviderGroup");
let matches;
const regex = /\[(.*?),(.*?),(.*?),(.*?),(.*?)\]/g;
const str = decodeURIComponent(ruleProviderParams);
while ((matches = regex.exec(str)) !== null) {
const div = createRuleProvider();
div.children[0].value = matches[1];
div.children[1].value = matches[2];
div.children[2].value = matches[3];
div.children[3].value = matches[4];
div.children[4].value = matches[5];
ruleProviderGroup.appendChild(div);
}
}
function parseAndFillRuleParams(ruleParams) {
const ruleGroup = document.getElementById("ruleGroup");
let matches;
const regex = /\[(.*?),(.*?),(.*?)\]/g;
const str = decodeURIComponent(ruleParams);
while ((matches = regex.exec(str)) !== null) {
const div = createRule();
div.children[0].value = matches[1];
div.children[1].value = matches[2];
div.children[2].value = matches[3];
ruleGroup.appendChild(div);
}
const ruleGroup = document.getElementById("ruleGroup");
let matches;
const regex = /\[(.*?),(.*?),(.*?)\]/g;
const str = decodeURIComponent(ruleParams);
while ((matches = regex.exec(str)) !== null) {
const div = createRule();
div.children[0].value = matches[1];
div.children[1].value = matches[2];
div.children[2].value = matches[3];
ruleGroup.appendChild(div);
}
}
async function copyToClipboard(elem, e) {
const apiLinkInput = document.querySelector(`#${elem}`).value;
try {
await navigator.clipboard.writeText(apiLinkInput);
let text = e.textContent;
e.addEventListener("mouseout", function () {
e.textContent = text;
});
e.textContent = "复制成功";
} catch (err) {
console.error("复制到剪贴板失败:", err);
}
const apiLinkInput = document.querySelector(`#${elem}`).value;
try {
await navigator.clipboard.writeText(apiLinkInput);
let text = e.textContent;
e.addEventListener("mouseout", function () {
e.textContent = text;
});
e.textContent = "复制成功";
} catch (err) {
console.error("复制到剪贴板失败:", err);
}
}
function createRuleProvider() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="ruleProvider" placeholder="Behavior">
<input type="text" class="form-control" name="ruleProvider" placeholder="Url">
<input type="text" class="form-control" name="ruleProvider" placeholder="Group">
@ -337,85 +346,150 @@ function createRuleProvider() {
<input type="text" class="form-control" name="ruleProvider" placeholder="Name">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
return div;
}
function createReplace() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="replace" placeholder="原字符串(正则表达式)">
<input type="text" class="form-control" name="replace" placeholder="替换为(可为空)">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
return div;
}
function createRule() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="rule" placeholder="Rule">
<input type="text" class="form-control" name="rule" placeholder="Prepend">
<input type="text" class="form-control" name="rule" placeholder="Group">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
return div;
}
function listenInput() {
let selectElements = document.querySelectorAll("select");
let inputElements = document.querySelectorAll("input");
let textAreaElements = document.querySelectorAll("textarea");
inputElements.forEach(function (element) {
element.addEventListener("input", function () {
generateURL();
});
});
textAreaElements.forEach(function (element) {
element.addEventListener("input", function () {
generateURL();
});
});
selectElements.forEach(function (element) {
element.addEventListener("change", function () {
generateURL();
});
});
}
function addRuleProvider() {
const div = createRuleProvider();
document.getElementById("ruleProviderGroup").appendChild(div);
const div = createRuleProvider();
document.getElementById("ruleProviderGroup").appendChild(div);
listenInput();
}
function addRule() {
const div = createRule();
document.getElementById("ruleGroup").appendChild(div);
const div = createRule();
document.getElementById("ruleGroup").appendChild(div);
listenInput();
}
function addReplace() {
const div = createReplace();
document.getElementById("replaceGroup").appendChild(div);
const div = createReplace();
document.getElementById("replaceGroup").appendChild(div);
listenInput();
}
function removeElement(button) {
button.parentElement.remove();
button.parentElement.remove();
}
function generateURL() {
const apiLink = document.getElementById("apiLink");
let uri = generateURI();
if (uri === "") {
return;
}
apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`;
const apiLink = document.getElementById("apiLink");
let uri = generateURI();
if (uri === "") {
return;
}
apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`;
}
function generateShortLink() {
const apiShortLink = document.getElementById("apiShortLink");
const password = document.getElementById("password");
let uri = generateURI();
if (uri === "") {
return;
}
axios
.post(
"./short",
{
url: uri,
password: password.value.trim(),
},
{
headers: {
"Content-Type": "application/json",
},
}
)
.then((response) => {
apiShortLink.value = `${window.location.origin}${window.location.pathname}s/${response.data}`;
})
.catch((error) => {
console.log(error);
alert("生成短链失败,请重试!");
});
const apiShortLink = document.getElementById("apiShortLink");
const password = document.getElementById("password");
let uri = generateURI();
if (uri === "") {
return;
}
axios
.post(
"./short",
{
url: uri,
password: password.value.trim(),
},
{
headers: {
"Content-Type": "application/json",
},
}
)
.then((response) => {
apiShortLink.value = `${window.location.origin}${window.location.pathname}s/${response.data}`;
})
.catch((error) => {
console.log(error);
alert("生成短链失败,请重试!");
});
}
function updateShortLink() {
const password = document.getElementById("password");
const apiShortLink = document.getElementById("apiShortLink");
let hash = apiShortLink.value;
if (hash.startsWith("http")) {
let u = new URL(hash);
hash = u.pathname.substring(u.pathname.lastIndexOf("/s/") + 3);
}
if (password.value.trim() === "") {
alert("请输入密码!");
return;
}
let uri = generateURI();
if (uri === "") {
return;
}
axios
.put(
"./short",
{
hash: hash,
url: uri,
password: password.value.trim(),
},
{
headers: {
"Content-Type": "application/json",
},
}
)
.then((response) => {
alert("更新短链成功!");
})
.catch((error) => {
console.log(error);
alert(error.response.data);
});
}
listenInput();

View File

@ -3,6 +3,7 @@ package config
import (
"errors"
"os"
"regexp"
"strconv"
"github.com/joho/godotenv"
@ -24,6 +25,15 @@ var Default *Config
var Version string
var Dev string
func init() {
reg := regexp.MustCompile(`^v\d+\.\d+\.\d+$`)
if reg.MatchString(Version) {
Dev = "false"
} else {
Dev = "true"
}
}
func LoadConfig() error {
Default = &Config{
MetaTemplate: "template_meta.yaml",

56
go.mod
View File

@ -4,48 +4,54 @@ go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.9.0
github.com/glebarez/sqlite v1.10.0
github.com/joho/godotenv v1.5.1
go.uber.org/zap v1.26.0
golang.org/x/text v0.13.0
go.uber.org/zap v1.27.0
golang.org/x/text v0.14.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.4
gorm.io/gorm v1.25.7
)
require (
github.com/bytedance/sonic v1.10.1 // indirect
github.com/bytedance/sonic v1.11.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/glebarez/go-sqlite v1.22.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.15.4 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/knz/go-libedit v1.10.1 // 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.1.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.44.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.29.4 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

62
go.sum
View File

@ -2,12 +2,16 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -15,14 +19,20 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -31,17 +41,25 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@ -53,18 +71,29 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@ -85,29 +114,46 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@ -115,13 +161,29 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/libc v1.44.0 h1:71bbnKgb0mCg7GOOI/PHlzz7Bv6obELGNKnIEeowX8c=
modernc.org/libc v1.44.0/go.mod h1:RRqfGVjvILF5AdNP3RPCiihj7+Dn2pIBrdlU60lA9vs=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
modernc.org/sqlite v1.29.4 h1:mbvQTJ3Tl5Vz+wLA6z8hdBFSeNQ0XXQ+KVwn8NkUliw=
modernc.org/sqlite v1.29.4/go.mod h1:MjUIBKZ+tU/lqjNLbVAAMjsQPdWdA/ciwdhsT9kBwk8=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -56,6 +56,9 @@ type Proxy struct {
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
Ports string `yaml:"ports,omitempty"`
AuthStringOLD string `yaml:"auth_str,omitempty"`
AuthString string `yaml:"auth-str,omitempty"`
}
func (p Proxy) MarshalYAML() (interface{}, error) {

View File

@ -17,6 +17,7 @@ type ProxyGroup struct {
Size int `yaml:"-"`
DisableUDP bool `yaml:"disable-udp,omitempty"`
Strategy string `yaml:"strategy,omitempty"`
Icon string `yaml:"icon,omitempty"`
}
type SelectProxyGroup struct {
@ -24,6 +25,7 @@ type SelectProxyGroup struct {
Type string `yaml:"type,omitempty"`
Proxies []string `yaml:"proxies,omitempty"`
DisableUDP bool `yaml:"disable-udp,omitempty"`
Icon string `yaml:"icon,omitempty"`
}
type UrlTestProxyGroup struct {
@ -35,6 +37,7 @@ type UrlTestProxyGroup struct {
Tolerance int `yaml:"tolerance,omitempty"`
Lazy bool `yaml:"lazy"`
DisableUDP bool `yaml:"disable-udp,omitempty"`
Icon string `yaml:"icon,omitempty"`
}
type LoadBalanceProxyGroup struct {
@ -46,12 +49,14 @@ type LoadBalanceProxyGroup struct {
Interval int `yaml:"interval,omitempty"`
Lazy bool `yaml:"lazy"`
Strategy string `yaml:"strategy,omitempty"`
Icon string `yaml:"icon,omitempty"`
}
type RelayProxyGroup struct {
Name string `yaml:"name,omitempty"`
Type string `yaml:"type,omitempty"`
Proxies []string `yaml:"proxies,omitempty"`
Icon string `yaml:"icon,omitempty"`
}
func (p ProxyGroup) MarshalYAML() (interface{}, error) {
@ -62,6 +67,7 @@ func (p ProxyGroup) MarshalYAML() (interface{}, error) {
Type: p.Type,
Proxies: p.Proxies,
DisableUDP: p.DisableUDP,
Icon: p.Icon,
}, nil
case "url-test", "fallback":
return UrlTestProxyGroup{
@ -73,6 +79,7 @@ func (p ProxyGroup) MarshalYAML() (interface{}, error) {
Tolerance: p.Tolerance,
Lazy: p.Lazy,
DisableUDP: p.DisableUDP,
Icon: p.Icon,
}, nil
case "load-balance":
return LoadBalanceProxyGroup{
@ -84,18 +91,21 @@ func (p ProxyGroup) MarshalYAML() (interface{}, error) {
Interval: p.Interval,
Lazy: p.Lazy,
Strategy: p.Strategy,
Icon: p.Icon,
}, nil
case "relay":
return RelayProxyGroup{
Name: p.Name,
Type: p.Type,
Proxies: p.Proxies,
Icon: p.Icon,
}, nil
default:
return SelectProxyGroup{
Name: p.Name,
Type: p.Type,
Proxies: p.Proxies,
Icon: p.Icon,
}, nil
}
}

View File

@ -13,6 +13,7 @@ type Hysteria struct {
Down string `yaml:"down"`
DownSpeed int `yaml:"down-speed,omitempty"` // compatible with Stash
Auth string `yaml:"auth,omitempty"`
AuthStringOLD string `yaml:"auth_str,omitempty"`
AuthString string `yaml:"auth-str,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
SNI string `yaml:"sni,omitempty"`
@ -34,9 +35,13 @@ func ProxyToHysteria(p Proxy) Hysteria {
Name: p.Name,
Server: p.Server,
Port: p.Port,
Ports: p.Ports,
Protocol: p.Protocol,
Up: p.Up,
Down: p.Down,
Auth: p.Auth,
AuthStringOLD: p.AuthStringOLD,
AuthString: p.AuthString,
Obfs: p.Obfs,
SNI: p.Sni,
SkipCertVerify: p.SkipCertVerify,

View File

@ -1,18 +1,224 @@
package model
type Subscription struct {
Port int `yaml:"port,omitempty"`
SocksPort int `yaml:"socks-port,omitempty"`
AllowLan bool `yaml:"allow-lan"`
Mode string `yaml:"mode,omitempty"`
LogLevel string `yaml:"logger-level,omitempty"`
ExternalController string `yaml:"external-controller,omitempty"`
Proxies []Proxy `yaml:"proxies,omitempty"`
ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
Rules []string `yaml:"rules,omitempty"`
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"`
}
// type Subscription struct {
// Port int `yaml:"port,omitempty"`
// SocksPort int `yaml:"socks-port,omitempty"`
// AllowLan bool `yaml:"allow-lan"`
// Mode string `yaml:"mode,omitempty"`
// LogLevel string `yaml:"logger-level,omitempty"`
// ExternalController string `yaml:"external-controller,omitempty"`
// Proxies []Proxy `yaml:"proxies,omitempty"`
// ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
// Rules []string `yaml:"rules,omitempty"`
// RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"`
// }
type NodeList struct {
Proxies []Proxy `yaml:"proxies,omitempty"`
}
type Subscription struct {
Port int `yaml:"port,omitempty" json:"port"`
SocksPort int `yaml:"socks-port,omitempty" json:"socks-port"`
RedirPort int `yaml:"redir-port,omitempty" json:"redir-port"`
TProxyPort int `yaml:"tproxy-port,omitempty" json:"tproxy-port"`
MixedPort int `yaml:"mixed-port,omitempty" json:"mixed-port"`
ShadowSocksConfig string `yaml:"ss-config,omitempty"`
VmessConfig string `yaml:"vmess-config,omitempty"`
InboundTfo bool `yaml:"inbound-tfo,omitempty"`
InboundMPTCP bool `yaml:"inbound-mptcp,omitempty"`
Authentication []string `yaml:"authentication,omitempty" json:"authentication"`
SkipAuthPrefixes []string `yaml:"skip-auth-prefixes,omitempty"`
LanAllowedIPs []string `yaml:"lan-allowed-ips,omitempty"`
LanDisAllowedIPs []string `yaml:"lan-disallowed-ips,omitempty"`
AllowLan bool `yaml:"allow-lan,omitempty" json:"allow-lan"`
BindAddress string `yaml:"bind-address,omitempty" json:"bind-address"`
Mode string `yaml:"mode,omitempty" json:"mode"`
UnifiedDelay bool `yaml:"unified-delay,omitempty" json:"unified-delay"`
LogLevel string `yaml:"log-level,omitempty" json:"log-level"`
IPv6 bool `yaml:"ipv6,omitempty" json:"ipv6"`
ExternalController string `yaml:"external-controller,omitempty"`
ExternalControllerTLS string `yaml:"external-controller-tls,omitempty"`
ExternalUI string `yaml:"external-ui,omitempty"`
ExternalUIURL string `yaml:"external-ui-url,omitempty" json:"external-ui-url"`
ExternalUIName string `yaml:"external-ui-name,omitempty" json:"external-ui-name"`
Secret string `yaml:"secret,omitempty"`
Interface string `yaml:"interface-name,omitempty"`
RoutingMark int `yaml:"routing-mark,omitempty"`
//Tunnels []LC.Tunnel `yaml:"tunnels,omitempty"`
GeoAutoUpdate bool `yaml:"geo-auto-update,omitempty" json:"geo-auto-update"`
GeoUpdateInterval int `yaml:"geo-update-interval,omitempty" json:"geo-update-interval"`
GeodataMode bool `yaml:"geodata-mode,omitempty" json:"geodata-mode"`
GeodataLoader string `yaml:"geodata-loader,omitempty" json:"geodata-loader"`
GeositeMatcher string `yaml:"geosite-matcher,omitempty" json:"geosite-matcher"`
TCPConcurrent bool `yaml:"tcp-concurrent,omitempty" json:"tcp-concurrent"`
FindProcessMode string `yaml:"find-process-mode,omitempty" json:"find-process-mode"`
GlobalClientFingerprint string `yaml:"global-client-fingerprint,omitempty"`
GlobalUA string `yaml:"global-ua,omitempty"`
KeepAliveInterval int `yaml:"keep-alive-interval,omitempty"`
Sniffer RawSniffer `yaml:"sniffer,omitempty" json:"sniffer"`
ProxyProvider map[string]map[string]any `yaml:"proxy-providers,omitempty"`
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty"`
Hosts map[string]any `yaml:"hosts,omitempty" json:"hosts"`
NTP RawNTP `yaml:"ntp,omitempty" json:"ntp"`
DNS RawDNS `yaml:"dns,omitempty" json:"dns"`
Tun RawTun `yaml:"tun,omitempty"`
TuicServer RawTuicServer `yaml:"tuic-server,omitempty"`
EBpf EBpf `yaml:"ebpf,omitempty"`
IPTables IPTables `yaml:"iptables,omitempty"`
Experimental Experimental `yaml:"experimental,omitempty"`
Profile Profile `yaml:"profile,omitempty"`
GeoXUrl GeoXUrl `yaml:"geox-url,omitempty"`
Proxies []Proxy `yaml:"proxies,omitempty"`
ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
Rules []string `yaml:"rules,omitempty"`
SubRules map[string][]string `yaml:"sub-rules,omitempty"`
RawTLS TLS `yaml:"tls,omitempty"`
Listeners []map[string]any `yaml:"listeners,omitempty"`
ClashForAndroid RawClashForAndroid `yaml:"clash-for-android,omitempty" json:"clash-for-android"`
}
type RawClashForAndroid struct {
AppendSystemDNS bool `yaml:"append-system-dns,omitempty" json:"append-system-dns"`
UiSubtitlePattern string `yaml:"ui-subtitle-pattern,omitempty" json:"ui-subtitle-pattern"`
}
type TLS struct {
Certificate string `yaml:"certificate,omitempty"`
PrivateKey string `yaml:"private-key,omitempty"`
CustomTrustCert []string `yaml:"custom-certifactes,omitempty"`
}
type GeoXUrl struct {
GeoIp string `yaml:"geoip,omitempty" json:"geoip"`
Mmdb string `yaml:"mmdb,omitempty" json:"mmdb"`
GeoSite string `yaml:"geosite,omitempty" json:"geosite"`
}
type Experimental struct {
Fingerprints []string `yaml:"fingerprints,omitempty"`
QUICGoDisableGSO bool `yaml:"quic-go-disable-gso,omitempty"`
QUICGoDisableECN bool `yaml:"quic-go-disable-ecn,omitempty"`
IP4PEnable bool `yaml:"dialer-ip4p-convert,omitempty"`
}
type Profile struct {
StoreSelected bool `yaml:"store-selected,omitempty"`
StoreFakeIP bool `yaml:"store-fake-ip,omitempty"`
}
type IPTables struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
InboundInterface string `yaml:"inbound-interface,omitempty" json:"inbound-interface"`
Bypass []string `yaml:"bypass,omitempty" json:"bypass"`
DnsRedirect bool `yaml:"dns-redirect,omitempty" json:"dns-redirect"`
}
// EBpf config
type EBpf struct {
RedirectToTun []string `yaml:"redirect-to-tun,omitempty" json:"redirect-to-tun"`
AutoRedir []string `yaml:"auto-redir,omitempty" json:"auto-redir"`
}
type RawSniffer struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
OverrideDest bool `yaml:"override-destination,omitempty" json:"override-destination"`
Sniffing []string `yaml:"sniffing,omitempty" json:"sniffing"`
ForceDomain []string `yaml:"force-domain,omitempty" json:"force-domain"`
SkipDomain []string `yaml:"skip-domain,omitempty" json:"skip-domain"`
Ports []string `yaml:"port-whitelist,omitempty" json:"port-whitelist"`
ForceDnsMapping bool `yaml:"force-dns-mapping,omitempty" json:"force-dns-mapping"`
ParsePureIp bool `yaml:"parse-pure-ip,omitempty" json:"parse-pure-ip"`
Sniff map[string]RawSniffingConfig `yaml:"sniff,omitempty" json:"sniff"`
}
type RawSniffingConfig struct {
Ports []string `yaml:"ports,omitempty" json:"ports"`
OverrideDest *bool `yaml:"override-destination,omitempty" json:"override-destination"`
}
type RawNTP struct {
Enable bool `yaml:"enable,omitempty"`
Server string `yaml:"server,omitempty"`
ServerPort int `yaml:"server-port,omitempty"`
Interval int `yaml:"interval,omitempty"`
DialerProxy string `yaml:"dialer-proxy,omitempty"`
WriteToSystem bool `yaml:"write-to-system,omitempty"`
}
type RawDNS struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
PreferH3 bool `yaml:"prefer-h3,omitempty" json:"prefer-h3"`
IPv6 bool `yaml:"ipv6,omitempty" json:"ipv6"`
IPv6Timeout uint `yaml:"ipv6-timeout,omitempty" json:"ipv6-timeout"`
UseHosts bool `yaml:"use-hosts,omitempty" json:"use-hosts"`
NameServer []string `yaml:"nameserver,omitempty" json:"nameserver"`
Fallback []string `yaml:"fallback,omitempty" json:"fallback"`
FallbackFilter RawFallbackFilter `yaml:"fallback-filter,omitempty" json:"fallback-filter"`
Listen string `yaml:"listen,omitempty" json:"listen"`
EnhancedMode string `yaml:"enhanced-mode,omitempty" json:"enhanced-mode"`
FakeIPRange string `yaml:"fake-ip-range,omitempty" json:"fake-ip-range"`
FakeIPFilter []string `yaml:"fake-ip-filter,omitempty" json:"fake-ip-filter"`
DefaultNameserver []string `yaml:"default-nameserver,omitempty" json:"default-nameserver"`
CacheAlgorithm string `yaml:"cache-algorithm,omitempty" json:"cache-algorithm"`
//NameServerPolicy *orderedmap.OrderedMap[string, any] `yaml:"nameserver-policy,omitempty" json:"nameserver-policy"`
ProxyServerNameserver []string `yaml:"proxy-server-nameserver,omitempty" json:"proxy-server-nameserver"`
}
type RawFallbackFilter struct {
GeoIP bool `yaml:"geoip,omitempty" json:"geoip"`
GeoIPCode string `yaml:"geoip-code,omitempty" json:"geoip-code"`
IPCIDR []string `yaml:"ipcidr,omitempty" json:"ipcidr"`
Domain []string `yaml:"domain,omitempty" json:"domain"`
GeoSite []string `yaml:"geosite,omitempty" json:"geosite"`
}
type RawTun struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
Device string `yaml:"device,omitempty" json:"device"`
Stack string `yaml:"stack,omitempty" json:"stack"`
DNSHijack []string `yaml:"dns-hijack,omitempty" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route,omitempty" json:"auto-route"`
AutoDetectInterface bool `yaml:"auto-detect-interface,omitempty"`
RedirectToTun []string `yaml:"-,omitempty" json:"-"`
MTU uint32 `yaml:"mtu,omitempty" json:"mtu,omitempty"`
GSO bool `yaml:"gso,omitempty" json:"gso,omitempty"`
GSOMaxSize uint32 `yaml:"gso-max-size,omitempty" json:"gso-max-size,omitempty"`
//Inet4Address []netip.Prefix `yaml:"inet4-address,omitempty" json:"inet4_address,omitempty"`
Inet6Address []uint32 `yaml:"inet6-address,omitempty" json:"inet6_address,omitempty"`
StrictRoute bool `yaml:"strict-route,omitempty" json:"strict_route,omitempty"`
Inet4RouteAddress []uint32 `yaml:"inet4-route-address,omitempty" json:"inet4_route_address,omitempty"`
Inet6RouteAddress []uint32 `yaml:"inet6-route-address,omitempty" json:"inet6_route_address,omitempty"`
Inet4RouteExcludeAddress []uint32 `yaml:"inet4-route-exclude-address,omitempty" json:"inet4_route_exclude_address,omitempty"`
Inet6RouteExcludeAddress []uint32 `yaml:"inet6-route-exclude-address,omitempty" json:"inet6_route_exclude_address,omitempty"`
IncludeInterface []string `yaml:"include-interface,omitempty" json:"include-interface,omitempty"`
ExcludeInterface []string `yaml:"exclude-interface,omitempty" json:"exclude-interface,omitempty"`
IncludeUID []uint32 `yaml:"include-uid,omitempty" json:"include_uid,omitempty"`
IncludeUIDRange []string `yaml:"include-uid-range,omitempty" json:"include_uid_range,omitempty"`
ExcludeUID []uint32 `yaml:"exclude-uid,omitempty" json:"exclude_uid,omitempty"`
ExcludeUIDRange []string `yaml:"exclude-uid-range,omitempty" json:"exclude_uid_range,omitempty"`
IncludeAndroidUser []int `yaml:"include-android-user,omitempty" json:"include_android_user,omitempty"`
IncludePackage []string `yaml:"include-package,omitempty" json:"include_package,omitempty"`
ExcludePackage []string `yaml:"exclude-package,omitempty" json:"exclude_package,omitempty"`
EndpointIndependentNat bool `yaml:"endpoint-independent-nat,omitempty" json:"endpoint_independent_nat,omitempty"`
UDPTimeout int64 `yaml:"udp-timeout,omitempty" json:"udp_timeout,omitempty"`
FileDescriptor int `yaml:"file-descriptor,omitempty" json:"file-descriptor"`
}
type RawTuicServer struct {
Enable bool `yaml:"enable,omitempty" json:"enable"`
Listen string `yaml:"listen,omitempty" json:"listen"`
Token []string `yaml:"token,omitempty" json:"token"`
Users map[string]string `yaml:"users,omitempty" json:"users,omitempty"`
Certificate string `yaml:"certificate,omitempty" json:"certificate"`
PrivateKey string `yaml:"private-key,omitempty" json:"private-key"`
CongestionController string `yaml:"congestion-controller,omitempty" json:"congestion-controller,omitempty"`
MaxIdleTime int `yaml:"max-idle-time,omitempty" json:"max-idle-time,omitempty"`
AuthenticationTimeout int `yaml:"authentication-timeout,omitempty" json:"authentication-timeout,omitempty"`
ALPN []string `yaml:"alpn,omitempty" json:"alpn,omitempty"`
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size,omitempty" json:"max-udp-relay-packet-size,omitempty"`
CWND int `yaml:"cwnd,omitempty" json:"cwnd,omitempty"`
}

View File

@ -46,6 +46,10 @@ func ParseShadowsocksR(proxy string) (model.Proxy, error) {
remarks, err = DecodeBase64(params.Get("remarks"))
}
if err != nil {
return model.Proxy{}, err
}
result := model.Proxy{
Name: remarks,
Type: "ssr",

View File

@ -43,18 +43,22 @@ func ParseVless(proxy string) (model.Proxy, error) {
UDP: true,
Sni: params.Get("sni"),
Network: params.Get("type"),
TLS: params.Get("security") == "reality",
Flow: params.Get("flow"),
ClientFingerprint: params.Get("fp"),
Servername: params.Get("sni"),
RealityOpts: model.RealityOptions{
PublicKey: params.Get("pbk"),
},
}
if params.Get("alpn") != "" {
result.Alpn = strings.Split(params.Get("alpn"), ",")
}
if params.Get("security") == "reality" {
result.TLS = true
result.RealityOpts = model.RealityOptions{
PublicKey: params.Get("pbk"),
ShortID: params.Get("sid"),
}
}
if params.Get("type") == "ws" {
result.TLS = true
result.WSOpts = model.WSOptions{
Path: params.Get("path"),
Headers: map[string]string{
@ -63,6 +67,7 @@ func ParseVless(proxy string) (model.Proxy, error) {
}
}
if params.Get("type") == "grpc" {
result.TLS = true
result.GrpcOpts = model.GrpcOptions{
GrpcServiceName: params.Get("serviceName"),
}

File diff suppressed because it is too large Load Diff

View File

@ -5,123 +5,123 @@ mode: Rule
log-level: info
proxies:
proxy-groups:
- name: 节点选择
type: select
proxies:
- <countries>
- 手动切换
- DIRECT
- name: 手动切换
type: select
proxies:
- <all>
- name: 游戏平台(中国)
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 游戏平台(全球)
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 巴哈姆特
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 哔哩哔哩
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Telegram
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: OpenAI
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Youtube
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Microsoft
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Onedrive
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Apple
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Netflix
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 广告拦截
type: select
proxies:
- REJECT
- DIRECT
- name: 漏网之鱼
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 节点选择
type: select
proxies:
- <countries>
- 手动切换
- DIRECT
- name: 手动切换
type: select
proxies:
- <all>
- name: 游戏平台(中国)
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 游戏平台(全球)
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 巴哈姆特
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 哔哩哔哩
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Telegram
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: OpenAI
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Youtube
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Microsoft
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Onedrive
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Apple
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Netflix
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 广告拦截
type: select
proxies:
- REJECT
- DIRECT
- name: 漏网之鱼
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
rules:
- GEOSITE,private,DIRECT,no-resolve
- GEOIP,private,DIRECT
- GEOSITE,category-ads-all,广告拦截
- GEOSITE,microsoft,Microsoft
- GEOSITE,apple,Apple
- GEOSITE,netflix,Netflix
- GEOIP,netflix,Netflix
- GEOSITE,onedrive,Onedrive
- GEOSITE,youtube,Youtube
- GEOSITE,telegram,Telegram
- GEOIP,telegram,Telegram
- GEOSITE,openai,OpenAI
- GEOSITE,bilibili,哔哩哔哩
- GEOSITE,bahamut,巴哈姆特
- GEOSITE,category-games@cn,游戏平台(中国)
- GEOSITE,category-games,游戏平台(全球)
- GEOSITE,geolocation-!cn,节点选择
- GEOSITE,CN,DIRECT
- GEOIP,CN,DIRECT
- MATCH,漏网之鱼
- GEOSITE,private,DIRECT,no-resolve
- GEOIP,private,DIRECT
- GEOSITE,category-ads-all,广告拦截
- GEOSITE,microsoft,Microsoft
- GEOSITE,apple,Apple
- GEOSITE,netflix,Netflix
- GEOIP,netflix,Netflix
- GEOSITE,onedrive,Onedrive
- GEOSITE,youtube,Youtube
- GEOSITE,telegram,Telegram
- GEOIP,telegram,Telegram
- GEOSITE,openai,OpenAI
- GEOSITE,bilibili,哔哩哔哩
- GEOSITE,bahamut,巴哈姆特
- GEOSITE,category-games@cn,游戏平台(中国)
- GEOSITE,category-games,游戏平台(全球)
- GEOSITE,geolocation-!cn,节点选择
- GEOSITE,CN,DIRECT
- GEOIP,CN,DIRECT
- MATCH,漏网之鱼

View File

@ -27,9 +27,6 @@ func ConnectDB() error {
if err != nil {
return err
}
if err != nil {
return err
}
DB = db
err = db.AutoMigrate(&model.ShortLink{})
if err != nil {

View File

@ -65,17 +65,11 @@ func AddProxy(
countryName := GetContryName(proxy.Name)
for i := range sub.ProxyGroups {
group := &sub.ProxyGroups[i]
if group.Name == countryName {
group.Proxies = append(group.Proxies, proxy.Name)
group.Size++
haveProxyGroup = true
}
if group.Name == "手动切换" {
group.Proxies = append(group.Proxies, proxy.Name)
group.Size++
}
}
if !haveProxyGroup {
var newGroup model.ProxyGroup

View File

@ -81,8 +81,5 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("failed to write to sub.yaml: %w", err)
}
if err != nil {
return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
}
return data, nil
}

View File

@ -25,9 +25,6 @@ func LoadTemplate(template string) ([]byte, error) {
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
return result, nil
}
return nil, errors.New("模板文件不存在")

View File

@ -6,6 +6,12 @@ type ShortLinkGenValidator struct {
}
type ShortLinkGetValidator struct {
Hash string `form:"hash" binding:"required"`
Hash string `form:"hash" binding:"required"` // Hash: 短链接
Password string `form:"password"`
}
type ShortLinkUpdateValidator struct {
Hash string `form:"hash" binding:"required"`
Url string `form:"url" binding:"required"`
Password string `form:"password" binding:"required"`
}

View File

@ -12,24 +12,25 @@ import (
)
type SubValidator struct {
Sub string `form:"sub" binding:""`
Subs []string `form:"-" binding:""`
Proxy string `form:"proxy" binding:""`
Proxies []string `form:"-" binding:""`
Refresh bool `form:"refresh,default=false" binding:""`
Template string `form:"template" binding:""`
RuleProvider string `form:"ruleProvider" binding:""`
RuleProviders []RuleProviderStruct `form:"-" binding:""`
Rule string `form:"rule" binding:""`
Rules []RuleStruct `form:"-" binding:""`
AutoTest bool `form:"autoTest,default=false" binding:""`
Lazy bool `form:"lazy,default=false" binding:""`
Sort string `form:"sort" binding:""`
Remove string `form:"remove" binding:""`
Replace string `form:"replace" binding:""`
ReplaceKeys []string `form:"-" binding:""`
ReplaceTo []string `form:"-" binding:""`
NodeListMode bool `form:"nodeList,default=false" binding:""`
Sub string `form:"sub" binding:""`
Subs []string `form:"-" binding:""`
Proxy string `form:"proxy" binding:""`
Proxies []string `form:"-" binding:""`
Refresh bool `form:"refresh,default=false" binding:""`
Template string `form:"template" binding:""`
RuleProvider string `form:"ruleProvider" binding:""`
RuleProviders []RuleProviderStruct `form:"-" binding:""`
Rule string `form:"rule" binding:""`
Rules []RuleStruct `form:"-" binding:""`
AutoTest bool `form:"autoTest,default=false" binding:""`
Lazy bool `form:"lazy,default=false" binding:""`
Sort string `form:"sort" binding:""`
Remove string `form:"remove" binding:""`
Replace string `form:"replace" binding:""`
ReplaceKeys []string `form:"-" binding:""`
ReplaceTo []string `form:"-" binding:""`
NodeListMode bool `form:"nodeList,default=false" binding:""`
IgnoreCountryGrooup bool `form:"ignoreCountryGroup,default=false" binding:""`
}
type RuleProviderStruct struct {