update 2026-09-20 16:59:13

This commit is contained in:
action committed 2026-09-20 16:59:13 +08:00
1 parent 4dd6850a75
commit 735d247cbe
23 files changed
+237 -115

No files matched your search

+4 -4
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=clashoo
PKG_SHORT_SHA:=fbb6742
PKG_COMMIT_DATE:=2026.09.16
PKG_SHORT_SHA:=5019cc0
PKG_COMMIT_DATE:=2026.09.19
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
PKG_RELEASE:=1
@@ -11,8 +11,8 @@ PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=fbb674227d5cf5a3796a1dd1451849fa1872884a
PKG_HASH:=ecdd7fb1d5847884b350317e09c6d366fe6ea57e96f3ac0cd928df110f541ac8
PKG_SOURCE_VERSION:=5019cc090ed7cafb76643f964a76b1e97aee2985
PKG_HASH:=52f127d0125a3fb8e17dc7fe48c8f01e831fbc56cf4c86d7674a11c990e71848
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
PKG_LICENSE:=GPL3.0+
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 640 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

@@ -175,6 +175,7 @@ end
function action_events()
local logs = ""
local query ={}
local json_parse = luci.jsonc.parse
local dk = docker.new()
-- Get events from last 24 hours
@@ -187,7 +188,32 @@ function action_events()
local date = "unknown"
if v and v.time then
date = os.date("%Y-%m-%d %H:%M:%S", v.time)
-- 安全检查:确保events对象和body存在
if not events then
logs = "[ERROR] Failed to connect to Docker daemon. Please ensure Docker is running.\n"
elseif not events.body then
local error_msg = events.message or events.body and events.body.message or tostring(events.body) or "Unknown error"
logs = string.format("[ERROR] Docker API error (code: %d): %s\n", events.code or 0, error_msg)
elseif events.code ~= 200 then
local error_msg = events.body.message or events.body or "Unknown error"
logs = string.format("[ERROR] Docker API error (code: %d): %s\n", events.code or 0, error_msg)
elseif type(events.body) ~= "table" and type(events.body) ~= "string" then
logs = "[INFO] No events received in the last 24 hours.\n"
else
-- 统一处理:如果是字符串,转换为table;如果是table,直接使用
local event_list = {}
if type(events.body) == "string" and events.body ~= "" then
-- 尝试解析JSON数组格式
local parsed = json_parse(events.body)
if type(parsed) == "table" then
event_list = parsed
else
-- 解析失败,当作单条事件处理
event_list = {events.body}
end
elseif type(events.body) == "table" then
event_list = events.body
end
local name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
local action = v.Action or "unknown"
@@ -202,10 +228,125 @@ function action_events()
elseif v.Type == "image" then
local id = v.Actor and v.Actor.ID or "unknown"
logs = logs .. string.format("[%s] %s %s Image: %s Image name: %s\n", date, v.Type, action, id, name)
-- 检查事件列表是否为空
if #event_list == 0 then
logs = "[INFO] No events received in the last 24 hours.\n"
else
-- 遍历事件列表
for _, item in ipairs(event_list) do
local v = item
-- 如果是字符串,尝试解析JSON
if type(v) == "string" and v ~= "" then
v = json_parse(v)
end
-- 确保v是有效的table
if type(v) ~= "table" then
logs = logs .. string.format("[PARSE ERROR] Invalid event data\n")
else
-- 解析时间戳:可读时间格式
local ts = tonumber(v.time) or 0
local readable_time = os.date("%Y-%m-%d %H:%M:%S", ts)
-- 获取事件类型和动作
local event_type = v.Type or v.type or "unknown"
local action = v.Action or v.action or "unknown"
-- 根据事件类型格式化输出:时间戳单独一行,其他信息分行列出
if event_type == "container" then
local container_id = v.Actor and v.Actor.ID or "unknown"
-- 预计算显示值,避免在string.format参数中使用连接操作符
local short_id = #container_id > 12 and container_id:sub(1, 12) or container_id
local container_name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
local image = v.Actor and v.Actor.Attributes and v.Actor.Attributes.image or ""
local display_name = container_name .. " (" .. short_id .. ")"
logs = logs .. string.format(
"%s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n\n",
readable_time,
"", "[TYPE]", event_type,
"", "[ACTION]", action,
"", "[CONTAINER]", display_name,
"", "[IMAGE]", image
)
elseif event_type == "network" then
local network_name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
local container_id = v.Actor and v.Actor.Attributes and v.Actor.Attributes.container or ""
-- 预计算显示值
local short_container = #container_id > 12 and container_id:sub(1, 12) or container_id
logs = logs .. string.format(
"%s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n\n",
readable_time,
"", "[TYPE]", event_type,
"", "[ACTION]", action,
"", "[NETWORK]", network_name,
"", "[CONTAINER]", short_container
)
elseif event_type == "image" then
local image_name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
logs = logs .. string.format(
"%s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n\n",
readable_time,
"", "[TYPE]", event_type,
"", "[ACTION]", action,
"", "[IMAGE]", image_name
)
elseif event_type == "volume" then
local volume_name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
logs = logs .. string.format(
"%s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n\n",
readable_time,
"", "[TYPE]", event_type,
"", "[ACTION]", action,
"", "[VOLUME]", volume_name
)
else
-- 未知类型
local actor_id = v.Actor and v.Actor.ID or "unknown"
local short_id = #actor_id > 12 and actor_id:sub(1, 12) or actor_id
logs = logs .. string.format(
"%s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n" ..
"%-24s %-10s %-15s\n\n",
readable_time,
"", "[TYPE]", event_type,
"", "[ACTION]", action,
"", "[ID]", short_id
)
end
end
end
end
end
-- 如果最终logs仍为空
if logs == "" then
logs = "[INFO] No events received in the last 24 hours.\n"
end
luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}})
end
@@ -615,4 +756,4 @@ function untag_image(tag)
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
end
end
end
@@ -4,9 +4,50 @@
<div class="cbi-section">
<h3><%:Events%></h3>
<% end %>
<%
-- 安全处理:确保syslog存在且为字符串
local log_content = self.syslog or ""
local row_count = 2
-- 安全检查:只有当log_content非空时才计算行数
if log_content and log_content ~= "" then
local newline_count = 0
for _ in string.gmatch(log_content, "\n") do
newline_count = newline_count + 1
end
row_count = newline_count + 2
if row_count < 5 then
row_count = 5
end
if row_count > 50 then
row_count = 50
end
end
%>
<div id="content_syslog">
<textarea readonly="readonly" wrap="off" rows="<%=self.syslog:cmatch('\n')+2%>" id="syslog"><%=self.syslog:pcdata()%></textarea>
<% if log_content == "" then %>
<!-- 当没有事件日志时显示提示信息 -->
<p style="color: #888; padding: 10px;"><%:No events found in the last 24 hours.%></p>
<% elseif string.find(log_content, "^%[ERROR%]") then %>
<!-- Docker连接错误:显示红色错误信息 -->
<div style="color: #c00; padding: 10px; background: #fee; border: 1px solid #c00; border-radius: 4px;">
<%:Docker Error:%> <%=log_content:pcdata()%>
</div>
<% elseif string.find(log_content, "^%[DEBUG%]") then %>
<!-- 调试信息:显示原始响应数据 -->
<p style="color: #f90; padding: 10px;"><%:Debug Info - Raw Response:%></p>
<textarea readonly="readonly" wrap="off" rows="<%=row_count%>" id="syslog"><%=log_content:pcdata()%></textarea>
<% elseif string.find(log_content, "^%[INFO%]") then %>
<!-- 信息提示:显示友好提示 -->
<p style="color: #888; padding: 10px;"><%:No events found in the last 24 hours.%></p>
<% else %>
<!-- 正常显示事件日志 -->
<textarea readonly="readonly" wrap="off" rows="<%=row_count%>" id="syslog"><%=log_content:pcdata()%></textarea>
<% end %>
</div>
<% if self.title == "Events" then %>
</div>
<%+footer%>
@@ -96,6 +96,7 @@ if load_urltest_options then -- [[ URLTest Start ]]
o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, <a target='_blank' href='https://sing-box.sagernet.org/configuration/outbound/urltest'>document</a>"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
o.cast = "table"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
@@ -106,30 +107,6 @@ if load_urltest_options then -- [[ URLTest Start ]]
end
end
end
-- 读取旧 DynamicList
function o.cfgvalue(self, section)
return table.concat(m:get(section, "urltest_node") or {}, " ")
end
-- 写入保持 DynamicList
function o.write(self, section, value)
local old = m:get(section, "urltest_node") or {}
local new, set = {}, {}
for v in value:gmatch("%S+") do
new[#new + 1] = v
set[v] = 1
end
for _, v in ipairs(old) do
if not set[v] then
m:set(section, "urltest_node", new)
return
end
set[v] = nil
end
for _ in pairs(set) do
m:set(section, "urltest_node", new)
return
end
end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
@@ -90,6 +90,7 @@ if load_balancing_options then -- [[ Load balancing Start ]]
o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, <a target='_blank' href='https://xtls.github.io/config/routing.html#balancerobject'>document</a>"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
o.cast = "table"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
@@ -100,30 +101,6 @@ if load_balancing_options then -- [[ Load balancing Start ]]
end
end
end
-- 读取旧 DynamicList
function o.cfgvalue(self, section)
return table.concat(m:get(section, "balancing_node") or {}, " ")
end
-- 写入保持 DynamicList
function o.write(self, section, value)
local old = m:get(section, "balancing_node") or {}
local new, set = {}, {}
for v in value:gmatch("%S+") do
new[#new + 1] = v
set[v] = 1
end
for _, v in ipairs(old) do
if not set[v] then
m:set(section, "balancing_node", new)
return
end
set[v] = nil
end
for _ in pairs(set) do
m:set(section, "balancing_node", new)
return
end
end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
@@ -36,7 +36,7 @@ o = s:option(Flag, "firewall_allow", translate("Firewall Allow"))
o.default = "0"
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
@@ -50,7 +50,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -79,7 +79,8 @@ o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(DynamicList, "users", translate("User"))
o = s:option(MultiValue, "users", translate("User"))
o.cast = "table"
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
@@ -535,7 +536,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
@@ -46,7 +46,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -72,7 +72,8 @@ o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(DynamicList, "users", translate("User"))
o = s:option(MultiValue, "users", translate("User"))
o.cast = "table"
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
@@ -521,7 +522,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
@@ -41,7 +41,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -79,7 +79,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
@@ -54,7 +54,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -110,7 +110,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
@@ -31,7 +31,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -45,7 +45,8 @@ o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(DynamicList, "users", translate("User"))
o = s:option(MultiValue, "users", translate("User"))
o.cast = "table"
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
@@ -168,7 +169,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
+25 -2
View File
@@ -1542,11 +1542,18 @@ function set_type_cbi(s)
obj.option_prefix = s_self.option_prefix
obj.option = s_self.option_prefix .. option
obj.cfgvalue = function(self, section)
local v
if self.rewrite_option then
return self.map:get(section, self.rewrite_option)
v = self.map:get(section, self.rewrite_option)
else
return self.map:get(section, self.config_option)
v = self.map:get(section, self.config_option)
end
if util.instanceof(self, cbi.MultiValue) then
if v and self.cast == "table" then
return table.concat(v, " ")
end
end
return v
end
obj.write = function(self, section, value)
if s1.fields["type"]:formvalue(s_self.section) == s_self.type_name then
@@ -1564,6 +1571,21 @@ function set_type_cbi(s)
new_val = new_t
end
end
if util.instanceof(self, cbi.MultiValue) then
local new_t = {}
if type(value) == "table" then
new_t = table_remove_duplicates(value)
else
string.gsub(value, '[^' .. " " .. ']+', function(v)
new_t[#new_t + 1] = v
end)
end
if self.cast == "string" then
new_val = table.concat(new_t, " ")
else
new_val = new_t
end
end
if self.rewrite_option then
self.map:set(section, self.rewrite_option, new_val)
else
@@ -1619,6 +1641,7 @@ end
function type_cbi_section(s, s2)
for i, v in ipairs(s2.children) do
local o = s2.children[i]
o.section = s
s:append(o)
s.fields[o.option] = o
end
@@ -99,6 +99,7 @@ if load_urltest_options then -- [[ URLTest Start ]]
o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, <a target='_blank' href='https://sing-box.sagernet.org/configuration/outbound/urltest'>document</a>"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
o.cast = "table"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
@@ -109,28 +110,6 @@ if load_urltest_options then -- [[ URLTest Start ]]
end
end
end
function o.cfgvalue(self, section)
return table.concat(m:get(section, "urltest_node") or {}, " ")
end
function o.write(self, section, value)
local old = m:get(section, "urltest_node") or {}
local new, set = {}, {}
for v in value:gmatch("%S+") do
new[#new + 1] = v
set[v] = 1
end
for _, v in ipairs(old) do
if not set[v] then
m:set(section, "urltest_node", new)
return
end
set[v] = nil
end
for _ in pairs(set) do
m:set(section, "urltest_node", new)
return
end
end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
@@ -91,6 +91,7 @@ if load_balancing_options then -- [[ Load balancing Start ]]
o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, <a target='_blank' href='https://xtls.github.io/config/routing.html#balancerobject'>document</a>"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
o.cast = "table"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
@@ -101,28 +102,6 @@ if load_balancing_options then -- [[ Load balancing Start ]]
end
end
end
function o.cfgvalue(self, section)
return table.concat(m:get(section, "balancing_node") or {}, " ")
end
function o.write(self, section, value)
local old = m:get(section, "balancing_node") or {}
local new, set = {}, {}
for v in value:gmatch("%S+") do
new[#new + 1] = v
set[v] = 1
end
for _, v in ipairs(old) do
if not set[v] then
m:set(section, "balancing_node", new)
return
end
set[v] = nil
end
for _ in pairs(set) do
m:set(section, "balancing_node", new)
return
end
end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
+3 -1
View File
@@ -10,7 +10,7 @@ Write narrow `[data-page]`/class-scoped overrides with native declarations, stil
The `@reference` + `@apply` route still _compiles_ for theme-repo patches and has real upsides — build-time validation (a typo'd utility fails the build; a typo'd `var()` fails silently at runtime) and the shared `dark:`/`md:`/`hover:` vocabulary — native is the default for the size numbers above, not a hard gate. App-shipped patches bypass the build entirely and have always been plain-CSS-only.
Globally-applicable chrome tweaks (e.g. icon opacity) belong in `_shared.css`, not here.
Before writing a patch, ask whether the symptom is really page-specific. Something that breaks the same way on any app's page is a theme bug: fix it in the core CSS so every page gets the fix, and keep globally-applicable chrome tweaks (e.g. icon opacity) in `_shared.css`, not here. A patch is for what is genuinely local to one app — or, for a preference the theme should not adopt for everyone, the global `admin.css` case below.
## Discovery and matching
@@ -20,6 +20,8 @@ Globally-applicable chrome tweaks (e.g. icon opacity) belong in `_shared.css`, n
Because discovery is at render time, **any package — not just the theme — may drop a `<page-prefix>.css` into `luci-static/shadcn/patches/`** and it takes effect immediately, no theme rebuild required.
**`patches/admin.css` is therefore the global patch.** Every admin page's path starts with `admin`, so the shortest prefix matches all of them; sorting loads it first and the page-specific patches cascade on top (the login page loads no patches at all). That is how a user carries a preference the theme should not ship for everyone — a CJK-friendly monospace UI font, say — without forking it: write the file by hand on the device at `/www/luci-static/shadcn/patches/admin.css`. The theme neither builds nor installs that path, so a theme upgrade leaves it alone.
`PATCH_ALIASES` in `vite.config.ts` can duplicate one built payload (CSS and JS) under several page names when unrelated pages share it. It is currently empty — the log viewer ships only `patches/admin-status-logs.js`, whose prefix covers both log tabs on every supported release; its CSS is core-page styling and lives in `components/_syslog.css` inside `main.css`, where the `.syslog-view` markup contract is deliberately global so other packages can reuse the viewer. `_`-prefixed files in `patches/` are `@import`-only fragments, never entries.
## JS payloads