🎉 Sync 2026-10-01 14:45:51

This commit is contained in:
github-actions[bot] committed 2026-10-01 14:45:51 +08:00
1 parent f1518a803c
commit 620e59fee5
124 files changed
+21594 -1

No files matched your search

@@ -0,0 +1,65 @@
{
"admin/system/liquid/save_config": {
"title": "Liquid Config",
"order": 100,
"action": {
"type": "function",
"module": "luci.controller.liquid",
"function": "act_save_config"
}
},
"admin/system/liquid/version": {
"title": "Liquid Version",
"order": 101,
"action": {
"type": "function",
"module": "luci.controller.liquid",
"function": "act_version"
}
},
"admin/system/liquid/detect_pkgmgr": {
"title": "Liquid OTA Detect PkgMgr",
"order": 102,
"action": {
"type": "function",
"module": "luci.controller.liquid",
"function": "act_detect_pkgmgr"
}
},
"admin/system/liquid/ota_download": {
"title": "Liquid OTA Download",
"order": 103,
"action": {
"type": "function",
"module": "luci.controller.liquid",
"function": "act_ota_download"
}
},
"admin/system/liquid/ota_download_progress": {
"title": "Liquid OTA Download Progress",
"order": 104,
"action": {
"type": "function",
"module": "luci.controller.liquid",
"function": "act_ota_download_progress"
}
},
"admin/system/liquid/ota_install": {
"title": "Liquid OTA Install",
"order": 105,
"action": {
"type": "function",
"module": "luci.controller.liquid",
"function": "act_ota_install"
}
},
"admin/system/liquid/ota_clear": {
"title": "Liquid OTA Clear",
"order": 106,
"action": {
"type": "function",
"module": "luci.controller.liquid",
"function": "act_ota_clear"
}
}
}
@@ -0,0 +1,271 @@
'use strict';
/* Liquid theme: save theme config (mode / accent / bing) directly.
Called via XHR POST to /cgi-bin/luci/admin/system/liquid/save_config
(like luci-app-pushbot's save_config), bypassing the uci rpc which is
unreliable across LuCI versions. Writes /etc/config/liquid with fs
(system()/uci CLI are not available in the ucode dispatcher sandbox). */
import * as fs from 'fs';
import { popen } from 'fs';
return {
act_version: function() {
let ver = "";
/* mtime 校验 + /tmp 缓存(与 header/footer 模板同一套机制),避免每次 fork */
let db_file = '/lib/apk/db/installed';
let db_mtime = 0;
let st = fs.stat(db_file);
if (st && st.type == 'file') {
db_mtime = st.mtime;
} else {
db_file = '/usr/lib/opkg/status';
st = fs.stat(db_file);
if (st && st.type == 'file')
db_mtime = st.mtime;
}
if (db_mtime > 0) {
let cm = match(fs.readfile('/tmp/liquid-version.cache') ?? '', /^(\d+) (.*)$/);
if (cm !== null && sprintf('%d', db_mtime) == cm[1])
ver = cm[2];
}
if (ver == "") {
/* apk (OpenWrt 24.10+): /lib/apk/db/installed */
let f = popen("awk '/^P:luci-theme-liquid$/{f=1;next} f&&/^V:/{print substr($0,3);exit}' " + db_file + " 2>/dev/null", "r");
if (f) { ver = replace(f.read("all"), /\s+/, ""); f.close(); }
/* opkg (legacy): /usr/lib/opkg/status */
if (ver == "") {
f = popen("awk '/^Package: luci-theme-liquid$/{f=1;next} f&&/^Version:/{print $2;exit}' /usr/lib/opkg/status 2>/dev/null", "r");
if (f) { ver = replace(f.read("all"), /\s+/, ""); f.close(); }
}
if (ver != "" && db_mtime > 0)
fs.writefile('/tmp/liquid-version.cache', sprintf('%d %s', db_mtime, ver));
}
http.prepare_content("application/json");
http.write_json({ version: ver });
},
act_save_config: function() {
http.prepare_content("application/json");
let body;
try { body = http.content(); } catch { body = null; }
if (!body) {
http.write_json({ ok: false, error: "no data" });
return;
}
let data;
try { data = json(body); } catch { data = null; }
if (type(data) != 'object') {
http.write_json({ ok: false, error: "invalid json" });
return;
}
/* 白名单字段 + 值校验 */
let writes = {};
for (let k in data) {
let v = sprintf("%s", data[k]);
if (k == 'mode' && match(v, /^(light|dark|auto)$/))
writes['mode'] = v;
else if (k == 'accent' && match(v, /^(blue|magenta|amber|purple|lime|custom)$/))
writes['accent'] = v;
else if (k == 'accent_custom' && match(v, /^#[0-9a-fA-F]{6}$/))
writes['accent_custom'] = v;
else if (k == 'bing' && match(v, /^(0|1)$/))
writes['bing'] = v;
else if (k == 'glass_opacity') {
let n = int(v, 10);
if (n >= 0 && n <= 100)
writes['glass_opacity'] = sprintf('%d', n);
}
}
let cfg_path = '/etc/config/liquid';
let cfg = fs.readfile(cfg_path) ?? '';
let lines = split(cfg, '\n');
let out = [];
if (!match(cfg, /config theme/))
push(out, "config theme 'theme'");
let inSection = false;
for (let i = 0; i < length(lines); i++) {
let line = lines[i];
if (match(line, /^config/))
inSection = true;
let m = match(line, /^[ \t]*option[ \t]+([a-z_]+)[ \t]+/);
if (inSection && m !== null && m[1] in writes) {
push(out, sprintf("\toption %s '%s'", m[1], writes[m[1]]));
delete writes[m[1]];
continue;
}
/* 丢弃纯空行(避免历史累积空行污染) */
if (trim(line) == '')
continue;
push(out, line);
}
/* 追加尚未出现的 option */
for (let k in writes)
push(out, sprintf("\toption %s '%s'", k, writes[k]));
fs.writefile(cfg_path, join('\n', out) + '\n');
http.write_json({ ok: true, saved: true });
},
/* ── OTA: detect package manager (apk vs opkg),与 pushbot 同款 ── */
act_detect_pkgmgr: function() {
let mgr = "opkg";
let f = popen("command -v apk 2>/dev/null", "r");
if (f) { let o = f.read("all"); f.close(); if (o && length(replace(o, /\s+/, "")) > 0) mgr = "apk"; }
http.prepare_content("application/json");
http.write_json({ pkgmgr: mgr });
},
/* ── OTA: 后台下载 Release 包(tag 方案 luci-theme-liquid-vX.Y-rN),与 pushbot 同款重试逻辑 ── */
act_ota_download: function() {
let ver = http.formvalue("ver") ?? "";
let rel = http.formvalue("rel") ?? "";
if (ver == "" || rel == "") {
http.prepare_content("application/json");
http.write_json({ ok: false, error: "missing ver/rel" });
return;
}
/* 防注入:只放行数字与点 */
ver = replace(ver, /[^0-9.]/g, "");
rel = replace(rel, /[^0-9]/g, "");
if (ver == "" || rel == "") {
http.prepare_content("application/json");
http.write_json({ ok: false, error: "invalid ver/rel" });
return;
}
/* 检测包管理器 */
let mgr = "opkg";
let f0 = popen("command -v apk 2>/dev/null", "r");
if (f0) { let o = f0.read("all"); f0.close(); if (o && length(replace(o, /\s+/, "")) > 0) mgr = "apk"; }
/* 组装下载地址(主题无 po/i18n 子包,单文件) */
let base = "https://github.com/zzsj0928/luci-theme-liquid/releases/download/luci-theme-liquid-v" + ver + "-r" + rel + "/";
let files;
if (mgr == "apk") {
files = ["luci-theme-liquid-" + ver + "-r" + rel + ".apk"];
} else {
files = ["luci-theme-liquid_" + ver + "-r" + rel + "_all.ipk"];
}
/* 进度文件 */
let pfile = "/tmp/liquid/ota_progress";
system("mkdir -p /tmp/liquid && echo '0' > " + pfile + " 2>/dev/null");
/* 后台下载脚本:每个文件最多重试 3 次,>10KB 视为有效 */
let dl_script = "#!/bin/sh\n"
+ "PFILE='" + pfile + "'\n"
+ "BASE='" + base + "'\n"
+ "MAX_RETRY=3\n"
+ "TOTAL=" + length(files) + "\n"
+ "OK=0\n"
+ "for f in " + join(" ", files) + "; do\n"
+ " URL=\"${BASE}${f}\"\n"
+ " DEST=\"/tmp/${f}\"\n"
+ " ATTEMPT=0\n"
+ " while [ $ATTEMPT -lt $MAX_RETRY ]; do\n"
+ " ATTEMPT=$((ATTEMPT+1))\n"
+ " curl -k -Lf --connect-timeout 15 --max-time 120 -o \"${DEST}\" \"${URL}\" 2>/dev/null\n"
+ " if [ $? -eq 0 ] && [ -s \"${DEST}\" ] && [ $(wc -c < \"${DEST}\") -gt 10000 ]; then\n"
+ " OK=$((OK+1))\n"
+ " echo \"$((OK * 100 / TOTAL))\" > \"${PFILE}\"\n"
+ " [ $OK -lt $TOTAL ] && sleep 1\n"
+ " break\n"
+ " fi\n"
+ " rm -f \"${DEST}\"\n"
+ " sleep 2\n"
+ " done\n"
+ "done\n"
+ "if [ $OK -eq $TOTAL ]; then\n"
+ " sleep 1\n"
+ " echo 'done' > \"${PFILE}\"\n"
+ "else\n"
+ " echo 'fail' > \"${PFILE}\"\n"
+ "fi\n";
fs.writefile("/tmp/liquid/ota_download.sh", dl_script);
system("chmod +x /tmp/liquid/ota_download.sh && /tmp/liquid/ota_download.sh &");
http.prepare_content("application/json");
http.write_json({ ok: true });
},
/* ── OTA: 轮询下载进度(0-100 / done / fail) ── */
act_ota_download_progress: function() {
let pfile = "/tmp/liquid/ota_progress";
let progress = "0";
let f = popen("cat " + pfile + " 2>/dev/null || echo '0'", "r");
if (f) { progress = replace(f.read("all"), /\s+/, ""); f.close(); }
if (progress == "") progress = "0";
http.prepare_content("application/json");
http.write_json({ progress: progress });
},
/* ── OTA: 安装已下载的包(apk/opkg 双支持) ── */
act_ota_install: function() {
let mgr = "opkg";
let f0 = popen("command -v apk 2>/dev/null", "r");
if (f0) { let o = f0.read("all"); f0.close(); if (o && length(replace(o, /\s+/, "")) > 0) mgr = "apk"; }
let ifile = "/tmp/liquid/ota_install.log";
let trust = "";
let cmd;
if (mgr == "apk") {
/* 装前检测 Zed 自签公钥:无则写入(添加不覆盖),随后安装
免 --allow-untrusted(老设备/新用户首次走 OTA 时设备上还没
有公钥,靠这步建立信任;包内也带同一公钥
root/etc/apk/keys/zed-openwrt-apk.pem,装完即常驻信任列表,
仅装本主题的用户同样获得信任)。 */
trust = "[ -f /etc/apk/keys/zed-openwrt-apk.pem ] || { mkdir -p /etc/apk/keys; "
+ "printf '%s" + "\\n" + "' '-----BEGIN PUBLIC KEY-----' "
+ "'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE16+nzzY9Lx5wvzZoWs/18vZxsNZD' "
+ "'jv+CqECJLUj+fA7J228Iu13DVUO8CK9jQyLHtqkw0f4/X2bKLlLiz281zQ==' "
+ "'-----END PUBLIC KEY-----' > /etc/apk/keys/zed-openwrt-apk.pem; }; ";
cmd = "apk add /tmp/luci-theme-liquid-*.apk";
} else {
/* opkg 同版本会 up to date 跳过,需 --force-reinstall 覆盖 */
cmd = "opkg install --force-reinstall /tmp/luci-theme-liquid_*.ipk";
}
/* 后台安装 + 结果标记(postinst 自动清 luci 缓存并 reload rpcd)。
world 哈希锁清理【装前 + 装后各一次】:
- 装前:即使安装链中途被杀(重启/OOM),锁也已经清掉,
不会留下"中毒 world 卡死后续所有 apk 事务"的状态
- 装后:本次安装(本地文件)自己又会写一把新锁,再清一次
降级为裸包名(语义等价"保持安装",且这些包不在官方源、
无被替换风险)。
整链必须放进单个 ( ... ) & 后台执行:否则 system() 会同步
等安装结束,阻塞 rpcd 处理器(全站请求卡住)。 */
let heal = "[ -f /etc/apk/world ] && sed -i '/></ s/>.*$//' /etc/apk/world; ";
let install_cmd = "( "
+ trust
+ heal
+ cmd + " > " + ifile + " 2>&1; "
+ "RC=$?; "
+ heal
+ "if [ $RC -eq 0 ]; then echo 'ok' >> " + ifile + "; "
+ "else echo 'fail' >> " + ifile + "; fi ) &";
system("mkdir -p /tmp/liquid && " + install_cmd);
http.prepare_content("application/json");
http.write_json({ ok: true, pkgmgr: mgr });
},
/* ── OTA: 清理已下载的包与进度文件 ── */
act_ota_clear: function() {
let patterns = [
"/tmp/luci-theme-liquid-*.apk",
"/tmp/luci-theme-liquid_*.ipk",
"/tmp/liquid/ota_progress",
"/tmp/liquid/ota_install.log",
"/tmp/liquid/ota_download.sh"
];
system("rm -f " + join(" ", patterns) + " 2>/dev/null");
http.prepare_content("application/json");
http.write_json({ ok: true });
}
};