mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 04:15:06 +08:00
Compare commits
2
Commits
d9c423b124
...
0ee799dbde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ee799dbde | ||
|
|
91e0df6c86 |
+1
-1
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=frp
|
||||
PKG_VERSION:=0.71.0
|
||||
PKG_RELEASE:=9
|
||||
PKG_RELEASE:=10
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/fatedier/frp/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -19,7 +19,6 @@ config init
|
||||
config conf 'common'
|
||||
option bind_addr '0.0.0.0'
|
||||
option bind_port '7000'
|
||||
|
||||
option authentication_method 'token'
|
||||
# option token 'your_token'
|
||||
# Alternatively, load a token from a file or command. Exec token sources
|
||||
@@ -32,12 +31,10 @@ config conf 'common'
|
||||
# list token_source_exec_args '--format'
|
||||
# list token_source_exec_args 'raw'
|
||||
# list token_source_exec_env 'TOKEN_SERVICE=production'
|
||||
|
||||
option max_pool_count '5'
|
||||
option tcp_mux 'true'
|
||||
option tls_force 'false'
|
||||
option detailed_errors_to_client 'true'
|
||||
|
||||
# Web server is disabled when admin_port is empty or 0.
|
||||
# option admin_addr '127.0.0.1'
|
||||
# option admin_port '7500'
|
||||
@@ -48,15 +45,12 @@ config conf 'common'
|
||||
option enable_prometheus 'false'
|
||||
# option admin_tls_cert_file '/etc/ssl/acme/example.com.fullchain.crt'
|
||||
# option admin_tls_key_file '/etc/ssl/acme/example.com.key'
|
||||
|
||||
# Allow ports can be single ports or ranges.
|
||||
# list allow_ports '2000-3000'
|
||||
# list allow_ports '3001'
|
||||
|
||||
option log_file 'console'
|
||||
option log_level 'info'
|
||||
option log_max_days '3'
|
||||
|
||||
# List options with name "_" will be directly appended as raw TOML lines.
|
||||
# Use this only for options not covered by UCI options above.
|
||||
# Do not duplicate keys generated by UCI options above.
|
||||
@@ -69,7 +63,6 @@ config conf 'common'
|
||||
# option path '/handler'
|
||||
# list ops 'Login'
|
||||
# option tls_verify 'false'
|
||||
|
||||
#config http_plugin 'port_manager'
|
||||
# option name 'port-manager'
|
||||
# option addr '127.0.0.1:9001'
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_kaiplus:=$(ARCH)
|
||||
|
||||
PKG_NAME:=kaiplus
|
||||
PKG_VERSION:=1.0.7
|
||||
PKG_RELEASE:=11
|
||||
PKG_VERSION:=1.0.8
|
||||
PKG_RELEASE:=12
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/kaiplus-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease-common-bin
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=29
|
||||
PKG_RELEASE:=30
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=af4346ed87096bc76261ca089d336e3724731806
|
||||
PKG_SOURCE_VERSION:=07a6d4293d28946ee8c39d0604985613c0c50e2d
|
||||
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=32
|
||||
PKG_RELEASE:=33
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=af4346ed87096bc76261ca089d336e3724731806
|
||||
PKG_SOURCE_VERSION:=07a6d4293d28946ee8c39d0604985613c0c50e2d
|
||||
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
@@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkeasefull
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=3.0.19
|
||||
PKG_RELEASE:=31
|
||||
PKG_RELEASE:=32
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
||||
PKG_HASH:=skip
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=af4346ed87096bc76261ca089d336e3724731806
|
||||
PKG_SOURCE_VERSION:=07a6d4293d28946ee8c39d0604985613c0c50e2d
|
||||
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
|
||||
|
||||
@@ -530,7 +530,7 @@ const commonTransportConf = [
|
||||
const commonTlsQuicConf = [
|
||||
[form.Flag, 'tls_force', _('Force TLS'),
|
||||
_('Only accept TLS-encrypted frpc connections.'),
|
||||
{ datatype: 'bool', default: 'false' }],
|
||||
{ datatype: 'bool', default: 'false', optional: false, rmempty: false }],
|
||||
|
||||
[form.Value, 'tls_cert_file', _('TLS certificate path'),
|
||||
_('Path to the TLS certificate file.'),
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=KaiPlus
|
||||
PKG_VERSION:=1.0.7
|
||||
PKG_RELEASE:=8
|
||||
PKG_VERSION:=1.0.8
|
||||
PKG_RELEASE:=9
|
||||
LUCI_DEPENDS:=+kaiplus +luci-compat +luci-lib-linkeaseauth
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
|
||||
|
||||
## [2.2.1] - 2026-09-10
|
||||
|
||||
本次发布为紧急热修版本,修复基本设置页面渲染崩溃、一键升级无效、设备配对异常处理缺陷,并改善 Web 控制台安全上下文与图片附件支持。
|
||||
|
||||
### 修复与优化
|
||||
|
||||
- **修复基本设置页面崩溃与升级失效 (#106)**:
|
||||
- 修复 `basic.lua` 中未定义 `service_ctl_url` 变量导致页面白屏崩溃的缺陷;
|
||||
- 绑定正确的请求端点,确保「一键升级 OpenClaw 核心」能够正常向后台发起升级事务请求。
|
||||
- **修复设备配对错误处理与假成功语义 (#106)**:
|
||||
- 控制器端严格校验 `devices list --json` 及 `devices approve` 的执行退出码与 JSON 解析结果,杜绝网关异常时伪装为 0 待配对成功;
|
||||
- 前端界面区分语义:仅当实际批准设备数大于 0 时提示成功并重新载入控制台,无待配对时仅展示信息提示,杜绝死循环重新载入。
|
||||
- **改善 Web 控制台安全上下文与图片附件支持 (#105)**:
|
||||
- 识别现代浏览器安全上下文限制(图片/附件与语音需 HTTPS 或 localhost),新增「访问条件提示(图片/附件与安全上下文)」面板及使用指引;
|
||||
- 支持配置反代 HTTPS 控制台地址(`option console_url`),并提供「新窗口直达」入口避免 Mixed Content 限制。
|
||||
- **契约测试与健壮性加固**:
|
||||
- 新增 `test_luci_basic_contract.sh`,在测试流水线中以 Lua 解释器真实执行 CBI 模型渲染,杜绝变量缺失;
|
||||
- 强化设备配对测试与控制台接入测试断言。
|
||||
|
||||
## [2.2.0] - 2026-09-06
|
||||
|
||||
本次发布全面适配 OpenClaw 2026.9.1 最新稳定版基线,完成安全升级事务、外部 Supervisor 治理、微信插件 2.4.8 与废弃特性清理。
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-openclaw
|
||||
PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0"))
|
||||
PKG_RELEASE:=27
|
||||
PKG_RELEASE:=28
|
||||
|
||||
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.2.0
|
||||
2.2.1
|
||||
|
||||
@@ -2396,14 +2396,26 @@ function action_devices_list()
|
||||
|
||||
local output, exit_code = run_openclaw_devices_cli("devices list --json")
|
||||
http.prepare_content("application/json")
|
||||
if not output then
|
||||
http.write_json({ status = "error", message = "执行失败", pending = {}, paired = {} })
|
||||
if not output or exit_code ~= 0 then
|
||||
local err_msg = "查询设备列表失败"
|
||||
if output and output:gsub("^%s+", ""):gsub("%s+$", "") ~= "" then
|
||||
err_msg = output:sub(1, 300)
|
||||
elseif type(exit_code) == "string" and exit_code ~= "" then
|
||||
err_msg = exit_code
|
||||
end
|
||||
http.write_json({
|
||||
status = "error",
|
||||
message = err_msg,
|
||||
exit_code = type(exit_code) == "number" and exit_code or -1,
|
||||
pending = {},
|
||||
paired = {}
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local json_str = output:match("({%s*\"pending\".*})") or output:match("({.*})")
|
||||
local data = json_str and json.parse(json_str) or nil
|
||||
if data and (data.pending or data.paired) then
|
||||
if type(data) == "table" and (data.pending ~= nil or data.paired ~= nil) then
|
||||
http.write_json({
|
||||
status = "ok",
|
||||
pending = data.pending or {},
|
||||
@@ -2411,7 +2423,8 @@ function action_devices_list()
|
||||
})
|
||||
else
|
||||
http.write_json({
|
||||
status = "ok",
|
||||
status = "error",
|
||||
message = "解析设备列表失败",
|
||||
pending = {},
|
||||
paired = {},
|
||||
raw = output:sub(1, 300)
|
||||
@@ -2432,9 +2445,38 @@ function action_devices_approve()
|
||||
|
||||
if approve_all then
|
||||
local list_out, list_code = run_openclaw_devices_cli("devices list --json")
|
||||
local json_str = list_out and (list_out:match("({%s*\"pending\".*})") or list_out:match("({.*})"))
|
||||
if not list_out or list_code ~= 0 then
|
||||
http.prepare_content("application/json")
|
||||
local err_msg = "查询设备列表失败"
|
||||
if list_out and list_out:gsub("^%s+", ""):gsub("%s+$", "") ~= "" then
|
||||
err_msg = list_out:sub(1, 300)
|
||||
elseif type(list_code) == "string" and list_code ~= "" then
|
||||
err_msg = list_code
|
||||
end
|
||||
http.write_json({
|
||||
status = "error",
|
||||
message = err_msg,
|
||||
exit_code = type(list_code) == "number" and list_code or -1,
|
||||
success_count = 0,
|
||||
fail_count = 0
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local json_str = list_out:match("({%s*\"pending\".*})") or list_out:match("({.*})")
|
||||
local data = json_str and json.parse(json_str) or nil
|
||||
local pending = (data and data.pending) or {}
|
||||
if type(data) ~= "table" or data.pending == nil then
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({
|
||||
status = "error",
|
||||
message = "解析设备列表失败",
|
||||
success_count = 0,
|
||||
fail_count = 0
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local pending = data.pending or {}
|
||||
|
||||
if #pending == 0 then
|
||||
http.prepare_content("application/json")
|
||||
@@ -2447,12 +2489,16 @@ function action_devices_approve()
|
||||
if rid and rid ~= "" and rid:match("^[a-zA-Z0-9%-_]+$") then
|
||||
local out, exit_code = run_openclaw_devices_cli("devices approve " .. shellquote(rid))
|
||||
local is_ok = (exit_code == 0) and (out and not out:match("No pending device") and not out:match("Error") and not out:match("ERROR"))
|
||||
local err_detail = out
|
||||
if (not err_detail or err_detail == "") and type(exit_code) == "string" then
|
||||
err_detail = exit_code
|
||||
end
|
||||
if is_ok or (out and out:lower():match("approved")) then
|
||||
success_count = success_count + 1
|
||||
table.insert(results, { requestId = rid, ok = true, message = out })
|
||||
else
|
||||
fail_count = fail_count + 1
|
||||
table.insert(results, { requestId = rid, ok = false, message = out or "批准失败" })
|
||||
table.insert(results, { requestId = rid, ok = false, message = err_detail or "批准失败" })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2464,12 +2510,16 @@ function action_devices_approve()
|
||||
end
|
||||
local out, exit_code = run_openclaw_devices_cli("devices approve " .. shellquote(request_id))
|
||||
local is_ok = (exit_code == 0) and (out and not out:match("No pending device") and not out:match("Error") and not out:match("ERROR"))
|
||||
local err_detail = out
|
||||
if (not err_detail or err_detail == "") and type(exit_code) == "string" then
|
||||
err_detail = exit_code
|
||||
end
|
||||
if is_ok or (out and out:lower():match("approved")) then
|
||||
success_count = 1
|
||||
table.insert(results, { requestId = request_id, ok = true, message = out })
|
||||
else
|
||||
fail_count = 1
|
||||
table.insert(results, { requestId = request_id, ok = false, message = out or "批准失败" })
|
||||
table.insert(results, { requestId = request_id, ok = false, message = err_detail or "批准失败" })
|
||||
end
|
||||
else
|
||||
http.prepare_content("application/json")
|
||||
|
||||
@@ -22,6 +22,7 @@ act = s3:option(DummyValue, "_actions")
|
||||
act.rawhtml = true
|
||||
act.cfgvalue = function(self, section)
|
||||
local ctl_url = luci.dispatcher.build_url("admin", "services", "openclaw", "service_ctl")
|
||||
local service_ctl_url = ctl_url
|
||||
local log_url = luci.dispatcher.build_url("admin", "services", "openclaw", "setup_log")
|
||||
local check_url = luci.dispatcher.build_url("admin", "services", "openclaw", "check_update")
|
||||
local uninstall_url = luci.dispatcher.build_url("admin", "services", "openclaw", "uninstall")
|
||||
|
||||
@@ -158,7 +158,12 @@ local pty_token = uci:get("openclaw", "main", "pty_token") or ""
|
||||
(new XHR()).get(devicesListUrl, null, function(x) {
|
||||
try {
|
||||
var data = JSON.parse(x.responseText);
|
||||
var pending = (data && data.pending) || [];
|
||||
if (!data || data.status !== 'ok') {
|
||||
if (advPairingBadge) advPairingBadge.textContent = '!';
|
||||
if (advPairingBanner) advPairingBanner.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
var pending = data.pending || [];
|
||||
if (advPairingBadge) advPairingBadge.textContent = pending.length;
|
||||
if (pending.length > 0) {
|
||||
var html = '';
|
||||
@@ -204,11 +209,19 @@ local pty_token = uci:get("openclaw", "main", "pty_token") or ""
|
||||
try {
|
||||
var res = JSON.parse(x.responseText);
|
||||
if (res.status === 'ok') {
|
||||
advPairingMsg.style.color = '#52c41a';
|
||||
advPairingMsg.innerHTML = '✅ ' + (res.message || '批准成功!');
|
||||
setTimeout(function() {
|
||||
checkAdvDevices();
|
||||
}, 1500);
|
||||
if ((res.success_count || 0) > 0) {
|
||||
advPairingMsg.style.color = '#52c41a';
|
||||
advPairingMsg.innerHTML = '✅ ' + (res.message || '批准成功!');
|
||||
setTimeout(function() {
|
||||
checkAdvDevices();
|
||||
}, 1500);
|
||||
} else {
|
||||
advPairingMsg.style.color = '#faad14';
|
||||
advPairingMsg.innerHTML = 'ℹ️ ' + (res.message || '当前没有待配对的设备请求');
|
||||
setTimeout(function() {
|
||||
checkAdvDevices();
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
advPairingMsg.style.color = '#cf222e';
|
||||
advPairingMsg.innerHTML = '❌ ' + (res.message || '批准失败');
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<%
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
local raw_console_url = tostring(uci:get("openclaw", "main", "console_url") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local https_console_url = ""
|
||||
if raw_console_url ~= "" and raw_console_url:match("^https://[a-zA-Z0-9%-%._~:/?#%[%]@!$&'()*+,;=]+$") then
|
||||
https_console_url = raw_console_url:gsub("/+$", "")
|
||||
end
|
||||
%>
|
||||
|
||||
<style type="text/css">
|
||||
@@ -128,8 +133,8 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
<button type="button" id="oc-console-pairing-toggle-btn" class="btn-action" title="管理/批准设备配对请求">📱 设备配对 <span id="oc-console-pairing-nav-badge" style="display:none; background:#ff4d4f; color:#fff; padding:0 6px; border-radius:10px; font-size:11px; font-weight:600; margin-left:2px;">0</span></button>
|
||||
<button type="button" id="oc-console-refresh-btn" class="btn-action" title="重新载入当前会话">🔄 刷新</button>
|
||||
<button type="button" id="oc-console-fullscreen-btn" class="btn-action" title="全屏查看">⛶ 全屏</button>
|
||||
<a id="oc-console-open-btn" class="btn-open" href="#" target="_blank" rel="noopener">
|
||||
↗ 新窗口打开
|
||||
<a id="oc-console-open-btn" class="btn-open" href="#" target="_blank" rel="noopener noreferrer" title="新窗口打开">
|
||||
↗ 新窗口直达
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,7 +167,23 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
</div>
|
||||
|
||||
<div id="oc-console-https-hint" style="display:none; padding:10px 14px; margin-bottom:14px; background:#fffbe6; border:1px solid #ffe58f; border-radius:6px; font-size:13px; color:#d48806; line-height:1.6;">
|
||||
⚠️ <strong>浏览器安全提示</strong>:当前正在通过 HTTPS 访问 LuCI。由于 OpenClaw 网关为 HTTP 服务,浏览器可能会拦截跨协议内嵌页面(Mixed Content)。若下方界面显示空白,请点击右上角「<strong>↗ 新窗口打开</strong>」直接访问会话,或改用 HTTP 协议访问 LuCI。
|
||||
⚠️ <strong>浏览器安全提示</strong>:当前正在通过 HTTPS 访问 LuCI。由于 OpenClaw 网关默认提供 HTTP 服务,浏览器可能会拦截跨协议内嵌页面(Mixed Content)。若下方界面显示空白,请配置 HTTPS 控制台地址,或点击右上角「<strong>↗ 新窗口直达</strong>」直接访问会话。
|
||||
</div>
|
||||
|
||||
<div id="oc-console-context-hint" style="display:none; padding:10px 14px; margin-bottom:14px; background:#e6f4ff; border:1px solid #91caff; border-left:4px solid #1677ff; border-radius:6px; font-size:13px; color:#0958d9; line-height:1.6;">
|
||||
<div style="font-weight:600; margin-bottom:4px; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px;">
|
||||
<span>💡 访问条件提示(图片/附件与安全上下文)</span>
|
||||
<a id="oc-console-direct-btn" href="#" target="_blank" rel="noopener noreferrer" class="btn-action" style="background:#fff; color:#0958d9; border-color:#91caff; font-weight:normal; text-decoration:none; display:inline-flex; align-items:center; gap:4px;">
|
||||
↗ 新窗口直达控制台
|
||||
</a>
|
||||
</div>
|
||||
<div id="oc-console-context-detail" style="font-size:12px; color:#333;">
|
||||
当前运行于非安全上下文(HTTP 局域网地址)。受现代浏览器安全策略(Secure Context)限制,图片粘贴、附件拖拽上传与麦克风/剪贴板权限可能受限。<br/>
|
||||
<strong>如需使用完整附件/图片与语音功能,建议:</strong><br/>
|
||||
1. <strong>HTTPS 控制台</strong>:配置反向代理并启用 HTTPS,在 UCI 中配置经过验证的 <code>option console_url 'https://...'</code>;<br/>
|
||||
2. <strong>本地安全访问</strong>:在路由器本机或通过 SSH 端口转发访问 <code>http://localhost:18789/</code>;<br/>
|
||||
3. <strong>新窗口直达</strong>:点击右上角或上方的「<strong>新窗口直达</strong>」在独立窗口运行。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oc-console-wrap" id="oc-console-wrap">
|
||||
@@ -178,6 +199,7 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
//<![CDATA[
|
||||
(function() {
|
||||
var gwPort = '<%=port%>';
|
||||
var uciHttpsConsoleUrl = '<%=https_console_url%>';
|
||||
var gwToken = '';
|
||||
var statusUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "status_api")%>';
|
||||
var tokenUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "get_token")%>';
|
||||
@@ -197,10 +219,34 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
function getConsoleUrl() {
|
||||
var host = window.location.hostname;
|
||||
var url = 'http://' + host + ':' + gwPort + '/';
|
||||
if (gwToken) url += '#token=' + encodeURIComponent(gwToken);
|
||||
if (uciHttpsConsoleUrl && uciHttpsConsoleUrl.indexOf('https://') === 0) {
|
||||
url = uciHttpsConsoleUrl;
|
||||
if (url.indexOf('?') === -1 && url.indexOf('#') === -1 && url.slice(-1) !== '/') {
|
||||
url += '/';
|
||||
}
|
||||
}
|
||||
if (gwToken) {
|
||||
url += (url.indexOf('#') === -1 ? '#' : '&') + 'token=' + encodeURIComponent(gwToken);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function updateContextHint(url) {
|
||||
var hintEl = document.getElementById('oc-console-context-hint');
|
||||
var directBtn = document.getElementById('oc-console-direct-btn');
|
||||
if (directBtn) directBtn.href = url;
|
||||
var isHttps = (url.indexOf('https://') === 0);
|
||||
var isSecure = (typeof window.isSecureContext !== 'undefined') ? window.isSecureContext : (location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1');
|
||||
|
||||
if (hintEl) {
|
||||
if (!isHttps && !isSecure && location.protocol !== 'https:') {
|
||||
hintEl.style.display = 'block';
|
||||
} else {
|
||||
hintEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullScreen() {
|
||||
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
|
||||
if (wrapEl.requestFullscreen) {
|
||||
@@ -227,7 +273,11 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
}
|
||||
|
||||
function checkAndLoad() {
|
||||
addrEl.textContent = window.location.hostname + ':' + gwPort;
|
||||
if (uciHttpsConsoleUrl && uciHttpsConsoleUrl.indexOf('https://') === 0) {
|
||||
addrEl.textContent = uciHttpsConsoleUrl;
|
||||
} else {
|
||||
addrEl.textContent = window.location.hostname + ':' + gwPort;
|
||||
}
|
||||
|
||||
(new XHR()).post(tokenUrl, { token: ocCsrfToken }, function(tx) {
|
||||
try {
|
||||
@@ -247,7 +297,8 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
statusTextEl.innerHTML = '<span style="color:#1a7f37;">● 网关运行中</span>';
|
||||
openBtn.href = url;
|
||||
actionsEl.style.display = 'flex';
|
||||
if (location.protocol === 'https:') {
|
||||
updateContextHint(url);
|
||||
if (location.protocol === 'https:' && url.indexOf('https://') !== 0) {
|
||||
var hintEl = document.getElementById('oc-console-https-hint');
|
||||
if (hintEl) hintEl.style.display = 'block';
|
||||
}
|
||||
@@ -331,7 +382,16 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
(new XHR()).get(devicesListUrl, null, function(x) {
|
||||
try {
|
||||
var data = JSON.parse(x.responseText);
|
||||
var pending = (data && data.pending) || [];
|
||||
if (!data || data.status !== 'ok') {
|
||||
if (pairingBadge) pairingBadge.textContent = '!';
|
||||
var errMsg = (data && data.message) || '查询设备列表失败';
|
||||
if (manualOpen) {
|
||||
pairingItems.innerHTML = '<div style="color:#cf222e; font-size:12px;">❌ ' + escapeHtml(errMsg) + '</div>';
|
||||
pairingBanner.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
var pending = data.pending || [];
|
||||
if (pairingBadge) pairingBadge.textContent = pending.length;
|
||||
var navBadge = document.getElementById('oc-console-pairing-nav-badge');
|
||||
if (navBadge) {
|
||||
@@ -396,14 +456,22 @@ local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
try {
|
||||
var res = JSON.parse(x.responseText);
|
||||
if (res.status === 'ok') {
|
||||
pairingMsg.style.color = '#52c41a';
|
||||
pairingMsg.innerHTML = '✅ ' + (res.message || '批准成功!') + ' 正在重新载入控制台...';
|
||||
setTimeout(function() {
|
||||
checkDevices(false);
|
||||
if (iframeEl) {
|
||||
iframeEl.src = getConsoleUrl();
|
||||
}
|
||||
}, 1500);
|
||||
if ((res.success_count || 0) > 0) {
|
||||
pairingMsg.style.color = '#52c41a';
|
||||
pairingMsg.innerHTML = '✅ ' + (res.message || '批准成功!') + ' 正在重新载入控制台...';
|
||||
setTimeout(function() {
|
||||
checkDevices(false);
|
||||
if (iframeEl) {
|
||||
iframeEl.src = getConsoleUrl();
|
||||
}
|
||||
}, 1500);
|
||||
} else {
|
||||
pairingMsg.style.color = '#faad14';
|
||||
pairingMsg.innerHTML = 'ℹ️ ' + (res.message || '当前没有待配对的设备请求');
|
||||
setTimeout(function() {
|
||||
checkDevices(false);
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
pairingMsg.style.color = '#cf222e';
|
||||
pairingMsg.innerHTML = '❌ ' + (res.message || '批准失败');
|
||||
|
||||
@@ -74,9 +74,18 @@ for f in "$CONSOLE" "$ADVANCED"; do
|
||||
grep -Fq "escapeHtml(item.requestId" "$f" || fail "$(basename "$f") must escape requestId"
|
||||
done
|
||||
|
||||
# ── 7. 控制器退出码与参数完整性契约 ──
|
||||
# ── 7. 控制器退出码与错误分支解析契约 ──
|
||||
grep -Fq "__EXIT" "$CONTROLLER" || fail "controller must check command exit code"
|
||||
grep -Fq "缺少 request_id 或 all 参数" "$CONTROLLER" || fail "controller must reject empty approve requests"
|
||||
grep -Fq "if not output or exit_code ~= 0 then" "$CONTROLLER" || fail "controller must reject non-zero exit_code in devices_list"
|
||||
grep -Fq "if not list_out or list_code ~= 0 then" "$CONTROLLER" || fail "controller must reject non-zero exit_code when querying devices list in approve_all"
|
||||
grep -Fq "解析设备列表失败" "$CONTROLLER" || fail "controller must report error on JSON parse failure in devices_list"
|
||||
|
||||
# ── 8. 前端状态与批准语义契约 ──
|
||||
for f in "$CONSOLE" "$ADVANCED"; do
|
||||
grep -Fq "success_count" "$f" || fail "$(basename "$f") must check success_count to prevent false reload on empty list"
|
||||
grep -Fq "data.status !==" "$f" || fail "$(basename "$f") must check data.status before consuming devices list"
|
||||
done
|
||||
|
||||
# ── 8. 换行符约束 (LF only) ──
|
||||
cr=$(printf '\r')
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
# LuCI 基本设置页面渲染与变量拼接契约测试。
|
||||
# 覆盖:
|
||||
# 1. service_ctl_url 与 ctl_url 变量定义完整性,避免 nil 拼接崩溃
|
||||
# 2. XHR post 请求 URL 必须非空且有效
|
||||
# 3. Lua 解释器环境下实际执行 act.cfgvalue() 渲染无报错
|
||||
# 4. LF 换行符约束
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
BASIC_MODEL="$REPO_ROOT/luasrc/model/cbi/openclaw/basic.lua"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -f "$BASIC_MODEL" ] || fail "missing $BASIC_MODEL"
|
||||
|
||||
# ── 1. 变量定义完整性 ──
|
||||
grep -Fq "local ctl_url =" "$BASIC_MODEL" || fail "basic.lua must define ctl_url"
|
||||
grep -Fq "local service_ctl_url =" "$BASIC_MODEL" || fail "basic.lua must define service_ctl_url"
|
||||
grep -Fq 'service_ctl_url' "$BASIC_MODEL" || fail "basic.lua must reference service_ctl_url"
|
||||
|
||||
# ── 2. Lua 运行时执行契约 ──
|
||||
lua_bin=""
|
||||
for cand in lua5.1 lua; do
|
||||
command -v "$cand" >/dev/null 2>&1 && { lua_bin="$cand"; break; }
|
||||
done
|
||||
|
||||
if [ -n "$lua_bin" ]; then
|
||||
"$lua_bin" -e '
|
||||
package.path = "./luasrc/?.lua;" .. package.path
|
||||
_G.luci = {
|
||||
sys = { exec = function() return "" end },
|
||||
dispatcher = {
|
||||
build_url = function(...) return "/" .. table.concat({...}, "/") end,
|
||||
context = { authtoken = "dummy-csrf-token" }
|
||||
}
|
||||
}
|
||||
package.loaded["luci.sys"] = _G.luci.sys
|
||||
package.loaded["luci.dispatcher"] = _G.luci.dispatcher
|
||||
_G.Map = function(...)
|
||||
return {
|
||||
section = function(...)
|
||||
return {
|
||||
option = function(self, opt, name)
|
||||
local o = {}
|
||||
if name == "_actions" then _G.act = o end
|
||||
return o
|
||||
end
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
_G.SimpleSection = {}
|
||||
_G.DummyValue = {}
|
||||
|
||||
dofile("'"$BASIC_MODEL"'")
|
||||
|
||||
if not _G.act or type(_G.act.cfgvalue) ~= "function" then
|
||||
error("act.cfgvalue function not exported")
|
||||
end
|
||||
|
||||
local rendered = _G.act.cfgvalue({}, {})
|
||||
if type(rendered) ~= "string" or #rendered < 1000 then
|
||||
error("cfgvalue rendered content too short or invalid type: " .. type(rendered))
|
||||
end
|
||||
|
||||
if not rendered:find("/admin/services/openclaw/service_ctl") then
|
||||
error("rendered output missing service_ctl endpoint URL")
|
||||
end
|
||||
|
||||
if rendered:find("post%(\"\",") or rendered:find("post%(undefined,") or rendered:find("post%(null,") then
|
||||
error("rendered output has empty or undefined XHR post URL")
|
||||
end
|
||||
' || fail "basic.lua failed to execute or render cleanly in Lua"
|
||||
fi
|
||||
|
||||
# ── 3. 换行符约束 (LF only) ──
|
||||
cr=$(printf '\r')
|
||||
if LC_ALL=C grep -q "$cr" "$BASIC_MODEL"; then
|
||||
fail "$(basename "$BASIC_MODEL") must use LF line endings"
|
||||
fi
|
||||
|
||||
echo "ok"
|
||||
@@ -10,7 +10,12 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -Fq "var url = 'http://' + host + ':' + gwPort + '/'" "$CONSOLE_VIEW" || fail "console view should force HTTP gateway URL"
|
||||
grep -Fq "https_console_url" "$CONSOLE_VIEW" || fail "console view should extract and validate https_console_url from UCI"
|
||||
grep -Fq "uciHttpsConsoleUrl" "$CONSOLE_VIEW" || fail "console view should expose uciHttpsConsoleUrl to JavaScript"
|
||||
grep -Fq "'http://' + host + ':' + gwPort" "$CONSOLE_VIEW" || fail "console view should fall back to HTTP host:port"
|
||||
grep -Fq "新窗口直达" "$CONSOLE_VIEW" || fail "console view should provide new window direct access"
|
||||
grep -Fq "访问条件提示" "$CONSOLE_VIEW" || fail "console view should provide access conditions hint for secure context"
|
||||
grep -Fq "isSecureContext" "$CONSOLE_VIEW" || fail "console view should check isSecureContext"
|
||||
grep -Fq "document.createElement('iframe')" "$CONSOLE_VIEW" || fail "console view should embed the OpenClaw UI in an iframe"
|
||||
grep -Fq "oc-console-iframe" "$CONSOLE_VIEW" || fail "console view should define oc-console-iframe"
|
||||
grep -Fq "allowfullscreen" "$CONSOLE_VIEW" || fail "console view should support fullscreen"
|
||||
|
||||
@@ -181,8 +181,11 @@ grep -q "yiwanai/gpt-5.5" root/usr/share/openclaw/oc-config.sh || fail "shell Yi
|
||||
grep -q "anthropic-compatible" root/usr/share/openclaw/oc-config.sh || fail "shell custom Anthropic provider missing"
|
||||
grep -q "anthropic-messages" root/usr/share/openclaw/oc-config.sh || fail "shell custom Anthropic provider mode missing"
|
||||
|
||||
grep -q "var url = 'http://'" luasrc/view/openclaw/console.htm || fail "console must force HTTP gateway URL"
|
||||
grep -q "uciHttpsConsoleUrl" luasrc/view/openclaw/console.htm || fail "console must support HTTPS console url"
|
||||
grep -q "var url = 'http://'" luasrc/view/openclaw/console.htm || fail "console must fallback to HTTP gateway URL"
|
||||
grep -q "新窗口直达" luasrc/view/openclaw/console.htm || fail "console must expose a new-window direct entry"
|
||||
grep -q "新窗口打开" luasrc/view/openclaw/console.htm || fail "console must expose a new-window entry"
|
||||
grep -q "访问条件提示" luasrc/view/openclaw/console.htm || fail "console must provide access condition hint"
|
||||
grep -q "document.createElement('iframe')" luasrc/view/openclaw/console.htm || fail "console must embed OpenClaw in an iframe"
|
||||
|
||||
grep -q "root/usr/libexec" scripts/build_ipk.sh || fail "ipk script must package shell helpers"
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.9.9
|
||||
PKG_RELEASE:=264
|
||||
PKG_RELEASE:=265
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -181,9 +181,7 @@ o:value("1:65535", translate("All"))
|
||||
o:depends("mode", "1")
|
||||
o.validate = port_validate
|
||||
|
||||
o = s:taboption("Main", DummyValue, "_hide_node_option", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o.value = "1"
|
||||
o = s:taboption("Main", HideValue, "_hide_node_option", "")
|
||||
o:depends("mode", "0")
|
||||
o:depends({ tcp_no_redir_ports = "1:65535", udp_no_redir_ports = "1:65535" })
|
||||
if TCP_NO_REDIR_PORTS == "1:65535" and UDP_NO_REDIR_PORTS == "1:65535" then
|
||||
@@ -208,19 +206,14 @@ if node_value then
|
||||
end
|
||||
current_node = current_node_id and m:get(current_node_id) or {}
|
||||
|
||||
o = s:taboption("Main", DummyValue, "node_save_before", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o = s:taboption("Main", HideValue, "node_save_before", "")
|
||||
o.value = current_node[".name"]
|
||||
o.cbid = function(self, section) return "node_save_before" end
|
||||
|
||||
o = s:taboption("Main", DummyValue, "_acl_node_bool", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o.value = "1"
|
||||
o = s:taboption("Main", HideValue, "_acl_node_bool", "")
|
||||
o:depends({ node = "", ['!reverse'] = true })
|
||||
|
||||
o = s:taboption("Main", DummyValue, "_diff_global_node", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o.value = "1"
|
||||
o = s:taboption("Main", HideValue, "_diff_global_node", "")
|
||||
if NODE ~= "" then
|
||||
o:depends({ node = NODE, ['!reverse'] = true })
|
||||
else
|
||||
@@ -319,14 +312,10 @@ o.template = m:template_path("/global/proxy")
|
||||
o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
||||
|
||||
-- Node → DNS Depends Settings
|
||||
o = s:taboption("Main", DummyValue, "_node_sel_shunt", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o.value = "1"
|
||||
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
|
||||
o:depends({ node = "__always__" })
|
||||
|
||||
o = s:taboption("Main", DummyValue, "_node_sel_other", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o.value = "1"
|
||||
o = s:taboption("Main", HideValue, "_node_sel_other", "")
|
||||
o:depends({ _node_sel_shunt = "1", ['!reverse'] = true })
|
||||
|
||||
---- DNS
|
||||
|
||||
@@ -162,25 +162,18 @@ o = s:taboption("Main", Flag, "node_socks_bind_local", translate("Node") .. " So
|
||||
o.default = "1"
|
||||
o:depends("_node", "1")
|
||||
|
||||
o = s:taboption("Main", DummyValue, "node_save_before", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o = s:taboption("Main", HideValue, "node_save_before", "")
|
||||
o.value = current_node[".name"]
|
||||
o.cbid = function(self, section) return "node_save_before" end
|
||||
|
||||
o = s:taboption("Main", DummyValue, "_node", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o.value = "1"
|
||||
o = s:taboption("Main", HideValue, "_node", "")
|
||||
o:depends({ node = "", ['!reverse'] = true })
|
||||
|
||||
-- Node → DNS Depends Settings
|
||||
o = s:taboption("Main", DummyValue, "_node_sel_shunt", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o.value = "1"
|
||||
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
|
||||
o:depends({ node = "__always__" })
|
||||
|
||||
o = s:taboption("Main", DummyValue, "_node_sel_other", "")
|
||||
o.template = m:template_path("/cbi/hidevalue")
|
||||
o.value = "1"
|
||||
o = s:taboption("Main", HideValue, "_node_sel_other", "")
|
||||
o:depends({ _node_sel_shunt = "1", ['!reverse'] = true })
|
||||
|
||||
-- [[ DNS Settings ]]--
|
||||
|
||||
@@ -1517,11 +1517,111 @@ function set_default_cbi()
|
||||
return cbi.AbstractValue.write(self, section, new_val)
|
||||
end
|
||||
end
|
||||
if true then
|
||||
--HideValue
|
||||
local HideValue = util.class(cbi.DummyValue)
|
||||
function HideValue.__init__(self, ...)
|
||||
cbi.DummyValue.__init__(self, ...)
|
||||
self.template = self.map:template_path("/cbi/hidevalue")
|
||||
self.value = "1"
|
||||
end
|
||||
cbi.HideValue = HideValue
|
||||
end
|
||||
end
|
||||
|
||||
function set_type_cbi(s)
|
||||
local cbi = require "luci.cbi"
|
||||
local s1 = s.parent
|
||||
function s.option(s_self, class, option, ...)
|
||||
local obj = class(s_self.map, s_self, option, ...)
|
||||
obj.config_option = option
|
||||
obj.option_prefix = s_self.option_prefix
|
||||
obj.option = s_self.option_prefix .. option
|
||||
obj.cfgvalue = function(self, section)
|
||||
if self.rewrite_option then
|
||||
return self.map:get(section, self.rewrite_option)
|
||||
else
|
||||
return self.map:get(section, self.config_option)
|
||||
end
|
||||
end
|
||||
obj.write = function(self, section, value)
|
||||
if s1.fields["type"]:formvalue(s_self.section) == s_self.type_name then
|
||||
local new_val = value
|
||||
if util.instanceof(self, cbi.DynamicList) then
|
||||
local new_t = {}
|
||||
if type(value) == "table" then
|
||||
new_t = table_remove_duplicates(value)
|
||||
else
|
||||
new_t = { value }
|
||||
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
|
||||
self.map:set(section, self.config_option, new_val)
|
||||
end
|
||||
end
|
||||
end
|
||||
obj.remove = function(self, section)
|
||||
if s1.fields["type"]:formvalue(s_self.section) == s_self.type_name then
|
||||
if self.rewrite_option then
|
||||
self.map:del(section, self.rewrite_option)
|
||||
else
|
||||
self.map:del(section, self.config_option)
|
||||
end
|
||||
end
|
||||
end
|
||||
obj.deplist2json = function(self, section, deplist)
|
||||
local deps, i, d = { }
|
||||
if type(self.deps) == "table" then
|
||||
if not next(self.deps) then
|
||||
self:depends({ type = s.type_name })
|
||||
end
|
||||
local list = deplist or self.deps
|
||||
for i, d in ipairs(list) do
|
||||
if s.type_name and not d["type"] then
|
||||
d["type"] = s.type_name
|
||||
end
|
||||
local a, k, v = { }
|
||||
for k, v in pairs(d) do
|
||||
if k:find("!", 1, true) then
|
||||
a[k] = v
|
||||
elseif k:find("^", 1, true) then
|
||||
a[k:sub(2)] = v
|
||||
elseif k:find(".", 1, true) then
|
||||
a['cbid%s' % k] = v
|
||||
elseif s_self.fields[k] then
|
||||
a['cbid.%s.%s.%s' %{ self.config, section, s_self.fields[k].option }] = v
|
||||
else
|
||||
a['cbid.%s.%s.%s' %{ self.config, section, k }] = v
|
||||
end
|
||||
end
|
||||
deps[#deps+1] = a
|
||||
end
|
||||
end
|
||||
return util.serialize_json(deps)
|
||||
end
|
||||
s_self:append(obj)
|
||||
s_self.fields[option] = obj
|
||||
return obj
|
||||
end
|
||||
end
|
||||
|
||||
function type_cbi_section(s, s2)
|
||||
for i, v in ipairs(s2.children) do
|
||||
local o = s2.children[i]
|
||||
s:append(o)
|
||||
s.fields[o.option] = o
|
||||
end
|
||||
end
|
||||
|
||||
function return_map(map)
|
||||
local cbi = require "luci.cbi"
|
||||
local api = require "luci.passwall.api"
|
||||
if true then
|
||||
-- header
|
||||
local header = cbi.Template(appname .. "/cbi/header")
|
||||
@@ -1537,91 +1637,6 @@ function return_map(map)
|
||||
return map
|
||||
end
|
||||
|
||||
function set_type_cbi(s)
|
||||
local cbi = require "luci.cbi"
|
||||
local s1 = s.parent
|
||||
function s.option(self, class, option, ...)
|
||||
local obj = class(self.map, self, option, ...)
|
||||
obj.config_option = option
|
||||
obj.option_prefix = self.option_prefix
|
||||
obj.option = self.option_prefix .. option
|
||||
obj.cfgvalue = function(o_self, section)
|
||||
if o_self.rewrite_option then
|
||||
return o_self.map:get(section, o_self.rewrite_option)
|
||||
else
|
||||
return o_self.map:get(section, o_self.config_option)
|
||||
end
|
||||
end
|
||||
obj.write = function(o_self, section, value)
|
||||
if s1.fields["type"]:formvalue(self.section) == self.type_name then
|
||||
local new_val = value
|
||||
if util.instanceof(o_self, cbi.DynamicList) then
|
||||
local new_t = {}
|
||||
if type(value) == "table" then
|
||||
new_t = table_remove_duplicates(value)
|
||||
else
|
||||
new_t = { value }
|
||||
end
|
||||
if o_self.cast == "string" then
|
||||
new_val = table.concat(new_t, " ")
|
||||
else
|
||||
new_val = new_t
|
||||
end
|
||||
end
|
||||
if o_self.rewrite_option then
|
||||
o_self.map:set(section, o_self.rewrite_option, new_val)
|
||||
else
|
||||
o_self.map:set(section, o_self.config_option, new_val)
|
||||
end
|
||||
end
|
||||
end
|
||||
obj.remove = function(o_self, section)
|
||||
if s1.fields["type"]:formvalue(self.section) == self.type_name then
|
||||
if o_self.rewrite_option then
|
||||
o_self.map:del(section, o_self.rewrite_option)
|
||||
else
|
||||
o_self.map:del(section, o_self.config_option)
|
||||
end
|
||||
end
|
||||
end
|
||||
obj.depends = function(o_self, field, value)
|
||||
local deps
|
||||
if type(field) == "string" then
|
||||
deps = {}
|
||||
deps[field] = value
|
||||
else
|
||||
deps = field
|
||||
end
|
||||
local new_deps = {}
|
||||
for k, v in pairs(deps) do
|
||||
local n_k = k
|
||||
if self.fields[k] then
|
||||
n_k = self.fields[k].option
|
||||
end
|
||||
new_deps[n_k] = v
|
||||
end
|
||||
new_deps["type"] = self.type_name
|
||||
if next(new_deps) then
|
||||
table.insert(o_self.deps, new_deps)
|
||||
end
|
||||
end
|
||||
self:append(obj)
|
||||
self.fields[option] = obj
|
||||
return obj
|
||||
end
|
||||
end
|
||||
|
||||
function type_cbi_section(s, s2)
|
||||
for i, v in ipairs(s2.children) do
|
||||
local o = s2.children[i]
|
||||
if #o.deps == 0 then
|
||||
o:depends({ type = s2.type_name })
|
||||
end
|
||||
s:append(o)
|
||||
s.fields[o.option] = o
|
||||
end
|
||||
end
|
||||
|
||||
function get_std_domain(domain)
|
||||
domain = trim(domain)
|
||||
if domain == "" then return "" end
|
||||
|
||||
@@ -337,10 +337,10 @@ run_socks() {
|
||||
[ -n "$config_file" ] && [ -z "$(echo ${config_file} | grep $TMP_PATH)" ] && config_file=$TMP_PATH/$config_file
|
||||
[ -n "$http_port" ] || http_port=0
|
||||
[ -n "$http_config_file" ] && [ -z "$(echo ${http_config_file} | grep $TMP_PATH)" ] && http_config_file=$TMP_PATH/$http_config_file
|
||||
if [ -n "$log_file" ] && [ "$log_file" != "/dev/null" ] && [ -z "$(echo ${log_file} | grep $TMP_PATH)" ]; then
|
||||
log_file=$TMP_PATH/$log_file
|
||||
else
|
||||
if [ -z "$log_file" ] || [ "$log_file" = "/dev/null" ]; then
|
||||
log_file="/dev/null"
|
||||
elif [ "${log_file#"$TMP_PATH/"}" = "$log_file" ]; then
|
||||
log_file=$TMP_PATH/$log_file
|
||||
fi
|
||||
|
||||
local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z')
|
||||
@@ -1580,8 +1580,9 @@ acl_app() {
|
||||
}
|
||||
local _redir_port=$(get_cache_var "node_${node}_redir_port")
|
||||
local _socks_port=$(get_cache_var "node_${node}_socks_port")
|
||||
local _enable_log=$(get_cache_var "node_${node}_enable_log")
|
||||
local _dns_port
|
||||
if [ -n "${_socks_port}" ] && [ -n "${_redir_port}" ]; then
|
||||
if [ -n "${_socks_port}" ] && [ -n "${_redir_port}" ] && [ "${_enable_log}" != "1" ] && [ "${log}" != "1" ]; then
|
||||
socks_port=${_socks_port}
|
||||
node_port=${_redir_port}
|
||||
_dns_port=$(get_cache_var "node_${node}_${dns_cache_key}")
|
||||
@@ -1593,7 +1594,10 @@ acl_app() {
|
||||
set_cache_var "node_${node}_redir_port" "${redir_port}"
|
||||
node_port=$redir_port
|
||||
local log_file="/dev/null"
|
||||
[ "${log}" = "1" ] && log_file="${TMP_ACL_PATH}/${sid}/node.log"
|
||||
[ "${log}" = "1" ] && {
|
||||
log_file="${TMP_ACL_PATH}/${sid}/node.log"
|
||||
set_cache_var "node_${node}_enable_log" "1"
|
||||
}
|
||||
|
||||
if [ "${type}" = "sing-box" ] || [ "${type}" = "xray" ]; then
|
||||
config_file="acl/${node}_${redir_port}.json"
|
||||
|
||||
@@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=LuCI shared OpenWrt auth bridge for LinkEase apps
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=1.0.1
|
||||
PKG_RELEASE:=4
|
||||
PKG_VERSION:=1.0.3
|
||||
PKG_RELEASE:=5
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
|
||||
FOOTSTRAP_VERSION?=
|
||||
ifneq ($(FOOTSTRAP_VERSION),)
|
||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||
PKG_RELEASE:=66
|
||||
PKG_RELEASE:=68
|
||||
endif
|
||||
|
||||
LUCI_TITLE:=Footstrap Theme
|
||||
|
||||
@@ -195,12 +195,66 @@ function holdFloor(records) {
|
||||
}
|
||||
if (!dirty.length) return;
|
||||
|
||||
/* THE SWEEP MAY NOT COST THE READER THE CLAMP IT EXISTS TO PREVENT — task resid. Clearing every
|
||||
* floor before the measure pass is what makes the answers honest (above), and for the length of
|
||||
* that pass the document stands without them. Measured live (chromium/owrt2512b @390 top normal,
|
||||
* /admin/network/dhcp, `../tmp/task-resid/dbg-before.json`): 38 sweeps, 33 of them took the
|
||||
* document DOWN between the clear and the write-back, and on 13 the offset went with it. Eight of
|
||||
* those 13 are this fault — the unscoped, every-box sweep, document 4730px to 4729px and straight
|
||||
* back to 4730px, offset 3886 to 3885 and NOT back. Nothing in this function is asynchronous and
|
||||
* `scrolling()` at the top already refused a reader who is moving, so an offset that is LOWER
|
||||
* after the write-back than before the clear was lowered by this pass and by nothing else — 0
|
||||
* sweeps of the 38 moved it the other way.
|
||||
*
|
||||
* One pixel, and it is not the pixel that matters: `lateDrift()` reads the offset twice,
|
||||
* SCROLL_IDLE apart, and treats any difference as "the reader has moved since" (its own comment
|
||||
* below). That 1px discarded the whole 60px correction the same tick's shrink was owed
|
||||
* (`late-refuse why: moving, seen 3886, now 3885`), and the unforced `rememberRest()` a
|
||||
* millisecond later adopted the wrong offset as the reference the NEXT refill measures against:
|
||||
* 59px off, carried forward, which is the 47-64px `REPEAT` reports on the second or third of
|
||||
* three back-to-back refills of one section (`tools/scroll-anchor.mjs`, docs/anchoring.md).
|
||||
*
|
||||
* ONLY WHERE THE SCROLLER IS AS TALL AGAIN AS IT WAS, which is what separates this pass's own
|
||||
* transient dip from a floor that came down because the CONTENT really shrank — the eight sweeps
|
||||
* above against the other five, on the same run, that lost 3958 to 3886 with the document
|
||||
* staying 120px shorter for good. That second clamp is real, it belongs to the shrink, and
|
||||
* `lateDrift()`'s `floorShrink` path already corrects for it; touching it here was measured too
|
||||
* — restoring unconditionally and letting the browser re-clamp the write is green on this cell
|
||||
* as well, but it also makes every genuine shrink's clamp this file's OWN write
|
||||
* (`sawOwnWrite()`), so the motion window that clamp used to open stops opening and
|
||||
* `sampleMotion()`'s terminal sweep stops running with it. The narrow form holds the cell on its
|
||||
* own, so the wider one does not ship.
|
||||
*
|
||||
* `writeOffset()` rather than a bare assignment: the restore is a scroll write like the two
|
||||
* corrections, and the motion sampler must read it as this file's own rather than as the reader
|
||||
* arriving. */
|
||||
const sc = scroller(), page = sc || document.documentElement;
|
||||
const at = scrollTop(), tall = page.scrollHeight;
|
||||
dirty.forEach((box) => { box.style.minHeight = ''; });
|
||||
dirty.forEach((box) => hs.push(box.offsetHeight));
|
||||
dirty.forEach((box, i) => {
|
||||
if (hs[i] > 0) { box.style.minHeight = hs[i] + 'px'; box.setAttribute('data-fs-floor', ''); }
|
||||
else box.removeAttribute('data-fs-floor');
|
||||
});
|
||||
/* AND WHERE IT IS A REAL SHRINK, SAY SO — task wk1440. The other side of the same test: the
|
||||
* offset came down and the scroller stayed shorter, so the box really did give height up and
|
||||
* the drop is the browser's clamp into it. `applyAnchor()` is the one thing that can put back
|
||||
* the part of the shrink the clamp did NOT take, and it refuses while `scrolling()` — which the
|
||||
* clamp's own scroll event has just made true. Measured (webkit/owrtsnapb @1440 side compact,
|
||||
* /admin/status/overview, `../tmp/task-wk1440/`, the engine's own anchoring ablated away with
|
||||
* `overflow-anchor: none` so the theme is the only corrector): a 120px pad removed above the
|
||||
* reader took the scroller 2793 -> 2673px while the offset clamped 1889 -> 1829 — 60px short of
|
||||
* the 1769 the reader needed, because a clamp only ever gives back what the document lost at its
|
||||
* BOTTOM. One frame later `applyAnchor()` read `scrolling() true, 400ms left` and returned with
|
||||
* a correct -60px correction in hand; the terminal sweep's `rememberRest()` then adopted 1829 as
|
||||
* the reference, and every refill after it measured 0px drift against ground that was already
|
||||
* 60px wrong — `3x repeat 0px/-60px/-60px`, the gate's "left the reader -60px off ... corrected
|
||||
* never". Recorded as a PIXEL, not as a flag or a timestamp, and read back the way
|
||||
* `sawOwnWrite()` reads its own: it stands only while the offset has not left it, so a reader
|
||||
* who really does move clears it by moving. */
|
||||
const landed = scrollTop();
|
||||
if (landed >= at) return;
|
||||
if (page.scrollHeight >= tall) writeOffset(sc, at); else _clampedTo = landed;
|
||||
}
|
||||
|
||||
/* ---- is the page moving right now? asked of the position, never of the events ----
|
||||
@@ -266,6 +320,22 @@ function sawOwnWrite(y) {
|
||||
_ownWrite = null;
|
||||
return true;
|
||||
}
|
||||
/* Where the browser's own clamp last put the offset down, set by `holdFloor()`'s real-shrink branch
|
||||
* and by nothing else — see the comment there for the measurement. NOT a second `_ownWrite`: this
|
||||
* marker does not touch `scrolling()`, which keeps answering "the page is moving, whoever moves it"
|
||||
* for the whole theme (task resid measured what happens when a clamp stops opening that window —
|
||||
* `sampleMotion()`'s terminal sweep stops running behind it). It answers ONE narrower question, for
|
||||
* `applyAnchor()` alone: is the motion that is blocking this correction the clamp the correction is
|
||||
* FOR? */
|
||||
let _clampedTo = null;
|
||||
function sawClamp() {
|
||||
/* the identical `scrollTop() !== seen` shape `lateDrift()` asks its own offset — the pixel was
|
||||
* read out of `scrollTop()` in the first place, so equality is the whole test, and `null` is
|
||||
* never equal to a number */
|
||||
if (scrollTop() === _clampedTo) return true;
|
||||
_clampedTo = null;
|
||||
return false;
|
||||
}
|
||||
/* The one place either correction may write the scroll position, so `_ownWrite` cannot go stale by a
|
||||
* write skipping it. Reads the offset back rather than trusting the argument: a write near either
|
||||
* end of the document is clamped by the browser, and the pixel that actually lands is the one the
|
||||
@@ -1081,8 +1151,19 @@ function applyAnchor(ref) {
|
||||
if (!ref) return;
|
||||
/* not into a moving page: the correction is scheduled from the mutation and applied a frame
|
||||
* later, and a reader who starts scrolling in between would be put back onto a page they have
|
||||
* already left */
|
||||
if (scrolling()) return;
|
||||
* already left.
|
||||
*
|
||||
* UNLESS THE MOTION IS THE CLAMP THIS CORRECTION EXISTS FOR — task wk1440, the same trap
|
||||
* `settleDeferredFloor()`'s own third attempt already fell into once ("gating on `scrolling()`
|
||||
* refuses on the very motion it is trying to observe") and `lateDrift()` was built to avoid.
|
||||
* A shrink above the reader clamps the offset down inside `holdFloor()`'s own synchronous pass,
|
||||
* that clamp dispatches a `scroll` event of its own, and one frame later this guard reads it as
|
||||
* a reader who started moving: measured on the cell this task closes, `apply-enter scrolling
|
||||
* true, moving 400ms, at 1829` with a -60px correction already computed and never written.
|
||||
* `sawClamp()` is the pixel `holdFloor()` watched the clamp land on, and it stands only while
|
||||
* the offset has not left it — a reader who really is scrolling has moved off it by definition,
|
||||
* so this reopens the guard for exactly one case and no other. */
|
||||
if (scrolling() && !sawClamp()) return;
|
||||
/* through scroller(), not a second probe: two copies of the same question can answer
|
||||
* differently within one frame */
|
||||
const sc = scroller();
|
||||
|
||||
+2
-2
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=natflow
|
||||
PKG_VERSION:=20260531
|
||||
PKG_RELEASE:=80
|
||||
PKG_RELEASE:=82
|
||||
|
||||
PKG_SOURCE:=$(PKG_VERSION).tar.xz
|
||||
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_VERSION:=1c38f7df1a17128ee15c2b27f5b3360ee3256cda
|
||||
PKG_SOURCE_VERSION:=ecd6455caed450157af8b59ffb616ef06a34ddb2
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
|
||||
@@ -16,7 +16,7 @@ include $(TOPDIR)/rules.mk
|
||||
include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=shortcut-fe
|
||||
PKG_RELEASE:=10
|
||||
PKG_RELEASE:=11
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
@@ -69,7 +69,6 @@ define Build/Compile
|
||||
$(KERNEL_MAKE_FLAGS) \
|
||||
$(PKG_MAKE_FLAGS) \
|
||||
M="$(PKG_BUILD_DIR)" \
|
||||
EXTRA_CFLAGS+="-DSFE_SUPPORT_IPV6" \
|
||||
SFE_SUPPORT_IPV6=y \
|
||||
modules
|
||||
endef
|
||||
|
||||
@@ -10,6 +10,8 @@ endif
|
||||
|
||||
obj-m += shortcut-fe-cm.o
|
||||
|
||||
ccflags-$(SFE_SUPPORT_IPV6) += -DSFE_SUPPORT_IPV6
|
||||
|
||||
shortcut-fe-objs := \
|
||||
sfe_ipv4.o
|
||||
|
||||
|
||||
+3
-3
@@ -8,12 +8,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=torrserver
|
||||
PKG_VERSION:=MatriX.144
|
||||
PKG_RELEASE:=20
|
||||
PKG_VERSION:=MatriX.144.2
|
||||
PKG_RELEASE:=21
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/YouROK/TorrServer.git
|
||||
PKG_SOURCE_VERSION:=c416a8f2b89b244c779f03f1f86e2f0f690c2668
|
||||
PKG_SOURCE_VERSION:=aebd3895424c4de6063a2d89c279211ae8c9ad59
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
||||
|
||||
Reference in New Issue
Block a user