Add FastLink_Lite custom rules enhancement
- Add custom rules editor to LUCI web interface - Support 32 rule types (DOMAIN, IP-CIDR, GEOSITE, etc.) - Rule CRUD, sorting, batch import, templates - netflow-rules script for mihomo config injection - Standalone IPK repackaging support
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Large binary files
|
||||||
|
*.ipk
|
||||||
|
*.tar.gz
|
||||||
|
*.dat
|
||||||
|
*.mmdb
|
||||||
|
*.metadb
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Temp / build dirs
|
||||||
|
_tmp_extract/
|
||||||
|
_clean/
|
||||||
|
_repack_verify/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# External projects
|
||||||
|
clash-verge-rev-dev/
|
||||||
|
zashboard-main/
|
||||||
|
|
||||||
|
# Personal files
|
||||||
|
*.backup.json
|
||||||
|
reorganize*.ps1
|
||||||
|
reorganize*.py
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
本项目为**个人电脑助手**,协助完成软件编译构建、软件安装配置、系统故障排查、环境调试等日常开发运维任务。
|
||||||
|
|
||||||
|
## 运行环境
|
||||||
|
|
||||||
|
- **操作系统**:Windows(PowerShell 7+)
|
||||||
|
- **包管理**:scoop / winget / choco 视系统已有工具选用
|
||||||
|
- **Shell**:pwsh(PowerShell 7+),避免 cmd / bash 语法
|
||||||
|
|
||||||
|
## 网络代理
|
||||||
|
|
||||||
|
遇到 GitHub、npm、cargo、pip、curl 等网络连接问题(如超时、443 错误、resolve 失败)时,通过以下代理访问:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://10.10.10.2:7890
|
||||||
|
```
|
||||||
|
|
||||||
|
使用方式示例:
|
||||||
|
- Git: `git config --global http.proxy http://10.10.10.2:7890`
|
||||||
|
- 环境变量: `$env:HTTP_PROXY='http://10.10.10.2:7890'; $env:HTTPS_PROXY='http://10.10.10.2:7890'`
|
||||||
|
- curl/wget: `--proxy http://10.10.10.2:7890`
|
||||||
|
- npm: `npm config set proxy http://10.10.10.2:7890`
|
||||||
|
|
||||||
|
无需代理时记得取消设置。
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
1. **安全优先**:不执行危险命令(rm -rf 等)不确认用户意图之前,下载/安装软件前说明来源
|
||||||
|
2. **解释操作**:运行非琐碎命令时,简要说明该命令做什么
|
||||||
|
3. **诊断先行**:排查问题先收集信息(日志、配置、版本),再给方案
|
||||||
|
4. **最小改动**:修改文件优先用编辑工具而非整体重写
|
||||||
|
5. **遇到权限问题**:提示用户使用管理员权限运行,或提供提权命令
|
||||||
|
6. **中文优先**:思考过程与回复均使用中文,技术术语可保留英文原名
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Build LuCI template version from working file."""
|
||||||
|
with open(r'E:\AI Project\WangAgent\_tmp_extract\router_live.htm', 'r', encoding='utf-8') as f:
|
||||||
|
c = f.read()
|
||||||
|
|
||||||
|
# Remove standalone HTML wrapper (everything before <style> and after </script>)
|
||||||
|
# Current file structure: <!DOCTYPE html>\n<html...><head...><style>...</style>...<script>...</script></body></html>
|
||||||
|
# LuCI template needs: <%+header%>\n<style>...</style>\n...\n<script>...</script>\n<%+footer%>
|
||||||
|
|
||||||
|
# Find the actual content
|
||||||
|
style_start = c.find('<style>')
|
||||||
|
script_end = c.find('</script>') + 9
|
||||||
|
|
||||||
|
# Remove the outer HTML wrapper
|
||||||
|
content = c[style_start:script_end]
|
||||||
|
|
||||||
|
# Add LuCI template tags
|
||||||
|
content = '<%+header%>\n' + content + '\n<%+footer%>'
|
||||||
|
|
||||||
|
# Replace API URLs
|
||||||
|
old_api = "var apiUrl = '<%=luci.dispatcher.build_url(\"admin\", \"services\", \"netflow\", \"api\")%>';"
|
||||||
|
new_api = "var apiUrl = '/cgi-bin/luci/admin/services/netflow/api';"
|
||||||
|
if old_api not in content:
|
||||||
|
# It was already hardcoded, update to LuCI format
|
||||||
|
content = content.replace(
|
||||||
|
"var apiUrl = '/cgi-bin/luci/admin/services/netflow/api';",
|
||||||
|
"var apiUrl = '<%=luci.dispatcher.build_url(\"admin\", \"services\", \"netflow\", \"api\")%>';"
|
||||||
|
)
|
||||||
|
|
||||||
|
old_upload = "xhr.open('POST', '<%=luci.dispatcher.build_url(\"admin\",\"services\",\"netflow\",\"upload_core\")%>', true);"
|
||||||
|
new_upload = "xhr.open('POST', '/cgi-bin/luci/admin/services/netflow/upload_core', true);"
|
||||||
|
# The upload URL is usually hardcoded now, let's use LuCI format instead
|
||||||
|
content = content.replace(new_upload, old_upload)
|
||||||
|
|
||||||
|
# Clean up old tab navigation remnants
|
||||||
|
import re
|
||||||
|
content = re.sub(r'/\* Tab Navigation \*/\s*\nfunction nfSwitchTab\(btn\)\{.*?\n\}', '', content, flags=re.DOTALL)
|
||||||
|
content = re.sub(r'\s*<!-- Tab Navigation -->\s*\n\s*<div id="nf-tab-bar">.*?</div>\s*\n', '', content, flags=re.DOTALL)
|
||||||
|
content = re.sub(r'\s*<!-- Tab content wrappers -->\s*\n\s*(<div class="nf-tab-full-card".*?</div>\s*\n)+', '', content, flags=re.DOTALL)
|
||||||
|
|
||||||
|
# Ensure LF
|
||||||
|
content = content.replace('\r\n', '\n').replace('\r', '\n')
|
||||||
|
|
||||||
|
out = r'E:\AI Project\WangAgent\_tmp_extract\router_main_lf.htm'
|
||||||
|
with open(out, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(f'Template: {len(content)} bytes, LF: {chr(13)+chr(10) not in content[:300]}')
|
||||||
|
print(f'Has <%+header%>: {"<%+header%>" in content}')
|
||||||
|
print(f'Has <%+footer%>: {"<%+footer%>" in content}')
|
||||||
|
print(f'Has standalone DOCTYPE: {"<!DOCTYPE" in content}')
|
||||||
|
print(f'nf-ipv6-proxy-toggle: {content.count("nf-ipv6-proxy-toggle")}')
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import paramiko, os, time
|
||||||
|
time.sleep(3)
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.get_host_keys().clear()
|
||||||
|
c.connect('10.10.10.3', 22, 'root', 'zxc9607.', timeout=30, allow_agent=False, look_for_keys=False)
|
||||||
|
print('Connected')
|
||||||
|
|
||||||
|
with c.open_sftp() as s:
|
||||||
|
s.put(r'E:\AI Project\WangAgent\luci\luci-app-flclient-v2.ipk', '/tmp/v2.ipk')
|
||||||
|
|
||||||
|
cmds = (
|
||||||
|
'opkg remove luci-app-flclient 2>/dev/null; '
|
||||||
|
'rm -rf /tmp/luci-* 2>/dev/null; '
|
||||||
|
'opkg install /tmp/v2.ipk 2>&1; '
|
||||||
|
'/etc/init.d/uhttpd restart 2>&1; '
|
||||||
|
'echo DONE'
|
||||||
|
)
|
||||||
|
si, so, se = c.exec_command(cmds)
|
||||||
|
print(so.read().decode('utf-8', 'replace'))
|
||||||
|
err = se.read().decode('utf-8', 'replace')
|
||||||
|
if err.strip(): print('ERR:', err)
|
||||||
|
|
||||||
|
# Verify files
|
||||||
|
si, so, se = c.exec_command('ls -la /www/netflow/index.html /usr/lib/lua/luci/controller/netflow.lua 2>/dev/null')
|
||||||
|
print(so.read().decode().strip())
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
print('Deployed!')
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# repack_netflow_ipk.py — 将修改后的 control/ 和 data/ 目录重新打包为 IPK (tar.gz 格式)
|
||||||
|
# OpenWrt IPK 格式: 外层 tar.gz,内含 debian-binary + control.tar.gz + data.tar.gz
|
||||||
|
# 用法: python repack_netflow_ipk.py <data_dir> <control_dir> <output.ipk>
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
# 需要在 tar 中设为 755 权限的文件
|
||||||
|
EXEC_FILES = {
|
||||||
|
'./usr/bin/netflow',
|
||||||
|
'./usr/bin/netflow-rules',
|
||||||
|
'./usr/bin/netflow_x86_64',
|
||||||
|
'./usr/bin/netflow_aarch64',
|
||||||
|
'./usr/bin/netflow_arm',
|
||||||
|
'./usr/bin/netflow_mips',
|
||||||
|
'./usr/bin/netflow_mipsel',
|
||||||
|
'./etc/init.d/netflow',
|
||||||
|
'./etc/uci-defaults/luci-app-netflow',
|
||||||
|
}
|
||||||
|
|
||||||
|
# control.tar.gz 中的脚本全部需要 755
|
||||||
|
CONTROL_SCRIPTS = {'./preinst', './postinst', './prerm', './postrm'}
|
||||||
|
|
||||||
|
|
||||||
|
def fix_perms(tarinfo, is_control=False):
|
||||||
|
"""设置 Unix 权限"""
|
||||||
|
if tarinfo.isdir():
|
||||||
|
tarinfo.mode = 0o755
|
||||||
|
elif is_control and tarinfo.name in CONTROL_SCRIPTS:
|
||||||
|
tarinfo.mode = 0o755
|
||||||
|
elif tarinfo.name in EXEC_FILES:
|
||||||
|
tarinfo.mode = 0o755
|
||||||
|
else:
|
||||||
|
tarinfo.mode = 0o644
|
||||||
|
return tarinfo
|
||||||
|
|
||||||
|
|
||||||
|
def make_tar_gz_bytes(source_dir, is_control=False):
|
||||||
|
"""将目录打包为 tar.gz,返回 bytes"""
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=buf, mode='w:gz', format=tarfile.GNU_FORMAT) as tf:
|
||||||
|
for root, dirs, files in os.walk(source_dir):
|
||||||
|
for d in sorted(dirs):
|
||||||
|
fpath = os.path.join(root, d)
|
||||||
|
arcname = './' + os.path.relpath(fpath, source_dir).replace('\\', '/')
|
||||||
|
ti = tf.gettarinfo(fpath, arcname)
|
||||||
|
fix_perms(ti, is_control)
|
||||||
|
ti.uid = ti.gid = 0
|
||||||
|
ti.uname = ti.gname = 'root'
|
||||||
|
tf.addfile(ti)
|
||||||
|
|
||||||
|
for fn in sorted(files):
|
||||||
|
fpath = os.path.join(root, fn)
|
||||||
|
arcname = './' + os.path.relpath(fpath, source_dir).replace('\\', '/')
|
||||||
|
ti = tf.gettarinfo(fpath, arcname)
|
||||||
|
fix_perms(ti, is_control)
|
||||||
|
ti.uid = ti.gid = 0
|
||||||
|
ti.uname = ti.gname = 'root'
|
||||||
|
with open(fpath, 'rb') as f:
|
||||||
|
tf.addfile(ti, f)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 4:
|
||||||
|
print('用法: python repack_netflow_ipk.py <data_dir> <control_dir> <output.ipk>')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
data_dir = sys.argv[1]
|
||||||
|
control_dir = sys.argv[2]
|
||||||
|
output_ipk = sys.argv[3]
|
||||||
|
|
||||||
|
# 1. 生成 debian-binary
|
||||||
|
debian_data = b'2.0\n'
|
||||||
|
|
||||||
|
# 2. 打包 control.tar.gz
|
||||||
|
control_tgz = make_tar_gz_bytes(control_dir, is_control=True)
|
||||||
|
|
||||||
|
# 3. 打包 data.tar.gz
|
||||||
|
data_tgz = make_tar_gz_bytes(data_dir)
|
||||||
|
|
||||||
|
# 4. 创建外层 tar.gz (IPK 格式)
|
||||||
|
with tarfile.open(output_ipk, 'w:gz', format=tarfile.GNU_FORMAT) as outer:
|
||||||
|
for name, data, mode in [
|
||||||
|
('./debian-binary', debian_data, 0o644),
|
||||||
|
('./control.tar.gz', control_tgz, 0o644),
|
||||||
|
('./data.tar.gz', data_tgz, 0o644),
|
||||||
|
]:
|
||||||
|
ti = tarfile.TarInfo(name)
|
||||||
|
ti.size = len(data)
|
||||||
|
ti.mode = mode
|
||||||
|
ti.uid = ti.gid = 0
|
||||||
|
ti.uname = ti.gname = 'root'
|
||||||
|
ti.mtime = 0
|
||||||
|
outer.addfile(ti, io.BytesIO(data))
|
||||||
|
|
||||||
|
size = os.path.getsize(output_ipk)
|
||||||
|
print(f'IPK 打包完成: {output_ipk}')
|
||||||
|
print(f' 大小: {size:,} bytes')
|
||||||
|
print(f' 控制文件: {len(control_tgz):,} bytes')
|
||||||
|
print(f' 数据文件: {len(data_tgz):,} bytes')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# netflow-rules — 从 UCI 读取自定义规则,注入 mihomo config.yaml
|
||||||
|
# 用法: netflow-rules → 只生成 /tmp/netflow_custom_rules.yaml
|
||||||
|
# netflow-rules inject → 生成并注入到 config.yaml 中
|
||||||
|
# netflow-rules reload → 注入后通过 mihomo API 热重载
|
||||||
|
|
||||||
|
CONFIG="/etc/netflow/config.yaml"
|
||||||
|
CUSTOM_RULES="/tmp/netflow_custom_rules.yaml"
|
||||||
|
BAK="${CONFIG}.bak"
|
||||||
|
|
||||||
|
API_PORT=$(uci -q get netflow.config.api_port 2>/dev/null || echo "9091")
|
||||||
|
API_SECRET=$(uci -q get netflow.config.api_secret 2>/dev/null || echo "netflow_secret")
|
||||||
|
|
||||||
|
generate() {
|
||||||
|
>"$CUSTOM_RULES"
|
||||||
|
count=0
|
||||||
|
for sec in $(uci -q show netflow 2>/dev/null | grep '^netflow\.@custom_rule\[' | sed 's/=.*//'); do
|
||||||
|
t=$(uci -q get "${sec}.type" 2>/dev/null)
|
||||||
|
v=$(uci -q get "${sec}.value" 2>/dev/null)
|
||||||
|
p=$(uci -q get "${sec}.policy" 2>/dev/null)
|
||||||
|
if [ -n "$t" ] && [ -n "$v" ] && [ -n "$p" ]; then
|
||||||
|
echo " - ${t},${v},${p}" >> "$CUSTOM_RULES"
|
||||||
|
count=$((count + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return $count
|
||||||
|
}
|
||||||
|
|
||||||
|
inject() {
|
||||||
|
generate
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
logger -t netflow-rules "no custom rules defined, skipping"
|
||||||
|
rm -f "$CUSTOM_RULES"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIG" ] || [ ! -s "$CONFIG" ]; then
|
||||||
|
logger -t netflow-rules "config.yaml not found or empty, skip inject"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
MATCH_LINE=$(grep -n -m1 'MATCH,' "$CONFIG" 2>/dev/null | cut -d: -f1)
|
||||||
|
if [ -z "$MATCH_LINE" ]; then
|
||||||
|
logger -t netflow-rules "no MATCH rule found in config.yaml, skip inject"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_CONFIG="/tmp/netflow_config_patched.yaml"
|
||||||
|
cp "$CONFIG" "$BAK"
|
||||||
|
|
||||||
|
head -n $((MATCH_LINE - 1)) "$CONFIG" > "$TMP_CONFIG"
|
||||||
|
cat "$CUSTOM_RULES" >> "$TMP_CONFIG"
|
||||||
|
tail -n +${MATCH_LINE} "$CONFIG" >> "$TMP_CONFIG"
|
||||||
|
|
||||||
|
if [ -s "$TMP_CONFIG" ]; then
|
||||||
|
mv "$TMP_CONFIG" "$CONFIG"
|
||||||
|
logger -t netflow-rules "custom rules injected into config.yaml"
|
||||||
|
else
|
||||||
|
logger -t netflow-rules "ERROR: patched config is empty, restoring backup"
|
||||||
|
cp "$BAK" "$CONFIG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$CUSTOM_RULES"
|
||||||
|
}
|
||||||
|
|
||||||
|
reload_mihomo() {
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -s -X PUT \
|
||||||
|
-H "Authorization: Bearer ${API_SECRET}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"path\":\"${CONFIG}\"}" \
|
||||||
|
"http://127.0.0.1:${API_PORT}/configs" >/dev/null 2>&1
|
||||||
|
ret=$?
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -qO- --method=PUT \
|
||||||
|
--header="Authorization: Bearer ${API_SECRET}" \
|
||||||
|
--header="Content-Type: application/json" \
|
||||||
|
--body-data="{\"path\":\"${CONFIG}\"}" \
|
||||||
|
"http://127.0.0.1:${API_PORT}/configs" >/dev/null 2>&1
|
||||||
|
ret=$?
|
||||||
|
else
|
||||||
|
logger -t netflow-rules "curl/wget not found, cannot reload mihomo"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $ret -eq 0 ]; then
|
||||||
|
logger -t netflow-rules "mihomo config reloaded"
|
||||||
|
else
|
||||||
|
logger -t netflow-rules "mihomo reload failed (code=${ret})"
|
||||||
|
fi
|
||||||
|
return $ret
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
inject)
|
||||||
|
inject
|
||||||
|
;;
|
||||||
|
reload)
|
||||||
|
inject
|
||||||
|
reload_mihomo
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
generate
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
|
||||||
|
START=99
|
||||||
|
STOP=10
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
if [ ! -x "/usr/bin/netflow" ]; then
|
||||||
|
logger -t netflow "binary not found: /usr/bin/netflow"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
procd_open_instance
|
||||||
|
procd_set_param command /usr/bin/netflow
|
||||||
|
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
|
||||||
|
procd_set_param stdout 1
|
||||||
|
procd_set_param stderr 1
|
||||||
|
procd_close_instance
|
||||||
|
|
||||||
|
logger -t netflow "service started"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_service() {
|
||||||
|
logger -t netflow "service stopping"
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "netflow"
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
module("luci.controller.netflow", package.seeall)
|
||||||
|
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local http = require "luci.http"
|
||||||
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
|
||||||
|
--- 调用本机 netflow 后端:优先 curl(与 Makefile 依赖一致),失败时用 wget 兜底(部分魔改固件无 curl)。
|
||||||
|
local function call_backend_post(action, json_file_path)
|
||||||
|
local port = uci:get("netflow", "config", "backend_port") or "9190"
|
||||||
|
if not port:match("^%d+$") then port = "9190" end
|
||||||
|
local url = string.format("http://127.0.0.1:%s/api/%s", port, action)
|
||||||
|
local curl = string.format(
|
||||||
|
"curl -sS --connect-timeout 8 --max-time 45 -X POST '%s' -H 'Content-Type: application/json' -d @%s 2>/dev/null",
|
||||||
|
url, json_file_path
|
||||||
|
)
|
||||||
|
local out = sys.exec(curl)
|
||||||
|
if out and out ~= "" then
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
local wget = string.format(
|
||||||
|
"wget -qO- --timeout=50 --header='Content-Type: application/json' --post-file=%s '%s' 2>/dev/null",
|
||||||
|
json_file_path, url
|
||||||
|
)
|
||||||
|
return sys.exec(wget) or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
function index()
|
||||||
|
-- OpenWrt 25+ / LuCI(js)按 rpcd ACL 过滤菜单;键名须与 /usr/share/rpcd/acl.d/*.json 顶层键一致(CI 在 pkg_tag≠netflow 时改为 luci-app-<tag>)
|
||||||
|
entry({"admin", "services", "netflow"}, template("netflow/main"), _("FastLink_Lite"), 80).acl_depends = { "luci-app-flclient" }
|
||||||
|
entry({"admin", "services", "netflow", "api"}, call("action_api"), nil).acl_depends = { "luci-app-flclient" }
|
||||||
|
entry({"admin", "services", "netflow", "upload_core"}, call("action_upload_core"), nil).acl_depends = { "luci-app-flclient" }
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_api()
|
||||||
|
local action = http.formvalue("action") or ""
|
||||||
|
|
||||||
|
if not action:match("^[%w_]+$") then
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"error","message":"invalid action"}')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 自定义规则 API(本地 UCI 操作,不经过 Go 后端)
|
||||||
|
if action == "rule_list" then
|
||||||
|
action_rule_list_local()
|
||||||
|
return
|
||||||
|
elseif action == "rule_add" then
|
||||||
|
action_rule_add_local()
|
||||||
|
return
|
||||||
|
elseif action == "rule_delete" then
|
||||||
|
action_rule_delete_local()
|
||||||
|
return
|
||||||
|
elseif action == "rule_update" then
|
||||||
|
action_rule_update_local()
|
||||||
|
return
|
||||||
|
elseif action == "rule_sort" then
|
||||||
|
action_rule_sort_local()
|
||||||
|
return
|
||||||
|
elseif action == "rule_import" then
|
||||||
|
action_rule_import_local()
|
||||||
|
return
|
||||||
|
elseif action == "rule_templates" then
|
||||||
|
action_rule_templates_local()
|
||||||
|
return
|
||||||
|
elseif action == "rule_apply" then
|
||||||
|
action_rule_apply_local()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local function urldecode(s)
|
||||||
|
if not s then return s end
|
||||||
|
s = s:gsub('+', ' ')
|
||||||
|
s = s:gsub('%%(%x%x)', function(h) return string.char(tonumber(h, 16)) end)
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
local params = {}
|
||||||
|
local known_keys = {
|
||||||
|
"email", "password", "group", "node", "mode", "state", "run_mode", "source"
|
||||||
|
}
|
||||||
|
for _, key in ipairs(known_keys) do
|
||||||
|
local val = http.formvalue(key)
|
||||||
|
if val and val ~= "" then
|
||||||
|
params[key] = urldecode(val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local jsonc = require "luci.jsonc"
|
||||||
|
local json_body = jsonc.stringify(params) or "{}"
|
||||||
|
|
||||||
|
local tmp = os.tmpname()
|
||||||
|
local f = io.open(tmp, "w")
|
||||||
|
if f then
|
||||||
|
f:write(json_body)
|
||||||
|
f:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = call_backend_post(action, tmp)
|
||||||
|
os.remove(tmp)
|
||||||
|
|
||||||
|
if not result or result == "" then
|
||||||
|
result = '{"status":"error","message":"后端未响应,请检查服务是否运行(或安装 curl/wget)"}'
|
||||||
|
end
|
||||||
|
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ===== 自定义规则 API =====
|
||||||
|
|
||||||
|
function action_rule_list_local()
|
||||||
|
local rules = {}
|
||||||
|
uci:foreach("netflow", "custom_rule", function(s)
|
||||||
|
if s[".name"] then
|
||||||
|
table.insert(rules, {id = s[".name"], type = s.type or "", value = s.value or "", policy = s.policy or ""})
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
local jsonc = require "luci.jsonc"
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write(jsonc.stringify({status = "success", rules = rules}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_rule_add_local()
|
||||||
|
local t = http.formvalue("type") or ""
|
||||||
|
local v = http.formvalue("value") or ""
|
||||||
|
local p = http.formvalue("policy") or ""
|
||||||
|
if t == "" or v == "" or p == "" then
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"error","message":"参数不完整"}')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local section = uci:add("netflow", "custom_rule")
|
||||||
|
uci:set("netflow", section, "type", t)
|
||||||
|
uci:set("netflow", section, "value", v)
|
||||||
|
uci:set("netflow", section, "policy", p)
|
||||||
|
uci:commit("netflow")
|
||||||
|
local jsonc = require "luci.jsonc"
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write(jsonc.stringify({status = "success", message = "规则已添加", id = section}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_rule_delete_local()
|
||||||
|
local id = http.formvalue("id") or ""
|
||||||
|
if id == "" then
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"error","message":"缺少规则ID"}')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
uci:delete("netflow", id)
|
||||||
|
uci:commit("netflow")
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"success","message":"规则已删除"}')
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_rule_update_local()
|
||||||
|
local id = http.formvalue("id") or ""
|
||||||
|
local t = http.formvalue("type") or ""
|
||||||
|
local v = http.formvalue("value") or ""
|
||||||
|
local p = http.formvalue("policy") or ""
|
||||||
|
if id == "" or t == "" or v == "" or p == "" then
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"error","message":"参数不完整"}')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
uci:set("netflow", id, "type", t)
|
||||||
|
uci:set("netflow", id, "value", v)
|
||||||
|
uci:set("netflow", id, "policy", p)
|
||||||
|
uci:commit("netflow")
|
||||||
|
local jsonc = require "luci.jsonc"
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write(jsonc.stringify({status = "success", message = "规则已更新"}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_rule_sort_local()
|
||||||
|
local id = http.formvalue("id") or ""
|
||||||
|
local dir = http.formvalue("dir") or ""
|
||||||
|
if id == "" or (dir ~= "up" and dir ~= "down") then
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"error","message":"参数错误"}')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local rules = {}
|
||||||
|
uci:foreach("netflow", "custom_rule", function(s)
|
||||||
|
if s[".name"] then
|
||||||
|
table.insert(rules, {id = s[".name"], type = s.type or "", value = s.value or "", policy = s.policy or ""})
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
local idx = nil
|
||||||
|
for i, r in ipairs(rules) do if r.id == id then idx = i; break end end
|
||||||
|
if not idx then
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"error","message":"规则不存在"}')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local swap = nil
|
||||||
|
if dir == "up" and idx > 1 then swap = idx - 1
|
||||||
|
elseif dir == "down" and idx < #rules then swap = idx + 1 end
|
||||||
|
if not swap then
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"error","message":"已在边界"}')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
rules[idx], rules[swap] = rules[swap], rules[idx]
|
||||||
|
uci:foreach("netflow", "custom_rule", function(s)
|
||||||
|
if s[".name"] then uci:delete("netflow", s[".name"]) end
|
||||||
|
end)
|
||||||
|
for _, r in ipairs(rules) do
|
||||||
|
local section = uci:add("netflow", "custom_rule")
|
||||||
|
uci:set("netflow", section, "type", r.type)
|
||||||
|
uci:set("netflow", section, "value", r.value)
|
||||||
|
uci:set("netflow", section, "policy", r.policy)
|
||||||
|
end
|
||||||
|
uci:commit("netflow")
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"success","message":"排序已更新"}')
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_rule_import_local()
|
||||||
|
local text = http.formvalue("text") or ""
|
||||||
|
if text == "" then
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"error","message":"内容为空"}')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local count = 0
|
||||||
|
for line in text:gmatch("[^\r\n]+") do
|
||||||
|
line = line:match("^%s*(.-)%s*$")
|
||||||
|
if line ~= "" and line:sub(1,1) ~= "#" then
|
||||||
|
local parts = {}
|
||||||
|
for p in line:gmatch("[^,]+") do table.insert(parts, p:match("^%s*(.-)%s*$")) end
|
||||||
|
local tp, val, pol
|
||||||
|
if #parts >= 3 then tp, val, pol = parts[1], parts[2], parts[3]
|
||||||
|
elseif #parts == 2 then tp, val, pol = "DOMAIN-SUFFIX", parts[1], parts[2] end
|
||||||
|
if tp and val and pol then
|
||||||
|
local section = uci:add("netflow", "custom_rule")
|
||||||
|
uci:set("netflow", section, "type", tp)
|
||||||
|
uci:set("netflow", section, "value", val)
|
||||||
|
uci:set("netflow", section, "policy", pol)
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
uci:commit("netflow")
|
||||||
|
local jsonc = require "luci.jsonc"
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write(jsonc.stringify({status = "success", message = "已导入 " .. count .. " 条规则", count = count}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_rule_templates_local()
|
||||||
|
local templates = {
|
||||||
|
{name = "屏蔽常见广告域名", rules = {
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "doubleclick.net", policy = "REJECT"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "googlesyndication.com", policy = "REJECT"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "googleadservices.com", policy = "REJECT"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "adservice.google.com", policy = "REJECT"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "googletagmanager.com", policy = "REJECT"},
|
||||||
|
}},
|
||||||
|
{name = "屏蔽 iOS 更新", rules = {
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "mesu.apple.com", policy = "REJECT"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "appldnld.apple.com", policy = "REJECT"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "gdmf.apple.com", policy = "REJECT"},
|
||||||
|
}},
|
||||||
|
{name = "国内直连", rules = {
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "cn", policy = "DIRECT"},
|
||||||
|
{type = "DOMAIN-KEYWORD", value = "baidu", policy = "DIRECT"},
|
||||||
|
{type = "DOMAIN-KEYWORD", value = "taobao", policy = "DIRECT"},
|
||||||
|
{type = "DOMAIN-KEYWORD", value = "alibaba", policy = "DIRECT"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "189.cn", policy = "DIRECT"},
|
||||||
|
}},
|
||||||
|
{name = "AI 工具走代理", rules = {
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "openai.com", policy = "Proxy"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "claude.ai", policy = "Proxy"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "chatgpt.com", policy = "Proxy"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "anthropic.com", policy = "Proxy"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "gemini.google.com", policy = "Proxy"},
|
||||||
|
{type = "DOMAIN-SUFFIX", value = "perplexity.ai", policy = "Proxy"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
local jsonc = require "luci.jsonc"
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write(jsonc.stringify({status = "success", templates = templates}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_rule_apply_local()
|
||||||
|
sys.call("/usr/bin/netflow-rules reload")
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write('{"status":"success","message":"规则已应用并热重载"}')
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_upload_core()
|
||||||
|
local fp
|
||||||
|
local upload_path = "/tmp/mihomo_upload"
|
||||||
|
|
||||||
|
http.setfilehandler(function(meta, chunk, eof)
|
||||||
|
if not fp and meta and meta.name == "corefile" then
|
||||||
|
fp = io.open(upload_path, "w")
|
||||||
|
end
|
||||||
|
if fp and chunk then
|
||||||
|
fp:write(chunk)
|
||||||
|
end
|
||||||
|
if fp and eof then
|
||||||
|
fp:close()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
http.formvalue("corefile")
|
||||||
|
|
||||||
|
local empty_json = os.tmpname()
|
||||||
|
local ef = io.open(empty_json, "w")
|
||||||
|
if ef then
|
||||||
|
ef:write("{}")
|
||||||
|
ef:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = call_backend_post("core_install_upload", empty_json)
|
||||||
|
os.remove(empty_json)
|
||||||
|
|
||||||
|
if not result or result == "" then
|
||||||
|
result = '{"status":"error","message":"后端未响应,请检查服务是否运行(或安装 curl/wget)"}'
|
||||||
|
end
|
||||||
|
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
http.write(result)
|
||||||
|
end
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user