OpenClash v0.47.116
Some checks failed
Clean jsDelivr Cache / jsDelivr (push) Has been cancelled
Sync Core and Package to Master When release / Sync_core_to_master (push) Has been cancelled
Compile The New Clash Core / Get-Commit-id (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[abi:1 goarch:loong64 goos:linux output:loong64-abi1 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goamd64:v1 goarch:amd64 goos:linux output:amd64-compatible upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goamd64:v1 goarch:amd64 goos:linux output:amd64-v1 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goamd64:v2 goarch:amd64 goos:linux output:amd64-v2 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goamd64:v3 goarch:amd64 goos:linux output:amd64 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goamd64:v3 goarch:amd64 goos:linux output:amd64-v3 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:386 goos:linux output:386 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:arm goarm:5 goos:linux output:armv5 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:arm goarm:6 goos:linux output:armv6 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:arm goarm:7 goos:linux output:armv7 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:arm64 goos:linux output:arm64 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:loong64 goos:linux output:loong64-abi2 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:mips gomips:hardfloat goos:linux output:mips-hardfloat upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:mips gomips:softfloat goos:linux output:mips-softfloat upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:mips64 goos:linux output:mips64 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:mips64le goos:linux output:mips64le upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:mipsle gomips:hardfloat goos:linux output:mipsle-hardfloat upxflag:--best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:mipsle gomips:softfloat goos:linux output:mipsle-softfloat upxflag:--best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:riscv64 goos:linux output:riscv64 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:meta-core goversion:1.26 repo:MetaCubeX/mihomo src:metacubex_src type:meta version_prefix:alpha-g], map[goarch:s390x goos:linux output:s390x upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[abi:1 goarch:loong64 goos:linux output:loong64-abi1 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goamd64:v1 goarch:amd64 goos:linux output:amd64-compatible upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goamd64:v1 goarch:amd64 goos:linux output:amd64-v1 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goamd64:v2 goarch:amd64 goos:linux output:amd64-v2 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goamd64:v3 goarch:amd64 goos:linux output:amd64 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goamd64:v3 goarch:amd64 goos:linux output:amd64-v3 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:386 goos:linux output:386 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:arm goarm:5 goos:linux output:armv5 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:arm goarm:6 goos:linux output:armv6 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:arm goarm:7 goos:linux output:armv7 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:arm64 goos:linux output:arm64 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:loong64 goos:linux output:loong64-abi2 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:mips gomips:hardfloat goos:linux output:mips-hardfloat upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:mips gomips:softfloat goos:linux output:mips-softfloat upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:mips64 goos:linux output:mips64 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:mips64le goos:linux output:mips64le upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:mipsle gomips:hardfloat goos:linux output:mipsle-hardfloat upxflag:--best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:mipsle gomips:softfloat goos:linux output:mipsle-softfloat upxflag:--best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:riscv64 goos:linux output:riscv64 upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Compile_Core (map[artifact:smart-core goversion:1.26 repo:vernesong/mihomo src:vernesong_src type:smart version_prefix:alpha-smart-g], map[goarch:s390x goos:linux output:s390x upxflag:--lzma --best]) (push) Has been cancelled
Compile The New Clash Core / Publish (push) Has been cancelled
Trim Core and Package History / trim (core) (push) Has been cancelled
Trim Core and Package History / trim (package) (push) Has been cancelled
Delete old workflow runs / del_runs (push) Has been cancelled

This commit is contained in:
xiexiaobao 2026-07-10 16:46:39 +08:00
commit 6909ba0cea
279 changed files with 157640 additions and 0 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: vernesong
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

153
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,153 @@
name: Bug报告
description: Create a Bug report to help us improve
title: "[Bug] "
labels: ["bug"]
body:
- type: checkboxes
id: verify_step
attributes:
label: Verify Steps
description: |
在提交之前,请确认 / Please verify that you've followed these steps.
options:
- label: Tracker 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
required: true
- label: Branch 我知道 OpenClash 的 Dev 分支切换开关位于插件设置-版本更新中,或者我会手动下载并安装 Dev 分支的 OpenClash
required: true
- label: Latest 我已经**使用最新 Dev 版本**测试过,问题依旧存在
required: true
- label: Relevant 我知道 OpenClash 与 内核(Core)、控制面板(Dashboard)、在线订阅转换(Subconverter)等项目之间**无直接关系**,仅相互调用
required: true
- label: Definite 这确实是 OpenClash 出现的问题
required: true
- label: Contributors 我有能力协助 OpenClash 开发并解决此问题
required: false
- label: Meaningless 我提交的**是无意义的**催促更新或修复请求
required: false
- type: input
id: openclash_version
attributes:
label: OpenClash Version
description: |
OpenClash 版本号
placeholder: "v0.0.0-beta"
validations:
required: true
- type: dropdown
id: bug_os
attributes:
label: Bug on Environment
description: |
发现问题所在的系统环境 / System Environment
multiple: true
options:
- Official OpenWrt
- Lean
- Immortalwrt
- Istoreos
- Docker
- Other
validations:
required: true
- type: input
id: openwrt_version
attributes:
label: OpenWrt Version
description: |
Openwrt 固件版本
placeholder: "OpenWrt 0.0.0 r0-0"
validations:
required: true
- type: dropdown
id: bug_platform
attributes:
label: Bug on Platform
description: |
发现问题所在的架构平台 / Platform
multiple: true
options:
- Linux-386
- Linux-amd64(x86-64)
- Linux-amd64-v3(x86-64)
- Linux-armv5
- Linux-armv6
- Linux-armv7
- Linux-arm64
- Linux-loong64
- Linux-mips-hardfloat
- Linux-mips-softfloat
- Linux-mips64
- Linux-mips64le
- Linux-mipsle-hardfloat
- Linux-mipsle-softfloat
- Linux-riscv64
- Other
- All
validations:
required: true
- type: textarea
id: describe_bug
attributes:
label: Describe the Bug
description: |
对 Bug 本身清晰而简洁的描述 / Describe the Bug
validations:
required: true
- type: textarea
id: reproduce_bug
attributes:
label: To Reproduce
description: |
复现此 Bug 的步骤 / How to reproduce?
validations:
required: true
- type: textarea
id: openclash_log
attributes:
label: OpenClash Log
description: |
在下方附上 OpenClash 调试日志 / OpenClash Debug Log
调试日志在插件设置-调试日志中生成,**并非只有运行日志**,如调试日志过长,可作为附件在最下方上传
**隐私提示: 上传此日志前请注意检查、屏蔽公网IP、节点、密码等相关敏感信息**
placeholder: "我已知晓缺失日志可能会导致开发者无法了解我的情况并降低本issue的处理优先级"
render: log
validations:
required: true
- type: textarea
id: openclash_config
attributes:
label: OpenClash Config
description: |
在下方附上与 Bug 相关的系统配置、防火墙规则或环境变量 / System config
非 Clash Yaml 文件
**非必填项**
render: shell
validations:
required: false
- type: textarea
id: excepted_behavior
attributes:
label: Expected Behavior
description: |
对预期修复后情况的清晰明了的描述 / Expected behavior
validations:
required: true
- type: textarea
id: additional_context
attributes:
label: Additional Context
description: |
在此处添加其它信息或屏幕截图 / Additional Context
validations:
required: false

47
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,47 @@
blank_issues_enabled: false
contact_links:
- name: 发表Issue前请先在Issues与Discussions中搜索相关内容
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md#在提问之前
about: 请有意义且描述明确的描述问题症状而非你的猜测 问题解决后加个简短的补充说明
- name: 启动成功后无法访问个别网站 | 启动成功后个别网站不想走代理流量
url: https://www.bing.com/search?q=clash+rule+写+教&ensearch=1
about: 想要了解clash规则编写相关的问题
- name: CLASH CORE | PERIUM CORE & 内核问题 | 节点协议加密方式 | 规则 | 内核功能建议
url: https://github.com/Dreamacro/clash/issues/new/choose
about: 原版内核 | P核
- name: META CORE | MIHOMO CORE & 内核问题 | 节点协议加密方式 | 规则 | 内核功能建议
url: https://github.com/MetaCubeX/mihomo/issues/new/choose
about: M核
- name: 在线订阅转换问题 | 在线订阅转换增加节点协议加密方式 | 在线订阅转换功能建议
url: https://github.com/tindy2013/subconverter/issues/new/choose
about: 提供并增加预设订阅转换服务地址请通过上方 功能请求 发表
- name: DASHBOARD & 控制面板问题 | 面板功能建议
url: https://github.com/Dreamacro/clash-dashboard/issues/new/choose
about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问
- name: YACD & 控制面板问题 | 面板功能建议
url: https://github.com/haishanh/yacd/issues/new/choose
about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问
- name: METACUBEXD & 控制面板问题 | 面板功能建议
url: https://github.com/MetaCubeX/metacubexd/issues/new/choose
about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问
- name: YACD-META & 控制面板问题 | 面板功能建议
url: https://github.com/MetaCubeX/Yacd-meta/issues/new/choose
about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问
- name: DASHBOARD-META & 控制面板问题 | 面板功能建议
url: https://github.com/MetaCubeX/Razord-meta/issues/new/choose
about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问
- name: 我想讨论OpenClash的某个功能 | 我不确定是什么问题 | 我的问题不属于上述选项 | 我的问题无法得到原作者原仓库的帮助
url: https://github.com/vernesong/OpenClash/discussions/new/choose
about: 礼多人不怪 而且有时还很有帮助

View File

@ -0,0 +1,40 @@
name: 功能请求
description: Suggest an idea for this project
title: "[Feature] "
labels: [enhancement]
body:
- type: checkboxes
id: verify_step
attributes:
label: Verify Steps
description: "在提交之前,请确认 / Please verify that you've followed these steps"
options:
- label: Tracker 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
required: true
- label: Latest 我已经**使用最新 Dev 版本**查看过,并不包含该功能特性或者还不完善
required: true
- label: Relevant 我知道 OpenClash 与 内核(Core)、控制面板(Dashboard)、在线订阅转换(Subconverter)等项目之间**无直接关系**,仅相互调用
required: true
- label: Definite 这确实是 OpenClash 应包含的特性
required: true
- label: Contributors 我有能力协助 OpenClash 开发或完善此功能特性
required: false
- label: Meaningless 我提交的**是无意义的**催促更新或修复请求
required: false
- type: textarea
attributes:
label: Describe the Feature
description: |
清晰明了地描述您的想法. 例如你想实现什么 Feature 特性或功能? 如何实现该功能? 目前 OpenClash 的行为是什么?
validations:
required: true
- type: textarea
attributes:
label: Describe Alternatives
description: |
对您考虑过的任何替代解决方案或备选功能进行清晰、简洁的描述.
validations:
required: false

File diff suppressed because it is too large Load Diff

41
.github/workflows/clear_cache.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Clean jsDelivr Cache
on:
push:
branches:
- master
- dev
- core
- package
paths-ignore:
- 'README.md'
- 'LICENSE'
jobs:
jsDelivr:
runs-on: ubuntu-latest
steps:
- name: Apt Update
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install jq
- uses: actions/checkout@v6
# https://github.com/marketplace/actions/git-changesets
- id: changed_files
name: git-changesets
uses: collin-miller/git-changesets@v1
with:
# Default format is 'csv'. Other valid options are 'space-delimited' and 'json'.
format: json
- name: Clean jsDelivr Cache
run: |
echo '${{ steps.changed_files.outputs.added_modified }}' | jq -r '.[]' | while read file; do
# echo ${{ steps.changed_files.outputs.all }}
curl -sL --retry 3 "https://purge.jsdelivr.net/gh/vernesong/OpenClash@${{ github.ref_name }}/${file}"
done

19
.github/workflows/close_issues.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Mark and close stale issues
on:
schedule:
- cron: "30 8 * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
days-before-stale: 60
days-before-close: 5
operations-per-run: 200

118
.github/workflows/compile_dev_core.yml vendored Normal file
View File

@ -0,0 +1,118 @@
name: Compile The New Clash Dev Core
on:
#schedule:
# - cron: "0 19 * * 1,3,5,6"
workflow_dispatch:
jobs:
Get-Commit-id:
runs-on: ubuntu-latest
outputs:
current_id: ${{ steps.current_id.outputs.current_id }}
upstream_id: ${{ steps.upstream_id.outputs.upstream_id }}
steps:
- name: Apt Update
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install git
- name: Clone OpenClash Repository
uses: actions/checkout@v4
with:
ref: core
- name: Get Current Version
id: current_id
run: |
echo "current_id=$(sed -n 1p ./dev/core_version)" >> $GITHUB_OUTPUT
echo "current_id: $(sed -n 1p ./dev/core_version)"
- name: Clone Clash Repository
uses: actions/checkout@v4
with:
repository: Dreamacro/clash
ref: dev
fetch-depth: '0'
- name: Get Upstream Version
id: upstream_id
run: |
echo "upstream_id=$(git describe --tags)" >> $GITHUB_OUTPUT
echo "upstream_id: $(git describe --tags)"
Compile:
runs-on: ubuntu-latest
needs: Get-Commit-id
if: ${{ needs.Get-Commit-id.outputs.current_id != needs.Get-Commit-id.outputs.upstream_id }}
steps:
- name: Apt Update
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install wget git tar
- name: Setup UPX
run: |
cd ..
wget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz
tar xf upx-3.95-amd64_linux.tar.xz
echo "upx=../upx-3.95-amd64_linux/upx" >> $GITHUB_ENV
- name: Clone OpenClash Repository
uses: actions/checkout@v4
with:
ref: core
- name: Copy Makefile
run: |
cd ..
mkdir tmp
mkdir tmp/bin
cp ./OpenClash/.github/makefile/dev ./tmp/Makefile
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: "1.21"
check-latest: true
- name: Clone Clash Repository
uses: actions/checkout@v4
with:
repository: Dreamacro/clash
ref: dev
fetch-depth: '0'
- name: Compile Dev Clash
run: |
cp ../tmp/Makefile ./Makefile
make releases
- name: Copy Clash Bin
run: |
cp -rf "./bin/." "../tmp/bin/"
- name: Clone OpenClash Repository
uses: actions/checkout@v4
with:
ref: core
- name: Change Version
run: |
sed -i '1d' ./dev/core_version
sed -i "1i ${{ needs.Get-Commit-id.outputs.upstream_id }}" ./dev/core_version
- name: Commit and push
run: |
rm -rf ./dev/dev/*
cp -rf "../tmp/bin/." "./dev/dev/"
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add .
git commit -m "Auto update Dev core: ${{ needs.Get-Commit-id.outputs.upstream_id }}"
git push

333
.github/workflows/compile_meta_core.yml vendored Normal file
View File

@ -0,0 +1,333 @@
name: Compile The New Clash Core
on:
schedule:
- cron: "0 20 * * 1,3,5,6"
workflow_dispatch:
jobs:
Get-Commit-id:
runs-on: ubuntu-latest
outputs:
current_id: ${{ steps.current_id.outputs.current_id }}
current_smart_id: ${{ steps.current_id.outputs.current_smart_id }}
upstream_id: ${{ steps.upstream_id.outputs.upstream_id }}
upstream_smart_id: ${{ steps.upstream_id.outputs.upstream_smart_id }}
steps:
- name: Apt Update
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install git
- name: Clone OpenClash Repository
uses: actions/checkout@v6
with:
ref: core
- name: Get Current Version
id: current_id
run: |
current_meta_ver=$(sed -n '1p' ./dev/core_version 2>/dev/null || echo "null")
echo "current_id=$current_meta_ver" >> $GITHUB_OUTPUT
echo "current_id: $current_meta_ver"
current_smart_ver=$(sed -n '2p' ./dev/core_version 2>/dev/null || echo "null")
echo "current_smart_id=$current_smart_ver" >> $GITHUB_OUTPUT
echo "current_smart_id: $current_smart_ver"
- name: Get Upstream Version
id: upstream_id
run: |
# MetaCubeX/mihomo
git clone --depth=1 --branch=Alpha https://github.com/MetaCubeX/mihomo.git metacubex_mihomo
cd metacubex_mihomo
echo "upstream_id=alpha-g$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "upstream_id: alpha-g$(git rev-parse --short HEAD)"
cd ..
# vernesong/mihomo
git clone --depth=1 --branch=Alpha https://github.com/vernesong/mihomo.git vernesong_mihomo
cd vernesong_mihomo
echo "upstream_smart_id=alpha-smart-g$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "upstream_smart_id: alpha-smart-g$(git rev-parse --short HEAD)"
Compile_Core:
runs-on: ubuntu-latest
needs: Get-Commit-id
if: ${{ needs.Get-Commit-id.outputs.current_id != needs.Get-Commit-id.outputs.upstream_id || needs.Get-Commit-id.outputs.current_smart_id != needs.Get-Commit-id.outputs.upstream_smart_id }}
strategy:
matrix:
core:
- { type: meta, repo: MetaCubeX/mihomo, src: metacubex_src, version_prefix: alpha-g, artifact: meta-core, goversion: '1.26' }
- { type: smart, repo: vernesong/mihomo, src: vernesong_src, version_prefix: alpha-smart-g, artifact: smart-core, goversion: '1.26' }
jobs:
- { goos: linux, goarch: '386', output: '386', upxflag: '--lzma --best' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, upxflag: '--lzma --best' }
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64, upxflag: '--lzma --best' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1, upxflag: '--lzma --best' }
- { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2, upxflag: '--lzma --best' }
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3, upxflag: '--lzma --best' }
- { goos: linux, goarch: arm, goarm: '5', output: armv5, upxflag: '--lzma --best' }
- { goos: linux, goarch: arm, goarm: '6', output: armv6, upxflag: '--lzma --best' }
- { goos: linux, goarch: arm, goarm: '7', output: armv7, upxflag: '--lzma --best' }
- { goos: linux, goarch: arm64, output: arm64, upxflag: '--lzma --best' }
- { goos: linux, goarch: mips, gomips: softfloat, output: mips-softfloat, upxflag: '--lzma --best' }
- { goos: linux, goarch: mips, gomips: hardfloat, output: mips-hardfloat, upxflag: '--lzma --best' }
- { goos: linux, goarch: mipsle, gomips: softfloat, output: mipsle-softfloat, upxflag: '--best' }
- { goos: linux, goarch: mipsle, gomips: hardfloat, output: mipsle-hardfloat, upxflag: '--best' }
- { goos: linux, goarch: mips64, output: mips64, upxflag: '--lzma --best' }
- { goos: linux, goarch: mips64le, output: mips64le, upxflag: '--lzma --best' }
- { goos: linux, goarch: riscv64, output: riscv64, upxflag: '--lzma --best' }
- { goos: linux, goarch: s390x, output: s390x, upxflag: '--lzma --best' }
- { goos: linux, goarch: loong64, output: loong64-abi2, upxflag: '--lzma --best' }
- { goos: linux, goarch: loong64, abi: '1', output: loong64-abi1, upxflag: '--lzma --best' }
steps:
- name: Check if build needed
id: check_skip
run: |
if [ "${{ matrix.core.type }}" = "meta" ] && [ "${{ needs.Get-Commit-id.outputs.current_id }}" = "${{ needs.Get-Commit-id.outputs.upstream_id }}" ]; then
echo "Meta is up to date, skipping build"
echo "skip=true" >> $GITHUB_OUTPUT
elif [ "${{ matrix.core.type }}" = "smart" ] && [ "${{ needs.Get-Commit-id.outputs.current_smart_id }}" = "${{ needs.Get-Commit-id.outputs.upstream_smart_id }}" ]; then
echo "Smart is up to date, skipping build"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Apt Update
if: ${{ steps.check_skip.outputs.skip != 'true' }}
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install wget git tar
- name: Setup UPX
if: ${{ steps.check_skip.outputs.skip != 'true' }}
run: |
wget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz
tar xf upx-3.95-amd64_linux.tar.xz
echo "upx=${{ github.workspace }}/upx-3.95-amd64_linux/upx" >> $GITHUB_ENV
- name: Setup Go
if: ${{ steps.check_skip.outputs.skip != 'true' && !(matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1') }}
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.core.goversion }}
check-latest: true
cache: false
- name: Cache Go loongarch abi1 tarball
id: cache_go_abi1
if: ${{ steps.check_skip.outputs.skip != 'true' && matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/go-archives/go1.26.0.linux-amd64-abi1.tar.gz
key: go-loongarch-abi1-1.26.0
- name: Setup Go for loongarch abi1
if: ${{ steps.check_skip.outputs.skip != 'true' && matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
run: |
GO_TAR_FILE="go1.26.0.linux-amd64-abi1.tar.gz"
GO_ARCHIVE_DIR="${{ runner.temp }}/go-archives"
mkdir -p "$GO_ARCHIVE_DIR"
if [[ "${{ steps.cache_go_abi1.outputs.cache-hit }}" != 'true' ]]; then
echo "Downloading $GO_TAR_FILE..."
wget -q "https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0/$GO_TAR_FILE" \
-O "$GO_ARCHIVE_DIR/$GO_TAR_FILE"
else
echo "Restored $GO_TAR_FILE from cache."
fi
sudo rm -rf /usr/local/go
sudo tar zxf "$GO_ARCHIVE_DIR/$GO_TAR_FILE" -C /usr/local
echo "/usr/local/go/bin" >> $GITHUB_PATH
- name: Clone source (${{ matrix.core.type }})
if: ${{ steps.check_skip.outputs.skip != 'true' }}
uses: actions/checkout@v6
with:
repository: ${{ matrix.core.repo }}
ref: Alpha
path: ./${{ matrix.core.src }}
- name: Update CA
if: ${{ steps.check_skip.outputs.skip != 'true' }}
run: |
sudo apt-get update && sudo apt-get install ca-certificates
sudo update-ca-certificates
cp -f /etc/ssl/certs/ca-certificates.crt ./${{ matrix.core.src }}/component/ca/ca-certificates.crt
- name: Build Core (${{ matrix.core.type }}-${{ matrix.jobs.output }})
id: build
if: ${{ steps.check_skip.outputs.skip != 'true' }}
env:
GOOS: ${{ matrix.jobs.goos }}
GOARCH: ${{ matrix.jobs.goarch }}
GOAMD64: ${{ matrix.jobs.goamd64 }}
GOARM: ${{ matrix.jobs.goarm }}
GOMIPS: ${{ matrix.jobs.gomips }}
CGO_ENABLED: 0
run: |
if [ "${{ matrix.core.type }}" = "meta" ] && [ "${{ needs.Get-Commit-id.outputs.current_id }}" = "${{ needs.Get-Commit-id.outputs.upstream_id }}" ]; then
echo "Meta is up to date, skipping build"
echo "skip=true" >> $GITHUB_OUTPUT
exit 0
fi
if [ "${{ matrix.core.type }}" = "smart" ] && [ "${{ needs.Get-Commit-id.outputs.current_smart_id }}" = "${{ needs.Get-Commit-id.outputs.upstream_smart_id }}" ]; then
echo "Smart is up to date, skipping build"
echo "skip=true" >> $GITHUB_OUTPUT
exit 0
fi
set -e
cd ./${{ matrix.core.src }}
VERSION="${{ matrix.core.version_prefix }}$(git rev-parse --short HEAD)"
BUILDTIME="$(date -u)"
mkdir -p ./bin
go build -tags with_gvisor -trimpath \
-ldflags "-X 'github.com/metacubex/mihomo/constant.Version=${VERSION}' -X 'github.com/metacubex/mihomo/constant.BuildTime=${BUILDTIME}' -w -s -buildid=" \
-o ./bin/clash
chmod +x ./bin/clash
${upx} ${{ matrix.jobs.upxflag }} ./bin/clash || true
mkdir -p ../build_output
tar -zcvf ../build_output/clash-linux-${{ matrix.jobs.output }}.tar.gz -C ./bin clash
- name: Upload Core Artifact (${{ matrix.core.type }}-${{ matrix.jobs.output }})
if: ${{ steps.check_skip.outputs.skip != 'true' }}
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.core.artifact }}-${{ matrix.jobs.output }}
path: build_output/clash-linux-${{ matrix.jobs.output }}.tar.gz
Publish:
runs-on: ubuntu-latest
needs: [Get-Commit-id, Compile_Core]
if: always() && (needs.Get-Commit-id.outputs.current_id != needs.Get-Commit-id.outputs.upstream_id || needs.Get-Commit-id.outputs.current_smart_id != needs.Get-Commit-id.outputs.upstream_smart_id)
steps:
- name: Checkout OpenClash Repository
uses: actions/checkout@v6
with:
ref: core
- name: Download All Artifacts
uses: actions/download-artifact@v4
with:
path: ./downloaded-artifacts
- name: Organize Artifacts, Update Version, and Prepare Commit Data
id: organize_commit_data
run: |
commit_description=""
changes_made_to_dev_meta=false
changes_made_to_dev_smart=false
core_version_updated=false
mkdir -p ./dev/meta
mkdir -p ./dev/smart
if [ "${{ needs.Get-Commit-id.outputs.current_id }}" != "${{ needs.Get-Commit-id.outputs.upstream_id }}" ]; then
echo "Organizing MetaCubeX compiled files..."
if [ "${{ needs.Compile_Core.result }}" == "success" ]; then
rm -rf ./dev/meta/*
for artifact_dir in ./downloaded-artifacts/meta-core-*/; do
if [ -d "$artifact_dir" ] && [ -n "$(ls -A "$artifact_dir")" ]; then
echo "Copying $artifact_dir files..."
cp -rf "$artifact_dir". ./dev/meta/
changes_made_to_dev_meta=true
fi
done
if [ "$changes_made_to_dev_meta" = true ]; then
git add ./dev/meta
commit_description="Meta to ${{ needs.Get-Commit-id.outputs.upstream_id }}"
fi
else
echo "Compile_Core job failed, skipping meta..."
fi
fi
if [ "${{ needs.Get-Commit-id.outputs.current_smart_id }}" != "${{ needs.Get-Commit-id.outputs.upstream_smart_id }}" ]; then
echo "Organizing vernesong compiled files..."
if [ "${{ needs.Compile_Core.result }}" == "success" ]; then
rm -rf ./dev/smart/*
for artifact_dir in ./downloaded-artifacts/smart-core-*/; do
if [ -d "$artifact_dir" ] && [ -n "$(ls -A "$artifact_dir")" ]; then
echo "Copying $artifact_dir files..."
cp -rf "$artifact_dir". ./dev/smart/
changes_made_to_dev_smart=true
fi
done
if [ "$changes_made_to_dev_smart" = true ]; then
git add ./dev/smart
if [ -n "$commit_description" ]; then
commit_description="$commit_description, "
fi
commit_description="${commit_description}Smart to ${{ needs.Get-Commit-id.outputs.upstream_smart_id }}"
fi
else
echo "Compile_Core job failed, skipping smart..."
fi
fi
final_meta_id="${{ needs.Get-Commit-id.outputs.current_id }}"
if [ "${{ needs.Get-Commit-id.outputs.current_id }}" != "${{ needs.Get-Commit-id.outputs.upstream_id }}" ] && [ "$changes_made_to_dev_meta" = true ]; then
final_meta_id="${{ needs.Get-Commit-id.outputs.upstream_id }}"
fi
final_smart_id="${{ needs.Get-Commit-id.outputs.current_smart_id }}"
if [ "${{ needs.Get-Commit-id.outputs.current_smart_id }}" != "${{ needs.Get-Commit-id.outputs.upstream_smart_id }}" ] && [ "$changes_made_to_dev_smart" = true ]; then
final_smart_id="${{ needs.Get-Commit-id.outputs.upstream_smart_id }}"
fi
echo "Updating ./dev/core_version with:"
echo "Meta: $final_meta_id"
echo "Smart: $final_smart_id"
current_core_version_content=$(cat ./dev/core_version 2>/dev/null || printf "null\nnull")
new_core_version_content=$(printf "%s\n%s" "$final_meta_id" "$final_smart_id")
if [ "$current_core_version_content" != "$new_core_version_content" ]; then
echo "$final_meta_id" > ./dev/core_version
echo "$final_smart_id" >> ./dev/core_version
git add ./dev/core_version
core_version_updated=true
echo "./dev/core_version updated."
else
echo "./dev/core_version is already up to date."
fi
echo "Content of ./dev/core_version after potential update:"
cat ./dev/core_version
overall_changes_exist=false
if [ "$changes_made_to_dev_meta" = true ] || [ "$changes_made_to_dev_smart" = true ] || [ "$core_version_updated" = true ]; then
overall_changes_exist=true
fi
echo "commit_description_output=$commit_description" >> $GITHUB_OUTPUT
echo "overall_changes_exist_output=$overall_changes_exist" >> $GITHUB_OUTPUT
- name: Commit and push
env:
COMMIT_DESCRIPTION: ${{ steps.organize_commit_data.outputs.commit_description_output }}
OVERALL_CHANGES_EXIST: ${{ steps.organize_commit_data.outputs.overall_changes_exist_output }}
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
echo "Commit description from output: $COMMIT_DESCRIPTION"
echo "Overall changes exist from output: $OVERALL_CHANGES_EXIST"
if ! git diff --staged --quiet || [ "$OVERALL_CHANGES_EXIST" = "true" ]; then
final_commit_message="Auto update"
if [ -n "$COMMIT_DESCRIPTION" ]; then
final_commit_message="$final_commit_message: $COMMIT_DESCRIPTION"
elif [ "$OVERALL_CHANGES_EXIST" = "true" ]; then
final_commit_message="$final_commit_message: Update core versions"
fi
echo "Committing with message: $final_commit_message"
git commit -m "$final_commit_message"
git push
else
echo "No changes to commit."
fi

209
.github/workflows/compile_new_ipk.yml vendored Normal file
View File

@ -0,0 +1,209 @@
name: Compile The New Version OpenClash
on:
push:
branches:
- dev
paths:
- 'luci-app-openclash/Makefile'
workflow_dispatch:
jobs:
Get-Version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
current_version: ${{ steps.current_version.outputs.version }}
steps:
- name: Clone Repository
uses: actions/checkout@v6
with:
ref: ${{ github.ref_name }}
- name: New Version
id: version
run: |
echo "version=$(grep 'PKG_VERSION:=' ./luci-app-openclash/Makefile |awk -F '=' '{print $2}')" >> $GITHUB_OUTPUT
- name: Clone Repository
uses: actions/checkout@v6
with:
ref: package
- name: Current Version
id: current_version
run: |
echo "version=$(sed -n 1p ./${{ github.ref_name }}/version |awk -F 'v' '{print $2}')" >> $GITHUB_OUTPUT
Compile:
runs-on: ubuntu-latest
needs: Get-Version
if: ${{ needs.Get-Version.outputs.version != needs.Get-Version.outputs.current_version }}
strategy:
matrix:
target:
- { name: ipk, sdk_url: "https://mirrors.pku.edu.cn/openwrt/releases/22.03.0/targets/x86/64/openwrt-sdk-22.03.0-x86-64_gcc-11.2.0_musl.Linux-x86_64.tar.xz", sdk_tar: "SDK.tar.xz", sdk_backup_tar: "SDK.tar.bz2", sdk_dir: "SDK", artifact_pattern: "luci-app-openclash_*.ipk" }
- { name: apk, sdk_url: "snapshot", sdk_tar: "SNAPSDK.tar.zst", sdk_backup_tar: "SNAPSDK.tar.zst", sdk_dir: "SNAPSDK", artifact_pattern: "luci-app-openclash-*.apk" }
steps:
- name: Checkout OpenClash Source
uses: actions/checkout@v6
with:
ref: ${{ github.ref_name }}
- name: Install Dependencies
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install curl git tar zstd python3 python3-pyelftools python3-setuptools
- name: Download SDK
run: |
mkdir -p tmp
if [ "${{ matrix.target.name }}" = "ipk" ]; then
if ! curl -SLk --connect-timeout 30 --retry 2 "${{ matrix.target.sdk_url }}" -o "./tmp/${{ matrix.target.sdk_tar }}"; then
curl -SLk --connect-timeout 30 --retry 2 "https://archive.openwrt.org/chaos_calmer/15.05.1/ar71xx/generic/OpenWrt-SDK-15.05.1-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2" -o "./tmp/SDK.tar.xz"
cd tmp
tar xjf ${{ matrix.target.sdk_backup_tar }}
mv OpenWrt-SDK-* ${{ matrix.target.sdk_dir }}
else
cd tmp
tar xf ${{ matrix.target.sdk_tar }}
mv openwrt-sdk-* ${{ matrix.target.sdk_dir }}
fi
else
BASE_URL="https://downloads.openwrt.org/snapshots/targets/x86/64/"
SDK_NAME=$(curl -s $BASE_URL | grep -oE 'openwrt-sdk-x86-64[^"]+\.tar\.zst' | head -n 1)
if ! curl -SLk --connect-timeout 30 --retry 2 "$BASE_URL/$SDK_NAME" -o "./tmp/${{ matrix.target.sdk_tar }}"; then
BASE_URL2="https://mirrors.pku.edu.cn/files/openwrt/snapshots/targets/x86/64/"
SDK_NAME=$(curl -s $BASE_URL2 | grep -oE 'openwrt-sdk-x86-64[^"]+\.tar\.zst' | head -n 1)
curl -SLk --connect-timeout 30 --retry 2 "$BASE_URL2/$SDK_NAME" -o "./tmp/${{ matrix.target.sdk_tar }}"
fi
cd tmp
zstd -d ${{ matrix.target.sdk_tar }}
tar xf SNAPSDK.tar
SDK_DIR=$(tar tf SNAPSDK.tar | head -n 1 | cut -d/ -f1)
mv "$SDK_DIR" "${{ matrix.target.sdk_dir }}"
fi
- name: Copy OpenClash Source Codes
run: |
mkdir -p tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash
cp -rf ./luci-app-openclash/. tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/
if [ "${{ matrix.target.name }}" = "apk" ]; then
find tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash -type f -name "Makefile" -exec sed -i 's/DEPENDS:=+dnsmasq-full/DEPENDS:=+dnsmasq-full +luci-compat/g' {} \;
fi
- name: Update Third-Party Resources
run: |
CHNR_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/etc/openclash/china_ip_route.ipset
CHNR6_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/etc/openclash/china_ip6_route.ipset
ZASHBOARD_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/usr/share/openclash/ui/zashboard
METACUBEXD_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/usr/share/openclash/ui/metacubexd
GEOSITE_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/etc/openclash/GeoSite.dat
COUNTRY_MMDB_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/etc/openclash/Country.mmdb
# 更新 China IP Route
mkdir -p tmp
curl -sSL https://ispip.clang.cn/all_cn.txt -o tmp/china_ip_route.ipset
mkdir -p $(dirname $CHNR_PATH)
cp tmp/china_ip_route.ipset $CHNR_PATH
curl -sSL https://ispip.clang.cn/all_cn_ipv6.txt -o tmp/china_ip6_route.ipset
mkdir -p $(dirname $CHNR6_PATH)
cp tmp/china_ip6_route.ipset $CHNR6_PATH
# 更新 MetaCubeXD UI
mkdir -p tmp/metacubexd_zip
curl -sSL https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip -o tmp/metacubexd.zip
unzip -q -o tmp/metacubexd.zip -d tmp/metacubexd_zip
METACUBEXD_UNZIP_PATH="tmp/metacubexd_zip/metacubexd-gh-pages"
mkdir -p "$METACUBEXD_PATH"
rm -rf "$METACUBEXD_PATH"/*
cp -rf "$METACUBEXD_UNZIP_PATH"/* "$METACUBEXD_PATH"
# 更新 ZashBoard UI
mkdir -p tmp/zashboard_zip
curl -sSL https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip -o tmp/zashboard.zip
unzip -q -o tmp/zashboard.zip -d tmp/zashboard_zip
mkdir -p "$ZASHBOARD_PATH"
rm -rf "$ZASHBOARD_PATH"/*
cp -rf tmp/zashboard_zip/dist/* "$ZASHBOARD_PATH"
# 更新 GeoSite.dat
RULES_RELEASE=$(curl -s https://api.github.com/repos/Loyalsoldier/v2ray-rules-dat/releases/latest | jq -r '.tag_name')
curl -sSL "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/${RULES_RELEASE}/geosite.dat" -o tmp/GeoSite.dat
mkdir -p $(dirname "$GEOSITE_PATH")
cp tmp/GeoSite.dat "$GEOSITE_PATH"
# 更新 Country.mmdb
curl -sSL "https://github.com/alecthw/mmdb_china_ip_list/releases/latest/download/Country-lite.mmdb" -o tmp/Country.mmdb
mkdir -p $(dirname "$COUNTRY_MMDB_PATH")
cp tmp/Country.mmdb "$COUNTRY_MMDB_PATH"
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Minify CSS (lightningcss)
run: |
CSS_PATH="tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/www/luci-static/resources/openclash/css/oc.css"
SIZE_BEFORE=$(wc -c < "$CSS_PATH")
npx --yes --package lightningcss-cli lightningcss --minify --targets 'iOS >= 12, safari >= 12, android >= 4.4' "$CSS_PATH" -o "$CSS_PATH.tmp"
mv "$CSS_PATH.tmp" "$CSS_PATH"
SIZE_AFTER=$(wc -c < "$CSS_PATH")
echo "CSS minified: ${SIZE_BEFORE} -> ${SIZE_AFTER} bytes ($(( 100 - SIZE_AFTER * 100 / SIZE_BEFORE ))% reduction)"
- name: Compile po2lmo
run: |
cd tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/tools/po2lmo
make && sudo make install
- name: Compile OpenClash
run: |
cd tmp/${{ matrix.target.sdk_dir }}
if [ -f ../${{ matrix.target.sdk_tar }} ]; then
make defconfig
fi
make package/luci-app-openclash/compile V=99
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: openclash-${{ matrix.target.name }}
path: tmp/${{ matrix.target.sdk_dir }}/bin/packages/x86_64/base/${{ matrix.target.artifact_pattern }}
Post-Process:
runs-on: ubuntu-latest
needs: [Compile, Get-Version]
steps:
- name: Checkout package branch
uses: actions/checkout@v6
with:
ref: package
- name: Download IPK Artifact
uses: actions/download-artifact@v4
with:
name: openclash-ipk
path: ./
- name: Download APK Artifact
uses: actions/download-artifact@v4
with:
name: openclash-apk
path: ./
- name: Commit and Push New Version
run: |
rm -rf ./${{ github.ref_name }}/luci-app-openclash*
echo "v${{ needs.Get-Version.outputs.version }}" > ./${{ github.ref_name }}/version
echo "https://img.shields.io/badge/New Release-v${{ needs.Get-Version.outputs.version }}-orange.svg" >> ./${{ github.ref_name }}/version
cp ./luci-app-openclash_*.ipk "./${{ github.ref_name }}/"
cp ./luci-app-openclash-*.apk "./${{ github.ref_name }}/"
rm -f ./luci-app-openclash_*.ipk
rm -f ./luci-app-openclash-*.apk
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add .
git commit -m "Auto Release: v${{ needs.Get-Version.outputs.version }}"
git push

View File

@ -0,0 +1,95 @@
name: Compile The New Clash Premium Core
on:
#schedule:
# - cron: "0 21 * * 1,3,5"
workflow_dispatch:
jobs:
Get-Commit-id:
runs-on: ubuntu-latest
outputs:
current_id: ${{ steps.current_id.outputs.current_id }}
upstream_id: ${{ steps.upstream_id.outputs.upstream_id }}
steps:
- name: Apt Update
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install curl git gzip
- name: Clone OpenClash Repository
uses: actions/checkout@v4
with:
ref: core
- name: Get Current Version
id: current_id
run: |
echo "current_id=$(sed -n 2p ./dev/core_version)" >> $GITHUB_OUTPUT
echo "current_id: $(sed -n 2p ./dev/core_version)"
- name: Get Upstream Version
id: upstream_id
run: |
cd ..
mkdir tmp
GZNAME="clash-latest.gz"
curl -kSL --retry 2 https://release.dreamacro.workers.dev/latest//clash-linux-amd64-latest.gz -o ./tmp/$GZNAME
gzip -d ./tmp/$GZNAME
chmod +x ./tmp/clash-latest
echo "upstream_id=$(./tmp/clash-latest -v |awk -F ' ' '{print $2}')" >> $GITHUB_OUTPUT
echo "upstream_id: $(./tmp/clash-latest -v |awk -F ' ' '{print $2}')"
Compile:
runs-on: ubuntu-latest
needs: Get-Commit-id
if: ${{ needs.Get-Commit-id.outputs.current_id != needs.Get-Commit-id.outputs.upstream_id }}
steps:
- name: Apt Update
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install curl git tar gzip
- name: Setup UPX
run: |
cd ..
wget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz
tar xf upx-3.95-amd64_linux.tar.xz
echo "upx=../upx-3.95-amd64_linux/upx" >> $GITHUB_ENV
- name: Clone OpenClash Repository
uses: actions/checkout@v4
with:
ref: core
- name: Copy Scripts
run: |
cd ..
mkdir tmp
mkdir tmp/bin
cp ./OpenClash/.github/makefile/tun.sh ./tmp/tun.sh
- name: Compile Premium Clash
run: |
bash ../tmp/tun.sh
rm -rf ../tmp/tun.sh
- name: Change Version
run: |
sed -i '2d' ./dev/core_version
sed -i "2i ${{ needs.Get-Commit-id.outputs.upstream_id }}" ./dev/core_version
- name: Commit and push
run: |
rm -rf ./dev/premium/*
cp -rf "../tmp/." "./dev/premium/"
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add .
git commit -m "Auto update Premium core: ${{ needs.Get-Commit-id.outputs.upstream_id }}"
git push

View File

@ -0,0 +1,17 @@
name: Delete old workflow runs
on:
schedule:
- cron: '0 0 1 * *'
# Run monthly, at 00:00 on the 1st day of month.
jobs:
del_runs:
runs-on: ubuntu-latest
steps:
- name: Delete workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: 30
keep_minimum_runs: 6

View File

@ -0,0 +1,62 @@
name: Sync Core and Package to Master When release
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
Sync_core_to_master:
runs-on: ubuntu-latest
steps:
- name: Apt Update
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install git
- name: Clone OpenClash Repository
uses: actions/checkout@v6
with:
ref: core
- name: Sync dev core to master
run: |
if [ -n "$(diff -r ./master/ ./dev/)" ]; then
rm -rf ./master/premium
rm -rf ./master/dev
rm -rf ./master/meta/*
rm -rf ./master/smart/*
rm -rf ./master/core_version
cp -rf "./dev/." "./master/"
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add .
git commit -m "Release: Auto sync dev core"
git push
else
echo "master core already sync to dev, exit"
fi
- name: Clone OpenClash Repository
uses: actions/checkout@v6
with:
ref: package
- name: Sync dev package to master
run: |
if [ -n "$(diff -r ./master/ ./dev/)" ]; then
rm -rf ./master/README.md
rm -rf ./master/luci-app-openclash*
rm -rf ./master/version
cp -rf "./dev/." "./master/"
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add .
git commit -m "Release: Auto sync dev package"
git push
else
echo "master package already sync to dev, exit"
fi

View File

@ -0,0 +1,133 @@
name: Trim Core and Package History
on:
schedule:
- cron: "0 18 * * 3"
workflow_dispatch:
jobs:
trim:
runs-on: ubuntu-latest
strategy:
matrix:
branch: [core, package]
steps:
- name: Apt Update
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update
sudo apt-get -y install git
- name: Trim History
env:
REPO_URL: https://github.com/vernesong/OpenClash.git
BRANCH: ${{ matrix.branch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if [ "${BRANCH}" = "core" ]; then
MAX_COMMITS=20
LIMIT_COMMITS=10
else
MAX_COMMITS=50
LIMIT_COMMITS=30
fi
echo "Start full clone for history check (branch: ${BRANCH})..."
mkdir -p ../full_clone_tmp
cd ../full_clone_tmp
git clone --branch "${BRANCH}" --single-branch --no-tags --progress --depth=1 "$REPO_URL" .
MAX_FETCH_RETRIES=3
FETCH_OK=false
for i in $(seq 1 $MAX_FETCH_RETRIES); do
if git fetch --unshallow; then
FETCH_OK=true
break
else
echo "git fetch --unshallow failed (attempt $i/$MAX_FETCH_RETRIES), retrying..." >&2
sleep 2
fi
done
if [ "$FETCH_OK" = false ]; then
echo "Warning: git fetch --unshallow failed after $MAX_FETCH_RETRIES attempts, proceeding with available history." >&2
fi
count=$(git rev-list --count HEAD 2>/dev/null || echo 0)
echo "Current commit count on ${BRANCH} branch: $count"
if [ "$count" -le "$MAX_COMMITS" ]; then
echo "No trimming needed (commit count: $count <= $MAX_COMMITS)."
cd ..
rm -rf full_clone_tmp
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
echo "Collecting recent commits..."
mapfile -t recent_commits < <(git rev-list --max-count=$LIMIT_COMMITS HEAD)
echo "Looking for sync commit (grep 'Release: Auto sync')..."
sync_commit=$(git log --grep='Release: Auto sync' --format='%H' | tail -n 1 || true)
keep_commits=("${recent_commits[@]}")
if [ -n "$sync_commit" ]; then
found=false
for c in "${recent_commits[@]}"; do
if [ "$c" = "$sync_commit" ]; then
found=true
break
fi
done
if [ "$found" = false ]; then
keep_commits+=("$sync_commit")
fi
fi
echo "Rebuilding trimmed branch from kept commits (no extra commits)..."
ORIG_HEAD=$(git rev-parse HEAD)
LAST_COMMIT=""
for c in $(printf "%s\n" "${keep_commits[@]}" | tac); do
echo "Processing commit $c ..."
git checkout -- . || true
git checkout "$c" -- . || true
git add -A || true
TREE_ID=$(git write-tree)
AUTHOR_NAME=$(git show -s --format='%an' $c)
AUTHOR_EMAIL=$(git show -s --format='%ae' $c)
COMMIT_DATE=$(git show -s --format='%ad' --date=iso-strict $c)
COMMIT_MSG=$(git log --format=%B -n 1 $c)
echo "Commit message for $c:"
printf "%s\n" "$COMMIT_MSG"
MSG_FILE=$(mktemp)
printf "%s\n" "$COMMIT_MSG" > "$MSG_FILE"
if [ -z "$LAST_COMMIT" ]; then
NEW_COMMIT=$(GIT_AUTHOR_NAME="$AUTHOR_NAME" GIT_AUTHOR_EMAIL="$AUTHOR_EMAIL" GIT_AUTHOR_DATE="$COMMIT_DATE" git commit-tree $TREE_ID -F "$MSG_FILE")
else
NEW_COMMIT=$(GIT_AUTHOR_NAME="$AUTHOR_NAME" GIT_AUTHOR_EMAIL="$AUTHOR_EMAIL" GIT_AUTHOR_DATE="$COMMIT_DATE" git commit-tree $TREE_ID -p "$LAST_COMMIT" -F "$MSG_FILE")
fi
rm -f "$MSG_FILE"
LAST_COMMIT=$NEW_COMMIT
done
if [ -n "$LAST_COMMIT" ]; then
git update-ref refs/heads/__tmp_history_trim $LAST_COMMIT
echo "Built commit: $LAST_COMMIT"
if [ -n "${GITHUB_TOKEN:-}" ]; then
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/vernesong/OpenClash.git"
fi
git push --force --quiet origin "$LAST_COMMIT:refs/heads/${BRANCH}" || true
echo "Trim complete. New commit count (remote unchanged locally until fetched): $(git rev-list --count HEAD)"
else
echo "No kept commits; skipping trim." >&2
fi
cd ..
rm -rf full_clone_tmp

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-2026 vernesong
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

125
README.md Normal file
View File

@ -0,0 +1,125 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/vernesong/OpenClash/dev/img/logo.png" alt="Clash" width="200">
<br>OpenClash<br>
</h1>
<p align="center">
本插件是一个可运行在 OpenWrt 上的<a href="https://github.com/MetaCubeX/mihomo" target="_blank"> Mihomo(Clash) </a>客户端
</p>
<p align="center">
兼容 Shadowsocks、ShadowsocksR、Vmess、Trojan、Snell 等协议,根据灵活的规则配置实现策略代理
</p>
使用手册
---
* [Wiki](https://github.com/vernesong/OpenClash/wiki)
下载地址
---
* IPK & APK [前往下载](https://github.com/vernesong/OpenClash/releases)
依赖
---
* luci
* luci-base
* dnsmasq-full
* bash
* curl
* ca-bundle
* ipset
* ip-full
* ruby
* ruby-yaml
* unzip
* iptables(iptables)
* kmod-ipt-nat(iptables)
* iptables-mod-tproxy(iptables)
* iptables-mod-extra(iptables)
* kmod-tun(TUN模式)
* luci-compat(Luci >= 19.07)
* ip6tables-mod-nat(iptables-ipv6)
* kmod-inet-diag(PROCESS-NAME)
* kmod-nft-tproxy(Firewall4)
编译
---
从 OpenWrt 的 [SDK](https://archive.openwrt.org/chaos_calmer/15.05.1/ar71xx/generic/OpenWrt-SDK-15.05.1-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2) 编译
```bash
# 解压下载好的 SDK
curl -SLk --connect-timeout 30 --retry 2 "https://archive.openwrt.org/chaos_calmer/15.05.1/ar71xx/generic/OpenWrt-SDK-15.05.1-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2" -o "/tmp/SDK.tar.bz2"
cd \tmp
tar xjf SDK.tar.bz2
cd OpenWrt-SDK-15.05.1-*
# Clone 项目
mkdir package/luci-app-openclash
cd package/luci-app-openclash
git init
git remote add -f origin https://github.com/vernesong/OpenClash.git
git config core.sparsecheckout true
echo "luci-app-openclash" >> .git/info/sparse-checkout
git pull --depth 1 origin master
git branch --set-upstream-to=origin/master master
# 编译 po2lmo (如果有po2lmo可跳过)
pushd luci-app-openclash/tools/po2lmo
make && sudo make install
popd
# 开始编译
# 先回退到SDK主目录
cd ../..
make package/luci-app-openclash/luci-app-openclash/compile V=99
# IPK文件位置
./bin/ar71xx/packages/base/luci-app-openclash_*-beta_all.ipk
```
```bash
# 同步源码
cd package/luci-app-openclash/luci-app-openclash
git pull
# 您也可以直接拷贝 `luci-app-openclash` 文件夹至其他 `OpenWrt` 项目的 `Package` 目录下随固件编译
make menuconfig
# 选择要编译的包 LuCI -> Applications -> luci-app-openclash
```
许可
---
* [MIT License](https://github.com/vernesong/OpenClash/blob/master/LICENSE)
* 内核 [Mihomo](https://github.com/MetaCubeX/mihomo) by [MetaCubeX](https://github.com/MetaCubeX)
* 本项目代码基于 [Luci For Clash](https://github.com/frainzy1477/luci-app-clash) by [frainzy1477](https://github.com/frainzy1477)
* IP检查 [IP](https://ip.skk.moe/) by [SukkaW](https://ip.skk.moe/)
* 控制面板 [zashboard](https://github.com/Zephyruso/zashboard) by [Zephyruso](https://github.com/Zephyruso)
* 控制面板 [yacd](https://github.com/haishanh/yacd) by [haishanh](https://github.com/haishanh)
* 流媒体解锁检测 [RegionRestrictionCheck](https://github.com/lmc999/RegionRestrictionCheck) by [lmc999](https://github.com/lmc999)
请作者喝杯咖啡
---
* PayPal
<p align="left">
<a href="https://ko-fi.com/vernesong"><img width="300" src="https://www.ko-fi.com/img/githubbutton_sm.svg"> </a>
</p>
* USDT-BSC
<p align="left">
<img width="300" src="https://github.com/vernesong/OpenClash/raw/master/img/USDT-Wallet.png">
</p>

14
announcement Normal file
View File

@ -0,0 +1,14 @@
[
{
"zh": "覆写模块从 v0.47.081 开始支持 [YAML] 块来进行配置文件覆写,格式使用 yaml, 从而降低代码覆写编辑的难度, 使用方式请参考 default 文件的示例。",
"en": "Overwrite module now supports YAML blocks for configuration file overwriting, using YAML format, which reduces the difficulty of code overwriting. Please refer to the default file example for usage."
},
{
"zh": "覆写模块现已支持远程订阅,更加方便插件的远程管理及统一配置、一键订阅,欢迎各位分享配置。",
"en": "Overwrite module now supports subscription, more convenient remote management and configuration, one-click subscription, welcome to share the configuration."
},
{
"zh": "Smart 策略组内核现已支持 policy-priority 参数来自定义调整节点权重,请自行替换测试、使用。",
"en": "The Smart policy group core now supports the policy-priority parameter to customize node weights. Please replace it for testing and use."
}
]

BIN
img/BTC-Wallet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
img/ETH-Wallet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
img/USDT-Wallet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
img/config-subscribe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
img/config-subscribe1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
img/config.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
img/game-settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
img/game1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
img/game2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
img/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
img/meta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
img/proxy1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
img/proxy2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
img/proxy3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
img/proxy4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
img/servers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
img/set1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
img/set10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
img/set11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
img/set12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
img/set2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
img/set3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
img/set4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
img/set6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
img/set7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
img/set8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
img/set9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
img/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

BIN
img/state.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
img/state1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
img/state2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
img/state3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
img/state4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
img/state5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

146
luci-app-openclash/Makefile Normal file
View File

@ -0,0 +1,146 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclash
PKG_VERSION:=0.47.110
PKG_MAINTAINER:=vernesong <https://github.com/vernesong/OpenClash>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)/config
config PACKAGE_kmod-inet-diag
default y if PACKAGE_$(PKG_NAME)
config PACKAGE_luci-compat
default y if PACKAGE_$(PKG_NAME)
config PACKAGE_kmod-nft-tproxy
default y if PACKAGE_firewall4
config PACKAGE_kmod-ipt-nat
default y if ! PACKAGE_firewall4
config PACKAGE_iptables-mod-tproxy
default y if ! PACKAGE_firewall4
config PACKAGE_iptables-mod-extra
default y if ! PACKAGE_firewall4
config PACKAGE_dnsmasq_full_ipset
default y if ! PACKAGE_firewall4
config PACKAGE_dnsmasq_full_nftset
default y if PACKAGE_firewall4
config PACKAGE_ipset
default y if ! PACKAGE_firewall4
endef
define Package/$(PKG_NAME)
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=LuCI support for clash
PKGARCH:=all
DEPENDS:=+dnsmasq-full +bash +curl +ca-bundle +ip-full \
+ruby +ruby-yaml +kmod-tun +unzip
MAINTAINER:=vernesong
endef
define Package/$(PKG_NAME)/description
A LuCI support for clash
endef
define Build/Prepare
$(CP) $(CURDIR)/root $(PKG_BUILD_DIR)
$(CP) $(CURDIR)/luasrc $(PKG_BUILD_DIR)
$(foreach po,$(wildcard ${CURDIR}/po/zh-cn/*.po), \
po2lmo $(po) $(PKG_BUILD_DIR)/$(patsubst %.po,%.lmo,$(notdir $(po)));)
chmod 0755 $(PKG_BUILD_DIR)/root/etc/init.d/openclash
chmod -R 0755 $(PKG_BUILD_DIR)/root/usr/share/openclash/
exit 0
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/conffiles
endef
define Package/$(PKG_NAME)/preinst
#!/bin/sh
if [ -f "/etc/config/openclash" ] && [ ! -f "/tmp/openclash.bak" ]; then
cp -f "/etc/config/openclash" "/tmp/openclash.bak" >/dev/null 2>&1
cp -rf "/etc/openclash" "/tmp/openclash" >/dev/null 2>&1
cp -rf "/usr/share/openclash/ui" "/tmp/openclash_ui" >/dev/null 2>&1
cp -rf "/www/luci-static/resources/openclash/pac" "/tmp/pac" >/dev/null 2>&1
fi
exit 0
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
exit 0
endef
define Package/$(PKG_NAME)/prerm
#!/bin/sh
[ -n "$(pidof clash)" ] && /etc/init.d/openclash stop 2>/dev/null
if [ -f "/etc/config/openclash" ] && [ ! -f "/tmp/openclash.bak" ]; then
cp -f "/etc/config/openclash" "/tmp/openclash.bak" >/dev/null 2>&1
cp -rf "/etc/openclash" "/tmp/openclash" >/dev/null 2>&1
cp -rf "/usr/share/openclash/ui" "/tmp/openclash_ui" >/dev/null 2>&1
cp -rf "/www/luci-static/resources/openclash/pac" "/tmp/pac" >/dev/null 2>&1
fi
exit 0
endef
define Package/$(PKG_NAME)/postrm
#!/bin/sh
DEFAULT_DNSMASQ_CFGID="$$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}' 2>/dev/null)"
if [ -f "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID" ]; then
DNSMASQ_CONF_DIR="$$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID" 2>/dev/null)"
else
DNSMASQ_CONF_DIR="/tmp/dnsmasq.d"
fi
DNSMASQ_CONF_DIR=$${DNSMASQ_CONF_DIR%*/}
rm -rf /etc/openclash >/dev/null 2>&1
rm -rf /etc/config/openclash >/dev/null 2>&1
rm -rf /tmp/openclash.log >/dev/null 2>&1
rm -rf /tmp/openclash_start.log >/dev/null 2>&1
rm -rf /tmp/openclash_last_version >/dev/null 2>&1
rm -rf /tmp/openclash.change >/dev/null 2>&1
rm -rf /tmp/clash_last_version >/dev/null 2>&1
rm -rf /usr/share/openclash >/dev/null 2>&1
rm -rf ${DNSMASQ_CONF_DIR}/dnsmasq_openclash_custom_domain.conf >/dev/null 2>&1
rm -rf ${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute_pass.conf >/dev/null 2>&1
rm -rf ${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute6_pass.conf >/dev/null 2>&1
rm -rf /tmp/etc/openclash >/dev/null 2>&1
rm -rf /tmp/openclash_announcement >/dev/null 2>&1
rm -rf /www/luci-static/resources/openclash >/dev/null 2>&1
rm -rf /tmp/oix* >/dev/null 2>&1
sed -i '/OpenClash Append/,/OpenClash Append End/d' "/usr/lib/lua/luci/model/network.lua" >/dev/null 2>&1
sed -i '/.*kB maximum content size*/c\HTTP_MAX_CONTENT = 1024*100 -- 100 kB maximum content size' /usr/lib/lua/luci/http.lua >/dev/null 2>&1
sed -i '/.*kB maximum content size*/c\export let HTTP_MAX_CONTENT = 1024*100; // 100 kB maximum content size' /usr/share/ucode/luci/http.uc >/dev/null 2>&1
uci -q delete firewall.openclash
uci -q commit firewall
[ -f "/etc/config/ucitrack" ] && {
uci -q delete ucitrack.@openclash[-1]
uci -q commit ucitrack
}
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /var/luci-indexcache /var/luci-indexcache.* >/dev/null 2>&1
rm -rf /tmp/luci-modulecache /var/luci-modulecache >/dev/null 2>&1
exit 0
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
$(INSTALL_DATA) $(PKG_BUILD_DIR)/*.*.lmo $(1)/usr/lib/lua/luci/i18n/
$(CP) $(PKG_BUILD_DIR)/root/* $(1)/
$(CP) $(PKG_BUILD_DIR)/luasrc/* $(1)/usr/lib/lua/luci/
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require("luci.model.uci").cursor()
m = SimpleForm("openclash",translate("OpenClash"))
m.description = translate("A Mihomo(Clash) Client For OpenWrt")
m.reset = false
m.submit = false
m:section(SimpleSection).template = "openclash/status"
if fs.uci_get_config("config", "oix_token") then
m:append(Template("openclash/oixcloud"))
end
m:append(Template("openclash/myip"))
m:append(Template("openclash/developer"))
m:append(Template("openclash/select_git_cdn"))
m:append(Template("openclash/config_edit"))
m:append(Template("openclash/config_upload"))
return m

View File

@ -0,0 +1,620 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local json = require "luci.jsonc"
local datatype = require "luci.cbi.datatypes"
-- 优化 CBI UI(新版 LuCI 专用)
local function optimize_cbi_ui()
HTTP.write([[
<script type="text/javascript">
// 修正上移、下移按钮名称
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
btn.value = "]] .. translate("Move up") .. [[";
});
document.querySelectorAll("input.btn.cbi-button.cbi-button-down").forEach(function(btn) {
btn.value = "]] .. translate("Move down") .. [[";
});
// 删除控件和说明之间的多余换行
document.querySelectorAll("div.cbi-value-description").forEach(function(descDiv) {
var prev = descDiv.previousSibling;
while (prev && prev.nodeType === Node.TEXT_NODE && prev.textContent.trim() === "") {
prev = prev.previousSibling;
}
if (prev && prev.nodeType === Node.ELEMENT_NODE && prev.tagName === "BR") {
prev.remove();
}
});
</script>
]])
end
font_green = [[<b style=color:green>]]
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
local op_mode = string.sub(luci.sys.exec('uci get openclash.config.operation_mode 2>/dev/null'),0,-2)
if not op_mode then op_mode = "redir-host" end
local lan_ip = fs.lanip()
m = Map("openclash", translate("Overwrite Settings"))
m.pageaction = false
m.description = translate("Note: To restore the default configuration, try accessing:").." <a href='javascript:void(0)' onclick='javascript:restore_config(this)'>http://"..lan_ip.."/cgi-bin/luci/admin/services/openclash/restore</a>"..
"<br/>"..font_green..translate("For More Useful Meta Core Functions Go Wiki")..": "..font_off.."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/\")'>"..translate("https://wiki.metacubex.one/").."</a>"
s = m:section(TypedSection, "openclash")
s.anonymous = true
s:tab("settings", translate("General Settings"))
s:tab("dns", "DNS "..translate("Settings"))
s:tab("meta", translate("Meta Settings"))
s:tab("smart", translate("Smart Settings"))
s:tab("rules", translate("Rules Setting"))
----- General Settings
o = s:taboption("settings", ListValue, "interface_name", translate("Bind Network Interface"))
local de_int = SYS.exec("ip route |grep 'default' |awk '{print $5}' 2>/dev/null") or SYS.exec("/usr/share/openclash/openclash_get_network.lua 'dhcp'")
o.description = translate("Default Interface Name:").." "..font_green..bold_on..de_int..bold_off..font_off..translate(",Try Enable If Network Loopback")
local interfaces = SYS.exec("ls -l /sys/class/net/ 2>/dev/null |awk '{print $9}' 2>/dev/null")
for interface in string.gmatch(interfaces, "%S+") do
o:value(interface)
end
o:value("0", translate("Disable"))
o.default = "0"
o = s:taboption("settings", Value, "tolerance", translate("URL-Test Group Tolerance").."(ms)")
o.description = translate("Switch To The New Proxy When The Delay Difference Between Old and The Fastest Currently is Greater Than This Value")
o:value("0", translate("Disable"))
o:value("100")
o:value("150")
o.datatype = "uinteger"
o.default = "0"
o = s:taboption("settings", Value, "urltest_interval_mod", translate("URL-Test Interval Modify").."(s)")
o.description = translate("Modify The URL-Test Interval In The Config")
o:value("0", translate("Disable"))
o:value("180")
o:value("300")
o.datatype = "uinteger"
o.default = "0"
o = s:taboption("settings", Value, "urltest_address_mod", translate("URL-Test Address Modify"))
o.description = translate("Modify The URL-Test Address In The Config")
o:value("0", translate("Disable"))
o:value("http://www.gstatic.com/generate_204")
o:value("http://cp.cloudflare.com/generate_204")
o:value("https://cp.cloudflare.com/generate_204")
o:value("http://captive.apple.com/generate_204")
o.default = "0"
o = s:taboption("settings", Value, "github_address_mod", translate("Github Address Modify"))
o.description = translate("Modify The Github Address In The Config And OpenClash With Proxy(CDN) To Prevent File Download Faild. Format Reference:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://ghfast.top/\")'>https://ghfast.top/</a>"
o:value("0", translate("Disable"))
o:value("https://fastly.jsdelivr.net/")
o:value("https://testingcf.jsdelivr.net/")
o:value("https://cdn.jsdelivr.net/")
o.default = "0"
o = s:taboption("settings", ListValue, "log_level", translate("Log Level"))
o.description = translate("Select Core's Log Level")
o:value("0", translate("Disable"))
o:value("info", translate("Info Mode"))
o:value("warning", translate("Warning Mode"))
o:value("error", translate("Error Mode"))
o:value("debug", translate("Debug Mode"))
o:value("silent", translate("Silent Mode"))
o.default = "0"
o = s:taboption("settings", Value, "dns_port")
o.title = translate("DNS Port")
o.default = "7874"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "proxy_port")
o.title = translate("Redir Port")
o.default = "7892"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "tproxy_port")
o.title = translate("TProxy Port")
o.default = "7895"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "http_port")
o.title = translate("HTTP(S) Port")
o.default = "7890"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "socks_port")
o.title = translate("SOCKS5 Port")
o.default = "7891"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "mixed_port")
o.title = translate("Mixed Port")
o.default = "7893"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
---- DNS Settings
o = s:taboption("dns", Flag, "enable_custom_dns", font_red..bold_on..translate("Custom DNS Setting")..bold_off..font_off)
o.description = font_red..bold_on..translate("Set OpenClash Upstream DNS Resolve Server")..bold_off..font_off
o.default = 0
o = s:taboption("dns", Flag, "enable_respect_rules", font_red..bold_on..translate("Respect Rules").."(respect-rules)"..bold_off..font_off)
o.description = font_red..bold_on..translate("Whether or not The Connection to the DNS Server Follow the Rules in Config")..bold_off..font_off
o.default = 0
o = s:taboption("dns", Flag, "append_wan_dns", translate("Append Upstream DNS"))
o.description = translate("Append The Upstream Assigned DNS And Gateway IP To The Nameserver")
o.default = 1
o = s:taboption("dns", Flag, "append_default_dns", translate("Append Default DNS"))
o.description = translate("Automatically Append Compliant DNS to default-nameserver")
o.default = 1
if op_mode == "fake-ip" then
o = s:taboption("dns", Value, "fakeip_range", translate("Fake-IP Range").." (IPv4 Cidr)")
o.description = translate("Set Fake-IP Range").." (IPv4 Cidr)"
o:value("0", translate("Disable"))
o:value("198.18.0.1/16")
o.default = "0"
o.placeholder = "198.18.0.1/16"
function o.validate(self, value)
if value == "0" then
return "0"
end
if datatype.cidr4(value) then
return value
end
return "198.18.0.1/16"
end
o = s:taboption("dns", Flag, "store_fakeip", translate("Persistence Fake-IP"))
o.description = font_red..bold_on..translate("Cache Fake-IP DNS Resolution Records To File, Improve The Response Speed After Startup")..bold_off..font_off
o.default = 1
end
o = s:taboption("dns", Flag, "custom_fallback_filter", translate("Fallback-Filter"))
o.description = translate("Take Effect If Fallback DNS Setted, Prevent DNS Pollution")
o.default = 0
custom_fallback_filter = s:taboption("dns", Value, "custom_fallback_fil")
custom_fallback_filter.template = "cbi/tvalue"
custom_fallback_filter.rows = 20
custom_fallback_filter.wrap = "off"
custom_fallback_filter:depends("custom_fallback_filter", "1")
function custom_fallback_filter.cfgvalue(self, section)
return fs.readfile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml") or ""
end
function custom_fallback_filter.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml")
if value ~= old_value then
fs.writefile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml", value)
end
end
end
if op_mode == "fake-ip" then
o = s:taboption("dns", Flag, "custom_fakeip_filter", translate("Fake-IP-Filter"))
o.default = 0
o = s:taboption("dns", ListValue, "custom_fakeip_filter_mode", translate("Fake-IP-Filter-Mode"))
o.description = translate("Fake-IP is not returned if the matching succeeds when blacklist mode or Fake-IP is returned if the matching succeeds when whitelist mode")
o.default = "blacklist"
o:value("blacklist", translate("Blacklist Mode"))
o:value("whitelist", translate("Whitelist Mode"))
o:value("rule", translate("Rule Mode"))
o:depends("custom_fakeip_filter", "1")
custom_fake_black = s:taboption("dns", Value, "custom_fake_filter")
custom_fake_black.template = "cbi/tvalue"
custom_fake_black.description = translate("Domain Names In The List Do Not Return Fake-IP, One rule per line")
custom_fake_black.rows = 20
custom_fake_black.wrap = "off"
custom_fake_black:depends("custom_fakeip_filter", "1")
function custom_fake_black.cfgvalue(self, section)
return fs.readfile("/etc/openclash/custom/openclash_custom_fake_filter.list") or ""
end
function custom_fake_black.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_fake_filter.list")
if value ~= old_value then
fs.writefile("/etc/openclash/custom/openclash_custom_fake_filter.list", value)
end
end
end
end
o = s:taboption("dns", Flag, "custom_name_policy", translate("Nameserver-Policy"))
o.default = 0
custom_domain_dns_policy = s:taboption("dns", Value, "custom_domain_dns_core")
custom_domain_dns_policy.template = "cbi/tvalue"
custom_domain_dns_policy.description = translate("Domain Names In The List Use The Custom DNS Server, But Still Return Fake-IP Results, One rule per line")
custom_domain_dns_policy.rows = 20
custom_domain_dns_policy.wrap = "off"
custom_domain_dns_policy:depends("custom_name_policy", "1")
function custom_domain_dns_policy.cfgvalue(self, section)
return fs.readfile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list") or ""
end
function custom_domain_dns_policy.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list")
if value ~= old_value then
fs.writefile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list", value)
end
end
end
o = s:taboption("dns", Flag, "custom_proxy_server_policy", translate("Proxy-Server-Nameserver-Policy"))
o.default = 0
custom_proxy_server_dns_policy = s:taboption("dns", Value, "custom_proxy_server_dns_policy")
custom_proxy_server_dns_policy.template = "cbi/tvalue"
custom_proxy_server_dns_policy.description = translate("Domain Names In The List Use The Custom DNS Server, But Still Return Fake-IP Results, One rule per line")
custom_proxy_server_dns_policy.rows = 20
custom_proxy_server_dns_policy.wrap = "off"
custom_proxy_server_dns_policy:depends("custom_proxy_server_policy", "1")
function custom_proxy_server_dns_policy.cfgvalue(self, section)
return fs.readfile("/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list") or ""
end
function custom_proxy_server_dns_policy.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list")
if value ~= old_value then
fs.writefile("/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list", value)
end
end
end
o = s:taboption("dns", Flag, "custom_host", translate("Hosts"))
o.default = 0
custom_hosts = s:taboption("dns", Value, "custom_hosts")
custom_hosts.template = "cbi/tvalue"
custom_hosts.description = translate("Custom Hosts Here, You May Need to Turn off The Rebinding Protection Option of Dnsmasq When Hosts Has Set a Reserved Address, For More Go:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://lancellc.gitbook.io/clash/clash-config-file/dns/host\")'>https://lancellc.gitbook.io/clash/clash-config-file/dns/host</a>"
custom_hosts.rows = 20
custom_hosts.wrap = "off"
custom_hosts:depends("custom_host", "1")
function custom_hosts.cfgvalue(self, section)
return fs.readfile("/etc/openclash/custom/openclash_custom_hosts.list") or ""
end
function custom_hosts.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_hosts.list")
if value ~= old_value then
fs.writefile("/etc/openclash/custom/openclash_custom_hosts.list", value)
end
end
end
-- Meta
o = s:taboption("meta", Flag, "enable_tcp_concurrent", font_red..bold_on..translate("Enable Tcp Concurrent")..bold_off..font_off)
o.description = font_red..bold_on..translate("TCP Concurrent Request IPs, Choose The Lowest Latency One To Connection")..bold_off..font_off
o.default = "0"
o = s:taboption("meta", Flag, "enable_unified_delay", font_red..bold_on..translate("Enable Unified Delay")..bold_off..font_off)
o.description = font_red..bold_on..translate("Change The Delay Calculation Method To Remove Extra Delays Such as Handshaking")..bold_off..font_off
o.default = "0"
o = s:taboption("meta", Value, "global_ua", translate("Global User-Agent"))
o:value("0", translate("Disable"))
o:value("clash-verge/v2.4.5")
o:value("clash.meta/1.19.20")
o:value("Clash")
o.default = "0"
o = s:taboption("meta", ListValue, "find_process_mode", translate("Enable Process Rule"))
o.description = translate("Whether to Enable Process Rules, Only Works on Routerself, If You Are Not Sure, Please Choose off Which Useful in Router Environment, Depend on kmod-inet-diag")
o:value("0", translate("Disable"))
o:value("off", translate("OFF "))
o:value("always", translate("Always "))
o:value("strict", translate("strict "))
o.default = "0"
o = s:taboption("meta", ListValue, "geodata_loader", translate("Geodata Loader Mode"))
o:value("0", translate("Disable"))
o:value("memconservative", translate("Memconservative"))
o:value("standard", translate("Standard"))
o.default = "0"
o = s:taboption("meta", Flag, "enable_geoip_dat", translate("Enable GeoIP Dat"))
o.description = translate("Replace GEOIP MMDB With GEOIP Dat, Large Size File")..", "..font_red..bold_on..translate("Need Download First")..bold_off..font_off
o.default = 0
o = s:taboption("meta", Flag, "enable_meta_sniffer", font_red..bold_on..translate("Enable Sniffer")..bold_off..font_off)
o.description = font_red..bold_on..translate("Sniffer Will Prevent Domain Name Proxy and DNS Hijack Failure")..bold_off..font_off
o.default = 1
o = s:taboption("meta", Flag, "enable_meta_sniffer_pure_ip", translate("Forced Sniff Pure IP"))
o.description = translate("Forced Sniff Pure IP Connections")
o.default = 1
o:depends("enable_meta_sniffer", "1")
o = s:taboption("meta", Flag, "enable_meta_sniffer_custom", translate("Custom Sniffer Settings"))
o.description = translate("Custom The Force and Skip Sniffing Doamin Lists")
o.default = 0
o:depends("enable_meta_sniffer", "1")
sniffer_custom = s:taboption("meta", Value, "sniffer_custom")
sniffer_custom:depends("enable_meta_sniffer_custom", "1")
sniffer_custom.template = "cbi/tvalue"
sniffer_custom.rows = 20
sniffer_custom.wrap = "off"
function sniffer_custom.cfgvalue(self, section)
return fs.readfile("/etc/openclash/custom/openclash_custom_sniffer.yaml") or ""
end
function sniffer_custom.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_sniffer.yaml")
if value ~= old_value then
fs.writefile("/etc/openclash/custom/openclash_custom_sniffer.yaml", value)
end
end
end
-- Smart Settings
o = s:taboption("smart", Flag, "auto_smart_switch", font_red..bold_on..translate("Smart Auto Switch")..bold_off..font_off)
o.description = font_red..bold_on..translate("Auto Switch Url-test and Load-balance Group to Smart Group")..bold_off..font_off
o.default = 0
o = s:taboption("smart", Value, "smart_policy_priority", translate("Policy Priority"))
o.default = ""
o.placeholder = "Premium:0.9;SG:1.3"
o.description = translate("Nodes Weight Priority, <1 Means Lower Priority, >1 Means Higher Priority, The Default is 1, Pattern Support Regex and String")
o = s:taboption("smart", Flag, "smart_prefer_asn", font_red..bold_on..translate("Prefer-ASN")..bold_off..font_off)
o.description = translate("Select Nodes Force Lookup and Use Target ASN Info First For More Stable Experience")
o.default = 0
o = s:taboption("smart", Flag, "smart_enable_lgbm", font_red..bold_on..translate("Enable LightGBM Model")..bold_off..font_off)
o.description = font_red..bold_on..translate("Use LightGBM Model To Predict Weight")..bold_off..font_off
o.default = 0
o = s:taboption("smart", Flag, "smart_collect", translate("Colletct Training Data"))
o.default = 0
o = s:taboption("smart", Value, "smart_collect_size", translate("Data Colletct File Size (MB)"))
o.default = 100
o:depends("smart_collect", "1")
o.description = translate("Limit The File Size of Collected Data, The Default is 100MB")
o = s:taboption("smart", Value, "smart_collect_rate", translate("Data Colletct Rate"))
o.default = 1
o:depends("smart_collect", "1")
o.description = translate("Data Acquisition Rate, Desirable Values are 0-1, The Default is 1")
o = s:taboption("smart", Flag, "lgbm_auto_update", translate("Auto Update LightGBM Model"))
o.default = 0
o = s:taboption("smart", Value, "lgbm_update_interval", translate("Update Interval(hour)"))
o.default = "72"
o:depends("lgbm_auto_update", "1")
o = s:taboption("smart", Value, "lgbm_custom_url")
o.title = translate("Custom Model URL")
o.rmempty = true
o.description = translate("Custom LightGBM Model URL, Click Button Below To Refresh After Edit")
o:value("https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin", translate("Light Version").." "..translate("(Default)"))
o:value("https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model-middle.bin", translate("Middle Version"))
o:value("https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model-large.bin", translate("Large Version"))
o.default = "https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin"
o:depends("lgbm_auto_update", "1")
o = s:taboption("smart", Button, translate("Model Update"))
o.description = translate("Current Version:").." "..font_green..bold_on..translate(fs.get_resourse_mtime("/etc/openclash/Model.bin"))..bold_off..font_off
o.title = translate("Update Model")
o.inputtitle = translate("Check And Update")
o.inputstyle = "reload"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/usr/share/openclash/openclash_lgbm.sh >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
o = s:taboption("smart", DummyValue, "flush_smart_cache", translate("Flush Smart Cache"))
o.template = "openclash/flush_smart_cache"
---- Rules Settings
o = s:taboption("rules", Flag, "enable_rule_proxy", translate("Rule Match Proxy Mode"))
o.description = translate("Append Some Rules to Config, Allow Only Traffic Proxies that Match the Rule, Prevent BT/P2P Passing")
o.default = 0
o = s:taboption("rules", Flag, "enable_custom_clash_rules", font_red..bold_on..translate("Custom Clash Rules")..bold_off..font_off)
o.description = translate("Use Custom Rules")
o.default = 0
custom_rules = s:taboption("rules", Value, "custom_rules")
custom_rules:depends("enable_custom_clash_rules", 1)
custom_rules.template = "cbi/tvalue"
custom_rules.description = font_green..bold_on..translate("(Priority)")..bold_off..font_off.." "..translate("Custom Rules Here, For More Go:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/config/rules/\")'>https://wiki.metacubex.one/config/rules/</a>".." ,"..translate("IP To CIDR:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"http://ip2cidr.com\")'>http://ip2cidr.com</a>"
custom_rules.rows = 20
custom_rules.wrap = "off"
function custom_rules.cfgvalue(self, section)
return fs.readfile("/etc/openclash/custom/openclash_custom_rules.list") or ""
end
function custom_rules.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_rules.list")
if value ~= old_value then
fs.writefile("/etc/openclash/custom/openclash_custom_rules.list", value)
end
end
end
custom_rules_2 = s:taboption("rules", Value, "custom_rules_2")
custom_rules_2:depends("enable_custom_clash_rules", 1)
custom_rules_2.template = "cbi/tvalue"
custom_rules_2.description = font_green..bold_on..translate("(Extended)")..bold_off..font_off.." "..translate("Custom Rules Here, For More Go:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/config/rules/\")'>https://wiki.metacubex.one/config/rules/</a>".." ,"..translate("IP To CIDR:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"http://ip2cidr.com\")'>http://ip2cidr.com</a>"
custom_rules_2.rows = 20
custom_rules_2.wrap = "off"
function custom_rules_2.cfgvalue(self, section)
return fs.readfile("/etc/openclash/custom/openclash_custom_rules_2.list") or ""
end
function custom_rules_2.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_rules_2.list")
if value ~= old_value then
fs.writefile("/etc/openclash/custom/openclash_custom_rules_2.list", value)
end
end
end
-- [[ Edit Custom DNS ]] --
ds = m:section(TypedSection, "dns_servers", translate("Add Custom DNS Servers")..translate("(Take Effect After Choose Above)"))
ds.anonymous = true
ds.addremove = true
ds.sortable = true
ds.template = "openclash/tblsection"
ds.extedit = DISP.build_url("admin/services/openclash/custom-dns-edit/%s")
function ds.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "group", name)
end
HTTP.redirect(ds.extedit % sid)
return
end
end
---- enable flag
o = ds:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- group
o = ds:option(ListValue, "group", translate("DNS Server Group"))
o:value("nameserver", translate("nameserver "))
o:value("fallback", translate("fallback "))
o:value("default", translate("default-nameserver"))
o.default = "nameserver"
o.rempty = false
---- IP address
o = ds:option(Value, "ip", translate("DNS Server Address"))
o.placeholder = translate("Not Null")
o.datatype = "or(host, string)"
o.rmempty = true
---- port
o = ds:option(Value, "port", translate("DNS Server Port"))
o.datatype = "port"
o.rempty = true
---- type
o = ds:option(ListValue, "type", translate("DNS Server Type"))
o:value("udp", translate("UDP"))
o:value("tcp", translate("TCP"))
o:value("tls", translate("TLS"))
o:value("https", translate("HTTPS"))
o:value("quic", translate("QUIC"))
o.default = "udp"
o.rempty = false
---- Disable-IPv6
o = ds:option(Flag, "disable_ipv6", translate("Disable-IPv6"))
o.rmempty = false
o.default = o.disbled
-- [[ Edit Authentication ]] --
s = m:section(TypedSection, "authentication", translate("Set Authentication of SOCKS5/HTTP(S)"))
s.anonymous = true
s.addremove = true
s.sortable = false
s.template = "cbi/tblsection"
s.rmempty = false
s.render = function(self, ...)
Map.render(self, ...)
if type(optimize_cbi_ui) == "function" then
optimize_cbi_ui()
end
end
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- username
o = s:option(Value, "username", translate("Username"))
o.placeholder = translate("Not Null")
o.rempty = true
---- password
o = s:option(Value, "password", translate("Password"))
o.placeholder = translate("Not Null")
o.rmempty = true
local t = {
{Commit, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/config_editor"))
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,334 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local json = require "luci.jsonc"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
local age_section
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map(openclash, translate("Config Subscribe Edit"))
m.pageaction = false
m.description=translate("Convert Subscribe function of Online is Supported By subconverter Written By tindy X") ..
"<br/>"..
"<br/>"..translate("Subconverter external configuration (subscription conversion template) Description: https://github.com/tindy2013/subconverter#external-configuration-file")..
"<br/>"..
"<br/>"..translate("If you need to customize the external configuration file (subscription conversion template), please write it according to the instructions, upload it to the accessible location of the external network, and fill in the address correctly when using it")..
"<br/>"..
"<br/>"..translate("If you have a recommended external configuration file (subscription conversion template), you can modify by following The file format of /usr/share/openclash/res/sub_ini.list and pr")
m.redirect = DISP.build_url("admin/services/openclash/config-subscribe")
if m.uci:get(openclash, sid) ~= "config_subscribe" then
HTTP.redirect(m.redirect)
return
end
-- [[ Config Subscribe Setting ]]--
s = m:section(NamedSection, sid, "config_subscribe")
s.anonymous = true
s.addremove = false
---- name
o = s:option(Value, "name", translate("Config Alias"))
o.description = font_red..bold_on..translate("Name For Distinguishing")..bold_off..font_off
o.placeholder = translate("config")
o.rmempty = true
---- address
o = s:option(Value, "address", translate("Subscribe Address"))
o.template = "cbi/tvalue"
o.rows = 10
o.wrap = "off"
o.description = font_red..bold_on..translate("SS/SSR/Vmess or Other Link And Subscription Address is Supported When Online Subscription Conversion is Enabled, Multiple Links Should be One Per Line or Separated By |")..bold_off..font_off
o.placeholder = translate("Not Null")
o.rmempty = false
function o.validate(self, value)
if value then
value = value:gsub("\r\n?", "\n")
value = value:gsub("%c*$", "")
end
return value
end
---- UA
o = s:option(Value, "sub_ua", "User-Agent")
o.default = "clash-verge/v2.4.5"
o.description = font_red..bold_on..translate("Used for Downloading Subscriptions, Defaults to").." "..o.default..bold_off..font_off
o:value("clash-verge/v2.4.5")
o:value("clash.meta/1.19.20")
o:value("Clash")
o.rmempty = true
o = s:option(ListValue, "config_age_algo", translate("Age Key Type"))
o.description = font_red..bold_on..translate("Age Encryption For Config, Click For More:")..bold_off..font_off.." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/config/proxy-providers/?age-secret-key#age-secret-key\")'>"..translate("Age Encryption Introduce").."</a>"
o:value("keygen", "x25519")
o:value("pq", "PQ (mlkem768-x25519)")
o.rmempty = true
function o.cfgvalue(self, section)
local name = m.uci:get(openclash, section, "name") or section
local v = ""
m.uci:foreach(openclash, "config_age_secret", function(s)
if s.name == name then
v = s.algo or ""
age_section = s['.name']
return false
end
end)
return v
end
function o.write(self, section, value)
local name = m.uci:get(openclash, section, "name") or section
m.uci:foreach(openclash, "config_age_secret", function(s)
if s.name == name then
age_section = s['.name']
return false
end
end)
if not age_section and value and value ~= "" then
age_section = m.uci:add(openclash, "config_age_secret")
if age_section then m.uci:set(openclash, age_section, "name", name) end
end
if age_section then
if value and value ~= "" then
m.uci:set(openclash, age_section, "algo", value)
else
m.uci:delete(openclash, age_section, "algo")
end
end
end
function o.remove(self, section)
self:write(section, "")
end
o = s:option(Value, "config_age_secret", translate("Secret Key"))
o.rmempty = true
o.placeholder = "AGE-SECRET-KEY-..."
function o.cfgvalue(self, section)
local name = m.uci:get(openclash, section, "name") or section
local v = ""
m.uci:foreach(openclash, "config_age_secret", function(s)
if s.name == name then
v = s.secret or ""
age_section = s['.name']
return false
end
end)
return v
end
function o.write(self, section, value)
local name = m.uci:get(openclash, section, "name") or section
m.uci:foreach(openclash, "config_age_secret", function(s)
if s.name == name then
age_section = s['.name']
return false
end
end)
if not age_section and value and value ~= "" then
age_section = m.uci:add(openclash, "config_age_secret")
if age_section then m.uci:set(openclash, age_section, "name", name) end
end
if age_section then
if value and value ~= "" then
m.uci:set(openclash, age_section, "secret", value)
else
m.uci:delete(openclash, age_section, "secret")
end
end
end
function o.remove(self, section)
self:write(section, "")
end
o = s:option(Value, "config_age_public", translate("Public Key"))
o.rmempty = true
o.placeholder = "age..."
function o.cfgvalue(self, section)
local name = m.uci:get(openclash, section, "name") or section
local v = ""
m.uci:foreach(openclash, "config_age_secret", function(s)
if s.name == name then
v = s.public or ""
age_section = s['.name']
return false
end
end)
return v
end
function o.write(self, section, value)
local name = m.uci:get(openclash, section, "name") or section
m.uci:foreach(openclash, "config_age_secret", function(s)
if s.name == name then
age_section = s['.name']
return false
end
end)
if not age_section and value and value ~= "" then
age_section = m.uci:add(openclash, "config_age_secret")
if age_section then m.uci:set(openclash, age_section, "name", name) end
end
if age_section then
if value and value ~= "" then
m.uci:set(openclash, age_section, "public", value)
else
m.uci:delete(openclash, age_section, "public")
end
end
end
function o.remove(self, section)
self:write(section, "")
end
o = s:option(DummyValue, "_generate_age_btn", "")
o.template = "openclash/generate_age"
---- subconverter
o = s:option(Flag, "sub_convert", translate("Subscribe Convert Online"))
o.description = translate("Convert Subscribe Online With Template")
o.default = 0
---- Convert Address
o = s:option(Value, "convert_address", translate("Convert Address"))
o.rmempty = true
o.description = font_red..bold_on..translate("Note: There is A Risk of Privacy Leakage in Online Convert")..bold_off..font_off
o:depends("sub_convert", "1")
o:value("https://api.asailor.org/sub", translate("api.asailor.org"))
o:value("https://api.wcc.best/sub", translate("api.wcc.best"))
o.default = "https://api.asailor.org/sub"
o.placeholder = "https://api.asailor.org/sub"
---- Template
o = s:option(ListValue, "template", translate("Template Name"))
o.rmempty = true
o:depends("sub_convert", "1")
file = io.open("/usr/share/openclash/res/sub_ini.list", "r");
for l in file:lines() do
if l ~= "" and l ~= nil then
o:value(string.sub(luci.sys.exec(string.format("echo '%s' |awk -F ',' '{print $1}' 2>/dev/null",l)),1,-2))
end
end
file:close()
o:value("0", translate("Custom Template"))
---- Custom Template
o = s:option(Value, "custom_template_url", translate("Custom Template URL"))
o.rmempty = true
o.placeholder = translate("Not Null")
o.datatype = "or(host, string)"
o:depends("template", "0")
---- emoji
o = s:option(ListValue, "emoji", translate("Emoji"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- udp
o = s:option(ListValue, "udp", translate("UDP Enable"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- skip-cert-verify
o = s:option(ListValue, "skip_cert_verify", translate("skip-cert-verify"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- sort
o = s:option(ListValue, "sort", translate("Sort"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- node type
o = s:option(ListValue, "node_type", translate("Append Node Type"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- rule provider
o = s:option(ListValue, "rule_provider", translate("Use Rule Provider"))
o.description = font_red..bold_on..translate("Note: Please Make Sure Backend Service Supports This Feature")..bold_off..font_off
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- custom params
o = s:option(DynamicList, "custom_params", translate("Custom Params"))
o.description = font_red..bold_on..translate("eg: \"rename=match@replace\" , \"rename=\\s+([2-9])[xX]@ (HIGH:$1)\"")..bold_off..font_off
o.rmempty = false
o:depends("sub_convert", "1")
---- key
o = s:option(DynamicList, "keyword", font_red..bold_on..translate("Keyword Match")..bold_off..font_off)
o.description = font_red..bold_on..translate("eg: hk or tw&bgp")..bold_off..font_off
o.rmempty = true
---- exkey
o = s:option(DynamicList, "ex_keyword", font_red..bold_on..translate("Exclude Keyword Match")..bold_off..font_off)
o.description = font_red..bold_on..translate("eg: hk or tw&bgp")..bold_off..font_off
o.rmempty = true
---- de_exkey
o = s:option(MultiValue, "de_ex_keyword", font_red..bold_on..translate("Exclude Keyword Match Default")..bold_off..font_off)
o.rmempty = true
o:value(translate("Expire"))
o:value(translate("Traffic"))
o:value(translate("Plan"))
o:value(translate("Official"))
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
local to_delete = {}
m.uci:foreach(openclash, "config_age_secret", function(s)
local pub = m.uci:get(openclash, s['.name'], "public") or ""
local sec = m.uci:get(openclash, s['.name'], "secret") or ""
if (pub == "" or pub == nil) and (sec == "" or sec == nil) then
table.insert(to_delete, s['.name'])
end
end)
for _, n in ipairs(to_delete) do
m.uci:delete(openclash, n)
end
m.uci:commit(openclash)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
m.uci:revert(openclash, age_section)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,175 @@
local m, s, o
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
-- 优化 CBI UI(新版 LuCI 专用)
local function optimize_cbi_ui()
HTTP.write([[
<script type="text/javascript">
// 修正上移、下移按钮名称
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
btn.value = "]] .. translate("Move up") .. [[";
});
document.querySelectorAll("input.btn.cbi-button.cbi-button-down").forEach(function(btn) {
btn.value = "]] .. translate("Move down") .. [[";
});
// 删除控件和说明之间的多余换行
document.querySelectorAll("div.cbi-value-description").forEach(function(descDiv) {
var prev = descDiv.previousSibling;
while (prev && prev.nodeType === Node.TEXT_NODE && prev.textContent.trim() === "") {
prev = prev.previousSibling;
}
if (prev && prev.nodeType === Node.ELEMENT_NODE && prev.tagName === "BR") {
prev.remove();
}
});
</script>
]])
end
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map("openclash", translate("Config Subscribe"))
m.pageaction = false
s = m:section(TypedSection, "openclash")
s.anonymous = true
---- update Settings
o = s:option(Flag, "auto_update", translate("Auto Update"))
o.description = translate("Auto Update Server subscription")
o.default = 0
o = s:option(ListValue, "config_auto_update_mode", translate("Update Mode"))
o:depends("auto_update", "1")
o:value("0", translate("Appointment Mode"))
o:value("1", translate("Loop Mode"))
o.default = "0"
o.rmempty = true
o = s:option(ListValue, "config_update_week_time", translate("Update Time (Every Week)"))
o:depends("config_auto_update_mode", "0")
o:value("*", translate("Every Day"))
o:value("1", translate("Every Monday"))
o:value("2", translate("Every Tuesday"))
o:value("3", translate("Every Wednesday"))
o:value("4", translate("Every Thursday"))
o:value("5", translate("Every Friday"))
o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday"))
o.default = "1"
o.rmempty = true
o = s:option(ListValue, "auto_update_time", translate("Update time (every day)"))
o:depends("config_auto_update_mode", "0")
for t = 0,23 do
o:value(t, t..":00")
end
o.default = "0"
o.rmempty = true
o = s:option(Value, "config_update_interval", translate("Update Interval(min)"))
o.default = "60"
o.datatype = "integer"
o:depends("config_auto_update_mode", "1")
o.rmempty = true
-- [[ Edit Server ]] --
s = m:section(TypedSection, "config_subscribe", translate("Config Subscribe Edit"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = DISP.build_url("admin/services/openclash/config-subscribe-edit/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
HTTP.redirect(s.extedit % sid)
return
end
end
s.render = function(self, ...)
Map.render(self, ...)
if type(optimize_cbi_ui) == "function" then
optimize_cbi_ui()
end
end
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- name
o = s:option(DummyValue, "name", translate("Config Alias"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("config")
end
---- address
o = s:option(TextValue, "address", translate("Subscribe Address"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
function o.validate(self, value)
if value then
value = value:gsub("\r\n?", "\n")
value = value:gsub("%c*$", "")
end
return value
end
---- template
o = s:option(DummyValue, "template", translate("Template Name"))
function o.cfgvalue(...)
if Value.cfgvalue(...) ~= "0" then
return Value.cfgvalue(...) or translate("None")
else
return translate("Custom Template")
end
end
---- update
o = s:option(DummyValue, "name", translate("Update"))
o.template = "openclash/update_config"
local t = {
{Commit, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Update Config")
o.inputstyle = "apply"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/usr/share/openclash/openclash.sh >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,433 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require("luci.model.uci").cursor()
local CHIF = "0"
font_green = [[<b style=color:green>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
align_mid = [[<p align="center">]]
align_mid_off = [[</p>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
function default_config_set(f)
local cf = fs.uci_get_config("config", "config_path")
if cf == "/etc/openclash/config/"..f or not cf or cf == "" or not fs.isfile(cf) then
if CHIF == "1" and cf == "/etc/openclash/config/"..f then
return
end
local fis = fs.glob("/etc/openclash/config/*")[1]
if fis ~= nil then
fcf = fs.basename(fis)
if fcf then
uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..fcf)
uci:commit("openclash")
end
else
uci:set("openclash", "config", "config_path", "/etc/openclash/config/config.yaml")
uci:commit("openclash")
end
end
end
ful = SimpleForm("upload", translate("Config Manage"), nil)
ful.reset = false
ful.submit = false
sul =ful:section(SimpleSection, "")
o = sul:option(FileUpload, "")
o.template = "openclash/upload"
um = sul:option(DummyValue, "", nil)
um.template = "openclash/dvalue"
local dir, fd, clash
dir = "/etc/openclash/config/"
proxy_pro_dir="/etc/openclash/proxy_provider/"
rule_pro_dir="/etc/openclash/rule_provider/"
core_dir="/etc/openclash/core/core/"
backup_dir="/tmp/"
HTTP.setfilehandler(
function(meta, chunk, eof)
local fp = HTTP.formvalue("file_type")
if not fd then
if not meta then return end
if fp == "config" then
if meta and chunk then fd = nixio.open(dir .. meta.file, "w") end
elseif fp == "proxy-provider" then
if meta and chunk then fd = nixio.open(proxy_pro_dir .. meta.file, "w") end
elseif fp == "rule-provider" then
if meta and chunk then fd = nixio.open(rule_pro_dir .. meta.file, "w") end
elseif fp == "clash_meta" then
create_core_dir=fs.mkdir(core_dir)
if meta and chunk then fd = nixio.open(core_dir .. meta.file, "w") end
elseif fp == "backup-file" then
if meta and chunk then fd = nixio.open(backup_dir .. meta.file, "w") end
end
if not fd then
um.value = translate("upload file error.")
return
end
end
if chunk and fd then
fd:write(chunk)
end
if eof and fd then
fd:close()
fd = nil
if fp == "config" then
CHIF = "1"
if IsYamlFile(meta.file) then
default_config_set(meta.file)
end
if IsYmlFile(meta.file) then
local ymlname=string.lower(string.sub(meta.file,0,-5))
local c=fs.rename(dir .. meta.file,"/etc/openclash/config/".. ymlname .. ".yaml")
local yamlname=ymlname .. ".yaml"
default_config_set(yamlname)
end
um.value = translate("File saved to") .. ' "/etc/openclash/config/"'
elseif fp == "proxy-provider" then
um.value = translate("File saved to") .. ' "/etc/openclash/proxy_provider/"'
elseif fp == "rule-provider" then
um.value = translate("File saved to") .. ' "/etc/openclash/rule_provider/"'
elseif fp == "clash_meta" then
local archive_path = core_dir .. meta.file
if string.lower(string.sub(meta.file, -7, -1)) == ".tar.gz" then
-- tar.gz
os.execute(string.format("tar -C '/etc/openclash/core/core' -xzf '%s' >/dev/null 2>&1", archive_path))
os.execute(string.format("rm -f '%s' >/dev/null 2>&1", archive_path))
local first_file_cmd = "find /etc/openclash/core/core -type f ! -name '*.tar.gz' ! -name '*.tar' ! -name '*.gz' 2>/dev/null | head -1"
local first_file = io.popen(first_file_cmd):read("*line")
if first_file and first_file ~= "" then
os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", first_file, fp))
end
elseif string.lower(string.sub(meta.file, -4, -1)) == ".tar" then
-- tar
os.execute(string.format("tar -C '/etc/openclash/core/core' -xf '%s' >/dev/null 2>&1", archive_path))
os.execute(string.format("rm -f '%s' >/dev/null 2>&1", archive_path))
local first_file_cmd = "find /etc/openclash/core/core -type f ! -name '*.tar' ! -name '*.gz' 2>/dev/null | head -1"
local first_file = io.popen(first_file_cmd):read("*line")
if first_file and first_file ~= "" then
os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", first_file, fp))
end
elseif string.lower(string.sub(meta.file, -3, -1)) == ".gz" then
-- gz
os.execute(string.format("gzip -fd '%s' >/dev/null 2>&1", archive_path))
os.execute(string.format("rm -f '%s' >/dev/null 2>&1", archive_path))
local first_file_cmd = "find /etc/openclash/core/core -type f ! -name '*.gz' 2>/dev/null | head -1"
local first_file = io.popen(first_file_cmd):read("*line")
if first_file and first_file ~= "" then
os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", first_file, fp))
end
else
os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", (core_dir .. meta.file), fp))
end
os.execute(string.format("chmod 4755 '/etc/openclash/core/%s' >/dev/null 2>&1", fp))
os.execute(string.format("rm -rf %s >/dev/null 2>&1", core_dir))
um.value = translate("File saved to") .. ' "/etc/openclash/core/"'
elseif fp == "backup-file" then
os.execute("tar -C '/etc/openclash/' -xzf %s >/dev/null 2>&1" % (backup_dir .. meta.file))
os.execute("mv /etc/openclash/openclash /etc/config/openclash >/dev/null 2>&1")
fs.unlink(backup_dir .. meta.file)
um.value = translate("Backup File Restore Successful!")
end
end
end
)
if HTTP.formvalue("upload") then
if not um.value then
um.value = translate("No Specify Upload File")
end
end
local e,a={}
for t,o in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(o)
if a then
e[t]={}
e[t].name=fs.basename(o)
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",a.mtime)
if fs.uci_get_config("config", "config_path") and string.sub(fs.uci_get_config("config", "config_path"), 23, -1) == e[t].name then
e[t].state=translate("Enabled")
else
e[t].state=translate("Disabled")
end
e[t].size=fs.filesize(a.size)
e[t].remove=0
end
end
form=SimpleForm("config_file_list",translate("Config File List"))
form.reset=false
form.submit=false
tb=form:section(Table,e)
st=tb:option(DummyValue,"state",translate("State"))
nm=tb:option(DummyValue,"name",translate("Config Alias"))
sb=tb:option(DummyValue,"name",translate("Subscription Info"))
mt=tb:option(DummyValue,"mtime",translate("Update Time"))
sz=tb:option(DummyValue,"size",translate("Size"))
st.template="openclash/cfg_check"
sb.template="openclash/sub_info_show"
btnis=tb:option(Button,"switch",translate("SwiTch"))
btnis.render=function(o,t,a)
if not e[t] then return false end
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
a.display=""
else
a.display="none"
end
o.inputstyle="apply"
Button.render(o,t,a)
end
btnis.write=function(a,t)
uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..e[t].name)
uci:commit("openclash")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
btned=tb:option(Button,"edit",translate("Edit"))
btned.inputstyle="apply"
btned.write=function(a,t)
local file_path = "/etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "config") .. "?file=" .. HTTP.urlencode(file_path))
end
btnrn=tb:option(DummyValue,"/etc/openclash/config/",translate("Rename"))
btnrn.template="openclash/input_rename"
btnrn.rawhtml = true
btnrn.render=function(c,t,a)
if not e[t] then return end
c.value = e[t].name
DummyValue.render(c,t,a)
end
local actions = tb:option(ListValue, "actions", translate("Other"))
actions.render = function(self, t, a)
if not e[t] then return end
self.keylist = {}
self.vallist = {}
-- Servers manage
table.insert(self.keylist, "servers_manage")
table.insert(self.vallist, translate("Servers Manage"))
-- Copy
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
table.insert(self.keylist, "copy")
table.insert(self.vallist, translate("Copy Config"))
end
-- Download
table.insert(self.keylist, "download")
table.insert(self.vallist, translate("Download Config"))
-- Download Running
if NXFS.access("/etc/openclash/"..e[t].name) then
table.insert(self.keylist, "download_run")
table.insert(self.vallist, translate("Download Running Config"))
end
-- Remove
table.insert(self.keylist, "remove")
table.insert(self.vallist, translate("Remove"))
ListValue.render(self, t, a)
end
local btnapply = tb:option(Button, "apply", translate("Apply"))
btnapply.inputstyle = "apply"
btnapply.write = function(self, t)
if not e[t] then return end
local action = self.map:formvalue("cbid." .. self.map.config .. "." .. t .. ".actions")
if action == "servers_manage" then
local file_path = "/etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers") .. "?file=" .. HTTP.urlencode(file_path))
elseif action == "copy" then
local num = 1
while true do
num = num + 1
if not fs.isfile("/etc/openclash/config/"..fs.filename(e[t].name).."("..num..")"..".yaml") then
fs.copy("/etc/openclash/config/"..e[t].name, "/etc/openclash/config/"..fs.filename(e[t].name).."("..num..")"..".yaml")
break
end
end
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
elseif action == "download" then
local sPath, sFile, fd, block
sPath = "/etc/openclash/config/"..e[t].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then return end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block == 0) then break end
HTTP.write(block)
end
fd:close()
HTTP.close()
elseif action == "download_run" then
local sPath, sFile, fd, block
sPath = "/etc/openclash/"..e[t].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then return end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block == 0) then break end
HTTP.write(block)
end
fd:close()
HTTP.close()
elseif action == "remove" then
fs.unlink("/etc/openclash/history/"..fs.filename(e[t].name)..".db")
fs.unlink("/etc/openclash/"..fs.basename(e[t].name))
local a=fs.unlink("/etc/openclash/config/"..fs.basename(e[t].name))
default_config_set(fs.basename(e[t].name))
if a then table.remove(e,t) end
HTTP.redirect(DISP.build_url("admin", "services", "openclash","config"))
end
end
p = SimpleForm("provider_file_manage",translate("Provider File Manage"))
p.reset = false
p.submit = false
local provider_manage = {
{proxy_mg, rule_mg}
}
promg = p:section(Table, provider_manage)
o = promg:option(Button, "proxy_mg", " ")
o.inputtitle = translate("Proxy Provider File List")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage"))
end
o = promg:option(Button, "rule_mg", " ")
o.inputtitle = translate("Rule Providers File List")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage"))
end
m = SimpleForm("openclash",translate("Config File Edit"))
m.reset = false
m.submit = false
local tab = {
{user, default}
}
s = m:section(Table, tab)
s.description = align_mid..translate("Support syntax check, press").." "..font_green..bold_on.."F10"..bold_off..font_off.." "..translate("to control diff option, press").." "..font_green..bold_on.."F11"..bold_off..font_off.." "..translate("to enter full screen editing mode")..align_mid_off
s.anonymous = true
s.addremove = false
local conf = fs.uci_get_config("config", "config_path")
local dconf = "/usr/share/openclash/res/default.yaml"
if not conf then conf = "/etc/openclash/config/config.yaml" end
local conf_name = fs.basename(conf)
if not conf_name then conf_name = "config.yaml" end
local sconf = "/etc/openclash/"..conf_name
sev = s:option(TextValue, "user")
---sev.description = align_mid..translate("Modify Your Config file:").." "..font_green..bold_on..conf_name..bold_off..font_off.." "..translate("Here, Except The Settings That Were Taken Over")..align_mid_off
sev.rows = 40
sev.wrap = "off"
sev.cfgvalue = function(self, section)
return fs.readfile(conf) or fs.readfile(dconf) or ""
end
sev.write = function(self, section, value)
if (CHIF == "0") then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile(conf)
if value ~= old_value then
fs.writefile(conf, value)
end
end
end
def = s:option(TextValue, "default")
if fs.isfile(sconf) then
---def.description = align_mid..translate("Config File Edited By OpenClash For Running")..align_mid_off
else
---def.description = align_mid..translate("Default Config File With Correct Template")..align_mid_off
end
def.rows = 40
def.wrap = "off"
def.readonly = true
def.cfgvalue = function(self, section)
return fs.readfile(sconf) or fs.readfile(dconf) or ""
end
def.write = function(self, section, value)
end
local t = {
{Commit, Create, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
uci:commit("openclash")
end
o = a:option(DummyValue, "Create", " ")
o.rawhtml = true
o.template = "openclash/input_file_name"
o.value = "/etc/openclash/config/"
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
uci:set("openclash", "config", "enable", 1)
uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/config_editor"))
return ful , form , p , m

View File

@ -0,0 +1,184 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local SYS = require "luci.sys"
local DISP = require "luci.dispatcher"
local HTTP = require "luci.http"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_green = [[<b style=color:green>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map(openclash, translate("Add Custom DNS Servers"))
m.pageaction = false
m.redirect = DISP.build_url("admin/services/openclash/config-overwrite")
if m.uci:get(openclash, sid) ~= "dns_servers" then
HTTP.redirect(m.redirect)
return
end
-- [[ Edit Custom DNS ]] --
s = m:section(NamedSection, sid, "dns_servers")
s.anonymous = true
s.addremove = false
---- group
o = s:option(ListValue, "group", translate("DNS Server Group"))
o.description = font_red..bold_on..translate("NameServer Group Must Be Set")..bold_off..font_off
o:value("nameserver", translate("NameServer "))
o:value("fallback", translate("FallBack "))
o:value("default", translate("Default-NameServer"))
o.default = "nameserver"
o.rempty = false
---- IP address
o = s:option(Value, "ip", translate("DNS Server Address"))
o.description = translate("Do Not Add Type Ahead")
o.placeholder = translate("Not Null")
o.datatype = "or(host, string)"
o.rmempty = true
---- port
o = s:option(Value, "port", translate("DNS Server Port"))
o.description = translate("Require When Use Non-Standard Port")
o.datatype = "port"
o.rempty = true
---- type
o = s:option(ListValue, "type", translate("DNS Server Type"))
o.description = translate("Communication protocol")
o:value("udp", translate("UDP"))
o:value("tcp", translate("TCP"))
o:value("tls", translate("TLS"))
o:value("https", translate("HTTPS"))
o:value("quic", translate("QUIC"))
o.default = "udp"
o.rempty = false
---- interface
o = s:option(Value, "interface", translate("Specific Interface"))
o.description = translate("DNS Lookup Only Through The Specific Interface")
local interfaces = SYS.exec("ls -l /sys/class/net/ 2>/dev/null |awk '{print $9}' 2>/dev/null")
for interface in string.gmatch(interfaces, "%S+") do
o:value(interface)
end
o:value("Disable", translate("Disable"))
o.default = "Disable"
o.rempty = false
---- direct-nameserver
o = s:option(Flag, "direct_nameserver", translate("Direct Nameserver"), translate("Use For Domain Need Direct"))
o.rmempty = false
o.default = o.disbled
---- Node Domain Resolve
o = s:option(Flag, "node_resolve", translate("Node Domain Resolve"), translate("Use For Node Domain Resolve"))
o.rmempty = false
o.default = o.disbled
---- disable-reuse
o = s:option(Flag, "disable_reuse", translate("Disable Reuse"), translate("Disable Reuse The Connection"))
o:depends("type", "tls")
o.rmempty = false
o.default = o.disbled
---- Force HTTP/3
o = s:option(Flag, "http3", translate("Force HTTP/3"), translate("Force HTTP/3 to connect"))
o:depends("type", "https")
o.rmempty = false
o.default = o.disbled
---- Skip-cert-verify
o = s:option(Flag, "skip_cert_verify", translate("skip-cert-verify"), translate("skip-cert-verify"))
o:depends("type", "https")
o:depends("type", "quic")
o.rmempty = false
o.default = o.disbled
---- ECS Subnet
o = s:option(Value, "ecs_subnet", translate("ECS Subnet"),translate("Specify the ECS Subnet Address"))
o:depends("type", "https")
o.rmempty = true
o.datatype = "ipaddr"
o:value("1.1.1.1/24")
---- ECS Override
o = s:option(Flag, "ecs_override", translate("ECS Override"),translate("Override the ECS Subnet Address"))
o:depends("type", "https")
o.rmempty = false
o.default = o.disbled
---- disable-ipv4
o = s:option(Flag, "disable_ipv4", translate("Disable-IPv4"),translate("Drop The Type of DNS Responsed"))
o.rmempty = false
o.default = o.disbled
---- disable-ipv6
o = s:option(Flag, "disable_ipv6", translate("Disable-IPv6"),translate("Drop The Type of DNS Responsed"))
o.rmempty = false
o.default = o.disbled
---- disable-qtype
o = s:option(DynamicList, "disable_qtype", translate("Disable-Qtype"),translate("Drop The Type of DNS Responsed"))
o.rmempty = true
o.datatype = "uinteger"
o.default = o.disbled
---- Proxy group
o = s:option(Value, "specific_group", translate("Specific Group (Support Regex)"))
o.description = translate("Group Use For Proxy The DNS")
o:depends("group", "nameserver")
o:depends("group", "fallback")
local groupnames,filename
filename = m.uci:get(openclash, "config", "config_path")
if filename then
groupnames = SYS.exec(string.format('ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "YAML.load_file(\'%s\')[\'proxy-groups\'].each do |i| puts i[\'name\']+\'##\' end" 2>/dev/null',filename))
if groupnames then
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
end
end
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
o:value(s.name)
end
end)
o:value("DIRECT")
o:value("GLOBAL")
o:value("Disable", translate("Disable"))
o.default = "Disable"
o.rempty = false
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,333 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
local file_path = fs.get_file_path_from_request()
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers"))
return
end
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Edit Group"))
m.pageaction = false
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
if m.uci:get(openclash, sid) ~= "groups" then
HTTP.redirect(m.redirect)
return
end
-- [[ Groups Setting ]]--
s = m:section(NamedSection, sid, "groups")
s.anonymous = true
s.addremove = false
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
o = s:option(ListValue, "type", translate("Group Type"))
o.rmempty = false
o.description = translate("Choose The Operation Mode")
o:value("select", translate("Manual-Select"))
o:value("smart", translate("Smart-Select"))
o:value("url-test", translate("URL-Test"))
o:value("fallback", translate("Fallback"))
o:value("load-balance", translate("Load-Balance"))
o = s:option(Value, "name", translate("Group Name"))
o.rmempty = false
o.default = "Group - "..sid
o = s:option(ListValue, "disable_udp", translate("Disable UDP"))
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o.rmempty = true
o = s:option(Value, "test_url", translate("Test URL"))
o:value("http://cp.cloudflare.com/generate_204")
o:value("http://www.gstatic.com/generate_204")
o:value("https://cp.cloudflare.com/generate_204")
o.rmempty = true
o:depends("type", "url-test")
o:depends("type", "fallback")
o:depends("type", "load-balance")
o:depends("type", "smart")
o = s:option(Value, "test_interval", translate("Test Interval(s)"))
o.default = "300"
o.rmempty = true
o:depends("type", "url-test")
o:depends("type", "fallback")
o:depends("type", "load-balance")
o:depends("type", "smart")
o = s:option(ListValue, "strategy", translate("Strategy Type"))
o.rmempty = true
o.description = translate("Choose The Load-Balance's Strategy Type")
o:value("round-robin", translate("Round-robin"))
o:value("consistent-hashing", translate("Consistent-hashing"))
o:value("sticky-sessions", translate("Sticky-sessions"))
o:depends("type", "load-balance")
o = s:option(ListValue, "uselightgbm", translate("Uselightgbm"))
o.description = translate("Use LightGBM Model For Smart Group Weight Prediction")
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o.rmempty = true
o:depends("type", "smart")
o = s:option(ListValue, "collectdata", translate("Collectdata"))
o.description = translate("Collect Datas For Smart Group Model Training")
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o.rmempty = true
o:depends("type", "smart")
o = s:option(Value, "policy_priority", translate("Policy Priority"))
o.description = translate("The Priority Of The Nodes, The Higher Than 1, The More Likely It Is To Be Selected, The Default Is 1, Support Regex")
o.rmempty = true
o.placeholder = "Premium:0.9;SG:1.3"
o.rmempty = true
o:depends("type", "smart")
o = s:option(Value, "tolerance", translate("Tolerance(ms)"))
o.default = "150"
o.rmempty = true
o:depends("type", "url-test")
o = s:option(Value, "policy_filter", translate("Provider Filter"))
o.rmempty = true
o.placeholder = "bgp|sg"
o = s:option(Value, "icon", translate("Icon"))
o.rmempty = true
-- [[ other-setting ]]--
o = s:option(Value, "other_parameters", translate("Other Parameters"))
o.template = "cbi/tvalue"
o.rows = 20
o.wrap = "off"
o.description = font_red..bold_on..translate("Edit Your Other Parameters Here")..bold_off..font_off
o.rmempty = true
function o.cfgvalue(self, section)
if self.map:get(section, "other_parameters") == nil then
return "# Example:\n"..
"# Only support YAML, four spaces need to be reserved at the beginning of each line to maintain formatting alignment\n"..
"# 示例:\n"..
"# 仅支持 YAML, 每行行首需要多保留四个空格以使脚本处理后能够与上方配置保持格式对齐\n"..
"# type: select\n"..
"# proxies:\n"..
"# - DIRECT\n"..
"# - ss\n"..
"# use:\n"..
"# - provider1\n"..
"# - provider1\n"..
"# url: 'https://www.gstatic.com/generate_204'\n"..
"# interval: 300\n"..
"# lazy: true\n"..
"# timeout: 5000\n"..
"# max-failed-times: 5\n"..
"# disable-udp: true\n"..
"# interface-name: en0\n"..
"# routing-mark: 11451\n"..
"# include-all: false\n"..
"# include-all-proxies: false\n"..
"# include-all-providers: false\n"..
"# filter: \"(?i)港|hk|hongkong|hong kong\"\n"..
"# exclude-filter: \"美|日\"\n"..
"# exclude-type: \"Shadowsocks|Http\"\n"..
"# expected-status: 204\n"..
"# hidden: true\n"..
"# icon: xxx"
else
return Value.cfgvalue(self, section)
end
end
function o.validate(self, value)
if value then
value = value:gsub("\r\n?", "\n")
value = value:gsub("%c*$", "")
end
return value
end
o = s:option(DynamicList, "other_group", translate("Other Group (Support Regex)"))
o.description = font_red..bold_on..translate("The Added Proxy Groups Must Exist Except 'DIRECT' & 'REJECT' & 'REJECT-DROP' & 'PASS' & 'GLOBAL'")..bold_off..font_off
o:value("all", translate("All Groups"))
uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil and s.name ~= m.uci:get(openclash, sid, "name") and (s.config == m.uci:get(openclash, sid, "config") or s.config == "all") then
o:value(s.name)
end
end)
o:value("DIRECT")
o:value("REJECT")
o:value("REJECT-DROP")
o:value("PASS")
o:value("GLOBAL")
o.rmempty = true
local function sync_group_name(section, old_name, new_name)
if old_name == "" or old_name == nil or old_name == new_name then
return
end
local function matches(value, pattern)
if not value or not pattern then
return false
end
local ok, result = pcall(function() return string.match(value, pattern) end)
if ok and result then
return true
end
return value == pattern
end
-- servers 的 groups 列表
uci:foreach(openclash, "servers", function(s)
local groups = uci:get(openclash, s[".name"], "groups")
if groups then
local new_groups = {}
local changed = false
for _, item in ipairs(groups) do
if matches(old_name, item) then
table.insert(new_groups, new_name)
changed = true
else
table.insert(new_groups, item)
end
end
if changed then
uci:delete(openclash, s[".name"], "groups")
uci:set_list(openclash, s[".name"], "groups", new_groups)
end
end
end)
-- proxy-provider 的 groups 列表
uci:foreach(openclash, "proxy-provider", function(s)
local groups = uci:get(openclash, s[".name"], "groups")
if groups then
local new_groups = {}
local changed = false
for _, item in ipairs(groups) do
if matches(old_name, item) then
table.insert(new_groups, new_name)
changed = true
else
table.insert(new_groups, item)
end
end
if changed then
uci:delete(openclash, s[".name"], "groups")
uci:set_list(openclash, s[".name"], "groups", new_groups)
end
end
end)
-- groups 的 other_group 列表
uci:foreach(openclash, "groups", function(s)
if s[".name"] ~= section then
local other_group = uci:get(openclash, s[".name"], "other_group")
if other_group then
local new_other = {}
local changed = false
for _, item in ipairs(other_group) do
if matches(old_name, item) then
table.insert(new_other, new_name)
changed = true
else
table.insert(new_other, item)
end
end
if changed then
uci:delete(openclash, s[".name"], "other_group")
uci:set_list(openclash, s[".name"], "other_group", new_other)
end
end
end
end)
-- dns_servers 的 specific_group 选项
uci:foreach(openclash, "dns_servers", function(s)
local specific_group = uci:get(openclash, s[".name"], "specific_group")
if matches(old_name, specific_group) then
uci:set(openclash, s[".name"], "specific_group", new_name)
end
end)
-- servers 的 dialer_proxy 选项
uci:foreach(openclash, "servers", function(s)
local dialer_proxy = uci:get(openclash, s[".name"], "dialer_proxy")
if matches(old_name, dialer_proxy) then
uci:set(openclash, s[".name"], "dialer_proxy", new_name)
end
end)
uci:commit(openclash)
end
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
local old_name = m.uci:get(openclash, sid, "old_name") or ""
local new_name = HTTP.formvalue("cbid.openclash." .. sid .. ".name") or m.uci:get(openclash, sid, "name")
sync_group_name(sid, old_name, new_name)
m.uci:set(openclash, sid, "old_name", new_name)
m.uci:commit(openclash)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
m:append(Template("openclash/config_editor"))
return m

View File

@ -0,0 +1,22 @@
--
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
m = Map("openclash", translate("Server Logs"))
s = m:section(TypedSection, "openclash")
m.pageaction = false
s.anonymous = true
s.addremove=false
log = s:option(TextValue, "clog")
log.readonly=true
log.pollcheck=true
log.template="openclash/log"
log.description = translate("")
log.rows = 29
m:append(Template("openclash/toolbar_show"))
m:append(Template("openclash/config_editor"))
return m

View File

@ -0,0 +1,60 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local fs = require "luci.openclash"
local DISP = require "luci.dispatcher"
local file_path = fs.get_file_path_from_request()
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "%s") % arg[1])
return
end
m = Map("openclash", translate("File Edit"))
m.pageaction = false
m.redirect = DISP.build_url("admin", "services", "openclash", "other-file-edit", "%s") % arg[1].."?file="..HTTP.urlencode(file_path)
s = m:section(TypedSection, "openclash")
s.anonymous = true
s.addremove=false
o = s:option(TextValue, "edit_file")
o.rows = 50
o.wrap = "off"
function o.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = fs.readfile(file_path)
if value ~= old_value then
fs.writefile(file_path, value)
end
end
end
function o.cfgvalue(self, section)
return fs.readfile(file_path) or ""
end
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "%s") % arg[1])
end
m:append(Template("openclash/config_editor"))
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,197 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local sys = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
local fs = require "luci.openclash"
local file_path = fs.get_file_path_from_request()
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers"))
return
end
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Edit Proxy-Provider"))
m.pageaction = false
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
if m.uci:get(openclash, sid) ~= "proxy-provider" then
HTTP.redirect(m.redirect)
return
end
-- [[ Provider Setting ]]--
s = m:section(NamedSection, sid, "proxy-provider")
s.anonymous = true
s.addremove = false
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
o = s:option(ListValue, "type", translate("Provider Type"))
o.rmempty = true
o.description = translate("Choose The Provider Type")
o:value("http")
o:value("file")
o:value("inline")
o = s:option(Value, "name", translate("Provider Name"))
o.rmempty = false
o.default = "Proxy-provider - "..sid
o = s:option(ListValue, "path", translate("Provider Path"))
o.description = translate("Update Your Proxy Provider File From Config Luci Page")
local p,h={}
for t,f in ipairs(fs.glob("/etc/openclash/proxy_provider/*"))do
h=fs.stat(f)
if h then
p[t]={}
p[t].name=fs.basename(f)
if IsYamlFile(p[t].name) or IsYmlFile(p[t].name) then
o:value("./proxy_provider/"..p[t].name)
end
end
end
o.rmempty = false
o:depends("type", "file")
o = s:option(Value, "provider_url", translate("Provider URL"))
o.rmempty = false
o:depends("type", "http")
o = s:option(Value, "provider_filter", translate("Provider Filter"))
o.rmempty = true
o.placeholder = "bgp|sg"
o = s:option(Value, "provider_interval", translate("Provider Interval(s)"))
o.default = "3600"
o.rmempty = false
o:depends("type", "http")
o = s:option(ListValue, "health_check", translate("Provider Health Check"))
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = true
o = s:option(Value, "health_check_url", translate("Health Check URL"))
o:value("http://cp.cloudflare.com/generate_204")
o:value("http://www.gstatic.com/generate_204")
o:value("https://cp.cloudflare.com/generate_204")
o.rmempty = false
o = s:option(Value, "health_check_interval", translate("Health Check Interval(s)"))
o.default = "300"
o.rmempty = false
-- [[ other-setting ]]--
o = s:option(Value, "other_parameters", translate("Other Parameters"))
o.template = "cbi/tvalue"
o.rows = 20
o.wrap = "off"
o.description = font_red..bold_on..translate("Edit Your Other Parameters Here")..bold_off..font_off
o.rmempty = true
function o.cfgvalue(self, section)
if self.map:get(section, "other_parameters") == nil then
return "# Example:\n"..
"# Only support YAML, four spaces need to be reserved at the beginning of each line to maintain formatting alignment\n"..
"# 示例:\n"..
"# 仅支持 YAML, 每行行首需要多保留四个空格以使脚本处理后能够与上方配置保持格式对齐\n"..
"# header:\n"..
"# User-Agent:\n"..
"# - \"Clash/v1.18.0\"\n"..
"# - \"mihomo/1.18.3\"\n"..
"# Authorization:\n"..
"# - \"token 1231231\"\n"..
"# override:\n"..
"# skip-cert-verify: true\n"..
"# additional-prefix: \"provider1 prefix |\"\n"..
"# additional-suffix: \"| provider1 suffix\"\n"..
"# proxy-name:\n"..
"# - pattern: \"IPLC-(.*?)倍\"\n"..
"# target: \"iplc x $1\"\n"..
"# exclude-type: \"ss|http\"\n"..
"\n"..
"# inline Example:\n"..
"# payload:\n"..
"# - name: \"ss1\"\n"..
"# type: ss\n"..
"# server: server\n"..
"# port: 443\n"..
"# cipher: chacha20-ietf-poly1305\n"..
"# password: \"password\""
else
return Value.cfgvalue(self, section)
end
end
function o.validate(self, value)
if value then
value = value:gsub("\r\n?", "\n")
value = value:gsub("%c*$", "")
end
return value
end
o = s:option(DynamicList, "groups", translate("Proxy Group (Support Regex)"))
o.description = font_red..bold_on..translate("The added Proxy Groups Must Exist")..bold_off..font_off
o.rmempty = true
o:value("all", translate("All Groups"))
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil and (s.config == m.uci:get(openclash, sid, "config") or s.config == "all") then
o:value(s.name)
end
end)
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
m:append(Template("openclash/config_editor"))
return m

View File

@ -0,0 +1,122 @@
local proxy_form
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local p,r={}
for x,y in ipairs(fs.glob("/etc/openclash/proxy_provider/*"))do
r=fs.stat(y)
if r then
p[x]={}
p[x].num=string.format(x)
p[x].name=fs.basename(y)
p[x].mtime=os.date("%Y-%m-%d %H:%M:%S",r.mtime)
p[x].size=fs.filesize(r.size)
p[x].remove=0
p[x].enable=false
end
end
proxy_form=SimpleForm("proxy_provider_file_list",translate("Proxy Provider File List"))
proxy_form.reset=false
proxy_form.submit=false
tb1=proxy_form:section(Table,p)
nu1=tb1:option(DummyValue,"num",translate("Serial Number"))
nm1=tb1:option(DummyValue,"name",translate("File Name"))
mt1=tb1:option(DummyValue,"mtime",translate("Update Time"))
sz1=tb1:option(DummyValue,"size",translate("Size"))
btned1=tb1:option(Button,"edit",translate("Edit"))
btned1.render=function(p,x,r)
p.inputstyle="apply"
Button.render(p,x,r)
end
btned1.write=function(r,x)
local file_path = "/etc/openclash/proxy_provider/" .. fs.basename(p[x].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "proxy-provider-file-manage") .. "?file=" .. HTTP.urlencode(file_path))
end
btndl1 = tb1:option(Button,"download1",translate("Download Config"))
btndl1.render=function(y,x,r)
y.inputstyle="remove"
Button.render(y,x,r)
end
btndl1.write = function (r,x)
local sPath, sFile, fd, block
sPath = "/etc/openclash/proxy_provider/"..p[x].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btnrm1=tb1:option(Button,"remove1",translate("Remove"))
btnrm1.render=function(p,x,r)
p.inputstyle="reset"
Button.render(p,x,r)
end
btnrm1.write=function(r,x)
local r=fs.unlink("/etc/openclash/proxy_provider/"..luci.openclash.basename(p[x].name))
if r then table.remove(p,x)end
return r
end
local t = {
{Refresh, Create, Delete_all, Apply}
}
a = proxy_form:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage"))
end
o = a:option(DummyValue, "Create", " ")
o.rawhtml = true
o.template = "openclash/input_file_name"
o.value = "/etc/openclash/proxy_provider/"
o = a:option(Button, "Delete_all", " ")
o.inputtitle = translate("Delete All File")
o.inputstyle = "remove"
o.write = function()
luci.sys.call("rm -rf /etc/openclash/proxy_provider/* >/dev/null 2>&1")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
proxy_form:append(Template("openclash/toolbar_show"))
return proxy_form

View File

@ -0,0 +1,122 @@
local rule_form
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local g,h={}
for n,m in ipairs(fs.glob("/etc/openclash/rule_provider/*"))do
h=fs.stat(m)
if h then
g[n]={}
g[n].num=string.format(n)
g[n].name=fs.basename(m)
g[n].mtime=os.date("%Y-%m-%d %H:%M:%S",h.mtime)
g[n].size=fs.filesize(h.size)
g[n].remove=0
g[n].enable=false
end
end
rule_form=SimpleForm("rule_provider_file_list",translate("Rule Providers File List"))
rule_form.reset=false
rule_form.submit=false
tb2=rule_form:section(Table,g)
nu2=tb2:option(DummyValue,"num",translate("Serial Number"))
nm2=tb2:option(DummyValue,"name",translate("File Name"))
mt2=tb2:option(DummyValue,"mtime",translate("Update Time"))
sz2=tb2:option(DummyValue,"size",translate("Size"))
btned1=tb2:option(Button,"edit",translate("Edit"))
btned1.render=function(g,n,h)
g.inputstyle="apply"
Button.render(g,n,h)
end
btned1.write=function(h,n)
local file_path = "/etc/openclash/rule_provider/" .. fs.basename(g[n].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "rule-providers-file-manage") .. "?file=" .. HTTP.urlencode(file_path))
end
btndl2 = tb2:option(Button,"download2",translate("Download Config"))
btndl2.render=function(m,n,h)
m.inputstyle="remove"
Button.render(m,n,h)
end
btndl2.write = function (h,n)
local sPath, sFile, fd, block
sPath = "/etc/openclash/rule_provider/"..g[n].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btnrm2=tb2:option(Button,"remove2",translate("Remove"))
btnrm2.render=function(g,n,h)
g.inputstyle="reset"
Button.render(g,n,h)
end
btnrm2.write=function(h,n)
local h=fs.unlink("/etc/openclash/rule_provider/"..luci.openclash.basename(g[n].name))
if h then table.remove(g,n)end
return h
end
local t = {
{Refresh, Create, Delete_all, Apply}
}
a = rule_form:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage"))
end
o = a:option(DummyValue, "Create", " ")
o.rawhtml = true
o.template = "openclash/input_file_name"
o.value = "/etc/openclash/rule_provider/"
o = a:option(Button, "Delete_all", " ")
o.inputtitle = translate("Delete All File")
o.inputstyle = "remove"
o.write = function()
luci.sys.call("rm -rf /etc/openclash/rule_provider/* >/dev/null 2>&1")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
rule_form:append(Template("openclash/toolbar_show"))
return rule_form

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,259 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local file_path = fs.get_file_path_from_request()
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
return
end
m = Map(openclash, translate("Servers & Groups manage"))
m.pageaction = false
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
m.description=translate("Attention:")..
"<br/>"..translate("1. Before modifying the configuration file, please click the button below to read the configuration file")..
"<br/>"..translate("2. Proxy-providers address can be directly filled in the subscription link")..
"<br/>"..
"<br/>"..translate("Introduction to proxy usage:").." <a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/config/proxies/\")'>"..translate("https://wiki.metacubex.one/config/proxies/").."</a>"..
"<br/>"..translate("Introduction to proxy-provider usage:").." <a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/config/proxy-providers/\")'>"..translate("https://wiki.metacubex.one/config/proxy-providers/").."</a>"
-- [[ Groups Manage ]]--
gs = m:section(TypedSection, "groups", translate("Proxy Groups"))
gs.anonymous = true
gs.addremove = true
gs.sortable = true
gs.template = "openclash/tblsection"
gs.extedit = DISP.build_url("admin/services/openclash/groups-config/%s").."?file="..file_path
function gs.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "config", name)
end
HTTP.redirect(gs.extedit % sid)
return
end
end
---- enable flag
o = gs:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = gs:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = gs:option(DummyValue, "type", translate("Group Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = gs:option(DummyValue, "name", translate("Group Name"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
-- [[ Proxy-Provider Manage ]]--
ps = m:section(TypedSection, "proxy-provider", translate("Proxy-Provider"))
ps.anonymous = true
ps.addremove = true
ps.sortable = true
ps.template = "openclash/tblsection"
ps.extedit = DISP.build_url("admin/services/openclash/proxy-provider-config/%s").."?file="..file_path
function ps.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "config", name)
end
HTTP.redirect(ps.extedit % sid)
return
end
end
o = ps:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = ps:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = ps:option(DummyValue, "type", translate("Provider Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = ps:option(DummyValue, "name", translate("Provider Name"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
-- [[ Servers Manage ]]--
ss = m:section(TypedSection, "servers", translate("Proxies"))
ss.anonymous = true
ss.addremove = true
ss.sortable = true
ss.template = "openclash/tblsection"
ss.extedit = DISP.build_url("admin/services/openclash/servers-config/%s").."?file="..file_path
function ss.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "config", name)
end
HTTP.redirect(ss.extedit % sid)
return
end
end
---- enable flag
o = ss:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = ss:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = ss:option(DummyValue, "type", translate("Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = ss:option(DummyValue, "name", translate("Server Alias"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = ss:option(DummyValue, "server", translate("Server Address"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = ss:option(DummyValue, "port", translate("Server Port"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = ss:option(DummyValue, "udp", translate("UDP Support"))
function o.cfgvalue(...)
if Value.cfgvalue(...) == "true" then
return translate("Enable")
elseif Value.cfgvalue(...) == "false" then
return translate("Disable")
else
return translate("None")
end
end
local tt = {
{Delete_Unused_Servers, Delete_Servers, Delete_Proxy_Provider, Delete_Groups}
}
b = m:section(Table, tt)
o = b:option(Button,"Delete_Unused_Servers", " ")
o.inputtitle = translate("Delete Unused Servers")
o.inputstyle = "reset"
o.write = function()
m.uci:foreach("openclash", "servers",
function(s)
if s.enabled ~= "1" then
m.uci:delete("openclash", s[".name"])
end
end)
m.uci:commit("openclash")
HTTP.redirect(m.redirect)
end
o = b:option(Button,"Delete_Servers", " ")
o.inputtitle = translate("Delete Servers")
o.inputstyle = "reset"
o.write = function()
m.uci:delete_all("openclash", "servers", function(s) return true end)
m.uci:commit("openclash")
HTTP.redirect(m.redirect)
end
o = b:option(Button,"Delete_Proxy_Provider", " ")
o.inputtitle = translate("Delete Proxy Providers")
o.inputstyle = "reset"
o.write = function()
m.uci:delete_all("openclash", "proxy-provider", function(s) return true end)
m.uci:commit("openclash")
HTTP.redirect(m.redirect)
end
o = b:option(Button,"Delete_Groups", " ")
o.inputtitle = translate("Delete Groups")
o.inputstyle = "reset"
o.write = function()
m.uci:delete_all("openclash", "groups", function(s) return true end)
m.uci:commit("openclash")
HTTP.redirect(m.redirect)
end
local t = {
{Load_Config, Commit, Apply, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Load_Config", " ")
o.inputtitle = translate("Read Config")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
luci.sys.call("/usr/share/openclash/yml_groups_get.sh \"%s\" 2>/dev/null" % file_path)
HTTP.redirect(m.redirect)
end
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
HTTP.redirect(m.redirect)
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
luci.sys.call("/usr/share/openclash/yml_groups_set.sh \"%s\" >/dev/null 2>&1 &" % file_path)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
m:append(Template("openclash/toolbar_show"))
return m

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,526 @@
--[[
LuCI - Filesystem tools
Description:
A module offering often needed filesystem manipulation functions
FileId:
$Id$
License:
Copyright 2008 Steven Barth <steven@midlink.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--
local io = require "io"
local os = require "os"
local ltn12 = require "luci.ltn12"
local fs = require "nixio.fs"
local nutil = require "nixio.util"
local uci = require "luci.model.uci".cursor()
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local type = type
local string = string
local tostring = tostring
local table = table
local math = math
local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
--- LuCI filesystem library.
module "luci.openclash"
--- Test for file access permission on given path.
-- @class function
-- @name access
-- @param str String value containing the path
-- @return Number containing the return code, 0 on sucess or nil on error
-- @return String containing the error description (if any)
-- @return Number containing the os specific errno (if any)
access = fs.access
--- Evaluate given shell glob pattern and return a table containing all matching
-- file and directory entries.
-- @class function
-- @name glob
-- @param filename String containing the path of the file to read
-- @return Table containing file and directory entries or nil if no matches
-- @return String containing the error description (if no matches)
-- @return Number containing the os specific errno (if no matches)
function glob(...)
local iter, code, msg = fs.glob(...)
if iter then
return nutil.consume(iter)
else
return nil, code, msg
end
end
--- Checks wheather the given path exists and points to a regular file.
-- @param filename String containing the path of the file to test
-- @return Boolean indicating wheather given path points to regular file
function isfile(filename)
return fs.stat(filename, "type") == "reg"
end
--- Checks wheather the given path exists and points to a directory.
-- @param dirname String containing the path of the directory to test
-- @return Boolean indicating wheather given path points to directory
function isdirectory(dirname)
return fs.stat(dirname, "type") == "dir"
end
--- Read the whole content of the given file into memory.
-- @param filename String containing the path of the file to read
-- @return String containing the file contents or nil on error
-- @return String containing the error message on error
-- Wrapped readfile: if file is age-encrypted, try to decrypt with matching UCI secret first
function readfile(filename)
local content, err = fs.readfile(filename)
if not content then return nil, err end
if content:find("BEGIN AGE ENCRYPTED FILE") then
local keys = get_age_keys(filename)
if keys and keys.secret and keys.secret ~= "" then
return age_decrypt(keys.secret, content) or content
else
return content
end
end
return content
end
--- Write the contents of given string to given file.
-- @param filename String containing the path of the file to read
-- @param data String containing the data to write
-- @return Boolean containing true on success or nil on error
-- @return String containing the error message on error
-- Wrapped writefile: if public key exists for filename, encrypt before writing
function writefile(filename, data)
local keys = get_age_keys(filename)
if keys and keys.public and keys.public ~= "" then
return fs.writefile(filename, age_encrypt(keys.public, data) or data)
end
return fs.writefile(filename, data)
end
--- Copies a file.
-- @param source Source file
-- @param dest Destination
-- @return Boolean containing true on success or nil on error
copy = fs.datacopy
--- Renames a file.
-- @param source Source file
-- @param dest Destination
-- @return Boolean containing true on success or nil on error
rename = fs.move
--- Get the last modification time of given file path in Unix epoch format.
-- @param path String containing the path of the file or directory to read
-- @return Number containing the epoch time or nil on error
-- @return String containing the error description (if any)
-- @return Number containing the os specific errno (if any)
function mtime(path)
return fs.stat(path, "mtime")
end
--- Set the last modification time of given file path in Unix epoch format.
-- @param path String containing the path of the file or directory to read
-- @param mtime Last modification timestamp
-- @param atime Last accessed timestamp
-- @return 0 in case of success nil on error
-- @return String containing the error description (if any)
-- @return Number containing the os specific errno (if any)
function utime(path, mtime, atime)
return fs.utimes(path, atime, mtime)
end
--- Return the last element - usually the filename - from the given path with
-- the directory component stripped.
-- @class function
-- @name basename
-- @param path String containing the path to strip
-- @return String containing the base name of given path
-- @see dirname
basename = fs.basename
--- Return the directory component of the given path with the last element
-- stripped of.
-- @class function
-- @name dirname
-- @param path String containing the path to strip
-- @return String containing the directory component of given path
-- @see basename
dirname = fs.dirname
--- Return a table containing all entries of the specified directory.
-- @class function
-- @name dir
-- @param path String containing the path of the directory to scan
-- @return Table containing file and directory entries or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function dir(...)
local iter, code, msg = fs.dir(...)
if iter then
local t = nutil.consume(iter)
t[#t+1] = "."
t[#t+1] = ".."
return t
else
return nil, code, msg
end
end
--- Create a new directory, recursively on demand.
-- @param path String with the name or path of the directory to create
-- @param recursive Create multiple directory levels (optional, default is true)
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function mkdir(path, recursive)
return recursive and fs.mkdirr(path) or fs.mkdir(path)
end
--- Remove the given empty directory.
-- @class function
-- @name rmdir
-- @param path String containing the path of the directory to remove
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
rmdir = fs.rmdir
local stat_tr = {
reg = "regular",
dir = "directory",
lnk = "link",
chr = "character device",
blk = "block device",
fifo = "fifo",
sock = "socket"
}
--- Get information about given file or directory.
-- @class function
-- @name stat
-- @param path String containing the path of the directory to query
-- @return Table containing file or directory properties or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function stat(path, key)
local data, code, msg = fs.stat(path)
if data then
data.mode = data.modestr
data.type = stat_tr[data.type] or "?"
end
return key and data and data[key] or data, code, msg
end
--- Set permissions on given file or directory.
-- @class function
-- @name chmod
-- @param path String containing the path of the directory
-- @param perm String containing the permissions to set ([ugoa][+-][rwx])
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
chmod = fs.chmod
--- Create a hard- or symlink from given file (or directory) to specified target
-- file (or directory) path.
-- @class function
-- @name link
-- @param path1 String containing the source path to link
-- @param path2 String containing the destination path for the link
-- @param symlink Boolean indicating wheather to create a symlink (optional)
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function link(src, dest, sym)
return sym and fs.symlink(src, dest) or fs.link(src, dest)
end
--- Remove the given file.
-- @class function
-- @name unlink
-- @param path String containing the path of the file to remove
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
unlink = fs.unlink
--- Retrieve target of given symlink.
-- @class function
-- @name readlink
-- @param path String containing the path of the symlink to read
-- @return String containing the link target or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
readlink = fs.readlink
function filename(str)
if not str then
return nil
end
local idx = str:match(".+()%.%w+$")
if idx then
return str:sub(1, idx-1)
else
return str
end
end
function filesize(e)
local t=0
local a={' KB',' MB',' GB',' TB',' PB'}
if e < 0 then
e = -e
end
repeat
e=e/1024
t=t+1
until(e<=1024)
return string.format("%.1f",e)..a[t] or "0.0 KB"
end
function lanip()
local lan_int_name = uci:get("openclash", "@overwrite[0]", "lan_interface_name") or uci:get("openclash", "config", "lan_interface_name") or "0"
local lan_ip
if lan_int_name == "0" then
lan_ip = SYS.exec("uci -q get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null |tr -d '\n'")
else
lan_ip = SYS.exec(string.format("ip address show %s 2>/dev/null | grep -w 'inet' 2>/dev/null | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -1 | tr -d '\n'", lan_int_name))
end
if not lan_ip or lan_ip == "" then
lan_ip = SYS.exec("ip address show $(uci -q -p /tmp/state get network.lan.ifname || uci -q -p /tmp/state get network.lan.device) | grep -w 'inet' 2>/dev/null | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -1 | tr -d '\n'")
end
if not lan_ip or lan_ip == "" then
lan_ip = SYS.exec("ip addr show 2>/dev/null | grep -w 'inet' | grep 'global' | grep 'brd' | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -n 1 | tr -d '\n'")
end
return lan_ip
end
function find_case_insensitive_path(path)
local dir = dirname(path)
local base = basename(path)
local files = dir and fs.dir(dir)
if not files then
return nil
end
for f in files do
if f:lower() == base:lower() then
return dir .. "/" .. f
end
end
return nil
end
function get_resourse_mtime(path)
local real_path = path
if not fs.access(path) then
local found = find_case_insensitive_path(path)
if found then
real_path = found
else
return "File Not Exist"
end
end
local file = fs.readlink(real_path) or real_path
local resourse_etag_version = SYS.exec(string.format("source /usr/share/openclash/openclash_etag.sh && GET_ETAG_TIMESTAMP_BY_PATH '%s'", real_path))
if resourse_etag_version and resourse_etag_version ~= "" then
return resourse_etag_version
end
local resourse_version = os.date("%Y-%m-%d %H:%M:%S", mtime(real_path))
if resourse_version and resourse_version ~= "" then
return resourse_version
end
return "Unknown"
end
function uci_get_config(section, key)
local val
if section == "config" then
val = uci:get("openclash", "@overwrite[0]", key)
end
if val == nil then
val = uci:get("openclash", section, key)
end
return val
end
function get_file_path_from_request()
local file_path
local referer = HTTP.getenv("HTTP_REFERER")
if referer then
local _, _, file_value = referer:find("file=([^&]*)$")
if file_value and file_value ~= "" then
file_path = HTTP.urldecode(file_value)
end
end
if not file_path or file_path == "/" then
file_path = HTTP.formvalue("file")
if not file_path then
file_path = HTTP.urldecode(file_path)
end
end
return file_path
end
function get_age_keys(file)
local name = filename(basename(file))
local pub, sec
uci:foreach("openclash", "config_age_secret", function(s)
if s and s.name then
if s.name == name and (not s.hidden or (s.hidden ~= "true")) then
if not pub and s.public then pub = s.public end
if not sec and s.secret then sec = s.secret end
end
end
end)
return { public = pub, secret = sec }
end
function age_encrypt(public, content)
if not public or public == "" or not content then return nil end
local tmp_in = os.tmpname()
local tmp_out = os.tmpname()
local f = io.open(tmp_in, "w")
if not f then return nil end
f:write(content)
f:close()
local cmd = string.format(
"/etc/openclash/core/clash_meta age encrypt %s %s %s 2>/dev/null",
public, tmp_in, tmp_out
)
os.execute(cmd)
local out = nil
local f_out = io.open(tmp_out, "r")
if f_out then
out = f_out:read("*a") or ""
f_out:close()
end
os.remove(tmp_in)
os.remove(tmp_out)
return out
end
function age_decrypt(secret, content)
if not secret or secret == "" or not content then return nil end
local tmp_in = os.tmpname()
local tmp_out = os.tmpname()
local f = io.open(tmp_in, "w")
if not f then return nil end
f:write(content)
f:close()
local cmd = string.format(
"/etc/openclash/core/clash_meta age decrypt %s %s %s 2>/dev/null",
secret, tmp_in, tmp_out
)
os.execute(cmd)
local out = nil
local f_out = io.open(tmp_out, "r")
if f_out then
out = f_out:read("*a") or ""
f_out:close()
end
os.remove(tmp_in)
os.remove(tmp_out)
return out
end
function decode64(data)
if not data then return nil end
data = data:gsub('[%s]', '')
if #data % 4 ~= 0 then return data end
local out = {}
for i = 1, #data, 4 do
local c1, c2, c3, c4 = data:byte(i, i+3)
local i1 = b64chars:find(string.char(c1), 1, true) - 1
local i2 = b64chars:find(string.char(c2), 1, true) - 1
local i3 = (c3 == 61) and -1 or (b64chars:find(string.char(c3), 1, true) - 1)
local i4 = (c4 == 61) and -1 or (b64chars:find(string.char(c4), 1, true) - 1)
if not i1 or not i2 or (c3 ~= 61 and not i3) or (c4 ~= 61 and not i4) then
return nil
end
local x = i1 * 0x40000 + i2 * 0x1000
if i3 >= 0 then x = x + i3 * 0x40 end
if i4 >= 0 then x = x + i4 end
table.insert(out, string.char(math.floor(x / 0x10000) % 0x100))
if i3 >= 0 then
table.insert(out, string.char(math.floor(x / 0x100) % 0x100))
end
if i4 >= 0 then
table.insert(out, string.char(x % 0x100))
end
end
return table.concat(out)
end
--- Returns the appropriate ps command string for the system's ps implementation.
-- Detects procps-ng (ps -efw) vs busybox (ps -w).
-- @return String containing the ps command prefix
function ps_cmd()
local ps_version = SYS.exec("ps --version 2>&1 |grep -c procps-ng |tr -d '\n'")
if ps_version == "1" then
return "ps -efw"
else
return "ps -w"
end
end
--- Returns the package manager type (opkg or apk).
-- @return String "opkg" or "apk"
function pkg_type()
if fs.access("/usr/bin/apk") then
return "apk"
else
return "opkg"
end
end
--- Returns the installed version of luci-app-openclash.
-- Supports both opkg and apk package managers.
-- @return String containing the version number, or "0" if not found
function oc_version()
local v
if pkg_type() == "opkg" then
v = SYS.exec("rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep '^Version:' |awk '{print $2}' |tr -d '\n'")
else
v = SYS.exec("rm -f /lib/apk/db/lock && apk info luci-app-openclash 2>/dev/null |grep '^luci-app-openclash-[0-9]' |sed 's/luci-app-openclash-//' |tr -d '\n'")
end
if v == "" then
v = "0"
end
return v
end

View File

@ -0,0 +1,18 @@
<%+cbi/valueheader%>
<div>
<%
local val = self:cfgvalue(section)
if val == translate("Enable") or val == translate("Enabled") or val == translate("Config Normal") or val == translate("Exist") then
%>
<div style="color: green; font-weight:bold;">
<%
else
%>
<div style="color: red; font-weight:bold;">
<%
end
write(pcdata(val))
%>
</div>
</div>
<%+cbi/valuefooter%>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,457 @@
<%
local fs = require "luci.openclash"
local plugin_version = fs.oc_version()
%>
<link rel="stylesheet" href="/luci-static/resources/openclash/css/oc.css?v=<%=plugin_version%>">
<script type="text/javascript">//<![CDATA[
var _cmReady = false;
var _cmQueue = [];
function _cmWhenReady(callback) {
if (_cmReady) { callback(); return; }
_cmQueue.push(callback);
}
(function() {
var basePath = '/luci-static/resources/openclash';
var head = document.head || document.getElementsByTagName('head')[0];
var cssFiles = [
'lib/codemirror.css',
'theme/material.css',
'theme/log.css',
'addon/fold/foldgutter.css',
'addon/lint/lint.css',
'addon/display/fullscreen.css',
'addon/dialog/dialog.css',
'addon/search/matchesonscrollbar.css',
'addon/merge/merge.css'
];
var jsFiles = [
'lib/codemirror.js',
'mode/yaml/yaml.js',
'mode/log/log.js',
'mode/shell/shell.js',
'addon/fold/foldcode.js',
'addon/fold/foldgutter.js',
'addon/fold/indent-fold.js',
'addon/edit/matchbrackets.js',
'addon/selection/active-line.js',
'addon/lint/lint.js',
'addon/lint/yaml-lint.js',
'addon/lint/js-yaml.min.js',
'addon/display/fullscreen.js',
'addon/display/autorefresh.js',
'addon/dialog/dialog.js',
'addon/search/searchcursor.js',
'addon/search/search.js',
'addon/scroll/annotatescrollbar.js',
'addon/search/matchesonscrollbar.js',
'addon/search/jump-to-line.js',
'addon/merge/diff_match_patch.js',
'addon/merge/merge.js'
];
var promises = [];
cssFiles.forEach(function(file) {
promises.push(new Promise(function(resolve) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = basePath + '/' + file;
link.onload = resolve;
link.onerror = resolve;
if (head) head.appendChild(link);
else resolve();
}));
});
var loadJS = function(index) {
if (index >= jsFiles.length) return Promise.resolve();
return new Promise(function(resolve) {
var script = document.createElement('script');
script.src = basePath + '/' + jsFiles[index];
script.onload = function() { loadJS(index + 1).then(resolve); };
script.onerror = function() { loadJS(index + 1).then(resolve); };
if (head) head.appendChild(script);
else resolve();
});
};
promises.push(loadJS(0));
Promise.all(promises).then(function() {
_cmReady = true;
var queue = _cmQueue;
_cmQueue = [];
queue.forEach(function(fn) { fn(); });
});
})();
//]]>
</script>
<%-
local uci = require("luci.model.uci").cursor()
local fs = require "luci.openclash"
local conf = fs.uci_get_config("config", "config_path")
if not conf then conf = "/etc/openclash/config/config.yaml" end
local conf_name = fs.basename(conf)
if not conf_name then conf_name = "config.yaml" end
local sconf = "/etc/openclash/"..conf_name
local path_info = os.getenv("PATH_INFO") or ""
local sid = path_info:match("(cfg[0-9a-f]+)") or ""
-%>
<table id="my_editor_div" style="width: 100%; text-align: center; display: none;">
<tr>
<td style="width: 50%; overflow: hidden;"><%:Modify Your Config file:%> <b style=color:green><%=conf_name%></b> <%:Here, Except The Settings That Were Taken Over%></td>
<%-
if fs.isfile(sconf) then
-%>
<td style="width: 50%; overflow: hidden;"><%:Config File Edited By OpenClash For Running%></td>
<%-
else
-%>
<td style="width: 50%; overflow: hidden;"><%:Default Config File With Correct Template%></td>
<%-
end
-%>
</tr>
</table>
<script src="/luci-static/resources/openclash/js/common.js"></script>
<script type="text/javascript">//<![CDATA[
var levelTranslations = {
'info': '<%:Info%>',
'warning': '<%:Warning%>',
'error': '<%:Error%>',
'debug': '<%:Debug%>',
'tip': '<%:Tip%>',
'watchdog': '<%:Watchdog%>',
'fatal': '<%:Fatal%>'
};
function merge_editor(id, id2, target, target2, readOnly, readOnly2, wid, height)
{
var value, orig1, orig2, merge_editor, descr, gap, vscrollbar, vscrollbar_oc, panes = 2, highlight = true, connect = null, collapse = false;
descr = document.getElementById("my_editor_div");
target.parentNode.parentNode.parentNode.insertBefore(descr, target.parentNode.parentNode);
descr.style.display="inline-table"
value = id.value;
if (value == null) {
editor(id, readOnly, wid, height);
editor(id2, readOnly2, wid, height);
}
else {
target2.parentNode.style.display="none";
orig1 = '';
orig2 = id2.value;
target.style.height= "700px"
id.style.display = "none";
id2.style.display = "none";
merge_editor = CodeMirror.MergeView(target, {
mode: "text/yaml",
value: value,
origLeft: panes == 3 ? orig1 : null,
orig: orig2,
theme: "material",
lineNumbers: true,
autoRefresh: true,
styleActiveLine: true,
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
lint: true,
highlightDifferences: highlight,
connect: connect,
collapseIdentical: collapse,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
extraKeys: {
"F10": function() {
merge_editor.setShowDifferences(highlight = !highlight);
},
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
gap = document.getElementsByClassName("CodeMirror-merge-gap")[0];
vscrollbar = document.getElementsByClassName("CodeMirror-vscrollbar")[0];
vscrollbar_oc = document.getElementsByClassName("CodeMirror-vscrollbar-oc")[0];
if (cm.getOption("fullScreen")) {
console.log(gap.style.display)
gap.style.display = "none";
vscrollbar.className = "CodeMirror-vscrollbar-oc";
}
else {
gap.style.display = "";
vscrollbar_oc.className = "CodeMirror-vscrollbar";
};
},
"Esc": function(cm) {
gap = document.getElementsByClassName("CodeMirror-merge-gap")[0];
vscrollbar = document.getElementsByClassName("CodeMirror-vscrollbar")[0];
vscrollbar_oc = document.getElementsByClassName("CodeMirror-vscrollbar-oc")[0];
if (cm.getOption("fullScreen")) {
cm.setOption("fullScreen", false);
gap.style.display = "";
vscrollbar_oc.className = "CodeMirror-vscrollbar";
};
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
merge_editor.edit.on("change", function(cm) {
id.value = cm.getValue();
});
if (wid && height) {
merge_editor.editor().setSize(wid, height);
merge_editor.rightOriginal().setSize(wid, height);
};
};
};
function editor(id, readOnly, wid, height)
{
var editor = CodeMirror.fromTextArea(id, {
mode: "text/yaml",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "material",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
lint: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
if (readOnly) {
editor.setOption("readOnly","true");
};
if (wid && height) {
editor.setSize(wid, height);
};
};
function shell_editor(id, readOnly, wid, height)
{
var editor = CodeMirror.fromTextArea(id, {
mode: "shell",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "material",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
if (wid && height) {
editor.setSize(wid, height);
};
};
function other_editor(id, readOnly, wid, height)
{
var editor = CodeMirror.fromTextArea(id, {
mode: "text/x-properties",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "material",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
if (wid && height) {
editor.setSize(wid, height);
};
};
function log_editor(id, name, readOnly, wid, height)
{
window['editor_' + name] = CodeMirror.fromTextArea(id, {
mode: "log",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "log",
lineWrapping: true,
matchBrackets: false,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
}
}
});
if (readOnly) {
window['editor_' + name].setOption("readOnly","true");
};
if (wid && height) {
window['editor_' + name].setSize(wid, height);
};
if (isDarkBackground(document.body)) {
document.documentElement.setAttribute('data-darkmode', 'true');
};
};
_cmWhenReady(function() {
var core_log = document.getElementById("core_log");
var oc_log = document.getElementById("cbid.openclash.config.clog");
if (core_log && oc_log) {
log_editor(oc_log, 'oc', true, '100%', '540px');
log_editor(core_log, 'core', true, '100%', '540px');
};
var custom_cfg_value = document.getElementById("cbid.openclash.<%=sid%>.other_parameters");
if (custom_cfg_value) {
editor(custom_cfg_value, false, '100%', '300px');
};
var custom_firewall = document.getElementById("cbid.openclash.config.firewall_custom");
if (custom_firewall) {
shell_editor(custom_firewall, false, "100%", '300px');
};
var myEditor_use = document.getElementById("cbid.table.1.user");
var myEditor_def = document.getElementById("cbid.table.1.default");
if (myEditor_use && myEditor_def) {
var myEditor_div_use = document.getElementById("cbi-table-1-user");
var myEditor_div_def = document.getElementById("cbi-table-1-default");
myEditor_div_def.parentNode.parentNode.style.cssText="text-align: left !important"
myEditor_div_def.parentNode.className='cbi-value';
myEditor_div_def.parentNode.style.display="revert"
myEditor_div_def.parentNode.style.width="100%"
myEditor_div_use.parentNode.className='cbi-value';
myEditor_div_use.parentNode.style.display="revert"
myEditor_div_use.parentNode.style.width="100%"
merge_editor(myEditor_use, myEditor_def, myEditor_div_use, myEditor_div_def, true, false, 'auto', '700px');
};
var myEditor_hosts = document.getElementById("cbid.openclash.config.custom_hosts");
var myEditor_fall_fil = document.getElementById("cbid.openclash.config.custom_fallback_fil");
var myEditor_name_pol = document.getElementById("cbid.openclash.config.custom_domain_dns_core");
var myEditor_name_cus_r1 = document.getElementById("cbid.openclash.config.custom_rules_2");
var myEditor_name_cus_r2 = document.getElementById("cbid.openclash.config.custom_rules");
var myEditor_fake_filter = document.getElementById("cbid.openclash.config.custom_fake_filter");
var myEditor_custom_domain_dns = document.getElementById("cbid.openclash.config.custom_domain_dns");
var myEditor_edit_file = document.getElementById("cbid.openclash.config.edit_file");
var myEditor_custom_sniffer = document.getElementById("cbid.openclash.config.sniffer_custom");
var myEditor_proxy_name_pol = document.getElementById("cbid.openclash.config.custom_proxy_server_dns_policy");
if (myEditor_hosts) {
editor(myEditor_hosts, false, '100%', '300px');
};
if (myEditor_edit_file) {
editor(myEditor_edit_file, false, '100%', '700px');
};
if (myEditor_fall_fil) {
editor(myEditor_fall_fil, false, '100%', '300px');
};
if (myEditor_name_pol) {
editor(myEditor_name_pol, false, '100%', '300px');
};
if (myEditor_proxy_name_pol) {
editor(myEditor_proxy_name_pol, false, '100%', '300px');
};
if (myEditor_name_cus_r1) {
editor(myEditor_name_cus_r1, false, '100%', '300px');
editor(myEditor_name_cus_r2, false, '100%', '300px');
};
if (myEditor_fake_filter) {
other_editor(myEditor_fake_filter, false, '100%', '300px');
};
if (myEditor_custom_domain_dns) {
other_editor(myEditor_custom_domain_dns, false, '100%', '300px');
};
if (myEditor_custom_sniffer) {
other_editor(myEditor_custom_sniffer, false, '100%', '300px');
};
var proxy_mg = document.getElementById('cbi-table-1-proxy_mg');
var rule_mg = document.getElementById('cbi-table-1-rule_mg');
var Commit = document.getElementById('cbi-table-1-Commit');
var Apply = document.getElementById('cbi-table-1-Apply');
var Create = document.getElementById('cbi-table-1-Create');
if (proxy_mg) {
proxy_mg.style.textAlign="center";
rule_mg.style.textAlign="center";
Commit.style.textAlign="center";
Apply.style.textAlign="center";
Create.style.textAlign="center";
};
});
//]]>
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,203 @@
<%#
Copyright 2010 Jo-Philipp Wich <jow@openwrt.org>
Licensed to the public under the Apache License 2.0.
-%>
<%
local diag_host = "www.instagram.com"
local dns_host = "www.instagram.com"
%>
<script type="text/javascript">//<![CDATA[
function show_diag_info(addr)
{
var addr = addr;
var legend = document.getElementById('diag-rc-legend');
var output = document.getElementById('diag-rc-output');
if (legend && output)
{
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "diag_connection")%>', {addr: addr}, function(x, status) {
legend.textContent = '<%:Connection Test Result%>';
if (x && x.status == 200 && x.responseText != "")
{
output.innerHTML = '';
var ta = document.createElement('textarea');
ta.style.width = '100%';
ta.value = x.responseText;
output.appendChild(ta);
_cmWhenReady(function() {
other_editor(ta, true, '100%', '400px');
});
}
else if (x.status == 500)
{
output.innerHTML = '<span class="error"><%:Bad address specified!%></span>';
}
else
{
output.innerHTML = '<span class="error"><%:Could not find any connection logs!%></br></br><%:1. It may be that the plugin is not running%></br></br><%:2. It may be that the cache causes the browser to directly use the IP for access%></br></br><%:3. It may be that DNS hijacking did not take effect, so clash unable to reverse the domain name%></br></br><%:4. It may be that the filled address cannot be resolved and connected%></span>';
}
});
}
}
function update_status(field)
{
var addr = field.value;
var legend = document.getElementById('diag-rc-legend');
var output = document.getElementById('diag-rc-output');
if (legend && output)
{
output.innerHTML =
'<img src="<%=resource%>/icons/loading.svg" onerror="this.onerror=null;this.src=\'<%=resource%>/icons/loading.gif\'" alt="<%:Loading%>" style="vertical-align:middle" /> ' +
'<%:Waiting for command to complete...%>';
legend.textContent = '<%:Collecting data...%>';
legend.parentNode.style.display = 'block';
legend.style.display = 'block';
}
let HTTP = {
checker: (domain) => {
let img = new Image;
let timeout = setTimeout(() => {
img.onerror = img.onload = null;
show_diag_info(addr);
}, 10000);
img.onerror = () => {
clearTimeout(timeout);
show_diag_info(addr);
}
img.onload = () => {
clearTimeout(timeout);
show_diag_info(addr);
}
img.src = `https://${domain}/favicon.ico?${+(new Date)}`
},
runcheck: () => {
HTTP.checker(addr);
}
};
HTTP.runcheck();
}
function diag_dns(field)
{
var addr = field.value;
var legend = document.getElementById('dns-rc-legend');
var output = document.getElementById('dns-rc-output');
if (legend && output)
{
output.innerHTML =
'<img src="<%=resource%>/icons/loading.svg" onerror="this.onerror=null;this.src=\'<%=resource%>/icons/loading.gif\'" alt="<%:Loading%>" style="vertical-align:middle" /> ' +
'<%:Waiting for command to complete...%>';
legend.textContent = '<%:Collecting data...%>';
legend.parentNode.style.display = 'block';
legend.style.display = 'block';
}
if (legend && output)
{
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "diag_dns")%>', {addr: addr}, function(x, status) {
legend.textContent = '<%:DNS Test Result%>';
if (x && x.status == 200 && x.responseText != "")
{
output.innerHTML = '';
var ta = document.createElement('textarea');
ta.style.width = '100%';
ta.value = x.responseText;
output.appendChild(ta);
_cmWhenReady(function() {
other_editor(ta, true, '100%', '400px');
});
}
else if (x.status == 500)
{
output.innerHTML = '<span class="error"><%:Bad address specified!%></span>';
}
else
{
output.innerHTML = '<span class="error"><%:No Response Found!%></span>';
}
});
}
}
function gen_debug_logs()
{
var legend = document.getElementById('debug-rc-legend');
var output = document.getElementById('debug-rc-output');
if (legend && output)
{
output.innerHTML =
'<img src="<%=resource%>/icons/loading.svg" onerror="this.onerror=null;this.src=\'<%=resource%>/icons/loading.gif\'" alt="<%:Loading%>" style="vertical-align:middle" /> ' +
'<%:Waiting for command to complete...%>';
legend.textContent = '<%:Collecting data...%>';
legend.parentNode.style.display = 'block';
legend.style.display = 'block';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "gen_debug_logs")%>', null, function(x, status) {
legend.textContent = '<%:Generated Logs%>';
if (x && x.status == 200 && x.responseText != "")
{
output.innerHTML = '';
var ta = document.createElement('textarea');
ta.style.width = '100%';
ta.value = x.responseText;
output.appendChild(ta);
_cmWhenReady(function() {
other_editor(ta, true, '100%', '400px');
});
}
else
{
output.innerHTML = '<span class="error"><%:Some error occurred!%></span>';
}
}
);
}
}
//]]></script>
<form>
<fieldset class="diag-style">
<div style="width:33%; float: left; text-align: center; white-space: nowrap;">
<%:Connection Test%>&nbsp;&nbsp;&nbsp;&nbsp;
<input type="text" value="<%=diag_host%>" name="diag" />
<input type="button" value="<%:Click to Test%>" class="btn cbi-button cbi-button-apply" onclick="update_status(this.form.diag)" />
</div>
<div style="width:33%; float: left; text-align: center; white-space: nowrap;">
<%:DNS Test%>&nbsp;&nbsp;&nbsp;&nbsp;
<input type="text" value="<%=dns_host%>" name="dns_host" />
<input type="button" value="<%:Click to Test%>" class="btn cbi-button cbi-button-apply" onclick="diag_dns(this.form.dns_host)" />
</div>
<div style="width:33%; float: left; text-align: center; white-space: nowrap;">
<%:Generate Logs%>&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" value="<%:Click to Generate%>" class="btn cbi-button cbi-button-apply" onclick="gen_debug_logs(this)" />
</div>
</fieldset>
<div class="diag-style" style="display:none">
<div id="diag-rc-legend" class="diag-legend"><%:Collecting data...%></div>
<span id="diag-rc-output"></span>
</div>
<div class="diag-style" style="display:none">
<div id="dns-rc-legend" class="diag-legend"><%:Collecting data...%></div>
<span id="dns-rc-output"></span>
</div>
<div class="diag-style" style="display:none">
<div id="debug-rc-legend" class="diag-legend"><%:Collecting data...%></div>
<span id="debug-rc-output"></span>
</div>
</form>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
<%+cbi/valueheader%>
<div style="text-align: center; margin:2% auto; display: block; width: 100%; text-overflow: ellipsis;">
<div>
<%:Note: Please Upload File According To File Type, File Will Be Saved To The Prompt Path%>
</div>
<div style="color: green; margin-top: 1%;">
<%
local val = self:cfgvalue(section) or self.default or ""
write(pcdata(val))
%>
</div>
</div>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,37 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function act_flush_dns_cache(btn)
{
btn.disabled = true;
btn.value = '<%:Flushing...%> ';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash","flush_dns_cache")%>',
null,
function(x,status)
{
if ( x && x.status == 200 ) {
if(status.flush_status == "0" || status.flush_status != "")
{
btn.value = '<%:Flush Failed%>';
}
else
{
btn.value = '<%:Flush Successful%>';
}
}
else {
btn.value = '<%:Flush Timeout%>';
}
}
);
btn.disabled = false;
return false;
};
//]]></script>
<input type="button" class="btn cbi-button cbi-input-reload" value="<%:Flush%>" onclick="return act_flush_dns_cache(this)" />
<%+cbi/valuefooter%>

View File

@ -0,0 +1,37 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function act_flush_smart_cache(btn)
{
btn.disabled = true;
btn.value = '<%:Flushing...%> ';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash","flush_smart_cache")%>',
null,
function(x,status)
{
if ( x && x.status == 200 ) {
if(status.flush_status == "0" || status.flush_status != "")
{
btn.value = '<%:Flush Failed%>';
}
else
{
btn.value = '<%:Flush Successful%>';
}
}
else {
btn.value = '<%:Flush Timeout%>';
}
}
);
btn.disabled = false;
return false;
};
//]]></script>
<input type="button" class="btn cbi-button cbi-input-reload" value="<%:Flush%>" onclick="return act_flush_smart_cache(this)" />
<%+cbi/valuefooter%>

View File

@ -0,0 +1,74 @@
<%+cbi/valueheader%>
<script type="text/javascript">
//<![CDATA[
(function(){
var algoSelect = document.querySelector('select[name$="config_age_algo"]') || document.querySelector('select[name*="config_age_algo"]');
var age_public = document.querySelector('input[name$="config_age_public"]') || document.querySelector('input[name*="config_age_public"]');
var age_secret = document.querySelector('input[name$="config_age_secret"]') || document.querySelector('input[name*="config_age_secret"]');
if (!algoSelect || !age_public || !age_secret) return;
var btn = document.createElement('input');
btn.type = 'button';
btn.className = 'btn cbi-button cbi-button-apply';
btn.id = 'cbi-age-generate-btn';
btn.value = '<%:Generate Age Key%>';
var parent = algoSelect.parentNode || algoSelect;
parent.insertBefore(btn, algoSelect.nextSibling);
var pbtn = document.createElement('input');
pbtn.type = 'button';
pbtn.className = 'btn cbi-button cbi-button-apply';
pbtn.id = 'cbi-age-cal-public-btn';
pbtn.value = '<%:Calculate Public Key%>';
var pparent = age_public.parentNode || age_public;
pparent.appendChild(pbtn);
btn.onclick = function() {
var algo = (algoSelect && algoSelect.value) ? algoSelect.value : 'keygen';
btn.disabled = true;
var old = btn.value;
btn.value = '<%:Generating...%>';
XHR.get("<%=luci.dispatcher.build_url('admin','services','openclash','generate_age_key')%>", { algo: algo }, function(xhr, data) {
btn.disabled = false;
btn.value = old;
if (xhr && xhr.status == 200 && data && data.status === 'success') {
if (age_secret) age_secret.value = data.secret || '';
if (age_public) age_public.value = data.public || '';
btn.value = '<%:Generate Age Key%>';
setTimeout(function(){ btn.value = old; }, 4000);
} else {
btn.value = '<%:Failed to generate age key%>';
setTimeout(function(){ btn.value = old; }, 4000);
}
});
return false;
};
pbtn.onclick = function() {
var secret = age_secret.value.trim();
pbtn.disabled = true;
var old = pbtn.value;
pbtn.value = '<%:Generating...%>';
if (!secret) {
alert("<%:Please enter the Age Secret Key to calculate the public key!%>");
} else {
XHR.get("<%=luci.dispatcher.build_url('admin','services','openclash','cal_age_public_key')%>", { secret: secret }, function(xhr, data) {
pbtn.disabled = false;
pbtn.value = old;
if (xhr && xhr.status == 200 && data && data.status === 'success') {
if (age_public) age_public.value = data.public || '';
pbtn.value = '<%:Calculate Public Key%>';
setTimeout(function(){ pbtn.value = old; }, 4000);
} else {
pbtn.value = '<%:Failed to calculate public key%>';
setTimeout(function(){ pbtn.value = old; }, 4000);
}
});
}
pbtn.disabled = false;
pbtn.value = old;
return false;
};
})();
//]]>
</script>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,19 @@
<%+cbi/valueheader%>
<script type="text/javascript">
//<![CDATA[
function import_file_name(btn, option, file_path) {
var file_name = prompt("<%:Paste the name of the new file (including the extension) here:%>", "");
if (file_name == null || file_name == "") {
return false;
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "create_file")%>', {filename: file_name, filepath: file_path}, function(x, status) {
if (x && x.status == 200)
{
location.reload();
}
});
}
//]]>
</script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Create File%>" onclick="return import_file_name(this, '<%=self.option%>', '<%=self.value%>')" />
<%+cbi/valuefooter%>

View File

@ -0,0 +1,23 @@
<%+cbi/valueheader%>
<script type="text/javascript">
//<![CDATA[
function import_new_file_name(btn, file_path, file_name) {
var new_file_name = prompt("<%:Paste the new file name (including the extension) here:%>", "");
if (new_file_name == null || new_file_name == "") {
return false;
}
if (file_path == "/etc/openclash/config/" && new_file_name.split('.').pop() != "yaml" && new_file_name.split('.').pop() != "yml") {
alert('<%:Illegal extension!%>')
return false;
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "rename_file")%>', {new_file_name: new_file_name, file_path: file_path, file_name: file_name}, function(x, status) {
if (x && x.status == 200)
{
location.reload();
}
});
}
//]]>
</script>
<input type="button" class="btn cbi-button cbi-input-apply" value="<%:Rename%>" onclick="return import_new_file_name(this, '<%=self.option%>', '<%=self.value%>')" />
<%+cbi/valuefooter%>

View File

@ -0,0 +1,693 @@
<%+cbi/valueheader%>
<body>
<div id="tab" class="cbi-section">
<div id="tab-header">
<ul class="cbi-tabmenu">
<li name="tab-header" class="cbi-tab"><a href="#"><%:OpenClash Log%></a></li>
<li name="tab-header" class="cbi-tab-disabled"><a href="#"><%:Core Log%></a></li>
</ul>
</div>
<div id="tab-content">
<div class="dom" style="display: block;">
<textarea id="cbid.openclash.config.clog" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="32" cols="60" readonly="readonly" ></textarea>
</div>
<div class="dom">
<textarea id="core_log" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="32" cols="60" readonly="readonly" ></textarea>
<div class="radio-button">
<input type="radio" id="info" name="radios" value="info" checked onclick="return switch_log_level(this.value)"/>
<label for="info"><%:Info%></label>
<input type="radio" id="warning" name="radios" value="warning" onclick="return switch_log_level(this.value)"/>
<label for="warning"><%:Warning%></label>
<input type="radio" id="error" name="radios" value="error" onclick="return switch_log_level(this.value)"/>
<label for="error"><%:Error%></label>
<input type="radio" id="debug" name="radios" value="debug" onclick="return switch_log_level(this.value)"/>
<label for="debug"><%:Debug%></label>
<input type="radio" id="silent" name="radios" value="silent" onclick="return switch_log_level(this.value)"/>
<label for="silent"><%:Silent%></label>
</div>
</div>
</div>
</div>
<fieldset style="text-align: center; width: 100%" class="cbi-section">
<div class="btn-group">
<input type="button" class="btn cbi-button cbi-button-apply" id="stop_refresh_button" value="<%:Stop Refresh%>" onclick=" return stop_refresh() "/>
<input type="button" class="btn cbi-button cbi-button-apply" id="start_refresh_button" value="<%:Start Refresh%>" onclick=" return start_refresh() "/>
<input type="button" class="btn cbi-button cbi-button-apply" id="del_log_button" value="<%:Clean%>" onclick=" return del_log() " />
<input type="button" class="btn cbi-button cbi-button-apply" id="down_log_button" value="<%:Download Log%>" onclick=" return download_log() " />
</div>
</fieldset>
</body>
<script type="text/javascript">//<![CDATA[
var r;
var s;
var log_len = 0;
var lv = document.getElementById('cbid.openclash.config.clog');
var cl = document.getElementById('core_log');
var animatingOC = false;
var animatingCore = false;
var coreWebSocket = null;
var coreLogBuffer = [];
var currentLogLevel = 'info';
var wsRetryDelay = 2000;
var wsRetryTimer = null;
var isPolling = false;
var core_refresh = true;
function setLogTab(log_level) {
var radio = document.getElementsByName("radios");
for (i=0; i<radio.length; i++) {
if (radio[i].value == log_level && ! radio[i].checked) {
radio[i].checked = true;
}
}
};
function get_log_level() {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "log_level")%>', null, function(x, status) {
if (x && x.status == 200 && status.log_level != "") {
currentLogLevel = status.log_level;
setLogTab(currentLogLevel);
coreLogWebSocket();
}
});
};
function switch_log_level(value)
{
if (!coreWebSocket || coreWebSocket.readyState !== WebSocket.OPEN) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_log")%>', {log_level: value}, function(x, status) {
if (!x || x.status != 200) {
alert(' <%:Log Level%>: ' + value + ' <%:switching failed!%>');
return
}
});
}
currentLogLevel = value;
setLogTab(currentLogLevel);
coreLogWebSocket();
};
function stop_refresh() {
clearTimeout(r);
coreLogWebSocketStop();
return
};
function start_refresh() {
clearTimeout(r);
coreLogWebSocket();
r=setTimeout("poll_log()",1000);
return
};
function createAndDownloadFile(fileName, content) {
var aTag = document.createElement('a');
var blob = new Blob([content]);
aTag.download = fileName;
aTag.href = URL.createObjectURL(blob);
aTag.click();
URL.revokeObjectURL(blob);
};
function download_log(){
var dt = new Date();
var timestamp = dt.getFullYear()+"-"+(dt.getMonth()+1)+"-"+dt.getDate()+"-"+dt.getHours()+"-"+dt.getMinutes()+"-"+dt.getSeconds();
var oc_content = "";
if (typeof editor_oc !== 'undefined' && editor_oc) {
oc_content = editor_oc.getValue();
} else if (lv && lv.value) {
oc_content = lv.value;
} else if (lv && lv.innerHTML) {
oc_content = lv.innerHTML;
}
var core_content = "";
if (typeof editor_core !== 'undefined' && editor_core) {
core_content = editor_core.getValue();
} else if (cl && cl.value) {
core_content = cl.value;
} else if (cl && cl.innerHTML) {
core_content = cl.innerHTML;
}
oc_content = oc_content.split('\n').filter(function(line) {
return line.indexOf("】<%:Downloading URL%>【") === -1;
}).join('\n');
if (!oc_content.trim() && !core_content.trim()) {
return;
}
var downloadContent = "";
if (oc_content.trim()) {
downloadContent += "<%:OpenClash Log%>:\n" + oc_content + "\n\n";
}
if (core_content.trim()) {
downloadContent += "<%:Core Log%>:\n" + core_content;
}
createAndDownloadFile("OpenClash-"+timestamp+".log", downloadContent);
return;
};
function del_log() {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "del_log")%>',null,function(x, data){
lv.innerHTML="";
cl.innerHTML="";
coreLogBuffer = [];
log_len = 0;
editor_oc.setValue(lv.value);
editor_core.setValue(cl.value);
editor_core.refresh();
editor_oc.refresh();
});
return
};
function p(s) {
return s < 10 ? '0' + s: s;
};
function getWebSocketConnectionInfo(callback) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "status")%>', null, function(x, status) {
if (x && x.status == 200 && status) {
callback(status);
} else {
callback(null);
}
});
}
function coreLogWebSocketStop() {
if (wsRetryTimer) {
clearTimeout(wsRetryTimer);
wsRetryTimer = null;
}
var ws = coreWebSocket;
coreWebSocket = null;
if (!ws) return;
["onopen", "onmessage", "onerror", "onclose"].forEach(function(event) {
try {
ws[event] = null;
} catch (e) {}
});
try {
if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
ws.close();
}
} catch (e) {}
coreLogBuffer = [];
}
function coreLogWebSocketRetry() {
wsRetryTimer = setTimeout(function() {
coreLogWebSocket();
}, wsRetryDelay);
}
function coreLogWebSocket() {
coreLogWebSocketStop();
getWebSocketConnectionInfo(function(status) {
if (!status || !status.daip || !status.cn_port || !status.clash) {
return;
}
var protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
var host, port;
if (status.daip && window.location.hostname === status.daip) {
host = status.daip;
port = status.cn_port;
} else if (status.daip && window.location.hostname !== status.daip &&
status.db_foward_domain && status.db_foward_port) {
host = status.db_foward_domain;
port = status.db_foward_port;
} else {
host = status.daip;
port = status.cn_port;
}
var token = status.dase;
var level = currentLogLevel;
var wsUrl = protocol + host + ":" + port + "/logs?token=" + token + "&level=" + level;
try {
coreWebSocket = new WebSocket(wsUrl);
coreWebSocket.onopen = function() {
coreLogBuffer = [];
};
coreWebSocket.onmessage = function(event) {
if (event.data) {
try {
var msgData = JSON.parse(event.data);
if (msgData && msgData.payload) {
var now = new Date();
var timestamp = now.getFullYear() + "-" + p(now.getMonth() + 1) + "-" + p(now.getDate()) + " " +
p(now.getHours()) + ":" + p(now.getMinutes()) + ":" + p(now.getSeconds());
var type = msgData.type || 'info';
var translatedType = levelTranslations[type] || type;
coreLogBuffer.push(timestamp + " [" + translatedType + "] " + msgData.payload);
}
} catch (e) {
coreLogWebSocketRetry();
}
}
};
coreWebSocket.onerror = function(error) {
coreLogWebSocketRetry();
};
coreWebSocket.onclose = function(event) {
coreWebSocket = null;
if (!event.wasClean) {
coreLogWebSocketRetry();
}
};
} catch (e) {
coreLogWebSocketRetry();
}
});
}
function line_tolocal(str) {
var trans_local = new Array();
var local_count = 0;
str.trim().split('\n').forEach(function(v, i) {
var regex = /(time=)"([^"]*)"/g;
var res = regex.exec(v);
try {
if (res) {
var dt = new Date(res[2]);
if (!isNaN(dt.getTime())) {
var timestamp = dt.getFullYear() + "-" + p(dt.getMonth() + 1) + "-" + p(dt.getDate()) + " " +
p(dt.getHours()) + ":" + p(dt.getMinutes()) + ":" + p(dt.getSeconds());
var levelMatch = v.match(/level=(\w+)/);
var level = levelMatch ? levelMatch[1] : 'info';
var translatedLevel = levelTranslations[level] || level;
var msgMatch = v.match(/msg="(.*)"/);
var msg = msgMatch ? msgMatch[1] : '';
trans_local[local_count] = timestamp + " [" + translatedLevel + "] " + msg;
local_count++;
} else {
trans_local[local_count] = v;
local_count++;
}
} else {
try {
var dtt = new Date(v.substring(0, 19));
if (!isNaN(dtt.getTime()) && v.substring(0, 19).match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)) {
trans_local[local_count] = v;
local_count++;
} else {
trans_local[local_count] = v;
local_count++;
}
} catch (e) {
trans_local[local_count] = v;
local_count++;
}
}
} catch (e) {
trans_local[local_count] = v;
local_count++;
}
});
return trans_local;
};
function smoothlyDisplayLogs(newLines, target, isEditor, currentContent, isActiveTab) {
var scrollPosition = null;
var isAtTop = false;
var cursorPos = null;
var selectionRange = null;
var isFirstLoad = !currentContent || currentContent.trim() === "";
if (isEditor && target) {
scrollPosition = target.getScrollInfo();
isAtTop = (scrollPosition.top < 20);
if (target.hasFocus()) {
cursorPos = target.getCursor();
if (cursorPos.line === 0) {
cursorPos = null;
selectionRange = null;
} else if (target.somethingSelected()) {
selectionRange = {
from: target.getCursor(true),
to: target.getCursor(false)
};
} else {
selectionRange = null;
}
} else {
cursorPos = null;
selectionRange = null;
}
} else if (!isEditor && target) {
scrollPosition = target.scrollTop;
isAtTop = (target.scrollTop < 20);
}
if ((target === editor_oc && animatingOC) || (target === editor_core && animatingCore) || !isActiveTab) {
var content = "";
var lines = newLines.slice().reverse();
for (var i = 0; i < lines.length; i++) {
content += lines[i] + "\n";
}
content = content + (currentContent || "");
var allLines = content.split("\n");
if (allLines.length > 2000) {
allLines = allLines.slice(0, 1999);
allLines.push("...");
content = allLines.join("\n");
}
if (isEditor) {
var addedLines = lines.length;
target.setValue(content);
if (!isAtTop && scrollPosition) {
if (cursorPos) {
cursorPos.line += addedLines;
target.setCursor(cursorPos);
target.scrollIntoView({line: cursorPos.line, ch: cursorPos.ch}, 300);
} else {
target.scrollTo(scrollPosition.left, scrollPosition.top);
}
if (selectionRange) {
selectionRange.from.line += addedLines;
selectionRange.to.line += addedLines;
target.setSelection(selectionRange.from, selectionRange.to);
target.scrollIntoView({
from: selectionRange.from,
to: selectionRange.to
}, 300);
}
} else if (isAtTop) {
target.scrollTo(0, 0);
if (isFirstLoad) {
target.setCursor({line: 0, ch: 0});
}
}
target.refresh();
} else {
var oldScrollTop = scrollPosition;
target.innerHTML = content;
if (!isAtTop && oldScrollTop) {
target.scrollTop = oldScrollTop;
}
}
return;
}
if (target === editor_oc || target === lv) {
animatingOC = true;
} else {
animatingCore = true;
}
var totalLines = newLines.length;
var batchSize, interval;
if (totalLines <= 10) {
batchSize = 2;
interval = 90;
} else if (totalLines <= 50) {
batchSize = 10;
interval = 60;
} else if (totalLines <= 200) {
batchSize = 30;
interval = 35;
} else if (totalLines <= 500) {
batchSize = 60;
interval = 25;
} else if (totalLines <= 1000) {
batchSize = 120;
interval = 15;
} else if (totalLines <= 1500) {
batchSize = 180;
interval = 10;
} else {
batchSize = 250;
interval = 5;
}
var displayedContent = currentContent || "";
var logLines = newLines.slice();
var currentBatchCount = 0;
var accumulatedContent = "";
function displayNextBatch() {
if (currentBatchCount >= logLines.length) {
if (target === editor_oc || target === lv) {
animatingOC = false;
} else {
animatingCore = false;
}
if (isEditor && !isAtTop && cursorPos) {
cursorPos.line += logLines.length;
target.setCursor(cursorPos);
if (selectionRange) {
selectionRange.from.line += logLines.length;
selectionRange.to.line += logLines.length;
target.setSelection(selectionRange.from, selectionRange.to);
target.scrollIntoView({
from: selectionRange.from,
to: selectionRange.to
}, 300);
} else {
target.scrollIntoView({line: cursorPos.line, ch: cursorPos.ch}, 300);
}
}
return;
}
var nextBatchSize = Math.min(batchSize, logLines.length - currentBatchCount);
var batchLines = logLines.slice(currentBatchCount, currentBatchCount + nextBatchSize).reverse();
currentBatchCount += nextBatchSize;
if (accumulatedContent) {
accumulatedContent = batchLines.join("\n") + "\n" + accumulatedContent;
} else {
accumulatedContent = batchLines.join("\n");
}
var content = accumulatedContent + (displayedContent ? "\n" + displayedContent : "");
var contentLines = content.split("\n");
if (contentLines.length > 2000) {
contentLines = contentLines.slice(0, 1999);
contentLines.push("...");
content = contentLines.join("\n");
}
if (isEditor) {
var currentScrollInfo = isAtTop ? null : target.getScrollInfo();
target.setValue(content);
if (!isAtTop && currentScrollInfo) {
target.scrollTo(currentScrollInfo.left, currentScrollInfo.top);
}
target.refresh();
} else {
var currentScrollTop = isAtTop ? null : target.scrollTop;
target.innerHTML = content;
if (!isAtTop && currentScrollTop !== null) {
target.scrollTop = currentScrollTop;
}
}
if (currentBatchCount < logLines.length) {
setTimeout(displayNextBatch, interval);
} else {
if (target === editor_oc || target === lv) {
animatingOC = false;
} else {
animatingCore = false;
}
if (isEditor && !isAtTop && cursorPos) {
cursorPos.line += logLines.length;
target.setCursor(cursorPos);
if (selectionRange) {
selectionRange.from.line += logLines.length;
selectionRange.to.line += logLines.length;
target.setSelection(selectionRange.from, selectionRange.to);
target.scrollIntoView({
from: selectionRange.from,
to: selectionRange.to
}, 300);
} else {
target.scrollIntoView({line: cursorPos.line, ch: cursorPos.ch}, 300);
}
}
}
}
displayNextBatch();
}
function poll_log(){
var activeTabId = 0;
var titles = document.getElementsByName('tab-header');
var core_logs = [];
var oc_logs = [];
var currentCoreContent = "";
var currentContent = "";
var bufferToProcess = [];
clearTimeout(r);
for(var i=0; i<titles.length; i++){
if(titles[i].className === 'cbi-tab') {
activeTabId = i;
break;
}
}
if (coreWebSocket && coreWebSocket.readyState == WebSocket.OPEN) {
core_refresh = false;
if (coreLogBuffer.length > 0) {
bufferToProcess = coreLogBuffer;
coreLogBuffer = [];
core_logs = line_tolocal(bufferToProcess.join('\n'));
if (editor_core) {
currentCoreContent = editor_core.getValue();
smoothlyDisplayLogs(core_logs, editor_core, true, currentCoreContent, activeTabId === 1);
} else if (cl) {
currentCoreContent = cl.innerHTML;
smoothlyDisplayLogs(core_logs, cl, false, currentCoreContent, activeTabId === 1);
}
}
} else {
core_refresh = true;
}
if (isPolling) {
r = setTimeout("poll_log()", 1000);
return;
}
isPolling = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "refresh_log")%>',
{
log_len: log_len, core_refresh: core_refresh
},
function(x, status) {
if (x && x.status == 200) {
if (status) {
if (!status.update) {
isPolling = false;
r = setTimeout("poll_log()", 1000);
return;
}
if (status.len) {
log_len = status.len;
}
if (status.oc_log && status.oc_log !== "") {
oc_logs = line_tolocal(status.oc_log);
if (oc_logs.length > 0) {
if (editor_oc) {
currentContent = editor_oc.getValue();
smoothlyDisplayLogs(oc_logs, editor_oc, true, currentContent, activeTabId === 0);
} else if (lv) {
currentContent = lv.innerHTML;
smoothlyDisplayLogs(oc_logs, lv, false, currentContent, activeTabId === 0);
}
}
}
if (status.core_log && status.core_log !== "") {
core_logs = line_tolocal(status.core_log);
if (core_logs.length > 0) {
if (editor_core) {
currentCoreContent = editor_core.getValue();
smoothlyDisplayLogs(core_logs, editor_core, true, currentCoreContent, activeTabId === 1);
} else if (cl) {
currentCoreContent = cl.innerHTML;
smoothlyDisplayLogs(core_logs, cl, false, currentCoreContent, activeTabId === 1);
}
}
}
}
}
isPolling = false;
r = setTimeout("poll_log()", 1000);
}
);
};
window.onload = function(){
var titles = document.getElementsByName('tab-header');
var divs = document.getElementsByClassName('dom');
if(titles.length != divs.length) return;
for(var i=0; i<titles.length; i++){
var li = titles[i];
li.id = i;
function handleTabSwitch(tab) {
return function(e) {
if (tab.className === 'cbi-tab') return;
for(var j=0; j<titles.length; j++){
titles[j].className = 'cbi-tab-disabled';
divs[j].style.display = 'none';
}
tab.className = 'cbi-tab';
divs[tab.id].style.display = 'block';
if(tab.id == 0 && typeof editor_oc !== 'undefined') {
setTimeout(function(){
editor_oc.refresh();
}, 10);
} else if(tab.id == 1 && typeof editor_core !== 'undefined') {
setTimeout(function(){
editor_core.refresh();
}, 10);
}
};
}
li.onclick = handleTabSwitch(li);
li.ontouchstart = handleTabSwitch(li);
}
get_log_level();
poll_log();
};
//]]>
</script>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,813 @@
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//cdn.jsdelivr.net">
<link rel="dns-prefetch" href="//whois.pconline.com.cn">
<link rel="dns-prefetch" href="//pubstatic.b0.upaiyun.com">
<link rel="dns-prefetch" href="//api.ip.sb">
<link rel="dns-prefetch" href="//api.ipify.org">
<link rel="dns-prefetch" href="//api.ttt.sh">
<link rel="dns-prefetch" href="//myip.ipip.net">
<link rel="dns-prefetch" href="//qqwry.api.skk.moe">
<link rel="dns-prefetch" href="//d.skk.moe">
<link rel="preconnect" href="https://pubstatic.b0.upaiyun.com">
<link rel="preconnect" href="https://whois.pconline.com.cn">
<link rel="preconnect" href="https://api.ip.sb">
<link rel="preconnect" href="https://api.ipify.org">
<link rel="preconnect" href="https://api.ttt.sh">
<link rel="preconnect" href="https://qqwry.api.skk.moe">
<link rel="preconnect" href="https://d.skk.moe">
<link rel="preconnect" href="http://myip.ipip.net">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no,minimal-ui">
<title>IP 地址查询</title>
</head>
<fieldset class="cbi-section">
<table width="100%">
<tr>
<td>
<div class="oc">
<div class="myip-main-card">
<div class="myip-content-grid">
<div class="myip-ip-section">
<p class="myip-section-title">
<%:IP Address%>
<span class="myip-toolbar">
<svg id="eye-icon" class="myip-icon-btn" width="20" height="20" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg" onclick="return privacy_my_ip(this)">
<title><%:Hide IP%></title>
<path id="eye-open" d="M128,56C48,56,16,128,16,128s32,72,112,72,112-72,112-72S208,56,128,56Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"></path>
<circle id="eye-open-circle" cx="128" cy="128" r="40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"></circle>
<g id="eye-closed" style="display: none;">
<line x1="48" y1="40" x2="208" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"></line>
<path d="M154.9,157.6A39.6,39.6,0,0,1,128,168a40,40,0,0,1-26.9-69.6" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"></path>
<path d="M74,68.6C33.2,89.2,16,128,16,128s32,72,112,72a117.9,117.9,0,0,0,54-12.6" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"></path>
<path d="M208.6,169.1C230.4,149.6,240,128,240,128S208,56,128,56a123.9,123.9,0,0,0-20.7,1.7" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"></path>
<path d="M135.5,88.7a39.9,39.9,0,0,1,32.3,35.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"></path>
</g>
</svg>
</span>
</p>
<div class="myip-ip-list">
<div class="myip-ip-item">
<span class="ip-title">UpaiYun</span>
<div class="myip-ip-result">
<span class="ip-result" id="ip-upaiyun"></span>
<span class="ip-geo" id="ip-upaiyun-geo"></span>
</div>
</div>
<div class="myip-ip-item">
<span class="ip-title">IPIP.NET</span>
<div class="myip-ip-result">
<span class="ip-result" id="ip-ipip"></span>
<span class="ip-geo" id="ip-ipip-geo"></span>
</div>
</div>
<div class="myip-ip-item">
<span class="ip-title">IP.SB</span>
<div class="myip-ip-result">
<span class="ip-result" id="ip-ipsb"></span>
<span class="ip-geo" id="ip-ipsb-geo"></span>
</div>
</div>
<div class="myip-ip-item">
<span class="ip-title">IPIFY</span>
<div class="myip-ip-result">
<span class="ip-result" id="ip-ipify"></span>
<span class="ip-geo" id="ip-ipify-geo"></span>
</div>
</div>
</div>
</div>
<div class="myip-check-section">
<p class="myip-section-title">
<%:Access Check%>
<span class="myip-toolbar">
<svg id="mode-icon" class="mode-icon myip-icon-btn" width="22" height="22" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" onclick="return toggle_mode_by_icon(this)">
<title><%:Router Mode%></title>
<rect x="4" y="28" width="40" height="14" rx="2" fill="#2F88FF" stroke="#333" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 35L22 35" stroke="#FFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="30" y="33" width="4" height="4" rx="2" fill="#FFF"/>
<path d="M12 28L12 8" stroke="#333" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 28V8" stroke="#333" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg id="data-refresh-icon" class="myip-icon-btn" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" onclick="return refresh_myip(this)">
<title><%:Refresh%></title>
<path d="M23 4v6h-6"></path>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</span>
</p>
<div class="myip-check-list">
<div class="myip-check-item">
<span class="ip-state_title"><%:Baidu Search%></span>
<div class="myip-status-result">
<span id="http-baidu"></span>
<span id="ldtime-baidu"></span>
</div>
</div>
<div class="myip-check-item">
<span class="ip-state_title"><%:NetEase Music%></span>
<div class="myip-status-result">
<span id="http-163"></span>
<span id="ldtime-163"></span>
</div>
</div>
<div class="myip-check-item">
<span class="ip-state_title">GitHub</span>
<div class="myip-status-result">
<span id="http-github"></span>
<span id="ldtime-github"></span>
</div>
</div>
<div class="myip-check-item">
<span class="ip-state_title">YouTube</span>
<div class="myip-status-result">
<span id="http-youtube"></span>
<span id="ldtime-youtube"></span>
</div>
</div>
</div>
</div>
<div class="myip-footer">
<p>Powered by <a onclick="return ip_skk()" href="javascript:void(0);">ip.skk.moe</a></p>
</div>
</div>
</div>
</div>
</td>
</tr>
</table>
</fieldset>
<script>
function addTitleOnOverflow() {
document.querySelectorAll('.ip-result, .ip-geo').forEach(function (span) {
if (span.scrollWidth > span.clientWidth && localStorage.getItem('privacy_my_ip') !== 'true') {
span.setAttribute('title', span.textContent);
} else {
span.removeAttribute('title');
}
});
}
function ip_skk()
{
url2='https://ip.skk.moe';
window.open(url2);
}
const $$ = document;
var ip_ipip_ip;
var ip_ipsb_ip;
var ip_upaiyun_ip;
var ip_ipify_ip;
var refresh_http;
var refresh_ip;
$$.getElementById('ip-ipip').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipify').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-upaiyun').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipsb').innerHTML = '<%:Querying...%>';
let random = parseInt(Math.random() * 100000000);
let IP = {
get: (url, type) =>
fetch(url, { method: 'GET' }).then((resp) => {
if (type === 'text')
return Promise.all([resp.ok, resp.status, resp.text(), resp.headers]);
else {
return Promise.all([resp.ok, resp.status, resp.json(), resp.headers]);
}
}).then(([ok, status, data, headers]) => {
if (ok) {
let json = {
ok,
status,
data,
headers
}
return json;
} else {
throw new Error(JSON.stringify(json.error));
}
}).catch(error => {
throw error;
}),
parseIPIpip: (ip, elID) => {
const v4 = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}';
const v4Exact = new RegExp(`^${v4}$`);
const anonymizedIp = (() => {
if (v4Exact.test(ip)) {
const [a, b, c] = ip.split('.');
return `${a}.${b}.${c}.0`;
}
return ip;
})();
fetch(`https://api.ip.sb/geoip/${anonymizedIp}`, {
referrerPolicy: 'no-referrer-when-downgrade',
}).then(r => r.json())
.then(resp => {
if ( resp.country && resp.country != '' && resp.isp && resp.isp != '' ) {
$$.getElementById(elID).innerHTML = resp.country + ' ' + resp.isp;
}
else {
fetch(`https://qqwry.api.skk.moe/${anonymizedIp}`, {
referrerPolicy: 'no-referrer-when-downgrade',
}).then(r => r.json())
.then(resp => {
if ( resp.geo.indexOf('skk.moe') == -1 ) {
$$.getElementById(elID).innerHTML = resp.geo;
}
else {
$$.getElementById(elID).innerHTML = 'Unknown';
}
})
}
})
},
getPcolIP: () => {
fetch(`https://whois.pconline.com.cn/ipJson.jsp?z=${random}`)
.then(response => response.json())
.then(data => {
let pcisp = data.addr.split(' ');
if (localStorage.getItem('privacy_my_ip') != 'true') {
$$.getElementById('ip-pcol').innerHTML = data.ip;
};
$$.getElementById('ip-pcol-geo').innerHTML = `${data.pro} ${data.city} ${data.region} ${pcisp[1]}`;
addTitleOnOverflow();
})
},
getUpaiIP: () => {
IP.get(`https://pubstatic.b0.upaiyun.com/?_upnode&z=${random}`, 'json')
.then(resp => {
if (localStorage.getItem('privacy_my_ip') != 'true') {
$$.getElementById('ip-upaiyun').innerHTML = resp.data.remote_addr;
};
$$.getElementById('ip-upaiyun-geo').innerHTML = resp.data.remote_addr_location.country + ' ' + resp.data.remote_addr_location.province + ' ' + resp.data.remote_addr_location.city + ' ' + resp.data.remote_addr_location.isp;
addTitleOnOverflow();
})
},
getIpipIP: () => {
IP.get(`http://myip.ipip.net?z=${random}`, 'text')
.then(resp => {
const ipMatch = resp.data.match(/当前 IP:([0-9A-Fa-f:.]+)/);
const geoMatch = resp.data.match(/来自于:(.+)/);
if (ipMatch && geoMatch) {
if (localStorage.getItem('privacy_my_ip') != 'true') {
$$.getElementById('ip-ipip').innerHTML = ipMatch[1];
}
$$.getElementById('ip-ipip-geo').innerHTML = geoMatch[1].trim();
addTitleOnOverflow();
}
})
},
getIpifyIP: () => {
IP.get(`https://api.ipify.org/?format=json&z=${random}`, 'json')
.then(resp => {
if (localStorage.getItem('privacy_my_ip') != 'true') {
$$.getElementById('ip-ipify').innerHTML = resp.data.ip;
};
return resp.data.ip;
})
.then(ip => {
IP.parseIPIpip(ip, 'ip-ipify-geo');
addTitleOnOverflow();
})
},
getIpsbIP: () => {
fetch(`https://api.ip.sb/geoip?z=${random}`)
.then(response => response.json())
.then(data => {
if (localStorage.getItem('privacy_my_ip') != 'true') {
$$.getElementById('ip-ipsb').innerHTML = data.ip;
};
$$.getElementById('ip-ipsb-geo').innerHTML = `${data.country} ${data.isp}`;
addTitleOnOverflow();
})
}
};
$$.getElementById('http-baidu').innerHTML = '<span><%:Testing...%></span>';
$$.getElementById('http-163').innerHTML = '<span><%:Testing...%></span>';
$$.getElementById('http-github').innerHTML = '<span><%:Testing...%></span>';
$$.getElementById('http-youtube').innerHTML = '<span><%:Testing...%></span>';
let HTTP = {
checker: (domain, cbElID, cbLoID) => {
if (use_router_mode) {
HTTP.checker_router(domain, cbElID, cbLoID);
} else {
HTTP.checker_browser(domain, cbElID, cbLoID);
}
},
checker_browser: (domain, cbElID, cbLoID) => {
let img = new Image;
let img_start_time = (+new Date());
let timeout = setTimeout(() => {
img.onerror = img.onload = null;
$$.getElementById(cbLoID).style.display = 'none';
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-error"><%:Access Timed Out%></span>'
}, 5000);
img.onerror = () => {
clearTimeout(timeout);
$$.getElementById(cbLoID).style.display = 'none';
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-error"><%:Access Denied%></span>'
}
img.onload = () => {
clearTimeout(timeout);
let img_load_time = (new Date())- img_start_time;
if ($$.getElementById(cbLoID).style.display == 'none') {
$$.getElementById(cbLoID).style.display = '';
}
if (img_load_time <= 500) {
$$.getElementById(cbLoID).innerHTML = '<span class="sk-load-success">' + img_load_time + ' ms</span>'
$$.getElementById(cbLoID).style.color = '#32b643';
}
else if (img_load_time > 500 && img_load_time <= 1000) {
$$.getElementById(cbLoID).innerHTML = '<span class="sk-load-success">' + img_load_time + ' ms</span>'
$$.getElementById(cbLoID).style.color = 'var(--warning-color)';
}
else {
$$.getElementById(cbLoID).innerHTML = '<span class="sk-load-success">' + img_load_time + ' ms</span>'
$$.getElementById(cbLoID).style.color = '#e85600';
}
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-success"><%:Access Normal%></span>'
}
img.src = `https://${domain}/favicon.ico?${+(new Date)}`
},
checker_router: (domain, cbElID, cbLoID) => {
let start_time = (+new Date());
var xhr = new XMLHttpRequest();
xhr.open('GET', '/cgi-bin/luci/admin/services/openclash/website_check?domain=' + encodeURIComponent(domain), true);
xhr.timeout = 10000;
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
let response_time = (new Date()) - start_time;
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
if ($$.getElementById(cbLoID).style.display == 'none') {
$$.getElementById(cbLoID).style.display = '';
}
let load_time = response.response_time || response_time;
if (load_time <= 500) {
$$.getElementById(cbLoID).innerHTML = '<span class="sk-load-success">' + load_time + ' ms</span>'
$$.getElementById(cbLoID).style.color = '#32b643';
}
else if (load_time > 500 && load_time <= 1000) {
$$.getElementById(cbLoID).innerHTML = '<span class="sk-load-success">' + load_time + ' ms</span>'
$$.getElementById(cbLoID).style.color = 'var(--warning-color)';
}
else {
$$.getElementById(cbLoID).innerHTML = '<span class="sk-load-success">' + load_time + ' ms</span>'
$$.getElementById(cbLoID).style.color = '#e85600';
}
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-success"><%:Access Normal%></span>'
} else {
$$.getElementById(cbLoID).style.display = 'none';
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-error"><%:Access Denied%></span>'
}
} catch (e) {
$$.getElementById(cbLoID).style.display = 'none';
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-error"><%:Access Denied%></span>'
}
} else {
$$.getElementById(cbLoID).style.display = 'none';
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-error"><%:Access Denied%></span>'
}
}
};
xhr.ontimeout = function() {
$$.getElementById(cbLoID).style.display = 'none';
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-error"><%:Access Timed Out%></span>'
};
xhr.onerror = function() {
$$.getElementById(cbLoID).style.display = 'none';
$$.getElementById(cbElID).innerHTML = '<span class="sk-text-error"><%:Access Denied%></span>'
};
xhr.send();
},
runcheck: () => {
HTTP.checker('www.baidu.com', 'http-baidu', 'ldtime-baidu');
HTTP.checker('s1.music.126.net/style', 'http-163', 'ldtime-163');
HTTP.checker('github.com', 'http-github', 'ldtime-github');
HTTP.checker('www.youtube.com', 'http-youtube', 'ldtime-youtube');
}
};
function myip_Load()
{
//IP.getPcolIP();
IP.getUpaiIP();
IP.getIpipIP();
IP.getIpifyIP();
IP.getIpsbIP();
};
function show_my_ip() {
if (localStorage.getItem('privacy_my_ip') == 'true') {
update_eye_icon(false);
ip_ipip_ip = $$.getElementById('ip-ipip').innerHTML;
ip_ipsb_ip = $$.getElementById('ip-ipsb').innerHTML;
ip_upaiyun_ip = $$.getElementById('ip-upaiyun').innerHTML;
ip_ipify_ip = $$.getElementById('ip-ipify').innerHTML;
$$.getElementById('ip-ipip').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipsb').innerHTML = '***.***.***.***';
$$.getElementById('ip-upaiyun').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipify').innerHTML = '***.***.***.***';
addTitleOnOverflow();
}
else {
if (!refresh_http) {
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
}
if (!refresh_ip) {
if (use_router_mode) {
refresh_ip = setInterval("get_router_ip_info()", Math.floor(Math.random()*(40-15+1)+15)*1000);
} else {
refresh_ip = setInterval("myip_Load()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
}
};
};
function update_eye_icon(isOpen) {
var eyeIcon = document.getElementById('eye-icon');
var eyeOpen = document.getElementById('eye-open');
var eyeOpenCircle = document.getElementById('eye-open-circle');
var eyeClosed = document.getElementById('eye-closed');
var titleElement = eyeIcon.querySelector('title');
if (isOpen) {
eyeOpen.style.display = '';
eyeOpenCircle.style.display = '';
eyeClosed.style.display = 'none';
if (titleElement) titleElement.textContent = '<%:Hide IP%>';
} else {
eyeOpen.style.display = 'none';
eyeOpenCircle.style.display = 'none';
eyeClosed.style.display = '';
if (titleElement) titleElement.textContent = '<%:Show IP%>';
}
};
function privacy_my_ip(svgElement) {
var isCurrentlyOpen = document.getElementById('eye-open').style.display !== 'none';
if (isCurrentlyOpen) {
clearInterval(refresh_ip);
refresh_ip = null;
localStorage.setItem('privacy_my_ip', 'true');
update_eye_icon(false);
ip_ipip_ip = $$.getElementById('ip-ipip').innerHTML;
ip_ipsb_ip = $$.getElementById('ip-ipsb').innerHTML;
ip_upaiyun_ip = $$.getElementById('ip-upaiyun').innerHTML;
ip_ipify_ip = $$.getElementById('ip-ipify').innerHTML;
$$.getElementById('ip-ipip').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipsb').innerHTML = '***.***.***.***';
$$.getElementById('ip-upaiyun').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipify').innerHTML = '***.***.***.***';
addTitleOnOverflow();
} else {
update_eye_icon(true);
localStorage.removeItem('privacy_my_ip');
if (ip_ipip_ip && ip_ipsb_ip && ip_upaiyun_ip && ip_ipify_ip) {
$$.getElementById('ip-ipip').innerHTML = ip_ipip_ip;
$$.getElementById('ip-ipsb').innerHTML = ip_ipsb_ip;
$$.getElementById('ip-upaiyun').innerHTML = ip_upaiyun_ip;
$$.getElementById('ip-ipify').innerHTML = ip_ipify_ip;
} else {
if (use_router_mode) {
get_router_ip_info();
} else {
myip_Load();
}
}
if (refresh_ip) {
clearInterval(refresh_ip);
refresh_ip = null;
}
if (use_router_mode) {
refresh_ip = setInterval(get_router_ip_info, Math.floor(Math.random() * (40 - 15 + 1) + 15) * 1000);
} else {
refresh_ip = setInterval(myip_Load, Math.floor(Math.random() * (40 - 15 + 1) + 15) * 1000);
}
addTitleOnOverflow();
};
};
var use_router_mode = true;
function toggle_mode_by_icon(svgElement) {
var rect = svgElement.querySelector('rect[fill="#2F88FF"]');
if (rect && rect.getAttribute('fill') === '#2F88FF') {
use_router_mode = false;
localStorage.setItem('myip_check_mode', 'false');
update_mode_icon();
clearAllIntervals();
init_browser_mode();
} else {
use_router_mode = true;
localStorage.setItem('myip_check_mode', 'true');
update_mode_icon();
clearAllIntervals();
init_router_mode();
}
}
function update_mode_icon() {
var modeIcon = document.getElementById('mode-icon');
var rect = modeIcon.querySelector('rect[x="4"]');
var paths = modeIcon.querySelectorAll('path');
var smallRect = modeIcon.querySelector('rect[x="30"]');
var titleElement = modeIcon.querySelector('title');
if (use_router_mode) {
rect.setAttribute('fill', '#2F88FF');
rect.setAttribute('stroke', '#333');
paths[0].setAttribute('stroke', '#FFF');
paths[1].setAttribute('stroke', '#333');
paths[2].setAttribute('stroke', '#333');
smallRect.setAttribute('fill', '#FFF');
if (titleElement) titleElement.textContent = '<%:Router Mode%>';
} else {
rect.setAttribute('fill', '#666');
rect.setAttribute('stroke', '#999');
paths[0].setAttribute('stroke', '#CCC');
paths[1].setAttribute('stroke', '#999');
paths[2].setAttribute('stroke', '#999');
smallRect.setAttribute('fill', '#CCC');
if (titleElement) titleElement.textContent = '<%:Browser Mode%>';
}
}
function init_router_mode() {
if (refresh_ip) {
clearInterval(refresh_ip);
refresh_ip = null;
}
if (localStorage.getItem('privacy_my_ip') === 'true') {
$$.getElementById('ip-ipip').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipsb').innerHTML = '***.***.***.***';
$$.getElementById('ip-upaiyun').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipify').innerHTML = '***.***.***.***';
}
get_router_ip_info();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("get_router_ip_info()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
}
function init_browser_mode() {
if (localStorage.getItem('privacy_my_ip') === 'true') {
$$.getElementById('ip-ipip').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipsb').innerHTML = '***.***.***.***';
$$.getElementById('ip-upaiyun').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipify').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipip-geo').innerHTML = '';
$$.getElementById('ip-ipsb-geo').innerHTML = '';
$$.getElementById('ip-upaiyun-geo').innerHTML = '';
$$.getElementById('ip-ipify-geo').innerHTML = '';
} else {
reset_display();
}
myip_Load();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("myip_Load()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
}
function get_router_ip_info() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/cgi-bin/luci/admin/services/openclash/myip_check', true);
xhr.timeout = 15000;
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
if (response && (response.upaiyun || response.pcol || response.ipsb || response.ipify)) {
update_ip_display(response);
} else {
if (!has_ip_cache()) show_querying_state();
}
} catch (e) {
if (!has_ip_cache()) show_querying_state();
}
} else {
if (!has_ip_cache()) show_querying_state();
}
}
};
xhr.ontimeout = function() {
if (!has_ip_cache()) show_querying_state();
};
xhr.onerror = function() {
if (!has_ip_cache()) show_querying_state();
};
xhr.send();
}
function has_ip_cache() {
const ip_ids = ['ip-ipip', 'ip-ipsb', 'ip-upaiyun', 'ip-ipify'];
for (let id of ip_ids) {
let val = document.getElementById(id).innerHTML;
if (
val &&
val !== '<%:Querying...%>' &&
val !== '***.***.***.***'
) {
return true;
}
}
return false;
}
function show_querying_state() {
if (localStorage.getItem('privacy_my_ip') === 'true') {
$$.getElementById('ip-ipip').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipsb').innerHTML = '***.***.***.***';
$$.getElementById('ip-upaiyun').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipify').innerHTML = '***.***.***.***';
} else {
$$.getElementById('ip-ipip').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipsb').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-upaiyun').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipify').innerHTML = '<%:Querying...%>';
}
$$.getElementById('ip-ipip-geo').innerHTML = '';
$$.getElementById('ip-ipsb-geo').innerHTML = '';
$$.getElementById('ip-upaiyun-geo').innerHTML = '';
$$.getElementById('ip-ipify-geo').innerHTML = '';
}
function update_ip_display(data) {
const ipFields = [
{ id: 'ip-upaiyun', geo: 'ip-upaiyun-geo', key: 'upaiyun' },
{ id: 'ip-ipip', geo: 'ip-ipip-geo', key: 'ipip' },
{ id: 'ip-ipsb', geo: 'ip-ipsb-geo', key: 'ipsb' },
{ id: 'ip-ipify', geo: 'ip-ipify-geo', key: 'ipify' }
];
if (localStorage.getItem('privacy_my_ip') === 'true') {
ipFields.forEach(f => {
$$.getElementById(f.id).innerHTML = '***.***.***.***';
});
} else {
ipFields.forEach(f => {
let ipVal = '';
if (data[f.key] && data[f.key].ip) {
ipVal = data[f.key].ip;
} else {
let curVal = $$.getElementById(f.id).innerHTML;
if (curVal && curVal !== '<%:Querying...%>') {
ipVal = curVal;
} else {
ipVal = '<%:Querying...%>';
}
}
$$.getElementById(f.id).innerHTML = ipVal;
let geoVal = '';
if (data[f.key] && data[f.key].geo) {
geoVal = data[f.key].geo;
} else {
let curGeo = $$.getElementById(f.geo).innerHTML;
if (curGeo && curGeo !== '' && ipVal == curVal && curVal !== '<%:Querying...%>') {
geoVal = curGeo;
} else {
geoVal = '';
}
}
$$.getElementById(f.geo).innerHTML = geoVal;
});
}
addTitleOnOverflow();
}
function reset_display() {
$$.getElementById('ip-ipip').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipify').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-upaiyun').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipsb').innerHTML = '<%:Querying...%>';
$$.getElementById('ip-ipip-geo').innerHTML = '';
$$.getElementById('ip-ipify-geo').innerHTML = '';
$$.getElementById('ip-upaiyun-geo').innerHTML = '';
$$.getElementById('ip-ipsb-geo').innerHTML = '';
}
function clearAllIntervals() {
if (refresh_http) {
clearInterval(refresh_http);
refresh_http = null;
}
if (refresh_ip) {
clearInterval(refresh_ip);
refresh_ip = null;
}
}
function refresh_myip(svgElement) {
svgElement.style.transform = 'rotate(360deg)';
setTimeout(function() {
svgElement.style.transform = '';
}, 500);
clearAllIntervals();
if (use_router_mode) {
get_router_ip_info();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("get_router_ip_info()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
} else {
if (localStorage.getItem('privacy_my_ip') === 'true') {
$$.getElementById('ip-ipip-geo').innerHTML = '';
$$.getElementById('ip-ipsb-geo').innerHTML = '';
$$.getElementById('ip-upaiyun-geo').innerHTML = '';
$$.getElementById('ip-ipify-geo').innerHTML = '';
$$.getElementById('ip-ipip').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipsb').innerHTML = '***.***.***.***';
$$.getElementById('ip-upaiyun').innerHTML = '***.***.***.***';
$$.getElementById('ip-ipify').innerHTML = '***.***.***.***';
}
myip_Load();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("myip_Load()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
}
return false;
}
function init_page() {
var saved_mode = localStorage.getItem('myip_check_mode');
if (saved_mode === 'true' || saved_mode === null) {
use_router_mode = true;
update_mode_icon();
init_router_mode();
} else {
use_router_mode = false;
update_mode_icon();
init_browser_mode();
}
if (localStorage.getItem('privacy_my_ip') === 'true') {
update_eye_icon(false);
} else {
update_eye_icon(true);
}
}
window.addEventListener('load', function() {
init_page();
});
window.addEventListener('beforeunload', function() {
clearAllIntervals();
});
</script>
</html>

View File

@ -0,0 +1,112 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function oix_login(btn,option)
{
btn.disabled = true;
if (option == "oix_login") {
var s = document.getElementById(option+'-status');
var e = document.getElementsByName('cbid.openclash.config.oix_email');
var p = document.getElementsByName('cbid.openclash.config.oix_passwd');
var c = document.getElementsByName('cbid.openclash.config.oix_checkin');
if (!e[0].value || !p[0].value) {
btn.disabled = false;
s.innerHTML ="<font color='red'><strong>"+"<%:Error Login Info%>"+"</strong></font>";
return false;
};
if (c[0] && c[0].checked) {
c = "1";
var i = document.getElementsByName('cbid.openclash.config.oix_checkin_interval');
var m = document.getElementsByName('cbid.openclash.config.oix_checkin_multiple');
if (!i[0].value || !(/(^[1-9]\d*$)/.test(i[0].value))) { i = "1"} else {i = i[0].value};
if (!m[0].value || !(/(^[1-9]\d*$)/.test(m[0].value)))
{
btn.disabled = false;
s.innerHTML ="<font color='red'><strong>"+"<%:Multiple Must Be a Positive Integer and No More Than 100%>"+"</strong></font>";
return false;
}
else if (m[0].value < 1)
{
btn.disabled = false;
s.innerHTML ="<font color='red'><strong>"+"<%:Multiple Must Be a Positive Integer and No More Than 100%>"+"</strong></font>";
return false;
}
else if (m[0].value > 100)
{
btn.disabled = false;
s.innerHTML ="<font color='red'><strong>"+"<%:Multiple Must Be a Positive Integer and No More Than 100%>"+"</strong></font>";
return false;
}
else {
m = m[0].value;
};
}
else {
c = "0";
var i = "1";
var m = "1";
};
btn.value = '<%:Login...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "oix_login_info_save")%>', {email: e[0].value, passwd : p[0].value, checkin: c, interval: i, multiple: m}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "oix_login")%>', null, function(x, status) {
if (s)
{
if (x && x.status == 200 && status.result == 200) {
s.innerHTML ="<font color='green'><strong>"+"<%:oixCloud Login Successful%>"+"</strong></font>";
alert("<%:Note: oixCloud need a specific core installed, OpenClash will auto download%>\n\n<%:After the core start, an oixCloud nodes provider will be added, which you can customize as needed%>");
window.location.href='<%="settings?tab.openclash.config=oixcloud"%>';
}
else {
s.innerHTML ="<font color='red'><strong>"+"<%:oixCloud Login Faild%>"+"</strong></font>";
if (status.result) {
alert("<%:oixCloud Login Faild%>: " + status.result)
}
window.location.href='<%="settings?tab.openclash.config=oixcloud"%>';
}
}
btn.disabled = false;
btn.value = '<%:Login Account%>';
});
}
});
};
if (option == "oix_logout") {
var s = document.getElementById('oix_login-status');
btn.value = '<%:Logout...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "oix_logout")%>', null, function(x, status) {
if (s)
{
if (x && x.status == 200 && status.result == 200) {
s.innerHTML ="<font color='green'><strong>"+"<%:oixCloud Logout Successful%>"+"</strong></font>";
window.location.href='<%="settings?tab.openclash.config=oixcloud"%>';
}
else {
s.innerHTML ="<font color='red'><strong>"+"<%:oixCloud Logout Faild%>"+"</strong></font>";
if (status.result) {
alert("<%:oixCloud Logout Faild%>: " + status.result)
}
}
}
btn.disabled = false;
btn.value = '<%:Logout Account%>';
}
);
};
return false;
}
function web_oix(btn)
{
btn.disabled = true;
url='http://bit.ly/oixcloud-register';
window.open(url);
btn.disabled = false;
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Login Account%>" onclick="return oix_login(this,'oix_login')" />
<input type="button" class="btn cbi-button cbi-button-remove" value="<%:Logout Account%>" onclick="return oix_login(this,'oix_logout')" />
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Official Website%>" onclick="return web_oix(this)" />
<span id="<%=self.option%>-status"><%=self.value%></span>
<%+cbi/valuefooter%>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,88 @@
<%+cbi/valueheader%>
<%
local stream_id_name = string.gsub(self.value, "%W", "")
%>
<script type="text/javascript">//<![CDATA[
function act_manual_unlock_test_<%=stream_id_name%>(btn, type)
{
var manual_<%=stream_id_name%>_type_legend = document.getElementById('manual-<%=stream_id_name%>-test-state');
var manual_<%=stream_id_name%>_type_output = document.getElementById('manual-<%=stream_id_name%>-test-output');
var r = confirm("<%:Network instability may occur during testing, Are you sure want to start test?%>")
if (r == true) {
if (manual_<%=stream_id_name%>_type_legend && manual_<%=stream_id_name%>_type_output)
{
manual_<%=stream_id_name%>_type_output.innerHTML =
'<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" /> ' +
'<%:Waiting for command to complete...%>';
manual_<%=stream_id_name%>_type_legend.parentNode.style.display = 'block';
manual_<%=stream_id_name%>_type_legend.style.display = 'inline';
};
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "manual_stream_unlock_test")%>', {type: type}, function(x, status) {
if (x && x.status == 200 && x.responseText != "")
{
manual_<%=stream_id_name%>_type_legend.style.display = 'none';
manual_<%=stream_id_name%>_type_output.innerHTML = '<textarea id="manual_<%=stream_id_name%>_type_output" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="10" cols="50" readonly="readonly" >'+x.responseText+'</textarea>';
log_editor(document.getElementById("manual_<%=stream_id_name%>_type_output"), 'manual_stream_unlock_test', 'true', manual_<%=stream_id_name%>_type_output.offsetWidth, '250px');
}
else
{
manual_<%=stream_id_name%>_type_legend.style.display = 'none';
manual_<%=stream_id_name%>_type_output.innerHTML = '<span class="error"><%:Something Wrong While Testing...%></span>';
}
}
);
};
};
function act_all_proxies_test_<%=stream_id_name%>(btn, type)
{
var all_<%=stream_id_name%>_legend = document.getElementById('all-<%=stream_id_name%>-test-state');
var all_<%=stream_id_name%>_output = document.getElementById('all-<%=stream_id_name%>-test-output');
var r = confirm("<%:Network instability may occur during testing, Are you sure want to start test?%>")
if (r == true) {
if (all_<%=stream_id_name%>_legend && all_<%=stream_id_name%>_output)
{
all_<%=stream_id_name%>_output.innerHTML =
'<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" /> ' +
'<%:Waiting for command to complete...%>';
all_<%=stream_id_name%>_legend.parentNode.style.display = 'block';
all_<%=stream_id_name%>_legend.style.display = 'inline';
};
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "all_proxies_stream_test")%>', {type: type}, function(x, status) {
if (x && x.status == 200 && x.responseText != "")
{
all_<%=stream_id_name%>_legend.style.display = 'none';
all_<%=stream_id_name%>_output.innerHTML = '<textarea id="all_<%=stream_id_name%>_output" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="10" cols="50" readonly="readonly" >'+x.responseText+'</textarea>';
log_editor(document.getElementById("all_<%=stream_id_name%>_output"), 'all_proxies_stream_test', true, all_<%=stream_id_name%>_output.offsetWidth, '250px');
}
else
{
all_<%=stream_id_name%>_legend.style.display = 'none';
all_<%=stream_id_name%>_output.innerHTML = '<span class="error"><%:Something Wrong While Testing...%></span>';
}
}
);
};
};
//]]></script>
<input type="button" class="btn cbi-button cbi-input-reload" value="<%:Unlock Test(Select)%>" onclick="return act_manual_unlock_test_<%=stream_id_name%>(this, '<%=self.value%>')" />
<input type="button" class="btn cbi-button cbi-input-reload" value="<%:All Proxies Test%>" onclick="return act_all_proxies_test_<%=stream_id_name%>(this, '<%=self.value%>')" />
<fieldset style="display: none;margin: 0 auto;">
<legend id="manual-<%=stream_id_name%>-test-state"><%:Collecting data...%></legend>
<br />
<span id="manual-<%=stream_id_name%>-test-output"></span>
</fieldset>
<fieldset style="display: none;margin: 0 auto;">
<legend id="all-<%=stream_id_name%>-test-state"><%:Collecting data...%></legend>
<br />
<span id="all-<%=stream_id_name%>-test-output"></span>
</fieldset>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,357 @@
<body>
<div class="oc">
<div id="selectPopup" class="select-popup hidden">
<div class="select-popup-header">
<div class="config-editor-title">
<%:Check Update%>
</div>
<div class="config-editor-actions">
<button type="button" class="icon-btn close-btn" onclick="closeSelectPopup()" title="<%:Close%>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="select-config-section">
<div class="config-grid">
<div class="config-item">
<div class="config-label"><%:Compiled Version%></div>
<select class="select-class" id="CORE_VERSION_CDN">
<option value="linux-386"><%:linux-386%></option>
<option value="linux-amd64-v1"><%:linux-amd64-v1(x86-64)%></option>
<option value="linux-amd64-v2"><%:linux-amd64-v2(x86-64)%></option>
<option value="linux-amd64-v3"><%:linux-amd64-v3(x86-64)%></option>
<option value="linux-armv5"><%:linux-armv5%></option>
<option value="linux-armv6"><%:linux-armv6%></option>
<option value="linux-armv7"><%:linux-armv7%></option>
<option value="linux-arm64"><%:linux-arm64(armv8)%></option>
<option value="linux-loong64-abi1"><%:linux-loong64-abi1%></option>
<option value="linux-loong64-abi2"><%:linux-loong64-abi2%></option>
<option value="linux-riscv64"><%:linux-riscv64%></option>
<option value="linux-s390x"><%:linux-s390x%></option>
<option value="linux-mips-hardfloat"><%:linux-mips-hardfloat%></option>
<option value="linux-mips-softfloat"><%:linux-mips-softfloat%></option>
<option value="linux-mips64"><%:linux-mips64%></option>
<option value="linux-mips64le"><%:linux-mips64le%></option>
<option value="linux-mipsle-softfloat"><%:linux-mipsle-softfloat%></option>
<option value="linux-mipsle-hardfloat"><%:linux-mipsle-hardfloat%></option>
<option value="0"><%:Not Set%></option>
</select>
</div>
<div class="config-item">
<div class="config-label"><%:Release Branch%></div>
<select class="select-class" id="RELEASE_BRANCH_CDN">
<option value="master">Master</option>
<option value="dev">Developer</option>
</select>
</div>
<div class="config-item">
<div class="config-label">
<%:Smart Core%>
<a href="javascript:void(0);" onclick="window.open('https://github.com/vernesong/mihomo/releases', '_blank');" class="help-link" title="<%:View core infos that support smart group%>">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</a>
</div>
<select class="select-class" id="SMART_ENABLE_CDN">
<option value="0"><%:Disabled%></option>
<option value="1"><%:Enable%></option>
</select>
</div>
</div>
</div>
<div class="select-popup-body">
<div class="select-option" data-value="https://raw.githubusercontent.com/">
<span>https://raw.githubusercontent.com/ (<%:RAW address%>)</span>
</div>
<div class="select-option" data-value="https://fastly.jsdelivr.net/">
<span>https://fastly.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://testingcf.jsdelivr.net/">
<span>https://testingcf.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://cdn.jsdelivr.net/">
<span>https://cdn.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="custom">
<span><%:Custom Your CDN URL%></span>
<span style="display: flex; align-items: center; height: 100%;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="display: block;">
<polygon points="6,9 18,9 12,15"/>
</svg>
</span>
</div>
<input type="text" id="customOptionInput" class="custom-option-input" value="https://ghfast.top/" placeholder="<%:Type CDN URL, Format Like%> https://ghfast.top/" style="display: none;">
<div class="select-option" id="addCustomOption" style="display: none;"><%:Add%></div>
</div>
</div>
</div>
</body>
<script type="text/javascript">
//<![CDATA[
var core_version_cdn = document.getElementById('CORE_VERSION_CDN');
var release_branch_cdn = document.getElementById('RELEASE_BRANCH_CDN');
var smart_enable_cdn = document.getElementById('SMART_ENABLE_CDN');
function initDarkMode() {
var ocContainers = document.querySelectorAll('.oc');
if (!ocContainers.length) return;
var shouldUseDark = isDarkBackground(document.body);
ocContainers.forEach(function(ocContainer) {
if (shouldUseDark) {
ocContainer.setAttribute('data-darkmode', 'true');
document.documentElement.setAttribute('data-darkmode', 'true');
} else {
ocContainer.removeAttribute('data-darkmode');
document.documentElement.removeAttribute('data-darkmode');
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDarkMode);
} else {
initDarkMode();
}
function get_update_info() {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update_info")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.corever && status.corever != "0" && status.corever != "" ) {
core_version_cdn.value = status.corever;
}
else {
core_version_cdn.value = "0";
}
if ( status.release_branch && status.release_branch != "" ) {
release_branch_cdn.value = status.release_branch;
}
else {
release_branch_cdn.value = "master";
}
if ( status.smart_enable && status.smart_enable != "" ) {
smart_enable_cdn.value = status.smart_enable;
}
else {
smart_enable_cdn.value = "0";
}
}
else {
core_version_cdn.value = "0";
release_branch_cdn.value = "master";
smart_enable_cdn.value = "0";
}
});
}
function isValidURL(str) {
var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.&#126;+]*)*'+ // port and path
'(\\?[;&a-z\\d%_.&#126;+=-]*)?'+ // query string
'(\\#[-a-z\\d_]*)?'+'\/$','i'); // fragment locator
return !!pattern.test(str);
}
function HTTP_delay(domain, ID) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/cgi-bin/luci/admin/services/openclash/website_check?domain=' + encodeURIComponent(domain), true);
xhr.timeout = 5000;
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
let status, text, response_time = 0;
if (xhr.status === 200) {
try {
var resp = JSON.parse(xhr.responseText);
response_time = resp.response_time || 0;
if (resp.success) {
if (response_time <= 500) {
status = 'fast';
text = response_time + ' ms';
} else if (response_time > 500 && response_time <= 1000) {
status = 'medium';
text = response_time + ' ms';
} else {
status = 'slow';
text = response_time + ' ms';
}
} else {
status = 'error';
text = resp.error || '<%:Access Denied%>';
}
} catch (e) {
status = 'error';
text = '<%:Access Denied%>';
}
} else {
status = 'error';
text = '<%:Access Timed Out%>';
}
updateCDNStatus(ID, status, text);
}
};
xhr.ontimeout = function() {
updateCDNStatus(ID, 'error', '<%:Access Timed Out%>');
};
xhr.onerror = function() {
updateCDNStatus(ID, 'error', '<%:Access Denied%>');
};
xhr.send();
}
function updateCDNStatus(element, status, text) {
var existingStatus = element.querySelector('.cdn-status');
if (existingStatus) {
existingStatus.remove();
}
var statusSpan = document.createElement('span');
statusSpan.className = 'cdn-status ' + status;
statusSpan.textContent = text;
element.appendChild(statusSpan);
}
function select_git_cdn(btn, type) {
var selectPopup = document.getElementById('selectPopup');
get_update_info();
selectPopup.classList.remove('hidden');
var customOptionInput = document.getElementById('customOptionInput');
var addCustomOption = document.getElementById('addCustomOption');
var selectOptions = document.getElementsByClassName('select-option');
customOptionInput.style.display = 'none';
addCustomOption.style.display = 'none';
selectPopup.addEventListener('click', function(event) {
select_window(event, type);
});
document.addEventListener('click', select_window_close);
refresh_httpdelay = setInterval(() => {
for(var i = 0; i < selectOptions.length; i++) {
if (isValidURL(selectOptions[i].dataset.value)) {
HTTP_delay(selectOptions[i].dataset.value, selectOptions[i]);
}
}
}, Math.floor(Math.random()*(7-3+1)+3)*1000);
}
function select_window_close(event) {
var selectPopup = document.getElementById('selectPopup');
if (!selectPopup.contains(event.target)) {
selectPopup.classList.add('hidden');
if (typeof refresh_httpdelay !== 'undefined') {
clearInterval(refresh_httpdelay);
}
}
}
function select_window(event, type) {
var v = core_version_cdn.value;
var r = release_branch_cdn.value;
var s = smart_enable_cdn.value;
var selectOptions = document.getElementsByClassName('select-option');
var target = event.target;
var selectPopup = document.getElementById('selectPopup');
var customOptionInput = document.getElementById('customOptionInput');
var addCustomOption = document.getElementById('addCustomOption');
var optionElem = target.closest ? target.closest('.select-option') : null;
if (optionElem && optionElem.dataset.value === 'custom' && customOptionInput.style.display === 'block') {
customOptionInput.style.display = 'none';
addCustomOption.style.display = 'none';
} else if (optionElem && optionElem.dataset.value === 'custom') {
customOptionInput.style.display = 'block';
addCustomOption.style.display = 'block';
customOptionInput.focus();
} else if (target === customOptionInput) {
customOptionInput.style.display = 'block';
addCustomOption.style.display = 'block';
customOptionInput.focus();
} else if (target === addCustomOption) {
var newValue = customOptionInput.value.trim();
if (newValue !== '' && isValidURL(newValue)) {
var newOption = document.createElement('div');
newOption.classList.add('select-option');
newOption.innerHTML = '<span>' + newValue + '</span>';
newOption.dataset.value = newValue;
selectPopup.querySelector('.select-popup-body').insertBefore(newOption, selectPopup.querySelector('.select-popup-body').children[selectOptions.length - 2]);
customOptionInput.style.display = 'none';
addCustomOption.style.display = 'none';
}
else {
alert("<%:Please enter a valid URL!%>");
}
} else if (optionElem && optionElem != addCustomOption && optionElem != customOptionInput) {
customOptionInput.style.display = 'none';
addCustomOption.style.display = 'none';
if (typeof LogManager !== 'undefined' && LogManager) {
LogManager.startLogDisplay('<%:Checking...%>');
}
if (optionElem.dataset.value === 'https://raw.githubusercontent.com/') {
optionElem.dataset.value = '';
}
if (type === 'core_download') {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r, smart_enable: s}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "core_download")%>', {url: optionElem.dataset.value}, function(x, status) {
});
}
});
}
else {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r, smart_enable: s}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "one_key_update")%>', {url: optionElem.dataset.value}, function(x, status) {
});
}
});
}
selectPopup.classList.add('hidden');
if (typeof refresh_httpdelay !== 'undefined') {
clearInterval(refresh_httpdelay);
}
}
else if (target != addCustomOption && target != customOptionInput) {
customOptionInput.style.display = 'none';
addCustomOption.style.display = 'none';
}
else {
if (target != customOptionInput) {
selectPopup.classList.add('hidden');
if (typeof refresh_httpdelay !== 'undefined') {
clearInterval(refresh_httpdelay);
}
}
}
}
function closeSelectPopup() {
var selectPopup = document.getElementById('selectPopup');
selectPopup.classList.add('hidden');
if (typeof refresh_httpdelay !== 'undefined') {
clearInterval(refresh_httpdelay);
}
}
//]]>
</script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,420 @@
<%+cbi/valueheader%>
<%
local fs = require "luci.openclash"
local val = self:cfgvalue(section)
local filename = fs.filename(val)
local idname = math.random(1000)..(string.match(filename, "[%w_]+") or "")
%>
<div class="sub_div">
<span id='<%=idname%>' class="sub_tab"></span>
<button type="button" class="sub_setting" title='<%:Refresh%>' alt='<%:Refresh%>' onclick='return sub_info_refresh_<%=idname%>(true)'>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6"></path>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
<button type="button" class="sub_setting" title='<%:Specify URL%>' alt='<%:Specify URL%>' onclick='return set_subinfo_url_<%=idname%>(this,"<%=filename%>")'>
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="4">
<path d="M4 6H44V36H29L24 41L19 36H4V6Z"/>
<path d="M23 21H25.0025" />
<path d="M33.001 21H34.9999"/>
<path d="M13.001 21H14.9999"/>
</svg>
</button>
</div>
<script type="text/javascript">//<![CDATA[
var retry_<%=idname%> = 0;
var s_<%=idname%>;
sub_info_refresh_<%=idname%>(false);
setTimeout(function(){
initDarkMode();
}, 200);
function initDarkMode() {
if (typeof isDarkBackground === 'undefined') {
setTimeout(function() {
initDarkMode();
}, 100);
return;
}
if (isDarkBackground(document.body)) {
document.documentElement.setAttribute('data-darkmode', 'true');
}
}
function progressbar_<%=idname%>(v, m, pc, np, f, t, tr) {
var screenWidth = window.innerWidth || document.documentElement.clientWidth;
var fontSize = '12px';
var minWidth = '220px';
var topPosition = tr == "null" ? '6px' : '2px';
if (screenWidth <= 320) {
fontSize = '8px';
minWidth = '160px';
topPosition = tr == "null" ? '8px' : '4px';
} else if (screenWidth <= 480) {
fontSize = '9px';
minWidth = '180px';
topPosition = tr == "null" ? '7px' : '3px';
} else if (screenWidth <= 768) {
fontSize = '10px';
minWidth = '200px';
topPosition = tr == "null" ? '6px' : '3px';
} else if (screenWidth <= 1024) {
fontSize = '11px';
minWidth = '220px';
topPosition = tr == "null" ? '6px' : '2px';
}
// Build title for native tooltip (format: "v / m (pc%)\nt (Remaining tr days)")
var titleText = v + ' / ' + m + ' (' + pc + '%%)';
if (t && t !== 'null' && tr && tr !== 'null' && parseInt(tr) > 0) {
titleText += '\n' + t + ' (<%:Remaining%> ' + tr + ' <%:days%>)';
}
return String.format(
'<div class="progress_bar_bg" title="' + titleText + '" style="position:relative; border-radius: 6px; min-width: %s; display: flex; align-items: center;">' +
(pc >= 50 ? '<div class="progress_bar_high" style="width:%d%%; height:34px; border-radius: 6px">' :
(pc < 50 && pc >= 20 ? '<div class="progress_bar_medium" style="width:%d%%; height:34px; border-radius: 6px">' :
'<div class="progress_bar_low" style="width:%d%%; height:34px; border-radius: 6px">')) +
'<div style="position:absolute; left:0; top:50%%; transform: translateY(-50%%); text-align:center; width:100%%; padding:0 4px; box-sizing:border-box; line-height:1.2;">' +
'<small class="text_show" style="font-size:%s; line-height:1.2;">%s '+ (f ? f : '/') +' %s ' + (np ? "" : '(%s%%)') +
(tr == "null" ? '<div style="visibility: hidden; line-height:1.2;">' : '<div style="visibility: visible; line-height:1.2;">') +
'%s (<%:Remaining%> %s <%:days%>)</small>' +
'</div>' +
'</div>' +
'</div>',
minWidth, pc, fontSize, v, m, pc, t, tr
);
};
function progressbar_multi_<%=idname%>(providers) {
var providerCount = providers.length;
var itemWidth;
var containerClass = 'sub_providers_container';
if (providerCount === 2) {
itemWidth = 'calc(50% - 2px)';
containerClass += ' no-scroll';
} else if (providerCount === 3) {
itemWidth = 'calc(33.33% - 2.67px)';
containerClass += ' no-scroll';
} else {
itemWidth = '100px';
}
var container = '<div class="' + containerClass + '" id="sub_providers_<%=idname%>">';
providers.forEach(function(provider) {
var percent = parseFloat(provider.percent) || 0;
var providerName = provider.provider_name || 'Unknown';
var used = provider.used || '0 B';
var total = provider.total || 'N/A';
var surplus = provider.surplus || used;
var expire = provider.expire || 'null';
var dayLeft = parseInt(provider.day_left) || 0;
// Determine background color based on percentage
var bgColorClass = percent >= 50 ? 'progress_bar_high' :
(percent >= 20 ? 'progress_bar_medium' : 'progress_bar_low');
// Build title for native tooltip (format: "surplus / total (percent%)\nexpire (Remaining dayLeft days)")
var titleText = surplus + ' / ' + total + ' (' + percent + '%)';
if (expire !== 'null' && dayLeft > 0) {
titleText += '\n' + expire + ' (<%:Remaining%> ' + dayLeft + ' <%:days%>)';
}
container += '<div class="sub_provider_item" style="width: ' + itemWidth + '" title="' + titleText + '">' +
'<div class="sub_provider_fill ' + bgColorClass + '" style="width: ' + percent + '%"></div>' +
'<span class="sub_provider_name">' + providerName + '</span>' +
'<span class="sub_provider_percent">' + ' (' + percent + '%)' + '</span>' +
'</div>';
});
container += '</div>';
if (providerCount > 3) {
setTimeout(function() {
initDragScroll_<%=idname%>();
}, 100);
}
return container;
};
function initDragScroll_<%=idname%>() {
var container = document.getElementById('sub_providers_<%=idname%>');
if (!container) return;
var isDragging = false;
var startX;
var scrollLeft;
var hasMoved = false;
container.addEventListener('mousedown', function(e) {
if (e.target.tagName === 'A' || e.target.tagName === 'BUTTON') {
return;
}
isDragging = true;
hasMoved = false;
startX = e.pageX - container.offsetLeft;
scrollLeft = container.scrollLeft;
container.classList.add('dragging');
});
container.addEventListener('mousemove', function(e) {
if (!isDragging) return;
var x = e.pageX - container.offsetLeft;
var walk = (x - startX) * 2;
if (Math.abs(walk) > 5) {
hasMoved = true;
e.preventDefault();
container.scrollLeft = scrollLeft - walk;
}
});
container.addEventListener('mouseup', function() {
isDragging = false;
container.classList.remove('dragging');
});
container.addEventListener('mouseleave', function() {
isDragging = false;
container.classList.remove('dragging');
});
container.addEventListener('dragstart', function(e) {
e.preventDefault();
});
};
function dispaly_progressbar(id, status) {
document.getElementById(id).className = "sub_tab_show";
if (status && status.providers && status.providers.length > 0) {
if (status.providers.length === 1) {
var provider = status.providers[0];
document.getElementById(id).innerHTML = window["progressbar_" + id](
(provider.surplus ? provider.surplus : provider.used),
(provider.total),
(provider.percent),
false, false,
(provider.expire),
(provider.day_left)
);
} else {
document.getElementById(id).innerHTML = window["progressbar_multi_" + id](status.providers);
}
} else {
document.getElementById(id).innerHTML = "<span><%:No Sub Info Found%></span>";
}
};
function sub_info_refresh_<%=idname%>(force) {
if (s_<%=idname%>) { clearTimeout(s_<%=idname%>);};
if (force) {
localStorage.removeItem("sub_info_<%=filename%>");
}
var save_info = (localStorage.getItem("sub_info_<%=filename%>")) ? JSON.parse(localStorage.getItem("sub_info_<%=filename%>")) : null;
var shouldFetchNew = true;
if (!force) {
dispaly_progressbar('<%=idname%>', save_info);
if (save_info && save_info.get_time) {
var currentTime = Math.floor(Date.now() / 1000);
var cacheTime = parseInt(save_info.get_time);
var timeDiff = currentTime - cacheTime;
var halfHourInSeconds = 30 * 60;
if (timeDiff <= halfHourInSeconds) {
shouldFetchNew = false;
}
}
}
if (!shouldFetchNew) {
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 60000*15);
return;
}
if (!save_info) {
document.getElementById('<%=idname%>').className = "sub_tab_show";
document.getElementById('<%=idname%>').innerHTML = "<span><%:Collecting data...%></span>";
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "sub_info_get")%>', {filename: "<%=filename%>"}, function(x, status) {
var needsErrorHandling = false;
if (x && x.status == 200 && status.providers.length > 0) {
var save_info_length = (save_info && save_info.providers) ? save_info.providers.length : 0;
var newProvidersLength = status.providers.length;
if (save_info_length !== newProvidersLength && save_info_length > newProvidersLength) {
needsErrorHandling = true;
}
if (!needsErrorHandling) {
retry_<%=idname%> = 0;
localStorage.setItem("sub_info_<%=filename%>", JSON.stringify(status));
dispaly_progressbar('<%=idname%>', status);
}
} else {
needsErrorHandling = true;
};
if (needsErrorHandling && retry_<%=idname%> <= 2) {
retry_<%=idname%>++;
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 5000);
} else {
retry_<%=idname%> = 0;
dispaly_progressbar('<%=idname%>', status);
if (status && status.providers) {
localStorage.setItem("sub_info_<%=filename%>", JSON.stringify(status));
}
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 60000*15);
};
});
};
function set_subinfo_url_<%=idname%>(btn, filename) {
if (filename == null || filename == "") {
return false;
}
var dialogDiv = document.createElement('div');
dialogDiv.className = 'config-upload-model-overlay show';
var contentDiv = document.createElement('div');
contentDiv.className = 'config-upload-model';
var headerDiv = document.createElement('div');
headerDiv.className = 'config-upload-header';
var titleDiv = document.createElement('div');
titleDiv.className = 'config-upload-title';
titleDiv.innerHTML = '<%:Set Subscription Info URL%>';
headerDiv.appendChild(titleDiv);
var closeBtn = document.createElement('button');
closeBtn.className = 'icon-btn';
closeBtn.type = 'button';
closeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
closeBtn.onclick = function() {
document.body.removeChild(portal);
};
headerDiv.appendChild(closeBtn);
var bodyDiv = document.createElement('div');
bodyDiv.className = 'config-upload-content';
var formGroup = document.createElement('div');
formGroup.className = 'form-group';
var label = document.createElement('label');
label.className = 'highlight-label';
label.innerHTML = '<%:Paste the new url of subscribe infos sources here:%>';
formGroup.appendChild(label);
var textarea = document.createElement('textarea');
textarea.className = 'form-textarea';
textarea.placeholder = '<%:Enter URLs, one per line, use #name=example end for specifing display name%> (https://example.com#name=example)';
formGroup.appendChild(textarea);
bodyDiv.appendChild(formGroup);
var footerDiv = document.createElement('div');
footerDiv.className = 'config-upload-footer';
var statusDiv = document.createElement('div');
statusDiv.className = 'config-upload-status';
footerDiv.appendChild(statusDiv);
var buttonsDiv = document.createElement('div');
buttonsDiv.className = 'config-upload-buttons';
var cancelBtn = document.createElement('button');
cancelBtn.className = 'btn cancel-btn';
cancelBtn.type = 'button';
cancelBtn.innerHTML = '<%:Cancel%>';
var confirmBtn = document.createElement('button');
confirmBtn.className = 'btn upload-btn';
confirmBtn.type = 'button';
confirmBtn.innerHTML = '<%:Submit%>';
confirmBtn.onclick = function() {
var new_url = textarea.value.trim();
document.body.removeChild(portal);
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "set_subinfo_url")%>', {filename: filename, url: new_url}, function(x, status) {
if (x && x.status == 200 && status.info == "Success") {
sub_info_refresh_<%=idname%>(true);
alert("<%:Specify subscribe infos sources url successfully!%>");
} else if (x && x.status == 200 && status.info == "Delete success") {
sub_info_refresh_<%=idname%>(true);
alert("<%:Delete Specify Subscribe infos sources url successfully!%>");
} else {
alert("<%:Specify subscribe infos sources url failed:%>\n" + status.info);
}
});
};
cancelBtn.onclick = function() {
document.body.removeChild(portal);
};
buttonsDiv.appendChild(cancelBtn);
buttonsDiv.appendChild(confirmBtn);
footerDiv.appendChild(buttonsDiv);
contentDiv.appendChild(headerDiv);
contentDiv.appendChild(bodyDiv);
contentDiv.appendChild(footerDiv);
dialogDiv.appendChild(contentDiv);
var portal = document.createElement('div');
portal.className = 'oc';
portal.appendChild(dialogDiv);
document.body.appendChild(portal);
textarea.focus();
var handleSubURL = function(urlresult) {
var urls = '';
if (urlresult.type && urlresult.type === "multiple") {
for (var data in urlresult.providers) {
urls += urlresult.providers[data].url + '#name=' + urlresult.providers[data].name + '\n';
}
}
if (urlresult.type && urlresult.type === "single") {
urls = urlresult.url;
}
textarea.value = urls;
};
var existingData = localStorage.getItem('sub_info_' + filename);
if (existingData) {
existingData = JSON.parse(existingData);
if (existingData.url_result) {
handleSubURL(existingData.url_result);
return;
}
};
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "get_subscribe_info_data")%>', {
filename: filename
}, function(x, status) {
if (x && x.status == 200 && status) {
handleSubURL(status);
}
});
};
//]]></script>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,110 @@
<%+cbi/valueheader%>
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px;">
<div class="cbi-value-field" id="switch_dashboard_<%=self.option%>"><%:Collecting data...%></div>
<div class="cbi-value-field" id="delete_dashboard_<%=self.option%>">
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Delete%>" onclick="return delete_dashboard(this, '<%=self.option%>')"/>
</div>
<div class="cbi-value-field" id="default_dashboard_<%=self.option%>">
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Set to Default%>" onclick="return default_dashboard(this, '<%=self.option%>')"/>
</div>
</div>
<script type="text/javascript">//<![CDATA[
(function() {
var name = "<%=self.option%>";
function makeBtn(label, fn, args) {
var argStr = args.map(function(a){ return "'" + a + "'"; }).join(', ');
return '<input type="button" class="btn cbi-button cbi-button-apply" value="' + label + '" onclick="return ' + fn + '(this, ' + argStr + ')"/>';
}
var switchEl = document.getElementById('switch_dashboard_' + name);
var defaultEl = document.getElementById('default_dashboard_' + name);
var deleteEl = document.getElementById('delete_dashboard_' + name);
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "dashboard_type")%>', null, function(x, status) {
if (!x || x.status != 200) return;
var switchLabel;
if (name == "Metacubexd") {
switchLabel = ['<%:Update Metacubexd Version%>', 'Official'];
} else if (name == "Zashboard") {
switchLabel = ['<%:Update Zashboard Version%>', 'Official'];
} else {
var isMeta = (name == "Dashboard" ? status.dashboard_type : status.yacd_type) == "Meta";
switchLabel = isMeta
? ['<%:Switch To Official Version%>', 'Official']
: ['<%:Switch To Meta Version%>', 'Meta'];
}
switchEl.innerHTML = makeBtn(switchLabel[0], 'switch_dashboard', [name, switchLabel[1]]);
if (status.default_dashboard == name.toLowerCase()) {
defaultEl.innerHTML = makeBtn('<%:Default%>', 'default_dashboard', [name]);
defaultEl.firstElementChild.disabled = true;
}
if (!status[name.toLowerCase()]) {
defaultEl.firstElementChild.disabled = true;
deleteEl.firstElementChild.disabled = true;
}
});
window.switch_dashboard = window.switch_dashboard || function(btn, n, type) {
btn.disabled = true;
btn.value = '<%:Downloading File...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_dashboard")%>', {name: n, type: type}, function(x, status) {
if (!x || x.status != 200) return;
if (status.download_state == "0") {
var isUpdate = (n == "Metacubexd" || n == "Zashboard");
var nextLabel, nextType;
if (isUpdate) {
nextLabel = '<%:Update Successful%> - ' + (n == "Metacubexd" ? '<%:Update Metacubexd Version%>' : '<%:Update Zashboard Version%>');
nextType = 'Official';
} else {
nextLabel = '<%:Switch Successful%> - ' + (type == "Meta" ? '<%:Switch To Official Version%>' : '<%:Switch To Meta Version%>');
nextType = type == "Meta" ? 'Official' : 'Meta';
}
document.getElementById('switch_dashboard_' + n).innerHTML = makeBtn(nextLabel, 'switch_dashboard', [n, nextType]);
document.getElementById('default_dashboard_' + n).firstElementChild.disabled = false;
document.getElementById('delete_dashboard_' + n).firstElementChild.disabled = false;
} else {
btn.value = status.download_state == "2" ? '<%:Unzip Error%>'
: (n == "Metacubexd" || n == "Zashboard") ? '<%:Update Failed%>' : '<%:Switch Failed%>';
}
btn.disabled = false;
});
return false;
};
window.delete_dashboard = window.delete_dashboard || function(btn, n) {
if (!confirm("<%:Are you sure you want to delete this panel?%>")) return false;
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "delete_dashboard")%>', {name: n}, function(x, status) {
if (!x || x.status != 200) { btn.disabled = false; return; }
if (status.delete_state == "1") {
var defBtn = document.getElementById('default_dashboard_' + n).firstElementChild;
if (defBtn.disabled) defBtn.value = '<%:Set to Default%>';
defBtn.disabled = true;
} else btn.disabled = false;
});
return false;
};
window.default_dashboard = window.default_dashboard || function(btn, n) {
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "default_dashboard")%>', {name: n}, function(x, status) {
if (!x || x.status != 200) { btn.disabled = false; return; }
btn.value = '<%:Default%>';
document.querySelectorAll('[id^="default_dashboard_"]').forEach(function(el) {
var b = el.firstElementChild;
if (b && b !== btn && b.value === '<%:Default%>') {
b.disabled = false;
b.value = '<%:Set to Default%>';
}
});
});
return false;
};
})();
//]]></script>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,45 @@
<fieldset class="cbi-section">
<table width="100%">
<tr><td width="100%" colspan="4">
<p align="center" id="switch_mode">
<%:Collecting data...%>
</p>
</td></tr>
</table>
</fieldset>
<script type="text/javascript">//<![CDATA[
var switch_mode = document.getElementById('switch_mode');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "op_mode")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.op_mode == "redir-host" ) {
switch_mode.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch page to Fake-IP mode%>" onclick="return switch_modes(this)"/>';
}
else {
switch_mode.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch page to Redir-Host mode%>" onclick="return switch_modes(this)"/>';
}
}
});
function switch_modes(btn)
{
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_mode")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.switch_mode == "redir-host" ) {
alert('<%:Page has been switched to Fake-IP mode!%>')
window.location.href='<%="settings"%>';
}
else {
alert('<%:Page has been switched to Redir-Host mode!%>')
window.location.href='<%="settings"%>';
}
}
});
btn.disabled = false;
return false;
}
//]]></script>

View File

@ -0,0 +1,537 @@
<%-
local rowcnt = 1
local uci = require("luci.model.uci").cursor()
local fs = require "luci.openclash"
function rowstyle()
rowcnt = rowcnt + 1
return (rowcnt % 2) + 1
end
function width(o)
if o.width then
if type(o.width) == 'number' then
return ' style="width:%dpx"' % o.width
end
return ' style="width:%s"' % o.width
end
return ''
end
function table_include(table, value)
if table == nil then
return false
end
for k, v in pairs(table) do
if v == value then
return true
end
end
return false
end
local a={}
local e={}
if self.sectiontype == "dns_servers" then
table.insert(e, {name="nameserver", title="nameserver"})
table.insert(e, {name="fallback", title="fallback"})
table.insert(e, {name="default", title="default-nameserver"})
else
for t,o in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(o)
if a then
e[t]={}
e[t].name=fs.basename(o)
end
end
if not table_include(e, "all") then
table.insert(e, 1, {name="all"})
end
end
local file_path = fs.get_file_path_from_request()
local cfg_name = (file_path and fs.basename(file_path)) or nil
if not cfg_name and uci:get("openclash", "config", "config_path") then
cfg_name = fs.basename(uci:get("openclash", "config", "config_path"))
end
local sectiontype = "_"..self.config.."_"..string.match(self.sectiontype, "[%w_]+") or "self.sectiontype"
-%>
<style type="text/css">
*{margin: 0;padding: 0;}
ul{
list-style: none;
}
h3{
margin-top: 0;
}
.cbi-value-field .cbi-dropdown, .cbi-value-field .cbi-input-select, .cbi-value input[type="text"], .cbi-value input[type="password"] {
min-width: auto;
}
#tab-header-<%=self.config%>-<%=self.sectiontype%>{
display: flex;
align-items: center;
justify-content: flex-start;
padding: 16px 20px;
border-bottom: 1px solid var(--border-light, #e2e8f0);
background: var(--bg-gray, #f1f5f9);
min-width: 0;
box-shadow: none;
position: relative;
z-index: 1;
}
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul{
display: flex;
width: 100%;
background: #f1f5f9;
border-radius: 8px;
border-bottom: none !important;
border: none !important;
gap: 5px;
margin: 0;
padding: 0;
}
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li{
flex: 1 1 0;
min-width: 120px;
max-width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 0;
height: 100%;
font-size: 13px;
font-weight: 400;
color: #64748b;
background: transparent;
border: 1px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.18s;
margin: 0;
user-select: none;
position: relative;
box-sizing: border-box;
}
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li.cbi-tab{
background: #3b82f6;
color: #fff;
font-weight: 600;
z-index: 2;
border-radius: 6px;
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.03);
}
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li a{
display: block;
width: 100%;
height: 100%;
text-align: center;
text-decoration: none;
background: none;
border: none;
outline: none;
font: inherit;
color: inherit;
cursor: inherit;
padding: 5px;
margin: 0;
user-select: none;
transition: color 0.18s;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li.cbi-tab > a{
color: #fff !important;
font-weight: 500;
}
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li.cbi-tab-disabled{
color: #fff;
background: #f9f9f9;
opacity: 0.7;
cursor: pointer;
}
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li.cbi-tab-disabled a{
color: #000 !important;
}
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li:not(.cbi-tab):hover,
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li:not(.cbi-tab):focus{
color: #3b82f6;
background: rgba(59,130,246,0.10);
}
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li:not(.cbi-tab):hover a,
#tab-header-<%=self.config%>-<%=self.sectiontype%> ul li:not(.cbi-tab):focus a{
color: #3b82f6 !important;
}
#tab-content .dom-<%=self.config%>-<%=self.sectiontype%>{
display: none;
}
#tab-content .dom-<%=self.config%>-<%=self.sectiontype%> ul li{
float: left;
margin: 15px 10px;
width: 225px;
}
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%>{
background: #374151;
border-bottom: 1px solid #374151;
}
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul{
background: #374151;
}
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul li{
background: #1f2937;
color: #a1a1aa;
border-color: #374151;
}
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul li.cbi-tab{
background: #2563eb;
color: #fff;
}
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul li.cbi-tab a{
color: #fff !important;
}
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul li.cbi-tab-disabled{
color: #1f2937;
background: #1f2937;
opacity: 0.7;
}
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul li.cbi-tab-disabled a{
color: #fff !important;
}
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul li:not(.cbi-tab):hover,
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul li:not(.cbi-tab):focus{
color: #3b82f6;
border-color: #3b82f6;
background: #22304a;
}
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul li:not(.cbi-tab):hover a,
:root[data-darkmode="true"] #tab-header-<%=self.config%>-<%=self.sectiontype%> ul li:not(.cbi-tab):focus a{
color: #60a5fa !important;
}
</style>
<!-- tblsection -->
<fieldset class="cbi-section" id="cbi-<%=self.config%>-<%=self.sectiontype%>">
<%- if self.title and #self.title > 0 then -%>
<h3><%=self.title%></h3>
<%- end -%>
<%- if self.sortable then -%>
<input type="hidden" id="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" name="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" value="" />
<%- end -%>
<div class="cbi-section-descr"><%=self.description%></div>
<%- local count = 0 -%>
<div id="tab-header-<%=self.config%>-<%=self.sectiontype%>" class="cbi-tabmenu">
<ul class="cbi-tabmenu">
<%-
if self.sectiontype == "dns_servers" then
for i, tab in ipairs(e) do
if i == 1 then
-%>
<li name="tab-header-<%=self.config%>-<%=self.sectiontype%>" class="cbi-tab" data-group="<%=tab.name%>"><a title="<%=tab.name%>"><%=tab.title%></a></li>
<%- else -%>
<li name="tab-header-<%=self.config%>-<%=self.sectiontype%>" class="cbi-tab-disabled" data-group="<%=tab.name%>"><a title="<%=tab.name%>"><%=tab.title%></a></li>
<%- end -%>
<%- end -%>
<%- elseif #e ~= 1 then
for i, tab in ipairs(e) do
if tab.name == cfg_name and cfg_name then
-%>
<li name="tab-header-<%=self.config%>-<%=self.sectiontype%>" class="cbi-tab" data-group="<%=tab.name%>"><a title="<%=tab.name%>"><%=tab.name%></a></li>
<%- elseif not cfg_name and tab.name == "all" then -%>
<li name="tab-header-<%=self.config%>-<%=self.sectiontype%>" class="cbi-tab" data-group="<%=tab.name%>"><a title="All">ALL</a></li>
<%- elseif tab.name == "all" then -%>
<li name="tab-header-<%=self.config%>-<%=self.sectiontype%>" class="cbi-tab-disabled" data-group="<%=tab.name%>"><a title="All">ALL</a></li>
<%- else -%>
<li name="tab-header-<%=self.config%>-<%=self.sectiontype%>" class="cbi-tab-disabled" data-group="<%=tab.name%>"><a title="<%=tab.name%>"><%=tab.name%></a></li>
<%- end -%>
<%- end -%>
<%- else -%>
<li name="tab-header-<%=self.config%>-<%=self.sectiontype%>" class="cbi-tab" data-group="all"><a title="All">ALL</a></li>
<%- end -%>
</ul>
</div>
<div id="tab-content">
<%- for i, tab in ipairs(e) do
if self.sectiontype == "dns_servers" then
if tab.name == "nameserver" then
-%>
<div class="dom-<%=self.config%>-<%=self.sectiontype%>" style="display: block;" data-group="<%=tab.name%>">
<%- else -%>
<div class="dom-<%=self.config%>-<%=self.sectiontype%>" data-group="<%=tab.name%>">
<%- end -%>
<%- elseif #e ~= 1 and tab.name == cfg_name and cfg_name then -%>
<div class="dom-<%=self.config%>-<%=self.sectiontype%>" style="display: block;" data-group="<%=tab.name%>">
<%- elseif #e ~= 1 and tab.name == "all" and not cfg_name then -%>
<div class="dom-<%=self.config%>-<%=self.sectiontype%>" style="display: block;" data-group="<%=tab.name%>">
<%- elseif #e == 1 then -%>
<div class="dom-<%=self.config%>-<%=self.sectiontype%>" style="display: block;" data-group="all">
<%- else -%>
<div class="dom-<%=self.config%>-<%=self.sectiontype%>" data-group="<%=tab.name%>">
<%- end -%>
<table class="table cbi-section-table">
<tr class="tr cbi-section-table-titles">
<%- if not self.anonymous then -%>
<%- if self.sectionhead then -%>
<th class="th cbi-section-table-cell"><%=self.sectionhead%></th>
<%- else -%>
<th>&#160;</th>
<%- end -%>
<%- end -%>
<%- for i, k in pairs(self.children) do if not k.optional then -%>
<%- if i == 1 then -%>
<th class="th cbi-section-table-cell"><%:Serial Number%></th>
<%- end -%>
<th class="th cbi-section-table-cell"<%=width(k)%>>
<%- if k.titleref then -%><a title="<%=self.titledesc or translate('Go to relevant configuration page')%>" class="cbi-title-ref" href="<%=k.titleref%>"><%- end -%>
<%-=k.title-%>
<%- if k.titleref then -%></a><%- end -%>
</th>
<%- count = count + 1; end; end; if self.sortable then -%>
<th class="th cbi-section-table-cell"><%:Sort%></th>
<%- end; if self.extedit or self.addremove then -%>
<th class="th cbi-section-table-cell"><%:Edit%></th>
<%- count = count + 1; end -%>
</tr>
<tr class="tr cbi-section-table-descr">
<%- if not self.anonymous then -%>
<%- if self.sectiondesc then -%>
<th class="th cbi-section-table-cell"><%=self.sectiondesc%></th>
<%- else -%>
<th class="th cbi-section-table-cell"></th>
<%- end -%>
<%- end -%>
<%- for i, k in pairs(self.children) do if not k.optional then -%>
<%- if i == 1 then -%>
<th class="th cbi-section-table-cell"></th>
<%- end -%>
<th class="th cbi-section-table-cell"<%=width(k)%>><%=k.description%></th>
<%- end; end; if self.sortable then -%>
<th class="th cbi-section-table-cell"></th>
<%- end; if self.extedit or self.addremove then -%>
<th class="th cbi-section-table-cell"></th>
<%- end -%>
</tr>
<%- local isempty = true
local num = 1
for i, k in ipairs(self:cfgsections()) do
local section = k
local should_render = false
local section_name
if self.sectiontype == "dns_servers" then
section_name = uci:get("openclash", section, "group")
else
section_name = uci:get("openclash", section, "config")
end
if section_name == tab.name then
should_render = true
end
if should_render then
isempty = false
scope = { valueheader = "cbi/cell_valueheader", valuefooter = "cbi/cell_valuefooter" }
-%>
<tr class="tr cbi-section-table-row<% if self.extedit or self.rowcolors then %> cbi-rowstyle-<%=rowstyle()%><% end %>" id="cbi-<%=self.config%>-<%=section%>">
<%- if not self.anonymous then -%>
<th><h3><%=(type(self.sectiontitle) == "function") and self:sectiontitle(section) or k%></h3></th>
<%- end -%>
<td class="td cbi-section-table-cell">
<p><%=num%></p>
<%- num = num + 1 -%>
</td>
<%-
for k, node in ipairs(self.children) do
if not node.optional then
node:render(section, scope or {})
end
end
-%>
<%- if self.sortable then -%>
<td class="tdcbi-section-table-cell">
<input class="cbi-button cbi-button-up" type="button" value="<%:∧%>" onclick="return cbi_row_swap(this, true, 'cbi.sts.<%=self.config%>.<%=self.sectiontype%>')" alt="<%:Move up%>" title="<%:Move up%>" />
<input class="cbi-button cbi-button-down" type="button" value="<%:∨%>" onclick="return cbi_row_swap(this, false, 'cbi.sts.<%=self.config%>.<%=self.sectiontype%>')" alt="<%:Move down%>" title="<%:Move down%>" />
</td>
<%- end -%>
<%- if self.extedit or self.addremove then -%>
<td class="td cbi-section-table-cell">
<%- if self.extedit then -%>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>"
<%- if type(self.extedit) == "string" then
%> onclick="location.href='<%=self.extedit:format(section)%>';return switch_to_tab<%=sectiontype%>()"
<%- elseif type(self.extedit) == "function" then
%> onclick="location.href='<%=self:extedit(section)%>';return switch_to_tab<%=sectiontype%>()"
<%- end
%> alt="<%:Edit%>" title="<%:Edit%>" />
<%- end; if self.addremove then -%>
<%- if file_path then -%>
<input class="btn cbi-button cbi-button-remove" type="submit" value="<%:Delete%>"
onclick="this.form.cbi_state='del-section'; var redirectUrl = 'servers?file=<%=file_path%>'; var originalSubmit = this.form.onsubmit; this.form.onsubmit = function(e) { var result = true; if (originalSubmit) { result = originalSubmit.call(this, e); } if (result !== false) { setTimeout(function() { window.location.href = redirectUrl; }, 200); } return result; }; return switch_to_tab<%=sectiontype%>()"
name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" />
<%- else -%>
<input class="btn cbi-button cbi-button-remove" type="submit" value="<%:Delete%>" onclick="this.form.cbi_state='del-section'; return switch_to_tab<%=sectiontype%>()" name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" />
<%- end -%>
<%- end -%>
</td>
<%- end -%>
<%-
end
-%>
</tr>
<%- end -%>
<%- if isempty then -%>
<tr class="tr cbi-section-table-row placeholder">
<td class="td" colspan="<%=count%>"><em><%:This section contains no values yet%></em></td>
</tr>
<%- end -%>
</table>
<%- if self.error then -%>
<div class="cbi-section-error">
<ul><%- for _, c in pairs(self.error) do for _, e in ipairs(c) do -%>
<li><%=pcdata(e):gsub("\n","<br />")%></li>
<%- end end -%></ul>
</div>
<%- end -%>
<%- if self.addremove then -%>
<%- if self.template_addremove then include(self.template_addremove) else -%>
<div class="cbi-section-create cbi-tblsection-create">
<%- if self.anonymous then -%>
<input class="btn cbi-button cbi-button-add" type="submit" onclick="return switch_to_tab<%=sectiontype%>('<%=tab.name%>')" value="<%:Add%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" title="<%:Add%>" />
<%- else -%>
<% if self.invalid_cts then -%><div class="cbi-section-error"><% end %>
<input type="text" class="cbi-section-create-name" id="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" data-type="uciname" data-optional="true" />
<input type="submit" class="cbi-button cbi-button-add" onclick="this.form.cbi_state='add-section'; return switch_to_tab<%=sectiontype%>('<%=tab.name%>')" value="<%:Add%>" />
<% if self.invalid_cts then -%>
<br /><%:Invalid%></div>
<%- end %>
<%- end %>
</div>
<%- end -%>
<%- end -%>
</div>
<%- end -%>
<%- if self.addremove then -%>
<input type="hidden" id="cbi.cts.tagname.<%=self.config%>.<%=self.sectiontype%>" name="cbi.cts.tagname.<%=self.config%>.<%=self.sectiontype%>" value="" />
<%- end -%>
</div>
</fieldset>
<!-- /tblsection -->
<script src="/luci-static/resources/openclash/js/common.js"></script>
<script type="text/javascript">//<![CDATA[
window.addEventListener('load', onload<%=sectiontype%>, false);
var titles<%=sectiontype%> = document.getElementsByName('tab-header-<%=self.config%>-<%=self.sectiontype%>');
var divs<%=sectiontype%> = document.getElementsByClassName('dom-<%=self.config%>-<%=self.sectiontype%>');
function switch_to_tab<%=sectiontype%>(tab_name){
if(titles<%=sectiontype%>.length != divs<%=sectiontype%>.length) return;
for(var j=0; j < titles<%=sectiontype%>.length; j++){
if(titles<%=sectiontype%>[j].className === 'cbi-tab'){
localStorage.setItem("titles<%=sectiontype%>", j);
break;
}
}
if (tab_name) {
var name = document.getElementById('cbi.cts.tagname.<%=self.config%>.<%=self.sectiontype%>');
if (name) {
name.value = tab_name;
}
}
localStorage.setItem("id<%=sectiontype%>",'cbi-<%=self.config%>-<%=self.sectiontype%>');
};
function onload<%=sectiontype%>() {
if (isDarkBackground(document.body)) {
document.documentElement.setAttribute('data-darkmode', 'true');
};
if(titles<%=sectiontype%>.length != divs<%=sectiontype%>.length) return;
if (localStorage.getItem("id<%=sectiontype%>")) {
if (document.getElementById(localStorage.getItem("id<%=sectiontype%>"))) {
document.getElementById(localStorage.getItem("id<%=sectiontype%>")).scrollIntoView();
};
localStorage.removeItem("id<%=sectiontype%>");
};
if (localStorage.getItem("titles<%=sectiontype%>")) {
if (titles<%=sectiontype%>[localStorage.getItem("titles<%=sectiontype%>")]) {
for(var j=0; j < titles<%=sectiontype%>.length; j++){
titles<%=sectiontype%>[j].className = 'cbi-tab-disabled';
divs<%=sectiontype%>[j].style.display = 'none';
};
titles<%=sectiontype%>[localStorage.getItem("titles<%=sectiontype%>")].className = 'cbi-tab';
divs<%=sectiontype%>[localStorage.getItem("titles<%=sectiontype%>")].style.display = 'block';
};
localStorage.removeItem("titles<%=sectiontype%>");
};
for(var i=0; i < titles<%=sectiontype%>.length; i++){
var li<%=sectiontype%> = titles<%=sectiontype%>[i];
li<%=sectiontype%>.id = i;
var a<%=sectiontype%> = li<%=sectiontype%>.querySelector('a');
if (a<%=sectiontype%>) {
if (a<%=sectiontype%>.scrollWidth > a<%=sectiontype%>.clientWidth) {
a<%=sectiontype%>.style.textAlign = 'left';
} else {
a<%=sectiontype%>.style.textAlign = 'center';
}
}
li<%=sectiontype%>.onclick = function(){
for(var j=0; j < titles<%=sectiontype%>.length; j++){
titles<%=sectiontype%>[j].className = 'cbi-tab-disabled';
divs<%=sectiontype%>[j].style.display = 'none';
};
this.className = 'cbi-tab';
divs<%=sectiontype%>[this.id].style.display = 'block';
localStorage.setItem("titles<%=sectiontype%>",this.id);
};
li<%=sectiontype%>.onTouchStart = function(){
for(var j=0; j < titles<%=sectiontype%>.length; j++){
titles<%=sectiontype%>[j].className = 'cbi-tab-disabled';
divs<%=sectiontype%>[j].style.display = 'none';
};
this.className = 'cbi-tab';
divs<%=sectiontype%>[this.id].style.display = 'block';
localStorage.setItem("titles<%=sectiontype%>",this.id);
};
};
};
//]]>
</script>

View File

@ -0,0 +1,142 @@
<%
local fs = require "luci.openclash"
local plugin_version = fs.oc_version()
%>
<head>
<link rel="stylesheet" href="/luci-static/resources/openclash/css/oc.css?v=<%=plugin_version%>">
</head>
<li id="tool_label" class="tool_label">
<span class="tool_label_span">
&nbsp;&nbsp;<%:Current Config File%>:&nbsp;
<select class="tool_label_select" id="cfg_name">
</select>&nbsp;&nbsp;
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:SwiTch%>" onclick="return switch_config(this)" />
&nbsp;
</span>
</li>
<script src="/luci-static/resources/openclash/js/common.js"></script>
<script type="text/javascript">//<![CDATA[
var config_name = document.getElementById('cfg_name');
var tool_label = document.getElementById('tool_label');
var Commit = document.getElementById('cbi-table-1-Commit');
var Apply = document.getElementById('cbi-table-1-Apply');
var Back = document.getElementById('cbi-table-1-Back');
var Load_Config = document.getElementById('cbi-table-1-Load_Config');
var Delete_Unused_Servers = document.getElementById('cbi-table-1-Delete_Unused_Servers');
var Delete_Servers = document.getElementById('cbi-table-1-Delete_Servers');
var Delete_Proxy_Provider = document.getElementById('cbi-table-1-Delete_Proxy_Provider');
var Delete_Groups = document.getElementById('cbi-table-1-Delete_Groups');
var rule_mg = document.getElementById('cbi-table-1-rule_mg');
var pro_mg = document.getElementById('cbi-table-1-pro_mg');
setTimeout("get_header()",100);
if (Commit) {
Commit.style.textAlign="center";
}
if (Apply) {
Apply.style.textAlign="center";
}
if (Back) {
Back.style.textAlign="center";
}
if (Load_Config) {
Load_Config.style.textAlign="center";
Delete_Unused_Servers.style.textAlign="center";
Delete_Servers.style.textAlign="center";
Delete_Proxy_Provider.style.textAlign="center";
Delete_Groups.style.textAlign="center";
}
if (rule_mg) {
rule_mg.style.textAlign="center";
pro_mg.style.textAlign="center";
}
if (tool_label.style.display != "none") {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "config_name")%>', null, function(x, status) {
if (x && x.status == 200 && status.config_name != "") {
for(var i in status.config_name){
config_name.options.add(new Option(status.config_name[i].name,status.config_name[i].name));
}
if (status.config_path != "") {
config_name.value = status.config_path;
}
else
{
config_name.options.add(new Option("<%:Not Select%>",""));
config_name.value = "";
}
}
else if (x && x.status == 200 && status.config_path != "") {
config_name.options.add(new Option(status.config_path,status.config_path));
config_name.value = status.config_path;
}
else {
tool_label.style.display = "none";
}
});
};
function get_header() {
var header = document.getElementsByClassName("tabmenu-item-log ")[0];
if (header) {
insertAfter(tool_label,header);
}
else {
setTimeout("get_header()",100);
}
}
function insertAfter(newElement, targetElement) {
var parent = targetElement.parentNode;
if (parent.lastChild == targetElement) {
parent.appendChild(newElement, targetElement);
}
else {
parent.insertBefore(newElement, targetElement.nextSibling);
};
};
function switch_config(btn)
{
if (config_name.value && config_name.value != "") {
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_config")%>', {config_name: config_name.value}, function(x, status) {
if (x && x.status == 200) {
btn.disabled = false;
alert(' <%:Config File%>: ' + config_name.value + ' <%:switching succeeded!%>')
window.location.href=location.pathname;
}
else {
alert(' <%:Config File%>: ' + config_name.value + ' <%:switching failed!%>')
}
});
return false;
}
}
function restore_config(btn)
{
btn.value = '<%:Restore Default%>';
btn.disabled = true;
var r = confirm("<%:Are you sure want to restore the default config?%>")
if (r == true) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "restore")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
alert('<%:Restore succeeded!%>')
window.location.href='<%="settings"%>';
}
else {
alert('<%:Restore failed!%>')
window.location.href='<%="settings"%>';
}
});
}
btn.disabled = false;
return false;
}
//]]></script>

View File

@ -0,0 +1,414 @@
<fieldset class="cbi-section">
<table width="100%">
<tr>
<td width="100%" colspan="6">
<p align="center" id="update_tip" class="oc" style="background-color: unset;">
<b style="color: var(--info-color);"><%:Note: if the update fails, you can manually download and upload%></b>
</p>
</td>
</tr>
<tr>
<td width="16.6%" align="right"><%:Compiled Version%></td>
<td width="16.6%" align="left">
<select class="cbi-input-select" name="CORE_VERSION" id="CORE_VERSION">
<option value="linux-386"><%:linux-386%></option>
<option value="linux-amd64-v1"><%:linux-amd64-v1(x86-64)%></option>
<option value="linux-amd64-v2"><%:linux-amd64-v2(x86-64)%></option>
<option value="linux-amd64-v3"><%:linux-amd64-v3(x86-64)%></option>
<option value="linux-armv5"><%:linux-armv5%></option>
<option value="linux-armv6"><%:linux-armv6%></option>
<option value="linux-armv7"><%:linux-armv7%></option>
<option value="linux-arm64"><%:linux-arm64(armv8)%></option>
<option value="linux-loong64-abi1"><%:linux-loong64-abi1%></option>
<option value="linux-loong64-abi2"><%:linux-loong64-abi2%></option>
<option value="linux-riscv64"><%:linux-riscv64%></option>
<option value="linux-s390x"><%:linux-s390x%></option>
<option value="linux-mips-hardfloat"><%:linux-mips-hardfloat%></option>
<option value="linux-mips-softfloat"><%:linux-mips-softfloat%></option>
<option value="linux-mips64"><%:linux-mips64%></option>
<option value="linux-mips64le"><%:linux-mips64le%></option>
<option value="linux-mipsle-softfloat"><%:linux-mipsle-softfloat%></option>
<option value="linux-mipsle-hardfloat"><%:linux-mipsle-hardfloat%></option>
<option value="0"><%:Not Set%></option>
</select>
</td>
<td width="16.6%" align="right"><%:Release Branch%></td>
<td width="16.6%" align="left">
<select class="cbi-input-select" name="RELEASE_BRANCH" id="RELEASE_BRANCH">
<option value="master">Master</option>
<option value="dev">Developer</option>
</select>
</td>
<td width="16.6%" align="right"><%:Smart Core%>
<a href="javascript:void(0);" onclick="window.open('https://github.com/vernesong/mihomo/releases', '_blank');" style="color: #0066cc; text-decoration: none;" title="<%:View core infos that support smart group%>">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</a>
</td>
<td width="16.6%" align="left">
<select class="cbi-input-select" name="SMART_ENABLE" id="SMART_ENABLE">
<option value="0"><%:Disabled%></option>
<option value="1"><%:Enable%></option>
</select>
</td>
</tr>
</table>
</fieldset>
<fieldset class="cbi-section">
<table width="100%">
<tr>
<td width="100%" colspan="4">
<p align="center">
<b><%:Core Update%></b>
</p>
</td>
</tr>
<tr>
<td width="25%"><%:CPU Architecture%></td><td width="25%" align="left" id="CPU_MODEL"><%:Collecting data...%></td>
<td width="25%"><%:Last Check Update%></td><td width="25%" align="left" id="CHECKTIME"><%:Collecting data...%></td>
</tr>
<tr>
<td width="100%" colspan="4">
<p align="center">
<b><%:Core path:%>/etc/openclash/core/clash_meta</b>
</p>
</td>
</tr>
<tr><td width="25%">[Meta] <%:Current Core%></td><td width="25%" align="left" id="CORE_META_CV"><%:Collecting data...%></td><td width="25%">[Meta] <%:Latest Core%></td><td width="25%" align="left" id="CORE_META_LV"><%:Collecting data...%></td></tr>
<tr><td width="25%"><%:Update Core%></td><td width="25%" align="left" id="core_meta_up"><%:Collecting data...%></td><td width="25%"><%:Download Latest Core%></td><td width="25%" align="left" id="ma_core_meta_up"><%:Collecting data...%></td></tr>
</table>
</fieldset>
<fieldset class="cbi-section">
<table width="100%">
<tr>
<td width="100%" colspan="4">
<p align="center">
<b><%:Client Update%></b>
</p>
</td>
</tr>
<tr><td width="25%"><%:Current Client%></td><td width="25%" align="left" id="OP_CV"><%:Collecting data...%></td><td width="25%"><%:Latest Client%></td><td width="25%" align="left" id="OP_LV"><%:Collecting data...%></td></tr>
<tr><td width="25%"><%:Update Client%></td><td width="25%" align="left" id="op_up"><%:Collecting data...%></td><td width="25%"><%:Download Latest Client%></td><td width="25%" align="left" id="ma_op_up"><%:Collecting data...%></td></tr>
</table>
</fieldset>
<fieldset class="cbi-section">
<table width="100%">
<tr>
<td width="25%" align="center">
<div style="display: inline-block; white-space: nowrap;">
<select class="cbi-input-select" name="BACKUP_SELECT" id="BACKUP_SELECT">
<option value="backup"><%:Backup File%></option>
<option value="backup_ex_core"><%:Backup Exclude Cores%></option>
<option value="backup_only_core"><%:Backup Core%></option>
<option value="backup_only_config"><%:Backup Config%></option>
<option value="backup_only_rule"><%:Backup Rule Provider%></option>
<option value="backup_only_proxy"><%:Backup Proxy Provider%></option>
</select>
<span id="backup">
<%:Collecting data...%>
</span>
</div>
</td>
<td width="25%" align="center" id="restore">
<%:Collecting data...%>
</td>
<td width="25%" align="center" id="remove_core">
<%:Collecting data...%>
</td>
<td width="25%" align="center" id="one_key_update_cdn">
<%:Collecting data...%>
</td>
</tr>
</table>
</fieldset>
<script type="text/javascript">//<![CDATA[
var core_version = document.getElementById('CORE_VERSION');
var checktime = document.getElementById('CHECKTIME');
var cpu_model = document.getElementById('CPU_MODEL');
var core_meta_cv = document.getElementById('CORE_META_CV');
var core_meta_lv = document.getElementById('CORE_META_LV');
var op_cv = document.getElementById('OP_CV');
var op_lv = document.getElementById('OP_LV');
var core_meta_up = document.getElementById('core_meta_up');
var op_up = document.getElementById('op_up');
var update_tip = document.getElementById('update_tip');
var ma_core_meta_up = document.getElementById('ma_core_meta_up');
var ma_op_up = document.getElementById('ma_op_up');
var restore = document.getElementById('restore');
var backup = document.getElementById('backup');
var backup_select = document.getElementById('BACKUP_SELECT');
var one_key_update_cdn = document.getElementById('one_key_update_cdn');
var remove_core = document.getElementById('remove_core');
var release_branch = document.getElementById('RELEASE_BRANCH');
var smart_enable = document.getElementById('SMART_ENABLE');
core_meta_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Check And Update%>" onclick="return core_update(this,\'Meta\')"/>';
op_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Check And Update%>" onclick="return op_update(this)"/>';
ma_core_meta_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Download%>" onclick="return ma_core_update(this,\'Meta\')"/>';
ma_op_up.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Download%>" onclick="return ma_op_update(this)"/>';
restore.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Restore Default%>" onclick="return restore_config(this)"/>';
one_key_update_cdn.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Check Update%>" onclick="return all_one_key_update_cdn(this)"/>';
remove_core.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Remove Core%>" onclick="return remove_all_core(this)"/>';
backup.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Backup File%>" onclick="return backup_file(this)"/>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update_info")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.corever && status.corever != "0" && status.corever != "" ) {
core_version.value = status.corever;
}
else {
core_version.value = "0";
}
if ( status.release_branch && status.release_branch != "" ) {
release_branch.value = status.release_branch;
}
else {
release_branch.value = "master";
}
if ( status.smart_enable && status.smart_enable != "" ) {
smart_enable.value = status.smart_enable;
}
else {
smart_enable.value = "0";
}
}
else {
core_version.value = "0";
release_branch.value = "master";
smart_enable.value = "0";
}
});
XHR.poll(300, '<%=luci.dispatcher.build_url("admin", "services", "openclash", "get_last_version")%>', null, function(x, status) {
});
function updateStatus(x, status) {
if ( x && x.status == 200 ) {
cpu_model.innerHTML = status.coremodel ? "<b style=color:green>"+status.coremodel+"</b>" : "<b style=color:red><%:Model Not Found%></b>";
if ( status.upchecktime != "1" ) {
checktime.innerHTML = "<b style=color:green>"+status.upchecktime+"</b>";
} else {
checktime.innerHTML = "<b style=color:red><%:Check Failed%></b>";
}
if ( status.coremetacv == "0" ) {
core_meta_cv.innerHTML = "<b style=color:red><%:Unknown%></b>";
} else {
core_meta_cv.innerHTML = "<b style=color:green>"+status.coremetacv+"</b>";
}
var coremetalvis = status.corelv;
if (coremetalvis != status.coremetacv && coremetalvis != "" && coremetalvis != "loading...") {
core_meta_lv.innerHTML = "<b style=color:green>"+coremetalvis+" <%:<New>%></b>";
} else if (coremetalvis != "" && coremetalvis == status.coremetacv && coremetalvis != "loading...") {
core_meta_lv.innerHTML = "<b style=color:green>"+coremetalvis+"</b>";
} else if (coremetalvis == "loading...") {
core_meta_lv.innerHTML = "<b style=color:green><%:Getting Last Version...%></b>";
} else {
core_meta_lv.innerHTML = "<b style=color:red><%:Unknown%></b>";
}
var oplv = status.oplv;
op_cv.innerHTML = status.opcv != "0" ? "<b style=color:green>"+status.opcv+"</b>" : "<b style=color:red><%:Unknown%></b>";
if (oplv != "" && oplv != "loading..." && status.opcv != "0") {
function compareVersions(v1, v2) {
var ver1 = v1.replace(/^v/, '').split('.');
var ver2 = v2.replace(/^v/, '').split('.');
var maxLen = Math.max(ver1.length, ver2.length);
while (ver1.length < maxLen) ver1.push('0');
while (ver2.length < maxLen) ver2.push('0');
for (var i = 0; i < maxLen; i++) {
var num1 = parseInt(ver1[i], 10);
var num2 = parseInt(ver2[i], 10);
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
}
if (compareVersions(oplv, status.opcv) > 0) {
op_lv.innerHTML = "<b style=color:green>"+oplv+" <%:<New>%></b>";
} else {
op_lv.innerHTML = "<b style=color:green>"+oplv+"</b>";
}
} else if (oplv != "" && oplv != "loading...") {
op_lv.innerHTML = "<b style=color:green>"+oplv+"</b>";
} else if (oplv == "loading...") {
op_lv.innerHTML = "<b style=color:green><%:Getting Last Version...%></b>";
} else {
op_lv.innerHTML = "<b style=color:red><%:Unknown%></b>";
}
}
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update")%>', null, updateStatus);
XHR.poll(10, '<%=luci.dispatcher.build_url("admin", "services", "openclash", "update")%>', null, updateStatus);
function handleStartLog(x, status) {
if ( x && x.status == 200 ) {
if ( status.startlog == "\n" || status.startlog == "" || status.startlog == "##FINISH##\n" ) {
var rdmdl = Math.floor(Math.random()*3)+1;
if(rdmdl == 1) {
update_tip.innerHTML = '<b style=color:var(--info-color)><%:Note: if the update fails, you can manually download and upload%></b>';
}
else if(rdmdl == 2) {
update_tip.innerHTML = '<b style=color:var(--info-color)><%:Note: the client may not support update, because the firmware with squashfs format will not release flash space after updating%></b>';
}
else if(rdmdl == 3) {
update_tip.innerHTML = '<b style=color:var(--info-color)><%:Note: options will auto-save when you click to update or download%></b>';
}
} else if ( status.startlog.match("level=fatal")) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "del_start_log")%>', null, function(x) {});
if ( status.startlog.match("level=fatal") ) {
alert('<%:OpenClash Start Failed%> :\n\n' + status.startlog.split('msg=')[1]);
}
} else if ( status.startlog != "\n" && status.startlog != "" && status.startlog != "##FINISH##\n" ) {
for (var levelKey in window.levelTranslations) {
var translatedText = '[' + window.levelTranslations[levelKey] + ']';
if (status.startlog.match(translatedText)) {
update_tip.innerHTML = '<b style=color:var(--' + levelKey + '-color)>' + status.startlog + '</b>';
return;
}
}
update_tip.innerHTML = '<b style=color:var(--info-color)>' + status.startlog + '</b>';
}
}
}
XHR.poll(3, '<%=luci.dispatcher.build_url("admin", "services", "openclash", "startlog")%>', null, handleStartLog);
function core_update(btn,type) {
var v = core_version.value;
var r = release_branch.value;
var s = smart_enable.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r, smart_enable: s}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "coreupdate")%>', {core_type: type}, function(x, status) {
btn.value = '<%:Check And Update%>';
btn.disabled = false;
return false;
});
}
});
}
function op_update(btn) {
var v = core_version.value;
var r = release_branch.value;
var s = smart_enable.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r, smart_enable: s}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "opupdate")%>', null, function(x, status) {
btn.value = '<%:Check And Update%>';
btn.disabled = false;
return false;
});
}
});
}
function ma_core_update(btn,type) {
var v = core_version.value;
var r = release_branch.value;
var s = smart_enable.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r, smart_enable: s}, function(x, status) {
if (x && x.status == 200) {
btn.value = '<%:Download%>';
btn.disabled = false;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update_ma")%>', status.corever, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.corever != "0" ) {
if (type == "Meta") {
if (s == "1") {
url4='https://raw.githubusercontent.com/vernesong/OpenClash/core/'+r+'/smart/clash-'+status.corever+'.tar.gz';
window.location.href=url4;
} else {
url4='https://raw.githubusercontent.com/vernesong/OpenClash/core/'+r+'/meta/clash-'+status.corever+'.tar.gz';
window.location.href=url4;
}
}
} else {
alert('<%:No Compiled Version is Selected, Please Select on The Top and Try Again!%>')
}
}
});
return false;
}
});
}
function ma_op_update(btn) {
btn.value = '<%:Download%>';
btn.disabled = false;
var v = core_version.value;
var r = release_branch.value;
var s = smart_enable.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r, smart_enable: s}, function(x, status) {
if (x && x.status == 200) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "update_ma")%>', status.oplv, function(x, status) {
if ( x && x.status == 200 ) {
var oplv = status.oplv.substring(status.oplv.indexOf("v") + 1);
if ( oplv != "" && oplv != "loading..." ) {
if (status.pkg_type == "apk") {
url2='https://raw.githubusercontent.com/vernesong/OpenClash/package/'+r+'/luci-app-openclash-'+oplv+'.apk';
} else {
url2='https://raw.githubusercontent.com/vernesong/OpenClash/package/'+r+'/luci-app-openclash_'+oplv+'_all.ipk';
}
window.location.href=url2;
} else {
alert('<%:Failed to get the latest version. Please try again later!%>')
}
}
});
}
});
return false;
}
function remove_all_core(btn) {
btn.value = '<%:Remove Core%>';
btn.disabled = true;
var r = confirm("<%:Are you sure want to remove all core files?%>")
if (r == true) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "remove_all_core")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
alert('<%:Remove succeeded!%>')
} else {
alert('<%:Remove failed!%>')
}
});
}
btn.disabled = false;
return false;
}
function backup_file(btn) {
btn.value = '<%:Backup File%>';
btn.disabled = true;
window.location.href=backup_select.value;
btn.disabled = false;
return false;
}
function all_one_key_update_cdn(btn) {
var v = core_version.value;
var r = release_branch.value;
var s = smart_enable.value;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "save_corever_branch")%>', {core_ver: v, release_branch: r, smart_enable: s}, function(x, status) {
if (x && x.status == 200) {
btn.value = '<%:Check Update%>';
btn.disabled = false;
return select_git_cdn("one_key_update");
}
});
}
//]]></script>

View File

@ -0,0 +1,27 @@
<%+cbi/valueheader%>
<%
local fs = require "luci.openclash"
local filename = fs.filename(self:cfgvalue(section)) or "config"
local display = self.display or ""
local idname = math.random(1000)..(string.match(filename, "[%w_]+") or "")
%>
<script type="text/javascript">//<![CDATA[
function act_update_config_<%=idname%>(btn)
{
btn.disabled = true;
btn.value = '<%:UpDate%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash","update_config")%>',{filename: "<%=filename%>"}, function(x,status){});
btn.disabled = false;
return false;
};
//]]></script>
<%
if display == "" then
%>
<input type="button" class="btn cbi-button cbi-input-reload" value="<%:UpDate%>" onclick="return act_update_config_<%=idname%>(this)" />
<%
end
%>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,28 @@
<%+cbi/valueheader%>
<div style="text-align: center; margin:0 auto; display:block; white-space: nowrap;">
<label class="cbi-value" style="display:inline-block; width: 100%;" for="ulfiles"><%:Upload File Type%>&nbsp;<%:(Click to select)%>&nbsp;&nbsp;&nbsp;
<select name="file_type" style="width:auto">&nbsp;&nbsp;&nbsp;
<option value="config" selected="selected"><%:Config File%> (.yaml)</option>
<option value="proxy-provider"><%:Proxy Provider File%></option>
<option value="rule-provider"><%:Rule Provider File%></option>
<option value="clash_meta">[Meta] <%:Core File%> (.tar.gz)</option>
<option value="backup-file"><%:Backup File%> (.tar.gz)</option>
</select>
<input class="cbi-input-file" style="width: 30%" type="file" id="ulfile" name="ulfile" />
<input type="submit" class="btn cbi-button cbi-input-reload" name="upload" value="<%:Upload%>" />
<input type="submit" class="btn cbi-button cbi-button-reset" value="<%:Backup%>" onclick="return backup_all_file(this)"/>
</div>
<%+cbi/valuefooter%>
<script type="text/javascript">//<![CDATA[
function backup_all_file(btn)
{
btn.value = '<%:Backup%>';
btn.disabled = true;
window.location.href='<%="backup"%>';
btn.disabled = false;
return false;
};
//]]></script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More