🐤 Sync 2026-04-19 08:30:38

This commit is contained in:
github-actions[bot] committed 2026-04-19 08:30:38 +08:00
1 parent 746d64b361
commit a9b3f7579c
14 files changed
+327 -42

No files matched your search

+17
View File
@@ -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
@@ -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
@@ -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:") .. ' <a href=\"https://get-hermes.ai/\" target=\"_blank\">https://get-hermes.ai/</a>')
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") .. "<b>*</b>")
o.default = "8787"
o.datatype = "port"
local blocks = hermes_model.blocks()
o = s:option(Value, "data_path", translate("Data path") .. "<b>*</b>")
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") .. "<b>*</b>")
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
+38
View File
@@ -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
@@ -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"
-%>
<div class="cbi-value">
<label class="cbi-value-title"><%:Status%></label>
<div class="cbi-value-field">
<% if container_running then %>
<button class="cbi-button cbi-button-success" disabled="true"><%:Hermes is running%></button>
<% else %>
<button class="cbi-button cbi-button-negative" disabled="true"><%:Hermes is not running%></button>
<% end %>
</div>
</div>
<%
if container_running then
local port = util.trim(util.exec("/usr/libexec/istorec/hermes.sh port"))
if port == "" then
port = "8787"
end
-%>
<div class="cbi-value cbi-value-last">
<label class="cbi-value-title">&nbsp;</label>
<div class="cbi-value-field">
<input type="button" class="btn cbi-button cbi-button-apply" name="start" value="<%:Open Hermes%>" onclick="window.open('http://'+location.hostname+':<%=port%>/', '_blank')">
</div>
</div>
<% end %>
+44
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
zh-cn
+4
View File
@@ -0,0 +1,4 @@
config main
option 'port' '8787'
option 'data_path' ''
option 'workspace_path' ''
+67
View File
@@ -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
@@ -0,0 +1,11 @@
{
"luci-app-hermes": {
"description": "Grant UCI access for luci-app-hermes",
"read": {
"uci": [ "hermes" ]
},
"write": {
"uci": [ "hermes" ]
}
}
}
+1 -1
View File
@@ -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:= \
@@ -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 )<br>" ..
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"),
"<font color='red'>" ..
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!") ..
"</font>")
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
s.description = "<font color='red'>" ..
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.") .. "<br>" ..
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!") .. "</font>"
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
+3
View File
@@ -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 节点,否则健康检查将无效。"
@@ -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))