mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-07 04:34:14 +08:00
update 2026-11-02 20:03:25
This commit is contained in:
1 parent
d9d19b2880
commit
8a02ade817
55 files changed
+3023
-3443
No files matched your search
@@ -1091,7 +1091,6 @@ set clashoo_china6 {
|
||||
2402:e2c0::/32,
|
||||
2402:e3c0::/32,
|
||||
2402:e480::/32,
|
||||
2402:e540::/32,
|
||||
2402:e680::/32,
|
||||
2402:e740::/32,
|
||||
2402:e780::/32,
|
||||
@@ -2657,7 +2656,6 @@ set clashoo_china6 {
|
||||
2406:840:fee8::/46,
|
||||
2406:840:feec::/48,
|
||||
2406:840:feee::/47,
|
||||
2406:840:feff::/48,
|
||||
2406:840:ff00::/40,
|
||||
2406:880::/32,
|
||||
2406:8c0::/32,
|
||||
@@ -3387,7 +3385,16 @@ set clashoo_china6 {
|
||||
2a0a:d685:220::/43,
|
||||
2a0a:d685:240::/42,
|
||||
2a0a:d685:280::/41,
|
||||
2a0a:d685:400::/38,
|
||||
2a0a:d685:400::/40,
|
||||
2a0a:d685:500::/45,
|
||||
2a0a:d685:508::/48,
|
||||
2a0a:d685:50a::/47,
|
||||
2a0a:d685:50c::/46,
|
||||
2a0a:d685:510::/44,
|
||||
2a0a:d685:520::/43,
|
||||
2a0a:d685:540::/42,
|
||||
2a0a:d685:580::/41,
|
||||
2a0a:d685:600::/39,
|
||||
2a0a:d685:800::/37,
|
||||
2a0a:d685:1000::/36,
|
||||
2a0a:d685:2000::/35,
|
||||
|
||||
@@ -394,13 +394,16 @@ set clashoo_china {
|
||||
43.92.0.0/15,
|
||||
43.94.0.0/18,
|
||||
43.96.0.0/23,
|
||||
43.96.2.130/32,
|
||||
43.96.3.0/24,
|
||||
43.96.4.0/22,
|
||||
43.96.8.0/22,
|
||||
43.96.16.130/32,
|
||||
43.96.18.0/24,
|
||||
43.96.20.0/22,
|
||||
43.96.24.0/22,
|
||||
43.96.32.0/22,
|
||||
43.96.36.130/32,
|
||||
43.96.40.0/24,
|
||||
43.96.42.0/24,
|
||||
43.96.44.0/23,
|
||||
@@ -411,6 +414,7 @@ set clashoo_china {
|
||||
43.96.62.0/23,
|
||||
43.96.64.0/21,
|
||||
43.96.72.0/22,
|
||||
43.96.77.131/32,
|
||||
43.96.78.0/24,
|
||||
43.96.80.0/23,
|
||||
43.96.85.0/24,
|
||||
@@ -1007,21 +1011,12 @@ set clashoo_china {
|
||||
47.77.128.0/17,
|
||||
47.78.0.0/15,
|
||||
47.80.0.0/14,
|
||||
47.84.0.0/16,
|
||||
47.85.0.0/17,
|
||||
47.85.128.0/23,
|
||||
47.85.131.0/24,
|
||||
47.85.132.0/22,
|
||||
47.85.136.0/21,
|
||||
47.85.144.0/20,
|
||||
47.85.160.0/19,
|
||||
47.85.192.0/18,
|
||||
47.84.0.0/15,
|
||||
47.86.0.0/16,
|
||||
47.87.0.0/20,
|
||||
47.87.16.0/21,
|
||||
47.87.32.0/19,
|
||||
47.87.64.0/21,
|
||||
47.87.72.0/24,
|
||||
47.87.80.0/20,
|
||||
47.87.96.0/19,
|
||||
47.87.128.0/20,
|
||||
@@ -1528,7 +1523,8 @@ set clashoo_china {
|
||||
103.19.40.0/21,
|
||||
103.19.64.0/21,
|
||||
103.19.72.0/22,
|
||||
103.19.232.0/22,
|
||||
103.19.233.0/24,
|
||||
103.19.234.0/23,
|
||||
103.20.12.0/22,
|
||||
103.20.32.0/24,
|
||||
103.20.34.0/23,
|
||||
@@ -2214,7 +2210,7 @@ set clashoo_china {
|
||||
103.98.232.0/22,
|
||||
103.98.240.0/20,
|
||||
103.99.53.0/24,
|
||||
103.99.54.0/23,
|
||||
103.99.55.0/24,
|
||||
103.99.56.0/22,
|
||||
103.99.62.0/24,
|
||||
103.99.104.0/22,
|
||||
@@ -2267,7 +2263,6 @@ set clashoo_china {
|
||||
103.104.104.0/22,
|
||||
103.104.152.0/22,
|
||||
103.104.188.0/22,
|
||||
103.104.198.0/23,
|
||||
103.104.252.0/22,
|
||||
103.105.0.0/21,
|
||||
103.105.12.0/22,
|
||||
@@ -2678,7 +2673,6 @@ set clashoo_china {
|
||||
103.186.228.0/23,
|
||||
103.189.92.0/23,
|
||||
103.189.152.0/22,
|
||||
103.189.197.0/24,
|
||||
103.190.20.0/23,
|
||||
103.190.71.0/24,
|
||||
103.190.104.0/23,
|
||||
@@ -3639,7 +3633,7 @@ set clashoo_china {
|
||||
117.134.128.0/18,
|
||||
117.134.208.0/23,
|
||||
117.134.212.0/23,
|
||||
117.134.216.0/22,
|
||||
117.134.218.0/23,
|
||||
117.134.222.0/23,
|
||||
117.134.232.0/21,
|
||||
117.134.240.0/20,
|
||||
@@ -3729,7 +3723,9 @@ set clashoo_china {
|
||||
119.19.0.0/16,
|
||||
119.20.0.0/14,
|
||||
119.27.64.0/18,
|
||||
119.27.128.0/17,
|
||||
119.27.128.0/18,
|
||||
119.27.208.0/20,
|
||||
119.27.224.0/20,
|
||||
119.28.28.0/24,
|
||||
119.28.240.0/20,
|
||||
119.29.0.0/16,
|
||||
@@ -4240,7 +4236,7 @@ set clashoo_china {
|
||||
142.86.224.0/20,
|
||||
142.86.240.0/21,
|
||||
142.86.248.0/22,
|
||||
142.86.254.0/23,
|
||||
142.86.255.0/24,
|
||||
143.64.0.0/16,
|
||||
144.0.0.0/16,
|
||||
144.7.0.0/16,
|
||||
@@ -4514,7 +4510,6 @@ set clashoo_china {
|
||||
163.204.0.0/16,
|
||||
163.223.28.0/23,
|
||||
163.223.32.0/23,
|
||||
163.223.72.0/23,
|
||||
163.223.124.0/23,
|
||||
163.223.222.0/23,
|
||||
163.227.34.0/23,
|
||||
@@ -6299,8 +6294,7 @@ set clashoo_china {
|
||||
211.144.0.0/14,
|
||||
211.148.0.0/16,
|
||||
211.149.0.0/19,
|
||||
211.149.33.0/24,
|
||||
211.149.34.0/23,
|
||||
211.149.32.0/22,
|
||||
211.149.48.0/21,
|
||||
211.149.64.0/21,
|
||||
211.149.80.0/21,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6
-6
@@ -5,7 +5,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=honk
|
||||
PKG_VERSION:=2026.11.01
|
||||
PKG_VERSION:=2026.11.02
|
||||
PKG_RELEASE:=1
|
||||
|
||||
# Pinned honk-core revision (upstream daeuniverse/honk main), cross-compiled by
|
||||
@@ -14,10 +14,10 @@ PKG_RELEASE:=1
|
||||
# assets behind CORE_COMMIT, so a plain feed build downloads and verifies them
|
||||
# without any generated file. A staged run may still override them through
|
||||
# honk/generated-stage.mk, which is generated and never committed.
|
||||
CORE_COMMIT:=54b2e1cfbe1e7f8a93dba3a0f64fabf2a9f88e38
|
||||
CORE_COMMIT:=ce0380dc65d1eb933da7241bf88c287231efce70
|
||||
|
||||
# Immutable source asset tag holding this pin's core archives.
|
||||
HONK_SOURCE_TAG?=source-41281523460
|
||||
HONK_SOURCE_TAG?=source-41400973213
|
||||
HONK_SOURCE_URL?=https://github.com/kenzok8/openwrt-honk/releases/download/$(HONK_SOURCE_TAG)
|
||||
|
||||
-include $(CURDIR)/generated-stage.mk
|
||||
@@ -25,9 +25,9 @@ HONK_SOURCE_URL?=https://github.com/kenzok8/openwrt-honk/releases/download/$(HON
|
||||
HONK_ASSET_X86_64?=honk-core-$(CORE_COMMIT)-x86_64.tar.gz
|
||||
HONK_ASSET_AARCH64?=honk-core-$(CORE_COMMIT)-aarch64.tar.gz
|
||||
HONK_ASSET_ARMV7?=honk-core-$(CORE_COMMIT)-armv7.tar.gz
|
||||
HONK_HASH_X86_64?=a5e46463505cc6ca15d704d77b57eba37b3ffe5d71498af928f052f612724deb
|
||||
HONK_HASH_AARCH64?=98084529a1186ecc60d0b4882a0d419cc127b7d7c86ad5952ae0eda236709274
|
||||
HONK_HASH_ARMV7?=e1f2a1dff427220301e4a343202e2c2c2b5efd8e996423ca981c6e0145540d60
|
||||
HONK_HASH_X86_64?=cd465147cf1db1754a651be23d31124cca1cf8c1562744c4aacb37eff12acb43
|
||||
HONK_HASH_AARCH64?=28eb047028d0f5f9b8f6e4b44b0834ca3f5c8231b681b3b288b865abb5aa5f44
|
||||
HONK_HASH_ARMV7?=65ca485ceb09002bc96fd88c886f768483defd5ce3d0976136f0764028a6b875
|
||||
|
||||
# ARCH_PACKAGES is the package arch (e.g. arm_cortex-a7_neon-vfpv4);
|
||||
# ARCH is the CPU arch (x86_64/aarch64/arm) and coincides for 64-bit.
|
||||
|
||||
@@ -2,4 +2,10 @@
|
||||
# 开机自启是内置行为:装上就 enable,是否真正启动由 honk.main.enabled 决定,
|
||||
# 不再提供单独的 boot_enabled 开关。
|
||||
/etc/init.d/honk enable
|
||||
|
||||
# 配置模式已取消:只清遗留键,活动配置与启停状态一概不动。
|
||||
legacy_mode=$(uci -q get honk.main.config_mode 2>/dev/null)
|
||||
if [ -n "$legacy_mode" ]; then
|
||||
uci -q delete honk.main.config_mode && uci -q commit honk
|
||||
fi
|
||||
exit 0
|
||||
+16
-21
@@ -22,13 +22,13 @@ load_settings() {
|
||||
config_get_bool enabled main enabled 0
|
||||
config_get_bool boot_enabled main boot_enabled 0
|
||||
config_get_bool initialized main initialized 0
|
||||
config_get config_mode main config_mode managed
|
||||
config_get config_file main config_file /etc/honk/config.dae
|
||||
config_get listen_port main listen_port 9527
|
||||
config_get_bool clash_api_enabled main clash_api_enabled 0
|
||||
config_get clash_api_port main clash_api_port 9090
|
||||
config_get clash_api_secret main clash_api_secret ''
|
||||
config_get lan_network main lan_network lan
|
||||
settings_owner=$(config_owner "$config_file")
|
||||
}
|
||||
|
||||
valid_clash_api() {
|
||||
@@ -149,17 +149,11 @@ sync_local_direct() {
|
||||
check_config() {
|
||||
[ "$config_file" = /etc/honk/config.dae ] || { fail "配置路径必须为 /etc/honk/config.dae"; return 1; }
|
||||
[ -f /etc/honk/config.dae ] && [ ! -L /etc/honk/config.dae ] || { fail "/etc/honk/config.dae 不是普通文件(可能被替换成软链)"; return 1; }
|
||||
case "$config_mode" in
|
||||
manual)
|
||||
"$HONK_CORE" config validate --config /etc/honk/config.dae >/dev/null || { fail "手动模式的 config.dae 未通过静态校验,请先修正配置"; return 1; }
|
||||
return 0
|
||||
;;
|
||||
managed) ;;
|
||||
*) fail "配置模式取值非法(应为 managed 或 manual)"; return 1 ;;
|
||||
esac
|
||||
"$HONK_CORE" config validate --config /etc/honk/config.dae >/dev/null || { fail "config.dae 未通过静态校验,请先修正配置"; return 1; }
|
||||
[ -r /usr/share/doona/index.html ] || { fail "缺少 Doona 静态文件(/usr/share/doona/index.html),请重新安装 luci-app-honk"; return 1; }
|
||||
[ "$settings_owner" = system ] || return 0
|
||||
[ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] || { fail "/etc/honk/system.dae 不是普通文件(可能被替换成软链)"; return 1; }
|
||||
system_config_readonly || { fail "/etc/honk/system.dae 权限不对,应为 0600"; return 1; }
|
||||
[ -r /usr/share/doona/index.html ] || { fail "缺少 Doona 静态文件(/usr/share/doona/index.html),请重新安装 luci-app-honk"; return 1; }
|
||||
case "$listen_port" in
|
||||
''|*[!0-9]*) fail "监听端口必须是数字"; return 1 ;;
|
||||
esac
|
||||
@@ -276,13 +270,14 @@ lock_owner_invalid() {
|
||||
preflight() {
|
||||
load_settings
|
||||
[ "$enabled" = 1 ] && [ "$initialized" = 1 ] || { fail "请先在 LuCI 里启用并完成初始化"; return 1; }
|
||||
if [ "$config_mode" = manual ]; then
|
||||
maintenance_start_guard && check_config && check_kernel && no_honk_resources && check_conflicts
|
||||
# 只有 system.dae 负责这些字段时才检查 UCI 端口与网络;完整配置自行定义时不碰。
|
||||
if [ "$settings_owner" = system ]; then
|
||||
maintenance_start_guard && check_config && check_network && check_kernel && no_honk_resources && check_conflicts && check_port && {
|
||||
[ "$clash_api_enabled" = 0 ] || check_port "$clash_api_port"
|
||||
}
|
||||
return $?
|
||||
fi
|
||||
maintenance_start_guard && check_config && check_network && check_kernel && no_honk_resources && check_conflicts && check_port && {
|
||||
[ "$clash_api_enabled" = 0 ] || check_port "$clash_api_port"
|
||||
}
|
||||
maintenance_start_guard && check_config && check_kernel && no_honk_resources && check_conflicts
|
||||
}
|
||||
|
||||
prepare_runtime() {
|
||||
@@ -454,7 +449,7 @@ settings_rollback() {
|
||||
settings() {
|
||||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
|
||||
load_settings
|
||||
[ "$config_mode" = manual ] && { echo '{"ok":false,"message":"settings_unavailable_in_manual_mode"}'; return 1; }
|
||||
[ "$settings_owner" = system ] || { echo '{"ok":false,"message":"config_settings_owned_by_file"}'; return 1; }
|
||||
. /usr/share/libubox/jshn.sh || { echo '{"ok":false,"message":"jshn_unavailable"}'; return 1; }
|
||||
# Bound stdin to 64KiB (DoS protection); read one extra byte to detect truncation.
|
||||
settings_json=$(head -c 65537)
|
||||
@@ -600,7 +595,7 @@ diagnose() {
|
||||
managed_core_pid >/dev/null 2>&1 && running=1 || running=0
|
||||
check_config >/dev/null 2>&1 || errors="$errors invalid_package_config"
|
||||
check_kernel >/dev/null 2>&1 || errors="$errors kernel_requirements_unmet"
|
||||
if [ "$config_mode" = manual ]; then
|
||||
if [ "$settings_owner" != system ]; then
|
||||
if [ "$running" != 1 ]; then
|
||||
no_honk_resources >/dev/null 2>&1 || errors="$errors honk_resources_occupied"
|
||||
pidof honk-core >/dev/null 2>&1 && errors="$errors unmanaged_core_running"
|
||||
@@ -659,9 +654,9 @@ clash_system_matches() {
|
||||
repair() {
|
||||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
|
||||
load_settings
|
||||
if [ "$config_mode" = manual ]; then
|
||||
check_config && { echo '{"ok":true,"message":"manual_config_valid"}'; return 0; }
|
||||
echo '{"ok":false,"message":"manual_config_invalid"}'
|
||||
if [ "$settings_owner" != system ]; then
|
||||
check_config && { echo '{"ok":true,"message":"config_valid"}'; return 0; }
|
||||
echo '{"ok":false,"message":"config_invalid"}'
|
||||
return 1
|
||||
fi
|
||||
old_network=$lan_network
|
||||
@@ -675,7 +670,7 @@ repair() {
|
||||
[ "$config_file" = /etc/honk/config.dae ] || { echo '{"ok":false,"message":"invalid_config_path"}'; return 1; }
|
||||
[ -f /etc/honk/config.dae ] && [ ! -L /etc/honk/config.dae ] || { echo '{"ok":false,"message":"main_config_unavailable"}'; return 1; }
|
||||
main_hash=$(sha256sum /etc/honk/config.dae | awk '{print $1}')
|
||||
main_has_system_include || { echo '{"ok":false,"message":"required_system_include_missing_manual_repair"}'; return 1; }
|
||||
main_has_system_include || { echo '{"ok":false,"message":"required_system_include_missing"}'; return 1; }
|
||||
valid_network_name || { echo '{"ok":false,"message":"invalid_lan_network"}'; return 1; }
|
||||
case "$listen_port" in ''|*[!0-9]*) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
|
||||
[ "$listen_port" -ge 1024 ] && [ "$listen_port" -le 65535 ] || { echo '{"ok":false,"message":"invalid_listen_port"}'; return 1; }
|
||||
|
||||
@@ -2,7 +2,6 @@ config honk 'main'
|
||||
option enabled '0'
|
||||
option boot_enabled '0'
|
||||
option initialized '0'
|
||||
option config_mode 'managed'
|
||||
option config_file '/etc/honk/config.dae'
|
||||
option listen_port '9527'
|
||||
option clash_api_enabled '0'
|
||||
|
||||
+25
-29
@@ -16,7 +16,7 @@ boot() {
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled initialized config_file config_mode ready_token ready_hash ready_runtime ready_api_enabled ready_api_listen ready_clash_api_listen ready_clash_api_secret_set old_ready_token
|
||||
local enabled initialized config_file ready_token ready_hash ready_runtime ready_api_enabled ready_api_listen ready_clash_api_listen ready_clash_api_secret_set old_ready_token
|
||||
config_load honk
|
||||
config_get_bool enabled main enabled 0
|
||||
config_get_bool initialized main initialized 0
|
||||
@@ -25,7 +25,6 @@ start_service() {
|
||||
return 1
|
||||
}
|
||||
config_get config_file main config_file "$HONK_CONFIG"
|
||||
config_get config_mode main config_mode managed
|
||||
[ "$config_file" = "$HONK_CONFIG" ] || {
|
||||
logger -t honk "refusing non-default config path"
|
||||
return 1
|
||||
@@ -34,39 +33,36 @@ start_service() {
|
||||
"$HONK_CONTROL" local-direct || logger -t honk "local-direct sync skipped"
|
||||
unset HONK_MAINT_CAP
|
||||
|
||||
if [ "$config_mode" = manual ]; then
|
||||
ready_token=$(cat /proc/sys/kernel/random/uuid 2>/dev/null) || return 1
|
||||
case "$ready_token" in *[!0-9a-f-]*|'') return 1 ;; esac
|
||||
ready_hash=$(sha256sum "$HONK_CONFIG" 2>/dev/null | awk '{print $1}') || return 1
|
||||
case "$ready_hash" in *[!0-9a-f]*|'') return 1 ;; esac
|
||||
ready_runtime=$(/usr/bin/honk-core config validate --config "$HONK_CONFIG" 2>/dev/null) || return 1
|
||||
ready_api_enabled=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.native_api_enabled' 2>/dev/null)
|
||||
ready_api_listen=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.native_api_listen' 2>/dev/null)
|
||||
ready_clash_api_listen=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.clash_api_listen' 2>/dev/null)
|
||||
ready_clash_api_secret_set=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.clash_api_secret_set' 2>/dev/null)
|
||||
case "$ready_api_enabled" in true|false) ;; *) return 1 ;; esac
|
||||
case "$ready_api_listen" in *[!A-Za-z0-9:._-]*) return 1 ;; esac
|
||||
case "$ready_clash_api_listen" in *[!A-Za-z0-9:._-]*) return 1 ;; esac
|
||||
case "$ready_clash_api_secret_set" in true|false) ;; *) return 1 ;; esac
|
||||
mkdir -p /var/run/honk || return 1
|
||||
chmod 0700 /var/run/honk || return 1
|
||||
if [ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ]; then
|
||||
read -r old_ready_token _ < /var/run/honk/readiness
|
||||
case "$old_ready_token" in ''|*[!0-9a-f-]*) ;; *) rm -f "/var/run/honk/core-ready-$old_ready_token" ;; esac
|
||||
fi
|
||||
printf '%s %s\n' "$ready_token" "$ready_hash" > /var/run/honk/readiness || return 1
|
||||
chmod 0600 /var/run/honk/readiness || return 1
|
||||
# 所有配置都走 readiness 标记:状态页据此判断真正就绪,而不是只看进程在不在。
|
||||
ready_token=$(cat /proc/sys/kernel/random/uuid 2>/dev/null) || return 1
|
||||
case "$ready_token" in *[!0-9a-f-]*|'') return 1 ;; esac
|
||||
ready_hash=$(sha256sum "$HONK_CONFIG" 2>/dev/null | awk '{print $1}') || return 1
|
||||
case "$ready_hash" in *[!0-9a-f]*|'') return 1 ;; esac
|
||||
ready_runtime=$(/usr/bin/honk-core config validate --config "$HONK_CONFIG" 2>/dev/null) || return 1
|
||||
ready_api_enabled=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.native_api_enabled' 2>/dev/null)
|
||||
ready_api_listen=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.native_api_listen' 2>/dev/null)
|
||||
ready_clash_api_listen=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.clash_api_listen' 2>/dev/null)
|
||||
ready_clash_api_secret_set=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.clash_api_secret_set' 2>/dev/null)
|
||||
case "$ready_api_enabled" in true|false) ;; *) return 1 ;; esac
|
||||
case "$ready_api_listen" in *[!A-Za-z0-9:._-]*) return 1 ;; esac
|
||||
case "$ready_clash_api_listen" in *[!A-Za-z0-9:._-]*) return 1 ;; esac
|
||||
case "$ready_clash_api_secret_set" in true|false) ;; *) return 1 ;; esac
|
||||
mkdir -p /var/run/honk || return 1
|
||||
chmod 0700 /var/run/honk || return 1
|
||||
if [ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ]; then
|
||||
read -r old_ready_token _ < /var/run/honk/readiness
|
||||
case "$old_ready_token" in ''|*[!0-9a-f-]*) ;; *) rm -f "/var/run/honk/core-ready-$old_ready_token" ;; esac
|
||||
fi
|
||||
printf '%s %s\n' "$ready_token" "$ready_hash" > /var/run/honk/readiness || return 1
|
||||
chmod 0600 /var/run/honk/readiness || return 1
|
||||
procd_open_instance main
|
||||
procd_set_param command /usr/bin/honk-core --config "$HONK_CONFIG" --disable-timestamp
|
||||
# One env call, no empty values: procd drops the whole list when a KEY has an
|
||||
# empty value, which silently stripped the readiness hash the core requires.
|
||||
ready_env="DAE_LOCATION_ASSET=/usr/share/v2ray"
|
||||
if [ "$config_mode" = manual ]; then
|
||||
ready_env="$ready_env HONK_READY_TOKEN=$ready_token HONK_READY_CONFIG_HASH=$ready_hash HONK_READY_API_ENABLED=$ready_api_enabled HONK_READY_CLASH_API_SECRET_SET=$ready_clash_api_secret_set"
|
||||
[ -n "$ready_api_listen" ] && ready_env="$ready_env HONK_READY_API_LISTEN=$ready_api_listen"
|
||||
[ -n "$ready_clash_api_listen" ] && ready_env="$ready_env HONK_READY_CLASH_API_LISTEN=$ready_clash_api_listen"
|
||||
fi
|
||||
ready_env="$ready_env HONK_READY_TOKEN=$ready_token HONK_READY_CONFIG_HASH=$ready_hash HONK_READY_API_ENABLED=$ready_api_enabled HONK_READY_CLASH_API_SECRET_SET=$ready_clash_api_secret_set"
|
||||
[ -n "$ready_api_listen" ] && ready_env="$ready_env HONK_READY_API_LISTEN=$ready_api_listen"
|
||||
[ -n "$ready_clash_api_listen" ] && ready_env="$ready_env HONK_READY_CLASH_API_LISTEN=$ready_clash_api_listen"
|
||||
# shellcheck disable=SC2086
|
||||
procd_set_param env $ready_env
|
||||
procd_set_param stdout 1
|
||||
|
||||
+55
-41
@@ -2,8 +2,51 @@
|
||||
|
||||
HONK_LIFECYCLE_CORE=/usr/bin/honk-core
|
||||
|
||||
honk_lifecycle_procd_pid() {
|
||||
ubus call service list '{"name":"honk"}' 2>/dev/null | jsonfilter -e '@.honk.instances.main.pid' 2>/dev/null
|
||||
# 字段归属:自带 global/experimental → file;只 include system.dae(且写法规范)→ system;
|
||||
# 其余一律 unknown。写法不规范时不猜,直接拒绝。
|
||||
config_owner() {
|
||||
config_owner_file=${1:-/etc/honk/config.dae}
|
||||
[ -f "$config_owner_file" ] && [ ! -L "$config_owner_file" ] || { echo unknown; return 0; }
|
||||
own=$(awk '/^[[:space:]]*(global|experimental)[[:space:]]*\{/ { print "1"; exit }' "$config_owner_file")
|
||||
inc=$(awk '
|
||||
{
|
||||
line = $0
|
||||
sub(/\r$/, "", line)
|
||||
if (!in_block) {
|
||||
# 只接受「include {」独占一行;同行带内容一律算不规范
|
||||
if (line ~ /^[[:space:]]*include[[:space:]]*\{[[:space:]]*$/) { in_block = 1; next }
|
||||
if (line ~ /^[[:space:]]*include[[:space:]]*\{/) { print "irregular"; next }
|
||||
next
|
||||
}
|
||||
if (line ~ /^[[:space:]]*\}[[:space:]]*$/) { in_block = 0; next }
|
||||
if (line ~ /^[[:space:]]*\}/ || line ~ /\{/) { print "irregular"; next }
|
||||
entry = line
|
||||
sub(/#.*/, "", entry)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", entry)
|
||||
gsub(/["\047]/, "", entry)
|
||||
if (entry == "") next
|
||||
if (entry == "system.dae") print "system"
|
||||
else if (entry ~ /^[A-Za-z0-9._-]+$/) print "other"
|
||||
else print "irregular"
|
||||
}
|
||||
END { if (in_block) print "irregular" }
|
||||
' "$config_owner_file")
|
||||
has_system=
|
||||
has_bad=
|
||||
case "$inc" in *system*) has_system=1 ;; esac
|
||||
case "$inc" in *other*|*irregular*) has_bad=1 ;; esac
|
||||
if [ -n "$own" ]; then
|
||||
[ -z "$inc" ] && echo file || echo unknown
|
||||
elif [ -n "$has_bad" ]; then
|
||||
echo unknown
|
||||
elif [ -n "$has_system" ]; then
|
||||
echo system
|
||||
else
|
||||
echo unknown
|
||||
fi
|
||||
}
|
||||
|
||||
honk_lifecycle_procd_pid() { ubus call service list '{"name":"honk"}' 2>/dev/null | jsonfilter -e '@.honk.instances.main.pid' 2>/dev/null
|
||||
}
|
||||
|
||||
honk_lifecycle_procd_running() {
|
||||
@@ -29,35 +72,18 @@ honk_lifecycle_starttime() {
|
||||
}
|
||||
|
||||
honk_lifecycle_listen() {
|
||||
config_load honk
|
||||
config_get config_mode main config_mode managed
|
||||
if [ "$config_mode" = manual ]; then
|
||||
[ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ] || return 1
|
||||
read -r hlp_token _ < /var/run/honk/readiness || return 1
|
||||
case "$hlp_token" in ''|*[!0-9a-f-]*) return 1 ;; esac
|
||||
sed -n 's/^api_listen=//p' "/var/run/honk/core-ready-$hlp_token" 2>/dev/null
|
||||
return
|
||||
fi
|
||||
sed -n "s/^[[:space:]]*listen:[[:space:]]*'\([^']*\)'.*/\1/p" /etc/honk/system.dae 2>/dev/null | head -n 1
|
||||
[ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ] || return 1
|
||||
read -r hlp_token _ < /var/run/honk/readiness || return 1
|
||||
case "$hlp_token" in ''|*[!0-9a-f-]*) return 1 ;; esac
|
||||
sed -n 's/^api_listen=//p' "/var/run/honk/core-ready-$hlp_token" 2>/dev/null
|
||||
}
|
||||
|
||||
honk_lifecycle_parse_listen() {
|
||||
config_load honk
|
||||
config_get config_mode main config_mode managed
|
||||
if [ "$config_mode" = manual ]; then
|
||||
HONK_LIFECYCLE_LISTEN=$(honk_lifecycle_listen)
|
||||
if [ -z "$HONK_LIFECYCLE_LISTEN" ]; then
|
||||
HONK_LIFECYCLE_PORT=0
|
||||
return 0
|
||||
fi
|
||||
case "$HONK_LIFECYCLE_LISTEN" in *:*) ;; *) return 1 ;; esac
|
||||
HONK_LIFECYCLE_PORT=${HONK_LIFECYCLE_LISTEN##*:}
|
||||
case "$HONK_LIFECYCLE_LISTEN" in ''|*[!A-Za-z0-9:._-]*) return 1 ;; esac
|
||||
case "$HONK_LIFECYCLE_PORT" in ''|*[!0-9]*) return 1 ;; esac
|
||||
[ "$HONK_LIFECYCLE_PORT" -ge 1024 ] && [ "$HONK_LIFECYCLE_PORT" -le 65535 ]
|
||||
return $?
|
||||
fi
|
||||
HONK_LIFECYCLE_LISTEN=$(honk_lifecycle_listen)
|
||||
if [ -z "$HONK_LIFECYCLE_LISTEN" ]; then
|
||||
HONK_LIFECYCLE_PORT=0
|
||||
return 0
|
||||
fi
|
||||
case "$HONK_LIFECYCLE_LISTEN" in *:*) ;; *) return 1 ;; esac
|
||||
HONK_LIFECYCLE_PORT=${HONK_LIFECYCLE_LISTEN##*:}
|
||||
case "$HONK_LIFECYCLE_LISTEN" in ''|*[!A-Za-z0-9:._-]*) return 1 ;; esac
|
||||
@@ -116,14 +142,7 @@ honk_lifecycle_api_owned() {
|
||||
}
|
||||
|
||||
honk_lifecycle_runtime_owned() {
|
||||
hlp_pid=$1
|
||||
config_load honk
|
||||
config_get config_mode main config_mode managed
|
||||
if [ "$config_mode" = manual ]; then
|
||||
honk_lifecycle_ready_owned "$hlp_pid"
|
||||
else
|
||||
honk_lifecycle_api_owned "$hlp_pid"
|
||||
fi
|
||||
honk_lifecycle_ready_owned "$1"
|
||||
}
|
||||
|
||||
honk_lifecycle_resources_clear() {
|
||||
@@ -195,9 +214,6 @@ honk_lifecycle_start() {
|
||||
honk_lifecycle_parse_listen || { echo "honk lifecycle: invalid configured listener" >&2; return 1; }
|
||||
hlp_listen=$HONK_LIFECYCLE_LISTEN
|
||||
hlp_port=$HONK_LIFECYCLE_PORT
|
||||
config_load honk
|
||||
config_get config_mode main config_mode managed
|
||||
if [ "$config_mode" != manual ] && honk_lifecycle_port_open "$hlp_port"; then echo "honk lifecycle: listener port remains occupied" >&2; return 1; fi
|
||||
if [ -n "${HONK_MAINT_CAP:-}" ]; then export HONK_MAINT_CAP; fi
|
||||
/etc/init.d/honk start >/dev/null 2>&1
|
||||
hlp_start_status=$?
|
||||
@@ -225,9 +241,7 @@ honk_lifecycle_start() {
|
||||
hlp_new_starttime=$(honk_lifecycle_starttime "$hlp_new_pid") || hlp_new_starttime=
|
||||
if [ -n "$hlp_new_starttime" ] && \
|
||||
{ [ -z "${HONK_LIFECYCLE_STOP_PID:-}" ] || [ "$hlp_new_pid:$hlp_new_starttime" != "$HONK_LIFECYCLE_STOP_PID:$HONK_LIFECYCLE_STOP_STARTTIME" ]; } && \
|
||||
{ [ "$config_mode" = manual ] && honk_lifecycle_ready_owned "$hlp_new_pid" || \
|
||||
{ [ "$config_mode" != manual ] && honk_lifecycle_listener_owned "$hlp_new_pid" "$hlp_port" && \
|
||||
curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://$hlp_listen/api" -o /dev/null 2>/dev/null; }; }; then
|
||||
{ honk_lifecycle_ready_owned "$hlp_new_pid"; }; then
|
||||
if [ "$hlp_pid:$hlp_starttime" = "$hlp_new_pid:$hlp_new_starttime" ]; then
|
||||
hlp_stable=$((hlp_stable + 1))
|
||||
else
|
||||
|
||||
@@ -25,7 +25,6 @@ if [ "${HONK_MAINT_LIBRARY:-0}" != 1 ]; then
|
||||
. /usr/share/honk/lock.sh || exit 1
|
||||
. /usr/share/honk/lifecycle.sh || exit 1
|
||||
. /usr/share/libubox/jshn.sh || exit 1
|
||||
# lifecycle.sh 用 config_load/config_get 读模式,job 路径没有 rc.common 帮它加载。
|
||||
[ -r /lib/functions.sh ] && . /lib/functions.sh
|
||||
fi
|
||||
|
||||
@@ -445,7 +444,7 @@ load_uci_values() {
|
||||
u_geo_auto=$(get_uci geo_auto 2>/dev/null || printf 0)
|
||||
u_geo_auto_freq=$(get_uci geo_auto_freq 2>/dev/null || printf daily)
|
||||
case "$u_enabled:$u_boot:$u_initialized" in 0:0:0|0:0:1|0:1:0|0:1:1|1:0:0|1:0:1|1:1:0|1:1:1) ;; *) return 1 ;; esac
|
||||
case "$u_config_mode" in managed|manual) ;; *) return 1 ;; esac
|
||||
case "$u_config_mode" in ''|managed|manual) ;; *) return 1 ;; esac
|
||||
case "$u_geo_auto:$u_geo_auto_freq" in 0:daily|0:weekly|1:daily|1:weekly) ;; *) return 1 ;; esac
|
||||
[ "$u_config" = /etc/honk/config.dae ] || return 1
|
||||
case "$u_port" in ''|*[!0-9]*) return 1 ;; esac
|
||||
@@ -541,7 +540,7 @@ validate_config_tree() {
|
||||
main=$config_root/config.dae
|
||||
system=$config_root/system.dae
|
||||
[ -f "$main" ] && [ ! -L "$main" ] && [ -f "$system" ] && [ ! -L "$system" ] || return 1
|
||||
if [ "$u_config_mode" = manual ]; then
|
||||
if [ "$(config_owner "$main")" != system ]; then
|
||||
"$HONK_CORE" config validate --config "$main" >/dev/null 2>&1 || return 2
|
||||
find "$config_root" -type l -print 2>/dev/null | grep -q . && return 2
|
||||
find "$config_root" ! -type f ! -type d -print 2>/dev/null | grep -q . && return 2
|
||||
@@ -822,7 +821,7 @@ validate_candidate() {
|
||||
config_tree=$candidate/etc/honk
|
||||
mkdir -p "$temp_dir/uci-tmp" || return 1
|
||||
load_uci_values uci "$uci_dir" || return 1
|
||||
if [ "$u_config_mode" = manual ]; then
|
||||
if [ "$(config_owner "$config_tree/config.dae")" != system ]; then
|
||||
validate_config_tree "$config_tree" '' '' "$u_port" "$u_initialized" || return 2
|
||||
if [ "$state" = present ] || { [ "$state" = preview ] && { [ -e "$config_tree/state/honk.db" ] || [ -L "$config_tree/state/honk.db" ]; }; }; then
|
||||
archive_sqlite_check "$config_tree/state/honk.db" || return 1
|
||||
@@ -1025,7 +1024,7 @@ restore() {
|
||||
fi
|
||||
[ "$old_running" != 1 ] || [ "$restore_initialized" = 1 ] || { error running_service_requires_initialized_restore; return 1; }
|
||||
[ "$(sha_of "$RESTORE")" = "$archive_hash" ] || { error restore_archive_changed; return 1; }
|
||||
if [ "$u_config_mode" = managed ]; then
|
||||
if [ "$(config_owner "$restore_stage/etc/honk/config.dae")" = system ]; then
|
||||
write_live_system "$restore_stage/etc/honk/system.dae" "$candidate_device" "$candidate_ip" "$u_port" "$restore_initialized" || { error system_config_render_failed; return 1; }
|
||||
chmod 0600 "$restore_stage/etc/honk/system.dae" || { error system_config_render_failed; return 1; }
|
||||
fi
|
||||
@@ -1107,7 +1106,7 @@ reset_data() {
|
||||
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" import honk < "$temp_dir/old-uci" || { error reset_profile_stage_failed; return 1; }
|
||||
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.enabled=0 || { error reset_profile_stage_failed; return 1; }
|
||||
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.boot_enabled=0 || { error reset_profile_stage_failed; return 1; }
|
||||
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.config_mode=managed || { error reset_profile_stage_failed; return 1; }
|
||||
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" delete honk.main.config_mode >/dev/null 2>&1 || :
|
||||
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.listen_port=9527 || { error reset_profile_stage_failed; return 1; }
|
||||
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.lan_network=lan || { error reset_profile_stage_failed; return 1; }
|
||||
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.clash_api_enabled=0 || { error reset_profile_stage_failed; return 1; }
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.arch }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- aarch64_generic
|
||||
- i386_pentium-mmx
|
||||
- loongarch64_generic
|
||||
- mips_mips32
|
||||
- mipsel_24kc
|
||||
- mipsel_mips32
|
||||
- x86_64
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Building packages
|
||||
uses: sbwml/openwrt-gh-action-sdk@go1.24
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}-openwrt-24.10
|
||||
FEEDNAME: packages_ci
|
||||
PACKAGES: luci-app-eqosplus
|
||||
NO_REFRESH_CHECK: true
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.arch }}
|
||||
path: bin/packages/${{ matrix.arch }}/packages_ci/*.ipk
|
||||
|
||||
- name: Upload packages
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowUpdates: true
|
||||
replacesArtifacts: true
|
||||
artifacts: "bin/packages/${{ matrix.arch }}/packages_ci/*.ipk"
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 sirpdboy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,39 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2006-2017 OpenWrt.org#
|
||||
# Copyright (C) 2022-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-eqosplus
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
THEME_NAME:=eqosplus
|
||||
PKG_NAME:=luci-app-$(THEME_NAME)
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_TITLE:=LuCI support for eqosplus.
|
||||
LUCI_DESCRIPTION:=LuCI support for Easy eqosplus(Support speed limit based on IP address).
|
||||
LUCI_DEPENDS:=+ip-full +tc +nftables +bc +kmod-ifb
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=1.3.0
|
||||
PKG_RELEASE:=20251128
|
||||
PKG_MAINTAINER:=sirpdboy <herboy2008@gmail.com>
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
rm -f /tmp/luci-*
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/eqosplus
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
 [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
|
||||
|
||||
<h1 align="center">
|
||||
<br>luci-app-eqosplus<br>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://openwrt.org"><img alt="OpenWrt" src="https://img.shields.io/badge/OpenWrt-%E2%89%A519.07-ff0000?logo=openwrt&logoColor=white"></a>
|
||||
<a href="https://www.google.com/chrome/"><img alt="Chrome" src="https://img.shields.io/badge/Chrome-%E2%89%A5111-4285F3?logo=googlechrome&logoColor=white"></a>
|
||||
<a href="https://www.apple.com/safari/"><img alt="Safari" src="https://img.shields.io/badge/Safari-%E2%89%A516.4-000000?logo=safari&logoColor=white"></a>
|
||||
<a href="https://www.mozilla.org/firefox/"><img alt="Firefox" src="https://img.shields.io/badge/Firefox-%E2%89%A5128-FF7138?logo=firefoxbrowser&logoColor=white"></a>
|
||||
<a target="_blank" href="https://github.com/sirpdboy/luci-app-eqosplus/releases"> <img alt="GitHub release" src="https://img.shields.io/github/v/release/sirpdboy/luci-app-eqosplus"></a>
|
||||
<a href="https://github.com/sirpdboy/luci-app-eqosplus/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/sirpdboy/luci-app-eqosplus/total"></a>
|
||||
</p>
|
||||
|
||||
[中文] | [English](README.md)
|
||||
|
||||

|
||||
|
||||
Please read this page carefully, which includes precautions and instructions on how to use it.
|
||||
|
||||
## Enhanced version of EQOS with timed speed limit and added features such as timed limit.
|
||||
|
||||
### Method for downloading source code:
|
||||
|
||||
```Brach
|
||||
# downloading
|
||||
git clone https://github.com/sirpdboy/luci-app-eqosplus package/luci-app-eqosplus
|
||||
make menuconfig
|
||||
```
|
||||
### Configuration Menu
|
||||
```Brach
|
||||
make menuconfig
|
||||
# find LuCI -> Applications, select luci-app-eqosplus, save and exit
|
||||
```
|
||||
### compile
|
||||
|
||||
```Brach
|
||||
# compile
|
||||
make package/luci-app-eqosplus/compile V=s
|
||||
```
|
||||
|
||||
|
||||
## describe
|
||||
|
||||
- luci-app-eqosplus : https://github.com/sirpdboy/luci-app-eqosplus
|
||||
|
||||

|
||||
|
||||
|
||||
## interface
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
# My other project
|
||||
|
||||
- Watch Dog : https://github.com/sirpdboy/luci-app-watchdog
|
||||
- Net Speedtest : https://github.com/sirpdboy/luci-app-netspeedtest
|
||||
- Task Plan : https://github.com/sirpdboy/luci-app-taskplan
|
||||
- Power Off Device : https://github.com/sirpdboy/luci-app-poweroffdevice
|
||||
- OpentoPD Theme : https://github.com/sirpdboy/luci-theme-opentopd
|
||||
- KuCat Theme : https://github.com/sirpdboy/luci-theme-kucat
|
||||
- KuCat Theme Config : https://github.com/sirpdboy/luci-app-kucat-config
|
||||
- NFT Time Control : https://github.com/sirpdboy/luci-app-timecontrol
|
||||
- Parent Control: https://github.com/sirpdboy/luci-theme-parentcontrol
|
||||
- Eqos Plus: https://github.com/sirpdboy/luci-app-eqosplus
|
||||
- Advanced : https://github.com/sirpdboy/luci-app-advanced
|
||||
- ddns-go : https://github.com/sirpdboy/luci-app-ddns-go
|
||||
- Advanced Plus): https://github.com/sirpdboy/luci-app-advancedplus
|
||||
- Net Wizard: https://github.com/sirpdboy/luci-app-netwizard
|
||||
- Part Exp: https://github.com/sirpdboy/luci-app-partexp
|
||||
- Lukcy: https://github.com/sirpdboy/luci-app-lukcy
|
||||
|
||||
## HELP
|
||||
|
||||
| <img src="https://img.shields.io/badge/-Alipay-F5F5F5.svg" href="#赞助支持本项目-" height="25" alt="图飞了"/> | <img src="https://img.shields.io/badge/-WeChat-F5F5F5.svg" height="25" alt="图飞了" href="#赞助支持本项目-"/> |
|
||||
| :-----------------: | :-------------: |
|
||||
| |  |
|
||||
|
||||
<a href="#readme">
|
||||
<img src="https://img.shields.io/badge/-TOP-orange.svg" alt="no" title="Return TOP" align="right"/>
|
||||
</a>
|
||||
|
||||
 [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
|
||||
@@ -1,109 +0,0 @@
|
||||
 [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
|
||||
|
||||
<h1 align="center">
|
||||
<br>luci-app-eqosplus<br>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
|
||||
<a target="_blank" href="https://github.com/sirpdboy/luci-app-eqosplus/releases">
|
||||
<img src="https://img.shields.io/github/release/sirpdboy/luci-app-eqosplus.svg?style=flat-square&label=luci-app-eqosplus&colorB=green">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[中文] | [English](README.md)
|
||||
|
||||

|
||||
|
||||
请 **认真阅读完毕** 本页面,本页面包含注意事项和如何使用。
|
||||
|
||||
## 功能说明:
|
||||
|
||||
### 定时限速1.2.2版
|
||||
#### 2023.7.19 定时限速1.2.2:增加更多日期:工作日和休息日,自定义日期1,2,3中间用逗号分隔;加入MAC地址限速,从此不用担心IPV6和IPV4的限速问题。
|
||||
|
||||
### 定时限速1.0版
|
||||
#### 2022.12.24 定时限速在eqos的加强版,加入定时限制等功能。
|
||||
|
||||
## 编译使用方法 [](#编译使用方法-)
|
||||
|
||||
将luci-app-eqosplus添加至 LEDE/OpenWRT 源码的方法。
|
||||
|
||||
### 下载源码方法一:
|
||||
编辑源码文件夹根目录feeds.conf.default并加入如下内容:
|
||||
|
||||
```Brach
|
||||
# feeds获取源码:
|
||||
src-git eqosplus https://github.com/sirpdboy/luci-app-eqosplus
|
||||
```
|
||||
```Brach
|
||||
# 更新feeds,并安装主题:
|
||||
scripts/feeds update eqosplus
|
||||
scripts/feeds install luci-app-eqosplus
|
||||
```
|
||||
|
||||
### 下载源码方法:
|
||||
```Brach
|
||||
# 下载源码
|
||||
git clone https://github.com/sirpdboy/luci-app-eqosplus package/luci-app-eqosplus
|
||||
make menuconfig
|
||||
```
|
||||
### 配置菜单
|
||||
```Brach
|
||||
make menuconfig
|
||||
# 找到 LuCI -> Applications, 选择 luci-app-eqosplus, 保存后退出。
|
||||
```
|
||||
### 编译
|
||||
```Brach
|
||||
# 编译固件
|
||||
make package/luci-app-eqosplus/compile V=s
|
||||
```
|
||||
|
||||
## 说明 [](#说明-)
|
||||
|
||||
源码来源:https://github.com/sirpdboy/luci-app-eqosplus
|
||||
|
||||
|
||||
- 你可以随意使用其中的源码,但请注明出处。
|
||||
|
||||

|
||||
|
||||
## 界面
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# My other project
|
||||
|
||||
- 路由安全看门狗 :https://github.com/sirpdboy/luci-app-watchdog
|
||||
- 网络速度测试 :https://github.com/sirpdboy/luci-app-netspeedtest
|
||||
- 计划任务插件(原定时设置) : https://github.com/sirpdboy/luci-app-taskplan
|
||||
- 关机功能插件 : https://github.com/sirpdboy/luci-app-poweroffdevice
|
||||
- opentopd主题 : https://github.com/sirpdboy/luci-theme-opentopd
|
||||
- kucat酷猫主题: https://github.com/sirpdboy/luci-theme-kucat
|
||||
- kucat酷猫主题设置工具: https://github.com/sirpdboy/luci-app-kucat-config
|
||||
- NFT版上网时间控制插件: https://github.com/sirpdboy/luci-app-timecontrol
|
||||
- 家长控制: https://github.com/sirpdboy/luci-theme-parentcontrol
|
||||
- 定时限速: https://github.com/sirpdboy/luci-app-eqosplus
|
||||
- 系统高级设置 : https://github.com/sirpdboy/luci-app-advanced
|
||||
- ddns-go动态域名: https://github.com/sirpdboy/luci-app-ddns-go
|
||||
- 进阶设置(系统高级设置+主题设置kucat/agron/opentopd): https://github.com/sirpdboy/luci-app-advancedplus
|
||||
- 网络设置向导: https://github.com/sirpdboy/luci-app-netwizard
|
||||
- 一键分区扩容: https://github.com/sirpdboy/luci-app-partexp
|
||||
- lukcy大吉: https://github.com/sirpdboy/luci-app-lukcy
|
||||
|
||||
## 捐助
|
||||
|
||||

|
||||
|
||||
| <img src="https://img.shields.io/badge/-支付宝-F5F5F5.svg" href="#赞助支持本项目-" height="25" alt="图飞了"/> | <img src="https://img.shields.io/badge/-微信-F5F5F5.svg" height="25" alt="图飞了" href="#赞助支持本项目-"/> |
|
||||
| :-----------------: | :-------------: |
|
||||
| |  |
|
||||
|
||||
<a href="#readme">
|
||||
<img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="图飞了" title="返回顶部" align="right"/>
|
||||
</a>
|
||||
|
||||
 [](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 645 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 664 KiB |
@@ -1,19 +0,0 @@
|
||||
module("luci.controller.eqosplus", package.seeall)
|
||||
-- Copyright 2022-2023 sirpdboy <herboy2008@gmail.com>
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/eqosplus") then return end
|
||||
entry({"admin", "control"}, firstchild(), "Control", 44).dependent = false
|
||||
|
||||
local e = entry({"admin", "control", "eqosplus"}, cbi("eqosplus"), _("Eqosplus"), 10)
|
||||
e.dependent=false
|
||||
e.acl_depends = { "luci-app-eqosplus" }
|
||||
entry({"admin", "control", "eqosplus", "status"}, call("act_status")).leaf = true
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local sys = require "luci.sys"
|
||||
local e = {}
|
||||
e.status = sys.call(" busybox ps -w | grep eqosplus | grep -v grep >/dev/null ") == 0
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
@@ -1,190 +0,0 @@
|
||||
-- Copyright 2022-2023 sirpdboy <herboy2008@gmail.com>
|
||||
-- Licensed to the public under the Apache License 2.0.
|
||||
local sys = require "luci.sys"
|
||||
local ifaces = sys.net:devices()
|
||||
local WADM = require "luci.tools.webadmin"
|
||||
local ipc = require "luci.ip"
|
||||
local a, t, e
|
||||
|
||||
a = Map("eqosplus", translate("Network speed limit"))
|
||||
a.description = translate("Users can limit the network speed for uploading/downloading through MAC, IP, and IP segments (192.168.110.00-192.168.10.200). The speed unit is MB/second.")
|
||||
a.template = "eqosplus/index"
|
||||
|
||||
t = a:section(TypedSection, "eqosplus")
|
||||
t.anonymous = true
|
||||
|
||||
e = t:option(DummyValue, "eqosplus_status", translate("Status"))
|
||||
e.template = "eqosplus/eqosplus"
|
||||
e.value = translate("Collecting data...")
|
||||
|
||||
|
||||
ipi = t:option(ListValue, "ifname", translate("Interface"), translate("Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"))
|
||||
ipi.default = "1"
|
||||
ipi:value(1,translate("Automatic settings"))
|
||||
ipi:value("br-lan", translate("br-lan (LAN Bridge)"))
|
||||
local function get_wan_interfaces()
|
||||
local result = {}
|
||||
local ubus = require "ubus"
|
||||
|
||||
local conn = ubus.connect()
|
||||
if not conn then
|
||||
return result
|
||||
end
|
||||
|
||||
local network_status = conn:call("network.interface", "dump", {})
|
||||
for _, iface in ipairs(network_status.interface) do
|
||||
if iface.interface:match("^wan") or iface.interface:match("^pppoe") or iface.proto == "pppoe" then
|
||||
local dev = iface.l3_device or iface.device
|
||||
if dev then
|
||||
table.insert(result, {
|
||||
name = dev,
|
||||
proto = iface.proto,
|
||||
logical_name = iface.interface
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
conn:close()
|
||||
return result
|
||||
end
|
||||
|
||||
local wan_ifaces = get_wan_interfaces()
|
||||
for _, iface in ipairs(wan_ifaces) do
|
||||
ipi:value(iface.name, translate(iface.name) .. (iface.proto == "pppoe" and " (PPPoE)" or " (WAN)"))
|
||||
end
|
||||
|
||||
t = a:section(TypedSection, "device")
|
||||
t.template = "cbi/tblsection"
|
||||
t.anonymous = true
|
||||
t.addremove = true
|
||||
|
||||
comment = t:option(Value, "comment", translate("Comment"))
|
||||
comment.size = 8
|
||||
|
||||
e = t:option(Flag, "enable", translate("Enabled"))
|
||||
e.rmempty = false
|
||||
e.size = 4
|
||||
|
||||
ip = t:option(Value, "mac", translate("IP/MAC"))
|
||||
ip.size = 8
|
||||
|
||||
local function get_devices()
|
||||
local devices = {}
|
||||
local seen_ips = {}
|
||||
local ubus = require "ubus"
|
||||
local conn = ubus.connect()
|
||||
|
||||
local function get_hostname(ip)
|
||||
local f = io.popen("nslookup "..ip.." 2>/dev/null | grep 'name =' | cut -d'=' -f2 | sed 's/\\.$//'")
|
||||
if f then
|
||||
local name = f:read("*l")
|
||||
f:close()
|
||||
if name and name ~= "" then
|
||||
return name:match("^%s*(.-)%s*$")
|
||||
end
|
||||
end
|
||||
local leases_file = io.open("/tmp/dhcp.leases", "r")
|
||||
if leases_file then
|
||||
for line in leases_file:lines() do
|
||||
local mac, ip_lease, _, hostname = line:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)")
|
||||
if ip_lease == ip and hostname ~= "*" then
|
||||
leases_file:close()
|
||||
return hostname
|
||||
end
|
||||
end
|
||||
leases_file:close()
|
||||
end
|
||||
return "unknown"
|
||||
end
|
||||
if conn then
|
||||
local leases = conn:call("dhcp", "ipv4leases", {}) or {}
|
||||
for _, lease in ipairs(leases) do
|
||||
if lease.ipaddr and lease.mac then
|
||||
local hostname = lease.hostname or get_hostname(lease.ipaddr)
|
||||
devices[#devices+1] = {
|
||||
ip = lease.ipaddr,
|
||||
mac = lease.mac:upper(),
|
||||
hostname = hostname,
|
||||
display = string.format("%s (%s) - %s", lease.ipaddr, lease.mac:upper(), hostname)
|
||||
}
|
||||
seen_ips[lease.ipaddr] = true
|
||||
end
|
||||
end
|
||||
conn:close()
|
||||
end
|
||||
local arp_cmd = io.popen("ip -4 neigh show dev br-lan 2>/dev/null")
|
||||
if arp_cmd then
|
||||
for line in arp_cmd:lines() do
|
||||
local ip_addr, mac = line:match("^(%S+)%s+.+%s+(%S+)%s+")
|
||||
if ip_addr and mac and mac ~= "00:00:00:00:00:00" and not seen_ips[ip_addr] then
|
||||
mac = mac:upper()
|
||||
local hostname = get_hostname(ip_addr)
|
||||
devices[#devices+1] = {
|
||||
ip = ip_addr,
|
||||
mac = mac,
|
||||
hostname = hostname,
|
||||
display = string.format("%s (%s) - %s", ip_addr, mac, hostname)
|
||||
}
|
||||
seen_ips[ip_addr] = true
|
||||
end
|
||||
end
|
||||
arp_cmd:close()
|
||||
end
|
||||
table.sort(devices, function(a, b) return a.ip < b.ip end)
|
||||
return devices
|
||||
end
|
||||
|
||||
local devices = get_devices()
|
||||
for _, dev in ipairs(devices) do
|
||||
ip:value(dev.ip, dev.display)
|
||||
end
|
||||
dl = t:option(Value, "download", translate("Downloads"))
|
||||
dl.default = '0.1'
|
||||
dl.size = 4
|
||||
|
||||
ul = t:option(Value, "upload", translate("Uploads"))
|
||||
ul.default = '0.1'
|
||||
ul.size = 4
|
||||
function validate_time(self, value, section)
|
||||
local hh, mm, ss
|
||||
hh, mm, ss = string.match (value, "^(%d?%d):(%d%d)$")
|
||||
hh = tonumber (hh)
|
||||
mm = tonumber (mm)
|
||||
if hh and mm and hh <= 23 and mm <= 59 then
|
||||
return value
|
||||
else
|
||||
return nil, "Time HH:MM or space"
|
||||
end
|
||||
end
|
||||
|
||||
e = t:option(Value, "timestart", translate("Start control time"))
|
||||
e.placeholder = '00:00'
|
||||
e.default = '00:00'
|
||||
e.validate = validate_time
|
||||
e.rmempty = true
|
||||
e.size = 4
|
||||
|
||||
e = t:option(Value, "timeend", translate("Stop control time"))
|
||||
e.placeholder = '00:00'
|
||||
e.default = '00:00'
|
||||
e.validate = validate_time
|
||||
e.rmempty = true
|
||||
e.size = 4
|
||||
|
||||
week=t:option(Value,"week",translate("Week Day(1~7)"))
|
||||
week.rmempty = true
|
||||
week:value('0',translate("Everyday"))
|
||||
week:value(1,translate("Monday"))
|
||||
week:value(2,translate("Tuesday"))
|
||||
week:value(3,translate("Wednesday"))
|
||||
week:value(4,translate("Thursday"))
|
||||
week:value(5,translate("Friday"))
|
||||
week:value(6,translate("Saturday"))
|
||||
week:value(7,translate("Sunday"))
|
||||
week:value('1,2,3,4,5',translate("Workday"))
|
||||
week:value('6,7',translate("Rest Day"))
|
||||
week.default='0'
|
||||
week.size = 6
|
||||
|
||||
return a
|
||||
@@ -1,3 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<font class="eqosplus_status"><%=pcdata(self:cfgvalue(section) or self.default or "")%></font>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,12 +0,0 @@
|
||||
<% include("cbi/map") %>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
XHR.poll(2, '<%=luci.dispatcher.build_url("admin", "control", "eqosplus", "status" )%>', null,
|
||||
function (x, result) {
|
||||
var status = document.getElementsByClassName('eqosplus_status')[0];
|
||||
status.setAttribute("style", "font-weight:bold;");
|
||||
status.setAttribute("color", result.status ? "green" : "red");
|
||||
status.innerHTML = result.status ? '<%=translate("RUNNING")%>' : '<%=translate("NOT RUNNING")%>';
|
||||
}
|
||||
)
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,61 +0,0 @@
|
||||
|
||||
msgid "Eqosplus"
|
||||
msgstr ""
|
||||
|
||||
msgid "Network speed limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Running state"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not running"
|
||||
msgstr ""
|
||||
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
msgid "Users can limit the network speed for uploading/downloading through MAC, IP, and IP segments (192.168.110.00-192.168.10.200). The speed unit is MB/second."
|
||||
msgstr ""
|
||||
|
||||
msgid "MAC/IP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Downloads"
|
||||
msgstr ""
|
||||
|
||||
msgid "Uploads"
|
||||
msgstr ""
|
||||
|
||||
msgid "Comment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Upload bandwidth(Mbit/s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Download bandwidth(Mbit/s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Start control time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stop control time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Week Day(1~7)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Suggested feedback:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Rest Day"
|
||||
msgstr ""
|
||||
|
||||
msgid "Workday"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatic settings"
|
||||
msgstr ""
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
|
||||
msgid "Eqosplus"
|
||||
msgstr "定时限速"
|
||||
|
||||
msgid "Network speed limit"
|
||||
msgstr "定时网速限制"
|
||||
|
||||
msgid "Running state"
|
||||
msgstr "运行状态"
|
||||
|
||||
msgid "Not running"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "已运行"
|
||||
|
||||
msgid "Users can limit the network speed for uploading/downloading through MAC, IP.The speed unit is MB/second."
|
||||
msgstr "可以通过MAC,IP限制用户上传/下载的网速。速度单位为<font color=\"red\"><b> MB/秒 </b></font>。"
|
||||
|
||||
msgid "MAC/IP"
|
||||
msgstr "MAC/IP"
|
||||
|
||||
msgid "Downloads"
|
||||
msgstr "下载速度"
|
||||
|
||||
msgid "Uploads"
|
||||
msgstr "上传速度"
|
||||
|
||||
msgid "Comment"
|
||||
msgstr "备注"
|
||||
|
||||
msgid "Upload bandwidth(Mbit/s)"
|
||||
msgstr "上传总带宽(Mbit/s)"
|
||||
|
||||
msgid "Download bandwidth(Mbit/s)"
|
||||
msgstr "下载总带宽(Mbit/s)"
|
||||
|
||||
msgid "Start control time"
|
||||
msgstr "起控时间"
|
||||
|
||||
msgid "Stop control time"
|
||||
msgstr "停控时间"
|
||||
|
||||
msgid "Week Day(1~7)"
|
||||
msgstr "星期(1~7)"
|
||||
|
||||
msgid "Suggested feedback:"
|
||||
msgstr "建议反馈:"
|
||||
|
||||
msgid "Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"
|
||||
msgstr "设定用来限制网络速度的接口,拨号使用pppoe-wan,DHCP模式使用WAN口的硬件接口(如:eth1),通用使用br-lan接口"
|
||||
|
||||
msgid "Rest Day"
|
||||
msgstr "休息日"
|
||||
|
||||
msgid "Workday"
|
||||
msgstr "工作日"
|
||||
|
||||
msgid "Automatic settings"
|
||||
msgstr "自动设置"
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
|
||||
msgid "Eqosplus"
|
||||
msgstr "定时限速"
|
||||
|
||||
msgid "Network speed limit"
|
||||
msgstr "定时网速限制"
|
||||
|
||||
msgid "Running state"
|
||||
msgstr "运行状态"
|
||||
|
||||
msgid "Not running"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "已运行"
|
||||
|
||||
msgid "Users can limit the network speed for uploading/downloading through MAC, IP, and IP segments (192.168.110.00-192.168.10.200). The speed unit is MB/second."
|
||||
msgstr "用户可以通过MAC、IP、IP段(192.168.10.100-192.168.10.200),限制上传/下载的网络速度。速度单位为MB/秒。。"
|
||||
|
||||
msgid "MAC/IP"
|
||||
msgstr "MAC/IP"
|
||||
|
||||
msgid "Downloads"
|
||||
msgstr "下载速度"
|
||||
|
||||
msgid "Uploads"
|
||||
msgstr "上传速度"
|
||||
|
||||
msgid "Comment"
|
||||
msgstr "备注"
|
||||
|
||||
msgid "Upload bandwidth(Mbit/s)"
|
||||
msgstr "上传总带宽(Mbit/s)"
|
||||
|
||||
msgid "Download bandwidth(Mbit/s)"
|
||||
msgstr "下载总带宽(Mbit/s)"
|
||||
|
||||
msgid "Start control time"
|
||||
msgstr "起控时间"
|
||||
|
||||
msgid "Stop control time"
|
||||
msgstr "停控时间"
|
||||
|
||||
msgid "Week Day(1~7)"
|
||||
msgstr "星期(1~7)"
|
||||
|
||||
msgid "Suggested feedback:"
|
||||
msgstr "建议反馈:"
|
||||
|
||||
msgid "Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"
|
||||
msgstr "设定用来限制网络速度的接口,拨号使用pppoe-wan,DHCP模式使用WAN口的硬件接口(如:eth1),通用使用br-lan接口"
|
||||
|
||||
msgid "Rest Day"
|
||||
msgstr "休息日"
|
||||
|
||||
msgid "Workday"
|
||||
msgstr "工作日"
|
||||
|
||||
msgid "Automatic settings"
|
||||
msgstr "自动设置"
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
config eqosplus
|
||||
option enabled '0'
|
||||
option download '1000'
|
||||
option upload '30'
|
||||
option ifname '1'
|
||||
|
||||
config device
|
||||
option timestart '00:00'
|
||||
option week '0'
|
||||
option download '1'
|
||||
option timeend '23:55'
|
||||
option upload '1'
|
||||
option mac '192.168.10.10'
|
||||
option enable '0'
|
||||
|
||||
config device
|
||||
option mac '192.168.10.10-192.168.10.200'
|
||||
option timestart '00:00'
|
||||
option timeend '00:00'
|
||||
option week '0'
|
||||
option download '0.1'
|
||||
option upload '0.1'
|
||||
option enable '0'
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
[ "$ACTION" = "ifup" ] && [ "$INTERFACE" = "wan" ] && {
|
||||
sleep 5
|
||||
/etc/init.d/eqosplus restart
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
#
|
||||
# Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-eqosplus
|
||||
#
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
|
||||
NAME=eqosplus
|
||||
LOCK="/var/lock/$NAME.lock"
|
||||
CR=/etc/crontabs/root
|
||||
|
||||
start_instance() {
|
||||
procd_open_instance
|
||||
procd_set_param command /usr/bin/eqosplusctrl
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
_eqosplus_start() {
|
||||
if [ "$(grep -c 'option enable .1.' /etc/config/$NAME 2>/dev/null)" -gt "0" ]; then
|
||||
if [ x$(uci -q get turboacc.config.sw_flow) = 'x1' ] || [ x$(uci -q get turboacc.config.sfe_flow) = 'x1' ] ;then
|
||||
uci -q set turboacc.config.sw_flow='0'
|
||||
uci -q set turboacc.config.sfe_flow='0'
|
||||
uci -q set turboacc.config.hw_flow='0'
|
||||
uci commit turboacc
|
||||
/etc/init.d/turboacc restart
|
||||
fi
|
||||
touch $LOCK
|
||||
eqosplus start
|
||||
start_instance
|
||||
(crontab -l ; echo "00 1 * * * /etc/init.d/eqosplus start") | sort - | uniq - | crontab -
|
||||
fi
|
||||
}
|
||||
|
||||
start_service(){
|
||||
[ -f $LOCK ] && exit
|
||||
stop_service
|
||||
_eqosplus_start
|
||||
rm -f $LOCK
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger 'eqosplus'
|
||||
}
|
||||
|
||||
stop_service(){
|
||||
kill -9 $(busybox ps -w | grep 'eqosplusctrl' | grep -v 'grep' | awk '{print $1}') >/dev/null 2>&1
|
||||
sed -i '/eqosplus/d' $CR >/dev/null 2>&1
|
||||
rm -f $LOCK 2>/dev/null
|
||||
eqosplus stop
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
chmod +x /etc/init.d/eqosplus /usr/bin/eqosplus*
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@eqosplus[-1]
|
||||
add ucitrack eqosplus
|
||||
set ucitrack.@eqosplus[-1].init=eqosplus
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
[ -s /etc/config/eqosplus ] || echo "config eqosplus" > /etc/config/eqosplus
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
||||
@@ -1,379 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (C) 2006 OpenWrt.org
|
||||
# Copyright 2022-2025 sirpdboy <herboy2008@gmail.com>
|
||||
|
||||
crrun=$1
|
||||
crid=$2
|
||||
NAME=eqosplus
|
||||
IDLIST="/var/$NAME.idlist"
|
||||
LOCK="/var/lock/$NAME.lock"
|
||||
TMPID="/var/$NAME.tmpid"
|
||||
LOG="/var/$NAME_log.log"
|
||||
# idlist=`uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' `
|
||||
|
||||
|
||||
if [ x$(uci get $NAME.@$NAME[0].ifname 2>/dev/null) = 'x1' ]; then
|
||||
ifname=$(uci -q get network.lan.ifname)
|
||||
[ "x$ifname" = "x" ] && ifname="device" || ifname="ifname"
|
||||
dev=$(uci -q get network.wan.$ifname)
|
||||
[ ! "$dev" ] && dev=br-lan
|
||||
else
|
||||
dev=$(uci -q get $NAME.@$NAME[0].ifname 2>/dev/null)
|
||||
[ ! "$dev" ] && dev=br-lan
|
||||
fi
|
||||
|
||||
bin_nft=$(which nft)
|
||||
bin_tc=$(which tc)
|
||||
bin_ip=$(which ip)
|
||||
DEBUG=1
|
||||
|
||||
nft_type() {
|
||||
if command -v nft >/dev/null; then
|
||||
nftables_ver="true"
|
||||
fi
|
||||
}
|
||||
|
||||
# Debug functions
|
||||
dbg_nft() {
|
||||
[ "${DEBUG:-0}" -eq 0 ] || echo "DEBUG: nft $*"
|
||||
$bin_nft "$@"
|
||||
}
|
||||
|
||||
dbg_tc() {
|
||||
[ "${DEBUG:-0}" -eq 0 ] || echo "DEBUG: tc $*"
|
||||
$bin_tc "$@"
|
||||
}
|
||||
|
||||
dbg_ip() {
|
||||
[ "${DEBUG:-0}" -eq 0 ] || echo "DEBUG: ip $*"
|
||||
$bin_ip "$@"
|
||||
}
|
||||
|
||||
is_macaddr() {
|
||||
ret=1
|
||||
if echo "$1" | grep -qE '^([0-9A-Fa-f]{2}[-:]){5}[0-9A-Fa-f]{2}$'; then
|
||||
ret=0
|
||||
fi
|
||||
return $ret
|
||||
}
|
||||
|
||||
# 数值比较函数
|
||||
is_gt_zero() {
|
||||
local value=$1
|
||||
# 使用bc进行浮点数比较
|
||||
echo "$value > 0" | bc -l 2>/dev/null | grep -q 1
|
||||
}
|
||||
|
||||
# 转换为kbit
|
||||
to_kbit() {
|
||||
local mb=$1
|
||||
# MB/s 转 kbit/s: 1 MB/s = 8192 kbit/s
|
||||
echo "scale=0; $mb * 8192 / 1" | bc 2>/dev/null || echo "0"
|
||||
}
|
||||
|
||||
# Default commands
|
||||
tc="dbg_tc"
|
||||
ip="dbg_ip"
|
||||
nft="dbg_nft"
|
||||
|
||||
nft_type
|
||||
|
||||
stop_qos() {
|
||||
for face in $(tc qdisc show 2>/dev/null | grep htb | awk '{print $5}'); do
|
||||
$tc qdisc del dev $face root 2>/dev/null
|
||||
done
|
||||
|
||||
$tc qdisc del dev ${dev} root 2>/dev/null
|
||||
$tc qdisc del dev ${dev}_ifb root 2>/dev/null
|
||||
$tc qdisc del dev ${dev} ingress 2>/dev/null
|
||||
$ip link del dev ${dev}_ifb 2>/dev/null 2>&1
|
||||
|
||||
if [ -n "$nftables_ver" ]; then
|
||||
$nft delete table inet ${NAME} 2>/dev/null
|
||||
$nft delete table bridge ${NAME} 2>/dev/null
|
||||
fi
|
||||
|
||||
echo "" > "$IDLIST" 2>/dev/null
|
||||
}
|
||||
|
||||
init_qosplus() {
|
||||
|
||||
insmod sch_htb 2>/dev/null
|
||||
insmod act_mirred 2>/dev/null
|
||||
insmod ifb 2>/dev/null
|
||||
|
||||
$ip link del dev ${dev}_ifb 2>/dev/null
|
||||
$ip link add dev ${dev}_ifb name ${dev}_ifb type ifb
|
||||
$ip link set dev ${dev}_ifb up
|
||||
|
||||
$tc qdisc del dev ${dev} root 2>/dev/null
|
||||
$tc qdisc add dev ${dev} root handle 1:0 htb default 1
|
||||
$tc class add dev ${dev} parent 1:0 classid 1:1 htb rate 1000mbit ceil 1000mbit
|
||||
|
||||
$tc qdisc del dev ${dev}_ifb root 2>/dev/null
|
||||
$tc qdisc add dev ${dev}_ifb root handle 1:0 htb default 1
|
||||
$tc class add dev ${dev}_ifb parent 1:0 classid 1:1 htb rate 1000mbit ceil 1000mbit
|
||||
lanipaddr=$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F '.' '{print $1"."$2"."$3".0/24"}')
|
||||
if [ -n "$lanipaddr" ]; then
|
||||
$tc filter add dev $dev parent 1:0 protocol ip prio 1 u32 match ip src "$lanipaddr" match ip dst "$lanipaddr" flowid 1:1
|
||||
$tc filter add dev ${dev}_ifb parent 1:0 protocol ip prio 1 u32 match ip src "$lanipaddr" match ip dst "$lanipaddr" flowid 1:1
|
||||
fi
|
||||
$tc qdisc del dev ${dev} ingress 2>/dev/null
|
||||
$tc qdisc add dev ${dev} ingress
|
||||
$tc filter add dev ${dev} parent ffff: protocol all prio 2 u32 match u32 0 0 flowid 1:1 action mirred egress redirect dev ${dev}_ifb
|
||||
|
||||
if [ -n "$nftables_ver" ]; then
|
||||
$nft delete table inet ${NAME} 2>/dev/null
|
||||
$nft delete table bridge ${NAME} 2>/dev/null
|
||||
|
||||
$nft add table inet ${NAME}
|
||||
|
||||
$nft add chain inet ${NAME} mark_forward { type filter hook forward priority -200\; policy accept\; }
|
||||
$nft add chain inet ${NAME} mark_output { type filter hook output priority -200\; policy accept\; }
|
||||
$nft add chain inet ${NAME} mark_postrouting { type filter hook postrouting priority -200\; policy accept\; }
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
del_id() {
|
||||
local list_id=$1
|
||||
id=$((list_id * 10 + 1000))
|
||||
[ "${DEBUG:-0}" -eq 0 ] || echo "D: del_id $@ --$id --$mac"
|
||||
$tc filter del dev ${dev} parent 1: handle 800::$(printf "%x" $id) 2>/dev/null
|
||||
$tc class del dev ${dev} parent 1:1 classid 1:$id 2>/dev/null
|
||||
$tc filter del dev ${dev}_ifb parent 1: handle 800::$(printf "%x" $id) 2>/dev/null
|
||||
$tc class del dev ${dev}_ifb parent 1:1 classid 1:$id 2>/dev/null
|
||||
mac=$(uci -q get $NAME.@device[$list_id].mac 2>/dev/null)
|
||||
if [ -n "$nftables_ver" ] && [ -n "$mac" ]; then
|
||||
if is_macaddr "$mac"; then
|
||||
$nft delete rule inet ${NAME} mark_forward ether saddr $mac counter 2>/dev/null
|
||||
$nft delete rule inet ${NAME} mark_output ether saddr $mac counter 2>/dev/null
|
||||
$nft delete rule inet ${NAME} mark_postrouting ether saddr $mac counter 2>/dev/null
|
||||
else
|
||||
Z=$(echo $mac | awk -F '[/]' '{print $2}')
|
||||
[ -n "$Z" ] && mac=$(echo $mac | awk -F '[/]' '{print $1}') || Z=32
|
||||
$nft delete rule inet ${NAME} mark_forward ip saddr $mac/$Z counter 2>/dev/null
|
||||
$nft delete rule inet ${NAME} mark_output ip saddr $mac/$Z counter 2>/dev/null
|
||||
$nft delete rule inet ${NAME} mark_postrouting ip saddr $mac/$Z counter 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
add_mac() {
|
||||
local list_id=$1
|
||||
id=$((list_id * 10 + 1000))
|
||||
M0=$(echo $mac | cut -d : -f 1)$(echo $mac | cut -d : -f 2)
|
||||
M1=$(echo $mac | cut -d : -f 3)$(echo $mac | cut -d : -f 4)
|
||||
M2=$(echo $mac | cut -d : -f 5)$(echo $mac | cut -d : -f 6)
|
||||
[ "${DEBUG:-0}" -eq 0 ] || echo "D: add_mac $@ --id:$id --mac:$mac M012--$M0-$M1-$M2"
|
||||
|
||||
if is_gt_zero "$UL"; then
|
||||
if [ -n "$nftables_ver" ]; then
|
||||
$nft add rule inet ${NAME} mark_forward ether saddr $mac counter meta mark set $id
|
||||
$nft add rule inet ${NAME} mark_output ether saddr $mac counter meta mark set $id
|
||||
$nft add rule inet ${NAME} mark_postrouting ether saddr $mac counter meta mark set $id
|
||||
fi
|
||||
$tc class add dev ${dev}_ifb parent 1:1 classid 1:$id htb rate "${UL}kbit" ceil "${UL}kbit" prio $id burst 15k cburst 15k
|
||||
$tc qdisc add dev ${dev}_ifb parent 1:$id handle ${id}: sfq perturb 10
|
||||
|
||||
$tc filter add dev ${dev}_ifb parent 1: protocol ip prio $id handle $id fw flowid 1:$id
|
||||
|
||||
$tc filter add dev ${dev}_ifb parent 1: protocol ip prio $((id + 100)) u32 \
|
||||
match u16 0x0800 0xFFFF at -2 \
|
||||
match u16 0x${M2} 0xFFFF at -4 \
|
||||
match u32 0x${M0}${M1} 0xFFFFFFFF at -8 \
|
||||
flowid 1:$id
|
||||
fi
|
||||
if is_gt_zero "$DL"; then
|
||||
$tc class add dev ${dev} parent 1:1 classid 1:$id htb rate "${DL}kbit" ceil "${DL}kbit" prio $id burst 15k cburst 15k
|
||||
$tc qdisc add dev ${dev} parent 1:$id handle ${id}: sfq perturb 10
|
||||
|
||||
$tc filter add dev ${dev} parent 1: protocol ip prio $id u32 \
|
||||
match u16 0x0800 0xFFFF at -2 \
|
||||
match u32 0x${M1}${M2} 0xFFFFFFFF at -12 \
|
||||
match u16 0x${M0} 0xFFFF at -14 \
|
||||
flowid 1:$id
|
||||
fi
|
||||
}
|
||||
|
||||
add_ip() {
|
||||
local list_id=$1
|
||||
id=$((list_id * 10 + 1000))
|
||||
[ "${DEBUG:-0}" -eq 0 ] || echo "D: add_ip $@ --$id --$mac"
|
||||
|
||||
Z=$(echo $mac | awk -F '[/]' '{print $2}')
|
||||
[ -n "$Z" ] && mac=$(echo $mac | awk -F '[/]' '{print $1}') || Z=32
|
||||
if is_gt_zero "$UL"; then
|
||||
if [ -n "$nftables_ver" ]; then
|
||||
$nft add rule inet ${NAME} mark_forward ip saddr $mac/$Z counter meta mark set $id
|
||||
$nft add rule inet ${NAME} mark_output ip saddr $mac/$Z counter meta mark set $id
|
||||
$nft add rule inet ${NAME} mark_postrouting ip saddr $mac/$Z counter meta mark set $id
|
||||
fi
|
||||
$tc class add dev ${dev}_ifb parent 1:1 classid 1:$id htb rate "${UL}kbit" ceil "${UL}kbit" prio $id burst 15k cburst 15k
|
||||
$tc qdisc add dev ${dev}_ifb parent 1:$id handle ${id}: sfq perturb 10
|
||||
$tc filter add dev ${dev}_ifb parent 1: protocol ip prio $id handle $id fw flowid 1:$id
|
||||
$tc filter add dev ${dev}_ifb parent 1:0 prio $((id + 100)) protocol ip u32 \
|
||||
match ip src "$mac"/"$Z" \
|
||||
flowid 1:$id
|
||||
fi
|
||||
if is_gt_zero "$DL"; then
|
||||
$tc class add dev ${dev} parent 1:1 classid 1:$id htb rate "${DL}kbit" ceil "${DL}kbit" prio $id burst 15k cburst 15k
|
||||
$tc qdisc add dev ${dev} parent 1:$id handle ${id}: sfq perturb 10
|
||||
$tc filter add dev ${dev} parent 1:0 prio $id protocol ip u32 \
|
||||
match ip dst "$mac"/"$Z" \
|
||||
flowid 1:$id
|
||||
fi
|
||||
}
|
||||
|
||||
check_time() {
|
||||
local start=$1
|
||||
local end=$2
|
||||
local current=$(date +%H%M)
|
||||
local start_min=$((10#${start:0:2}*60 + 10#${start:3:2}))
|
||||
local end_min=$((10#${end:0:2}*60 + 10#${end:3:2}))
|
||||
local current_min=$((10#${current:0:2}*60 + 10#${current:2:2}))
|
||||
|
||||
if [[ $start_min -lt $end_min ]]; then
|
||||
[[ $current_min -ge $start_min && $current_min -lt $end_min ]]
|
||||
else
|
||||
[[ $current_min -ge $start_min || $current_min -lt $end_min ]]
|
||||
fi
|
||||
}
|
||||
|
||||
check_list() {
|
||||
local i=$1
|
||||
local start_time=$(uci -q get $NAME.@device[$i].timestart 2>/dev/null)
|
||||
local end_time=$(uci -q get $NAME.@device[$i].timeend 2>/dev/null)
|
||||
local wweek=$(uci -q get $NAME.@device[$i].week 2>/dev/null)
|
||||
local current_weekday=$(date +%u)
|
||||
[ -z "$start_time" ] && [ -z "$end_time" ] && [ -z "$wweek" ] && return 0
|
||||
|
||||
if [ -n "$wweek" ] && [ "$wweek" != "0" ]; then
|
||||
local day_match=0
|
||||
for day in $(echo $wweek | tr ',' ' '); do
|
||||
[ "$day" -eq "$current_weekday" ] && day_match=1 && break
|
||||
done
|
||||
[ "$day_match" -eq 0 ] && return 1
|
||||
fi
|
||||
|
||||
if [ -n "$start_time" ] && [ -n "$end_time" ]; then
|
||||
check_time "$start_time" "$end_time" || return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$crrun" in
|
||||
"stop")
|
||||
stop_qos
|
||||
;;
|
||||
"start")
|
||||
stop_qos > /dev/null 2>&1
|
||||
sleep 2
|
||||
init_qosplus
|
||||
idlist=$(uci show $NAME 2>/dev/null | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sort -nu | sed -e 's/^/!/g' -e 's/$/!/g' > $IDLIST; cat $IDLIST 2>/dev/null | sed -e 's/!//g')
|
||||
|
||||
if [ ! -s "$IDLIST" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
device_count=0
|
||||
for list in $(echo $idlist | sed -e 's/!//g'); do
|
||||
if check_list $list; then
|
||||
mac=$(uci -q get $NAME.@device[$list].mac 2>/dev/null)
|
||||
download_mb=$(uci -q get $NAME.@device[$list].download 2>/dev/null)
|
||||
upload_mb=$(uci -q get $NAME.@device[$list].upload 2>/dev/null)
|
||||
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
|
||||
|
||||
# 转换为kbit
|
||||
DL=$(to_kbit "$download_mb")
|
||||
UL=$(to_kbit "$upload_mb")
|
||||
|
||||
if [ -n "$mac" ]; then
|
||||
if is_macaddr "$mac"; then
|
||||
add_mac $list >>$LOG
|
||||
echo "✅ MAC限速: $comment ($mac) - 下载:${download_mb}MB/s(${DL}kbit/s) 上传:${upload_mb}MB/s(${UL}kbit/s)" >>$LOG
|
||||
else
|
||||
add_ip $list >>$LOG
|
||||
echo "✅ IP限速: $comment ($mac) - 下载:${download_mb}MB/s(${DL}kbit/s) 上传:${upload_mb}MB/s(${UL}kbit/s)" >>$LOG
|
||||
fi
|
||||
device_count=$((device_count + 1))
|
||||
fi
|
||||
else
|
||||
[ -f "$IDLIST" ] && [ $(grep "!${list}!" "$IDLIST" 2>/dev/null | wc -l) -gt 0 ] && {
|
||||
del_id $list >>$LOG
|
||||
sed -i "/!$list!/d" "$IDLIST" >/dev/null 2>&1
|
||||
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
|
||||
}
|
||||
fi
|
||||
done
|
||||
echo "EQOSPLUS限速服务启动完成,共配置 $device_count 个设备" >>$LOG
|
||||
echo ""
|
||||
echo "当前限速状态:" >>$LOG
|
||||
tc -s class show dev $dev | grep "rate" | head -5 >>$LOG
|
||||
tc -s class show dev ${dev}_ifb | grep "rate" | head -5 >>$LOG
|
||||
|
||||
;;
|
||||
"add")
|
||||
echo "➕ 添加限速规则..."
|
||||
for list in $(echo $crid | sed -e 's/!//g' | sed 's/,/ /g'); do
|
||||
mac=$(uci -q get $NAME.@device[$list].mac 2>/dev/null)
|
||||
download_mb=$(uci -q get $NAME.@device[$list].download 2>/dev/null)
|
||||
upload_mb=$(uci -q get $NAME.@device[$list].upload 2>/dev/null)
|
||||
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
|
||||
DL=$(to_kbit "$download_mb")
|
||||
UL=$(to_kbit "$upload_mb")
|
||||
|
||||
if is_macaddr $mac; then
|
||||
add_mac $list
|
||||
echo "✅ 添加MAC限速: $comment ($mac)" >>$LOG
|
||||
else
|
||||
add_ip $list
|
||||
echo "✅ 添加IP限速: $comment ($mac)" >>$LOG
|
||||
fi
|
||||
done
|
||||
;;
|
||||
"del")
|
||||
for list in $(echo $crid | sed -e 's/!//g' | sed 's/,/ /g'); do
|
||||
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
|
||||
mac=$(uci -q get $NAME.@device[$list].mac 2>/dev/null)
|
||||
del_id $list
|
||||
done
|
||||
;;
|
||||
"status")
|
||||
echo "EQOSPLUS限速服务状态"
|
||||
echo "### 网络接口: $dev ###"
|
||||
echo "# 下载队列统计 #"
|
||||
tc -s qdisc show dev $dev 2>/dev/null || echo "无下载队列"
|
||||
echo "# 下载类统计 #"
|
||||
tc -s class show dev $dev 2>/dev/null | grep -E "class htb|rate [0-9]" || echo "无下载类"
|
||||
echo "# 下载过滤器 #"
|
||||
tc -s filter show dev $dev parent 1: 2>/dev/null | head -10 || echo "无下载过滤器"
|
||||
|
||||
echo "### 虚拟接口: ${dev}_ifb ###"
|
||||
echo "# 上传队列统计 #"
|
||||
tc -s qdisc show dev ${dev}_ifb 2>/dev/null || echo "无上传队列"
|
||||
echo "# 上传类统计 #"
|
||||
tc -s class show dev ${dev}_ifb 2>/dev/null | grep -E "class htb|rate [0-9]" || echo "无上传类"
|
||||
echo "# 上传过滤器 #"
|
||||
tc -s filter show dev ${dev}_ifb parent 1: 2>/dev/null | head -10 || echo "无上传过滤器"
|
||||
|
||||
if [ -n "$nftables_ver" ]; then
|
||||
echo "### NFTables规则 ###"
|
||||
nft list table inet ${NAME} 2>/dev/null || echo "没有NFTables规则"
|
||||
else
|
||||
echo "NFTables不可用"
|
||||
fi
|
||||
|
||||
# 显示流量统计
|
||||
echo "### 流量统计 ###"
|
||||
echo "下载流量:"
|
||||
tc -s class show dev $dev 2>/dev/null | grep "Sent" | head -5
|
||||
echo "上传流量:"
|
||||
tc -s class show dev ${dev}_ifb 2>/dev/null | grep "Sent" | head -5
|
||||
;;
|
||||
|
||||
esac
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Copyright (C) 2006 OpenWrt.org
|
||||
# Copyright 2022-2025 sirpdboy <herboy2008@gmail.com>
|
||||
NAME=eqosplus
|
||||
IDLIST="/var/$NAME.idlist"
|
||||
TMPID="/var/$NAME.tmpid"
|
||||
|
||||
idlist=`uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' `
|
||||
|
||||
check_list() {
|
||||
i=$1
|
||||
checki='0'
|
||||
start_time=$(uci -q get $NAME.@device[$i].timestart 2>/dev/null)
|
||||
end_time=$(uci -q get $NAME.@device[$i].timeend 2>/dev/null)
|
||||
wweek=`uci -q get $NAME.@device[$i].week `
|
||||
current_time=$(date +%H:%M)
|
||||
current_weekday=$(date +%u)
|
||||
for ww in `echo $wweek | sed 's/,/ /g' `; do
|
||||
if [ $current_weekday = $ww ] || [ 'x0' = x$ww ] ;then
|
||||
[ "$start_time" = "$end_time" ] && checki='1' || {
|
||||
if [ "$start_time" \< "$end_time" ]; then
|
||||
if [ "$current_time" \> "$start_time" ] && [ "$current_time" \< "$end_time" ] ; then
|
||||
checki='1'
|
||||
fi
|
||||
else
|
||||
if [ "$start_time" \> "$end_time" ] ; then
|
||||
if [ "$current_time" \< "$start_time" ] && [ "$current_time" \> "$end_time" ] ; then
|
||||
checki='1'
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
fi
|
||||
done
|
||||
return
|
||||
}
|
||||
|
||||
idlistusr(){
|
||||
checki='0'
|
||||
[ -s $IDLIST ] || touch $IDLIST
|
||||
for list in $idlist ;do
|
||||
check_list $list
|
||||
if [ $checki == '1' ] ; then
|
||||
[ `cat $IDLIST 2>/dev/null | grep "!${list}!" | wc -l ` -gt 0 ] || {
|
||||
eqosplus add $list
|
||||
echo "!${list}!" >> $IDLIST ; cat $IDLIST | sort | uniq > $TMPID ;cat $TMPID >$IDLIST ;rm -rf $TMPID
|
||||
}
|
||||
else
|
||||
[ `cat $IDLIST 2>/dev/null | grep "!${list}!" | wc -l ` -gt 0 ] && {
|
||||
eqosplus del $list
|
||||
sed -i "/!$list!/d" $IDLIST >/dev/null 2>&1
|
||||
}
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
while :;do
|
||||
sleep 30
|
||||
idlistusr
|
||||
sleep 30
|
||||
done
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"luci-app-eqosplus": {
|
||||
"description": "Grant UCI access for luci-app-eqosplus",
|
||||
"read": {
|
||||
"uci": [ "eqosplus" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "eqosplus" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 585 KiB |
@@ -5,18 +5,18 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-honk
|
||||
PKG_VERSION:=2026.11.01
|
||||
PKG_VERSION:=2026.11.02
|
||||
PKG_RELEASE:=1
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
PKG_SOURCE:=doona-4569bd91-57b2b25287ba.tar.gz
|
||||
PKG_SOURCE:=doona-08b2a3e1-567a62ed5cac.tar.gz
|
||||
# Each package Makefile is evaluated on its own, so this tag is defined here too
|
||||
# instead of relying on the honk package's variable. It must match honk/Makefile:
|
||||
# scripts/check.sh enforces that.
|
||||
HONK_SOURCE_TAG?=source-41281523460
|
||||
HONK_SOURCE_TAG?=source-41400973213
|
||||
PKG_SOURCE_URL?=https://github.com/kenzok8/openwrt-honk/releases/download/$(HONK_SOURCE_TAG)
|
||||
PKG_SOURCE_SUBDIR:=doona
|
||||
PKG_HASH:=57b2b25287bae791e9a01e14e0c2deb1351e4135933959214d8996ed05458838
|
||||
PKG_HASH:=567a62ed5cac88be540d73d9b41b09b309538b7cedc3b69d56fef880a084d96e
|
||||
|
||||
-include $(CURDIR)/generated-doona-stage.mk
|
||||
|
||||
|
||||
@@ -24,6 +24,25 @@ function isHttpUrl(value) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 导入完整配置前要让人看清会替换掉哪些段落:按候选文件里实际出现的顶层块列。 */
|
||||
const SCOPE_LABELS = {
|
||||
node: 'nodes', subscription: 'subscriptions', group: 'groups',
|
||||
routing: 'routing', dns: 'DNS', global: 'global settings',
|
||||
experimental: 'API and UI settings', include: 'included files'
|
||||
};
|
||||
function describeConfigScope(text) {
|
||||
const found = [];
|
||||
String(text || '').split('\n').forEach(function(line) {
|
||||
const block = /^([A-Za-z_]+)[ \t]*\{/.exec(line);
|
||||
if (!block || !SCOPE_LABELS[block[1]])
|
||||
return;
|
||||
const label = _(SCOPE_LABELS[block[1]]);
|
||||
if (found.indexOf(label) < 0)
|
||||
found.push(label);
|
||||
});
|
||||
return found.length ? found.join(' · ') : _('no recognised sections');
|
||||
}
|
||||
|
||||
function loadYamlParser() {
|
||||
if (window.jsyaml && window.jsyaml.load)
|
||||
return Promise.resolve(window.jsyaml);
|
||||
@@ -93,13 +112,26 @@ return view.extend({
|
||||
|
||||
const page = E('div', { 'class': 'cbi-map honk-page' });
|
||||
page.appendChild(E('div', { 'class': 'honk-header' }, [
|
||||
E('h2', {}, _('Configuration')),
|
||||
E('p', { 'class': 'honk-header-sub' }, _('Honk reads its nodes, subscriptions and routing from /etc/honk/config.dae.'))
|
||||
E('h2', {}, _('Configuration'))
|
||||
]));
|
||||
|
||||
const modeText = E('span', {}, '');
|
||||
const modeNote = E('p', { 'class': 'honk-note honk-mode-note' }, [ modeText ]);
|
||||
page.appendChild(modeNote);
|
||||
const draftNote = E('div', { 'class': 'honk-note honk-draft-note' });
|
||||
draftNote.hidden = true;
|
||||
page.appendChild(E('section', { 'class': 'honk-card honk-config-hero' }, [
|
||||
E('p', { 'class': 'honk-hero-eyebrow' }, _('Configuration management')),
|
||||
E('p', { 'class': 'honk-hero-lead' }, _('Add subscriptions and nodes, or import and edit a complete configuration.')),
|
||||
E('p', { 'class': 'honk-hero-points' }, _('Append keeps your rules · validated before applying · rolled back when startup fails')),
|
||||
E('details', { 'class': 'honk-hero-details' }, [
|
||||
E('summary', {}, _('Read before use')),
|
||||
E('ul', {}, [
|
||||
E('li', {}, _('Subscriptions and nodes must use a protocol Honk supports; SSR is not supported.')),
|
||||
E('li', {}, _('For a complete configuration, check lan_interface and the group names against your nodes.')),
|
||||
E('li', {}, _('Importing a complete configuration replaces the current one; back it up first, startup failures roll back.')),
|
||||
E('li', {}, _('In the editor, checking only parses; a draft stays inactive until you save and apply.'))
|
||||
])
|
||||
]),
|
||||
draftNote
|
||||
]));
|
||||
|
||||
/* --- Add subscription --- */
|
||||
const subStatus = E('p', { 'class': 'honk-note', 'role': 'status', 'aria-live': 'polite' }, '');
|
||||
@@ -136,12 +168,18 @@ return view.extend({
|
||||
}).finally(function() { subBtn.disabled = false; });
|
||||
});
|
||||
|
||||
const subModeHint = E('p', { 'class': 'honk-note honk-mode-hint' }, _('Manual mode: this tool is disabled until you restore managed mode (link at the top).'));
|
||||
subModeHint.hidden = true;
|
||||
nameInput.id = 'honk-sub-name';
|
||||
urlInput.id = 'honk-sub-url';
|
||||
const subSection = E('section', { 'class': 'honk-card' }, [
|
||||
E('p', { 'class': 'honk-note' }, _('Enter a name and subscription URL. Honk checks for usable nodes before saving.')),
|
||||
subModeHint,
|
||||
E('div', { 'class': 'honk-subscription-add' }, [ nameInput, urlInput, subBtn ]),
|
||||
E('div', { 'class': 'honk-form-row' }, [
|
||||
E('label', { 'class': 'honk-form-label', 'for': 'honk-sub-name' }, _('Name')),
|
||||
E('div', { 'class': 'honk-form-field' }, [ nameInput ])
|
||||
]),
|
||||
E('div', { 'class': 'honk-form-row' }, [
|
||||
E('label', { 'class': 'honk-form-label', 'for': 'honk-sub-url' }, _('Subscription URL')),
|
||||
E('div', { 'class': 'honk-form-field' }, [ urlInput ])
|
||||
]),
|
||||
E('div', { 'class': 'honk-form-actions' }, [ subBtn ]),
|
||||
subStatus
|
||||
]);
|
||||
|
||||
@@ -273,11 +311,7 @@ return view.extend({
|
||||
}).finally(function() { pasteBtn.disabled = false; });
|
||||
});
|
||||
|
||||
const pasteModeHint = E('p', { 'class': 'honk-note honk-mode-hint' }, _('Manual mode: this tool is disabled until you restore managed mode (link at the top).'));
|
||||
pasteModeHint.hidden = true;
|
||||
const pasteSection = E('section', { 'class': 'honk-card' }, [
|
||||
E('p', { 'class': 'honk-note' }, _('Paste Clash YAML, Surge INI, Base64 or share links. They are parsed locally into nodes and appended to /etc/honk/config.dae.')),
|
||||
pasteModeHint,
|
||||
pasteInput,
|
||||
E('div', { 'class': 'honk-actions' }, [ pasteBtn ]),
|
||||
pasteStatus
|
||||
@@ -297,7 +331,6 @@ return view.extend({
|
||||
const importBtn = E('button', { 'class': 'cbi-button cbi-button-apply', 'type': 'button' }, [ _('Import and start') ]);
|
||||
|
||||
const importSection = E('section', { 'class': 'honk-card' }, [
|
||||
E('p', { 'class': 'honk-note' }, _('Upload a complete dae file (subscriptions, nodes and routing included) and Honk starts with it: the file is validated, written to /etc/honk/config.dae and the service is restarted. To hand-edit or paste instead, use the editor below.')),
|
||||
E('div', { 'class': 'honk-file-row' }, [ filePick, fileName, importBtn ]),
|
||||
importStatus
|
||||
]);
|
||||
@@ -309,11 +342,8 @@ return view.extend({
|
||||
'spellcheck': 'false', 'wrap': 'off', 'aria-label': _('Full dae configuration')
|
||||
});
|
||||
const saveBtn = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'button' }, [ _('Save and apply') ]);
|
||||
const reloadBtn = E('button', { 'class': 'cbi-button', 'type': 'button' }, [ _('Reload from disk') ]);
|
||||
const draftBtn = E('button', { 'class': 'cbi-button', 'type': 'button' }, [ _('Save draft') ]);
|
||||
const validateBtn = E('button', { 'class': 'cbi-button cbi-button-link', 'type': 'button' }, [ _('Check configuration') ]);
|
||||
const managedBtn = E('a', { 'href': '#', 'class': 'honk-inline-link', 'role': 'button' }, [ _('Restore managed mode') ]);
|
||||
managedBtn.hidden = true;
|
||||
modeNote.appendChild(managedBtn);
|
||||
|
||||
let editorBaseline = '';
|
||||
|
||||
@@ -608,39 +638,9 @@ return view.extend({
|
||||
/* A failed apply rolls the file back, so reload what is really on
|
||||
disk — but keep the outcome message the user needs to read. */
|
||||
const outcome = target.textContent;
|
||||
return refreshMode().then(function() {
|
||||
return loadCurrent(outcome === editorStatus.textContent ? outcome : '', true).catch(function() {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function refreshMode() {
|
||||
return honk.status().then(function(status) {
|
||||
const manual = !!status && status.config_mode === 'manual';
|
||||
managedBtn.hidden = !manual;
|
||||
const tabState = page._honkTabs;
|
||||
if (tabState) {
|
||||
tabState.tabs.forEach(function(tab) {
|
||||
const disabled = manual && tab.managedOnly;
|
||||
tab.button.disabled = disabled;
|
||||
tab.panel.hidden = tab.id !== tabState.getActive();
|
||||
});
|
||||
if (manual && ['sub', 'nodes'].indexOf(tabState.getActive()) >= 0)
|
||||
tabState.select('edit');
|
||||
}
|
||||
subModeHint.hidden = !manual;
|
||||
pasteModeHint.hidden = !manual;
|
||||
subBtn.disabled = manual;
|
||||
pasteBtn.disabled = manual;
|
||||
nameInput.disabled = manual;
|
||||
urlInput.disabled = manual;
|
||||
pasteInput.disabled = manual;
|
||||
modeText.textContent = manual
|
||||
? _('Manual mode: the file below owns nodes, groups, routing and DNS.')
|
||||
: _('Managed mode: the subscription and paste-nodes tools are active. Saving here switches Honk to manual mode.');
|
||||
return status;
|
||||
}).catch(function() {
|
||||
modeText.textContent = '';
|
||||
return loadCurrent(outcome === editorStatus.textContent ? outcome : '', true)
|
||||
.then(renderDraftNote)
|
||||
.catch(function() {});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -689,6 +689,8 @@ return view.extend({
|
||||
}
|
||||
if (plan.notes.length)
|
||||
setEditorContent(plan.text, _('Adapted in the editor: %s').format(plan.notes.join('; ')));
|
||||
if (!confirm(_('This file replaces the current configuration.\n\nIt defines: %s\n\nBack up the current configuration first if you are unsure. Continue?').format(describeConfigScope(editor.value))))
|
||||
return null;
|
||||
return applyContent(editor.value, importStatus).then(function() {
|
||||
if (importStatus.textContent === _('Configuration applied and Honk is running.'))
|
||||
importStatus.textContent = _('%s imported and Honk is running.').format(file.name);
|
||||
@@ -699,15 +701,6 @@ return view.extend({
|
||||
}).finally(function() { importBtn.disabled = false; });
|
||||
});
|
||||
|
||||
reloadBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (!confirmEditorReplace()) return;
|
||||
reloadBtn.disabled = true;
|
||||
loadCurrent(null, true).catch(function(error) {
|
||||
editorStatus.textContent = honk.errorMessage(error, _('Could not read the saved configuration.'));
|
||||
}).finally(function() { reloadBtn.disabled = false; });
|
||||
});
|
||||
|
||||
validateBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (!editor.value.trim()) { editorStatus.textContent = _('The editor is empty.'); return; }
|
||||
@@ -727,40 +720,88 @@ return view.extend({
|
||||
adaptEditor().then(function(proceed) {
|
||||
if (!proceed) { editorStatus.textContent = _('Save cancelled.'); return null; }
|
||||
return applyContent(editor.value, editorStatus);
|
||||
}).finally(function() { saveBtn.disabled = false; });
|
||||
}).finally(function() {
|
||||
saveBtn.disabled = false;
|
||||
renderDraftNote();
|
||||
});
|
||||
});
|
||||
|
||||
managedBtn.addEventListener('click', function(ev) {
|
||||
/* 草稿只备份到 /etc/honk/config-draft.dae:不动活动配置,也不重启服务。 */
|
||||
draftBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (!confirm(_('Restore the managed configuration and return to the subscription tools? Honk restarts if it is running.'))) return;
|
||||
managedBtn.disabled = true;
|
||||
editorStatus.textContent = _('Restoring the managed configuration…');
|
||||
withLongTimeout(function() { return honk.configUseManaged().then(honk.ensureOk); }).then(function(result) {
|
||||
editorStatus.textContent = honk.resultMessage(result, _('Managed mode restored.'));
|
||||
return refreshMode().then(function() { return loadCurrent(null, true); });
|
||||
if (!editor.value.trim()) { editorStatus.textContent = _('The editor is empty.'); return; }
|
||||
draftBtn.disabled = true;
|
||||
editorStatus.textContent = _('Saving draft…');
|
||||
honk.configSave(editor.value).then(function(result) {
|
||||
editorStatus.textContent = isOk(result)
|
||||
? _('Draft saved. It is not active until you apply it.')
|
||||
: validationText(result);
|
||||
return renderDraftNote();
|
||||
}).catch(function(error) {
|
||||
editorStatus.textContent = honk.errorMessage(error, _('Could not restore managed mode.'));
|
||||
}).finally(function() { managedBtn.disabled = false; });
|
||||
editorStatus.textContent = honk.errorMessage(error, _('Could not save the draft.'));
|
||||
}).finally(function() { draftBtn.disabled = false; });
|
||||
});
|
||||
|
||||
const editorSection = E('section', { 'class': 'honk-card' }, [
|
||||
E('p', { 'class': 'honk-note' }, _('Edit or paste a complete dae file. Check configuration only parses the text; Save and apply writes /etc/honk/config.dae, restarts Honk, and rolls back if startup fails.')),
|
||||
E('div', { 'class': 'honk-subscription-add' }, [ saveBtn, reloadBtn, validateBtn ]),
|
||||
E('div', { 'class': 'honk-actions' }, [ saveBtn, draftBtn, validateBtn ]),
|
||||
editor,
|
||||
editorStatus
|
||||
]);
|
||||
|
||||
/* 草稿提示:只提示存在与差异,载入必须由用户明确点击。 */
|
||||
function draftButton(label, source) {
|
||||
const button = E('button', { 'class': 'cbi-button cbi-button-link', 'type': 'button' }, [ label ]);
|
||||
button.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
loadDraft(source);
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function renderDraftNote() {
|
||||
return honk.status().then(function(status) {
|
||||
const parts = [];
|
||||
if (status && status.draft_pending) {
|
||||
parts.push(E('span', {}, _('An unapplied draft exists.')));
|
||||
parts.push(draftButton(_('Load draft'), 'draft'));
|
||||
}
|
||||
if (status && status.legacy_draft) {
|
||||
parts.push(E('span', {}, _('An old manual draft from a previous version is kept on disk.')));
|
||||
parts.push(draftButton(_('Load old draft'), 'legacy'));
|
||||
}
|
||||
draftNote.textContent = '';
|
||||
if (!parts.length) {
|
||||
draftNote.hidden = true;
|
||||
return status;
|
||||
}
|
||||
draftNote.hidden = false;
|
||||
parts.forEach(function(node) { draftNote.appendChild(node); });
|
||||
return status;
|
||||
}).catch(function() {
|
||||
draftNote.hidden = true;
|
||||
});
|
||||
}
|
||||
|
||||
function loadDraft(source) {
|
||||
if (!confirmEditorReplace()) return;
|
||||
honk.configDraftRead(source).then(honk.ensureOk).then(function(result) {
|
||||
setEditorContent(result.content || '', _('Draft loaded into the editor. It is not active until you apply it.'));
|
||||
}).catch(function(error) {
|
||||
editorStatus.textContent = honk.errorMessage(error, _('Could not read the draft.'));
|
||||
});
|
||||
}
|
||||
|
||||
loadCurrent().catch(function(error) {
|
||||
editorStatus.textContent = honk.errorMessage(error, _('Could not read the saved configuration.'));
|
||||
});
|
||||
refreshMode();
|
||||
renderDraftNote();
|
||||
|
||||
/* 一排标签页,只显示当前板块:四张卡片竖着堆太占屏,也不好一眼看出"有几种做法"。 */
|
||||
const tabs = [
|
||||
{ id: 'sub', label: _('Add subscription'), panel: subSection, managedOnly: true },
|
||||
{ id: 'nodes', label: _('Add nodes'), panel: pasteSection, managedOnly: true },
|
||||
{ id: 'import', label: _('Import config'), panel: importSection, managedOnly: false },
|
||||
{ id: 'edit', label: _('Edit config'), panel: editorSection, managedOnly: false }
|
||||
{ id: 'sub', label: _('Add subscription'), panel: subSection },
|
||||
{ id: 'nodes', label: _('Add nodes'), panel: pasteSection },
|
||||
{ id: 'import', label: _('Import config'), panel: importSection },
|
||||
{ id: 'edit', label: _('Edit config'), panel: editorSection }
|
||||
];
|
||||
let activeTab = 'sub';
|
||||
const tabBar = E('div', { 'class': 'honk-seg honk-tabs', 'role': 'tablist' });
|
||||
@@ -791,8 +832,6 @@ return view.extend({
|
||||
tabs.forEach(function(tab) { page.appendChild(tab.panel); });
|
||||
selectTab(activeTab);
|
||||
|
||||
/* 曝光给 refreshMode:手动模式下订阅/节点两个标签不可用,自动切到编辑配置。 */
|
||||
page._honkTabs = { tabs: tabs, select: selectTab, getActive: function() { return activeTab; } };
|
||||
return page;
|
||||
}
|
||||
});
|
||||
@@ -256,21 +256,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.honk-subscription-add {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 加 .honk-page 前缀压过后面的 `.honk-page .cbi-input-text{min-width:0}`,
|
||||
否则手机上两个输入框会被挤成一条窄缝而不是换行。 */
|
||||
.honk-page .honk-subscription-add .cbi-input-text {
|
||||
flex: 1 1 12rem;
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
/* ---------- Meta line (mono, muted) ---------- */
|
||||
|
||||
.honk-meta {
|
||||
@@ -622,14 +607,6 @@
|
||||
|
||||
/* ---------- Notes & alerts ---------- */
|
||||
|
||||
.honk-mode-note .honk-inline-link {
|
||||
margin-left: 8px;
|
||||
color: var(--hk-accent-border);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.honk-note {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
@@ -937,6 +914,127 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ---------- Configuration page ---------- */
|
||||
|
||||
/* 顶部说明卡片:一眼看出"能做什么",折叠区只放真正需要确认的三条。 */
|
||||
.honk-config-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 20px 22px;
|
||||
}
|
||||
|
||||
.honk-hero-eyebrow {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--hk-muted);
|
||||
}
|
||||
|
||||
.honk-hero-lead {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.honk-hero-points {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
color: var(--hk-muted);
|
||||
}
|
||||
|
||||
.honk-hero-details {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--hk-card-border);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
color: var(--hk-muted);
|
||||
}
|
||||
|
||||
.honk-hero-details summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
color: var(--hk-accent-border);
|
||||
}
|
||||
|
||||
.honk-hero-details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.honk-hero-details summary::before {
|
||||
content: '\25B8';
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.honk-hero-details[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.honk-hero-details ul {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 18px;
|
||||
max-width: 72ch;
|
||||
}
|
||||
|
||||
/* 订阅表单:桌面两行、标签右对齐;移动端上下排列(见下方媒体查询)。 */
|
||||
.honk-form-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.honk-form-label {
|
||||
flex: 0 0 7.5rem;
|
||||
padding-top: 8px;
|
||||
font-size: 12.5px;
|
||||
text-align: right;
|
||||
color: var(--hk-muted);
|
||||
}
|
||||
|
||||
.honk-form-field {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.honk-page .honk-form-field .cbi-input-text {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.honk-form-hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--hk-muted);
|
||||
}
|
||||
|
||||
.honk-form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.honk-draft-note {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.honk-page .honk-config-editor {
|
||||
height: 46vh;
|
||||
max-height: 560px;
|
||||
}
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -1047,3 +1145,36 @@ html[data-darkmode="true"] .honk-page .honk-log-output {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.honk-form-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.honk-form-label {
|
||||
flex: none;
|
||||
padding-top: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.honk-form-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.honk-form-actions .cbi-button {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.honk-config-hero {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.honk-page .honk-config-editor {
|
||||
height: auto;
|
||||
min-height: 300px;
|
||||
max-height: 60vh;
|
||||
}
|
||||
}
|
||||
@@ -230,15 +230,13 @@ return view.extend({
|
||||
clashPortInput.value = status.clash_api_port ? String(status.clash_api_port) : '9090';
|
||||
|
||||
card.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Service settings')));
|
||||
/* Manual mode owns the whole runtime from config.dae, so the backend
|
||||
rejects these settings. Disable them here instead of failing on save. */
|
||||
const manualMode = status.config_mode === 'manual';
|
||||
if (manualMode) {
|
||||
/* 字段归属由当前配置决定,非 system.dae 提供时这些设置只读。 */
|
||||
const owner = status.settings_owner || 'unknown';
|
||||
if (owner !== 'system') {
|
||||
[ networkSelect, portInput, clashEnabledInput, clashPortInput, clashSecretInput ].forEach(function(input) {
|
||||
input.disabled = true;
|
||||
});
|
||||
save.disabled = true;
|
||||
card.appendChild(E('p', { 'class': 'honk-note honk-mode-hint' }, _('Manual mode: these settings belong to config.dae, so they are read-only here. Restore managed mode on the Configuration page to change them.')));
|
||||
}
|
||||
if (!(data.networks || []).length)
|
||||
networkSelect.appendChild(E('option', { 'value': '' }, _('No network interfaces found')));
|
||||
|
||||
@@ -58,12 +58,16 @@ const callImportDae = rpc.declare({
|
||||
expect: { '': {} }
|
||||
});
|
||||
const callConfigRead = rpc.declare({ object: 'honk', method: 'config_read', expect: { '': {} } });
|
||||
const callConfigDraftRead = rpc.declare({ object: 'honk', method: 'config_draft_read', expect: { '': {} } });
|
||||
const callConfigDraftRead = rpc.declare({
|
||||
object: 'honk',
|
||||
method: 'config_draft_read',
|
||||
params: [ 'source' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
const callLanCandidates = rpc.declare({ object: 'honk', method: 'lan_candidates', expect: { '': {} } });
|
||||
const callConfigValidate = rpc.declare({ object: 'honk', method: 'config_validate', params: [ 'content' ], expect: { '': {} } });
|
||||
const callConfigSave = rpc.declare({ object: 'honk', method: 'config_save', params: [ 'content' ], expect: { '': {} } });
|
||||
const callConfigApply = rpc.declare({ object: 'honk', method: 'config_apply', expect: { '': {} } });
|
||||
const callConfigUseManaged = rpc.declare({ object: 'honk', method: 'config_use_managed', expect: { '': {} } });
|
||||
const callRepair = rpc.declare({ object: 'honk', method: 'repair', expect: { '': {} } });
|
||||
const callBackup = rpc.declare({ object: 'honk', method: 'backup', expect: { '': {} } });
|
||||
const callRestore = rpc.declare({ object: 'honk', method: 'restore', expect: { '': {} } });
|
||||
@@ -248,12 +252,10 @@ function tokenMessage(token) {
|
||||
config_too_large: _('The configuration file exceeds the 2 MiB limit.'),
|
||||
config_applied: _('Configuration applied and Honk is running.'),
|
||||
config_apply_failed_rolled_back: _('Honk could not start with this configuration. The previous file was restored.'),
|
||||
config_apply_rollback_failed_snapshot_retained: _('Startup failed and the automatic rollback did not finish. The snapshot was kept; run system recovery.'),
|
||||
config_apply_rollback_failed: _('Startup failed and the automatic rollback did not finish. The snapshot was kept; run system recovery.'),
|
||||
config_changed_concurrently: _('The configuration changed during the operation. Nothing was applied.'),
|
||||
config_write_failed_snapshot_retained: _('Could not write the configuration. The snapshot was kept.'),
|
||||
service_stop_unconfirmed_snapshot_retained: _('Could not confirm that Honk stopped. The operation was aborted and the snapshot was kept.'),
|
||||
manual_mode_required: _('Manual mode is active. Restore managed mode before using the subscription tools.'),
|
||||
managed_edit_unavailable: _('Manual mode is active, so this tool cannot edit the configuration. Select Restore managed mode first.'),
|
||||
service_stop_unconfirmed: _('Could not confirm that Honk stopped. The operation was aborted and the snapshot was kept.'),
|
||||
administrator_exists_reuse_credentials_or_reset_state: _('已经初始化过了,请用原来的账号登录 Doona。忘记密码:到「维护 → 重置管理员」清掉管理员记录(不动订阅和规则),再回到本页重新初始化。'),
|
||||
administrator_setup_failed: _('创建 Doona 管理员失败,请到「维护 → 日志」查看核心日志。'),
|
||||
invalid_credentials: _('用户名只能包含字母、数字、下划线、点、连字符(不超过 64 个字符);密码长度必须是 8 到 128 个字符,可以包含空格。'),
|
||||
@@ -271,9 +273,7 @@ function tokenMessage(token) {
|
||||
main_config_changed_concurrently: _('操作期间 config.dae 被其他进程改动,已中止。'),
|
||||
invalid_package_config: _('luci-app-honk 的软件包配置不完整,请重新安装。'),
|
||||
invalid_config_path: _('配置路径不是 /etc/honk/config.dae,拒绝操作。'),
|
||||
manual_config_valid: _('手动模式配置有效。'),
|
||||
manual_config_invalid: _('手动模式配置无效,请先修正 config.dae。'),
|
||||
required_system_include_missing_manual_repair: _('手动模式的配置缺少 include { system.dae },无法修复托管设置。'),
|
||||
required_system_include_missing: _('当前配置没有 include { system.dae },这些设置请到「编辑配置」里修改。'),
|
||||
native_api_setup_state_unavailable: _('读不到核心的 API 状态,请查看日志。'),
|
||||
ui_assets_missing: _('缺少 Doona 静态文件(/usr/share/doona),请重新安装 luci-app-honk。'),
|
||||
mock_core_start_failed: _('测试用核心启动失败:通常是内核缺 BTF(/sys/kernel/btf/vmlinux)或 eBPF 依赖模块。'),
|
||||
@@ -288,7 +288,6 @@ function tokenMessage(token) {
|
||||
setup_verification_failed: _('初始化后的自检没通过,请查看日志。'),
|
||||
lan_ipv4_unavailable: _('选中的 LAN 网络没有 IPv4 地址。'),
|
||||
mkdir_failed: _('创建目录失败,请检查磁盘空间与权限。'),
|
||||
settings_unavailable_in_manual_mode: _('手动模式下这些设置由 config.dae 决定,无法保存。'),
|
||||
settings_too_large: _('提交的设置数据过大。'),
|
||||
invalid_settings_json: _('设置的请求格式不正确。'),
|
||||
invalid_boolean_settings: _('开关类设置取值不合法。'),
|
||||
@@ -320,7 +319,7 @@ function tokenMessage(token) {
|
||||
subscription_empty: _('订阅内容是空的。'),
|
||||
subscription_too_large: _('订阅内容超过 4 MiB,拒绝处理。'),
|
||||
configuration_validation_failed: _('配置没有通过静态校验。'),
|
||||
config_changed_concurrently_restart_failed_snapshot_retained: _('操作期间配置被改动且重启失败,快照已保留。'),
|
||||
config_changed_concurrently_restart_failed: _('操作期间配置被改动且重启失败,快照已保留。'),
|
||||
invalid_job_id: _('任务编号不合法。'),
|
||||
job_id_unavailable: _('拿不到任务编号。'),
|
||||
job_not_found: _('找不到该任务。'),
|
||||
@@ -328,19 +327,18 @@ function tokenMessage(token) {
|
||||
job_storage_unavailable: _('任务存储不可用(/tmp 空间不足?)。'),
|
||||
prepared: _('上传目录已准备好。'),
|
||||
probe_failed: _('预检失败,请稍后重试。'),
|
||||
managed_switch_failed_rolled_back: _('Could not switch back to managed mode. The previous configuration was restored.'),
|
||||
managed_switch_rollback_failed_snapshot_retained: _('Switching back to managed mode failed and the rollback did not finish. The snapshot was kept.'),
|
||||
managed_snapshot_invalid: _('The saved managed configuration is not a valid regular file.'),
|
||||
config_draft_missing: _('There is no saved draft to apply.'),
|
||||
config_draft_invalid: _('The saved draft is not a valid regular file.'),
|
||||
managed_mode_restored: _('Managed mode restored.'),
|
||||
managed_snapshot_missing: _('There is no saved managed configuration to restore.'),
|
||||
lan_candidates_unavailable: _('Could not read the list of network interfaces.'),
|
||||
config_read_failed: _('Honk could not read the current configuration.'),
|
||||
config_request_invalid: _('The configuration request is empty or invalid.'),
|
||||
config_validation_failed: _('The configuration did not pass static validation.'),
|
||||
config_validation_unavailable: _('The core validator did not return a result.'),
|
||||
config_settings_owned_by_file: _('这些设置由当前配置文件决定,请到「配置 → 编辑配置」里修改。'),
|
||||
config_valid: _('配置通过校验。'),
|
||||
config_invalid: _('配置没有通过校验。'),
|
||||
config_saved_pending_apply: _('Configuration saved. The running service is unchanged until you apply it.'),
|
||||
quick_add_unsupported: _('当前配置不支持快捷追加,请在「编辑配置」中修改。'),
|
||||
config_loaded: _('Configuration loaded.'),
|
||||
dae_loaded: _('Configuration file loaded into the editor.'),
|
||||
upload_ready: _('Upload location ready.'),
|
||||
@@ -467,7 +465,6 @@ return baseclass.extend({
|
||||
configValidate: callConfigValidate,
|
||||
configSave: callConfigSave,
|
||||
configApply: callConfigApply,
|
||||
configUseManaged: callConfigUseManaged,
|
||||
repair: callRepair,
|
||||
backup: callBackup,
|
||||
restore: callRestore,
|
||||
|
||||
@@ -69,16 +69,13 @@ return view.extend({
|
||||
|
||||
section.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Honk settings')));
|
||||
|
||||
/* In manual mode the whole runtime comes from config.dae, so the backend
|
||||
refuses these settings. Disable them instead of letting the save fail
|
||||
with an error the user cannot act on. */
|
||||
const manualMode = !!(data.status && data.status.config_mode === 'manual');
|
||||
if (manualMode) {
|
||||
/* 配置自带 global/experimental 时这些值归文件,后端会拒绝写入。 */
|
||||
const owner = (data.status && data.status.settings_owner) || 'unknown';
|
||||
if (owner !== 'system') {
|
||||
[ networkSelect, portInput, clashEnabledInput, clashPortInput, clashSecretInput ].forEach(function(input) {
|
||||
input.disabled = true;
|
||||
});
|
||||
save.disabled = true;
|
||||
section.appendChild(E('p', { 'class': 'honk-note honk-mode-hint' }, _('Manual mode: these settings belong to config.dae, so they are read-only here. Restore managed mode on the Configuration page to change them.')));
|
||||
}
|
||||
if (data.statusError)
|
||||
section.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read current settings: %s').format(data.statusError)));
|
||||
|
||||
+1108
-1645
File diff suppressed because it is too large.
Load diff
@@ -30,14 +30,6 @@ emit_result() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
require_managed_mode() {
|
||||
managed_mode=$(uci -q get honk.main.config_mode 2>/dev/null)
|
||||
[ -n "$managed_mode" ] || managed_mode=managed
|
||||
[ "$managed_mode" = managed ] && return 0
|
||||
emit_result false managed_edit_unavailable
|
||||
return 1
|
||||
}
|
||||
|
||||
emit_errors() {
|
||||
ok=$1
|
||||
[ "$ok" = true ] && ok=1 || ok=0
|
||||
@@ -59,12 +51,11 @@ lock_owner_invalid() {
|
||||
}
|
||||
|
||||
status() {
|
||||
local enabled initialized boot_enabled running api_url api_ready core_version core_commit doona_version kernel lan_network listen_port errors listen clash_api_enabled clash_api_port clash_api_secret clash_api_url clash_api_ready clash_ui_ready clash_secret_set config_mode api_enabled core_ready config_pending manual_runtime clash_listen_addr
|
||||
local enabled initialized boot_enabled running api_url api_ready core_version core_commit doona_version kernel lan_network listen_port errors listen clash_api_enabled clash_api_port clash_api_secret clash_api_url clash_api_ready clash_ui_ready clash_secret_set settings_owner api_enabled core_ready config_pending draft_pending legacy_draft clash_listen_addr
|
||||
config_load honk
|
||||
config_get_bool enabled main enabled 0
|
||||
config_get_bool initialized main initialized 0
|
||||
config_get_bool boot_enabled main boot_enabled 0
|
||||
config_get config_mode main config_mode managed
|
||||
config_get listen_port main listen_port 9527
|
||||
config_get lan_network main lan_network lan
|
||||
config_get_bool clash_api_enabled main clash_api_enabled 0
|
||||
@@ -72,77 +63,70 @@ status() {
|
||||
config_get clash_api_secret main clash_api_secret ''
|
||||
managed_pid=$(procd_core_pid)
|
||||
if is_managed_core "$managed_pid"; then running=1; else running=0; fi
|
||||
api_enabled=1
|
||||
# 只有 system.dae 负责这些字段时,UCI 里的端口/密钥才有意义。
|
||||
settings_owner=$(config_owner /etc/honk/config.dae)
|
||||
api_enabled=0
|
||||
clash_api_enabled=0
|
||||
clash_api_port=0
|
||||
clash_secret_set=0
|
||||
clash_listen_addr=
|
||||
core_ready=0
|
||||
config_pending=0
|
||||
if [ "$config_mode" = manual ]; then
|
||||
api_enabled=0
|
||||
clash_api_enabled=0
|
||||
clash_api_port=0
|
||||
clash_secret_set=0
|
||||
clash_listen_addr=
|
||||
manual_runtime=$("$CORE" config validate --config /etc/honk/config.dae 2>/dev/null)
|
||||
if [ -n "$manual_runtime" ]; then
|
||||
json_load "$manual_runtime" 2>/dev/null
|
||||
json_select runtime 2>/dev/null
|
||||
json_get_var api_enabled native_api_enabled
|
||||
json_get_var listen native_api_listen
|
||||
json_get_var clash_listen_addr clash_api_listen
|
||||
json_get_var clash_secret_set clash_api_secret_set
|
||||
json_select .. 2>/dev/null
|
||||
fi
|
||||
[ "$api_enabled" = 1 ] || api_enabled=0
|
||||
if [ -n "$managed_pid" ] && honk_lifecycle_ready_owned "$managed_pid"; then core_ready=1; fi
|
||||
if [ "$core_ready" = 1 ]; then
|
||||
runtime_token=$(awk 'NR == 1 { print $1 }' /var/run/honk/readiness 2>/dev/null)
|
||||
runtime_marker=/var/run/honk/core-ready-$runtime_token
|
||||
runtime_api_enabled=$(sed -n 's/^api_enabled=//p' "$runtime_marker" 2>/dev/null)
|
||||
runtime_listen=$(sed -n 's/^api_listen=//p' "$runtime_marker" 2>/dev/null)
|
||||
runtime_clash_listen=$(sed -n 's/^clash_api_listen=//p' "$runtime_marker" 2>/dev/null)
|
||||
runtime_clash_secret_set=$(sed -n 's/^clash_api_secret_set=//p' "$runtime_marker" 2>/dev/null)
|
||||
case "$runtime_api_enabled" in true) api_enabled=1 ;; false) api_enabled=0 ;; esac
|
||||
[ -n "$runtime_listen" ] && listen=$runtime_listen
|
||||
clash_listen_addr=$runtime_clash_listen
|
||||
case "$runtime_clash_secret_set" in true) clash_secret_set=1 ;; false) clash_secret_set=0 ;; esac
|
||||
fi
|
||||
if [ -n "$clash_listen_addr" ]; then
|
||||
clash_api_enabled=1
|
||||
clash_api_port=${clash_listen_addr##*:}
|
||||
fi
|
||||
active_hash=$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')
|
||||
runtime_hash=$(awk 'NR == 1 { print $2 }' /var/run/honk/readiness 2>/dev/null)
|
||||
[ -n "$runtime_hash" ] && [ "$runtime_hash" = "$active_hash" ] || { [ "$running" = 1 ] && config_pending=1; }
|
||||
if [ -f /etc/honk/manual-config.dae ] && [ ! -L /etc/honk/manual-config.dae ]; then
|
||||
draft_hash=$(sha256sum /etc/honk/manual-config.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$draft_hash" = "$active_hash" ] || config_pending=1
|
||||
fi
|
||||
else
|
||||
listen=$(sed -n "s/^[[:space:]]*listen:[[:space:]]*'\\([^']*\\)'.*/\\1/p" /etc/honk/system.dae | head -n 1)
|
||||
draft_pending=0
|
||||
legacy_draft=0
|
||||
listen=
|
||||
runtime_config=$("$CORE" config validate --config /etc/honk/config.dae 2>/dev/null)
|
||||
if [ -n "$runtime_config" ]; then
|
||||
json_load "$runtime_config" 2>/dev/null
|
||||
json_select runtime 2>/dev/null
|
||||
json_get_var api_enabled native_api_enabled
|
||||
json_get_var listen native_api_listen
|
||||
json_get_var clash_listen_addr clash_api_listen
|
||||
json_get_var clash_secret_set clash_api_secret_set
|
||||
json_select .. 2>/dev/null
|
||||
fi
|
||||
case "$api_enabled" in true) api_enabled=1 ;; *) api_enabled=0 ;; esac
|
||||
case "$clash_secret_set" in true) clash_secret_set=1 ;; *) clash_secret_set=0 ;; esac
|
||||
if [ -n "$managed_pid" ] && honk_lifecycle_ready_owned "$managed_pid"; then core_ready=1; fi
|
||||
if [ "$core_ready" = 1 ]; then
|
||||
runtime_token=$(awk 'NR == 1 { print $1 }' /var/run/honk/readiness 2>/dev/null)
|
||||
runtime_marker=/var/run/honk/core-ready-$runtime_token
|
||||
runtime_api_enabled=$(sed -n 's/^api_enabled=//p' "$runtime_marker" 2>/dev/null)
|
||||
runtime_listen=$(sed -n 's/^api_listen=//p' "$runtime_marker" 2>/dev/null)
|
||||
runtime_clash_listen=$(sed -n 's/^clash_api_listen=//p' "$runtime_marker" 2>/dev/null)
|
||||
runtime_clash_secret_set=$(sed -n 's/^clash_api_secret_set=//p' "$runtime_marker" 2>/dev/null)
|
||||
case "$runtime_api_enabled" in true) api_enabled=1 ;; false) api_enabled=0 ;; esac
|
||||
[ -n "$runtime_listen" ] && listen=$runtime_listen
|
||||
clash_listen_addr=$runtime_clash_listen
|
||||
case "$runtime_clash_secret_set" in true) clash_secret_set=1 ;; false) clash_secret_set=0 ;; esac
|
||||
fi
|
||||
if [ -n "$clash_listen_addr" ]; then
|
||||
clash_api_enabled=1
|
||||
clash_api_port=${clash_listen_addr##*:}
|
||||
fi
|
||||
case "$listen" in
|
||||
''|*[!0-9.:]*) listen="127.0.0.1:$listen_port" ;;
|
||||
esac
|
||||
api_url="http://$listen"
|
||||
if [ "$config_mode" = manual ] && [ "$api_enabled" != 1 ]; then api_url=""; fi
|
||||
case "$clash_api_port" in
|
||||
''|*[!0-9]*) clash_api_port=0 ;;
|
||||
esac
|
||||
if [ "$config_mode" = manual ]; then
|
||||
clash_api_url=
|
||||
[ -n "$clash_listen_addr" ] && clash_api_url="http://$clash_listen_addr"
|
||||
else
|
||||
clash_api_url="http://${listen%:*}:$clash_api_port"
|
||||
[ "$api_enabled" = 1 ] || api_url=""
|
||||
clash_api_url=
|
||||
[ -n "$clash_listen_addr" ] && clash_api_url="http://$clash_listen_addr"
|
||||
active_hash=$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')
|
||||
runtime_hash=$(awk 'NR == 1 { print $2 }' /var/run/honk/readiness 2>/dev/null)
|
||||
[ -n "$runtime_hash" ] && [ "$runtime_hash" = "$active_hash" ] || { [ "$running" = 1 ] && config_pending=1; }
|
||||
# 草稿与活动配置分开标记:草稿存在不代表已生效,也不参与运行态判断。
|
||||
if [ -f /etc/honk/config-draft.dae ] && [ ! -L /etc/honk/config-draft.dae ]; then
|
||||
draft_hash=$(sha256sum /etc/honk/config-draft.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$draft_hash" = "$active_hash" ] || draft_pending=1
|
||||
fi
|
||||
[ -f /etc/honk/manual-config.dae ] && [ ! -L /etc/honk/manual-config.dae ] && legacy_draft=1
|
||||
clash_api_ready=0
|
||||
if [ "$running" = 1 ] && [ "$clash_api_enabled" = 1 ] && [ "$clash_api_port" -gt 0 ] && honk_lifecycle_listener_owned "$managed_pid" "$clash_api_port"; then clash_api_ready=1; fi
|
||||
clash_ui_ready=0
|
||||
[ -s /usr/share/honk/zashboard/index.html ] && clash_ui_ready=1
|
||||
# UCI 里的 secret 只对托管模式有意义:手动模式的密码在 config.dae 里,
|
||||
# 已经由运行标记读取过,这里不能再拿 UCI 覆盖。
|
||||
if [ "$config_mode" != manual ]; then
|
||||
clash_secret_set=0
|
||||
[ -n "$clash_api_secret" ] && clash_secret_set=1
|
||||
fi
|
||||
case "$clash_api_port" in
|
||||
''|*[!0-9]*) clash_api_port=0 ;;
|
||||
esac
|
||||
if [ "$running" = 1 ] && honk_lifecycle_api_owned "$managed_pid"; then api_ready=1; else api_ready=0; fi
|
||||
core_version=$($CORE --version 2>/dev/null | sed -n '1p')
|
||||
[ -n "$core_version" ] || core_version=unknown
|
||||
@@ -155,8 +139,8 @@ status() {
|
||||
if [ "$enabled" != 1 ]; then errors="$errors disabled"; fi
|
||||
if [ "$initialized" != 1 ]; then errors="$errors not_initialized"; fi
|
||||
if [ "$enabled" = 1 ] && [ "$initialized" = 1 ] && [ "$running" != 1 ]; then errors="$errors service_not_running"; fi
|
||||
if [ "$running" = 1 ] && [ "$api_ready" != 1 ] && { [ "$config_mode" != manual ] || [ "$api_enabled" = 1 ]; }; then errors="$errors api_unavailable"; fi
|
||||
if [ "$config_mode" = manual ] && [ "$running" = 1 ] && [ "$core_ready" != 1 ]; then errors="$errors core_not_ready"; fi
|
||||
if [ "$running" = 1 ] && [ "$api_ready" != 1 ] && [ "$api_enabled" = 1 ]; then errors="$errors api_unavailable"; fi
|
||||
if [ "$running" = 1 ] && [ "$core_ready" != 1 ]; then errors="$errors core_not_ready"; fi
|
||||
if [ -d "$HONK_LOCK_DIR" ]; then
|
||||
if lock_owner_invalid || honk_lock_is_stale; then errors="$errors stale_operation_lock maintenance_recovery_required"; else errors="$errors operation_in_progress"; fi
|
||||
fi
|
||||
@@ -167,8 +151,10 @@ status() {
|
||||
json_add_boolean initialized "$initialized"
|
||||
json_add_boolean boot_enabled "$boot_enabled"
|
||||
json_add_boolean running "$running"
|
||||
json_add_string config_mode "$config_mode"
|
||||
json_add_string settings_owner "$settings_owner"
|
||||
json_add_boolean config_pending "$config_pending"
|
||||
json_add_boolean draft_pending "$draft_pending"
|
||||
json_add_boolean legacy_draft "$legacy_draft"
|
||||
json_add_boolean core_ready "$core_ready"
|
||||
json_add_boolean api_enabled "$api_enabled"
|
||||
json_add_boolean api_ready "$api_ready"
|
||||
@@ -194,7 +180,6 @@ status() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
|
||||
service_ready_config() {
|
||||
local enabled initialized
|
||||
config_load honk
|
||||
@@ -331,9 +316,48 @@ logs() {
|
||||
rm -f "$log_file"
|
||||
}
|
||||
|
||||
# 判出厂配置时忽略本机直连区块:启动脚本写入本机地址后逐字比较必然失败。
|
||||
strip_local_direct() {
|
||||
awk -v begin=' # honk:local-direct-begin' -v end=' # honk:local-direct-end' '
|
||||
$0 == begin {
|
||||
if (skip) bad = 1
|
||||
b++
|
||||
skip = 1
|
||||
print
|
||||
next
|
||||
}
|
||||
$0 == end {
|
||||
if (!skip) bad = 1
|
||||
e++
|
||||
skip = 0
|
||||
print
|
||||
next
|
||||
}
|
||||
skip { next }
|
||||
{ print }
|
||||
END { if (bad || skip || b != 1 || e != 1) exit 1 }
|
||||
' "$1"
|
||||
}
|
||||
|
||||
# 标记缺失、重复或不成对时一律不认定为出厂配置,避免误改用户出口。
|
||||
is_factory_config() {
|
||||
local current=$1 factory=/usr/share/honk/default-config.dae a b rc
|
||||
[ -f "$factory" ] && [ ! -L "$factory" ] || return 1
|
||||
a=$(mktemp /tmp/honk-route.XXXXXX) || return 1
|
||||
b=$(mktemp /tmp/honk-route.XXXXXX) || { rm -f "$a"; return 1; }
|
||||
if strip_local_direct "$current" > "$a" && strip_local_direct "$factory" > "$b"; then
|
||||
cmp -s "$a" "$b"
|
||||
rc=$?
|
||||
else
|
||||
rc=1
|
||||
fi
|
||||
rm -f "$a" "$b"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
promote_default_route() {
|
||||
local current=$1 candidate=$2 dns_block
|
||||
if cmp -s "$current" /usr/share/honk/default-config.dae; then
|
||||
if is_factory_config "$current"; then
|
||||
sed 's/^ fallback: direct$/ fallback: proxy/' "$candidate" > "$candidate.route" || return 1
|
||||
elif [ "$(sha256sum "$current" | awk '{print $1}')" = 83d1cc32bfe5b44ecae4b05c2c82c81ce9a36ab29f391d1e1d1d6b4993e17d2b ]; then
|
||||
# The r1 package had only an include and a direct fallback. Preserve the
|
||||
@@ -387,9 +411,18 @@ validate_proxy_nodes() (
|
||||
trap 'validator_cleanup' EXIT
|
||||
trap 'exit 1' HUP INT TERM
|
||||
port=$((20000 + $(hexdump -n 2 -e '1/2 "%u"' /dev/urandom) % 20000))
|
||||
if ! cp -p /etc/honk/*.dae "$temp/" || ! cp "$candidate" "$temp/config.dae"; then
|
||||
# 先带完整文件树校验原始候选(候选名不是 .dae 结尾,复制成 candidate-original.dae)。
|
||||
if ! cp -p /etc/honk/*.dae "$temp/" || ! cp -p "$candidate" "$temp/candidate-original.dae"; then
|
||||
rm -rf "$temp"
|
||||
return 1
|
||||
fi
|
||||
if ! "$CORE" config validate --config "$temp/candidate-original.dae" >/dev/null 2>&1; then
|
||||
rm -rf "$temp"
|
||||
return 2
|
||||
fi
|
||||
# 候选原样作为被测配置,落盘路径与 API 由我们写的 system.dae 提供。
|
||||
cp -p "$candidate" "$temp/config.dae" || { rm -rf "$temp"; return 1; }
|
||||
chmod 0600 "$temp/config.dae" || { rm -rf "$temp"; return 1; }
|
||||
cat > "$temp/system.dae" <<EOF
|
||||
global {
|
||||
wan_interface: auto
|
||||
@@ -409,7 +442,8 @@ experimental {
|
||||
}
|
||||
}
|
||||
EOF
|
||||
DAE_LOCATION_ASSET=/usr/share/v2ray "$CORE" --config "$temp/config.dae" --mock-ebpf > "$temp/core.log" 2>&1 &
|
||||
DAE_LOCATION_ASSET=/usr/share/v2ray "$CORE" --config "$temp/config.dae" --mock-ebpf \
|
||||
--data-dir "$temp" --log-file "$temp/core.log" > "$temp/core.console" 2>&1 &
|
||||
core_pid=$!
|
||||
ready=0
|
||||
result=2
|
||||
@@ -576,7 +610,8 @@ add_subscription() {
|
||||
cfg=/etc/honk/config.dae
|
||||
[ -f "$cfg" ] || { emit_result false config_missing; return 1; }
|
||||
begin_mutation
|
||||
require_managed_mode || return 1
|
||||
# 快捷追加只支持系统托管结构;自带 global/experimental 或 include 其他文件一律拒绝。
|
||||
[ "$(config_owner "$cfg")" = system ] || { emit_result false quick_add_unsupported; return 1; }
|
||||
original_hash=$(sha256sum "$cfg" | awk '{print $1}')
|
||||
|
||||
if grep -qF "$sub_tag:" "$cfg" || grep -qF "'$sub_name':" "$cfg"; then
|
||||
@@ -634,7 +669,8 @@ add_nodes() {
|
||||
cfg=/etc/honk/config.dae
|
||||
[ -f "$cfg" ] || { emit_result false config_missing; return 1; }
|
||||
begin_mutation
|
||||
require_managed_mode || return 1
|
||||
# 快捷追加只支持系统托管结构;自带 global/experimental 或 include 其他文件一律拒绝。
|
||||
[ "$(config_owner "$cfg")" = system ] || { emit_result false quick_add_unsupported; return 1; }
|
||||
original_hash=$(sha256sum "$cfg" | awk '{print $1}')
|
||||
|
||||
node_prefix="node_$(hexdump -n 4 -e '4/1 "%02x"' /dev/urandom)_"
|
||||
@@ -714,7 +750,6 @@ import_dae() {
|
||||
cfg=/etc/honk/config.dae
|
||||
[ -f "$cfg" ] || { emit_result false config_missing; return 1; }
|
||||
begin_mutation
|
||||
require_managed_mode || return 1
|
||||
|
||||
# 提取 node 块(过滤 honk-core 不支持的 ssr/snell 协议)
|
||||
node_block=$(awk '
|
||||
@@ -859,25 +894,35 @@ config_read() {
|
||||
[ "$config_size" -le "$DAE_UPLOAD_LIMIT" ] || { emit_result false config_too_large; return 1; }
|
||||
config_content=$(cat /etc/honk/config.dae; printf '\001') || { emit_result false config_read_failed; return 1; }
|
||||
config_content=${config_content%?}
|
||||
config_load honk
|
||||
config_get config_mode main config_mode managed
|
||||
json_init
|
||||
json_add_boolean ok 1
|
||||
json_add_string message config_loaded
|
||||
json_add_string mode "$config_mode"
|
||||
json_add_string content "$config_content"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# 只读草稿,不触发任何应用。source=legacy 时读旧 manual-config.dae(用户明确选择才调)。
|
||||
config_draft_read() {
|
||||
[ -f /etc/honk/manual-config.dae ] && [ ! -L /etc/honk/manual-config.dae ] || { emit_result false config_draft_missing; return 1; }
|
||||
config_size=$(wc -c < /etc/honk/manual-config.dae)
|
||||
request=$(cat)
|
||||
draft_source=draft
|
||||
if [ -n "$request" ]; then
|
||||
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
|
||||
json_get_var draft_source source
|
||||
fi
|
||||
case "$draft_source" in
|
||||
draft) draft_file=/etc/honk/config-draft.dae ;;
|
||||
legacy) draft_file=/etc/honk/manual-config.dae ;;
|
||||
*) emit_result false invalid_request; return 1 ;;
|
||||
esac
|
||||
[ -f "$draft_file" ] && [ ! -L "$draft_file" ] || { emit_result false config_draft_missing; return 1; }
|
||||
config_size=$(wc -c < "$draft_file")
|
||||
[ "$config_size" -le "$DAE_UPLOAD_LIMIT" ] || { emit_result false config_too_large; return 1; }
|
||||
config_content=$(cat /etc/honk/manual-config.dae; printf '\001') || { emit_result false config_read_failed; return 1; }
|
||||
config_content=$(cat "$draft_file"; printf '\001') || { emit_result false config_read_failed; return 1; }
|
||||
config_content=${config_content%?}
|
||||
json_init
|
||||
json_add_boolean ok 1
|
||||
json_add_string message config_draft_loaded
|
||||
json_add_string source "$draft_source"
|
||||
json_add_string content "$config_content"
|
||||
json_dump
|
||||
}
|
||||
@@ -981,16 +1026,16 @@ config_save() {
|
||||
[ -n "$config_validation" ] && printf '%s\n' "$config_validation" || emit_result false config_validation_unavailable
|
||||
return 0
|
||||
fi
|
||||
draft_path=/etc/honk/manual-config.dae
|
||||
draft_path=/etc/honk/config-draft.dae
|
||||
draft_backup=
|
||||
if [ -e "$draft_path" ] || [ -L "$draft_path" ]; then
|
||||
[ -f "$draft_path" ] && [ ! -L "$draft_path" ] || { rm -f "$config_candidate"; emit_result false config_draft_invalid; return 1; }
|
||||
draft_backup=$(mktemp /etc/honk/.manual-config-backup.XXXXXX) || { rm -f "$config_candidate"; emit_result false config_write_failed; return 1; }
|
||||
draft_backup=$(mktemp /etc/honk/.config-draft-backup.XXXXXX) || { rm -f "$config_candidate"; emit_result false config_write_failed; return 1; }
|
||||
cp -p "$draft_path" "$draft_backup" || { rm -f "$config_candidate" "$draft_backup"; emit_result false config_write_failed; return 1; }
|
||||
fi
|
||||
if ! mv -f "$config_candidate" "$draft_path"; then
|
||||
if [ -n "$draft_backup" ]; then
|
||||
draft_restore=$(mktemp /etc/honk/.manual-config-restore.XXXXXX)
|
||||
draft_restore=$(mktemp /etc/honk/.config-draft-restore.XXXXXX)
|
||||
cp -p "$draft_backup" "$draft_restore" && mv -f "$draft_restore" "$draft_path"
|
||||
rm -f "$draft_restore"
|
||||
fi
|
||||
@@ -1000,16 +1045,15 @@ config_save() {
|
||||
fi
|
||||
chmod 0600 "$draft_path" || { emit_result false config_write_failed; return 1; }
|
||||
rm -f "$draft_backup"
|
||||
rm -f /etc/honk/manual-system.dae
|
||||
emit_result true config_saved_pending_apply
|
||||
}
|
||||
|
||||
config_apply() {
|
||||
begin_mutation
|
||||
draft_path=/etc/honk/manual-config.dae
|
||||
draft_path=/etc/honk/config-draft.dae
|
||||
[ -f "$draft_path" ] && [ ! -L "$draft_path" ] || { emit_result false config_draft_missing; return 1; }
|
||||
activate_dir=$(mktemp -d /etc/honk/.manual-activate.XXXXXX) || { emit_result false config_write_failed; return 1; }
|
||||
activate_file=/etc/honk/.manual-config-${activate_dir##*.manual-activate.}.dae
|
||||
activate_dir=$(mktemp -d /etc/honk/.config-activate.XXXXXX) || { emit_result false config_write_failed; return 1; }
|
||||
activate_file=/etc/honk/.config-${activate_dir##*.config-activate.}.dae
|
||||
(set -C; cp "$draft_path" "$activate_file") || { rm -rf "$activate_dir"; emit_result false config_write_failed; return 1; }
|
||||
rmdir "$activate_dir" || { rm -rf "$activate_dir"; rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
chmod 0600 "$activate_file" || { rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
@@ -1021,7 +1065,6 @@ config_apply() {
|
||||
fi
|
||||
candidate_hash=$(sha256sum "$activate_file" | awk '{print $1}')
|
||||
old_hash=$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')
|
||||
old_mode=$(uci -q get honk.main.config_mode || echo managed)
|
||||
old_running=0
|
||||
old_pid=$(honk_lifecycle_managed_pid 2>/dev/null)
|
||||
if [ -n "$old_pid" ]; then
|
||||
@@ -1031,42 +1074,29 @@ config_apply() {
|
||||
emit_result false unmanaged_core_running
|
||||
return 1
|
||||
fi
|
||||
backup_dir=$(mktemp -d /etc/honk/.manual-apply-backup.XXXXXX) || { rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
backup_dir=$(mktemp -d /etc/honk/.config-apply-backup.XXXXXX) || { rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
chmod 0700 "$backup_dir" || { rm -rf "$backup_dir"; rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
cp -p /etc/honk/config.dae "$backup_dir/config.dae" && cp -p /etc/config/honk "$backup_dir/honk.uci" || {
|
||||
rm -rf "$backup_dir" "$activate_file"
|
||||
emit_result false config_write_failed
|
||||
return 1
|
||||
}
|
||||
managed_snapshot=/etc/honk/managed-config.dae
|
||||
if [ "$old_mode" = managed ] && { [ -e "$managed_snapshot" ] || [ -L "$managed_snapshot" ]; }; then
|
||||
[ -f "$managed_snapshot" ] && [ ! -L "$managed_snapshot" ] || { rm -rf "$backup_dir" "$activate_file"; emit_result false managed_snapshot_invalid; return 1; }
|
||||
fi
|
||||
if [ "$old_mode" = managed ]; then
|
||||
managed_snapshot_tmp=$(mktemp /etc/honk/.managed-config.XXXXXX) || { rm -rf "$backup_dir" "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
cp -p "$backup_dir/config.dae" "$managed_snapshot_tmp" && chmod 0600 "$managed_snapshot_tmp" && mv -f "$managed_snapshot_tmp" "$managed_snapshot" || {
|
||||
rm -f "$managed_snapshot_tmp"
|
||||
rm -rf "$backup_dir" "$activate_file"
|
||||
emit_result false config_write_failed
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
if [ "$old_running" = 1 ] && ! honk_lifecycle_stop 30; then
|
||||
rm -f "$activate_file"
|
||||
emit_result false service_stop_unconfirmed_snapshot_retained
|
||||
emit_result false service_stop_unconfirmed
|
||||
return 1
|
||||
fi
|
||||
if [ "$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')" != "$old_hash" ]; then
|
||||
if [ "$old_running" = 1 ]; then honk_lifecycle_start 20 || { emit_result false config_changed_concurrently_restart_failed_snapshot_retained; return 1; }; fi
|
||||
if [ "$old_running" = 1 ]; then honk_lifecycle_start 20 || { emit_result false config_changed_concurrently_restart_failed; return 1; }; fi
|
||||
rm -rf "$backup_dir" "$activate_file"
|
||||
emit_result false config_changed_concurrently
|
||||
return 1
|
||||
fi
|
||||
if ! mv -f "$activate_file" /etc/honk/config.dae; then
|
||||
emit_result false config_write_failed_snapshot_retained
|
||||
emit_result false config_write_failed
|
||||
return 1
|
||||
fi
|
||||
if ! uci set honk.main.config_mode=manual || ! uci set honk.main.enabled=1 || ! uci set honk.main.initialized=1 || ! uci commit honk || ! chmod 0600 /etc/config/honk; then
|
||||
if ! uci set honk.main.enabled=1 || ! uci set honk.main.initialized=1 || ! uci commit honk || ! chmod 0600 /etc/config/honk; then
|
||||
apply_failed=1
|
||||
else
|
||||
apply_failed=0
|
||||
@@ -1079,7 +1109,7 @@ config_apply() {
|
||||
[ "$apply_failed" = 1 ] || honk_lifecycle_stop 30 >/dev/null 2>&1 || :
|
||||
rollback_ok=1
|
||||
if [ "$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')" = "$candidate_hash" ]; then
|
||||
rollback_tmp=$(mktemp /etc/honk/.manual-rollback.XXXXXX) || rollback_ok=0
|
||||
rollback_tmp=$(mktemp /etc/honk/.config-rollback.XXXXXX) || rollback_ok=0
|
||||
[ "$rollback_ok" = 0 ] || { cp -p "$backup_dir/config.dae" "$rollback_tmp" && mv -f "$rollback_tmp" /etc/honk/config.dae || rollback_ok=0; }
|
||||
else
|
||||
rollback_ok=0
|
||||
@@ -1094,62 +1124,15 @@ config_apply() {
|
||||
rm -rf "$backup_dir"
|
||||
emit_result false config_apply_failed_rolled_back
|
||||
else
|
||||
emit_result false config_apply_rollback_failed_snapshot_retained
|
||||
emit_result false config_apply_rollback_failed
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
config_use_managed() {
|
||||
begin_mutation
|
||||
current_mode=$(uci -q get honk.main.config_mode 2>/dev/null)
|
||||
[ "$current_mode" = manual ] || { emit_result false manual_mode_required; return 1; }
|
||||
managed_snapshot=/etc/honk/managed-config.dae
|
||||
[ -f "$managed_snapshot" ] && [ ! -L "$managed_snapshot" ] || { emit_result false managed_snapshot_missing; return 1; }
|
||||
activate_dir=$(mktemp -d /etc/honk/.managed-activate.XXXXXX) || { emit_result false config_write_failed; return 1; }
|
||||
activate_file=/etc/honk/.managed-config-${activate_dir##*.managed-activate.}.dae
|
||||
(set -C; cp "$managed_snapshot" "$activate_file") || { rm -rf "$activate_dir"; emit_result false config_write_failed; return 1; }
|
||||
rmdir "$activate_dir" || { rm -rf "$activate_dir"; rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
chmod 0600 "$activate_file" || { rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
config_candidate=$activate_file
|
||||
if ! validate_config_candidate; then rm -f "$activate_file"; [ -n "$config_validation" ] && printf '%s\n' "$config_validation" || emit_result false config_validation_unavailable; return 1; fi
|
||||
candidate_hash=$(sha256sum "$activate_file" | awk '{print $1}')
|
||||
old_hash=$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')
|
||||
old_running=0
|
||||
old_pid=$(honk_lifecycle_managed_pid 2>/dev/null)
|
||||
if [ -n "$old_pid" ]; then old_running=1; elif pidof honk-core >/dev/null 2>&1; then rm -f "$activate_file"; emit_result false unmanaged_core_running; return 1; fi
|
||||
backup_dir=$(mktemp -d /etc/honk/.managed-switch-backup.XXXXXX) || { rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
chmod 0700 "$backup_dir" || { rm -rf "$backup_dir" "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
cp -p /etc/honk/config.dae "$backup_dir/config.dae" && cp -p /etc/config/honk "$backup_dir/honk.uci" || { rm -rf "$backup_dir" "$activate_file"; emit_result false config_write_failed; return 1; }
|
||||
if [ "$old_running" = 1 ] && ! honk_lifecycle_stop 30; then rm -f "$activate_file"; emit_result false service_stop_unconfirmed_snapshot_retained; return 1; fi
|
||||
if [ "$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')" != "$old_hash" ]; then
|
||||
if [ "$old_running" = 1 ]; then honk_lifecycle_start 20 || { emit_result false config_changed_concurrently_restart_failed_snapshot_retained; return 1; }; fi
|
||||
rm -rf "$backup_dir" "$activate_file"
|
||||
emit_result false config_changed_concurrently
|
||||
return 1
|
||||
fi
|
||||
if ! mv -f "$activate_file" /etc/honk/config.dae || ! uci set honk.main.config_mode=managed || ! uci commit honk || ! chmod 0600 /etc/config/honk; then
|
||||
switch_failed=1
|
||||
else
|
||||
switch_failed=0
|
||||
fi
|
||||
if [ "$switch_failed" = 0 ] && { [ "$old_running" = 0 ] || honk_lifecycle_start 45; }; then rm -rf "$backup_dir"; emit_result true managed_mode_restored; return 0; fi
|
||||
[ "$switch_failed" = 1 ] || honk_lifecycle_stop 30 >/dev/null 2>&1 || :
|
||||
rollback_ok=1
|
||||
rollback_tmp=$(mktemp /etc/honk/.managed-rollback.XXXXXX) || rollback_ok=0
|
||||
[ "$rollback_ok" = 0 ] || { cp -p "$backup_dir/config.dae" "$rollback_tmp" && mv -f "$rollback_tmp" /etc/honk/config.dae || rollback_ok=0; }
|
||||
uci_tmp=$(mktemp /etc/config/.honk-rollback.XXXXXX) || rollback_ok=0
|
||||
[ "$rollback_ok" = 0 ] || { cp -p "$backup_dir/honk.uci" "$uci_tmp" && mv -f "$uci_tmp" /etc/config/honk || rollback_ok=0; }
|
||||
[ "$rollback_ok" = 0 ] || [ "$(sha256sum /etc/config/honk 2>/dev/null | awk '{print $1}')" = "$(sha256sum "$backup_dir/honk.uci" | awk '{print $1}')" ] || rollback_ok=0
|
||||
uci revert honk 2>/dev/null || :
|
||||
if [ "$rollback_ok" = 1 ] && [ "$old_running" = 1 ]; then honk_lifecycle_start 10 || rollback_ok=0; fi
|
||||
rm -f "$rollback_tmp" "$uci_tmp"
|
||||
if [ "$rollback_ok" = 1 ]; then rm -rf "$backup_dir"; emit_result false managed_switch_failed_rolled_back; else emit_result false managed_switch_rollback_failed_snapshot_retained; fi
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"check":{},"initialize":{"username":"String","password":"String"},"start":{},"stop":{},"restart":{},"settings":{"lan_network":"String","listen_port":"Integer","boot_enabled":"Boolean","clash_api_enabled":"Boolean","clash_api_port":"Integer","clash_api_secret":"String"},"add_subscription":{"name":"String","url":"String"},"probe_subscription":{"url":"String"},"reset_admin":{},"add_nodes":{"links":"String"},"import_dae":{},"config_read":{},"config_draft_read":{},"lan_candidates":{},"config_validate":{"content":"String"},"config_save":{"content":"String"},"config_apply":{},"config_use_managed":{},"repair":{},"backup":{},"restore":{},"restore_prepare":{},"reset":{},"job_status":{"job_id":"String"},"logs":{"source":"String","level":"String","limit":"Integer"},"update_check":{},"update_apply":{}}'
|
||||
echo '{"status":{},"check":{},"initialize":{"username":"String","password":"String"},"start":{},"stop":{},"restart":{},"settings":{"lan_network":"String","listen_port":"Integer","boot_enabled":"Boolean","clash_api_enabled":"Boolean","clash_api_port":"Integer","clash_api_secret":"String"},"add_subscription":{"name":"String","url":"String"},"probe_subscription":{"url":"String"},"reset_admin":{},"add_nodes":{"links":"String"},"import_dae":{},"config_read":{},"config_draft_read":{},"lan_candidates":{},"config_validate":{"content":"String"},"config_save":{"content":"String"},"config_apply":{},"repair":{},"backup":{},"restore":{},"restore_prepare":{},"reset":{},"job_status":{"job_id":"String"},"logs":{"source":"String","level":"String","limit":"Integer"},"update_check":{},"update_apply":{}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
@@ -1198,9 +1181,6 @@ list)
|
||||
config_apply)
|
||||
config_apply
|
||||
;;
|
||||
config_use_managed)
|
||||
config_use_managed
|
||||
;;
|
||||
repair)
|
||||
exec "$CONTROL" repair
|
||||
;;
|
||||
|
||||
@@ -26,6 +26,34 @@ esac
|
||||
LOCK="/tmp/luci-app-honk.pkg-${PKG}.lock"
|
||||
LOG="/tmp/luci-app-honk.pkg-${PKG}.log"
|
||||
|
||||
. /usr/share/honk/lifecycle.sh 2>/dev/null || :
|
||||
|
||||
# 运行状态与恢复交给 lifecycle:它已经处理 dae0/BPF 排空、崩溃重启和就绪判定。
|
||||
service_running() {
|
||||
honk_lifecycle_managed_pid >/dev/null 2>&1
|
||||
}
|
||||
|
||||
restore_service_state() {
|
||||
if [ "$was_running" = 1 ]; then
|
||||
echo "--- 升级前为运行状态,重启加载新内核 ---"
|
||||
if honk_lifecycle_restart 30 >/dev/null 2>&1; then
|
||||
echo "result: 服务已恢复运行"
|
||||
return 0
|
||||
fi
|
||||
echo "result: 服务重启失败"
|
||||
return 1
|
||||
fi
|
||||
if service_running; then
|
||||
echo "--- 升级前为停止状态,停止服务 ---"
|
||||
honk_lifecycle_stop 30 >/dev/null 2>&1 || {
|
||||
echo "result: 服务停止或资源排空失败"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
echo "result: 服务保持停止"
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -f "$LOCK" ]; then
|
||||
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
|
||||
age=$(( $(date +%s) - mtime ))
|
||||
@@ -46,6 +74,9 @@ fi
|
||||
trap 'rm -f "$LOCK"; [ "${0#/tmp/.honk-upd-}" != "$0" ] && rm -f "$0"' EXIT INT TERM
|
||||
|
||||
echo "$(date '+%F %T') begin upgrade: $PKG"
|
||||
was_running=0
|
||||
service_running && was_running=1
|
||||
echo "升级前服务状态: $([ "$was_running" = 1 ] && echo 运行中 || echo 已停止)"
|
||||
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
(
|
||||
@@ -85,11 +116,20 @@ fi
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
|
||||
|
||||
if [ "$PKG" = "luci-app-honk" ] && [ "$rc" = "0" ]; then
|
||||
echo "reloading rpcd to pick up new ACL"
|
||||
service_rc=0
|
||||
if [ "$rc" = "0" ]; then
|
||||
echo "reloading rpcd to pick up new scripts"
|
||||
/etc/init.d/rpcd reload 2>&1
|
||||
restore_service_state || service_rc=$?
|
||||
fi
|
||||
|
||||
# 前端轮询见到 ✓/✗ 即停止,结论只在这里输出一次。
|
||||
if [ "$rc" != "0" ]; then
|
||||
echo "$(date '+%F %T') ✗ 升级失败 (rc=$rc)"
|
||||
elif [ "$service_rc" != "0" ]; then
|
||||
echo "$(date '+%F %T') ✗ 软件包升级成功,但服务启动失败,请查看日志后手动启动"
|
||||
else
|
||||
echo "$(date '+%F %T') ✓ 完成"
|
||||
fi
|
||||
) </dev/null >/dev/null 2>&1 &
|
||||
|
||||
|
||||
@@ -81,8 +81,7 @@
|
||||
"update_apply",
|
||||
"config_read",
|
||||
"config_draft_read",
|
||||
"config_apply",
|
||||
"config_use_managed"
|
||||
"config_apply"
|
||||
]
|
||||
},
|
||||
"file": {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-smartdns
|
||||
PKG_VERSION:=1.2026.48.2
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=1.2026.48.3
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
PKG_MAINTAINER:=Nick Peng <pymumu@gmail.com>
|
||||
|
||||
+1238
-66
File diff suppressed because it is too large.
Load diff
@@ -161,3 +161,8 @@ while IFS= read -r f; do
|
||||
done < "$LIST"
|
||||
rm -f "$MAP.ord"
|
||||
echo "mangle-tokens: seam renamed in $touched file(s)"
|
||||
# A name the map missed: one built at run time ('--fs-' + k), which the sheet no longer reads after
|
||||
# the rename (issue #89), or one no rule declares. Prose may still say `--fs-*`.
|
||||
if left=$(tr '\n' '\0' < "$LIST" | xargs -0 grep -HnE -- '--fs-([A-Za-z0-9_$"'"'"'`]|$)'); then
|
||||
printf 'mangle-tokens: an --fs- name survived the rename:\n%s\n' "$left" >&2; exit 1
|
||||
fi
|
||||
@@ -45,8 +45,8 @@
|
||||
--fs-glass-bar-panel: color-mix(in srgb, var(--fs-panel) var(--fs-bar-opacity), transparent);
|
||||
--fs-glass-bar-inset: var(--fs-glass-inset-on);
|
||||
}
|
||||
/* Flat: everything keeps its own colour at the same opacity, layers compound, and only what the
|
||||
* page scrolls under is blurred */
|
||||
/* Flat: everything keeps its own colour at the same opacity, layers compound, and nothing is
|
||||
* blurred but the sticky table cells; the menu follows the cards' material, Flat bar included */
|
||||
:root[data-wallpaper][data-material="flat"]:is([data-glass], [data-glass-bar]) {
|
||||
--fs-glass-inset-on: var(--fs-glass-inset);
|
||||
--fs-glass-track-on: var(--fs-glass-track);
|
||||
@@ -190,18 +190,24 @@
|
||||
:root[data-wallpaper][data-glass-bar]:not([data-material="flat"]) .fs-sidebar ul.nav ul {
|
||||
backdrop-filter: blur(var(--fs-glass-blur)) saturate(var(--fs-glass-sat));
|
||||
}
|
||||
/* what covers the page's text is blurred in Flat too, plainly and at the base radius, as Flat has
|
||||
* no Blur slider: the bar the page scrolls under, and every popup */
|
||||
:root[data-wallpaper][data-glass-bar][data-material="flat"]:is([data-layout="top"], [data-narrow]) .fs-sidebar::before,
|
||||
:root[data-wallpaper][data-glass-bar][data-material="flat"] .fs-sidebar ul.nav ul {
|
||||
backdrop-filter: blur(var(--fs-glass-blur-base));
|
||||
}
|
||||
/* the rail's name bubbles are labels over the page, not menus: solid */
|
||||
:root[data-wallpaper][data-glass-bar] .fs-sidebar ul.nav > li > a::after {
|
||||
--fs-panel: var(--fs-glass-o-panel);
|
||||
--fs-border: var(--fs-glass-o-border);
|
||||
--fs-hairline: 1px solid var(--fs-glass-o-border);
|
||||
}
|
||||
/* Acrylic's grain, lit edge and shadow on the menu as on the cards: without them the bar read as
|
||||
* Frosted with a stronger blur. The bar paints on its layer, the vertical sidebar on itself. */
|
||||
:root[data-wallpaper][data-glass-bar][data-material="acrylic"]:is([data-layout="top"], [data-narrow]) .fs-sidebar::before,
|
||||
:root[data-wallpaper][data-glass-bar][data-material="acrylic"][data-sb] .fs-sidebar {
|
||||
background-image: var(--fs-glass-noise), linear-gradient(160deg, color-mix(in srgb, #fff 7%, transparent), transparent 45%);
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 10%, transparent), 0 14px 32px -18px rgb(0 0 0 / .55);
|
||||
}
|
||||
:root[data-wallpaper][data-glass-bar][data-material="acrylic"]:not([data-darkmode="true"]):is([data-layout="top"], [data-narrow]) .fs-sidebar::before,
|
||||
:root[data-wallpaper][data-glass-bar][data-material="acrylic"]:not([data-darkmode="true"])[data-sb] .fs-sidebar {
|
||||
background-image: var(--fs-glass-noise), linear-gradient(160deg, color-mix(in srgb, #fff 45%, transparent), transparent 50%);
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 70%, transparent), 0 14px 32px -20px rgb(0 0 0 / .3);
|
||||
}
|
||||
@media (min-width: 521px) {
|
||||
/* the rail is static: the layer needs it as its containing block, and no z-index, so the
|
||||
* flyouts still rise over the page on their own (20-shell.css, issue #7) */
|
||||
|
||||
@@ -334,9 +334,10 @@
|
||||
flex-wrap: wrap; gap: var(--fs-space-2); padding-top: var(--fs-space-2);
|
||||
}
|
||||
/* LuCI groups buttons in ONE inner <div>, so this flex-wrap never fires and .fs-main clipped
|
||||
* the tail button. Copy in theme/65-dropdown.css. */
|
||||
* the tail button. No grow: a plain button sized to its label, like the ComboButton below,
|
||||
* where a stretched one stood a card wide beside it. Copy in theme/65-dropdown.css. */
|
||||
.table.fs-stacked .td.cbi-section-actions > :not(.cbi-dropdown) {
|
||||
flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2);
|
||||
flex: 0 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2);
|
||||
}
|
||||
/* width: 0 — the label's nowrap min-content (288px for a Russian item at 360px) sizes the table-row-group
|
||||
* past the card (td right 442 vs 315); basis `content` still sizes the item to its label (it was 266px, a whole card, with `1 1 auto`). Copy in theme/65-dropdown.css. */
|
||||
|
||||
@@ -357,9 +357,10 @@
|
||||
}
|
||||
.table.cbi-section-table .td.cbi-section-actions::before { content: none; }
|
||||
/* LuCI groups the row's buttons in ONE inner div, so the td's own wrap never fires — the
|
||||
* div is a single flex item. Copy in theme/30-tables.css; same trap on Startup. */
|
||||
* div is a single flex item. No grow: sized to its label like the ComboButton below.
|
||||
* Copy in theme/30-tables.css; same trap on Startup. */
|
||||
.table.cbi-section-table .td.cbi-section-actions > :not(.cbi-dropdown) {
|
||||
flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2);
|
||||
flex: 0 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2);
|
||||
}
|
||||
/* width: 0 — the label's nowrap min-content (288px for a Russian item at 360px) sizes the table-row-group
|
||||
* past the card (td right 442 vs 315); basis `content` still sizes the item to its label (it was 266px, a whole card, with `1 1 auto`). Copy in theme/30-tables.css. */
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
@layer theme {
|
||||
/* honoured globally, every transition here is decoration. This file is the one place for flags against the theme's own rules,
|
||||
* and there are two: this block (a layer cannot reach base's own transitions) and the forced-colors focus outline below
|
||||
* .01ms not 0s: a zero duration never fires transitionend, LuCI's teardown waits for one. */
|
||||
* and there are two: this block (a layer cannot reach base's own transitions) and the forced-colors focus outline below.
|
||||
* Transitions 0s: a nonzero duration on every element animates `transition-property: all`, the initial value, so WebKit
|
||||
* stepped an inherited font-size down the tree for ~200ms and a floor measured mid-way kept the old height. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: .01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: .01ms !important;
|
||||
transition-duration: 0s !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
/* .01ms, not 0s: a zero duration never fires transitionend, and ui.Dropdown focuses the open list on it */
|
||||
.cbi-dropdown[open] > ul.dropdown { transition-duration: .01ms !important; }
|
||||
}
|
||||
|
||||
/* re-states the tokens so every rule reading them follows — docs/css.md "A11y: prefers-contrast token shifts, and the numbers behind them" has why each number */
|
||||
|
||||
@@ -263,11 +263,13 @@
|
||||
const mq = (q) => window.matchMedia(q).matches;
|
||||
const solid = mq('(prefers-contrast: more)') || mq('(forced-colors: active)');
|
||||
const rt = mq('(prefers-reduced-transparency: reduce)');
|
||||
const glass = (k, a) => {
|
||||
if (num(k, 0, 100, 100, '--fs-' + k, '%') && !solid && !(rt && lsGet(k) === null)) attr(a, '');
|
||||
/* `p` written out, never built from `k`: mangle-tokens.sh renames only whole literals, so a
|
||||
built name set a property the sheet no longer reads (issue #89) */
|
||||
const glass = (k, p, a) => {
|
||||
if (num(k, 0, 100, 100, p, '%') && !solid && !(rt && lsGet(k) === null)) attr(a, '');
|
||||
};
|
||||
glass('card-opacity', 'glass');
|
||||
glass('bar-opacity', 'glass-bar');
|
||||
glass('card-opacity', '--fs-card-opacity', 'glass');
|
||||
glass('bar-opacity', '--fs-bar-opacity', 'glass-bar');
|
||||
list('material', [ 'flat', 'acrylic' ]);
|
||||
/* Frosted's and Acrylic's blur, same shape as the tint strength */
|
||||
num('blur', 0, 200, 100, '--fs-blur');
|
||||
|
||||
+12
-12
@@ -5,9 +5,9 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=upx-static
|
||||
PKG_VERSION:=5.2.1
|
||||
PKG_VERSION:=5.3.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_SOURCE_VERSION:=v5.2.1
|
||||
PKG_SOURCE_VERSION:=v5.3.0
|
||||
|
||||
# Map OpenWrt $(ARCH) to UPX pre-built binary arch name.
|
||||
# NOTE: OpenWrt uses "aarch64", UPX tarball uses "arm64".
|
||||
@@ -39,25 +39,25 @@ PKG_SOURCE_URL:=https://github.com/upx/upx/releases/download/$(PKG_SOURCE_VERSIO
|
||||
|
||||
# AUTO-GENERATED HASHES BEGIN
|
||||
ifeq ($(UPX_ARCH),amd64)
|
||||
PKG_HASH:=402162aad30af47e60dbd767fb2e64ca394ace9727ba1f40283641f1d1b91657
|
||||
PKG_HASH:=79cea8a50b6fa102d10c7b79a419cd77dead3cc2d78661de2dc5e5815dc505c1
|
||||
else ifeq ($(UPX_ARCH),i386)
|
||||
PKG_HASH:=44b505d881337ef17ad03f03fd80f2ecdebc3a36077e1dd260ae1b359c713ba5
|
||||
PKG_HASH:=d5ff15dfd7649d08970ffa86c9bae656b9fe3831285dd62db0ba7eabc6a3eae9
|
||||
else ifeq ($(UPX_ARCH),arm64)
|
||||
PKG_HASH:=a72d112c5970a904a31da0b9c84f919bc16b9a311787c12245508544a78c7d36
|
||||
PKG_HASH:=2a4afa860ac7aa6968f88df7841823746e5d00ba56affdf9342c2f5254fb776f
|
||||
else ifeq ($(UPX_ARCH),arm)
|
||||
PKG_HASH:=d74c0d8bfdd7aad9114470efaa14fa889158f3dbfbc7128ba76c403aee545ef8
|
||||
PKG_HASH:=541c8fc5e0ab43ae57273374215e6efe023aa43c2d5cf1e52a316a94a367bee5
|
||||
else ifeq ($(UPX_ARCH),armeb)
|
||||
PKG_HASH:=31ac41b612c4c1166dc7a7c194e2ad8fadcf9fdf25aa4bd3c14799823b9e5f9e
|
||||
PKG_HASH:=770e62b1a60b7fd281c34556e321566827f57a5eb77cd4976280b92df565b6f0
|
||||
else ifeq ($(UPX_ARCH),mipsel)
|
||||
PKG_HASH:=84fc6c0c33ed668c099d4f82d72ac7fd4070b19797ebb990f999278267b94e0d
|
||||
PKG_HASH:=a4e6dc515aede12aeec7e8169a5f4c96da41cc58b5c834d8b6b7bddd015bb59d
|
||||
else ifeq ($(UPX_ARCH),mips)
|
||||
PKG_HASH:=d2cd981ff725a31f9f596f80fcc5c1682e648a59231e220a3967c661c6d1fafb
|
||||
PKG_HASH:=84c3203cf70d17ced90b777bf843e412ebc7a586cb4ee1e4c9a4801227131bc6
|
||||
else ifeq ($(UPX_ARCH),powerpc)
|
||||
PKG_HASH:=5f89a57f6e76c5b16016330ee5a2ca87f9bfbdcca5420bc042a1fde1798818ed
|
||||
PKG_HASH:=2996e46af638fc20aa019f6c950ac91a829af0dae33fbfeca62012a310b995bb
|
||||
else ifeq ($(UPX_ARCH),powerpc64le)
|
||||
PKG_HASH:=e914046633567c3255a6440bae4d6c13d9f90d458445b47b8474b048ca4072b7
|
||||
PKG_HASH:=2102aa6c2e9fcdb147b27404bf87fc8f545327982ec9685efa5418be55be3480
|
||||
else ifeq ($(UPX_ARCH),riscv64)
|
||||
PKG_HASH:=a8663b4cb6523104a8a807362d0da6f5d6d55f6083152f78fa27fa9c05443c17
|
||||
PKG_HASH:=5a0663f369078cb069ac50252c9c8ca65a146307b7e6078c6f54f3bd146441ca
|
||||
else
|
||||
$(error Unsupported UPX_ARCH '$(UPX_ARCH)' for upx-static)
|
||||
endif
|
||||
|
||||
+3
-3
@@ -5,12 +5,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=v2rayA
|
||||
PKG_VERSION:=2.5.10
|
||||
PKG_VERSION:=2.5.13
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/v2rayA/v2rayA/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=046ff46ca35b102334c0a74c303867d1724d628ca45c258816b1bcaa9dfc6971
|
||||
PKG_HASH:=20f03a9143231e2f04b74cdcb7125c6a44b450a4a28fac92b3e64c13916ec4d9
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/service
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
@@ -60,7 +60,7 @@ define Download/v2raya-web
|
||||
URL:=https://github.com/v2rayA/v2rayA/releases/download/v$(PKG_VERSION)/
|
||||
URL_FILE:=web.tar.gz
|
||||
FILE:=$(WEB_FILE)
|
||||
HASH:=c25071a27f7b98eebb1a80d44b8d6a65d7533cf4cdff475ec86343d7deca5c86
|
||||
HASH:=76774f195ee74604fbc43a48fe74e5f3363e81ec68af02d1c26feeeacc0f63ae
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
|
||||
Reference in new issue
Block a user