Compare commits

...
2 Commits
Author SHA1 Message Date
github-actions[bot] 0ee799dbde 🍓 Sync 2026-09-11 01:35:11 2026-09-11 01:35:11 +08:00
github-actions[bot] 91e0df6c86 🐤 Sync 2026-09-10 23:45:40 2026-09-10 23:45:40 +08:00
31 changed files with 516 additions and 185 deletions
+1 -1
View File
@@ -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)?
-7
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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)
+2 -2
View File
@@ -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.'),
+2 -2
View 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
+19
View File
@@ -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 与废弃特性清理。
+1 -1
View File
@@ -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
View File
@@ -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') {
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() {
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,6 +456,7 @@ local port = uci:get("openclaw", "main", "port") or "18789"
try {
var res = JSON.parse(x.responseText);
if (res.status === 'ok') {
if ((res.success_count || 0) > 0) {
pairingMsg.style.color = '#52c41a';
pairingMsg.innerHTML = '✅ ' + (res.message || '批准成功!') + ' 正在重新载入控制台...';
setTimeout(function() {
@@ -404,6 +465,13 @@ local port = uci:get("openclaw", "main", "port") or "18789"
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"
+1 -1
View File
@@ -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 ]]--
+101 -86
View File
@@ -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"
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+1 -2
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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>