Sync 2026-06-30 01:11:33

This commit is contained in:
github-actions[bot] 2026-06-30 01:11:33 +08:00
parent fb3826f4ad
commit a09508c01e
157 changed files with 78873 additions and 73698 deletions

View File

@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dae
PKG_VERSION:=2026.06.14
PKG_RELEASE:=12
PKG_RELEASE:=13
PKG_SOURCE:=dae-src-2026.06.14-fa89ea7197a5.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src

View File

@ -0,0 +1,121 @@
--- a/cmd/reload_manager.go
+++ b/cmd/reload_manager.go
@@ -411,0 +412,3 @@ func dnsConfigFingerprint(dns config.Dns
+ b.WriteString("response_ttl=")
+ b.WriteString(strconv.Itoa(dns.ResponseTtl))
+ b.WriteByte(';')
--- a/cmd/run_shutdown_test.go
+++ b/cmd/run_shutdown_test.go
@@ -605,0 +606 @@ func TestDNSConfigFingerprintCoversAllDn
+ "ResponseTtl": {},
--- a/config/config.go
+++ b/config/config.go
@@ -156,0 +157 @@ type Dns struct {
+ ResponseTtl int `mapstructure:"response_ttl" default:"0"`
--- a/config/desc.go
+++ b/config/desc.go
@@ -68,0 +69 @@ var DnsDesc = Desc{
+ "response_ttl": "Override the TTL returned to downstream DNS clients. Zero keeps dae's default behavior.",
--- a/control/control_plane.go
+++ b/control/control_plane.go
@@ -774,0 +775 @@ func NewControlPlane(
+ plane.dnsResponseTtl = dnsConfig.ResponseTtl
@@ -1273,0 +1275 @@ func (c *ControlPlane) dnsControllerOption() *DnsControllerOption {
+ ResponseTtl: c.dnsResponseTtl,
--- a/control/dns_control.go
+++ b/control/dns_control.go
@@ -80,0 +81 @@ type DnsControllerOption struct {
+ ResponseTtl int
@@ -96,0 +98 @@ type dnsControllerRuntimeState struct {
+ responseTtl int
@@ -174,0 +177,3 @@ func normalizeDnsRuntimeBehavior(option *DnsControllerOption) (qtypePrefer uint
+ if option.ResponseTtl < 0 {
+ return 0, false, 0, 0, fmt.Errorf("response_ttl must be greater than or equal to 0")
+ }
@@ -424,0 +430 @@ func (c *DnsController) TryUpdateRuntime(routing *dns.Dns, option *DnsControlle
+ responseTtl: option.ResponseTtl,
@@ -1550 +1556,7 @@ func (c *DnsController) NormalizeAndCacheDnsResp_(msg *dnsmessage.Msg, response
- // For A/AAAA records, we set TTL to 0 to prevent downstream caching while we manage it.
+ responseTtl := 0
+ if rt := c.runtime(); rt != nil {
+ responseTtl = rt.responseTtl
+ }
+
+ // For A/AAAA records, we set TTL to 0 by default to prevent downstream caching while we manage it.
+ // When response_ttl is set, use it as the downstream-visible TTL.
@@ -1553 +1565 @@ func (c *DnsController) NormalizeAndCacheDnsResp_(msg *dnsmessage.Msg, response
- msg.Answer[i].Header().Ttl = 0
+ msg.Answer[i].Header().Ttl = uint32(responseTtl)
--- /dev/null
+++ b/control/dns_response_ttl_test.go
@@ -0,0 +1,64 @@
+/*
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Copyright (c) 2022-2026, daeuniverse Organization <dae@v2raya.org>
+ */
+
+package control
+
+import (
+ "net"
+ "testing"
+ "time"
+
+ dnsmessage "github.com/miekg/dns"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDnsController_ResponseTtlOverride(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ responseTtl int
+ wantTtl uint32
+ }{
+ {name: "default_zero", responseTtl: 0, wantTtl: 0},
+ {name: "override", responseTtl: 60, wantTtl: 60},
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ ctrl := setTestDnsControllerRuntime(newTestDnsController(), func(rt *dnsControllerRuntimeState) {
+ rt.responseTtl = tt.responseTtl
+ rt.newCache = func(fqdn string, answers, ns, extra []dnsmessage.RR, deadline time.Time, originalDeadline time.Time) (*DnsCache, error) {
+ return &DnsCache{
+ Answer: answers,
+ NS: ns,
+ Extra: extra,
+ Deadline: deadline,
+ OriginalDeadline: originalDeadline,
+ }, nil
+ }
+ })
+
+ msg := new(dnsmessage.Msg)
+ msg.SetReply(&dnsmessage.Msg{
+ Question: []dnsmessage.Question{{
+ Name: "example.com.",
+ Qtype: dnsmessage.TypeA,
+ Qclass: dnsmessage.ClassINET,
+ }},
+ })
+ msg.Answer = []dnsmessage.RR{
+ &dnsmessage.A{
+ Hdr: dnsmessage.RR_Header{
+ Name: "example.com.",
+ Rrtype: dnsmessage.TypeA,
+ Class: dnsmessage.ClassINET,
+ Ttl: 300,
+ },
+ A: net.IPv4(93, 184, 216, 34),
+ },
+ }
+
+ require.NoError(t, ctrl.NormalizeAndCacheDnsResp_(msg, "example.com.:1"))
+ require.Equal(t, tt.wantTtl, msg.Answer[0].Header().Ttl)
+ })
+ }
+}
--- a/control/dns_runtime.go
+++ b/control/dns_runtime.go
@@ -22,0 +23 @@ type controlPlaneDNSRuntime struct {
+ dnsResponseTtl int
@@ -264,0 +266 @@ func (r *controlPlaneDNSRuntime) release() {
+ r.dnsResponseTtl = 0

View File

@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=daed
PKG_VERSION:=2026.06.14
PKG_RELEASE:=14
PKG_RELEASE:=15
PKG_SOURCE:=daed-src-2026.06.14-4e215048068a.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src

View File

@ -2,36 +2,17 @@ From: kenzok8 <kenzok8@noreply>
Date: 2026-06-23
Subject: [PATCH] daed: detach BPF hooks when stopping from dashboard
The dashboard "stop" (GraphQL Run(dry:true)) is delivered to dae-wing as a
reload to EmptyConfig. The fast-reload path keeps the eBPF hooks attached via
EjectBpf() (it only skips bpf.Close(), the TC/clsact filters stay bound). On a
genuine stop this leaves the LAN/WAN clsact programs attached with no tproxy
behind them, so every TCP/UDP packet (including DNS:53) is steered into a dead
tproxy and black-holed; only `network restart`/reboot recovers. ICMP is not
hijacked, so the box still pings IPs ("internal works, external dead").
dae-core already has DetachBpfHooks() for exactly this, but upstream only calls
it from the standalone dae SIGTERM handler (dae-core/cmd/run.go), never on
dae-wing's stop path. Call it explicitly when reloading to EmptyConfig.
Detach BPF hooks when the dashboard stops dae through EmptyConfig.
Reported: https://github.com/kenzok8/openwrt-daede/issues/13
---
--- a/dae/run.go
+++ b/dae/run.go
@@ -138,6 +138,21 @@
@@ -138,6 +138,13 @@
/* dae-wing start */
newConf := newReloadMsg.Config
/* dae-wing end */
+
+ // openwrt-daede patch: a "stop" from the dashboard is delivered as a
+ // reload to EmptyConfig. The fast-reload path keeps the eBPF hooks
+ // attached via EjectBpf (only bpf.Close() is skipped, the TC filters
+ // stay bound). On a real stop that leaves the LAN/WAN clsact programs
+ // attached with no tproxy behind them, black-holing all TCP/UDP
+ // (incl. DNS) until `network restart`/reboot. Upstream only calls
+ // DetachBpfHooks() from the standalone dae SIGTERM handler, never on
+ // dae-wing's stop path. Detach this generation's BPF hooks now so the
+ // datapath is fully removed when stopping.
+ // openwrt-daede: dashboard stop reloads to EmptyConfig; detach hooks.
+ if newConf == EmptyConfig {
+ if e := c.DetachBpfHooks(); e != nil {
+ log.Errorln("[Stop] DetachBpfHooks:", e)

View File

@ -0,0 +1,18 @@
From: kenzok8 <kenzok8@noreply>
Date: 2026-06-29
Subject: [PATCH] daed: use bounded probes for manual latency tests
Use the existing bounded probe for manual dashboard latency tests.
Reported: https://github.com/kenzok8/openwrt-daede/issues/20
---
--- a/graphql/service/node/latency_mutation_utils.go
+++ b/graphql/service/node/latency_mutation_utils.go
@@ -108,7 +108,7 @@ func testSingleNodeLatency(option *diale
}
defer d.Close()
- result, err := d.ProbeLatency()
+ result, err := d.ProbeLatencyFast()
if err != nil {
msg := err.Error()
resolver.MessageV = &msg

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.5-20241208
PKG_RELEASE:=1
PKG_VERSION:=1.0.5-r20241208
PKG_RELEASE:=2
PKG_MAINTAINER:=jjm2473 <jjm2473@gmail.com>
LUCI_TITLE:=Easy Access AP / Modem

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.0-20250610
PKG_RELEASE:=3
PKG_VERSION:=1.0.0-r20250610
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for arcadia
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.3-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.3-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for ChineseSubFinder
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.2-20251011
PKG_RELEASE:=3
PKG_VERSION:=1.0.2-r20251011
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for CodeServer
LUCI_PKGARCH:=all

View File

@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede
PKG_VERSION:=1.14.7
PKG_RELEASE:=24
PKG_RELEASE:=25
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)

View File

@ -35,6 +35,7 @@ const DEFAULT_TEMPLATE =
'\n' +
'dns {\n' +
' ipversion_prefer: 4\n' +
' response_ttl: 0\n' +
' upstream {\n' +
" cndns: 'udp://dns.alidns.com:53'\n" +
" fallbackdns: 'tcp+udp://dns.google:53'\n" +
@ -302,6 +303,11 @@ function renderDaeForms(ctx) {
_('Resolves everything else.'));
o.default = 'tcp+udp://dns.google:53';
o.placeholder = 'tcp+udp://dns.google:53';
o = s.option(form.Value, 'response_ttl', _('DNS response TTL'),
_('TTL returned to LAN DNS clients. 0 keeps dae default behavior.'));
o.datatype = 'uinteger';
o.default = '0';
o.placeholder = '60';
/* Logging folded into the main form so the single Save button covers it
too (no separate native save bar) */

View File

@ -845,3 +845,9 @@ msgid ""
"%s is running. Deleting the daens netns now will break its networking until "
"you restart it. Continue?"
msgstr ""
msgid "DNS response TTL"
msgstr ""
msgid "TTL returned to LAN DNS clients. 0 keeps dae default behavior."
msgstr ""

View File

@ -574,6 +574,12 @@ msgstr "兜底 DNS 上游"
msgid "Resolves everything else."
msgstr "解析其余域名。"
msgid "DNS response TTL"
msgstr "DNS 响应 TTL"
msgid "TTL returned to LAN DNS clients. 0 keeps dae default behavior."
msgstr "返回给局域网 DNS 客户端的 TTL。0 表示保持 dae 默认行为。"
msgid "Apply failed: %s"
msgstr "应用失败:%s"

View File

@ -574,6 +574,12 @@ msgstr "兜底 DNS 上游"
msgid "Resolves everything else."
msgstr "解析其余域名。"
msgid "DNS response TTL"
msgstr "DNS 响应 TTL"
msgid "TTL returned to LAN DNS clients. 0 keeps dae default behavior."
msgstr "返回给局域网 DNS 客户端的 TTL。0 表示保持 dae 默认行为。"
msgid "Apply failed: %s"
msgstr "应用失败:%s"

View File

@ -97,6 +97,7 @@ if [ -f /etc/config/dae ]; then
uci -q set dae.dns=dns
uci -q set dae.dns.cn_upstream='udp://dns.alidns.com:53'
uci -q set dae.dns.fallback_upstream='tcp+udp://dns.google:53'
uci -q set dae.dns.response_ttl='0'
fi
# Migrate legacy group filters to the unified source / name_filter fields,
# then drop the old keys so the form shows real, editable values (the old

View File

@ -13,7 +13,7 @@
# config group name/policy, list source, list name_filter
# (old list filter_sub/filter_node still read as fallback)
# config routing 'routing' private_direct/cn_direct/block_ads/fallback, list custom
# config dns 'dns' cn_upstream/fallback_upstream
# config dns 'dns' cn_upstream/fallback_upstream/response_ttl
. /lib/functions.sh
@ -213,7 +213,7 @@ generate() {
fi
# routing / dns singletons
local private_direct cn_direct block_ads fallback cn_up fb_up
local private_direct cn_direct block_ads fallback cn_up fb_up response_ttl
config_get_bool private_direct routing private_direct 1
config_get_bool cn_direct routing cn_direct 1
config_get_bool block_ads routing block_ads 0
@ -222,8 +222,12 @@ generate() {
fallback="$(sanitize "$fallback")"
config_get cn_up dns cn_upstream "udp://dns.alidns.com:53"
config_get fb_up dns fallback_upstream "tcp+udp://dns.google:53"
config_get response_ttl dns response_ttl "0"
cn_up="$(sanitize "$cn_up")"
fb_up="$(sanitize "$fb_up")"
case "$response_ttl" in
''|*[!0-9]*) response_ttl=0 ;;
esac
{
echo "# 本配置由 luci-app-daede 表单自动生成,再次保存表单会覆盖你的手动修改。"
@ -258,6 +262,7 @@ generate() {
echo "dns {"
echo " ipversion_prefer: 4"
[ "$response_ttl" -gt 0 ] && echo " response_ttl: ${response_ttl}"
echo " upstream {"
echo " cndns: '${cn_up}'"
echo " fallbackdns: '${fb_up}'"

View File

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: luci-app-ddns 2.4.0-1\n"
"POT-Creation-Date: 2016-01-30 11:07+0100\n"
"PO-Revision-Date: 2026-04-20 17:56+0000\n"
"PO-Revision-Date: 2026-06-09 01:01+0000\n"
"Last-Translator: Mytai20100 <mytai232746@gmail.com>\n"
"Language-Team: Vietnamese <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsddns/vi/>\n"
@ -11,7 +11,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.6\n"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:423
msgid "\"../\" not allowed in path for Security Reason."
@ -208,7 +208,7 @@ msgstr "Kích hoạt giao tiếp an toàn với nhà cung cấp DDNS"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:612
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1140
msgid "Enabled"
msgstr "Kích Hoạt"
msgstr "Đã bật"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:966
msgid "Error"

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.2.0-20250625
PKG_RELEASE:=3
PKG_VERSION:=1.2.0-r20250625
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for demon
LUCI_PKGARCH:=all

View File

@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-05-22 21:12+0000\n"
"PO-Revision-Date: 2026-06-11 08:15+0000\n"
"Last-Translator: Pavel Borecki <pavel.borecki@gmail.com>\n"
"Language-Team: Czech <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/cs/>\n"
@ -8,7 +8,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n"
"X-Generator: Weblate 2026.6.dev0\n"
"X-Generator: Weblate 2026.6\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"

View File

@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-06-05 19:02+0000\n"
"PO-Revision-Date: 2026-06-22 22:15+0000\n"
"Last-Translator: Franco Castillo <castillofrancodamian@gmail.com>\n"
"Language-Team: Spanish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/es/>\n"
@ -8,7 +8,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.6\n"
"X-Generator: Weblate 2026.7.dev0\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"
@ -2309,7 +2309,7 @@ msgstr "detener"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:756
msgid "syslog"
msgstr "syslog"
msgstr "Registro de eventos (Syslog)"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:364
msgid "tag"

View File

@ -1,14 +1,14 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-04-18 17:10+0000\n"
"Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>\n"
"PO-Revision-Date: 2026-06-09 01:01+0000\n"
"Last-Translator: Oğuz Ersen <oguz@ersen.moe>\n"
"Language-Team: Turkish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/tr/>\n"
"Language: tr\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.6\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"
@ -1928,7 +1928,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:45
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:111
msgid "Total"
msgstr ""
msgstr "Toplam"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:170
msgid "Total Memory"

View File

@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-04-20 17:56+0000\n"
"PO-Revision-Date: 2026-06-09 01:01+0000\n"
"Last-Translator: Mytai20100 <mytai232746@gmail.com>\n"
"Language-Team: Vietnamese <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/vi/>\n"
@ -8,7 +8,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.6\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"
@ -251,7 +251,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1189
msgid "Clear"
msgstr ""
msgstr "Xóa"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:617
msgid "Click to add a new tag to this image"
@ -1451,7 +1451,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:396
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1805
msgid "Pause"
msgstr ""
msgstr "Tạm dừng"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:207
msgid "Pause this container"
@ -1726,7 +1726,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:423
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:599
msgid "Size"
msgstr "Dung lượng"
msgstr "Kích thước"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js:82
msgid ""
@ -1945,7 +1945,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:63
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:249
msgid "Type"
msgstr ""
msgstr "Loại"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1135
msgid "Type command here... (Ctrl+D to detach)"
@ -1963,7 +1963,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1639
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1682
msgid "Unknown error"
msgstr ""
msgstr "Lỗi không xác định"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:485
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:250
@ -2035,7 +2035,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:240
msgid "Version"
msgstr ""
msgstr "Phiên bản"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:374
msgid "Volume (named)"

View File

@ -3,8 +3,8 @@
include $(TOPDIR)/rules.mk
# Updated: 2025-10-09
PKG_VERSION:=1.0.0-20251009
PKG_RELEASE:=3
PKG_VERSION:=1.0.0-r20251009
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for DPanel
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.0-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.0-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for DrawIO
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.3-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.3-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for Emby
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.0-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.0-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for Excalidraw
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.2-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.2-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for FeiShuVpn
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.3-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.3-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for Gogs
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.1.2-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.1.2-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for heimdall
LUCI_PKGARCH:=all

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.0-20240101
PKG_RELEASE:=1
PKG_VERSION:=1.0.0-r20240101
PKG_RELEASE:=2
LUCI_TITLE:=LuCI support for Hermes
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.1.4-20250321
PKG_RELEASE:=3
PKG_VERSION:=1.1.4-r20250321
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for homeassistant
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.2-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.2-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for HTReader
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.2-20250321
PKG_RELEASE:=3
PKG_VERSION:=1.0.2-r20250321
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for Immich
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.1.7-20241211
PKG_RELEASE:=3
PKG_VERSION:=1.1.7-r20241211
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for istoredup
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.6-20250406
PKG_RELEASE:=3
PKG_VERSION:=1.0.6-r20250406
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for 1Panel
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.4-20250321
PKG_RELEASE:=3
PKG_VERSION:=1.0.4-r20250321
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for ITTools
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.1.1-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.1.1-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for jackett
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.2.2-20250321
PKG_RELEASE:=3
PKG_VERSION:=1.2.2-r20250321
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for jellyfin
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.1-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.1-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for LANraragi
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.2-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.2-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for Memos
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.2-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.2-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for MTPhotos
LUCI_PKGARCH:=all

View File

@ -10,8 +10,8 @@ LUCI_TITLE:=multi account Virtual WAN config generator
LUCI_PKGARCH:=all
LUCI_DEPENDS:=+kmod-macvlan +luci-app-mwan3
PKG_VERSION:=2.2-2
PKG_RELEASE:=1
PKG_VERSION:=2.2-r2
PKG_RELEASE:=2
define Package/luci-app-multiaccountdial/conffiles
/etc/config/multiaccountdial

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.1-20221119
PKG_RELEASE:=1
PKG_VERSION:=1.0.1-r20221119
PKG_RELEASE:=2
PKG_MAINTAINER:=xiaobao <xiaobao@linkease.com>
LUCI_TITLE:=LuCI support for mymind

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.1.3-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.1.3-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for nastools
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.1-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.0.1-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for Navidrome
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.1.1-20240822
PKG_RELEASE:=3
PKG_VERSION:=1.1.1-r20240822
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for nextcloud
LUCI_PKGARCH:=all

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.1-20250403
PKG_RELEASE:=3
PKG_VERSION:=1.0.1-r20250403
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for OneAPI
LUCI_PKGARCH:=all

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclash
PKG_VERSION:=0.47.097
PKG_RELEASE:=4
PKG_VERSION:=0.47.110
PKG_RELEASE:=5
PKG_MAINTAINER:=vernesong <https://github.com/vernesong/OpenClash>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@ -59,28 +59,6 @@ define Build/Prepare
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/
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/config
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/rule_provider
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/proxy_provider
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/core
mkdir -p $(PKG_BUILD_DIR)/root/usr/share/openclash/backup/overwrite
cp -f "$(PKG_BUILD_DIR)/root/etc/config/openclash" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_rules.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_rules.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_rules_2.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_rules_2.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_hosts.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_hosts.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_fake_filter.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_fake_filter.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_domain_dns.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_domain_dns.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_domain_dns_policy.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_domain_dns_policy.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_proxy_server_dns_policy.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_fallback_filter.yaml" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_fallback_filter.yaml" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_sniffer.yaml" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_sniffer.yaml" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_localnetwork_ipv4.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_localnetwork_ipv6.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_chnroute_pass.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_chnroute_pass.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_chnroute6_pass.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_chnroute6_pass.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_firewall_rules.sh" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_firewall_rules.sh" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_overwrite.sh" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_overwrite.sh" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/overwrite/default" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/overwrite/default" >/dev/null 2>&1
exit 0
endef
@ -144,6 +122,7 @@ define Package/$(PKG_NAME)/postrm
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

View File

@ -95,12 +95,20 @@ function index()
entry({"admin", "services", "openclash", "config_file_save"}, call("action_config_file_save"))
entry({"admin", "services", "openclash", "upload_config"}, call("action_upload_config"))
entry({"admin", "services", "openclash", "add_subscription"}, call("action_add_subscription"))
entry({"admin", "services", "openclash", "generate_age_key"}, call("action_generate_age_key"))
entry({"admin", "services", "openclash", "cal_age_public_key"}, call("action_cal_age_public_key"))
entry({"admin", "services", "openclash", "add_age_config"}, call("action_add_age_config"))
entry({"admin", "services", "openclash", "upload_overwrite"}, call("action_upload_overwrite"))
entry({"admin", "services", "openclash", "overwrite_subscribe_info"}, call("action_overwrite_subscribe_info"))
entry({"admin", "services", "openclash", "overwrite_file_list"}, call("action_overwrite_file_list"))
entry({"admin", "services", "openclash", "delete_overwrite_file"}, call("delete_overwrite_file"))
entry({"admin", "services", "openclash", "get_subscribe_data"}, call("action_get_subscribe_data"))
entry({"admin", "services", "openclash", "get_subscribe_info_data"}, call("action_get_subscribe_info_data"))
entry({"admin", "services", "openclash", "oix_info"}, call("oix_info"))
entry({"admin", "services", "openclash", "oix_checkin"}, call("oix_checkin"))
entry({"admin", "services", "openclash", "oix_logout"}, call("oix_logout"))
entry({"admin", "services", "openclash", "oix_login"}, call("oix_login"))
entry({"admin", "services", "openclash", "oix_login_info_save"}, call("oix_login_info_save"))
end
local fs = require "luci.openclash"
@ -176,22 +184,14 @@ local function startlog()
return info
end
local function pkg_type()
if fs.access("/usr/bin/apk") then
return "apk"
else
return "opkg"
end
end
local function coremodel()
if opkg and opkg.info("libc") and opkg.info("libc")["libc"] then
return opkg.info("libc")["libc"]["Architecture"]
else
if pkg_type() == "opkg" then
if fs.pkg_type() == "opkg" then
return luci.sys.exec("rm -f /var/lock/opkg.lock && opkg status libc 2>/dev/null |grep 'Architecture' |awk -F ': ' '{print $2}' 2>/dev/null")
else
return luci.sys.exec("apk list libc 2>/dev/null |awk '{print $2}'")
return luci.sys.exec("rm -f /lib/apk/db/lock && apk list libc 2>/dev/null |awk '{print $2}'")
end
end
end
@ -220,8 +220,12 @@ end
local function corelv()
local core_meta_lv = ""
local core_smart_enable = fs.uci_get_config("config", "smart_enable") or "0"
local core_type = fs.uci_get_config("config", "core_type") or "Meta"
local oix_token = fs.uci_get_config("config", "oix_token") or ""
if fs.access("/tmp/clash_last_version") then
if core_smart_enable == "1" then
if core_type == "Oix" or oix_token ~= "" then
core_meta_lv = luci.sys.exec("sed -n 1p /tmp/clash_last_version 2>/dev/null |tr -d '\n'")
elseif core_smart_enable == "1" then
core_meta_lv = luci.sys.exec("sed -n 2p /tmp/clash_last_version 2>/dev/null |tr -d '\n'")
else
core_meta_lv = luci.sys.exec("sed -n 1p /tmp/clash_last_version 2>/dev/null |tr -d '\n'")
@ -240,10 +244,10 @@ local function opcv()
if info and info["luci-app-openclash"] and info["luci-app-openclash"]["Version"] and info["luci-app-openclash"]["Installed-Time"] then
v = info["luci-app-openclash"]["Version"]
else
if pkg_type() == "opkg" then
if fs.pkg_type() == "opkg" then
v = luci.sys.exec("rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep 'Version' |awk -F 'Version: ' '{print $2}' |tr -d '\n'")
else
v = luci.sys.exec("apk list luci-app-openclash 2>/dev/null|grep 'installed' | grep -oE '[0-9]+(\\.[0-9]+)*' | head -1 |tr -d '\n'")
v = luci.sys.exec("rm -f /lib/apk/db/lock && apk list luci-app-openclash 2>/dev/null|grep 'installed' | grep -oE '[0-9]+(\\.[0-9]+)*' | head -1 |tr -d '\n'")
end
end
if v and v ~= "" then
@ -394,14 +398,16 @@ function action_restore_config()
uci:commit("openclash")
luci.sys.call("mkdir -p /etc/openclash/custom >/dev/null 2>&1")
luci.sys.call("mkdir -p /etc/openclash/overwrite >/dev/null 2>&1")
luci.sys.call("mkdir -p /etc/openclash/rule_provider >/dev/null 2>&1")
luci.sys.call("/etc/init.d/openclash stop >/dev/null 2>&1")
luci.sys.call("cp /usr/share/openclash/backup/openclash /etc/config/openclash >/dev/null 2>&1 &")
luci.sys.call("cp /usr/share/openclash/backup/openclash_custom* /etc/openclash/custom/ >/dev/null 2>&1 &")
luci.sys.call("cp /usr/share/openclash/backup/openclash_force_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &")
luci.sys.call("cp /usr/share/openclash/backup/openclash_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &")
luci.sys.call("cp /usr/share/openclash/backup/china_ip_route.ipset /etc/openclash/china_ip_route.ipset >/dev/null 2>&1 &")
luci.sys.call("cp /usr/share/openclash/backup/china_ip6_route.ipset /etc/openclash/china_ip6_route.ipset >/dev/null 2>&1 &")
luci.sys.call("cp /usr/share/openclash/backup/overwrite/default /etc/openclash/overwrite/default >/dev/null 2>&1 &")
luci.sys.call("cp -f /usr/share/openclash/backup/openclash /etc/config/openclash >/dev/null 2>&1 &")
luci.sys.call("cp -f /usr/share/openclash/backup/openclash_custom* /etc/openclash/custom/ >/dev/null 2>&1 &")
luci.sys.call("cp -f /usr/share/openclash/backup/openclash_force_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &")
luci.sys.call("cp -f /usr/share/openclash/backup/openclash_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &")
luci.sys.call("cp -f /usr/share/openclash/backup/china_ip_route.ipset /etc/openclash/china_ip_route.ipset >/dev/null 2>&1 &")
luci.sys.call("cp -f /usr/share/openclash/backup/china_ip6_route.ipset /etc/openclash/china_ip6_route.ipset >/dev/null 2>&1 &")
luci.sys.call("cp -f /usr/share/openclash/backup/overwrite/default /etc/openclash/overwrite/default >/dev/null 2>&1 &")
luci.sys.call("cp -f /usr/share/openclash/backup/oc-cn-domain.mrs /etc/openclash/rule_provider/oc-cn-domain.mrs >/dev/null 2>&1 &")
luci.sys.call("rm -rf /etc/openclash/history/* >/dev/null 2>&1 &")
end
@ -943,12 +949,12 @@ local a={' B/S',' KB/S',' MB/S',' GB/S',' TB/S',' PB/S'}
if (e<=1024) then
return e..a[1]
else
repeat
e=e/1024
t=t+1
until(e<=1024)
return string.format("%.1f",e)..a[t]
end
repeat
e=e/1024
t=t+1
until(e<=1024)
return string.format("%.1f",e)..a[t]
end
end
function action_toolbar_show_sys()
@ -1283,7 +1289,7 @@ function action_update_ma()
luci.http.prepare_content("application/json")
luci.http.write_json({
oplv = oplv(),
pkg_type = pkg_type(),
pkg_type = fs.pkg_type(),
corelv = corelv(),
corever = corever();
})
@ -1357,18 +1363,19 @@ function action_refresh_log()
local core_pattern = "level=|^time="
local limit = 1000
local start_line = (log_len > 0 and total_lines > log_len) and (log_len + 1) or 1
local read_count = math.max(0, total_lines - start_line + 1)
local core_cmd, oc_cmd, core_raw, oc_raw
local core_logs = {}
local oc_logs = {}
core_cmd = string.format(
"tail -n +%d '%s' | grep -v -E '%s' | grep -E '%s' | tail -n %d",
start_line, logfile, exclude_pattern, core_pattern, limit
"tail -n +%d '%s' | head -n %d | grep -v -E '%s' | grep -E '%s' | tail -n %d",
start_line, logfile, read_count, exclude_pattern, core_pattern, limit
)
oc_cmd = string.format(
"tail -n +%d '%s' | grep -v -E '%s' | grep -v -E '%s' | tail -n %d",
start_line, logfile, exclude_pattern, core_pattern, limit
"tail -n +%d '%s' | head -n %d | grep -v -E '%s' | grep -v -E '%s' | tail -n %d",
start_line, logfile, read_count, exclude_pattern, core_pattern, limit
)
if core_refresh then
@ -1676,6 +1683,13 @@ function rename_file()
uci:set("openclash", s[".name"], "config", new_file_name)
end
end)
uci:foreach("openclash", "config_age_secret",
function(s)
if s.name == fs.filename(old_file_name) and fs.filename(new_file_name) ~= new_file_name then
uci:set("openclash", s[".name"], "name", fs.filename(new_file_name))
end
end)
uci:commit("openclash")
end
@ -1801,13 +1815,7 @@ function trans_line(data)
end
function process_status(name)
local ps_version = luci.sys.exec("ps --version 2>&1 |grep -c procps-ng |tr -d '\n'")
local cmd
if ps_version == "1" then
cmd = string.format("ps -efw |grep '%s' |grep -v grep", name)
else
cmd = string.format("ps -w |grep '%s' |grep -v grep", name)
end
local cmd = string.format("%s |grep '%s' |grep -v grep", fs.ps_cmd(), name)
local result = luci.sys.exec(cmd)
return result ~= nil and result ~= "" and not result:match("^%s*$")
end
@ -1868,7 +1876,7 @@ function action_myip_check()
url = string.format("http://myip.ipip.net?z=%d", random),
parser = function(data)
if data and data ~= "" then
local ip = string.match(data, "当前 IP([%d%.]+)")
local ip = string.match(data, "当前 IP([%x:%.]+)")
local geo = string.match(data, "来自于:(.+)")
if ip and geo then
@ -2099,6 +2107,8 @@ function latency_test(addr)
local urls = {}
if addr == "raw.githubusercontent.com" then
table.insert(urls, "https://raw.githubusercontent.com/vernesong/OpenClash/dev/img/logo.png")
elseif addr:match("jsdelivr%.net$") then
table.insert(urls, "https://" .. addr .. "/gh/vernesong/OpenClash@dev/img/logo.png")
else
table.insert(urls, "https://" .. addr .. "/favicon.ico")
end
@ -2910,6 +2920,11 @@ local function is_safe_filename(filename)
return filename and filename:match("^[%w%._%-]+$") and not filename:match("^%.")
end
local function kill_process()
local cmd = string.format("%s |grep -E 'openclash|clash' |grep -v grep |awk '{print $1}' |xargs -r kill -9 >/dev/null 2>&1", fs.ps_cmd())
luci.sys.call(cmd)
end
function action_oc_action()
local action = luci.http.formvalue("action")
local config_file = luci.http.formvalue("config_file")
@ -2937,7 +2952,7 @@ function action_oc_action()
uci:commit("openclash")
end
if not is_running() then
luci.sys.call("ps | grep openclash | grep -v grep | awk '{print $1}' | xargs -r kill -9 >/dev/null 2>&1")
kill_process()
luci.sys.call("/etc/init.d/openclash start >/dev/null 2>&1")
else
luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1")
@ -2947,14 +2962,14 @@ function action_oc_action()
uci:set("openclash", "config", "enable", "0")
uci:commit("openclash")
end
luci.sys.call("ps | grep openclash | grep -v grep | awk '{print $1}' | xargs -r kill -9 >/dev/null 2>&1")
kill_process()
luci.sys.call("/etc/init.d/openclash stop >/dev/null 2>&1")
elseif action == "restart" then
if uci:get("openclash", "config", "enable") ~= "1" then
uci:set("openclash", "config", "enable", "1")
uci:commit("openclash")
end
luci.sys.call("ps | grep openclash | grep -v grep | awk '{print $1}' | xargs -r kill -9 >/dev/null 2>&1")
kill_process()
luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1")
else
luci.http.status(500, "Invalid action parameter")
@ -2967,6 +2982,7 @@ end
function action_config_file_list()
local config_files = {}
local age_files = {}
local current_config = ""
local config_path = fs.uci_get_config("config", "config_path")
@ -2974,6 +2990,19 @@ function action_config_file_list()
current_config = config_path
end
uci:foreach("openclash", "config_age_secret", function(a)
if a.name and (a.secret or a.public) then
table.insert(age_files, {
name = a.name,
secret = a.secret or "",
public = a.public or ""
})
else
uci:delete("openclash", "config_age_secret", a[".name"])
uci:commit("openclash")
end
end)
local config_dir = "/etc/openclash/config/"
if fs.access(config_dir) then
local files = fs.dir(config_dir)
@ -2981,13 +3010,29 @@ function action_config_file_list()
for _, file in ipairs(files) do
local full_path = config_dir .. file
local stat = fs.stat(full_path)
local name_no_ext = file:match("^(.*)%.ya?ml$")
if stat and stat.type == "regular" then
if name_no_ext then
local cfile = io.open(full_path,"r")
if cfile then
local content = cfile:read(1024)
local age_symbol = content:find("BEGIN AGE ENCRYPTED FILE")
for _, age in pairs(age_files) do
if age.name == name_no_ext and age.secret and age_symbol then
stat.age = true
break
end
end
end
cfile:close()
end
if string.match(file, "%.ya?ml$") then
table.insert(config_files, {
name = file,
path = full_path,
size = stat.size,
mtime = stat.mtime
mtime = stat.mtime,
age = stat.age or false
})
end
end
@ -3114,9 +3159,13 @@ end
function action_config_file_read()
local config_file = luci.http.formvalue("config_file")
luci.http.prepare_content("application/json")
if not config_file then
luci.http.status(500, "Missing config_file parameter")
luci.http.write_json({
status = "error",
message = "Missing config_file parameter"
})
return
end
@ -3132,7 +3181,6 @@ function action_config_file_read()
end
if not allow then
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "error",
message = "Invalid config file path"
@ -3141,7 +3189,6 @@ function action_config_file_read()
end
if not fs.access(config_file) then
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "success",
content = "",
@ -3158,7 +3205,6 @@ function action_config_file_read()
local stat = fs.stat(config_file)
if not stat or stat.type ~= "regular" then
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "error",
message = "Config file is not a regular file"
@ -3167,7 +3213,6 @@ function action_config_file_read()
end
if stat.size > 10 * 1024 * 1024 then
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "error",
message = "Config file too large (max 10MB)"
@ -3177,7 +3222,6 @@ function action_config_file_read()
local content = fs.readfile(config_file)
if content == nil then
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "error",
message = "Failed to read config file"
@ -3185,7 +3229,6 @@ function action_config_file_read()
return
end
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "success",
content = content,
@ -3202,17 +3245,24 @@ end
function action_config_file_save()
local config_file = luci.http.formvalue("config_file")
local content = luci.http.formvalue("content")
luci.http.prepare_content("application/json")
if content then
content = content:gsub("\r\n", "\n"):gsub("\r", "\n")
end
if not config_file then
luci.http.status(500, "Missing config_file parameter")
luci.http.write_json({
status = "error",
message = "Missing config_file parameter"
})
return
end
if not content then
luci.http.status(500, "Missing content parameter")
luci.http.write_json({
status = "error",
message = "Missing content parameter"
})
return
end
@ -3228,7 +3278,6 @@ function action_config_file_save()
end
else
if not (config_file == "/etc/openclash/custom/openclash_custom_overwrite.sh" or (config_file:match("^/etc/openclash/overwrite/[^/]+$") and not string.find(config_file, "%.%."))) then
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "error",
message = "Invalid overwrite file path"
@ -3238,7 +3287,6 @@ function action_config_file_save()
end
if string.len(content) > 10 * 1024 * 1024 then
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "error",
message = "Content too large (max 10MB)"
@ -3251,7 +3299,6 @@ function action_config_file_save()
backup_file = config_file .. ".backup." .. os.time()
local backup_success = luci.sys.call(string.format("cp '%s' '%s'", config_file, backup_file))
if backup_success ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "error",
message = "Failed to create backup file"
@ -3266,7 +3313,6 @@ function action_config_file_save()
luci.sys.call(string.format("mv '%s' '%s'", backup_file, config_file))
end
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "error",
message = "Failed to write config file"
@ -3280,7 +3326,6 @@ function action_config_file_save()
luci.sys.call(string.format("mv '%s' '%s'", backup_file, config_file))
end
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "error",
message = "File write verification failed"
@ -3316,7 +3361,6 @@ function action_config_file_save()
}
end
luci.http.prepare_content("application/json")
luci.http.write_json({
status = "success",
message = "Config file saved successfully",
@ -3332,6 +3376,7 @@ function action_add_subscription()
local sub_convert = luci.http.formvalue("sub_convert") or "0"
local convert_address = luci.http.formvalue("convert_address") or ""
local template = luci.http.formvalue("template") or ""
local custom_template_url = luci.http.formvalue("custom_template_url") or ""
local emoji = luci.http.formvalue("emoji") or "false"
local udp = luci.http.formvalue("udp") or "false"
local skip_cert_verify = luci.http.formvalue("skip_cert_verify") or "false"
@ -3342,20 +3387,19 @@ function action_add_subscription()
local keyword = luci.http.formvalue("keyword") or ""
local ex_keyword = luci.http.formvalue("ex_keyword") or ""
local de_ex_keyword = luci.http.formvalue("de_ex_keyword") or ""
luci.http.prepare_content("application/json")
if not name or not address then
if not name then
luci.http.write_json({
status = "error",
message = "Missing name or address parameter"
message = "Missing name parameter"
})
return
end
local is_valid_url = false
if sub_convert == "1" then
if address and address ~= "" and sub_convert == "1" then
local prefixed_http_pattern = "^[^,%s]+,https?://.+"
local encoded_prefixed_http_pattern = "^[^%%%s]+%%2[Cc]https?%%3[Aa]%%2[Ff]%%2[Ff].+"
@ -3390,10 +3434,12 @@ function action_add_subscription()
is_valid_url = true
end
end
else
elseif address and address ~= "" then
if string.find(address, "^https?://") and not string.find(address, "\n") and not string.find(address, "|") then
is_valid_url = true
end
else
is_valid_url = true
end
if not is_valid_url then
@ -3451,15 +3497,25 @@ function action_add_subscription()
if section_id then
uci:set("openclash", section_id, "name", name)
uci:set("openclash", section_id, "address", normalized_address)
if normalized_address and normalized_address ~= "" then
uci:set("openclash", section_id, "address", normalized_address)
else
uci:delete("openclash", section_id, "address")
end
uci:set("openclash", section_id, "sub_ua", sub_ua)
uci:set("openclash", section_id, "sub_convert", sub_convert)
if sub_convert == "1" then
uci:set("openclash", section_id, "convert_address", convert_address)
uci:set("openclash", section_id, "template", template)
if template == "0" then
uci:set("openclash", section_id, "custom_template_url", custom_template_url)
else
uci:delete("openclash", section_id, "custom_template_url")
end
else
uci:delete("openclash", section_id, "convert_address")
uci:delete("openclash", section_id, "template")
uci:delete("openclash", section_id, "custom_template_url")
end
uci:set("openclash", section_id, "emoji", emoji)
uci:set("openclash", section_id, "udp", udp)
@ -3993,6 +4049,19 @@ function action_get_subscribe_data()
end
end)
uci:foreach("openclash", "config_age_secret", function(a)
if a.name == filename and (not a.hidden or a.hidden ~= "true") then
if a.secret then data.config_age_secret = a.secret end
if a.public then data.config_age_public = a.public end
if a.algo then data.config_age_algo = a.algo end
return false
end
if a.name == filename and a.hidden and a.hidden == "true" then
data.config_age_hidden = true
return false
end
end)
luci.http.prepare_content("application/json")
luci.http.write_json(data)
end
@ -4005,4 +4074,294 @@ function action_get_subscribe_info_data()
end
luci.http.prepare_content("application/json")
luci.http.write_json(get_sub_url(filename))
end
function action_generate_age_key()
local algo = luci.http.formvalue("algo") or "keygen"
local cmd = string.format("%s age %s", meta_core_path, (algo == "pq" and "keygen-pq" or "keygen"))
local out = luci.sys.exec(cmd .. " 2>/dev/null")
local secret = out:match("(AGE%-SECRET%-KEY%-%S+)")
local public = out:match("# public key: ([^\n\r]+)")
luci.http.prepare_content("application/json")
if not secret then
luci.http.write_json({status = "error", message = "Failed to generate age key", output = out})
return
end
luci.http.write_json({status = "success", secret = secret, public = public})
end
function action_cal_age_public_key()
local secret = luci.http.formvalue("secret") or ""
if secret == "" then
luci.http.prepare_content("application/json")
luci.http.write_json({status = "error", message = "Secret key is required"})
return
end
local cmd = string.format("%s age convert %s", meta_core_path, secret)
local out = luci.sys.exec(cmd .. " 2>/dev/null")
luci.http.prepare_content("application/json")
if out and out:match("^age") then
luci.http.write_json({status = "success", public = out})
else
luci.http.write_json({status = "error", message = "Failed to calculate public key, invalid secret key", output = out})
end
end
function action_add_age_config()
local name = luci.http.formvalue("name")
local age_secret = luci.http.formvalue("age_secret") or ""
local age_public = luci.http.formvalue("age_public") or ""
local age_algo = luci.http.formvalue("age_algo") or ""
local age_section_id, age_section_hidden
luci.http.prepare_content("application/json")
if not name or name == "" then
luci.http.write_json({status = "error", message = "Missing name parameter"})
return
end
uci:foreach("openclash", "config_age_secret", function(s)
if s.name == name then
age_section_id = s['.name']
age_section_hidden = s.hidden and s.hidden == "true"
return false
end
end)
if age_section_hidden then
luci.http.write_json({status = "error", message = "Cannot modify hidden age configuration"})
return
end
if not age_section_id and (age_secret ~= "" or age_public ~= "" or age_algo ~= "") then
age_section_id = uci:add("openclash", "config_age_secret")
if age_section_id then
uci:set("openclash", age_section_id, "name", name)
end
end
if age_section_id then
if (age_secret == "" and age_public == "") then
uci:delete("openclash", age_section_id)
else
if age_secret and age_secret ~= "" then
uci:set("openclash", age_section_id, "secret", age_secret)
else
uci:delete("openclash", age_section_id, "secret")
end
if age_public and age_public ~= "" then
uci:set("openclash", age_section_id, "public", age_public)
else
uci:delete("openclash", age_section_id, "public")
end
if age_algo and age_algo ~= "" then
uci:set("openclash", age_section_id, "algo", age_algo)
else
uci:delete("openclash", age_section_id, "algo")
end
end
uci:commit("openclash")
end
luci.http.write_json({status = "success"})
end
function oix_login_info_save()
uci:set("openclash", "config", "oix_email", luci.http.formvalue("email"))
uci:set("openclash", "config", "oix_passwd", luci.http.formvalue("passwd"))
uci:set("openclash", "config", "oix_checkin", luci.http.formvalue("checkin"))
uci:set("openclash", "config", "oix_checkin_interval", luci.http.formvalue("interval"))
if tonumber(luci.http.formvalue("multiple")) > 100 then
uci:set("openclash", "config", "oix_checkin_multiple", "100")
elseif tonumber(luci.http.formvalue("multiple")) < 1 or not tonumber(luci.http.formvalue("multiple")) then
uci:set("openclash", "config", "oix_checkin_multiple", "1")
else
uci:set("openclash", "config", "oix_checkin_multiple", luci.http.formvalue("multiple"))
end
uci:commit("openclash")
luci.http.prepare_content("application/json")
luci.http.write_json({status = "success"})
end
function oix_login()
local result, info, token, get_sub, sub_info, sub_key, sub_match, sub_convert, sid, sub_file, SIGNATURE
local email = fs.uci_get_config("config", "oix_email")
local passwd = fs.uci_get_config("config", "oix_passwd")
token = fs.uci_get_config("config", "oix_token")
if email and passwd then
info = luci.sys.exec(string.format("curl -sL -H 'Content-Type: application/json' -H 'User-Agent: OpenClash' -d '{\"email\":\"%s\", \"passwd\":\"%s\", \"token_expire\":\"365\" }' -X POST https://oix-api.dler.io/api/v1/login", email, passwd))
if info then
info = json.parse(info)
end
if info and info.ret == 200 then
-- because of uci cache, need reload after delete if get new
if token and token ~= "" then
oix_logout(token)
end
token = info.data.token
uci:set("openclash", "config", "oix_token", token)
uci:commit("openclash")
get_sub = string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -X POST https://oix-api.dler.io/api/v1/managed/clash", token)
sub_info = luci.sys.exec(get_sub)
if sub_info then
sub_info = json.parse(sub_info)
end
if sub_info and sub_info.ret == 200 then
sub_key = {"openclash"}
for _,v in ipairs(sub_key) do
while true do
sub_match = false
sub_convert = false
uci:foreach("openclash", "config_subscribe",
function(s)
if s.name == "oixCloud - smart" and s.address == sub_info[v] then
sub_match = true
end
if s.name == "oixCloud - smart" and s.address ~= sub_info[v] then
sub_convert = true
sid = s['.name']
end
end)
if sub_match then break end
if sub_convert then
uci:set("openclash", sid, "address", sub_info[v])
elseif sub_info[v] then
sid = uci:add("openclash", "config_subscribe")
uci:set("openclash", sid, "name", "oixCloud - smart")
uci:set("openclash", sid, "address", sub_info[v])
end
uci:commit("openclash")
break
end
if sub_info[v] then
luci.sys.exec(string.format('curl -sL -m 10 --retry 2 --user-agent "clash" "%s" -o "/etc/openclash/config/oixCloud - smart.yaml" >/dev/null 2>&1', sub_info[v]))
luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
end
end
end
result = info.ret
else
uci:delete("openclash", "config", "oix_token")
uci:commit("openclash")
fs.unlink("/tmp/oix_checkin")
fs.unlink("/tmp/oix_info")
if info and info.msg then
result = info.msg
else
result = "login faild"
end
end
else
uci:delete("openclash", "config", "oix_token")
uci:commit("openclash")
fs.unlink("/tmp/oix_checkin")
fs.unlink("/tmp/oix_info")
result = "email or passwd is wrong"
end
luci.http.prepare_content("application/json")
luci.http.write_json({result = result})
end
function oix_logout(oldtoken)
local info, result, token
if not oldtoken then
token = fs.uci_get_config("config", "oix_token")
else
token = oldtoken
end
if token then
info = luci.sys.exec(string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -X POST https://oix-api.dler.io/api/v1/logout", token))
if info then
info = json.parse(info)
end
if info and info.ret == 200 then
uci:delete("openclash", "config", "oix_token")
if not oldtoken then
uci:delete("openclash", "config", "oix_checkin")
uci:delete("openclash", "config", "oix_checkin_interval")
uci:delete("openclash", "config", "oix_checkin_multiple")
end
uci:commit("openclash")
fs.unlink("/tmp/oix_checkin")
fs.unlink("/tmp/oix_info")
result = info.ret
else
if info and info.msg then
result = info.msg
else
result = "logout faild"
end
end
else
result = "logout faild"
end
if not oldtoken then
luci.http.prepare_content("application/json")
luci.http.write_json({result = result})
end
end
function oix_info()
local info, path, get_info
local result = "error"
local token = fs.uci_get_config("config", "oix_token")
path = "/tmp/oix_info"
if token then
get_info = string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -X POST https://oix-api.dler.io/api/v1/information -o %s", token, path)
if not fs.access(path) then
luci.sys.exec(get_info)
else
if fs.readfile(path) == "" or not fs.readfile(path) then
luci.sys.exec(get_info)
else
if (os.time() - fs.mtime(path) > 900) then
luci.sys.exec(get_info)
end
end
end
info = fs.readfile(path)
if info then
info = json.parse(info)
end
if info and info.ret == 200 and info.data then
result = info.data
elseif info and info.msg then
fs.writefile(path, json.stringify(info))
else
fs.unlink(path)
end
end
luci.http.prepare_content("application/json")
luci.http.write_json({result = result})
end
function oix_checkin()
local info, result
local path = "/tmp/oix_checkin"
local token = fs.uci_get_config("config", "oix_token")
local multiple = fs.uci_get_config("config", "oix_checkin_multiple") or 1
if token then
info = luci.sys.exec(string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '{\"multiple\":\"%s\"}' -X POST https://oix-api.dler.io/api/v1/checkin", token, token, multiple))
if info then
info = json.parse(info)
end
if info and info.ret == 200 then
fs.unlink("/tmp/oix_info")
fs.writefile(path, info)
luci.sys.exec(string.format("echo -e %s oixCloud Checkin Successful, Result:【%s】 >> /tmp/openclash.log", os.date("%Y-%m-%d %H:%M:%S"), info.data.checkin))
result = info
else
if info and info.msg then
luci.sys.exec(string.format("echo -e %s oixCloud Checkin Failed, Result:【%s】 >> /tmp/openclash.log", os.date("%Y-%m-%d %H:%M:%S"), info.msg))
else
luci.sys.exec(string.format("echo -e %s oixCloud Checkin Failed! Please Check And Try Again... >> /tmp/openclash.log",os.date("%Y-%m-%d %H:%M:%S")))
end
result = info
end
else
result = "error"
end
luci.http.prepare_content("application/json")
luci.http.write_json({result = result})
end

View File

@ -13,7 +13,9 @@ 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"))

View File

@ -205,14 +205,14 @@ custom_fallback_filter.wrap = "off"
custom_fallback_filter:depends("custom_fallback_filter", "1")
function custom_fallback_filter.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml") or ""
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 = NXFS.readfile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml", value)
fs.writefile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml", value)
end
end
end
@ -237,14 +237,14 @@ custom_fake_black.wrap = "off"
custom_fake_black:depends("custom_fakeip_filter", "1")
function custom_fake_black.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_fake_filter.list") or ""
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 = NXFS.readfile("/etc/openclash/custom/openclash_custom_fake_filter.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_fake_filter.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_fake_filter.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_fake_filter.list", value)
end
end
end
@ -261,14 +261,14 @@ custom_domain_dns_policy.wrap = "off"
custom_domain_dns_policy:depends("custom_name_policy", "1")
function custom_domain_dns_policy.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list") or ""
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 = NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list", value)
end
end
end
@ -284,14 +284,14 @@ 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 NXFS.readfile("/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list") or ""
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 = NXFS.readfile("/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list", value)
end
end
end
@ -307,14 +307,14 @@ custom_hosts.wrap = "off"
custom_hosts:depends("custom_host", "1")
function custom_hosts.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_hosts.list") or ""
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 = NXFS.readfile("/etc/openclash/custom/openclash_custom_hosts.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_hosts.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_hosts.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_hosts.list", value)
end
end
end
@ -374,14 +374,14 @@ sniffer_custom.rows = 20
sniffer_custom.wrap = "off"
function sniffer_custom.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_sniffer.yaml") or ""
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 = NXFS.readfile("/etc/openclash/custom/openclash_custom_sniffer.yaml")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_sniffer.yaml")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_sniffer.yaml", value)
fs.writefile("/etc/openclash/custom/openclash_custom_sniffer.yaml", value)
end
end
end
@ -466,14 +466,14 @@ custom_rules.rows = 20
custom_rules.wrap = "off"
function custom_rules.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_rules.list") or ""
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 = NXFS.readfile("/etc/openclash/custom/openclash_custom_rules.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_rules.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_rules.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_rules.list", value)
end
end
end
@ -486,14 +486,14 @@ custom_rules_2.rows = 20
custom_rules_2.wrap = "off"
function custom_rules_2.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_rules_2.list") or ""
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 = NXFS.readfile("/etc/openclash/custom/openclash_custom_rules_2.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_rules_2.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_rules_2.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_rules_2.list", value)
end
end
end

View File

@ -8,6 +8,7 @@ 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>]]
@ -59,13 +60,135 @@ end
---- UA
o = s:option(Value, "sub_ua", "User-Agent")
o.description = font_red..bold_on..translate("Used for Downloading Subscriptions, Defaults to Clash")..bold_off..font_off
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.default = "clash-verge/v2.4.5"
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")
@ -183,6 +306,17 @@ 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
@ -192,6 +326,7 @@ 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

View File

@ -373,14 +373,14 @@ sev = s:option(TextValue, "user")
sev.rows = 40
sev.wrap = "off"
sev.cfgvalue = function(self, section)
return NXFS.readfile(conf) or NXFS.readfile(dconf) or ""
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 = NXFS.readfile(conf)
local old_value = fs.readfile(conf)
if value ~= old_value then
NXFS.writefile(conf, value)
fs.writefile(conf, value)
end
end
end
@ -395,7 +395,7 @@ def.rows = 40
def.wrap = "off"
def.readonly = true
def.cfgvalue = function(self, section)
return NXFS.readfile(sconf) or NXFS.readfile(dconf) or ""
return fs.readfile(sconf) or fs.readfile(dconf) or ""
end
def.write = function(self, section, value)
end

View File

@ -24,15 +24,15 @@ o.wrap = "off"
function o.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile(file_path)
local old_value = fs.readfile(file_path)
if value ~= old_value then
NXFS.writefile(file_path, value)
fs.writefile(file_path, value)
end
end
end
function o.cfgvalue(self, section)
return NXFS.readfile(file_path) or ""
return fs.readfile(file_path) or ""
end
local t = {

View File

@ -185,6 +185,7 @@ o.write = function()
end
end)
m.uci:commit("openclash")
HTTP.redirect(m.redirect)
end
o = b:option(Button,"Delete_Servers", " ")
@ -193,6 +194,7 @@ 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", " ")
@ -201,6 +203,7 @@ 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", " ")
@ -209,6 +212,7 @@ 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 = {

View File

@ -79,6 +79,7 @@ s:tab("auto_restart", translate("Auto Restart"))
s:tab("version_update", translate("Version Update"))
s:tab("developer", translate("Developer Settings"))
s:tab("debug", translate("Debug Logs"))
s:tab("oixcloud", translate("oixCloud"))
o = s:taboption("op_mode", ListValue, "en_mode", font_red..bold_on..translate("Select Mode")..bold_off..font_off)
o.description = translate("Select Mode For OpenClash Work, Try Flush DNS Cache If Network Error")
@ -154,6 +155,51 @@ o:value("2", translate("Firewall Redirect"))
o = s:taboption("dns", DummyValue, "flush_dns_cache", translate("Flush DNS"))
o.template = "openclash/flush_dns_cache"
o = s:taboption("dns", Button, "dnsmasq_fix", translate("Dnsmasq Fix"))
o.description = translate("If DNS is abnormal after stopping the OpenClash, please try to fix")
o.inputtitle = translate("Fix")
o.inputstyle = "reload"
o.write = function()
uci:set("dhcp", "@dnsmasq[0]", "noresolv", "0")
uci:set("dhcp", "@dnsmasq[0]", "localuse", "1")
local resolv_file = uci:get("dhcp", "@dnsmasq[0]", "resolvfile")
local need_fix = false
if not resolv_file or resolv_file == "" then
need_fix = true
elseif not NXFS.access(resolv_file) then
need_fix = true
else
local content = fs.readfile(resolv_file) or ""
if not content:find("nameserver") then
need_fix = true
end
end
if need_fix then
for _, f in ipairs({"/tmp/resolv.conf.d/resolv.conf.auto", "/tmp/resolv.conf.auto"}) do
if NXFS.access(f) then
local content = fs.readfile(f) or ""
if content:find("nameserver") then
uci:set("dhcp", "@dnsmasq[0]", "resolvfile", f)
resolv_file = f
need_fix = false
break
end
end
end
end
if need_fix then
resolv_file = "/tmp/resolv.conf.d/resolv.conf.auto"
SYS.call("mkdir -p /tmp/resolv.conf.d")
fs.writefile(resolv_file, "# Interface lan\nnameserver 119.29.29.29\nnameserver 8.8.8.8\n")
uci:set("dhcp", "@dnsmasq[0]", "resolvfile", resolv_file)
end
uci:set("openclash", "config", "redirect_dns", "0")
uci:commit("dhcp")
uci:commit("openclash")
SYS.call("/etc/init.d/dnsmasq restart")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "settings"))
end
o = s:taboption("dns", Flag, "enable_custom_domain_dns_server", translate("Enable Specify DNS Server"))
o.default = 0
o:depends("enable_redirect_dns", "1")
@ -173,14 +219,14 @@ custom_domain_dns.wrap = "off"
custom_domain_dns:depends{enable_redirect_dns = "1", enable_custom_domain_dns_server = "1"}
function custom_domain_dns.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns.list") or ""
return fs.readfile("/etc/openclash/custom/openclash_custom_domain_dns.list") or ""
end
function custom_domain_dns.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_domain_dns.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_domain_dns.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_domain_dns.list", value)
end
end
end
@ -289,7 +335,7 @@ o.rmempty = true
o = s2:option(ListValue, "user", translate("User"))
o:value("")
o.default = ""
local passwd_content = NXFS.readfile("/etc/passwd")
local passwd_content = fs.readfile("/etc/passwd")
local users = ""
if passwd_content then
for line in string.gmatch(passwd_content, "[^\n]+") do
@ -482,14 +528,14 @@ o.rows = 20
o.wrap = "off"
function o.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list") or ""
return fs.readfile("/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list") or ""
end
function o.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list", value)
end
end
end
@ -503,14 +549,14 @@ o:depends("enable_redirect_dns", "1")
o:depends("enable_redirect_dns", "0")
function o.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_chnroute_pass.list") or ""
return fs.readfile("/etc/openclash/custom/openclash_custom_chnroute_pass.list") or ""
end
function o.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_chnroute_pass.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_chnroute_pass.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_chnroute_pass.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_chnroute_pass.list", value)
end
end
end
@ -1148,7 +1194,7 @@ o:value("https://ispip.clang.cn/all_cn.txt", translate("Clang-CN")..translate("(
o:value("https://ispip.clang.cn/all_cn_cidr.txt", translate("Clang-CN-CIDR"))
o:value("https://fastly.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/CN-ip-cidr.txt", translate("Hackl0us-CN-CIDR-fastly-jsdelivr"))
o:value("https://testingcf.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/CN-ip-cidr.txt", translate("Hackl0us-CN-CIDR-testingcf-jsdelivr"))
o:value("https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/direct-list.txt", translate("Loyalsoldier-github-Version"))
o:value("https://raw.githubusercontent.com/gaoyifan/china-operator-ip/refs/heads/ip-lists/china.txt", translate("gaoyifan-github-Version"))
o.default = "https://ispip.clang.cn/all_cn.txt"
o = s:taboption("chnr_update", Value, "chnr6_custom_url")
@ -1156,6 +1202,7 @@ o.title = translate("Custom Chnroute6 Lists URL")
o.rmempty = false
o.description = translate("Custom Chnroute6 Lists URL, Click Button Below To Refresh After Edit")
o:value("https://ispip.clang.cn/all_cn_ipv6.txt", translate("Clang-CN-IPV6")..translate("(Default)"))
o:value("https://raw.githubusercontent.com/gaoyifan/china-operator-ip/refs/heads/ip-lists/china6.txt", translate("gaoyifan-github-Version"))
o.default = "https://ispip.clang.cn/all_cn_ipv6.txt"
o = s:taboption("chnr_update", Button, translate("Chnroute Lists Update"))
@ -1309,14 +1356,14 @@ o.wrap = "off"
o:depends("ipv6_enable", "1")
function o.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list") or ""
return fs.readfile("/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list") or ""
end
function o.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list", value)
end
end
end
@ -1329,14 +1376,14 @@ o.wrap = "off"
o:depends({ipv6_enable = "1", enable_redirect_dns = "1"})
function o.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_chnroute6_pass.list") or ""
return fs.readfile("/etc/openclash/custom/openclash_custom_chnroute6_pass.list") or ""
end
function o.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_chnroute6_pass.list")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_chnroute6_pass.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_chnroute6_pass.list", value)
fs.writefile("/etc/openclash/custom/openclash_custom_chnroute6_pass.list", value)
end
end
end
@ -1353,14 +1400,14 @@ o.rows = 30
o.wrap = "off"
function o.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_firewall_rules.sh") or ""
return fs.readfile("/etc/openclash/custom/openclash_custom_firewall_rules.sh") or ""
end
function o.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_firewall_rules.sh")
local old_value = fs.readfile("/etc/openclash/custom/openclash_custom_firewall_rules.sh")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_firewall_rules.sh", value)
fs.writefile("/etc/openclash/custom/openclash_custom_firewall_rules.sh", value)
end
end
end
@ -1369,6 +1416,54 @@ end
o = s:taboption("debug", DummyValue, "", nil)
o.template = "openclash/debug"
---- oixcloud
o = s:taboption("oixcloud", Value, "oix_email")
o.title = translate("Account Email Address")
o.rmempty = true
o = s:taboption("oixcloud", Value, "oix_passwd")
o.title = translate("Account Password")
o.password = true
o.rmempty = true
if fs.uci_get_config("config", "oix_token") then
o = s:taboption("oixcloud", Flag, "oix_checkin")
o.title = translate("Checkin")
o.default = 0
o.rmempty = true
end
o = s:taboption("oixcloud", Value, "oix_checkin_interval")
o.title = translate("Checkin Interval (hour)")
o:depends("oix_checkin", "1")
o.default = "1"
o.rmempty = true
o = s:taboption("oixcloud", Value, "oix_checkin_multiple")
o.title = translate("Checkin Multiple")
o.datatype = "uinteger"
o.default = "1"
o:depends("oix_checkin", "1")
o.rmempty = true
o.description = font_green..bold_on..translate("Multiple Must Be a Positive Integer and No More Than 100")..bold_off..font_off
function o.validate(self, value)
if tonumber(value) < 1 then
return "1"
end
if tonumber(value) > 100 then
return "100"
end
return value
end
o = s:taboption("oixcloud", DummyValue, "oix_login", translate("Account Login"))
o.template = "openclash/oix_login"
if fs.uci_get_config("config", "oix_token") then
o.value = font_green..bold_on..translate("Account logged in")..bold_off..font_off
else
o.value = font_red..bold_on..translate("Account not logged in")..bold_off..font_off
end
local t = {
{Commit, Apply}
}

View File

@ -35,6 +35,10 @@ 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"
@ -83,14 +87,34 @@ end
-- @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
readfile = fs.readfile
-- 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
writefile = fs.writefile
-- 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
@ -356,4 +380,147 @@ function get_file_path_from_request()
end
return file_path
end
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

@ -1,669 +1,3 @@
<style>
@font-face {
font-family: 'Twemoji Mozilla';
src: url('/luci-static/resources/openclash/fonts/TwemojiMozilla-flags-B12sb_Bp.woff2') format('woff2');
}
.oc[data-darkmode="true"] .config-editor-model .CodeMirror {
background: var(--bg-white);
color: var(--text-primary);
}
.oc[data-darkmode="true"] .config-editor-model .CodeMirror-gutters {
background: var(--bg-gray);
border-right: 1px solid var(--border-light);
}
.oc[data-darkmode="true"] .config-editor-model .CodeMirror-linenumber {
color: var(--text-secondary);
}
.oc[data-darkmode="true"] .config-editor-model .CodeMirror-scrollbar-filler,
.oc[data-darkmode="true"] .config-editor-model .CodeMirror-gutter-filler {
background: var(--bg-gray);
}
.oc[data-darkmode="true"] #config-mode-tabs .mode-tab {
color: var(--text-secondary);
}
.oc[data-darkmode="true"] #config-mode-tabs .mode-tab:hover {
color: var(--text-primary);
background: rgba(96, 165, 250, 0.1);
}
.oc[data-darkmode="true"] #config-mode-tabs .mode-tab.active {
background: var(--primary-color);
color: white;
}
.oc[data-darkmode="true"] #config-mergeview-container .CodeMirror-merge-gap {
background: var(--text-secondary) !important;
}
.oc[data-darkmode="true"] .oc .config-editor-content {
border-bottom: 1px solid var(--border-light);
border-top: 1px solid var(--border-light);
}
.oc[data-darkmode="true"] .overwrite-banner {
background: rgba(255,80,80,0.18);
}
.oc[data-darkmode="true"] .overwrite-banner svg {
stroke: var(--error-color);
}
.oc[data-darkmode="true"] .overwrite-banner svg circle {
stroke: var(--error-color);
fill: rgba(255,80,80,0.18);
}
.oc .config-editor-model-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 5;
display: none;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.oc .config-editor-model-overlay.show {
display: flex;
}
.oc .config-editor-model {
background: var(--bg-white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
width: 90vw;
height: 85vh;
max-width: 1200px;
min-width: 600px;
min-height: 400px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border-light);
position: relative;
transition: all 0.3s ease;
}
.oc .config-editor-model.maximized {
width: 98vw !important;
height: 95vh !important;
max-width: none !important;
}
.oc .config-editor-model.minimized {
width: 70vw !important;
height: 70vh !important;
}
.oc .config-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-gray);
flex-shrink: 0;
cursor: move;
user-select: none;
min-width: 0;
}
.oc .config-editor-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
}
.oc .config-editor-title #editTitle,
.oc .config-editor-title #config-file-name {
user-select: text;
-webkit-user-select: text;
cursor: text;
}
.oc .config-editor-title .config-file-name {
color: var(--primary-color);
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: bottom;
padding: 0;
}
.oc .config-editor-actions {
display: flex;
align-items: center;
gap: 12px;
}
.oc .size-btn {
width: 24px !important;
height: 24px !important;
min-width: 24px !important;
padding: 0 !important;
}
.oc .size-btn svg {
width: 12px !important;
height: 12px !important;
}
#config-mergeview-container {
width: 100%;
height: 100%;
display: block;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-white);
z-index: 2;
}
.oc .config-editor-content {
flex: 1;
position: relative;
overflow: hidden;
border-bottom: 1px solid var(--border-light);
border-top: 1px solid var(--border-light);
}
.oc .config-editor-loading {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background: var(--bg-white);
color: var(--text-secondary);
font-size: 14px;
}
.oc .loading-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-light);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.oc .config-editor-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--bg-gray);
flex-shrink: 0;
position: relative;
}
.oc .config-editor-status {
font-size: 12px;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 50%;
}
.oc .config-editor-help {
font-size: 11px;
color: var(--text-secondary);
opacity: 0.8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 50%;
}
.oc .config-editor-resize-handle {
position: absolute;
bottom: 0; right: 0;
width: 20px; height: 20px;
cursor: nw-resize;
background: linear-gradient(-45deg,
transparent 0%,
transparent 40%,
var(--border-color) 40%,
var(--border-color) 45%,
transparent 45%,
transparent 50%,
var(--border-color) 50%,
var(--border-color) 55%,
transparent 55%,
transparent 60%,
var(--border-color) 60%,
var(--border-color) 65%,
transparent 65%);
}
.oc #config-editor-textarea {
width: 100%;
height: 100%;
border: none;
outline: none;
resize: none;
font-size: 14px;
line-height: 1.5;
padding: 12px;
background: var(--bg-white);
color: var(--text-primary);
}
.oc .config-editor-model .CodeMirror {
height: 100%;
font-size: 14px;
line-height: 1.5;
}
.oc #config-mode-tabs {
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
background: transparent;
border: none;
box-shadow: none;
border-radius: var(--radius-md);
}
.oc #config-mode-tabs .mode-tabs {
display: flex;
width: 100%;
background: var(--bg-gray);
border-radius: var(--radius-md);
padding: 4px;
gap: 4px;
margin: 0 auto;
}
.oc #config-mode-tabs .mode-tab {
flex: 1 1 0;
min-width: 0;
width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 0;
border: none;
border-radius: calc(var(--radius-md) - 2px);
background: transparent;
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
box-sizing: border-box;
}
.oc #config-mode-tabs .mode-tab:hover {
color: var(--text-primary);
background: rgba(59, 130, 246, 0.1);
}
.oc #config-mode-tabs .mode-tab.active {
background: var(--primary-color);
color: white;
box-shadow: var(--shadow-sm);
}
.overwrite-banner {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
padding: 10px 20px;
background: rgba(255,80,80,0.12);
font-size: 14px;
text-align: center;
}
.overwrite-banner svg {
flex-shrink: 0;
display: block;
}
.overwrite-banner span {
flex: unset;
text-align: center;
display: inline-block;
color: var(--error-color);
line-height: 1.5;
vertical-align: middle;
}
.oc .config-editor-model .CodeMirror.zoom-75 { font-size: 10.5px; }
.oc .config-editor-model .CodeMirror.zoom-90 { font-size: 12.6px; }
.oc .config-editor-model .CodeMirror.zoom-110 { font-size: 15.4px; }
.oc .config-editor-model .CodeMirror.zoom-125 { font-size: 17.5px; }
.oc .config-editor-model .CodeMirror.zoom-150 { font-size: 21px; }
.oc .config-editor-model .CodeMirror.zoom-200 { font-size: 28px; }
.oc .config-editor-model .CodeMirror, .oc .config-editor-model .CodeMirror-line {
font-family: 'Twemoji Mozilla', "Microsoft Yahei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB" !important;
}
.oc .config-editor-model .CodeMirror-hints.log {
font-family: 'Twemoji Mozilla', "Open Sans", "PingFangSC-Regular", "Microsoft Yahei", "WenQuanYi Micro Hei", "Helvetica Neue", "Helvetica", "Hiragino Sans GB", "sans-serif" !important;
}
#config-mergeview-container .CodeMirror-merge,
#config-mergeview-container .CodeMirror-merge-pane,
#config-mergeview-container .CodeMirror,
#config-mergeview-container .CodeMirror-scroll {
height: 100% !important;
min-height: 0 !important;
box-sizing: border-box;
}
#config-mergeview-container .CodeMirror-merge-gap {
height: 100% !important;
min-height: 0 !important;
}
#config-mergeview-container .CodeMirror-scroll {
overflow-y: auto !important;
overflow-x: hidden !important;
}
#config-mergeview-container .CodeMirror-merge-pane {
overflow: hidden;
}
#config-mergeview-container .CodeMirror-merge-r-chunk {
background: #0095ff2e !important;
}
#config-mergeview-container .CodeMirror-merge-r-connect {
fill: #0095ff2e !important;
stroke: #0095ff2e !important;
}
#config-mergeview-container .CodeMirror-merge {
border: none !important;
}
.oc .update-row {
display: flex;
gap: 12px;
width: 100%;
max-width: 100%;
}
.oc .update-row > .form-select-wrapper {
flex: 1 1 0;
min-width: 0;
}
.oc .update-row .form-select {
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.oc .overwrite-config-dropdown {
position: relative;
width: 100%;
}
.oc .overwrite-config-dropdown-btn {
width: 100%;
min-height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
cursor: pointer;
font-size: 13px;
line-height: 1.4;
}
.oc .overwrite-config-dropdown.form-select-wrapper {
width: 100%;
}
.oc .overwrite-config-dropdown-btn.form-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.oc .overwrite-config-dropdown-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100% - 20px);
text-align: left;
}
.oc .overwrite-config-dropdown-arrow {
margin-left: 8px;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid var(--text-secondary);
flex-shrink: 0;
}
.oc .overwrite-config-dropdown-panel {
display: none;
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 220px;
overflow-y: auto;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
box-shadow: var(--shadow-md);
z-index: 10003;
}
.oc .overwrite-config-dropdown.open .overwrite-config-dropdown-panel {
display: block;
}
.oc .overwrite-config-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-white);
color: var(--text-primary);
cursor: pointer;
}
.oc .overwrite-config-option:last-child {
border-bottom: none;
}
.oc .overwrite-config-option:hover {
background: var(--primary-color);
color: var(--select-hover);
}
.oc[data-darkmode="true"] .overwrite-config-option {
background: var(--bg-gray);
border-bottom-color: var(--border-light);
}
.oc[data-darkmode="true"] .overwrite-config-option:hover {
background: var(--primary-color);
color: var(--select-hover);
}
.oc .overwrite-config-option:hover .overwrite-config-option-state {
border-color: var(--select-hover);
}
.oc[data-darkmode="true"] .overwrite-config-option-state {
background: var(--bg-white);
border-color: var(--border-color);
}
.oc .overwrite-config-option.disabled-by-all {
cursor: not-allowed;
opacity: 0.55;
background: var(--bg-gray);
color: var(--text-secondary);
}
.oc .overwrite-config-option.disabled-by-all:hover {
background: var(--bg-gray);
color: var(--text-secondary);
}
.oc .overwrite-config-option.disabled-by-all .overwrite-config-option-state {
border-color: var(--border-light);
color: transparent;
}
.oc .overwrite-config-option-left {
display: flex;
align-items: center;
min-width: 0;
flex: 1;
}
.oc .overwrite-config-option-left input[type="checkbox"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.oc .overwrite-config-option-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
.oc .overwrite-config-option-state {
flex-shrink: 0;
width: 16px;
height: 16px;
border: 1px solid var(--border-color);
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
color: transparent;
background: var(--bg-white);
font-size: 12px;
line-height: 1;
}
.oc .overwrite-config-option.selected .overwrite-config-option-state {
border-color: var(--primary-color);
background: var(--primary-color);
color: var(--select-hover);
}
.oc .overwrite-config-dropdown.disabled .overwrite-config-dropdown-btn {
opacity: 0.6;
cursor: not-allowed;
}
.oc .overwrite-card-row {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
gap: 12px;
padding: 8px;
max-width: 100%;
min-width: 0;
align-items: stretch;
scrollbar-width: thin;
scrollbar-color: var(--primary-color) var(--bg-gray);
}
.oc .overwrite-card-row::-webkit-scrollbar {
height: 8px;
}
.oc .overwrite-card-row::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 4px;
}
.oc .overwrite-card-row::-webkit-scrollbar-track {
background: var(--bg-gray);
border-radius: 4px;
}
.oc .sub-card.overwrite-item {
width: 190px;
max-width: 220px;
flex: 0 0 auto;
box-sizing: border-box;
gap: 12px;
height: 75px;
}
.oc .sub-card.overwrite-item,
.oc .sub-card.overwrite-item:active,
.oc .sub-card.overwrite-item:focus {
-webkit-tap-highlight-color: transparent;
}
.oc .overwrite-drag-line {
display: block;
min-width: 4px;
width: 4px;
background: var(--primary-color);
border-radius: 2px;
margin: 0 2px;
align-self: stretch;
height: auto;
}
.oc .oc-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
vertical-align: middle;
}
.oc .oc-switch input {
opacity: 0;
width: 0;
height: 0;
}
.oc .oc-switch-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #ccc;
border-radius: 20px;
transition: .2s;
}
.oc .oc-switch-slider:before {
position: absolute;
content: "";
height: 16px; width: 16px;
left: 2px; bottom: 2px;
background-color: white;
border-radius: 50%;
transition: .2s;
}
.oc .oc-switch input:checked + .oc-switch-slider {
background-color: var(--primary-color, #2196F3);
}
.oc .oc-switch input:checked + .oc-switch-slider:before {
transform: translateX(16px);
}
.overwrite-add-icon {
font-size: 32px;
text-align: center;
color: var(--primary-color);
}
.overwrite-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.overwrite-title {
font-weight: bold;
font-size: 15px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 70%;
}
.overwrite-info {
font-size: 12px;
color: var(--text-secondary);
}
.overwrite-refresh-btn {
position: absolute;
right: 72px;
bottom: 8px;
}
.overwrite-gear-btn {
position: absolute;
right: 40px;
bottom: 8px;
}
.overwrite-del-btn {
position: absolute;
right: 8px;
bottom: 8px;
}
.sub-card.overwrite-item.active {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-color);
}
.sub-card.overwrite-item.dragging {
opacity: 0.5;
z-index: 1000;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
transition: none !important;
}
@media screen and (max-width: 768px) {
.oc .config-editor-model {
width: 95vw;
height: 80vh;
min-width: 320px;
}
.oc .config-editor-actions {
gap: 5px;
}
.oc .overwrite-card-row {
gap: 6px;
}
.oc .sub-card.overwrite-item {
width: 185px;
max-width: 200px;
gap: 10px;
}
.oc .overwrite-drag-line {
margin: -2px;
width: 2px;
}
}
</style>
<div class="oc">
<div class="config-editor-model-overlay" id="config-editor-overlay">
<div class="config-editor-model" id="config-editor-model">
@ -759,37 +93,6 @@
</div>
</div>
<link rel="stylesheet" href="/luci-static/resources/openclash/lib/codemirror.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/material.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/fold/foldgutter.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/lint/lint.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/display/fullscreen.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/dialog/dialog.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/search/matchesonscrollbar.css">
<script src="/luci-static/resources/openclash/lib/codemirror.js"></script>
<script src="/luci-static/resources/openclash/mode/yaml/yaml.js"></script>
<script src="/luci-static/resources/openclash/mode/shell/shell.js"></script>
<script src="/luci-static/resources/openclash/mode/properties/properties.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldcode.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldgutter.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/indent-fold.js"></script>
<script src="/luci-static/resources/openclash/addon/edit/matchbrackets.js"></script>
<script src="/luci-static/resources/openclash/addon/selection/active-line.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/yaml-lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/js-yaml.min.js"></script>
<script src="/luci-static/resources/openclash/addon/display/fullscreen.js"></script>
<script src="/luci-static/resources/openclash/addon/display/autorefresh.js"></script>
<script src="/luci-static/resources/openclash/addon/dialog/dialog.js"></script>
<script src="/luci-static/resources/openclash/addon/search/searchcursor.js"></script>
<script src="/luci-static/resources/openclash/addon/search/search.js"></script>
<script src="/luci-static/resources/openclash/addon/scroll/annotatescrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/matchesonscrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/jump-to-line.js"></script>
<script src="/luci-static/resources/openclash/addon/merge/diff_match_patch.js"></script>
<script src="/luci-static/resources/openclash/addon/merge/merge.js"></script>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/merge/merge.css">
<script type="text/javascript">
var levelTranslations = {
'info': '<%:Info%>',
@ -934,7 +237,89 @@ var ConfigEditor = {
this.makeResizable();
},
_codeMirrorLoaded: false,
_codeMirrorLoading: null,
_loadCodeMirror: function(callback) {
if (this._codeMirrorLoaded) {
if (callback) callback();
return;
}
if (this._codeMirrorLoading) {
if (callback) this._codeMirrorLoading.then(callback);
return this._codeMirrorLoading;
}
var self = this;
var basePath = '/luci-static/resources/openclash';
var head = document.head || document.getElementsByTagName('head')[0];
var cssFiles = [
'lib/codemirror.css',
'theme/material.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/shell/shell.js',
'mode/properties/properties.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));
this._codeMirrorLoading = Promise.all(promises).then(function() {
self._codeMirrorLoaded = true;
self._codeMirrorLoading = null;
});
if (callback) this._codeMirrorLoading.then(callback);
return this._codeMirrorLoading;
},
show: function(configFile) {
var self = this;
this.isOverwrite = false;
this.currentViewMode = 'original';
this.currentConfigFile = '';
@ -989,10 +374,13 @@ var ConfigEditor = {
this.hideMergeView();
this.updateModeTabs();
this.loadConfigContent();
this._loadCodeMirror(function() {
self.loadConfigContent();
});
},
showOverwrite: function() {
var self = this;
this.isOverwrite = true;
this.currentViewMode = 'original';
this.originalContent = '';
@ -1045,7 +433,9 @@ var ConfigEditor = {
}
this.hideMergeView();
this.loadConfigContent();
this._loadCodeMirror(function() {
self.loadConfigContent();
});
},
hide: function() {
@ -1231,14 +621,12 @@ var ConfigEditor = {
}
fetch(url)
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
return response.json();
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
if (data.content !== undefined) {
if (data.status === 'success' && data.content !== undefined) {
if (self.currentViewMode === 'runtime' && !self.isOverwrite) {
self.runtimeContent = data.content;
renderEditor(self.runtimeContent, "text/yaml", true, false);
@ -1250,10 +638,11 @@ var ConfigEditor = {
}
self.updateModeTabs();
} else {
throw new Error('Invalid response data');
loadingDiv.querySelector('span').textContent = '<%:Failed to load config file%>';
statusText.textContent = '<%:Load failed%>';
}
})
.catch(function(error) {
.catch(function(err) {
loadingDiv.querySelector('span').textContent = '<%:Failed to load config file%>';
statusText.textContent = '<%:Load failed%>';
});
@ -1426,11 +815,9 @@ var ConfigEditor = {
method: 'POST',
body: formData
})
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
return response.json();
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
saveBtn.disabled = false;
@ -1457,16 +844,10 @@ var ConfigEditor = {
}, 3000);
}
})
.catch(function(error) {
.catch(function(err) {
saveBtn.disabled = false;
statusText.textContent = '<%:Save failed%>';
alert('<%:Save config failed:%> ' + error.message);
setTimeout(function() {
if (statusText && statusText.textContent === '<%:Save failed%>') {
statusText.textContent = '<%:Ready%>';
}
}, 3000);
alert('<%:Failed to save config file:%> ' + err.message);
});
},

View File

@ -1,77 +1,86 @@
<style>
.CodeMirror {
text-align: left !important;
font-size: 15px;
line-height: 150%;
resize: both !important;
}
.CodeMirror-merge {
border: none !important;
}
.CodeMirror-merge-r-chunk {
background: #0095ff2e !important;
}
.CodeMirror-merge-2pane .CodeMirror-merge-gap {
height: 700px !important;
}
.CodeMirror-merge-r-connect {
fill: #0095ff2e !important;
stroke: #0095ff2e !important;
}
.CodeMirror-vscrollbar-oc {
display: none !important;
}
.CodeMirror, .CodeMirror-line {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
box-sizing: border-box;
word-break: break-all !important;
overflow-wrap: break-word !important;
font-family: 'Twemoji Mozilla', "Microsoft Yahei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB" !important;
}
.CodeMirror-hints.log {
font-family: 'Twemoji Mozilla', "Open Sans", "PingFangSC-Regular", "Microsoft Yahei", "WenQuanYi Micro Hei", "Helvetica Neue", "Helvetica", "Hiragino Sans GB", "sans-serif" !important;
}
:root[data-darkmode="true"] .CodeMirror-merge-gap {
background: #d0cfcf !important;
}
@font-face {
font-family: 'Twemoji Mozilla';
src: url('/luci-static/resources/openclash/fonts/TwemojiMozilla-flags-B12sb_Bp.woff2') format('woff2');
}
</style>
<link rel="stylesheet" href="/luci-static/resources/openclash/lib/codemirror.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/material.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/log.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/fold/foldgutter.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/lint/lint.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/display/fullscreen.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/dialog/dialog.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/search/matchesonscrollbar.css">
<script src="/luci-static/resources/openclash/lib/codemirror.js"></script>
<script src="/luci-static/resources/openclash/mode/yaml/yaml.js"></script>
<script src="/luci-static/resources/openclash/mode/log/log.js"></script>
<script src="/luci-static/resources/openclash/mode/shell/shell.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldcode.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldgutter.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/indent-fold.js"></script>
<script src="/luci-static/resources/openclash/addon/edit/matchbrackets.js"></script>
<script src="/luci-static/resources/openclash/addon/selection/active-line.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/yaml-lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/js-yaml.min.js"></script>
<script src="/luci-static/resources/openclash/addon/display/fullscreen.js"></script>
<script src="/luci-static/resources/openclash/addon/display/autorefresh.js"></script>
<script src="/luci-static/resources/openclash/addon/dialog/dialog.js"></script>
<script src="/luci-static/resources/openclash/addon/search/searchcursor.js"></script>
<script src="/luci-static/resources/openclash/addon/search/search.js"></script>
<script src="/luci-static/resources/openclash/addon/scroll/annotatescrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/matchesonscrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/jump-to-line.js"></script>
<script src="/luci-static/resources/openclash/addon/merge/diff_match_patch.js"></script>
<script src="/luci-static/resources/openclash/addon/merge/merge.js"></script>
<link rel=stylesheet href="/luci-static/resources/openclash/addon/merge/merge.css">
<%
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()
@ -103,6 +112,7 @@ local sid = path_info:match("(cfg[0-9a-f]+)") or ""
</tr>
</table>
<script src="/luci-static/resources/openclash/js/common.js"></script>
<script type="text/javascript">//<![CDATA[
var levelTranslations = {
'info': '<%:Info%>',
@ -114,43 +124,6 @@ var levelTranslations = {
'fatal': '<%:Fatal%>'
};
function isDarkBackground(element) {
var cachedTheme = localStorage.getItem('oc-theme');
if (cachedTheme === 'dark') {
return true;
} else if (cachedTheme === 'light') {
return false;
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return true;
}
var style = window.getComputedStyle(element);
var bgColor = style.backgroundColor;
let r, g, b;
if (/rgb\(/.test(bgColor)) {
var rgb = bgColor.match(/\d+/g);
r = parseInt(rgb);
g = parseInt(rgb);
b = parseInt(rgb);
} else if (/#/.test(bgColor)) {
if (bgColor.length === 4) {
r = parseInt(bgColor + bgColor, 16);
g = parseInt(bgColor + bgColor, 16);
b = parseInt(bgColor + bgColor, 16);
} else {
r = parseInt(bgColor.slice(1, 3), 16);
g = parseInt(bgColor.slice(3, 5), 16);
b = parseInt(bgColor.slice(5, 7), 16);
}
} else {
return false;
}
var luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return luminance < 128;
};
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;
@ -315,7 +288,7 @@ function shell_editor(id, readOnly, wid, height)
function other_editor(id, readOnly, wid, height)
{
var editor = CodeMirror.fromTextArea(id, {
mode: "text/yaml",
mode: "text/x-properties",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
@ -382,6 +355,7 @@ function log_editor(id, name, readOnly, wid, height)
};
};
_cmWhenReady(function() {
var core_log = document.getElementById("core_log");
var oc_log = document.getElementById("cbid.openclash.config.clog");
if (core_log && oc_log) {
@ -478,5 +452,6 @@ if (proxy_mg) {
Apply.style.textAlign="center";
Create.style.textAlign="center";
};
});
//]]>
</script>

File diff suppressed because it is too large Load Diff

View File

@ -8,66 +8,6 @@ local diag_host = "www.instagram.com"
local dns_host = "www.instagram.com"
%>
<style>
:root[data-darkmode="true"] .diag-style {
color: #ffffff;
background-color: #404040;
}
:root[data-darkmode="true"] .diag-style select, .diag-style input {
color: #ffffff;
background-color: #404040;
}
:root[data-darkmode="true"] #diag-rc-output > pre {
background-color: #404040;
color: #ffffff;
}
:root[data-darkmode="true"] #dns-rc-output > pre {
background-color: #404040;
color: #ffffff;
}
.diag-style {
color: #000;
background-color: #f5f5f5;
}
.diag-style select, .diag-style input {
color: #000;
background-color: #f5f5f5;
}
#diag-rc-output > pre {
background-color: #ffffff;
display: block;
padding: 8.5px;
margin: 0 0 18px;
line-height: 1.5rem;
-moz-border-radius: 3px;
white-space: pre-wrap;
word-wrap: break-word;
font-size: 1.4rem;
color: #404040;
}
#dns-rc-output > pre {
background-color: #ffffff;
display: block;
padding: 8.5px;
margin: 0 0 18px;
line-height: 1.5rem;
-moz-border-radius: 3px;
white-space: pre-wrap;
word-wrap: break-word;
font-size: 1.4rem;
color: #404040;
}
</style>
<script type="text/javascript">//<![CDATA[
function show_diag_info(addr)
{
@ -78,19 +18,24 @@ local dns_host = "www.instagram.com"
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 != "")
{
legend.style.display = 'none';
output.innerHTML = String.format('<pre>%h</pre>', 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)
{
legend.style.display = 'none';
output.innerHTML = '<span class="error"><%:Bad address specified!%></span>';
}
else
{
legend.style.display = 'none';
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>';
}
});
@ -109,8 +54,9 @@ local dns_host = "www.instagram.com"
'<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 = 'inline';
legend.style.display = 'block';
}
let HTTP = {
@ -152,26 +98,32 @@ local dns_host = "www.instagram.com"
'<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 = 'inline';
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 != "")
{
legend.style.display = 'none';
output.innerHTML = String.format('<pre>%h</pre>', 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)
{
legend.style.display = 'none';
output.innerHTML = '<span class="error"><%:Bad address specified!%></span>';
}
else
{
legend.style.display = 'none';
output.innerHTML = '<span class="error"><%:No Response Found!%></span>';
}
});
@ -189,18 +141,25 @@ local dns_host = "www.instagram.com"
'<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 = 'inline';
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 != "")
{
legend.style.display = 'none';
output.innerHTML = '<textarea class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="30" cols="60" readonly="readonly" >'+x.responseText+'</textarea>';
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
{
legend.style.display = 'none';
output.innerHTML = '<span class="error"><%:Some error occurred!%></span>';
}
}
@ -227,21 +186,18 @@ local dns_host = "www.instagram.com"
</div>
</fieldset>
<fieldset class="diag-style" style="display:none">
<legend id="diag-rc-legend"><%:Collecting data...%></legend>
<br />
<div class="diag-style" style="display:none">
<div id="diag-rc-legend" class="diag-legend"><%:Collecting data...%></div>
<span id="diag-rc-output"></span>
</fieldset>
</div>
<fieldset class="diag-style" style="display:none">
<legend id="dns-rc-legend"><%:Collecting data...%></legend>
<br />
<div class="diag-style" style="display:none">
<div id="dns-rc-legend" class="diag-legend"><%:Collecting data...%></div>
<span id="dns-rc-output"></span>
</fieldset>
</div>
<fieldset class="diag-style" style="display:none">
<legend id="debug-rc-legend"><%:Collecting data...%></legend>
<br />
<div class="diag-style" style="display:none">
<div id="debug-rc-legend" class="diag-legend"><%:Collecting data...%></div>
<span id="debug-rc-output"></span>
</fieldset>
</div>
</form>

File diff suppressed because one or more lines are too long

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

@ -1,261 +1,4 @@
<%+cbi/valueheader%>
<style type="text/css">
*{margin: 0;padding: 0;}
ul{
list-style: none;
}
#tab{
width: 100%;
height: 100%;
border: 1px solid #ddd;
box-shadow: 0 0 2px #ddd;
overflow: hidden;
}
#tab-header {
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 ul.cbi-tabmenu {
display: flex;
width: 100%;
background: #f1f5f9;
border-radius: 8px;
border-bottom: none !important;
border: none !important;
gap: 5px;
margin: 0;
padding: 0;
}
#tab-header ul.cbi-tabmenu li {
flex: 1 1 0;
min-width: 0;
width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 0;
height: 40px;
font-size: 15px;
font-weight: 500;
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 ul.cbi-tabmenu 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 ul.cbi-tabmenu li.cbi-tab a {
color: #fff !important;
font-weight: 600;
}
#tab-header ul.cbi-tabmenu li.cbi-tab-disabled {
color: #fff;
background: #f9f9f9;
opacity: 0.7;
cursor: pointer;
}
#tab-header ul.cbi-tabmenu li.cbi-tab-disabled a {
color: #000 !important;
}
#tab-header ul.cbi-tabmenu li:not(.cbi-tab):hover,
#tab-header ul.cbi-tabmenu li:not(.cbi-tab):focus {
color: #3b82f6;
background: rgba(59,130,246,0.10);
}
#tab-header ul.cbi-tabmenu li:not(.cbi-tab):hover a,
#tab-header ul.cbi-tabmenu li:not(.cbi-tab):focus a {
color: #3b82f6 !important;
}
#tab-header ul.cbi-tabmenu li a {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
text-align: center;
text-decoration: none;
background: none;
border: none;
outline: none;
font: inherit;
color: inherit;
cursor: inherit;
padding: 0;
margin: 0;
user-select: none;
transition: color 0.18s;
}
#tab-content .dom{
display: none;
}
#tab-content .dom ul li{
float: left;
margin: 15px 10px;
width: 225px;
}
.radio-button {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
background: #f1f5f9;
border-radius: 6px;
padding: 6px 10px;
margin: 10px auto;
width: 100%;
max-width: 500px;
flex-wrap: wrap;
box-sizing: border-box;
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05);
}
.radio-button input[type="radio"] {
display: none;
}
.radio-button label {
display: flex;
align-items: center;
justify-content: center;
padding: 6px 18px;
font-size: 15px;
color: #64748b;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
min-width: 60px;
user-select: none;
}
.radio-button label:hover {
color: #3b82f6;
border-color: #3b82f6;
background: #f0f6ff;
}
.radio-button input[type="radio"]:checked + label {
background: #3b82f6;
color: #fff;
border-color: #3b82f6;
box-shadow: 0 1px 2px 0 rgba(59,130,246,0.08);
}
.btn-group {
display: flex;
justify-content: space-between;
width: 80%;
margin: 10px auto;
}
:root[data-darkmode="true"] #tab-header {
background: #374151;
border-bottom: 1px solid #374151;
}
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu {
background: #374151;
}
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu li {
background: #1f2937;
color: #a1a1aa;
border-color: #374151;
}
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu li.cbi-tab {
background: #2563eb;
color: #fff;
}
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu li.cbi-tab a {
color: #fff !important;
}
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu li.cbi-tab-disabled {
color: #1f2937;
background: #1f2937;
opacity: 0.7;
}
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu li.cbi-tab-disabled a {
color: #fff !important;
}
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu li:not(.cbi-tab):hover,
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu li:not(.cbi-tab):focus {
color: #3b82f6;
border-color: #3b82f6;
background: #22304a;
}
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu li:not(.cbi-tab):hover a,
:root[data-darkmode="true"] #tab-header ul.cbi-tabmenu li:not(.cbi-tab):focus a {
color: #60a5fa !important;
}
:root[data-darkmode="true"] {
.radio-button {
background: #374151;
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.15);
}
.radio-button label {
background: #1f2937;
color: #d1d5db;
border-color: #4b5563;
}
.radio-button label:hover {
color: #3b82f6;
border-color: #3b82f6;
background: #22304a;
}
.radio-button input[type="radio"]:checked + label {
background: #3b82f6;
color: #fff;
border-color: #3b82f6;
box-shadow: 0 1px 2px 0 rgba(59,130,246,0.18);
}
}
</style>
<body>
<div id="tab" class="cbi-section">
<div id="tab-header">
@ -834,8 +577,8 @@ function poll_log(){
}
if (coreWebSocket && coreWebSocket.readyState == WebSocket.OPEN) {
core_refresh = false;
if (coreLogBuffer.length > 0) {
core_refresh = false;
bufferToProcess = coreLogBuffer;
coreLogBuffer = [];
core_logs = line_tolocal(bufferToProcess.join('\n'));
@ -847,6 +590,8 @@ function poll_log(){
smoothlyDisplayLogs(core_logs, cl, false, currentCoreContent, activeTabId === 1);
}
}
} else {
core_refresh = true;
}
if (isPolling) {

View File

@ -22,500 +22,6 @@
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no,minimal-ui">
<title>IP 地址查询</title>
<style>
.oc {
--card-item-padding: 12px 8px;
--card-item-min-height: 100px;
--card-item-border-radius: var(--radius-md);
}
.oc[data-darkmode="true"] #mode-icon rect[stroke="#333"] {
stroke: var(--text-primary, #fff);
}
.oc[data-darkmode="true"] #mode-icon path[stroke="#333"] {
stroke: var(--text-primary, #fff);
}
.oc[data-darkmode="true"] {
background-color: var(--bg-light, #111827);
}
.oc[data-darkmode="true"] .myip-main-card {
background: var(--bg-white, #1f2937);
border-color: var(--border-light, #374151);
}
.oc[data-darkmode="true"] .myip-ip-item,
.oc[data-darkmode="true"] .myip-check-item {
background: var(--bg-gray, #374151);
border-color: var(--border-light, #4b5563);
}
.oc[data-darkmode="true"] .myip-ip-item:hover,
.oc[data-darkmode="true"] .myip-check-item:hover {
background: var(--hover-bg, #4b5563);
border-color: var(--primary-color, #3b82f6);
}
.oc[data-darkmode="true"] .myip-section-title {
color: var(--text-primary, #f9fafb);
border-bottom-color: var(--border-light, #374151);
}
.oc[data-darkmode="true"] .ip-title,
.oc[data-darkmode="true"] .ip-state_title {
color: var(--text-title, --text-title);
}
.oc[data-darkmode="true"] .ip-title::after,
.oc[data-darkmode="true"] .ip-state_title::after {
background-color: var(--border-color, --border-color);
}
.oc[data-darkmode="true"] .ip-result {
color: var(--text-primary, #f9fafb);
}
.oc[data-darkmode="true"] .ip-geo {
color: var(--text-secondary, #d1d5db);
}
.oc[data-darkmode="true"] .myip-footer p {
color: var(--text-secondary, #d1d5db) !important;
}
.oc[data-darkmode="true"] .mode-label {
color: var(--text-secondary, #d1d5db);
}
.oc[data-darkmode="true"] .sk-text-success {
color: var(--success-color, #10b981);
}
.oc[data-darkmode="true"] .sk-text-error {
color: var(--error-color, #ef4444);
}
.oc .myip-main-card {
background: var(--bg-white, #ffffff);
margin: 0 auto;
width: 100%;
transition: all var(--transition-fast);
}
.oc .myip-content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
border: 1px solid var(--border-light, #e5e7eb);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
gap: 12px;
align-items: start;
padding: var(--card-padding);
}
.oc .myip-ip-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.oc .myip-check-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.oc .myip-section-title {
font-size: 25px;
font-weight: bold;
color: var(--text-title) !important;
margin: 8px;
padding: 0;
text-align: left;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 32px;
border-bottom: 2px solid var(--border-light, #e5e7eb);
padding-bottom: 12px;
}
.oc .myip-toolbar {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.oc .myip-ip-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin: auto 8px;
}
.oc .myip-check-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin: auto 8px;
}
.oc .myip-ip-item,
.oc .myip-check-item {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: var(--card-item-padding);
background: var(--bg-gray, #f9fafb);
border: 1px solid var(--border-light, #e5e7eb);
border-radius: var(--card-item-border-radius);
transition: all var(--transition-fast);
min-height: var(--card-item-min-height);
text-align: center;
position: relative;
max-height: 104px;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
.oc .myip-ip-item:hover,
.oc .myip-check-item:hover {
background: var(--hover-bg, #f3f4f6);
border-color: var(--primary-color, #3b82f6);
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
.oc .ip-title,
.oc .ip-state_title {
font-weight: bold;
font-size: 14px;
color: var(--text-title, --text-title);
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
white-space: nowrap;
text-align: center;
line-height: 1.2;
flex-shrink: 0;
position: relative;
padding-bottom: 4px;
}
.oc .ip-title::after,
.oc .ip-state_title::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 1px;
background-color: var(--border-color, --border-color);
}
.oc .myip-ip-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
flex: 1;
justify-content: center;
min-height: 50px;
line-height: 15px;
min-width: 0;
width: 100%;
box-sizing: border-box;
padding: 0px 6px;
}
.oc .ip-result {
font-size: 13px;
color: var(--text-primary, #111827);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
font-weight: 500;
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.oc .ip-geo {
font-size: 13px;
line-height: 16px;
color: var(--text-secondary, #6b7280);
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: normal;
width: 100%;
font-weight: 500;
}
.oc .myip-status-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
flex: 1;
justify-content: center;
min-height: 50px;
padding: 0px 6px;
}
.oc .sk-text-success,
.oc .sk-text-error {
font-size: 13px;
line-height: 18px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
width: 100%;
text-align: center;
display: inline-block;
vertical-align: bottom;
margin-bottom: 2px;
}
.oc .sk-text-success {
color: var(--success-color, #059669);
}
.oc .sk-text-error {
color: var(--error-color, #dc2626);
}
.oc .sk-load-success {
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
text-align: center;
display: inline-block;
vertical-align: bottom;
font-weight: 500;
}
.oc .myip-footer {
position: relative;
height: 24px;
padding-top: 8px;
grid-column: 1 / -1;
}
.oc .myip-footer p {
position: absolute;
right: 0% !important;
top: 0 !important;
margin: 0 !important;
font-size: 15px !important;
line-height: 20px !important;
padding: 0px 10px !important;
white-space: nowrap;
z-index: 1;
color: var(--text-secondary, #6b7280) !important;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
box-sizing: border-box;
text-align: right;
}
.oc .myip-footer a {
text-decoration: none;
color: var(--primary-color, #3b82f6);
opacity: 0.8;
transition: opacity var(--transition-fast);
font-weight: 500;
}
.oc .myip-footer a:hover {
opacity: 1;
text-decoration: underline;
}
.oc .mode-label {
font-size: 14px;
color: var(--text-secondary, #666);
vertical-align: middle;
}
.oc .mode-icon {
margin: 0 8px;
vertical-align: middle;
cursor: pointer;
}
.oc #mode-icon,
.oc #eye-icon,
.oc #data-refresh-icon {
vertical-align: bottom;
margin: 0 3px;
cursor: pointer;
}
.oc #mode-icon {
cursor: pointer;
transition: all 0.2s ease;
vertical-align: middle;
}
.oc #mode-icon:hover {
transform: scale(1.1);
}
.oc #eye-icon {
cursor: pointer;
transition: all 0.2s ease;
vertical-align: middle;
color: currentColor;
}
.oc #eye-icon:hover {
transform: scale(1.1);
}
.oc #data-refresh-icon {
cursor: pointer;
transition: all 0.2s ease;
vertical-align: middle;
color: currentColor;
margin-top: 1px;
}
.oc #data-refresh-icon:hover {
transform: scale(1.1) rotate(90deg);
}
.oc .myip-icon-btn {
cursor: pointer;
transition: all 0.2s ease;
vertical-align: middle;
color: currentColor;
}
.oc .myip-icon-btn:hover {
transform: scale(1.1);
color: var(--primary-color, #3b82f6);
}
@media screen and (max-width: 768px) {
.oc {
--card-item-padding: 8px 6px;
--card-item-min-height: 80px;
}
.oc .myip-content-grid {
grid-template-columns: 1fr;
}
.oc .myip-section-title {
font-size: 22px;
margin-bottom: 12px;
}
.oc .myip-ip-list,
.oc .myip-check-list {
grid-template-columns: repeat(2, 1fr);
}
.oc .ip-title,
.oc .ip-state_title {
font-size: 13px;
}
.oc .ip-title::after,
.oc .ip-state_title::after {
width: 25px;
}
.oc .ip-result {
font-size: 12px;
}
.oc .ip-geo {
font-size: 12px;
}
.oc .sk-text-success,
.oc .sk-text-error {
font-size: 12px;
}
.oc .sk-load-success {
font-size: 12px;
}
.oc .myip-footer p {
font-size: 13px !important;
line-height: 18px !important;
}
}
@media screen and (max-width: 575px) {
.oc {
--card-item-padding: 6px 4px;
--card-item-min-height: 70px;
}
.oc .myip-content-grid {
padding: 2px;
}
.oc .myip-section-title {
font-size: 20px;
margin-bottom: 10px;
}
.oc .ip-title,
.oc .ip-state_title {
font-size: 12px;
margin-bottom: 6px;
}
.oc .ip-title::after,
.oc .ip-state_title::after {
width: 20px;
}
.oc .ip-result {
font-size: 11px;
}
.oc .ip-geo {
font-size: 11px;
}
.oc .sk-text-success,
.oc .sk-text-error {
font-size: 11px;
}
.oc .sk-load-success {
font-size: 11px;
}
.oc .myip-footer p {
font-size: 11px !important;
line-height: 16px !important;
}
}
</style>
</head>
<fieldset class="cbi-section">
@ -753,7 +259,7 @@
getIpipIP: () => {
IP.get(`http://myip.ipip.net?z=${random}`, 'text')
.then(resp => {
const ipMatch = resp.data.match(/当前 IP([0-9.]+)/);
const ipMatch = resp.data.match(/当前 IP([0-9A-Fa-f:.]+)/);
const geoMatch = resp.data.match(/来自于:(.+)/);
if (ipMatch && geoMatch) {

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

@ -1,555 +1,3 @@
<style type="text/css">
.oc {
--bg-white: #ffffff;
--bg-light: #f8fafc;
--bg-gray: #f1f5f9;
--text-primary: #374151;
--text-secondary: #64748b;
--text-title: #4d4d4d;
--select-hover: #ebebeb;
--border-color: #b1b1b1;
--border-light: #e2e8f0;
--hover-bg: #f8fafc;
--primary-color: #3b82f6;
--success-color: #059669;
--success-dark: #047857;
--warning-color: #f59e0b;
--error-color: #dc2626;
--tip-color: #f9bb51;
--info-color: #2563eb;
--watchdog-color: #b300ff;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--radius-sm: 6px;
--radius-md: 6px;
--radius-lg: 8px;
--transition-fast: 0.15s ease;
--transition-normal: 0.3s ease;
--control-height: 36px;
--card-padding: 6px 8px;
--version-right: 8px;
--gap-size: 16px;
}
.oc[data-darkmode="true"] {
--bg-white: #1f2937;
--bg-light: #374151;
--bg-gray: #4b5563;
--text-primary: #ebebeb;
--text-secondary: #d0cfcf;
--text-title: #e5e7eb;
--select-hover: #ebebeb;
--border-color: #939393;
--border-light: #6b7280;
--hover-bg: #374151;
--primary-color: #3b82f6;
--success-color: #34d399;
--success-dark: #10b981;
--error-color: #ff7070;
--tip-color: #f9bb51;
--warning-color: #fa41c8;
--watchdog-color: #c147f5;
--info-color: #2563eb;
}
.oc * {
box-sizing: border-box;
}
.oc {
font-family: var(--font-family-base);
line-height: 1.6;
color: var(--text-primary);
background-color: var(--bg-light);
border-radius: var(--radius-lg);
}
.oc .select-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-white);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
padding: 24px;
z-index: 1000;
max-width: 90%;
min-width: 300px;
width: 60%;
box-shadow: var(--shadow-md);
transition: all var(--transition-normal);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.oc .close-btn {
background: none;
border: 1px solid var(--border-light);
color: var(--text-secondary);
cursor: pointer;
width: 24px !important;
height: 24px !important;
min-width: 24px !important;
padding: 0 !important;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-fast);
line-height: 1;
margin-left: 8px;
}
.oc .close-btn:hover {
background: var(--hover-bg) !important;
border-color: var(--primary-color) !important;
color: var(--primary-color) !important;
transform: translateY(-1px) !important;
box-shadow: var(--shadow-sm) !important;
}
.oc .close-btn:focus {
outline: none;
box-shadow: var(--shadow-sm) !important;
border-color: var(--primary-color) !important;
}
.oc .close-btn svg {
width: 14px !important;
height: 14px !important;
flex-shrink: 0 !important;
}
.oc .select-popup.hidden {
display: none;
}
.oc .select-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-gray);
flex-shrink: 0;
margin-bottom: 20px;
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.oc .select-config-section {
background: var(--bg-gray);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
padding: var(--card-padding);
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
}
.oc .config-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--gap-size);
align-items: center;
}
.oc .config-item {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
width: 100%;
position: relative;
}
.oc .config-item::after {
content: '';
position: absolute;
right: 12px;
bottom: 20px;
transform: translateY(50%);
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid var(--text-secondary);
pointer-events: none;
z-index: 1;
}
.oc .config-editor-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
text-align: center;
flex: 1;
}
.oc .config-label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-align: center;
position: relative;
}
.oc .config-label::after {
content: '';
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 1px;
background-color: var(--border-color);
}
.oc .select-class {
width: 100% !important;
height: var(--control-height);
padding: 8px 32px 8px 12px;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white) !important;
color: var(--text-primary) !important;
font-size: 13px;
appearance: none;
cursor: pointer;
transition: all var(--transition-fast);
box-sizing: border-box;
min-width: 0;
}
.oc .select-class:hover {
border-color: var(--primary-color);
}
.oc .select-class:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.oc .help-link {
display: inline-flex;
align-items: center;
color: var(--primary-color);
text-decoration: none;
opacity: 0.7;
transition: opacity var(--transition-fast);
margin-bottom: 2px;
border-radius: var(--radius-sm);
}
.oc .select-class option {
width: 100%;
padding: 8px 12px;
font-size: 13px;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background: var(--bg-white);
color: var(--text-primary);
}
.oc .help-link:hover {
opacity: 1;
background: rgba(59, 130, 246, 0.1);
}
.oc .select-popup-body {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
background: var(--bg-gray);
}
.oc .select-option {
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid var(--border-light);
transition: all var(--transition-fast);
font-size: 14px;
color: var(--text-primary);
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
}
.oc .select-option span {
white-space: normal;
word-break: break-all;
}
.oc .select-option:last-child {
border-bottom: none;
}
.oc .select-option:hover {
background-color: var(--hover-bg);
color: var(--primary-color);
}
.oc .select-option[data-value="custom"] {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(99, 102, 241, 0.1));
border-color: rgba(59, 130, 246, 0.2);
font-weight: 500;
}
.oc .select-option[data-value="custom"]:hover {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(99, 102, 241, 0.2));
}
.oc .custom-option-input {
margin: 12px auto;
padding: 12px;
width: calc(100% - 24px) !important;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
color: var(--text-primary);
font-size: 14px;
transition: all var(--transition-fast);
}
.oc .custom-option-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.oc .custom-option-input::placeholder {
color: var(--text-secondary);
opacity: 0.7;
}
.oc #addCustomOption {
background: var(--primary-color);
color: white;
font-weight: 500;
border-bottom: none !important;
margin: 0 12px 12px 12px;
border-radius: var(--radius-sm);
text-align: center;
}
.oc #addCustomOption:hover {
background: #2563eb;
color: white;
}
.oc .cdn-status {
font-size: 12px;
font-weight: 500;
padding: 4px 8px;
border-radius: var(--radius-sm);
margin-left: 8px;
white-space: nowrap !important;
}
.oc .cdn-status.fast {
background: rgba(5, 150, 105, 0.1);
color: var(--success-color);
}
.oc .cdn-status.medium {
background: rgba(245, 158, 11, 0.1);
color: var(--warning-color);
}
.oc .cdn-status.slow {
background: rgba(220, 38, 38, 0.1);
color: var(--error-color);
}
.oc .cdn-status.error {
background: rgba(220, 38, 38, 0.1);
color: var(--error-color);
}
@media screen and (max-width: 1200px) {
.oc .config-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.oc .config-item {
width: 100%;
max-width: 100%;
}
.oc .select-class {
max-width: 100%;
font-size: 13px;
}
}
@media screen and (max-width: 768px) {
.oc .select-popup {
min-width: 80%;
width: 80%;
padding: 15px;
max-height: 90vh;
overflow-y: auto;
}
.oc .config-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.oc .config-item {
text-align: center;
}
.oc .select-popup-header {
font-size: 16px;
margin-bottom: 16px;
padding-bottom: 12px;
}
.oc .select-config-section {
padding: 12px;
margin-bottom: 16px;
}
.oc .select-popup-body {
max-height: 200px;
}
.oc .select-option {
padding: 10px 12px;
font-size: 13px;
align-items: flex-start;
gap: 4px;
}
.oc .custom-option-input {
font-size: 13px;
padding: 10px;
}
}
@media screen and (max-width: 575px) {
.oc .select-popup {
min-width: 80%;
width: 80%;
padding: 12px;
}
.oc .select-popup-header {
font-size: 15px;
margin-bottom: 12px;
}
.oc .select-config-section {
padding: 10px;
}
.oc .config-label {
font-size: 12px;
}
.oc .select-class {
font-size: 12px;
padding: 6px 28px 6px 10px;
}
.oc .config-item::after {
right: 10px;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 3px solid var(--text-secondary);
}
.oc .select-option {
padding: 8px 10px;
font-size: 12px;
}
}
.oc[data-darkmode="true"] .select-popup {
background-color: var(--bg-white);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] .select-option:hover {
background-color: var(--hover-bg);
color: var(--primary-color);
}
.oc[data-darkmode="true"] .select-class {
background: var(--bg-white) !important;
border-color: var(--border-light) !important;
color: var(--text-primary) !important;
}
.oc[data-darkmode="true"] .select-class:hover {
border-color: var(--primary-color);
}
.oc[data-darkmode="true"] .select-class:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.oc[data-darkmode="true"] .select-class option {
background: var(--bg-white) !important;
color: var(--text-primary) !important;
}
.oc[data-darkmode="true"] .config-item::after {
border-top-color: var(--text-secondary);
}
.oc[data-darkmode="true"] .custom-option-input {
background: var(--bg-white);
border-color: var(--border-light);
color: var(--text-primary);
}
.oc[data-darkmode="true"] .custom-option-input::placeholder {
color: var(--text-secondary);
}
.oc[data-darkmode="true"] .select-config-section {
background: var(--bg-gray);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] .select-popup-body {
background: var(--bg-gray);
border-color: var(--border-light);
}
.oc[data-darkmode="true"] #addCustomOption {
background: var(--primary-color);
}
.oc[data-darkmode="true"] #addCustomOption:hover {
background: #2563eb;
}
.oc[data-darkmode="true"] .close-btn {
color: var(--text-secondary);
}
.oc[data-darkmode="true"] .close-btn:hover {
background-color: var(--hover-bg);
color: var(--error-color);
}
</style>
<body>
<div class="oc">
<div id="selectPopup" class="select-popup hidden">
@ -693,7 +141,7 @@
release_branch_cdn.value = "master";
}
if ( status.smart_enable && status.smart_enable != "" ) {
smart_enable_cdn.value = status.smart_enable;
smart_enable_cdn.value = status.smart_enable;
}
else {
smart_enable_cdn.value = "0";

File diff suppressed because it is too large Load Diff

View File

@ -1,567 +1,4 @@
<%+cbi/valueheader%>
<style>
.sub_div {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
width: calc(100% - 24px);
box-sizing: border-box;
min-width: 280px;
max-width: 350px;
margin: 0 auto;
}
.sub_div > span:first-child {
flex: 1;
min-width: 0;
}
.sub_tab {
display: block;
white-space: nowrap;
font-size: 12px;
margin: 0;
opacity: 0;
width: 100%;
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
}
.sub_tab_show {
display: block;
white-space: nowrap;
font-size: 12px;
margin: 0;
-webkit-transition: all 1s;
-moz-transition: all 1s;
-ms-transition: all 1s;
-o-transition: all 1s;
transition: all 1s;
opacity: 1;
width: 100%;
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
}
.sub_setting {
display: inline-block;
white-space: nowrap;
margin: 0;
opacity: 1;
vertical-align: middle;
line-height: 0;
flex-shrink: 0;
cursor: pointer;
background-color: unset;
border: unset;
}
.sub_setting svg {
vertical-align: middle;
display: inline-block;
width: 18px;
height: 18px;
color: #414c5c;
padding-left: 2px;
}
.sub_setting:hover svg {
opacity: 0.7;
}
.text_show {
color: #333333;
line-height: 1.2;
}
.progress_bar_bg {
border: 1px solid #999999;
background-color: #f5f5f5;
width: 100% !important;
max-width: 100% !important;
min-width: 220px;
box-sizing: border-box;
border-radius: 6px;
height: 36px;
}
.progress_bar_high {
background-color: #9edd9e;
}
.progress_bar_medium {
background-color: #ffc99f;
}
.progress_bar_low {
background-color: #ffb9b9;
}
.sub-info-url-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.sub-info-url-model {
background: var(--bg-white, white);
border-radius: var(--radius-lg, 8px);
box-shadow: var(--shadow-md, 0 4px 12px rgba(0,0,0,0.15));
width: 90vw;
max-width: 550px;
min-width: 400px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border-light, #e5e7eb);
max-height: 85vh;
transition: all var(--transition-fast, 0.2s);
}
.sub-info-url-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid var(--border-light, #e5e7eb);
background: var(--bg-gray, #f9fafb);
flex-shrink: 0;
}
.sub-info-url-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #1f2937);
}
.sub-info-url-footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 20px;
border-top: 1px solid var(--border-light, #e5e7eb);
background: var(--bg-gray, #f9fafb);
flex-shrink: 0;
gap: 12px;
}
.sub-info-url-content {
padding: 24px;
flex: 1;
overflow-y: auto;
}
.sub-info-url-form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.sub-info-url-form-group label {
font-size: 14px;
font-weight: 500;
color: var(--text-primary, #1f2937);
}
.sub-info-url-form-textarea {
width: 100%;
min-height: 120px;
padding: 10px 12px;
border: 1px solid var(--border-light, #e5e7eb);
border-radius: var(--radius-sm, 4px);
background: var(--bg-white, white);
color: var(--text-primary, #1f2937);
font-size: 14px;
font-family: inherit;
resize: vertical;
transition: all var(--transition-fast, 0.2s);
box-sizing: border-box;
}
.sub-info-url-form-textarea:focus {
outline: none;
border-color: var(--primary-color, #3b82f6);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.sub-info-url-form-textarea::placeholder {
color: var(--text-secondary, #6b7280);
}
.sub-info-url-btn {
padding: 8px 16px;
border: 1px solid var(--border-light, #e5e7eb);
border-radius: var(--radius-sm, 4px);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast, 0.2s);
white-space: nowrap;
}
.sub-info-url-cancel-btn {
background: var(--bg-white, white);
color: var(--text-secondary, #6b7280);
border-color: var(--border-light, #e5e7eb);
}
.sub-info-url-cancel-btn:hover {
background: var(--hover-bg, #f3f4f6);
color: var(--text-primary, #1f2937);
}
.sub-info-url-submit-btn {
background: var(--primary-color, #3b82f6);
color: white;
border-color: var(--primary-color, #3b82f6);
}
.sub-info-url-submit-btn:hover:not(:disabled) {
background: var(--primary-color, #3b82f6);
opacity: 0.9;
}
.sub-info-url-submit-btn:disabled {
background: var(--bg-gray, #f9fafb);
color: var(--text-secondary, #6b7280);
border-color: var(--border-light, #e5e7eb);
cursor: not-allowed;
}
:root[data-darkmode="true"] .sub_setting svg {
color: #e0e0e0;
}
:root[data-darkmode="true"] .text_show {
color: #e0e0e0;
line-height: 1.2;
}
:root[data-darkmode="true"] .progress_bar_bg {
border: 1px solid #666666;
background-color: #333333;
}
:root[data-darkmode="true"] .progress_bar_high {
background-color: #5da05d;
}
:root[data-darkmode="true"] .progress_bar_medium {
background-color: #cc8550;
}
:root[data-darkmode="true"] .progress_bar_low {
background-color: #cc6262;
}
:root[data-darkmode="true"] .sub-info-url-model {
background: var(--bg-white, #1f2937);
border-color: var(--border-light, #374151);
}
:root[data-darkmode="true"] .sub-info-url-header,
:root[data-darkmode="true"] .sub-info-url-footer {
background: var(--bg-gray, #111827);
border-color: var(--border-light, #374151);
}
:root[data-darkmode="true"] .sub-info-url-title {
color: var(--text-primary, #f3f4f6);
}
:root[data-darkmode="true"] .sub-info-url-form-group label {
color: var(--text-primary, #f3f4f6);
}
:root[data-darkmode="true"] .sub-info-url-form-textarea {
background: var(--bg-gray, #374151);
border-color: var(--border-light, #4b5563);
color: var(--text-primary, #f3f4f6);
}
:root[data-darkmode="true"] .sub-info-url-form-textarea:focus {
border-color: var(--primary-color, #60a5fa);
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.1);
}
:root[data-darkmode="true"] .sub-info-url-form-textarea::placeholder {
color: var(--text-secondary, #9ca3af);
}
:root[data-darkmode="true"] .sub-info-url-cancel-btn {
background: var(--bg-white, #374151);
color: var(--text-secondary, #9ca3af);
border-color: var(--border-light, #4b5563);
}
:root[data-darkmode="true"] .sub-info-url-cancel-btn:hover {
background: var(--hover-bg, #4b5563);
color: var(--text-primary, #f3f4f6);
}
:root[data-darkmode="true"] .sub-info-url-submit-btn {
background: var(--primary-color, #3b82f6);
border-color: var(--primary-color, #3b82f6);
}
:root[data-darkmode="true"] .sub-info-url-submit-btn:disabled {
background: var(--bg-gray, #4b5563);
color: var(--text-secondary, #9ca3af);
border-color: var(--border-light, #374151);
}
@media (max-width: 1024px) {
.sub_tab, .sub_tab_show {
font-size: 11px;
}
.progress_bar_bg {
min-width: 220px;
}
.sub_div {
gap: 6px;
width: calc(100% - 20px);
margin: 0 10px;
}
}
@media (max-width: 768px) {
.sub_tab, .sub_tab_show {
font-size: 10px;
}
.progress_bar_bg {
min-width: 200px;
}
.sub_div {
gap: 4px;
width: calc(100% - 16px);
margin: 0 8px;
}
}
@media (max-width: 480px) {
.sub_tab, .sub_tab_show {
font-size: 9px;
}
.progress_bar_bg {
min-width: 180px !important;
}
.sub_div {
gap: 2px;
width: calc(100% - 12px);
margin: 0 6px;
}
}
@media (max-width: 320px) {
.sub_tab, .sub_tab_show {
font-size: 8px;
}
.progress_bar_bg {
min-width: 160px !important;
}
.sub_div {
width: calc(100% - 8px);
margin: 0 4px;
}
}
/* Multiple providers container */
.sub_providers_container {
display: flex;
gap: 4px;
flex: 1;
min-width: 0;
overflow-x: auto;
scrollbar-width: thin;
cursor: grab;
user-select: none;
}
.sub_providers_container.no-scroll {
overflow-x: hidden;
cursor: default;
}
.sub_providers_container::-webkit-scrollbar {
height: 4px;
}
.sub_providers_container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 2px;
}
.sub_providers_container::-webkit-scrollbar-thumb {
background: #888;
border-radius: 2px;
}
.sub_providers_container::-webkit-scrollbar-thumb:hover {
background: #555;
}
.sub_providers_container.dragging {
cursor: grabbing;
}
.sub_provider_item {
flex: 0 0 auto;
width: 100px;
position: relative;
min-height: 36px;
background-color: #f5f5f5;
border: 1px solid #999999;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
gap: 2px;
}
.sub_provider_fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
transition: width 0.3s ease;
border-radius: 5px;
}
.sub_provider_name {
position: relative;
z-index: 1;
font-size: 12px;
color: #333;
font-weight: 500;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
padding: 0 4px;
line-height: 1;
}
.sub_provider_percent {
position: relative;
z-index: 1;
font-size: 12px;
color: #333;
font-weight: 400;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
padding: 0 4px;
line-height: 1;
}
/* Dark mode support */
:root[data-darkmode="true"] .sub_provider_item {
background-color: #333333;
border-color: #666666;
}
:root[data-darkmode="true"] .sub_provider_name {
color: #e0e0e0;
}
:root[data-darkmode="true"] .sub_provider_percent {
color: #e0e0e0;
}
/* Responsive adjustments for multiple providers */
@media (max-width: 1024px) {
.sub_providers_container {
gap: 3px;
}
.sub_provider_item {
width: 90px;
}
.sub_provider_name {
font-size: 11px;
}
.sub_provider_percent {
font-size: 11px;
}
}
@media (max-width: 768px) {
.sub_providers_container {
gap: 2px;
}
.sub_provider_item {
width: 80px;
min-height: 32px;
}
.sub_provider_name {
font-size: 10px;
}
.sub_provider_percent {
font-size: 10px;
}
}
@media (max-width: 480px) {
.sub_provider_item {
width: 70px;
min-height: 30px;
padding: 0 2px;
}
.sub_provider_name {
font-size: 9px;
}
.sub_provider_percent {
font-size: 9px;
}
}
@media (max-width: 320px) {
.sub_provider_item {
width: 60px;
min-height: 28px;
}
.sub_provider_name {
font-size: 8px;
}
.sub_provider_percent {
font-size: 8px;
}
}
</style>
<%
local fs = require "luci.openclash"
local val = self:cfgvalue(section)
@ -851,48 +288,69 @@ function set_subinfo_url_<%=idname%>(btn, filename) {
}
var dialogDiv = document.createElement('div');
dialogDiv.className = 'sub-info-url-overlay';
dialogDiv.className = 'config-upload-model-overlay show';
var contentDiv = document.createElement('div');
contentDiv.className = 'sub-info-url-model';
contentDiv.className = 'config-upload-model';
var headerDiv = document.createElement('div');
headerDiv.className = 'sub-info-url-header';
headerDiv.className = 'config-upload-header';
var titleDiv = document.createElement('div');
titleDiv.className = 'sub-info-url-title';
titleDiv.innerHTML = '<%:Paste the new url of subscribe infos sources here:%>';
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 = 'sub-info-url-content';
bodyDiv.className = 'config-upload-content';
var formGroup = document.createElement('div');
formGroup.className = 'sub-info-url-form-group';
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 = 'sub-info-url-form-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 = 'sub-info-url-footer';
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 = 'sub-info-url-btn sub-info-url-cancel-btn';
cancelBtn.className = 'btn cancel-btn';
cancelBtn.type = 'button';
cancelBtn.innerHTML = '<%:Cancel%>';
var confirmBtn = document.createElement('button');
confirmBtn.className = 'sub-info-url-btn sub-info-url-submit-btn';
confirmBtn.className = 'btn upload-btn';
confirmBtn.type = 'button';
confirmBtn.innerHTML = '<%:Submit%>';
confirmBtn.onclick = function() {
var new_url = textarea.value.trim();
document.body.removeChild(dialogDiv);
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") {
@ -908,17 +366,22 @@ function set_subinfo_url_<%=idname%>(btn, filename) {
};
cancelBtn.onclick = function() {
document.body.removeChild(dialogDiv);
document.body.removeChild(portal);
};
footerDiv.appendChild(cancelBtn);
footerDiv.appendChild(confirmBtn);
buttonsDiv.appendChild(cancelBtn);
buttonsDiv.appendChild(confirmBtn);
footerDiv.appendChild(buttonsDiv);
contentDiv.appendChild(headerDiv);
contentDiv.appendChild(bodyDiv);
contentDiv.appendChild(footerDiv);
dialogDiv.appendChild(contentDiv);
document.body.appendChild(dialogDiv);
var portal = document.createElement('div');
portal.className = 'oc';
portal.appendChild(dialogDiv);
document.body.appendChild(portal);
textarea.focus();

View File

@ -446,49 +446,13 @@ local sectiontype = "_"..self.config.."_"..string.match(self.sectiontype, "[%w_]
</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 isDarkBackground(element) {
var cachedTheme = localStorage.getItem('oc-theme');
if (cachedTheme === 'dark') {
return true;
} else if (cachedTheme === 'light') {
return false;
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return true;
}
var style = window.getComputedStyle(element);
var bgColor = style.backgroundColor;
let r, g, b;
if (/rgb\(/.test(bgColor)) {
var rgb = bgColor.match(/\d+/g);
r = parseInt(rgb);
g = parseInt(rgb);
b = parseInt(rgb);
} else if (/#/.test(bgColor)) {
if (bgColor.length === 4) {
r = parseInt(bgColor + bgColor, 16);
g = parseInt(bgColor + bgColor, 16);
b = parseInt(bgColor + bgColor, 16);
} else {
r = parseInt(bgColor.slice(1, 3), 16);
g = parseInt(bgColor.slice(3, 5), 16);
b = parseInt(bgColor.slice(5, 7), 16);
}
} else {
return false;
}
var luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return luminance < 128;
};
function switch_to_tab<%=sectiontype%>(tab_name){
if(titles<%=sectiontype%>.length != divs<%=sectiontype%>.length) return;

View File

@ -1,34 +1,9 @@
<%
local fs = require "luci.openclash"
local plugin_version = fs.oc_version()
%>
<head>
<style>
.tabs > li {
vertical-align: middle !important;;
}
.tool_label:hover {
border-color: rgba(0, 0, 0, 0) !important;;
background-color: unset !important;;
color: unset !important;;
cursor: unset !important;;
}
.tool_label::after {
border-color: rgba(0, 0, 0, 0) !important;;
background-color: unset !important;;
color: unset !important;;
cursor: unset !important;;
content: unset !important;;
}
.tool_label_span {
display: inline-block;
white-space: nowrap;
}
.tool_label_select {
width: auto;
}
</style>
<link rel="stylesheet" href="/luci-static/resources/openclash/css/oc.css?v=<%=plugin_version%>">
</head>
<li id="tool_label" class="tool_label">
@ -41,6 +16,7 @@
</span>
</li>
<script src="/luci-static/resources/openclash/js/common.js"></script>
<script type="text/javascript">//<![CDATA[
var config_name = document.getElementById('cfg_name');
@ -163,11 +139,4 @@
return false;
}
function winOpen(url)
{
var winOpen = window.open(url);
if(winOpen == null || typeof(winOpen) == 'undefined'){
window.location.href=url;
}
}
//]]></script>

View File

@ -1,9 +1,3 @@
<style>
.cbi-input-select {
width: auto !important;
}
</style>
<fieldset class="cbi-section">
<table width="100%">
<tr>

View File

@ -101,6 +101,9 @@ msgstr "Registros de depuración"
msgid "Generate Logs"
msgstr "Generar registros"
msgid "Generated Logs"
msgstr "Registros generados"
msgid "Click to Generate"
msgstr "Haz clic para generar"
@ -326,13 +329,62 @@ msgstr "Archivo del núcleo"
msgid "Upload"
msgstr "Subir"
msgid "Age Encryption"
msgstr "Cifrado Age"
msgid "Age Encryption File"
msgstr "Archivo de cifrado Age"
msgid "Generate Age Key"
msgstr "Generar clave Age"
msgid "Generating..."
msgstr "Generando..."
msgid "Public Key"
msgstr "Clave pública"
msgid "Secret Key"
msgstr "Clave secreta"
msgid "Age Key Type"
msgstr "Tipo de clave"
msgid "x25519"
msgstr "x25519"
msgid "PQ (mlkem768-x25519)"
msgstr "PQ (mlkem768-x25519)"
msgid "Failed to generate age key"
msgstr "Error al generar la clave Age"
msgid "No public key"
msgstr "No hay clave pública"
msgid "No secret key"
msgstr "No hay clave secreta"
msgid "Age Encryption For Config, Click For More:"
msgstr "Cifrado Age para la configuración. Haz clic aquí para obtener más información:"
msgid "Age Encryption Introduce"
msgstr "Presentación del cifrado Age"
msgid "Calculate Public Key"
msgstr "Calcular clave pública"
msgid "Please enter the Age Secret Key to calculate the public key!"
msgstr "¡Introduce la clave secreta de Age para calcular la clave pública!"
msgid "Failed to calculate public key"
msgstr "No se ha podido calcular la clave pública"
msgid "Add"
msgstr "Añadir"
msgid "File saved to"
msgstr "Archivo guardado en"
msgid "upload file error."
msgstr "Archivo guardado en"msgid "upload file error."
msgstr "Error al subir el archivo."
msgid "No specify upload file."
@ -1139,6 +1191,12 @@ msgstr "Restaurando..."
msgid "Saving..."
msgstr "Guardando..."
msgid "Save successful"
msgstr "Guardado con éxito"
msgid "Failed to save age config"
msgstr "Error al guardar la configuración de age"
msgid "Firewall Rules Reset Failed"
msgstr "Error al restablecer las reglas del firewall"
@ -1436,6 +1494,18 @@ msgstr "Iniciando descarga"
msgid "Download successful, start pre update test..."
msgstr "Descarga completa, iniciando pruebas previas a la actualización..."
msgid "opkg update failed or timed out, retrying..."
msgstr "opkg update falló o expiró, reintentando..."
msgid "opkg update failed, trying pre update test..."
msgstr "opkg update falló, se sigue intentando la prueba previa..."
msgid "apk update failed or timed out, retrying..."
msgstr "apk update falló o expiró, reintentando..."
msgid "apk update failed, trying pre update test..."
msgstr "apk update falló, se sigue intentando la prueba previa..."
msgid "Pre update test failed..."
msgstr "Las pruebas previas han fallado..."
@ -1712,7 +1782,7 @@ msgstr "La base de datos GEOSITE no está instalada; iniciando descarga..."
msgid "Detected that the GEOIP Dat is not Installed, Ready to Download..."
msgstr "La base de datos GEOIP Dat no está instalada; iniciando descarga..."
msgid "Detected that the Core is not Installed, Ready to Download..."
msgid "Core is not Detected installed, Ready to Download..."
msgstr "El núcleo no está instalado; iniciando descarga..."
msgid "Detected that the Chnroute Cidr is not Installed, Ready to Download..."
@ -1973,6 +2043,9 @@ msgstr "Hacer prueba"
msgid "Connection Test"
msgstr "Prueba de conexión"
msgid "Connection Test Result"
msgstr "Resultado de la prueba de conexión"
msgid "Backup File"
msgstr "Archivo de copia de seguridad"
@ -2066,8 +2139,8 @@ msgstr "Filtro de regiones"
msgid "Unlock Nodes Filter"
msgstr "Filtro de nodos desbloqueados"
msgid "Start Auto Select Unlock Proxy For"
msgstr "Iniciando selección automática de nodo"
msgid "Start Auto Select Unlock Proxy For..."
msgstr "Iniciando selección automática de nodo..."
msgid "Group:"
msgstr "Grupo:"
@ -2237,6 +2310,15 @@ msgstr "Limpiar"
msgid "Flush DNS Cache"
msgstr "Vaciar caché DNS"
msgid "Dnsmasq Fix"
msgstr "Reparar Dnsmasq"
msgid "If DNS is abnormal after stopping the OpenClash, please try to fix"
msgstr "Si el DNS es anormal después de detener OpenClash, intente repararlo"
msgid "Fix"
msgstr "Reparar"
msgid "No Specify Upload File"
msgstr "No se ha seleccionado ningún archivo"
@ -2645,6 +2727,9 @@ msgstr "Restaurar"
msgid "DNS Test"
msgstr "Prueba DNS"
msgid "DNS Test Result"
msgstr "Resultado de la prueba DNS"
msgid "No Response Found!"
msgstr "No se ha obtenido respuesta."
@ -2753,12 +2838,24 @@ msgstr "Cambia el método de cálculo de latencia para eliminar tiempos extra co
msgid "If the download fails, try setting the CDN in Overwrite Settings - General Settings - Github Address Modify Options"
msgstr "Si la descarga falla, prueba a configurar un CDN en Sobrescritura → General → Modificar direcciones de Github."
msgid "Used for Downloading Subscriptions, Defaults to Clash"
msgstr "User-Agent usado para descargar suscripciones (por defecto: Clash)"
msgid "Used for Downloading Subscriptions, Defaults to"
msgstr "User-Agent usado para descargar suscripciones, por defecto:"
msgid "Set Proxies Address Skip Failed,"
msgstr "Fallo al configurar la omisión de direcciones de proxy,"
msgid "Age decrypt failed,"
msgstr "Error al descifrar con Age para"
msgid "Failed to parse config file with Lua helper"
msgstr "Error al analizar el archivo de configuración con el auxiliar Lua"
msgid "File is AGE encrypted but no secret key provided"
msgstr "El archivo está cifrado con Age pero no se proporcionó clave secreta"
msgid "File is AGE encrypted, cannot parse with Lua"
msgstr "El archivo está cifrado con Age, no se puede analizar con Lua"
msgid "Flush Failed"
msgstr "Limpieza fallida"
@ -3296,9 +3393,6 @@ msgstr "Error al guardar el archivo de configuración:"
msgid "Unknown error"
msgstr "Error desconocido"
msgid "Save config failed:"
msgstr "Error al guardar configuración:"
msgid "Editor not ready"
msgstr "El editor no está listo"
@ -3449,6 +3543,12 @@ msgstr "Opciones avanzadas"
msgid "Show more subscription options"
msgstr "Mostrar más opciones de suscripción"
msgid "Show more upload options"
msgstr "Mostrar más opciones de carga"
msgid "Show more options"
msgstr "Mostrar más opciones"
msgid "Overwrite"
msgstr "Sobrescribir"
@ -3741,4 +3841,205 @@ msgid "depended package reinstalling..."
msgstr "reinstalando paquete dependiente..."
msgid "failed to install, please try to install it manually..."
msgstr "no se pudo instalar; por favor, intenta instalarlo manualmente."
msgstr "no se pudo instalar; por favor, intenta instalarlo manualmente."
msgid "Decrypted content empty or still encrypted"
msgstr "Contenido descifrado vacío o aún cifrado para"
msgid "Decrypt attempt failed:"
msgstr "Fallo en intento de descifrado:"
msgid "Decrypt attempt failed for a found secret:"
msgstr "Fallo en intento de descifrado con una clave encontrada:"
msgid "Encrypt attempt failed:"
msgstr "Fallo en intento de cifrado:"
msgid "Official Website"
msgstr "Sitio web oficial"
msgid "Login..."
msgstr "Iniciando sesión..."
msgid "oixCloud Login Successful"
msgstr "Inicio de sesión en oixCloud exitoso"
msgid "oixCloud Login Faild"
msgstr "Fallo al iniciar sesión en oixCloud"
msgid "Login Account"
msgstr "Iniciar sesión en la cuenta"
msgid "oixCloud Logout Successful"
msgstr "Cierre de sesión en oixCloud exitoso"
msgid "oixCloud Logout Faild"
msgstr "Fallo al cerrar sesión en oixCloud"
msgid "Logout Account"
msgstr "Cerrar sesión en la cuenta"
msgid "Account Login"
msgstr "Inicio de sesión de cuenta"
msgid "Account logged in"
msgstr "Cuenta iniciada"
msgid "Account not logged in"
msgstr "Cuenta no iniciada"
msgid "Logout..."
msgstr "Cerrando sesión..."
msgid "Error Login Info"
msgstr "Información de inicio de sesión incorrecta"
msgid "Simple & trustworthy"
msgstr "Simple y confiable"
msgid "Cross-platform Support"
msgstr "Amplia compatibilidad con aplicaciones de terceros"
msgid "Global Acceleration Network"
msgstr "Infraestructura de red global"
msgid "Easy to Use"
msgstr "Fácil de usar"
msgid "Intelligent Shunting"
msgstr "Derivación inteligente"
msgid "Perfect Technical Support"
msgstr "Soporte técnico completo"
msgid "Plan Expiration Time"
msgstr "Fecha de vencimiento del plan"
msgid "Account Balances"
msgstr "Saldo de la cuenta"
msgid "Aff Balances"
msgstr "Saldo de afiliado"
msgid "Account Integral"
msgstr "Puntos de la cuenta"
msgid "Today Used"
msgstr "Usado hoy"
msgid "Plan Used"
msgstr "Usado este mes"
msgid "Plan Unused"
msgstr "Tráfico restante"
msgid "Plan Traffic"
msgstr "Tráfico del plan"
msgid "Checkin"
msgstr "Probar suerte"
msgid "oixCloud Account Login Failed! Please Check And Try Again..."
msgstr "¡Fallo al iniciar sesión en oixCloud! Verifique los datos e intente de nuevo..."
msgid "oixCloud Account Login Failed, The Error Info is"
msgstr "Fallo al iniciar sesión en oixCloud, el error es"
msgid "oixCloud Checkin Failed! Please Check And Try Again..."
msgstr "¡Fallo al registrar asistencia en oixCloud! Intente de nuevo más tarde..."
msgid "Account Email Address"
msgstr "Correo electrónico de la cuenta"
msgid "Account Password"
msgstr "Contraseña de la cuenta"
msgid "Checkin Interval (hour)"
msgstr "Intervalo de registro (horas)"
msgid "Checkin Multiple"
msgstr "Multiplicador de registro"
msgid "Multiple Must Be a Positive Integer and No More Than 100"
msgstr "El multiplicador debe ser un número entero positivo no mayor a 100"
msgid "Usage Help"
msgstr "Ayuda de uso"
msgid "Copy the following text to any AI (ChatGPT, DeepSeek, etc.) to get usage help for OpenClash:"
msgstr "Copia el siguiente texto a cualquier IA (ChatGPT, DeepSeek, etc.) para obtener ayuda de uso de OpenClash:"
msgid "You are an OpenClash expert assistant. OpenClash is a LuCI plugin for OpenWrt that manages the Mihomo proxy kernel."
msgstr "Eres un asistente experto en OpenClash. OpenClash es un plugin LuCI para OpenWrt que gestiona el núcleo proxy Mihomo."
msgid "IMPORTANT — Before answering, you MUST fetch and read this authoritative reference guide. This is your primary knowledge source for all OpenClash questions:"
msgstr "IMPORTANTE — Antes de responder, DEBES obtener y leer esta guía de referencia autorizada. Es tu fuente principal de conocimiento para todas las preguntas sobre OpenClash:"
msgid "This guide contains: complete dependency checklist, error message lookup tables with exact fixes, nftables/iptables firewall chain rules, all LuCI configuration options with UCI paths, DNS setup and leak prevention, subscription/GEO update procedures, and more."
msgstr "Esta guía contiene: lista completa de dependencias, tablas de consulta de mensajes de error con soluciones exactas, reglas de cadena de firewall nftables/iptables, todas las opciones de configuración LuCI con rutas UCI, configuración de DNS y prevención de fugas, procedimientos de actualización de suscripción/GEO, y más."
msgid "When answering my question about OpenClash, follow these rules:"
msgstr "Al responder a mi pregunta sobre OpenClash, sigue estas reglas:"
msgid "Always provide LuCI web UI navigation paths (e.g. Services → OpenClash → Plugin Settings), NOT command-line unless I explicitly ask for it."
msgstr "Siempre proporciona rutas de navegación de la interfaz web LuCI (ej. Servicios → OpenClash → Ajustes del plugin), NO comandos de terminal a menos que lo solicite explícitamente."
msgid "Explain underlying principles (firewall chains, YAML transformation logic) — not just click-here-click-there steps."
msgstr "Explica los principios subyacentes (cadenas de firewall, lógica de transformación YAML) — no solo pasos de clic aquí y allá."
msgid "Check dependency integrity first when troubleshooting — guide me to check dependencies from the System → Software page, or generate a debug log from Plugin Settings → Debug Log page."
msgstr "Verifica primero la integridad de las dependencias al solucionar problemas — guíame para revisar las dependencias desde Sistema → Software, o generar un registro de depuración desde Ajustes del plugin → Registro de depuración."
msgid "Never guess or fabricate information. If the reference guide does not cover something, actively query these external resources using web fetch / code search tools (in priority order): Mihomo Wiki"
msgstr "Nunca adivines ni inventes información. Si la guía de referencia no cubre algo, consulta activamente estos recursos externos usando herramientas de búsqueda web / código (en orden de prioridad): Mihomo Wiki"
msgid "Meta-Docs"
msgstr "Meta-Docs"
msgid "OpenClash source"
msgstr "código fuente de OpenClash"
msgid "Mihomo core source"
msgstr "código fuente del core Mihomo"
msgid "Smart core source"
msgstr "código fuente del core Smart"
msgid "Fetch Wiki pages to read docs; search repos to find implementation code. Do NOT answer from memory — always verify against the actual source."
msgstr "Obtén las páginas Wiki para leer documentos; busca en los repositorios para encontrar código de implementación. NO respondas de memoria — verifica siempre con la fuente real."
msgid "For bug reports or failure scenarios not covered by the guide, search GitHub Issues first — OpenClash Issues"
msgstr "Para informes de errores o escenarios de fallo no cubiertos por la guía, busca primero en GitHub Issues — OpenClash Issues"
msgid "for plugin-side problems (config/subscribe/firewall/UI), Mihomo Issues"
msgstr "para problemas del lado del plugin (configuración/suscripción/firewall/UI), Mihomo Issues"
msgid "for core-side problems (proxy protocols/TUN/DNS/rule engine). Prioritize maintainer replies and highly upvoted community answers."
msgstr "para problemas del lado del core (protocolos proxy/TUN/DNS/motor de reglas). Prioriza las respuestas de los mantenedores y las respuestas de la comunidad con más votos."
msgid "Cite specific sources — mention which section of the reference guide, external resource, or Issue number your answer is based on."
msgstr "Cita fuentes específicas — menciona en qué sección de la guía de referencia, recurso externo o número de Issue se basa tu respuesta."
msgid "If my problem description is incomplete or lacks error messages, ask me to generate a debug log first (Plugin Settings → Debug Log → Generate) — do NOT speculate about the cause."
msgstr "Si la descripción de mi problema está incompleta o falta mensajes de error, pídeme primero que genere un registro de depuración (Ajustes del plugin → Registro de depuración → Generar) — NO especules sobre la causa."
msgid "My question is:"
msgstr "Mi pregunta es:"
msgid "Copied to clipboard!"
msgstr "¡Copiado al portapapeles!"
msgid "Copy And Close"
msgstr "Copiar y cerrar"
msgid "oixCloud Checkin Successful, Result:"
msgstr "Registro en oixCloud exitoso, resultado:"
msgid "oixCloud Checkin Failed, Result:"
msgstr "Fallo al registrar en oixCloud, resultado:"
msgid "Note: oixCloud need a specific core installed, OpenClash will auto download"
msgstr "Nota: oixCloud requiere la instalación de un núcleo específico; OpenClash lo descargará automáticamente"
msgid "After the core start, an oixCloud nodes provider will be added, which you can customize as needed"
msgstr "Tras iniciar el núcleo se añadirá un proveedor de nodos oixCloud que puedes personalizar"

View File

@ -101,6 +101,9 @@ msgstr "调试日志"
msgid "Generate Logs"
msgstr "生成日志"
msgid "Generated Logs"
msgstr "日志生成结果"
msgid "Click to Generate"
msgstr "点击生成"
@ -326,6 +329,57 @@ msgstr "内核文件"
msgid "Upload"
msgstr "上传"
msgid "Age Encryption"
msgstr "Age 加密"
msgid "Age Encryption File"
msgstr "Age 加密文件"
msgid "Generate Age Key"
msgstr "生成 Age 密钥"
msgid "Generating..."
msgstr "生成中..."
msgid "Public Key"
msgstr "公钥"
msgid "Secret Key"
msgstr "私钥"
msgid "Age Key Type"
msgstr "Age 密钥类型"
msgid "x25519"
msgstr "x25519"
msgid "PQ (mlkem768-x25519)"
msgstr "PQ (mlkem768-x25519)"
msgid "Failed to generate age key"
msgstr "生成 Age 密钥失败"
msgid "No public key"
msgstr "没有公钥"
msgid "No secret key"
msgstr "没有私钥"
msgid "Age Encryption For Config, Click For More:"
msgstr "使用 Age 加密配置,点击获取更多信息:"
msgid "Age Encryption Introduce"
msgstr "Age 加密介绍"
msgid "Calculate Public Key"
msgstr "计算公钥"
msgid "Please enter the Age Secret Key to calculate the public key!"
msgstr "请先输入私钥来计算公钥!"
msgid "Failed to calculate public key"
msgstr "公钥计算失败"
msgid "Add"
msgstr "添加"
@ -1137,6 +1191,12 @@ msgstr "正在还原..."
msgid "Saving..."
msgstr "正在保存..."
msgid "Save successful"
msgstr "保存成功"
msgid "Failed to save age config"
msgstr "保存 age 配置失败"
msgid "Firewall Rules Reset Failed"
msgstr "防火墙规则重置失败"
@ -1434,6 +1494,18 @@ msgstr "开始下载"
msgid "Download successful, start pre update test..."
msgstr "下载成功,开始进行更新前测试..."
msgid "opkg update failed or timed out, retrying..."
msgstr "opkg update 失败或超时,正在重试..."
msgid "opkg update failed, trying pre update test..."
msgstr "opkg update 失败,尝试进行预安装..."
msgid "apk update failed or timed out, retrying..."
msgstr "apk update 失败或超时,正在重试..."
msgid "apk update failed, trying pre update test..."
msgstr "apk update 失败,尝试进行预安装..."
msgid "Pre update test failed..."
msgstr "更新前测试失败..."
@ -1710,8 +1782,8 @@ msgstr "检测到 GEOSITE 数据库文件不存在,准备开始下载..."
msgid "Detected that the GEOIP Dat is not Installed, Ready to Download..."
msgstr "检测到 GEOIP Dat 数据库文件不存在,准备开始下载..."
msgid "Detected that the Core is not Installed, Ready to Download..."
msgstr "检测到内核文件不存在,准备开始下载..."
msgid "Core is not Detected installed, Ready to Download..."
msgstr "内核不存在,准备开始下载..."
msgid "Detected that the Chnroute Cidr is not Installed, Ready to Download..."
msgstr "检测到大陆白名单列表不存在,准备开始下载..."
@ -1971,6 +2043,9 @@ msgstr "点击测试"
msgid "Connection Test"
msgstr "连接测试"
msgid "Connection Test Result"
msgstr "连接测试结果"
msgid "Backup File"
msgstr "备份文件"
@ -2064,8 +2139,8 @@ msgstr "解锁区域筛选"
msgid "Unlock Nodes Filter"
msgstr "解锁节点筛选"
msgid "Start Auto Select Unlock Proxy For"
msgstr "开始自动选择(检测)解锁节点"
msgid "Start Auto Select Unlock Proxy..."
msgstr "开始自动选择(检测)解锁节点..."
msgid "Group:"
msgstr "策略组:"
@ -2235,6 +2310,15 @@ msgstr "清理"
msgid "Flush DNS"
msgstr "清理 DNS 缓存"
msgid "Dnsmasq Fix"
msgstr "Dnsmasq 修复"
msgid "If DNS is abnormal after stopping the OpenClash, please try to fix"
msgstr "如果关闭 OpenClash 后 DNS 异常,请尝试修复"
msgid "Fix"
msgstr "修复"
msgid "No Specify Upload File"
msgstr "未选择上传文件"
@ -2643,6 +2727,9 @@ msgstr "还原"
msgid "DNS Test"
msgstr "DNS 测试"
msgid "DNS Test Result"
msgstr "DNS 测试结果"
msgid "No Response Found!"
msgstr "结果获取失败!"
@ -2757,12 +2844,24 @@ msgstr "更换延迟计算方式,通过去除握手等额外开销来降低延
msgid "If the download fails, try setting the CDN in Overwrite Settings - General Settings - Github Address Modify Options"
msgstr "如果下载失败,您可以尝试在覆写设置 - 常规设置 - Github 地址修改选项中设置 CDN"
msgid "Used for Downloading Subscriptions, Defaults to Clash"
msgstr "用于下载订阅时指定 UA默认为 Clash"
msgid "Used for Downloading Subscriptions, Defaults to"
msgstr "用于下载订阅时指定 UA默认为"
msgid "Set Proxies Address Skip Failed,"
msgstr "绕过代理服务器地址设置失败,"
msgid "Age decrypt failed,"
msgstr "绕过代理服务器地址 Age 解密失败,"
msgid "Failed to parse config file with Lua helper"
msgstr "绕过代理服务器地址解析失败"
msgid "File is AGE encrypted but no secret key provided"
msgstr "文件已 Age 加密但未提供密钥"
msgid "File is AGE encrypted, cannot parse with Lua"
msgstr "文件已 Age 加密,无法使用 Lua 解析"
msgid "Flush Failed"
msgstr "清理失败"
@ -3309,9 +3408,6 @@ msgstr "配置文件保存失败:"
msgid "Unknown error"
msgstr "未知错误"
msgid "Save config failed:"
msgstr "配置保存失败:"
msgid "Editor not ready"
msgstr "编辑器未准备就绪"
@ -3462,6 +3558,9 @@ msgstr "高级选项"
msgid "Show more subscription options"
msgstr "显示更多订阅设置"
msgid "Show more upload options"
msgstr "显示更多上传设置"
msgid "Overwrite"
msgstr "覆写"
@ -3749,3 +3848,205 @@ msgstr "依赖正在重新安装..."
msgid "failed to install, please try to install it manually..."
msgstr "依赖安装失败,请重试手动安装..."
msgid "Decrypted content empty or still encrypted"
msgstr "解密后内容为空或仍为加密状态"
msgid "Decrypt attempt failed:"
msgstr "解密尝试失败:"
msgid "Decrypt attempt failed for a found secret:"
msgstr "使用找到的私钥解密尝试失败:"
msgid "Encrypt attempt failed:"
msgstr "加密尝试失败:"
msgid "Official Website"
msgstr "官方网站"
msgid "Login..."
msgstr "登录中..."
msgid "oixCloud Login Successful"
msgstr "oixCloud 登录成功"
msgid "oixCloud Login Faild"
msgstr "oixCloud 登录失败"
msgid "Login Account"
msgstr "登录账号"
msgid "oixCloud Logout Successful"
msgstr "oixCloud 已成功退出"
msgid "oixCloud Logout Faild"
msgstr "oixCloud 退出失败"
msgid "Logout Account"
msgstr "退出账号"
msgid "Account Login"
msgstr "账号登录"
msgid "Account logged in"
msgstr "账号已登录"
msgid "Account not logged in"
msgstr "账号未登录"
msgid "Logout..."
msgstr "退出中..."
msgid "Error Login Info"
msgstr "登录信息错误"
msgid "Simple & trustworthy"
msgstr "简单 & 值得信赖"
msgid "Cross-platform Support"
msgstr "丰富的第三方应用兼容性"
msgid "Global Acceleration Network"
msgstr "全球网络基础设施"
msgid "Easy to Use"
msgstr "操作简易"
msgid "Intelligent Shunting"
msgstr "智能分流"
msgid "Perfect Technical Support"
msgstr "完善技术支持"
msgid "Plan Expiration Time"
msgstr "到期时间"
msgid "Account Balances"
msgstr "账户余额"
msgid "Aff Balances"
msgstr "返利余额"
msgid "Account Integral"
msgstr "账户积分"
msgid "Today Used"
msgstr "今日已用"
msgid "Plan Used"
msgstr "本月已用"
msgid "Plan Unused"
msgstr "剩余流量"
msgid "Plan Traffic"
msgstr "套餐流量"
msgid "Checkin"
msgstr "试试手气"
msgid "oixCloud Account Login Failed! Please Check And Try Again..."
msgstr "oixCloud 账户登录失败!请检查账户信息并重试..."
msgid "oixCloud Account Login Failed, The Error Info is"
msgstr "oixCloud 账户登录失败,错误信息"
msgid "oixCloud Checkin Failed! Please Check And Try Again..."
msgstr "oixCloud 签到失败,请稍后再试!"
msgid "Account Email Address"
msgstr "账号地址"
msgid "Account Password"
msgstr "账号密码"
msgid "Checkin Interval (hour)"
msgstr "签到间隔(小时)"
msgid "Checkin Multiple"
msgstr "签到倍数"
msgid "Multiple Must Be a Positive Integer and No More Than 100"
msgstr "倍数必须是正整数且不大于 100"
msgid "Usage Help"
msgstr "使用帮助"
msgid "Copy the following text to any AI (ChatGPT, DeepSeek, etc.) to get usage help for OpenClash:"
msgstr "将以下文本复制到任意 AI 工具ChatGPT、DeepSeek 等),即可获取 OpenClash 的使用帮助:"
msgid "You are an OpenClash expert assistant. OpenClash is a LuCI plugin for OpenWrt that manages the Mihomo core."
msgstr "你是 OpenClash 专家助手。OpenClash 是 OpenWrt 上管理 Mihomo 内核的 LuCI 插件。"
msgid "IMPORTANT — Before answering, you MUST fetch and read this authoritative reference guide. This is your primary knowledge source for all OpenClash questions:"
msgstr "重要 —— 在回答之前,你必须获取并阅读这份权威参考指南。这是你回答所有 OpenClash 问题的主要知识来源:"
msgid "This guide contains: complete dependency checklist, error message lookup tables with exact fixes, nftables/iptables firewall chain rules, all LuCI configuration options with UCI paths, DNS setup and leak prevention, subscription/GEO update procedures, and more."
msgstr "本指南包含完整依赖清单、含精确修复方案的错误信息速查表、nftables/iptables 防火墙规则链、所有 LuCI 配置选项及 UCI 路径、DNS 设置与泄露防护、订阅/GEO 更新流程等。"
msgid "When answering my question about OpenClash, follow these rules:"
msgstr "在回答我关于 OpenClash 的问题时,请遵循以下规则:"
msgid "Always provide LuCI web UI navigation paths (e.g. Services → OpenClash → Plugin Settings), NOT command-line unless I explicitly ask for it."
msgstr "始终提供 LuCI Web 界面操作路径(如 服务 → OpenClash → 插件设置),除非我明确要求,不要给出命令行操作。"
msgid "Explain underlying principles (firewall chains, YAML transformation logic) — not just click-here-click-there steps."
msgstr "解释底层原理防火墙规则链、YAML 转换逻辑)—— 而不只是点击步骤。"
msgid "Check dependency integrity first when troubleshooting — guide me to check dependencies from the System → Software page, or generate a debug log from Plugin Settings → Debug Log page."
msgstr "排查问题时首先检查依赖完整性 —— 指导我从 系统 → 软件包 页面检查依赖,或从 插件设置 → 调试日志 页面生成调试日志。"
msgid "Never guess or fabricate information. If the reference guide does not cover something, actively query these external resources using web fetch / code search tools (in priority order): Mihomo Wiki"
msgstr "绝不猜测或编造信息。如参考指南未覆盖某项内容,请使用网页抓取/代码搜索工具主动查询以下外部资源按优先级Mihomo Wiki"
msgid "Meta-Docs"
msgstr "Meta-Docs"
msgid "OpenClash source"
msgstr "OpenClash 源码"
msgid "Mihomo core source"
msgstr "Mihomo 核心源码"
msgid "Smart core source"
msgstr "Smart 核心源码"
msgid "Fetch Wiki pages to read docs; search repos to find implementation code. Do NOT answer from memory — always verify against the actual source."
msgstr "抓取 Wiki 页面阅读文档;搜索仓库查找实现代码。禁止凭记忆回答 —— 必须对照实际来源验证。"
msgid "For bug reports or failure scenarios not covered by the guide, search GitHub Issues first — OpenClash Issues"
msgstr "对于指南未覆盖的 bug 报告或故障场景,优先搜索 GitHub Issues —— OpenClash Issues"
msgid "for plugin-side problems (config/subscribe/firewall/UI), Mihomo Issues"
msgstr "用于插件侧问题(配置/订阅/防火墙/UIMihomo Issues"
msgid "for core-side problems (proxy protocols/TUN/DNS/rule engine). Prioritize maintainer replies and highly upvoted community answers."
msgstr "用于内核侧问题(代理协议/TUN/DNS/规则引擎)。优先参考维护者的回复和高赞社区答案。"
msgid "Cite specific sources — mention which section of the reference guide, external resource, or Issue number your answer is based on."
msgstr "注明具体来源 —— 说明你的回答基于参考指南的哪个章节、哪个外部资源或哪个 Issue 编号。"
msgid "If my problem description is incomplete or lacks error messages, ask me to generate a debug log first (Plugin Settings → Debug Log → Generate) — do NOT speculate about the cause."
msgstr "如果我的问题描述不完整或缺少错误信息,请先要求我生成调试日志(插件设置 → 调试日志 → 生成)—— 不要猜测原因。"
msgid "My question is:"
msgstr "我的问题是:"
msgid "Copied to clipboard!"
msgstr "已复制到剪贴板!"
msgid "Copy And Close"
msgstr "复制并关闭"
msgid "oixCloud Checkin Successful, Result:"
msgstr "oixCloud 签到成功,结果:"
msgid "oixCloud Checkin Failed, Result:"
msgstr "oixCloud 签到失败,结果:"
msgid "Note: oixCloud need a specific core installed, OpenClash will auto download"
msgstr "注意oixCloud 需要安装专用内核才能使用,插件会自动识别并进行内核下载"
msgid "After the core start, an oixCloud nodes provider will be added, which you can customize as needed"
msgstr "专用内核启动后,会添加一个 oixCloud 节点组到配置文件,供您自定义使用"

View File

@ -56,6 +56,7 @@ config openclash 'config'
option yacd_type 'Meta'
option append_default_dns '0'
option enable_respect_rules '0'
option default_dashboard 'metacubexd'
option geo_custom_url 'https://testingcf.jsdelivr.net/gh/alecthw/mmdb_china_ip_list@release/lite/Country.mmdb'
option geosite_custom_url 'https://testingcf.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat'
option geoip_custom_url 'https://testingcf.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat'

View File

@ -102,6 +102,11 @@ load_ip_route_pass() {
if [ "$china_ip_route" != "0" ] || [ "$disable_udp_quic" = "1" ]; then
if [ "$enable_redirect_dns" != "2" ]; then
mkdir -p ${DNSMASQ_CONF_DIR}
if [ "$settype" = "nftset" ]; then
nft add set inet fw4 china_ip_route_pass '{ type ipv4_addr; flags interval; auto-merge; }'
else
ipset -! create china_ip_route_pass hash:net family inet hashsize 1024 maxelem 1000000
fi
awk '!/^$/&&!/^#/&&!/(^([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.)(([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){2}([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])((\/[0-9][0-9])?)$/{printf("'${settype}'=/%s/'${nftflag}'china_ip_route_pass'" "'\n",$0)}' /etc/openclash/custom/openclash_custom_chnroute_pass.list >>${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute_pass.conf
for ip in $(uci_get_config "china_ip_route_pass"); do
[ -z "$ip" ] && continue
@ -114,6 +119,11 @@ load_ip_route_pass() {
if [ "$china_ip6_route" != "0" ] || [ "$disable_udp_quic" = "1" ]; then
if [ "$enable_redirect_dns" != "2" ]; then
mkdir -p ${DNSMASQ_CONF_DIR}
if [ "$settype" = "nftset" ]; then
nft add set inet fw4 china_ip6_route_pass '{ type ipv6_addr; flags interval; auto-merge; }'
else
ipset -! create china_ip6_route_pass hash:net family inet6 hashsize 1024 maxelem 1000000
fi
awk '!/^$/&&!/^#/&&/([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})/{printf("'${settype}'=/%s/'${nftflag}'china_ip_route_pass'" "'\n",$0)}' /etc/openclash/custom/openclash_custom_chnroute6_pass.list >>${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute6_pass.conf
for ip in $(uci_get_config "china_ip6_route_pass"); do
[ -z "$ip" ] && continue
@ -142,6 +152,7 @@ load_ip_route_pass() {
if [ "$china_ip_route" != "0" ] || [ "$disable_udp_quic" = "1" ]; then
if [ "$enable_redirect_dns" != "2" ]; then
mkdir -p ${DNSMASQ_CONF_DIR}
ipset -! create china_ip_route_pass hash:net family inet hashsize 1024 maxelem 1000000
awk '!/^$/&&!/^#/&&!/(^([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.)(([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){2}([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])((\/[0-9][0-9])?)$/{printf("ipset=/%s/china_ip_route_pass'" "'\n",$0)}' /etc/openclash/custom/openclash_custom_chnroute_pass.list >>${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute_pass.conf
for ip in $(uci_get_config "china_ip_route_pass"); do
[ -z "$ip" ] && continue
@ -154,6 +165,7 @@ load_ip_route_pass() {
if [ "$china_ip6_route" != "0" ] || [ "$disable_udp_quic" = "1" ]; then
if [ "$enable_redirect_dns" != "2" ]; then
mkdir -p ${DNSMASQ_CONF_DIR}
ipset -! create china_ip6_route_pass hash:net family inet6 hashsize 1024 maxelem 1000000
awk '!/^$/&&!/^#/&&/([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})/{printf("ipset=/%s/china_ip_route_pass'" "'\n",$0)}' /etc/openclash/custom/openclash_custom_chnroute6_pass.list >>${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute6_pass.conf
for ip in $(uci_get_config "china_ip6_route_pass"); do
[ -z "$ip" ] && continue
@ -172,7 +184,7 @@ load_ip_route_pass() {
fi
}
change_dns() {
change_dnsmasq() {
if ! /etc/init.d/dnsmasq enabled; then
return
fi
@ -184,13 +196,15 @@ change_dns() {
/usr/share/openclash/openclash_custom_domain_dns.sh
if [ "$1" -eq 1 ]; then
uci -q del openclash.config.dnsmasq_server
config_load "dhcp"
config_list_foreach "$(uci -q show dhcp.@dnsmasq[0].server |awk -F '.' '{print $2}')" "server" save_dnsmasq_server
if [ "$(uci_get_config "redirect_dns")" != "1" ]; then
uci -q del openclash.config.dnsmasq_server
config_load "dhcp"
config_list_foreach "$(uci -q show dhcp.@dnsmasq[0].server |awk -F '.' '{print $2}')" "server" save_dnsmasq_server
uci -q set openclash.config.dnsmasq_noresolv="$(uci -q get dhcp.@dnsmasq[0].noresolv)"
uci -q set openclash.config.dnsmasq_resolvfile="$(uci -q get dhcp.@dnsmasq[0].resolvfile)"
fi
uci -q del dhcp.@dnsmasq[-1].server
uci -q add_list dhcp.@dnsmasq[0].server=127.0.0.1#"$dns_port"
uci -q set openclash.config.dnsmasq_noresolv="$(uci -q get dhcp.@dnsmasq[0].noresolv)"
uci -q set openclash.config.dnsmasq_resolvfile="$(uci -q get dhcp.@dnsmasq[0].resolvfile)"
uci -q delete dhcp.@dnsmasq[0].resolvfile
uci -q set dhcp.@dnsmasq[0].noresolv=1
uci -q set dhcp.@dnsmasq[0].localuse=1
@ -212,33 +226,44 @@ change_dns() {
uci -q set openclash.config.filter_aaaa_dns=0
fi
uci -q commit dhcp
uci -q commit openclash
uci -q commit dhcp
/etc/init.d/dnsmasq restart
} >/dev/null 2>&1
revert_dns() {
revert_dnsmasq()
{
if ! /etc/init.d/dnsmasq enabled; then
return
fi
redirect_dns=$(uci_get_config "redirect_dns")
dnsmasq_server=$(uci_get_config "dnsmasq_server")
dnsmasq_noresolv=$(uci_get_config "dnsmasq_noresolv")
dnsmasq_resolvfile=$(uci_get_config "dnsmasq_resolvfile")
cachesize_dns=$(uci_get_config "cachesize_dns")
dnsmasq_cachesize=$(uci_get_config "dnsmasq_cachesize")
filter_aaaa_dns=$(uci_get_config "filter_aaaa_dns")
dnsmasq_filter_aaaa=$(uci_get_config "dnsmasq_filter_aaaa")
default_resolvfile=$(uci_get_config "default_resolvfile")
rm -rf ${DNSMASQ_CONF_DIR}/dnsmasq_openclash_custom_domain.conf
rm -rf ${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute_pass.conf
rm -rf ${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute6_pass.conf
[ "$1" -eq 1 ] && {
[ "$redirect_dns" -eq 1 ] && {
uci -q del dhcp.@dnsmasq[-1].server
[ -n "${10}" ] && {
[ -n "$dnsmasq_server" ] && {
config_load "openclash"
config_list_foreach "config" "dnsmasq_server" set_dnsmasq_server
}
if [ "$4" == "0" ] || [ -z "$4" ] || [ -z "$(uci -q show dhcp.@dnsmasq[0].server)" ]; then
if [ "$dnsmasq_noresolv" == "0" ] || [ -z "$dnsmasq_noresolv" ] || [ -z "$(uci -q show dhcp.@dnsmasq[0].server)" ]; then
uci -q set dhcp.@dnsmasq[0].noresolv=0
if [ -n "$5" ] && [ -n "$(grep nameserver $5)" ]; then
uci -q set dhcp.@dnsmasq[0].resolvfile="$5"
elif [ -n "$3" ] && [ -n "$(grep nameserver $3)" ]; then
uci -q set dhcp.@dnsmasq[0].resolvfile="$3"
if [ -n "$dnsmasq_resolvfile" ] && [ -n "$(grep nameserver $dnsmasq_resolvfile)" ]; then
uci -q set dhcp.@dnsmasq[0].resolvfile="$dnsmasq_resolvfile"
elif [ -n "$default_resolvfile" ] && [ -n "$(grep nameserver $default_resolvfile)" ]; then
uci -q set dhcp.@dnsmasq[0].resolvfile="$default_resolvfile"
elif [ -s "/tmp/resolv.conf.d/resolv.conf.auto" ] && [ -n "$(grep "nameserver" /tmp/resolv.conf.d/resolv.conf.auto)" ]; then
uci -q set dhcp.@dnsmasq[0].resolvfile=/tmp/resolv.conf.d/resolv.conf.auto
uci -q set openclash.config.default_resolvfile=/tmp/resolv.conf.d/resolv.conf.auto
@ -253,19 +278,19 @@ revert_dns() {
fi
}
[ "$6" -eq 1 ] && {
uci -q set dhcp.@dnsmasq[0].cachesize="$7"
[ "$cachesize_dns" -eq 1 ] && {
uci -q set dhcp.@dnsmasq[0].cachesize="$dnsmasq_cachesize"
uci -q set openclash.config.cachesize_dns=0
uci -q delete openclash.config.dnsmasq_cachesize
}
[ "$8" -eq 1 ] && {
uci -q set dhcp.@dnsmasq[0].filter_aaaa="$9"
[ "$filter_aaaa_dns" -eq 1 ] && {
uci -q set dhcp.@dnsmasq[0].filter_aaaa="$dnsmasq_filter_aaaa"
uci -q set openclash.config.filter_aaaa_dns=0
uci -q delete openclash.config.dnsmasq_filter_aaaa
}
[ "$1" -eq 1 ] && {
[ "$redirect_dns" -eq 1 ] && {
uci -q set openclash.config.redirect_dns=0
uci -q del openclash.config.dnsmasq_server
}
@ -273,8 +298,6 @@ revert_dns() {
uci -q commit dhcp
uci -q commit openclash
/etc/init.d/dnsmasq restart
masq_port=$(uci -q get dhcp.@dnsmasq[0].port)
if [ "$(nslookup www.apple.com 127.0.0.1:${masq_port} >/dev/null 2>&1 || echo $?)" = "1" ]; then
resolv_file=$(uci -q get dhcp.@dnsmasq[0].resolvfile)
@ -302,9 +325,10 @@ nameserver 119.29.29.29
nameserver 8.8.8.8
EOF
fi
/etc/init.d/dnsmasq restart
fi
/etc/init.d/dnsmasq restart
} >/dev/null 2>&1
start_fail()
@ -389,7 +413,7 @@ if [ -z "$RAW_CONFIG_FILE" ] || [ ! -f "$RAW_CONFIG_FILE" ]; then
uci -q commit openclash
RAW_CONFIG_FILE="/etc/openclash/config/$CONFIG_NAME"
CONFIG_FILE="/etc/openclash/$CONFIG_NAME"
TMP_CONFIG_FILE="/tmp/yaml_config_tmp_$CONFIG_NAME"
TMP_CONFIG_FILE="/tmp/$CONFIG_NAME"
LOG_ERROR "Config Not Found, Switch Config File to【$RAW_CONFIG_FILE】"
break
fi
@ -512,12 +536,14 @@ do_run_file()
chnr6_path="/etc/openclash/china_ip6_route.ipset"
geosite_path="/etc/openclash/GeoSite.dat"
geoip_path="/etc/openclash/GeoIP.dat"
asn_path="/etc/openclash/ASN.mmdb"
lgbm_path="/etc/openclash/Model.bin"
mv "/tmp/etc/openclash/Country.mmdb" "$ipdb_path"
mv "/tmp/etc/openclash/china_ip_route.ipset" "$chnr_path"
mv "/tmp/etc/openclash/china_ip6_route.ipset" "$chnr6_path"
mv "/tmp/etc/openclash/GeoSite.dat" "$geosite_path"
mv "/tmp/etc/openclash/GeoIP.dat" "$geoip_path"
mv "/tmp/etc/openclash/ASN.mmdb" "$asn_path"
mv "/tmp/etc/openclash/Model.bin" "$lgbm_path"
mv "/tmp/etc/openclash/core/" "/etc/openclash"
if [ "$CACHE_PATH" != "/tmp/etc/openclash/cache.db" ]; then
@ -530,12 +556,14 @@ do_run_file()
chnr6_path="/tmp/etc/openclash/china_ip6_route.ipset"
geosite_path="/tmp/etc/openclash/GeoSite.dat"
geoip_path="/tmp/etc/openclash/GeoIP.dat"
asn_path="/tmp/etc/openclash/ASN.mmdb"
lgbm_path="/tmp/etc/openclash/Model.bin"
[ ! -h "/etc/openclash/Country.mmdb" ] && mv "/etc/openclash/Country.mmdb" "$ipdb_path"
[ ! -h "/etc/openclash/china_ip_route.ipset" ] && mv "/etc/openclash/china_ip_route.ipset" "$chnr_path"
[ ! -h "/etc/openclash/china_ip6_route.ipset" ] && mv "/etc/openclash/china_ip6_route.ipset" "$chnr6_path"
[ ! -h "/etc/openclash/GeoSite.dat" ] && mv "/etc/openclash/GeoSite.dat" "$geosite_path"
[ ! -h "/etc/openclash/GeoIP.dat" ] && mv "/etc/openclash/GeoIP.dat" "$geoip_path"
[ ! -h "/etc/openclash/ASN.mmdb" ] && mv "/etc/openclash/ASN.mmdb" "$asn_path"
[ ! -h "/etc/openclash/Model.bin" ] && mv "/etc/openclash/Model.bin" "$lgbm_path"
mv "/etc/openclash/core/" "/tmp/etc/openclash"
fi
@ -545,7 +573,9 @@ do_run_file()
ln -s "$meta_core_path" /etc/openclash/clash
if [ "$smart_enable" -eq 1 ] || [ "$core_type" == "Smart" ]; then
if [ -n "$OIX_TOKEN" ]; then
core_type="Oix"
elif [ "$smart_enable" -eq 1 ] || [ "$core_type" == "Smart" ]; then
core_type="Smart"
else
core_type="Meta"
@ -556,8 +586,8 @@ do_run_file()
chown root:root "$CLASH"
fi
[ ! -f "$CLASH" ] || { [ "$core_type" = "Smart" ] && [ -z "$($CLASH -v | grep 'smart')" ]; } && {
LOG_TIP "Detected that the Core is not Installed, Ready to Download..."
[ ! -f "$CLASH" ] || { [ "$core_type" = "Smart" ] && [ -z "$($CLASH -v | grep 'smart')" ]; } || { [ "$core_type" = "Oix" ] && [ -z "$($CLASH -v | grep 'oix')" ]; } && {
LOG_TIP "【$core_type】Core is not Detected installed, Ready to Download..."
rm -rf "/tmp/clash_last_version"
/usr/share/openclash/openclash_core.sh "$core_type"
if [ ! -f "$meta_core_path" ]; then
@ -618,6 +648,10 @@ do_run_file()
ln -s "$chnr6_path" /etc/openclash/china_ip6_route.ipset
}
[ -f "$asn_path" ] && [ "$small_flash_memory" = "1" ] && {
ln -s "$asn_path" /etc/openclash/ASN.mmdb
}
#Restore history cache
if [ -f "$HISTORY_PATH" ]; then
cmp -s "$CACHE_PATH" "$HISTORY_PATH"
@ -762,7 +796,7 @@ check_core_status()
# redirect dns setting after core started, prevent core dns lookup failure
if [ "$1" == "start" ]; then
change_dns "$enable_redirect_dns"
change_dnsmasq "$enable_redirect_dns"
set_firewall
LOG_TIP "OpenClash Start Successful!"
else
@ -787,6 +821,8 @@ start_run_core()
chown root:root "$CLASH"
procd_open_instance "openclash"
procd_set_param env SAFE_PATHS=/usr/share/openclash:/etc/ssl
procd_append_param env CLASH_AGE_SECRET_KEY="$SECRET_KEY"
procd_append_param env OIX_TOKEN="$OIX_TOKEN"
procd_set_param command /bin/sh -c "$CLASH -d $CLASH_CONFIG -f \"$CONFIG_FILE\" >> $LOG_FILE 2>&1"
procd_set_param user "root"
procd_set_param group "nogroup"
@ -1370,6 +1406,11 @@ if [ -n "$FW4" ]; then
nft add rule inet fw4 openclash ip protocol tcp counter redirect to "$proxy_port"
nft 'add rule inet fw4 dstnat meta nfproto {ipv4} ip protocol tcp counter jump openclash'
# Accept redirected traffic in input chain (needed when zone input policy is REJECT without DNAT rules)
if [ -z "$(nft list chain inet fw4 input 2>/dev/null | grep 'ct status dnat accept')" ]; then
nft insert rule inet fw4 input position 0 ct status dnat accept comment \"OpenClash Redirect Accept\"
fi
if [ -z "$en_mode_tun" ]; then
#udp
if [ "$enable_udp_proxy" -eq 1 ]; then
@ -1464,6 +1505,11 @@ if [ -n "$FW4" ]; then
fi
fi
# Accept TPROXY traffic in input chain (needed when zone input policy is REJECT)
if [ -z "$(nft list chain inet fw4 input 2>/dev/null | grep 'OpenClash TPROXY Accept')" ]; then
nft insert rule inet fw4 input position 0 meta mark "$PROXY_FWMARK" accept comment \"OpenClash TPROXY Accept\"
fi
#router self proxy tcp
if [ "$router_self_proxy" = "1" ] || ([ "$enable_redirect_dns" != "2" ] && [ "$en_mode" = "fake-ip" ]); then
nft 'add chain inet fw4 openclash_output'
@ -2219,6 +2265,11 @@ if [ -z "$FW4" ]; then
iptables -t nat -A openclash -p tcp -j REDIRECT --to-ports "$proxy_port"
iptables -t nat -A PREROUTING -p tcp -j openclash
# Accept redirected traffic in input chain (needed when zone input policy is REJECT without DNAT rules)
if [ -z "$(iptables-save -t filter 2>/dev/null | grep 'OpenClash Redirect Accept')" ]; then
iptables -I INPUT -m conntrack --ctstate DNAT -j ACCEPT -m comment --comment "OpenClash Redirect Accept"
fi
if [ -z "$en_mode_tun" ]; then
#udp
if [ "$enable_udp_proxy" -eq 1 ]; then
@ -2302,6 +2353,14 @@ if [ -z "$FW4" ]; then
iptables -t mangle -A OUTPUT -p udp -j openclash_output
fi
fi
# Accept TPROXY traffic in input chain (needed when zone input policy is REJECT)
if [ -z "$(iptables -nL INPUT 2>/dev/null | grep 'OpenClash TPROXY Accept')" ]; then
iptables -I INPUT -m mark --mark "$PROXY_FWMARK" -j ACCEPT -m comment --comment "OpenClash TPROXY Accept"
fi
if [ -z "$en_mode_tun" ]; then
#quic
if [ "$disable_udp_quic" -eq 1 ]; then
if [ "$china_ip_route" = "2" ]; then
@ -2584,6 +2643,11 @@ if [ -z "$FW4" ]; then
ip6tables -t nat -A PREROUTING -p tcp -j openclash
fi
# Accept redirected IPv6 traffic in input chain (needed when zone input policy is REJECT without DNAT rules)
if [ -z "$(ip6tables-save -t filter 2>/dev/null | grep 'OpenClash Redirect Accept')" ]; then
ip6tables -I INPUT -m conntrack --ctstate DNAT -j ACCEPT -m comment --comment "OpenClash Redirect Accept"
fi
#TProxy & TUN & Redirect udp
if [ "$enable_v6_udp_proxy" -eq 1 ] || [ "$ipv6_mode" -ne 1 ]; then
ip6tables -t mangle -N openclash
@ -2655,6 +2719,11 @@ if [ -z "$FW4" ]; then
ip6tables -t mangle -A PREROUTING -j openclash
# Accept TPROXY IPv6 traffic in input chain (needed when zone input policy is REJECT)
if [ -z "$(ip6tables-save -t filter 2>/dev/null | grep 'OpenClash TPROXY Accept')" ]; then
ip6tables -I INPUT -m mark --mark "$PROXY_FWMARK" -j ACCEPT -m comment --comment "OpenClash TPROXY Accept"
fi
#router self proxy
if [ "$router_self_proxy" = "1" ]; then
if [ "$ipv6_mode" -eq 1 ] || [ "$ipv6_mode" -eq 3 ]; then
@ -2987,6 +3056,8 @@ overwrite_file()
EOF
allowed_keys_types="\
AGE_SECRET_KEY:string \
AGE_PUBLIC_KEY:string \
APPEND_DEFAULT_DNS:int_bool \
APPEND_WAN_DNS:int_bool \
AUTO_SMART_SWITCH:int_bool \
@ -3118,6 +3189,11 @@ EOF
echo "# --- overwrite source: ${name} (sid=${sid}) ---" >> "$overwrite_script"
echo "export OPENCLASH_OVERWRITE_SID='${sid}'" >> "$overwrite_script"
cfg_name=$(basename "$(uci_get_config "config_path")" 2>/dev/null)
age_config_name="${cfg_name%.*}"
age_secret_key=""
age_public_key=""
param=$(uci -q get openclash."$sid".param 2>/dev/null || echo '')
if [ -n "$param" ]; then
OLD_IFS="$IFS"
@ -3251,8 +3327,9 @@ EOF
uci -q set openclash.@overwrite[0].config_path="$RAW_CONFIG_FILE"
cfg_name=$(basename "$RAW_CONFIG_FILE" 2>/dev/null)
if [ -n "$cfg_name" ]; then
age_config_name="${cfg_name%.*}"
CONFIG_FILE="/etc/openclash/${cfg_name}"
TMP_CONFIG_FILE="/tmp/yaml_config_tmp_${cfg_name}"
TMP_CONFIG_FILE="/tmp/${cfg_name}"
fi
elif printf "%s" "$key_u" | grep -q "^SUB_INFO_URL$"; then
val_clean=$(printf "%s" "$val" | sed "s/^[[:space:]]*['\"]//;s/['\"][[:space:]]*$//")
@ -3265,6 +3342,16 @@ EOF
uci -q set openclash.@subscribe_info[-1].name="${cfg_name%.*}"
uci -q add_list openclash.@subscribe_info[-1].url="$url_key"
fi
elif printf "%s" "$key_u" | grep -q "^AGE_SECRET_KEY$"; then
val_clean=$(printf "%s" "$val" | sed "s/^[[:space:]]*['\"]//;s/['\"][[:space:]]*$//")
val_key=$(eval "echo \"$val_clean\"")
[ -z "$val_key" ] && continue
age_secret_key="$val_key"
elif printf "%s" "$key_u" | grep -q "^AGE_PUBLIC_KEY$"; then
val_clean=$(printf "%s" "$val" | sed "s/^[[:space:]]*['\"]//;s/['\"][[:space:]]*$//")
val_key=$(eval "echo \"$val_clean\"")
[ -z "$val_key" ] && continue
age_public_key="$val_key"
elif printf "%s" "$key_u" | grep -q "^RESTART$"; then
val_clean=$(printf "%s" "$val" | sed "s/^[[:space:]]*['\"]//;s/['\"][[:space:]]*$//")
val_key=$(eval "echo \"$val_clean\"")
@ -3295,6 +3382,10 @@ EOF
fi
done < "$file"
if [ -n "$age_config_name" ] && { [ -n "$age_secret_key" ] || [ -n "$age_public_key" ]; }; then
uci_set_age_keys_by_name "$age_config_name" "$age_secret_key" "$age_public_key"
fi
if [ -n "$yaml_content" ]; then
LOG_TIP "Load YAML Override Block【YAML Block => $name】"
@ -3377,7 +3468,10 @@ get_config()
RAW_CONFIG_FILE=$(uci_get_config "config_path")
CFG_NAME=$(basename "$RAW_CONFIG_FILE" 2>/dev/null)
CONFIG_FILE="/etc/openclash/${CFG_NAME}"
TMP_CONFIG_FILE="/tmp/yaml_config_tmp_${CFG_NAME}"
TMP_CONFIG_FILE="/tmp/${CFG_NAME}"
CFG_NO_EXT_NAME="${CFG_NAME%.*}"
SECRET_KEY=$(uci_get_age_secret_keys "$CFG_NO_EXT_NAME")
OIX_TOKEN=$(uci_get_config "oix_token")
enable=$(uci_get_config "enable")
enable_custom_clash_rules=$(uci_get_config "enable_custom_clash_rules")
da_password=$(uci_get_config "dashboard_password")
@ -3662,20 +3756,6 @@ stop_service()
echo "OpenClash Already Stop!"
}
revert_dnsmasq()
{
redirect_dns=$(uci_get_config "redirect_dns")
dnsmasq_server=$(uci_get_config "dnsmasq_server")
dnsmasq_noresolv=$(uci_get_config "dnsmasq_noresolv")
dnsmasq_resolvfile=$(uci_get_config "dnsmasq_resolvfile")
cachesize_dns=$(uci_get_config "cachesize_dns")
dnsmasq_cachesize=$(uci_get_config "dnsmasq_cachesize")
filter_aaaa_dns=$(uci_get_config "filter_aaaa_dns")
dnsmasq_filter_aaaa=$(uci_get_config "dnsmasq_filter_aaaa")
default_resolvfile=$(uci_get_config "default_resolvfile")
revert_dns "$redirect_dns" "$enable" "$default_resolvfile" "$dnsmasq_noresolv" "$dnsmasq_resolvfile" "$cachesize_dns" "$dnsmasq_cachesize" "$filter_aaaa_dns" "$dnsmasq_filter_aaaa" "$dnsmasq_server"
} >/dev/null 2>&1
restart()
{
echo "OpenClash Restart..."

File diff suppressed because one or more lines are too long

View File

@ -123,6 +123,8 @@
# SKIP_PROXY_ADDRESS (跳过代理地址, 0: 禁用, 1: 启用)
# SUB_INFO_URL (订阅信息 URL)
# DOWNLOAD_FILE (下载外部文件到指定路径, need URL and path)
# AGE_PUBLIC_KEY (指定配置文件的 AGE 公钥)
# AGE_SECRET_KEY (指定配置文件的 AGE 私钥)
# RESTART (模块更新后重启插件, true: 启用, false: 禁用)
# =========================

View File

@ -25,6 +25,7 @@ uci -q commit firewall
mkdir -p /etc/openclash/config
mkdir -p /etc/openclash/proxy_provider
mkdir -p /etc/openclash/rule_provider
mkdir -p /etc/openclash/core
mkdir -p /etc/openclash/history
mkdir -p /usr/share/openclash/backup/overwrite
@ -151,6 +152,7 @@ cp -f "/etc/openclash/custom/openclash_custom_overwrite.sh" "/usr/share/openclas
cp -f "/etc/openclash/china_ip_route.ipset" "/usr/share/openclash/backup/china_ip_route.ipset" >/dev/null 2>&1
cp -f "/etc/openclash/china_ip6_route.ipset" "/usr/share/openclash/backup/china_ip6_route.ipset" >/dev/null 2>&1
cp -f "/etc/openclash/overwrite/default" "/usr/share/openclash/backup/overwrite/default" >/dev/null 2>&1
cp -f "/etc/openclash/rule_provider/oc-cn-domain.mrs" "/usr/share/openclash/backup/oc-cn-domain.mrs" >/dev/null 2>&1
#Restore
if [ -f "/tmp/openclash.bak" ]; then
@ -186,6 +188,10 @@ if [ -f "/tmp/openclash.bak" ]; then
cp -rf "/tmp/pac/." "/www/luci-static/resources/openclash/pac/" >/dev/null 2>&1
rm -rf "/tmp/pac/" >/dev/null 2>&1
fi
#oc-domain
if [ -f "/etc/openclash/rule_provider/oc-cn-domain.mrs" ]; then
cp -f "/usr/share/openclash/backup/oc-cn-domain.mrs" "/etc/openclash/rule_provider/oc-cn-domain.mrs" >/dev/null 2>&1
fi
rm -rf "/etc/openclash/openclash" >/dev/null 2>&1
rm -rf "/tmp/openclash" >/dev/null 2>&1
rm -rf "/tmp/openclash.bak" >/dev/null 2>&1

View File

@ -22,9 +22,47 @@ module YAML
end
def self.load_file(filename, *args, **kwargs)
yaml_content = File.read(filename)
processed_content = fix_short_id_quotes(yaml_content)
yaml_content = File.read(filename, mode: "r:bom|utf-8")
secret = nil
if kwargs.key?(:secret)
secret = kwargs.delete(:secret)
end
if secret && secret.to_s.strip != "" && yaml_content.include?("BEGIN AGE ENCRYPTED FILE")
begin
decrypted = decrypt_content_with_secret(secret.to_s, yaml_content)
if decrypted && !decrypted.empty? && !decrypted.include?("BEGIN AGE ENCRYPTED FILE")
processed = fix_short_id_quotes(decrypted)
return load(processed, *args, **kwargs)
else
LOG_WARN("【%s】Decrypted content empty or still encrypted" % [filename])
end
rescue => e
LOG_WARN("Decrypt attempt failed:【%s】" % [e.message])
end
end
if (secret.nil? || secret.to_s.strip == "") && yaml_content.include?("BEGIN AGE ENCRYPTED FILE")
keys = find_age_keys_for_filename(filename)
(keys[:secrets] || []).each do |sec|
begin
decrypted = decrypt_content_with_secret(sec, yaml_content)
if decrypted && !decrypted.empty? && !decrypted.include?("BEGIN AGE ENCRYPTED FILE")
processed = fix_short_id_quotes(decrypted)
return load(processed, *args, **kwargs)
end
rescue => e
LOG_WARN("Decrypt attempt failed for a found secret:【%s】" % [e.message])
end
end
end
if yaml_content.include?("BEGIN AGE ENCRYPTED FILE")
raise "Encrypted file: decryption failed for %s" % [filename]
end
processed_content = fix_short_id_quotes(yaml_content)
load(processed_content, *args, **kwargs)
end
@ -32,6 +70,45 @@ module YAML
begin
yaml_content = original_dump(obj, **options)
processed = fix_short_id_quotes(yaml_content)
public_key = nil
fname = nil
if options.key?(:public)
public_key = options.delete(:public)
end
if (!public_key || public_key.to_s.strip == "")
if options.key?(:filename)
fname = options.delete(:filename)
elsif io && io.respond_to?(:path)
fname = io.path
elsif io && io.respond_to?(:to_path)
fname = io.to_path
end
if fname && fname.to_s.strip != ""
keys = find_age_keys_for_filename(fname)
public_key = keys[:publics].first if keys[:publics] && !keys[:publics].empty?
end
end
if public_key && public_key.to_s.strip != ""
begin
encrypted = encrypt_content_with_public(public_key.to_s, processed)
if encrypted && !encrypted.empty?
if io.nil?
return encrypted
elsif io.respond_to?(:write)
io.write(encrypted)
return io
else
return encrypted
end
end
rescue => e
LOG_WARN("Encrypt attempt failed:【%s】" % [e.message])
end
end
if io.nil?
processed
elsif io.respond_to?(:write)
@ -46,6 +123,77 @@ module YAML
end
end
def self.popen_stream(cmd, input, chunk_size: 64 * 1024)
output = String.new
IO.popen(cmd, 'r+', err: [:child, :out]) do |io|
io.binmode
writer = Thread.new do
begin
input.bytesize.times do |i|
chunk = input.byteslice(i * chunk_size, chunk_size)
break if chunk.nil?
io.write(chunk)
end
io.close_write
rescue Errno::EPIPE
end
end
while chunk = io.read(chunk_size)
output << chunk
end
writer.join
end
[output, $?]
end
def self.decode64(input)
out, status = popen_stream(["base64", "-d"], input)
status.success? ? out : input
rescue Errno::ENOENT
input
end
def self.find_age_keys_for_filename(filename)
begin
basename = File.basename(filename)
basename_no_ext = File.basename(filename, File.extname(filename))
publics = []
secrets = []
[basename, basename_no_ext].uniq.each do |n|
cmd_public = ["/bin/sh", "-c", ". /usr/share/openclash/uci.sh; uci_get_age_public_keys \"$1\"", "sh", n]
IO.popen(cmd_public, "r") do |io|
io.each_line { |l| publics << l.strip unless l.nil? || l.strip == "" }
end
cmd_secret = ["/bin/sh", "-c", ". /usr/share/openclash/uci.sh; uci_get_age_secret_keys \"$1\"", "sh", n]
IO.popen(cmd_secret, "r") do |io|
io.each_line { |l| secrets << l.strip unless l.nil? || l.strip == "" }
end
end
{ publics: publics, secrets: secrets }
rescue => e
{ publics: [], secrets: [] }
end
end
def self.decrypt_content_with_secret(secret, content)
cmd = ['/etc/openclash/core/clash_meta', 'age', 'decrypt', secret, '-', '-']
out, status = popen_stream(cmd, content)
status.success? ? out : nil
rescue => e
nil
end
def self.encrypt_content_with_public(public, content)
cmd = ['/etc/openclash/core/clash_meta', 'age', 'encrypt', public, '-', '-']
out, status = popen_stream(cmd, content)
status.success? ? out : nil
rescue => e
nil
end
private
# fix_short_id_quotes:
@ -65,6 +213,8 @@ module YAML
# Input: short-id: null -> Output: short-id: ""
def self.fix_short_id_quotes(yaml_content)
yaml_content = decode64(yaml_content)
return yaml_content unless yaml_content.include?('short-id:')
begin

View File

@ -17,18 +17,30 @@ set_lock
DOWNLOAD_FILE="/tmp/clash_last_version"
RELEASE_BRANCH=$(uci_get_config "release_branch" || echo "master")
github_address_mod=$(uci_get_config "github_address_mod" || echo 0)
CORE_TYPE=$(uci_get_config "core_type")
OIX_TOKEN=$(uci_get_config "oix_token")
if [ -n "$1" ]; then
github_address_mod="$1"
fi
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}gh/vernesong/OpenClash@core/${RELEASE_BRANCH}/core_version"
if [ "$CORE_TYPE" = "Oix" ] || [ -n "$OIX_TOKEN" ]; then
OIX_VERSION_URL="https://github.com/vernesong/mihomo-oix/releases/download/Pre-Alpha/version.txt"
if [ "$github_address_mod" != "0" ] && [ "$github_address_mod" != "https://cdn.jsdelivr.net/" ] && [ "$github_address_mod" != "https://fastly.jsdelivr.net/" ] && [ "$github_address_mod" != "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}${OIX_VERSION_URL}"
else
DOWNLOAD_URL="${github_address_mod}https://raw.githubusercontent.com/vernesong/OpenClash/core/${RELEASE_BRANCH}/core_version"
DOWNLOAD_URL="$OIX_VERSION_URL"
fi
else
DOWNLOAD_URL="https://raw.githubusercontent.com/vernesong/OpenClash/core/${RELEASE_BRANCH}/core_version"
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}gh/vernesong/OpenClash@core/${RELEASE_BRANCH}/core_version"
else
DOWNLOAD_URL="${github_address_mod}https://raw.githubusercontent.com/vernesong/OpenClash/core/${RELEASE_BRANCH}/core_version"
fi
else
DOWNLOAD_URL="https://raw.githubusercontent.com/vernesong/OpenClash/core/${RELEASE_BRANCH}/core_version"
fi
fi
DOWNLOAD_FILE_CURL "$DOWNLOAD_URL" "$DOWNLOAD_FILE" "$DOWNLOAD_FILE"

View File

@ -49,7 +49,7 @@ config_test()
{
if [ -f "$CLASH" ]; then
LOG_OUT "Config File Download Successful, Test If There is Any Errors..."
test_info=$($CLASH -t -d $CLASH_CONFIG -f "$CFG_FILE")
test_info=$($CLASH -t -d $CLASH_CONFIG -f "$CFG_FILE" -age-secret-key "$SECRET_KEY")
local IFS=$'\n'
for i in $test_info; do
if [ -n "$(echo "$i" |grep "configuration file")" ]; then
@ -72,15 +72,14 @@ config_download()
LOG_TIP "Config File【$name】Downloading User-Agent【$sub_ua】..."
if [ -n "$subscribe_url_param" ] && [ -n "$c_address" ]; then
LOG_INFO "Config File【$name】Downloading URL【$c_address$subscribe_url_param】..."
local DOWNLOAD_URL="${c_address}${subscribe_url_param}"
local DOWNLOAD_PARAM="$sub_ua"
DOWNLOAD_URL="${c_address}${subscribe_url_param}"
fi
if [ -z "$DOWNLOAD_URL" ]; then
LOG_INFO "Config File【$name】Downloading URL【$subscribe_url】..."
local DOWNLOAD_URL="${subscribe_url}"
local DOWNLOAD_PARAM="$sub_ua"
DOWNLOAD_URL="${subscribe_url}"
fi
DOWNLOAD_FILE_CURL "$DOWNLOAD_URL" "$CFG_FILE" "$CONFIG_FILE" "$DOWNLOAD_PARAM"
DOWNLOAD_PARAM="$sub_ua"
DOWNLOAD_FILE_CURL "$DOWNLOAD_URL" "$CFG_FILE" "$CONFIG_FILE" "$DOWNLOAD_PARAM" "$SECRET_KEY"
DOWNLOAD_RESULT=$?
}
@ -317,7 +316,7 @@ convert_custom_param()
sub_info_get()
{
local section="$1" subscribe_url template_path subscribe_url_param template_path_encode key_match_param key_ex_match_param c_address de_ex_keyword sub_ua append_custom_params
local section="$1" subscribe_url template_path subscribe_url_param template_path_encode key_match_param key_ex_match_param c_address de_ex_keyword sub_ua append_custom_params SECRET_KEY DOWNLOAD_URL DOWNLOAD_PARAM
config_get_bool "enabled" "$section" "enabled" "1"
config_get "name" "$section" "name" "config"
config_get "sub_convert" "$section" "sub_convert" ""
@ -335,6 +334,7 @@ sub_info_get()
config_get "custom_template_url" "$section" "custom_template_url" ""
config_get "de_ex_keyword" "$section" "de_ex_keyword" ""
config_get "sub_ua" "$section" "sub_ua" "clash-verge/v2.4.5"
SECRET_KEY=$(uci_get_age_secret_keys "$name")
CONFIG_FILE="/etc/openclash/config/$name.yaml"
CFG_FILE="/tmp/$name.yaml"

View File

@ -44,8 +44,8 @@ if [ -z "$CHNR_CUSTOM_URL" ]; then
else
DOWNLOAD_FILE_CURL "$CHNR_CUSTOM_URL" "/tmp/china_ip_route.txt" "$chnr_path"
fi
if [ "$?" -eq 0 ]; then
DOWNLOAD_RESULT=$?
if [ "$DOWNLOAD_RESULT" -eq 0 ]; then
LOG_OUT "Chnroute Cidr List Download Success, Check Updated..."
#预处理
if [ -n "$FW4" ]; then
@ -69,7 +69,7 @@ if [ "$?" -eq 0 ]; then
else
LOG_OUT "Updated Chnroute Cidr List No Change, Do Nothing..."
fi
elif [ "$?" -eq 2 ]; then
elif [ "$DOWNLOAD_RESULT" -eq 2 ]; then
LOG_OUT "Updated Chnroute Cidr List No Change, Do Nothing..."
else
LOG_OUT "Chnroute Cidr List Update Error, Please Try Again Later..."

View File

@ -35,7 +35,9 @@ fi
CORE_TYPE="$1"
C_CORE_TYPE=$(uci_get_config "core_type")
SMART_ENABLE=$(uci_get_config "smart_enable" || echo 0)
OIX_TOKEN=$(uci_get_config "oix_token")
[ "$SMART_ENABLE" -eq 1 ] && CORE_TYPE="Smart"
[ "$CORE_TYPE" = "Oix" ] || [ -n "$OIX_TOKEN" ] && CORE_TYPE="Oix"
[ -z "$CORE_TYPE" ] && CORE_TYPE="Meta"
small_flash_memory=$(uci_get_config "small_flash_memory")
CPU_MODEL=$(uci_get_config "core_version")
@ -62,31 +64,45 @@ else
fi
CORE_CV=$($meta_core_path -v 2>/dev/null |awk -F ' ' '{print $3}' |head -1)
DOWNLOAD_FILE="/tmp/clash_meta.tar.gz"
TMP_FILE="/tmp/clash_meta"
TARGET_CORE_PATH="$meta_core_path"
if [ "$CORE_TYPE" = "Smart" ]; then
if [ "$CORE_TYPE" = "Oix" ]; then
CORE_URL_PATH=""
DOWNLOAD_FILE="/tmp/clash_meta.gz"
CORE_LV=$(sed -n 1p /tmp/clash_last_version 2>/dev/null)
elif [ "$CORE_TYPE" = "Smart" ]; then
CORE_URL_PATH="$RELEASE_BRANCH/smart"
DOWNLOAD_FILE="/tmp/clash_meta.tar.gz"
CORE_LV=$(sed -n 2p /tmp/clash_last_version 2>/dev/null)
else
CORE_URL_PATH="$RELEASE_BRANCH/meta"
DOWNLOAD_FILE="/tmp/clash_meta.tar.gz"
CORE_LV=$(sed -n 1p /tmp/clash_last_version 2>/dev/null)
fi
[ "$C_CORE_TYPE" = "$CORE_TYPE" ] || [ -z "$C_CORE_TYPE" ] && restart=1
[ "$C_CORE_TYPE" != "$CORE_TYPE" ] || [ -z "$C_CORE_TYPE" ] && restart=1
if [ "$CORE_CV" != "$CORE_LV" ] || [ -z "$CORE_CV" ]; then
if [ "$CPU_MODEL" != 0 ]; then
LOG_TIP "$CORE_TYPE】Core Downloading, Please Try to Download and Upload Manually If Fails"
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}gh/vernesong/OpenClash@core/${CORE_URL_PATH}/clash-${CPU_MODEL}.tar.gz"
if [ "$CORE_TYPE" = "Oix" ]; then
OIX_CORE_URL="https://github.com/vernesong/mihomo-oix/releases/download/Pre-Alpha/mihomo-${CPU_MODEL}-${CORE_LV}.gz"
if [ "$github_address_mod" != "0" ] && [ "$github_address_mod" != "https://cdn.jsdelivr.net/" ] && [ "$github_address_mod" != "https://fastly.jsdelivr.net/" ] && [ "$github_address_mod" != "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}${OIX_CORE_URL}"
else
DOWNLOAD_URL="${github_address_mod}https://raw.githubusercontent.com/vernesong/OpenClash/core/${CORE_URL_PATH}/clash-${CPU_MODEL}.tar.gz"
DOWNLOAD_URL="$OIX_CORE_URL"
fi
else
DOWNLOAD_URL="https://raw.githubusercontent.com/vernesong/OpenClash/core/${CORE_URL_PATH}/clash-${CPU_MODEL}.tar.gz"
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}gh/vernesong/OpenClash@core/${CORE_URL_PATH}/clash-${CPU_MODEL}.tar.gz"
else
DOWNLOAD_URL="${github_address_mod}https://raw.githubusercontent.com/vernesong/OpenClash/core/${CORE_URL_PATH}/clash-${CPU_MODEL}.tar.gz"
fi
else
DOWNLOAD_URL="https://raw.githubusercontent.com/vernesong/OpenClash/core/${CORE_URL_PATH}/clash-${CPU_MODEL}.tar.gz"
fi
fi
retry_count=0
@ -107,8 +123,12 @@ if [ "$CORE_CV" != "$CORE_LV" ] || [ -z "$CORE_CV" ]; then
LOG_TIP "【"$CORE_TYPE"】Core Download Successful, Start Update..."
extract_success=true
[ -s "$DOWNLOAD_FILE" ] && {
tar zxvfo "$DOWNLOAD_FILE" -C /tmp >/dev/null 2>&1 || extract_success=false
mv /tmp/clash "$TMP_FILE" >/dev/null 2>&1 || extract_success=false
if [ "$CORE_TYPE" = "Oix" ]; then
gzip -dc "$DOWNLOAD_FILE" > "$TMP_FILE" 2>/dev/null || extract_success=false
else
tar zxvfo "$DOWNLOAD_FILE" -C /tmp >/dev/null 2>&1 || extract_success=false
mv /tmp/clash "$TMP_FILE" >/dev/null 2>&1 || extract_success=false
fi
rm -rf "$DOWNLOAD_FILE" >/dev/null 2>&1
chmod 4755 "$TMP_FILE" >/dev/null 2>&1 || extract_success=false
"$TMP_FILE" -v >/dev/null 2>&1 || extract_success=false

View File

@ -8,28 +8,54 @@ DOWNLOAD_FILE_CURL() {
DOWNLOAD_PATH=$2
FILE_PATH=$3
DOWNLOAD_UA=$4
SECRET_KEY=$5
[ -z "$DOWNLOAD_UA" ] && DOWNLOAD_UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
CURL_OUTPUT=$(curl -sLI --connect-timeout 5 -m 10 --speed-time 5 --speed-limit 1 --retry 2 \
-H "User-Agent: ${DOWNLOAD_UA}" "$DOWNLOAD_URL" 2>&1)
NEW_ETAG=$(echo "$CURL_OUTPUT" | grep -i "^etag:" | cut -d' ' -f2- | tr -d '\r\n' | sed 's/^"//;s/"$//')
HTTP_CODE=$(echo "$CURL_OUTPUT" | grep -i "^HTTP" | tail -1 | cut -d' ' -f2)
HEADER_TMP="/tmp/openclash_curl_header_$$"
DOWNLOAD_TMP="${DOWNLOAD_PATH}.download.$$"
CACHED_ETAG=$(GET_ETAG_BY_PATH "$FILE_PATH")
ETAG_HEADER=""
if [ "$HTTP_CODE" = "200" ] && [ -n "$NEW_ETAG" ] && [ "$NEW_ETAG" = "$CACHED_ETAG" ] && [ -e "$FILE_PATH" ]; then
return 2
if [ -n "$CACHED_ETAG" ] && [ -e "$FILE_PATH" ]; then
FILE_MTIME=$(date -r "$FILE_PATH" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)
LAST_UPDATE=$(GET_ETAG_TIMESTAMP_BY_PATH "$FILE_PATH")
if [ -n "$LAST_UPDATE" ] && [ -n "$FILE_MTIME" ] && [ "$LAST_UPDATE" = "$FILE_MTIME" ]; then
ETAG_HEADER="If-None-Match: \"${CACHED_ETAG}\""
fi
fi
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
if [ "$SHOW_DOWNLOAD_PROGRESS" = "1" ] || [ "$SHOW_DOWNLOAD_PROGRESS" = "true" ]; then
TEMP_LOG="/tmp/curl_log_$$"
LOG_OUT "Downloading:【$(basename "$DOWNLOAD_PATH") - 0%】"
(
curl -# -L --connect-timeout 10 -m 60 --speed-time 20 --speed-limit 1 --retry 2 \
-H "User-Agent: ${DOWNLOAD_UA}" \
"$DOWNLOAD_URL" -o "$DOWNLOAD_PATH" 2>"$TEMP_LOG"
if [ -n "$SECRET_KEY" ] && [ -n "$ETAG_HEADER" ]; then
curl -# -L --connect-timeout 30 -m 180 --speed-time 30 --speed-limit 1 --retry 2 \
-D "$HEADER_TMP" \
-H "User-Agent: ${DOWNLOAD_UA}" \
-H "X-Age-Public-Key: ${SECRET_KEY}" \
-H "$ETAG_HEADER" \
"$DOWNLOAD_URL" -o "$DOWNLOAD_TMP" 2>"$TEMP_LOG"
elif [ -n "$SECRET_KEY" ]; then
curl -# -L --connect-timeout 30 -m 180 --speed-time 30 --speed-limit 1 --retry 2 \
-D "$HEADER_TMP" \
-H "User-Agent: ${DOWNLOAD_UA}" \
-H "X-Age-Public-Key: ${SECRET_KEY}" \
"$DOWNLOAD_URL" -o "$DOWNLOAD_TMP" 2>"$TEMP_LOG"
elif [ -n "$ETAG_HEADER" ]; then
curl -# -L --connect-timeout 30 -m 180 --speed-time 30 --speed-limit 1 --retry 2 \
-D "$HEADER_TMP" \
-H "User-Agent: ${DOWNLOAD_UA}" \
-H "$ETAG_HEADER" \
"$DOWNLOAD_URL" -o "$DOWNLOAD_TMP" 2>"$TEMP_LOG"
else
curl -# -L --connect-timeout 30 -m 180 --speed-time 30 --speed-limit 1 --retry 2 \
-D "$HEADER_TMP" \
-H "User-Agent: ${DOWNLOAD_UA}" \
"$DOWNLOAD_URL" -o "$DOWNLOAD_TMP" 2>"$TEMP_LOG"
fi
echo $? > "${TEMP_LOG}.exit"
) &
@ -55,6 +81,7 @@ DOWNLOAD_FILE_CURL() {
wait $CURL_PID
EXIR_CODE=$(cat "${TEMP_LOG}.exit" 2>/dev/null || echo "1")
HTTP_CODE=$(grep -i "^HTTP" "$HEADER_TMP" 2>/dev/null | tail -1 | cut -d' ' -f2)
if [ "$EXIR_CODE" -eq 0 ] && [ "$LAST_PROGRESS" -ne 100 ]; then
LOG_OUT "Downloading:【$(basename "$DOWNLOAD_PATH") - 100%】"
@ -66,29 +93,82 @@ DOWNLOAD_FILE_CURL() {
rm -f "$TEMP_LOG" "${TEMP_LOG}.exit"
if [ "$EXIR_CODE" -ne 0 ]; then
if [ "$EXIR_CODE" -eq 0 ] && [ "$HTTP_CODE" = "304" ] && [ -e "$FILE_PATH" ]; then
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
return 2
fi
if [ "$EXIR_CODE" -ne 0 ] || [ "$HTTP_CODE" != "200" ]; then
LOG_OUT "$DOWNLOAD_PATH】Download Failed:【$OUTPUT"
rm -rf $DOWNLOAD_PATH
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
SLOG_CLEAN
return 1
fi
else
CURL_OUTPUT=$(curl -w "\n%{http_code}" -SsL --connect-timeout 30 -m 60 --speed-time 30 --speed-limit 1 --retry 2 -H "User-Agent: ${DOWNLOAD_UA}" "$DOWNLOAD_URL" -o "$DOWNLOAD_PATH" 2>&1)
EXIR_CODE=$?
HTTP_CODE=$(echo "$CURL_OUTPUT" | tail -n1)
DOWNLOAD_TRY=0
MAX_DOWNLOAD_RETRIES=3
while [ "$DOWNLOAD_TRY" -lt "$MAX_DOWNLOAD_RETRIES" ]; do
DOWNLOAD_TRY=$((DOWNLOAD_TRY + 1))
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
if [ -n "$SECRET_KEY" ] && [ -n "$ETAG_HEADER" ]; then
CURL_OUTPUT=$(curl -w "\n%{http_code}" -SsL --connect-timeout 30 -m 180 --speed-time 30 --speed-limit 1 --retry 2 \
-D "$HEADER_TMP" \
-H "User-Agent: ${DOWNLOAD_UA}" \
-H "X-Age-Public-Key: ${SECRET_KEY}" \
-H "$ETAG_HEADER" \
"$DOWNLOAD_URL" -o "$DOWNLOAD_TMP" 2>&1)
elif [ -n "$SECRET_KEY" ]; then
CURL_OUTPUT=$(curl -w "\n%{http_code}" -SsL --connect-timeout 30 -m 180 --speed-time 30 --speed-limit 1 --retry 2 \
-D "$HEADER_TMP" \
-H "User-Agent: ${DOWNLOAD_UA}" \
-H "X-Age-Public-Key: ${SECRET_KEY}" \
"$DOWNLOAD_URL" -o "$DOWNLOAD_TMP" 2>&1)
elif [ -n "$ETAG_HEADER" ]; then
CURL_OUTPUT=$(curl -w "\n%{http_code}" -SsL --connect-timeout 30 -m 180 --speed-time 30 --speed-limit 1 --retry 2 \
-D "$HEADER_TMP" \
-H "User-Agent: ${DOWNLOAD_UA}" \
-H "$ETAG_HEADER" \
"$DOWNLOAD_URL" -o "$DOWNLOAD_TMP" 2>&1)
else
CURL_OUTPUT=$(curl -w "\n%{http_code}" -SsL --connect-timeout 30 -m 180 --speed-time 30 --speed-limit 1 --retry 2 \
-D "$HEADER_TMP" \
-H "User-Agent: ${DOWNLOAD_UA}" "$DOWNLOAD_URL" -o "$DOWNLOAD_TMP" 2>&1)
fi
EXIR_CODE=$?
HTTP_CODE=$(echo "$CURL_OUTPUT" | tail -n1)
if { [ "$EXIR_CODE" -eq 0 ] && [ "$HTTP_CODE" = "200" ]; } || { [ "$EXIR_CODE" -eq 0 ] && [ "$HTTP_CODE" = "304" ] && [ -e "$FILE_PATH" ]; }; then
break
fi
[ "$DOWNLOAD_TRY" -lt "$MAX_DOWNLOAD_RETRIES" ] && sleep 1
done
if [ "$EXIR_CODE" -ne 0 ] || [ "$HTTP_CODE" -ne 200 ]; then
if [ "$EXIR_CODE" -eq 0 ] && [ "$HTTP_CODE" = "304" ] && [ -e "$FILE_PATH" ]; then
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
return 2
fi
if [ "$EXIR_CODE" -ne 0 ] || [ "$HTTP_CODE" != "200" ]; then
OUTPUT=$(echo "$CURL_OUTPUT" | sed '$d' | grep -a 'curl:' | tail -n 1)
LOG_OUT "$DOWNLOAD_PATH】Download Failed:【$OUTPUT"
rm -rf $DOWNLOAD_PATH
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
SLOG_CLEAN
return 1
fi
fi
if ! mv -f "$DOWNLOAD_TMP" "$DOWNLOAD_PATH"; then
LOG_OUT "$DOWNLOAD_PATH】Download Failed:【Unable to save download file】"
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
SLOG_CLEAN
return 1
fi
NEW_ETAG=$(grep -i "^etag:" "$HEADER_TMP" 2>/dev/null | tail -1 | cut -d' ' -f2- | tr -d '\r\n' | sed 's/^"//;s/"$//')
if [ -n "$NEW_ETAG" ] && [ "$HTTP_CODE" = "200" ]; then
SAVE_ETAG_TO_CACHE "$DOWNLOAD_URL" "$NEW_ETAG" "$FILE_PATH"
fi
rm -f "$HEADER_TMP" "$DOWNLOAD_TMP"
return 0
}
}

View File

@ -16,9 +16,9 @@ del_lock() {
ipk_v()
{
if [ -x "/bin/opkg" ]; then
echo $(opkg status "$1" 2>/dev/null |grep 'Version' |awk -F ': ' '{print $2}' 2>/dev/null)
echo $(rm -f /var/lock/opkg.lock && opkg status "$1" 2>/dev/null |grep 'Version' |awk -F ': ' '{print $2}' 2>/dev/null)
elif [ -x "/usr/bin/apk" ]; then
echo $(apk list "$1" 2>/dev/null |grep 'installed' | grep -oE '\d+(\.\d+)*' | head -1)
echo $(rm -f /lib/apk/db/lock && apk list "$1" 2>/dev/null |grep 'installed' | grep -oE '\d+(\.\d+)*' | head -1)
fi
}
@ -42,9 +42,9 @@ RAW_CONFIG_FILE=$(uci_get_config "config_path")
CONFIG_FILE="/etc/openclash/$(uci_get_config "config_path" |awk -F '/' '{print $5}' 2>/dev/null)"
core_model=$(uci_get_config "core_version")
if [ -x "/bin/opkg" ]; then
cpu_model=$(opkg status libc 2>/dev/null |grep 'Architecture' |awk -F ': ' '{print $2}' 2>/dev/null)
cpu_model=$(rm -f /var/lock/opkg.lock && opkg status libc 2>/dev/null |grep 'Architecture' |awk -F ': ' '{print $2}' 2>/dev/null)
elif [ -x "/usr/bin/apk" ]; then
cpu_model=$(apk list libc 2>/dev/null|awk '{print $2}')
cpu_model=$(rm -f /lib/apk/db/lock && apk list libc 2>/dev/null|awk '{print $2}')
fi
core_meta_version=$(/etc/openclash/core/clash_meta -v 2>/dev/null |awk -F ' ' '{print $3}' |head -1 2>/dev/null)
op_version=$(ipk_v "luci-app-openclash")
@ -54,6 +54,19 @@ router_self_proxy=$(uci_get_config "router_self_proxy")
core_type=$(uci_get_config "core_type" || echo "Dev")
da_password=$(uci_get_config "dashboard_password")
cn_port=$(uci_get_config "cn_port")
stack_type=$(uci_get_config "stack_type")
delay_start=$(uci_get_config "delay_start")
log_size=$(uci_get_config "log_size")
bypass_gateway_compatible=$(uci_get_config "bypass_gateway_compatible")
disable_quic_go_gso=$(uci_get_config "disable_quic_go_gso")
small_flash_memory=$(uci_get_config "small_flash_memory")
enable_meta_sniffer=$(uci_get_config "enable_meta_sniffer")
enable_respect_rules=$(uci_get_config "enable_respect_rules")
skip_proxy_address=$(uci_get_config "skip_proxy_address")
disable_udp_quic=$(uci_get_config "disable_udp_quic")
lan_ac_mode=$(uci_get_config "lan_ac_mode")
ipv6_mode=$(uci_get_config "ipv6_mode")
china_ip6_route=$(uci_get_config "china_ip6_route")
lan_interface_name=$(uci_get_config "lan_interface_name" || echo "0")
if [ "$lan_interface_name" = "0" ]; then
lan_ip=$(uci -q get network.lan.ipaddr |awk -F '/' '{print $1}' 2>/dev/null || ip address show $(uci -q -p /tmp/state get network.lan.device || 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 || ip addr show 2>/dev/null | grep -w 'inet' | grep 'global' | grep 'brd' | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -n 1)
@ -122,6 +135,12 @@ cat >> "$DEBUG_LOG" <<-EOF
LuCI版本: $(ipk_v "luci")
内核版本: $(uname -r 2>/dev/null)
处理器架构: $cpu_model
系统运行时间: $(uptime 2>/dev/null)
#磁盘与内存
$(df -h / /tmp /etc/openclash 2>/dev/null)
$(free -m 2>/dev/null)
$(cat /proc/meminfo 2>/dev/null | grep -E '^(MemTotal|MemAvailable|SwapTotal|SwapFree)')
#此项有值时,如不使用IPv6,建议到网络-接口-lan的设置中禁用IPV6的DHCP
IPV6-DHCP: $(uci -q get dhcp.lan.dhcpv6)
@ -129,6 +148,8 @@ IPV6-DHCP: $(uci -q get dhcp.lan.dhcpv6)
DNS劫持: $(dns_re "$enable_redirect_dns")
#DNS劫持为Dnsmasq时此项结果应仅有配置文件的DNS监听地址
Dnsmasq转发设置: $(uci -q get dhcp.@dnsmasq[0].server)
Dnsmasq完整配置:
$(uci show dhcp.@dnsmasq[0] 2>/dev/null)
EOF
cat >> "$DEBUG_LOG" <<-EOF
@ -147,6 +168,8 @@ ruby: $(ts_re "$(ipk_v "ruby")")
ruby-yaml: $(ts_re "$(ipk_v "ruby-yaml")")
ruby-psych: $(ts_re "$(ipk_v "ruby-psych")")
ruby-pstore: $(ts_re "$(ipk_v "ruby-pstore")")
ruby版本: $(ruby --version 2>/dev/null || echo "未安装")
ruby功能测试: $(ruby -e "require 'yaml'; YAML.load('test: ok'); puts '正常'" 2>/dev/null || echo "异常")
kmod-tun(TUN模式): $(ts_re "$(ipk_v "kmod-tun")")
luci-compat(Luci >= 19.07): $(ts_re "$(ipk_v "luci-compat")")
kmod-inet-diag(PROCESS-NAME): $(ts_re "$(ipk_v "kmod-inet-diag")")
@ -166,6 +189,12 @@ kmod-ipt-nat: $(ts_re "$(ipk_v "kmod-ipt-nat")")
EOF
fi
cat >> "$DEBUG_LOG" <<-EOF
#内核模块加载状态:
$(lsmod | grep -E 'tun|tproxy|inet_diag' 2>/dev/null || echo "无相关模块")
EOF
#core
cat >> "$DEBUG_LOG" <<-EOF
@ -219,6 +248,22 @@ fi
cat >> "$DEBUG_LOG" <<-EOF
#===================== GEO 数据文件 =====================#
$(ls -lh /etc/openclash/Country.mmdb /etc/openclash/GeoIP.dat /etc/openclash/GeoSite.dat /etc/openclash/ASN.mmdb 2>/dev/null)
#===================== 模型、缓存文件状态 =====================#
Model.bin: $(ls -lh /etc/openclash/Model.bin 2>/dev/null || echo "不存在")
cache.db: $(ls -lh /etc/openclash/cache.db 2>/dev/null || echo "不存在")
#===================== 冲突插件检测 =====================#
$(ps | grep -E 'passwall|ssr-plus|bypass|helloworld' | grep -v grep 2>/dev/null || echo "未检测到冲突插件")
EOF
cat >> "$DEBUG_LOG" <<-EOF
#===================== 插件设置 =====================#
当前配置文件: $RAW_CONFIG_FILE
@ -236,11 +281,31 @@ IPV6-DNS解析: $(ts_cf "$ipv6_dns")
仅允许常用端口流量: $(ts_cf "$common_ports")
绕过中国大陆IP: $(ts_cf "$china_ip_route")
路由本机代理: $(ts_cf "$router_self_proxy")
TUN堆栈类型: ${stack_type:-system}
启动延迟: ${delay_start:-0}
日志大小: ${log_size:-1024}KB
旁路由兼容: $(ts_cf "$bypass_gateway_compatible")
禁用quic-go GSO: $(ts_cf "$disable_quic_go_gso")
小闪存模式: $(ts_cf "$small_flash_memory")
域名嗅探: $(ts_cf "$enable_meta_sniffer")
DNS代理: $(ts_cf "$enable_respect_rules")
绕过服务器地址: $(ts_cf "$skip_proxy_address")
禁用QUIC: $(ts_cf "$disable_udp_quic")
访问控制模式: $([ "$lan_ac_mode" = "1" ] && echo "White" || echo "Black")
IPv6模式: ${ipv6_mode:-0}
绕过IPv6区域: $(ts_cf "$china_ip6_route")
EOF
cat >> "$DEBUG_LOG" <<-EOF
#===================== Cron 定时任务 =====================#
$(crontab -l 2>/dev/null | grep -i openclash || echo "无 OpenClash 相关 cron 任务")
EOF
cat >> "$DEBUG_LOG" <<-EOF
#===================== 覆写模块设置 =====================#
$(uci -q show openclash.@overwrite[0])
@ -408,6 +473,14 @@ netstat -nlp |grep clash >> "$DEBUG_LOG" 2>/dev/null
cat >> "$DEBUG_LOG" <<-EOF
#===================== 网络接口状态 =====================#
EOF
ip link show >> "$DEBUG_LOG" 2>/dev/null
ip addr show | grep -E 'inet |utun' >> "$DEBUG_LOG" 2>/dev/null
cat >> "$DEBUG_LOG" <<-EOF
#===================== 测试本机DNS查询(www.baidu.com) =====================#
EOF
@ -462,6 +535,15 @@ else
curl -SsIL -m 3 --retry 2 "$VERSION_URL" >> "$DEBUG_LOG" 2>/dev/null
fi
if pidof clash >/dev/null; then
cat >> "$DEBUG_LOG" <<-EOF
#===================== Mihomo API 健康检查 =====================#
EOF
curl -Ss -m 3 http://127.0.0.1:${cn_port}/version >> "$DEBUG_LOG" 2>/dev/null
fi
cat >> "$DEBUG_LOG" <<-EOF
#===================== 最近运行日志 (切换为Debug模式) =====================#

View File

@ -50,7 +50,7 @@ if [ -z "$CONFIG_FILE" ] || [ ! -f "$CONFIG_FILE" ]; then
fi
if [ -n "$(pidof clash)" ] && [ -f "$CONFIG_FILE" ]; then
if [ "$small_flash_memory" == "1" ] || [ -n "$(echo $core_version |grep mips)" ] || [ -n "$(echo $DISTRIB_ARCH |grep mips)" ] || [ -n "$(opkg status libc 2>/dev/null |grep 'Architecture' |awk -F ': ' '{print $2}' |grep mips)" ] || [ -n "$(apk list libc 2>/dev/null |grep mips)" ]; then
if [ "$small_flash_memory" == "1" ] || [ -n "$(echo $core_version |grep mips)" ] || [ -n "$(echo $DISTRIB_ARCH |grep mips)" ] || [ -n "$(rm -f /var/lock/opkg.lock && opkg status libc 2>/dev/null |grep 'Architecture' |awk -F ': ' '{print $2}' |grep mips)" ] || [ -n "$(rm -f /lib/apk/db/lock && apk list libc 2>/dev/null |grep mips)" ]; then
CACHE_PATH="/tmp/etc/openclash/cache.db"
if [ -f "$CACHE_PATH" ]; then
cmp -s "$CACHE_PATH" "$HISTORY_PATH"

View File

@ -0,0 +1,49 @@
#!/usr/bin/lua
require "nixio"
require "luci.util"
require "luci.sys"
local uci = require("luci.model.uci").cursor()
local fs = require "luci.openclash"
local json = require "luci.jsonc"
local function oix_checkin()
local info, path, checkin
local token = fs.uci_get_config("config", "oix_token")
local enable = fs.uci_get_config("config", "oix_checkin") or 0
local interval = fs.uci_get_config("config", "oix_checkin_interval") or 1
local multiple = fs.uci_get_config("config", "oix_checkin_multiple") or 1
path = "/tmp/oix_checkin"
if token and enable == "1" then
checkin = string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '{\"multiple\":\"%s\"}' -X POST https://oix-api.dler.io/api/v1/checkin -o %s", token, multiple, path)
if fs.readfile(path) == "" or not fs.readfile(path) then
luci.sys.exec(checkin)
else
if (os.time() - fs.mtime(path) > interval*3600+1) then
fs.unlink(path)
luci.sys.exec(checkin)
else
os.exit(0)
end
end
if fs.readfile(path) == "" or not fs.readfile(path) then
fs.writefile(path, " ")
end
info = fs.readfile(path)
if info then
info = json.parse(info)
end
if info and info.ret == 200 then
luci.sys.exec(string.format('echo "%s [Info] oixCloud Checkin Successful, Result:【%s】" >> /tmp/openclash.log', os.date("%Y-%m-%d %H:%M:%S"), info.data.checkin))
else
if info and info.msg then
luci.sys.exec(string.format('echo "%s [Info] oixCloud Checkin Failed, Result:【%s】" >> /tmp/openclash.log', os.date("%Y-%m-%d %H:%M:%S"), info.msg))
else
luci.sys.exec(string.format('echo "%s [Info] oixCloud Checkin Failed! Please Check And Try Again..." >> /tmp/openclash.log', os.date("%Y-%m-%d %H:%M:%S")))
end
end
end
os.exit(0)
end
oix_checkin()

View File

@ -709,7 +709,7 @@ function table_sort_by_cache(t)
end
end
if #tab > 0 then
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." Group:"..""..group_match_name.."".."Cached Compliant Nodes Number:"..""..#(tab)..""..", Cached Non-compliant Nodes Number:"..""..#(tab_b)..""..", Prioritize Testing With Cached Compliant Nodes...")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..group_match_name.."".."Cached Compliant Nodes Number:"..""..#(tab)..""..", Cached Non-compliant Nodes Number:"..""..#(tab_b)..""..", Prioritize Testing With Cached Compliant Nodes...")
end
for k,v in pairs(tab_b) do table.insert(tab, v) end
return tab

View File

@ -4,6 +4,7 @@
local nixio = require "nixio"
local jsonc = require "luci.jsonc"
local util = require "luci.util"
local fs = require "luci.openclash"
if not nixio.fs.access("/usr/bin/base64") and not nixio.fs.access("/bin/base64") then
os.exit(1)
@ -14,31 +15,8 @@ if not file_path or not nixio.fs.access(file_path) then
os.exit(1)
end
local function raw_base64_decode(str)
if not str or str == "" then return nil end
str = str:gsub("-", "+"):gsub("_", "/")
local padding = #str % 4
if padding > 0 then
str = str .. string.rep("=", 4 - padding)
end
local cmd = "printf '%s' '" .. str .. "' | base64 -d 2>/dev/null"
local f = io.popen(cmd, "r")
if not f then return nil end
local result = f:read("*a")
f:close()
if result and result ~= "" then
return result
end
return nil
end
local function base64_decode_subscription(str)
local result = raw_base64_decode(str)
local result = fs.decode64(str)
if result and result:find("://") then
return result
end
@ -126,7 +104,7 @@ local function get_server_from_url(line)
original_body = original_body:match("([^#]+)") or original_body
-- base64
local decoded = raw_base64_decode(original_body)
local decoded = fs.decode64(original_body)
if decoded then
-- V2RayN JSON
local ok, data = pcall(jsonc.parse, decoded)
@ -160,7 +138,7 @@ local function get_server_from_url(line)
else
-- ss://base64
original_body = original_body:match("([^#]+)") or original_body
local decoded = raw_base64_decode(original_body)
local decoded = fs.decode64(original_body)
if decoded then
server = decoded:match("@([^:/]+)")
else
@ -170,7 +148,7 @@ local function get_server_from_url(line)
elseif scheme == "ssr" then
original_body = original_body:match("([^#]+)") or original_body
local decoded = raw_base64_decode(original_body)
local decoded = fs.decode64(original_body)
if decoded then
-- ssr://host:port:protocol:method:obfs:urlsafebase64pass/?params
local before_query = decoded:match("^([^/?]+)")
@ -214,7 +192,7 @@ local function get_server_from_url(line)
-- fallback
if not server then
local body_to_parse = raw_base64_decode(original_body) or original_body
local body_to_parse = fs.decode64(original_body) or original_body
server = body_to_parse:match("@([^:/]+)") or -- user@host
body_to_parse:match("([^:/]+)") or -- host
body_to_parse:match("//([^:/]+)") -- //host

View File

@ -51,12 +51,31 @@ version_compare() {
return 1
}
run_with_timeout() {
local timeout_sec="$1"
shift
"$@" &
local _pid=$!
(
sleep "$timeout_sec"
kill $_pid 2>/dev/null
sleep 0.5
kill -9 $_pid 2>/dev/null
) &
local _watchdog=$!
wait $_pid 2>/dev/null
local _ret=$?
kill $_watchdog 2>/dev/null
wait $_watchdog 2>/dev/null
return $_ret
}
LAST_OPVER="/tmp/openclash_last_version"
LAST_VER=$(sed -n 1p "$LAST_OPVER" 2>/dev/null |sed "s/^v//g" |tr -d "\n")
if [ -x "/bin/opkg" ]; then
OP_CV=$(rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep 'Version' |awk -F 'Version: ' '{print $2}' 2>/dev/null)
elif [ -x "/usr/bin/apk" ]; then
OP_CV=$(apk list luci-app-openclash 2>/dev/null|grep "installed" | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
OP_CV=$(rm -f /lib/apk/db/lock && apk list luci-app-openclash 2>/dev/null|grep "installed" | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
fi
OP_LV=$(sed -n 1p "$LAST_OPVER" 2>/dev/null |sed "s/^v//g" |tr -d "\n")
RELEASE_BRANCH=$(uci_get_config "release_branch" || echo "master")
@ -129,27 +148,48 @@ if [ -n "$OP_CV" ] && [ -n "$OP_LV" ] && version_compare "$OP_CV" "$OP_LV" && [
LOG_TIP "$retry_count/$max_retries】【OpenClash - v$LAST_VER】Download successful, start pre update test..."
pre_test_success=false
pkg_update_success=true
if [ -x "/bin/opkg" ]; then
opkg update >/dev/null 2>&1
if [ $? -ne 0 ]; then
sleep 2
pkg_update_success=false
continue
fi
update_retry=0
max_update_retry=2
while [ $update_retry -lt $max_update_retry ]; do
update_retry=$((update_retry + 1))
run_with_timeout 30 opkg update >/dev/null 2>&1
opkg_ret=$?
rm -f /var/lock/opkg.lock
if [ $opkg_ret -eq 0 ]; then
break
fi
if [ $update_retry -lt $max_update_retry ]; then
LOG_ERROR "$update_retry/$max_update_retry】【OpenClash - v$LAST_VER】opkg update failed or timed out, retrying..."
sleep 2
else
LOG_ERROR "$update_retry/$max_update_retry】【OpenClash - v$LAST_VER】opkg update failed, trying pre update test..."
fi
done
if [ -s "/tmp/openclash.ipk" ]; then
if [ -n "$(opkg install /tmp/openclash.ipk --noaction 2>/dev/null |grep 'Upgrading luci-app-openclash on root' 2>/dev/null)" ]; then
pre_test_success=true
fi
fi
elif [ -x "/usr/bin/apk" ]; then
apk update >/dev/null 2>&1
if [ $? -ne 0 ]; then
sleep 2
pkg_update_success=false
continue
fi
update_retry=0
max_update_retry=2
while [ $update_retry -lt $max_update_retry ]; do
update_retry=$((update_retry + 1))
run_with_timeout 30 apk update >/dev/null 2>&1
apk_ret=$?
rm -f /lib/apk/db/lock /tmp/apk.lock
if [ $apk_ret -eq 0 ]; then
break
fi
if [ $update_retry -lt $max_update_retry ]; then
LOG_ERROR "$update_retry/$max_update_retry】【OpenClash - v$LAST_VER】apk update failed or timed out, retrying..."
sleep 2
else
LOG_ERROR "$update_retry/$max_update_retry】【OpenClash - v$LAST_VER】apk update failed, trying pre update test..."
fi
done
if [ -s "/tmp/openclash.apk" ]; then
apk add -s -q --force-overwrite --clean-protected --allow-untrusted /tmp/openclash.apk >/dev/null 2>&1
if [ $? -eq 0 ]; then

View File

@ -37,7 +37,7 @@ RELEASE_BRANCH=$(uci_get_config "release_branch" || echo "master")
if [ -x "/bin/opkg" ]; then
OP_CV=$(rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep 'Version' |awk -F 'Version: ' '{print $2}' 2>/dev/null)
elif [ -x "/usr/bin/apk" ]; then
OP_CV=$(apk list luci-app-openclash 2>/dev/null|grep 'installed' | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
OP_CV=$(rm -f /lib/apk/db/lock && apk list luci-app-openclash 2>/dev/null|grep 'installed' | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
fi
OP_LV=$(sed -n 1p "$DOWNLOAD_FILE" 2>/dev/null |sed "s/^v//g" |tr -d "\n")
github_address_mod=$(uci_get_config "github_address_mod" || echo 0)

View File

@ -5,6 +5,7 @@
. /usr/share/openclash/uci.sh
LOG_FILE="/tmp/openclash.log"
CLASH="/etc/openclash/clash"
CFG_UPDATE_INT=0
SKIP_PROXY_ADDRESS=1
SKIP_PROXY_ADDRESS_INTERVAL=30
@ -29,8 +30,6 @@ begin
exit;
end
require 'thread'
servers_to_process = Array.new
# Servers from proxies
@ -46,20 +45,41 @@ begin
if provider.key?('path') and not provider['path'].empty?
path = provider['path'].start_with?('./') ? '/etc/openclash/' + provider['path'][2..-1] : provider['path']
if File.exist?(path)
file_is_age_encrypted = File.read(path, 512).include?('BEGIN AGE ENCRYPTED FILE') rescue false
begin
provider_config = YAML.load_file(path)
if provider.key?('age-secret-key') and not provider['age-secret-key'].to_s.empty?
begin
provider_config = YAML.load_file(path, secret: provider['age-secret-key']) rescue nil
rescue Exception => e
YAML.LOG_WARN('Set Proxies Address Skip Failed,【' + path + ': ' + e.message+'】')
continue
end
else
if file_is_age_encrypted
YAML.LOG_WARN('Set Proxies Address Skip Failed,【' + path + ': File is AGE encrypted but no secret key provided】')
next
end
provider_config = YAML.load_file(path)
end
if provider_config.is_a?(Hash) and provider_config.key?('proxies') and not provider_config['proxies'].nil?
provider_config['proxies'].each do |p|
servers_to_process.push(p['server']) if p.key?('server')
end
end
rescue Psych::SyntaxError, ArgumentError
begin
syscall = \"lua /usr/share/openclash/openclash_sub_parser.lua \\\"#{path}\\\"\"
sub_servers = IO.popen(syscall).read.split(/\n+/)
servers_to_process.concat(sub_servers) if sub_servers
rescue Exception => e
YAML.LOG_WARN('Failed to parse subscription file with Lua helper ' + path + ': ' + e.message)
if not provider.key?('age-secret-key') or provider['age-secret-key'].to_s.empty?
if file_is_age_encrypted
YAML.LOG_WARN('Failed to parse config file with Lua helper【' + path + ': File is AGE encrypted, cannot parse with Lua】')
next
end
begin
syscall = \"lua /usr/share/openclash/openclash_sub_parser.lua \\\"#{path}\\\"\"
sub_servers = IO.popen(syscall).read.split(/\n+/)
servers_to_process.concat(sub_servers) if sub_servers
rescue Exception => e
YAML.LOG_WARN('Failed to parse config file with Lua helper【' + path + ': ' + e.message+'】')
end
end
end
end
@ -195,8 +215,8 @@ fi
## Log File Size Manage:
LOGSIZE=`ls -l /tmp/openclash.log |awk '{print int($5/1024)}'`
if [ "$LOGSIZE" -gt "$log_size" ]; then
: > /tmp/openclash.log
LOG_WATCHDOG "Log Size Limit, Clean Up All Log Records..."
: > /tmp/openclash.log
LOG_WATCHDOG "Log Size Limit, Clean Up All Log Records..."
fi
## 防火墙检查
@ -361,6 +381,9 @@ fi
fi
fi
##Dler Cloud Checkin
/usr/share/openclash/openclash_oix_checkin.lua >/dev/null 2>&1
## 配置文件循环更新
if [ "$cfg_update" -eq 1 ] && [ "$cfg_update_mode" -eq 1 ]; then
if [ "$CFG_UPDATE_INT" -ne 0 ]; then
@ -374,59 +397,59 @@ fi
if [ "$STREAM_AUTO_SELECT" -ne 0 ]; then
if [ "$(expr "$STREAM_AUTO_SELECT" % "$stream_auto_select_interval")" -eq 0 ] || [ "$STREAM_AUTO_SELECT" -eq 1 ]; then
if [ "$stream_auto_select_netflix" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Netflix】..."
LOG_TIP "【Netflix】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Netflix" >> $LOG_FILE
fi
if [ "$stream_auto_select_disney" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Disney Plus】..."
LOG_TIP "【Disney Plus】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Disney Plus" >> $LOG_FILE
fi
if [ "$stream_auto_select_google_not_cn" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Google Not CN】..."
LOG_TIP "【Google Not CN】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Google" >> $LOG_FILE
fi
if [ "$stream_auto_select_ytb" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【YouTube Premium】..."
LOG_TIP "【YouTube Premium】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "YouTube Premium" >> $LOG_FILE
fi
if [ "$stream_auto_select_prime_video" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Amazon Prime Video】..."
LOG_TIP "【Amazon Prime Video】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Amazon Prime Video" >> $LOG_FILE
fi
if [ "$stream_auto_select_hbo_max" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【HBO Max】..."
LOG_TIP "【HBO Max】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "HBO Max" >> $LOG_FILE
fi
if [ "$stream_auto_select_tvb_anywhere" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【TVB Anywhere+】..."
LOG_TIP "【TVB Anywhere+】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "TVB Anywhere+" >> $LOG_FILE
fi
if [ "$stream_auto_select_dazn" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【DAZN】..."
LOG_TIP "【DAZN】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "DAZN" >> $LOG_FILE
fi
if [ "$stream_auto_select_paramount_plus" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Paramount Plus】..."
LOG_TIP "【Paramount Plus】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Paramount Plus" >> $LOG_FILE
fi
if [ "$stream_auto_select_discovery_plus" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Discovery Plus】..."
LOG_TIP "【Discovery Plus】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Discovery Plus" >> $LOG_FILE
fi
if [ "$stream_auto_select_bilibili" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Bilibili】..."
LOG_TIP "【Bilibili】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Bilibili" >> $LOG_FILE
fi
if [ "$stream_auto_select_openai" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【OpenAI】..."
LOG_TIP "【OpenAI】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "OpenAI" >> $LOG_FILE
fi
if [ "$stream_auto_select_claude" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Claude】..."
LOG_TIP "【Claude】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Claude" >> $LOG_FILE
fi
if [ "$stream_auto_select_gemini" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Gemini】..."
LOG_TIP "【Gemini】Start Auto Select Unlock Proxy..."
/usr/share/openclash/openclash_streaming_unlock.lua "Gemini" >> $LOG_FILE
fi
fi

View File

@ -1,6 +1,73 @@
#!/bin/sh
. /lib/functions.sh
uci_get_config() {
local key="$1"
uci -q get openclash.@overwrite[0]."$key" || uci -q get openclash.config."$key"
}
uci_get_age_public_keys() {
local name="$1"
[ -n "$name" ] || return 0
_print_pub() {
local section="$1"
config_get cfg_name "$section" name
if [ "$cfg_name" = "$name" ]; then
config_get pub "$section" public
[ -n "$pub" ] && printf '%s\n' "$pub"
fi
}
config_load openclash
config_foreach _print_pub config_age_secret
}
uci_get_age_secret_keys() {
local name="$1"
[ -n "$name" ] || return 0
_print_sec() {
local section="$1"
config_get cfg_name "$section" name
if [ "$cfg_name" = "$name" ]; then
config_get sec "$section" secret
[ -n "$sec" ] && printf '%s\n' "$sec"
fi
}
config_load openclash
config_foreach _print_sec config_age_secret
}
uci_set_age_keys_by_name() {
local name="$1"
local secret="$2"
local public="$3"
local target_section=""
[ -n "$name" ] || return 1
_find_age_section() {
local section="$1"
local cfg_name
config_get cfg_name "$section" name
[ "$cfg_name" = "$name" ] && target_section="$section"
}
config_load openclash
config_foreach _find_age_section config_age_secret
if [ -z "$target_section" ]; then
target_section=$(uci -q add openclash config_age_secret)
fi
[ -n "$target_section" ] || return 1
uci -q set openclash."$target_section".name="$name"
[ -n "$secret" ] && uci -q set openclash."$target_section".secret="$secret"
[ -n "$public" ] && uci -q set openclash."$target_section".public="$public"
uci -q commit openclash
return 0
}

View File

@ -427,9 +427,9 @@ begin
Value['external-ui-url'] = 'https://codeload.github.com/MetaCubeX/Yacd-meta/zip/refs/heads/gh-pages'
end
when 'metacubexd'
Value['external-ui-url'] = 'https://codeload.github.com/MetaCubeX/metacubexd/zip/refs/heads/gh-pages'
Value['external-ui-url'] = 'https://codeload.github.com/MetaCubeX/metacubexd/zip/refs/heads/gh-pages'
when 'zashboard'
Value['external-ui-url'] = 'https://codeload.github.com/Zephyruso/zashboard/zip/refs/heads/gh-pages-cdn-fonts'
Value['external-ui-url'] = 'https://codeload.github.com/Zephyruso/zashboard/zip/refs/heads/gh-pages-cdn-fonts'
end
if !Value.key?('keep-alive-interval') && !Value.key?('keep-alive-idle')
Value['keep-alive-interval'] = 15
@ -456,7 +456,7 @@ begin
end
if smart_collect
(Value['profile'] ||= {})['smart-collector-size'] = smart_collect_size.to_f
(Value['profile'] ||= {})['smart-collector-size'] = smart_collect_size.to_f
end
Value['geox-url'] ||= {}

View File

@ -22,7 +22,7 @@ yml_other_set()
begin
thread_pool = [];
# GEOIP replace
geoip_pattern = /GEOIP,([A-Za-z]{2}),([^,]+)(,.*)?/;
geoip_pattern = /^GEOIP,([A-Za-z]{2}),([^,]+)(,.*)?/;
match_pattern = /(^MATCH.*|^FINAL.*)/;
thread_pool << Thread.new{
#BT/P2P DIRECT Rules

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
/**
* OpenClash Common JavaScript Utilities
* Shared functions used across multiple view templates.
*/
/**
* Detects if an element has a dark background by analyzing its computed CSS.
* Used by CodeMirror log editor to set dark mode attribute.
* @param {HTMLElement} element - The element to check
* @returns {boolean} True if the background is dark
*/
function isDarkBackground(element) {
var cachedTheme = localStorage.getItem('oc-theme');
if (cachedTheme === 'dark') {
return true;
} else if (cachedTheme === 'light') {
return false;
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return true;
}
var style = window.getComputedStyle(element);
var bgColor = style.backgroundColor;
var r, g, b;
if (/rgb\(/.test(bgColor)) {
var rgb = bgColor.match(/\d+/g);
r = parseInt(rgb[0]);
g = parseInt(rgb[1]);
b = parseInt(rgb[2]);
} else if (/#/.test(bgColor)) {
if (bgColor.length === 4) {
r = parseInt(bgColor[1] + bgColor[1], 16);
g = parseInt(bgColor[2] + bgColor[2], 16);
b = parseInt(bgColor[3] + bgColor[3], 16);
} else {
r = parseInt(bgColor.slice(1, 3), 16);
g = parseInt(bgColor.slice(3, 5), 16);
b = parseInt(bgColor.slice(5, 7), 16);
}
} else {
return false;
}
var luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return luminance < 128;
}
/**
* Opens a URL in a new window. Used for external links (Wiki, GitHub, etc.).
* @param {string} url - The URL to open
* @returns {boolean} false to prevent default link behavior
*/
function winOpen(url) {
var win = window.open(url);
if (win == null || typeof(win) == 'undefined') {
window.location.href = url;
}
return false;
}

View File

@ -2,8 +2,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.0.8-20250207
PKG_RELEASE:=3
PKG_VERSION:=1.0.8-r20250207
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for OpenWebUI
LUCI_PKGARCH:=all

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