diff --git a/luci-app-hermes/Makefile b/luci-app-hermes/Makefile
new file mode 100644
index 00000000..4529211a
--- /dev/null
+++ b/luci-app-hermes/Makefile
@@ -0,0 +1,17 @@
+
+include $(TOPDIR)/rules.mk
+
+PKG_VERSION:=1.0.0-20240101
+PKG_RELEASE:=1
+
+LUCI_TITLE:=LuCI support for Hermes
+LUCI_PKGARCH:=all
+LUCI_DEPENDS:=+docker +dockerd +luci-lib-taskd
+
+define Package/luci-app-hermes/conffiles
+/etc/config/hermes
+endef
+
+include $(TOPDIR)/feeds/luci/luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/luci-app-hermes/luasrc/controller/hermes.lua b/luci-app-hermes/luasrc/controller/hermes.lua
new file mode 100644
index 00000000..45b3c901
--- /dev/null
+++ b/luci-app-hermes/luasrc/controller/hermes.lua
@@ -0,0 +1,6 @@
+module("luci.controller.hermes", package.seeall)
+
+function index()
+ entry({"admin", "services", "hermes"}, alias("admin", "services", "hermes", "config"), _("Hermes"), 30).dependent = true
+ entry({"admin", "services", "hermes", "config"}, cbi("hermes"))
+end
diff --git a/luci-app-hermes/luasrc/model/cbi/hermes.lua b/luci-app-hermes/luasrc/model/cbi/hermes.lua
new file mode 100644
index 00000000..d81bd158
--- /dev/null
+++ b/luci-app-hermes/luasrc/model/cbi/hermes.lua
@@ -0,0 +1,45 @@
+--[[
+LuCI - Lua Configuration Interface
+]]--
+
+local taskd = require "luci.model.tasks"
+local hermes_model = require "luci.model.hermes"
+local m, s, o
+
+m = taskd.docker_map("hermes", "hermes", "/usr/libexec/istorec/hermes.sh",
+ translate("Hermes"),
+ translate("The self-improving AI agent that runs on your server. Layered memory that accumulates across sessions, a cron scheduler that fires while you're offline, and a skills system that saves reusable procedures automatically.")
+ .. translate("Official website:") .. ' https://get-hermes.ai/')
+
+s = m:section(SimpleSection, translate("Service Status"), translate("Hermes status:"))
+s:append(Template("hermes/status"))
+
+s = m:section(TypedSection, "main", translate("Setup"), translate("The following parameters will only take effect during installation or upgrade:"))
+s.addremove = false
+s.anonymous = true
+
+o = s:option(Value, "port", translate("Port") .. "*")
+o.default = "8787"
+o.datatype = "port"
+
+local blocks = hermes_model.blocks()
+
+o = s:option(Value, "data_path", translate("Data path") .. "*")
+o.rmempty = false
+o.datatype = "string"
+local data_paths, data_default = hermes_model.find_paths(blocks, "data")
+for _, val in pairs(data_paths) do
+ o:value(val, val)
+end
+o.default = data_default
+
+o = s:option(Value, "workspace_path", translate("Workspace path") .. "*")
+o.rmempty = false
+o.datatype = "string"
+local ws_paths, ws_default = hermes_model.find_paths(blocks, "workspace")
+for _, val in pairs(ws_paths) do
+ o:value(val, val)
+end
+o.default = ws_default
+
+return m
diff --git a/luci-app-hermes/luasrc/model/hermes.lua b/luci-app-hermes/luasrc/model/hermes.lua
new file mode 100644
index 00000000..a1c34d19
--- /dev/null
+++ b/luci-app-hermes/luasrc/model/hermes.lua
@@ -0,0 +1,38 @@
+local util = require "luci.util"
+local jsonc = require "luci.jsonc"
+
+local hermes = {}
+
+hermes.blocks = function()
+ local f = io.popen("lsblk -s -f -b -o NAME,FSSIZE,MOUNTPOINT --json", "r")
+ local vals = {}
+ if f then
+ local ret = f:read("*all")
+ f:close()
+ local obj = jsonc.parse(ret)
+ if obj and obj["blockdevices"] then
+ for _, val in pairs(obj["blockdevices"]) do
+ local fsize = val["fssize"]
+ if fsize ~= nil and string.len(fsize) > 10 and val["mountpoint"] then
+ -- fsize > 1G
+ vals[#vals+1] = val["mountpoint"]
+ end
+ end
+ end
+ end
+ return vals
+end
+
+hermes.find_paths = function(blocks, subdir)
+ local configs = {}
+ local default_path = ''
+ for _, mountpoint in pairs(blocks) do
+ table.insert(configs, mountpoint .. "/hermes/" .. subdir)
+ end
+ if #configs > 0 then
+ default_path = configs[1]
+ end
+ return configs, default_path
+end
+
+return hermes
diff --git a/luci-app-hermes/luasrc/view/hermes/status.htm b/luci-app-hermes/luasrc/view/hermes/status.htm
new file mode 100644
index 00000000..4884d4e4
--- /dev/null
+++ b/luci-app-hermes/luasrc/view/hermes/status.htm
@@ -0,0 +1,30 @@
+<%
+local util = require "luci.util"
+local container_status = util.trim(util.exec("/usr/libexec/istorec/hermes.sh status"))
+local container_install = (string.len(container_status) > 0)
+local container_running = container_status == "running"
+-%>
+
+
+
+ <% if container_running then %>
+
+ <% else %>
+
+ <% end %>
+
+
+<%
+if container_running then
+ local port = util.trim(util.exec("/usr/libexec/istorec/hermes.sh port"))
+ if port == "" then
+ port = "8787"
+ end
+-%>
+
+
+
+
+
+
+<% end %>
diff --git a/luci-app-hermes/po/zh-cn/hermes.po b/luci-app-hermes/po/zh-cn/hermes.po
new file mode 100644
index 00000000..2fbe0b8a
--- /dev/null
+++ b/luci-app-hermes/po/zh-cn/hermes.po
@@ -0,0 +1,44 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+msgid "Official website:"
+msgstr "官方网站:"
+
+msgid "Hermes"
+msgstr "Hermes 消息服务"
+
+msgid "The self-improving AI agent that runs on your server. Layered memory that accumulates across sessions, a cron scheduler that fires while you're offline, and a skills system that saves reusable procedures automatically."
+msgstr "运行在您的服务器上的自我改进型人工智能代理。它采用跨会话累积的分层内存、在您离线时运行的定时任务调度程序,以及自动保存可重用程序的技能系统。"
+
+msgid "Port"
+msgstr "端口"
+
+msgid "Data path"
+msgstr "数据路径"
+
+msgid "Workspace path"
+msgstr "工作目录路径"
+
+msgid "Service Status"
+msgstr "服务状态"
+
+msgid "Hermes status:"
+msgstr "Hermes 状态信息如下:"
+
+msgid "Setup"
+msgstr "安装配置"
+
+msgid "The following parameters will only take effect during installation or upgrade:"
+msgstr "以下参数只在安装或者升级时才会生效:"
+
+msgid "Status"
+msgstr "状态"
+
+msgid "Hermes is running"
+msgstr "Hermes 运行中"
+
+msgid "Hermes is not running"
+msgstr "Hermes 未运行"
+
+msgid "Open Hermes"
+msgstr "打开 Hermes"
diff --git a/luci-app-hermes/po/zh_Hans b/luci-app-hermes/po/zh_Hans
new file mode 120000
index 00000000..41451e4a
--- /dev/null
+++ b/luci-app-hermes/po/zh_Hans
@@ -0,0 +1 @@
+zh-cn
\ No newline at end of file
diff --git a/luci-app-hermes/root/etc/config/hermes b/luci-app-hermes/root/etc/config/hermes
new file mode 100644
index 00000000..3472a9aa
--- /dev/null
+++ b/luci-app-hermes/root/etc/config/hermes
@@ -0,0 +1,4 @@
+config main
+ option 'port' '8787'
+ option 'data_path' ''
+ option 'workspace_path' ''
diff --git a/luci-app-hermes/root/usr/libexec/istorec/hermes.sh b/luci-app-hermes/root/usr/libexec/istorec/hermes.sh
new file mode 100755
index 00000000..775a611a
--- /dev/null
+++ b/luci-app-hermes/root/usr/libexec/istorec/hermes.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+
+ACTION=${1}
+shift 1
+
+do_install() {
+ local port=$(uci get hermes.@main[0].port 2>/dev/null | tr -d '\n\r')
+ local data=$(uci get hermes.@main[0].data_path 2>/dev/null | tr -d '\n\r')
+ local workspace=$(uci get hermes.@main[0].workspace_path 2>/dev/null | tr -d '\n\r')
+
+ local image_name="linkease/hermes:latest"
+ echo "docker pull ${image_name}"
+ docker pull ${image_name}
+ docker rm -f hermes
+
+ [ -z "$port" ] && port=8787
+
+ local cmd="docker run --restart=unless-stopped -d"
+
+ [ -n "$data" ] && cmd="$cmd -v \"$data:/data/.hermes\""
+ [ -n "$workspace" ] && cmd="$cmd -v \"$workspace:/workspace\""
+
+ cmd="$cmd -p $port:8787"
+
+ local tz=$(uci get system.@system[0].zonename 2>/dev/null | sed 's/ /_/g')
+ [ -z "$tz" ] || cmd="$cmd -e TZ=$tz"
+
+ cmd="$cmd --name hermes \"$image_name\""
+
+ echo "$cmd"
+ eval "$cmd"
+}
+
+usage() {
+ echo "usage: $0 sub-command"
+ echo "where sub-command is one of:"
+ echo " install Install the hermes"
+ echo " upgrade Upgrade the hermes"
+ echo " rm/start/stop/restart Remove/Start/Stop/Restart the hermes"
+ echo " status Hermes status"
+ echo " port Hermes port"
+}
+
+case ${ACTION} in
+ "install")
+ do_install
+ ;;
+ "upgrade")
+ do_install
+ ;;
+ "rm")
+ docker rm -f hermes
+ ;;
+ "start" | "stop" | "restart")
+ docker ${ACTION} hermes
+ ;;
+ "status")
+ docker ps --all -f 'name=^/hermes$' --format '{{.State}}'
+ ;;
+ "port")
+ docker ps --all -f 'name=^/hermes$' --format '{{.Ports}}' | grep -om1 '0.0.0.0:[0-9]*->8787/tcp' | sed 's/0.0.0.0:\([0-9]*\)->.*/\1/'
+ ;;
+ *)
+ usage
+ exit 1
+ ;;
+esac
diff --git a/luci-app-hermes/root/usr/share/rpcd/acl.d/luci-app-hermes.json b/luci-app-hermes/root/usr/share/rpcd/acl.d/luci-app-hermes.json
new file mode 100644
index 00000000..a07cd290
--- /dev/null
+++ b/luci-app-hermes/root/usr/share/rpcd/acl.d/luci-app-hermes.json
@@ -0,0 +1,11 @@
+{
+ "luci-app-hermes": {
+ "description": "Grant UCI access for luci-app-hermes",
+ "read": {
+ "uci": [ "hermes" ]
+ },
+ "write": {
+ "uci": [ "hermes" ]
+ }
+ }
+}
diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile
index f4292f11..75a0d508 100644
--- a/luci-app-passwall/Makefile
+++ b/luci-app-passwall/Makefile
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.4.15
-PKG_RELEASE:=81
+PKG_RELEASE:=82
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua
index 557fcf63..397944eb 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua
@@ -48,15 +48,23 @@ o.default = ""
o:depends("console_auth", true)
---- Console Port
-o = s:option(Value, "console_port", translate("Console Port"), translate(
- "In the browser input routing IP plus port access, such as:192.168.1.1:1188"))
-o.default = "1188"
+o = s:option(Value, "console_port", translate("Console Port"), translate("In the browser input routing IP plus port access, such as:192.168.1.1:1188"))
+o.datatype = "port"
+o.default = 1188
o:depends("balancing_enable", true)
o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "0"
o:depends("balancing_enable", true)
+o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy"))
+o:value("roundrobin", "roundRobin")
+o:value("random", "random")
+o:value("leastconn", "leastConn")
+o:value("source", "source")
+o.default = "roundrobin"
+o:depends("balancing_enable", true)
+
---- Health Check Type
o = s:option(ListValue, "health_check_type", translate("Health Check Type"))
o.default = "passwall_logic"
@@ -77,9 +85,12 @@ o.description = translate("The URL used to detect the connection status.")
o:depends("health_check_type", "passwall_logic")
---- Health Check Inter
-o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds"))
-o.default = "60"
+o = s:option(Value, "health_check_inter", translate("Health Check Inter"))
+o.datatype = "min(20)"
+o.default = 60
o:depends("balancing_enable", true)
+o.description = translate("Units:seconds") .. " ( ≥ 20s ) " ..
+ translate("Note: Frequent health checks increase CPU usage; an interval ≥ 60 seconds is recommended.")
o = s:option(DummyValue, "health_check_tips", " ")
o.rawhtml = true
@@ -89,11 +100,10 @@ end
o:depends("health_check_type", "passwall_logic")
-- [[ Balancing Settings ]]--
-s = m:section(TypedSection, "haproxy_config", translate("Node List"),
- "" ..
- translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") ..
- "\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") ..
- "")
+s = m:section(TypedSection, "haproxy_config", translate("Node List"))
+s.description = "" ..
+ translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. " " ..
+ translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. ""
s.template = "cbi/tblsection"
s.sortable = true
s.anonymous = true
@@ -148,7 +158,7 @@ o.rmempty = false
---- Node Weight
o = s:option(Value, "lbweight", translate("Node Weight"))
-o.datatype = "uinteger"
+o.datatype = "min(1)"
o.default = 5
o.rmempty = false
diff --git a/luci-app-passwall/po/zh-cn/passwall.po b/luci-app-passwall/po/zh-cn/passwall.po
index 8342d28b..6c4dd7ee 100644
--- a/luci-app-passwall/po/zh-cn/passwall.po
+++ b/luci-app-passwall/po/zh-cn/passwall.po
@@ -970,6 +970,9 @@ msgstr "内置实现"
msgid "Health Check Inter"
msgstr "健康检查节点间隔时间"
+msgid "Note: Frequent health checks increase CPU usage; an interval ≥ 60 seconds is recommended."
+msgstr "注意:检测过于频繁会增加 CPU 负载,建议间隔 ≥60 秒。"
+
msgid "When the URL test is used, the load balancing node will be converted into a Socks node. when node list set customizing, must be a Socks node, otherwise the health check will be invalid."
msgstr "当使用 URL 测试时,负载均衡节点将转换成 Socks 节点。下面的节点列表自定义时必须为 Socks 节点,否则健康检查将无效。"
diff --git a/luci-app-passwall/root/usr/share/passwall/haproxy.lua b/luci-app-passwall/root/usr/share/passwall/haproxy.lua
index fcff7344..046328ce 100644
--- a/luci-app-passwall/root/usr/share/passwall/haproxy.lua
+++ b/luci-app-passwall/root/usr/share/passwall/haproxy.lua
@@ -24,7 +24,8 @@ local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53"
local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1"
local health_check_type = uci:get(appname, "@global_haproxy[0]", "health_check_type") or "tcp"
-local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "10"
+local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "20"
+local balancingStrategy = uci:get(appname, "@global_haproxy[0]", "balancingStrategy") or "roundrobin"
local console_port = uci:get(appname, "@global_haproxy[0]", "console_port")
local bind_local = uci:get(appname, "@global_haproxy[0]", "bind_local") or "0"
local bind_address = "0.0.0.0"
@@ -40,35 +41,37 @@ local f_out = io.open(haproxy_file, "a")
local haproxy_config = [[
global
daemon
- log 127.0.0.1 local2
- maxconn 60000
- stats socket {{path}}/haproxy.sock
+ log 127.0.0.1 local2
+ maxconn 60000
+ stats socket {{path}}/haproxy.sock
nbthread {{nbthread}}
external-check
insecure-fork-wanted
+ spread-checks 50
+ tune.max-checks-per-thread 5
defaults
- mode tcp
- log global
- option tcplog
- option dontlognull
+ mode tcp
+ log global
+ option tcplog
+ option dontlognull
option http-server-close
- #option forwardfor except 127.0.0.0/8
- option redispatch
- retries 2
- timeout http-request 10s
- timeout queue 1m
- timeout connect 10s
- timeout client 1m
- timeout server 1m
+ #option forwardfor except 127.0.0.0/8
+ option redispatch
+ retries 2
+ timeout http-request 10s
+ timeout queue 1m
+ timeout connect 10s
+ timeout client 1m
+ timeout server 1m
timeout http-keep-alive 10s
- timeout check 10s
- maxconn 3000
-
+ timeout check 10s
+ maxconn 3000
+
resolvers mydns
- resolve_retries 1
- timeout resolve 5s
- hold valid 600s
+ resolve_retries 1
+ timeout resolve 5s
+ hold valid 600s
{{dns}}
]]
@@ -99,10 +102,12 @@ uci:foreach(appname, "haproxy_config", function(t)
local lbss = t.lbss
local listen_port = tonumber(t.haproxy_port) or 0
local server_node = uci:get_all(appname, lbss)
- if server_node and server_node.address and server_node.port then
- server_remark = server_node.address .. ":" .. server_node.port
+ local hop = (health_check_type == "passwall_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil
+ hop = hop and hop:gsub(":", "-") or nil
+ if server_node and server_node.address and (server_node.port or hop) then
+ server_remark = server_node.address .. ":" .. (server_node.port or hop)
server_address = server_node.address
- server_port = server_node.port
+ server_port = server_node.port or hop
t.origin_address = server_address
t.origin_port = server_port
if health_check_type == "passwall_logic" then
@@ -145,6 +150,10 @@ uci:foreach(appname, "haproxy_config", function(t)
end
end)
+local inter = tonumber((health_check_inter or "20"):match("^%d+"))
+inter = (inter and inter >= 20) and inter or 20
+local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s"
+
local sortTable = {}
for i in pairs(listens) do
if i ~= nil then
@@ -160,8 +169,8 @@ for i, port in pairs(sortTable) do
listen %s
bind %s:%s
mode tcp
- balance roundrobin
-]], port, bind_address, port))
+ balance %s
+]], port, bind_address, port, balancingStrategy))
if health_check_type == "passwall_logic" then
f_out:write(string.format([[
@@ -182,7 +191,7 @@ listen %s
count_B = count_B + 1
end
local server = o.server_address .. ":" .. o.server_port
- local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check inter {{inter}} rise 1 fall 3 {{backup}}"
+ local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}"
server_conf = server_conf:gsub("{{remark}}", remark)
server_conf = server_conf:gsub("{{server}}", server)
server_conf = server_conf:gsub("{{weight}}", o.lbweight)
@@ -191,10 +200,10 @@ listen %s
resolvers = ""
end
server_conf = server_conf:gsub("{{resolvers}}", resolvers)
- server_conf = server_conf:gsub("{{inter}}", tonumber(health_check_inter) .. "s")
+ server_conf = server_conf:gsub("{{inter_arg}}", inter_arg)
server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "")
- f_out:write(" " .. server_conf .. "\n")
+ f_out:write(" " .. api.trim(server_conf) .. "\n")
if o.export ~= "0" then
sys.call(string.format(". /usr/share/passwall/utils.sh ; add_ip2route %s %s", o.origin_address, o.export))