🎨 Sync 2026-06-24 20:46:52

This commit is contained in:
github-actions[bot] committed 2026-06-24 20:46:52 +08:00
1 parent 668e1f1cb8
commit 7e9cb31284
22 files changed
+642 -145

No files matched your search

+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=gowebdav
PKG_VERSION:=5.11.10
PKG_RELEASE:=13
PKG_VERSION:=5.11.11
PKG_RELEASE:=14
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/hacdias/webdav/tar.gz/v$(PKG_VERSION)?
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
-include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk
PKG_VERSION:=$(or $(EASYTIER_VERSION),2.6.4)
PKG_RELEASE:=10
PKG_RELEASE:=11
LUCI_TITLE:=LuCI support for EasyTier
LUCI_DEPENDS:=+kmod-tun +easytier +luci-compat
@@ -118,16 +118,36 @@ local translate = i18n.translate
flex-wrap: wrap;
}
.log-wiki-link {
display: inline-flex;
align-items: center;
height: 36px;
margin-right: 8px;
flex: 0 0 auto;
}
.log-wiki-link img {
display: block;
max-height: 20px;
}
.log-select {
padding: 8px 14px;
box-sizing: border-box;
width: auto;
min-width: 0;
height: 34px;
padding: 6px 24px 6px 10px;
border: 1px solid var(--log-border);
background: var(--log-bg);
color: var(--log-text);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
line-height: 1.2;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
flex: 0 0 auto;
max-width: 136px;
}
.log-select:hover, .log-select:focus {
@@ -138,6 +158,12 @@ local translate = i18n.translate
}
.log-btn {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 0;
height: 34px;
padding: 8px 16px;
border: 1px solid var(--log-border);
background: var(--log-bg);
@@ -148,6 +174,10 @@ local translate = i18n.translate
font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
flex: 0 0 auto;
white-space: nowrap;
word-break: keep-all;
overflow-wrap: normal;
}
.log-btn:hover {
@@ -162,15 +192,65 @@ local translate = i18n.translate
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.log-btn-primary {
background: var(--tab-active);
color: white;
border-color: var(--tab-active);
.log-switch {
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 6px;
height: 34px;
padding: 0 4px;
color: var(--log-text);
cursor: pointer;
flex: 0 0 auto;
user-select: none;
white-space: nowrap;
word-break: keep-all;
overflow-wrap: normal;
}
.log-btn-primary:hover {
opacity: 0.9;
box-shadow: 0 4px 12px rgba(33,150,243,0.3);
.log-switch input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.log-switch-track {
position: relative;
width: 42px;
height: 22px;
border-radius: 999px;
background: var(--log-border);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.18);
transition: background 0.2s ease, box-shadow 0.2s ease;
flex: 0 0 auto;
}
.log-switch-track::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 1px 4px rgba(0,0,0,0.28);
transition: transform 0.2s ease;
}
.log-switch input:checked + .log-switch-track {
background: var(--tab-active);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.12), 0 0 0 2px rgba(33,150,243,0.16);
}
.log-switch input:checked + .log-switch-track::after {
transform: translateX(20px);
}
.log-switch-text {
font-size: 13px;
font-weight: 500;
line-height: 1;
}
.log-content {
@@ -256,28 +336,129 @@ local translate = i18n.translate
.log-header {
flex-direction: column;
align-items: stretch;
padding: 12px 16px;
padding: 10px 12px;
gap: 10px;
}
.log-tabs {
width: 100%;
justify-content: space-around;
justify-content: flex-start;
gap: 6px;
}
.log-tab {
padding: 7px 12px;
font-size: 12px;
border-radius: 6px;
}
.log-controls {
width: 100%;
justify-content: space-between;
justify-content: flex-start;
align-items: center;
gap: 6px;
}
.log-wiki-link {
height: 30px;
margin-right: 2px;
}
.log-wiki-link img {
max-width: 96px;
}
.log-select {
height: 28px;
max-width: 96px;
padding: 3px 18px 3px 6px;
font-size: 12px;
line-height: 1.1;
border-radius: 6px;
}
.log-content {
height: 400px;
height: 60vh;
min-height: 320px;
font-size: 12px;
padding: 16px;
padding: 12px;
}
.log-btn {
flex: 1;
text-align: center;
flex: 0 0 auto;
height: 28px;
padding: 4px 8px;
font-size: 12px;
border-radius: 6px;
line-height: 1.1;
white-space: nowrap;
word-break: keep-all;
overflow-wrap: normal;
}
.log-switch {
height: 28px;
gap: 5px;
padding: 0 2px;
}
.log-switch-track {
width: 34px;
height: 18px;
}
.log-switch-track::after {
top: 2px;
left: 2px;
width: 14px;
height: 14px;
}
.log-switch input:checked + .log-switch-track::after {
transform: translateX(16px);
}
.log-switch-text {
font-size: 12px;
}
.log-footer {
padding: 6px 12px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
@media (max-width: 420px) {
.log-container {
border-radius: 8px;
margin: 10px 0;
}
.log-header {
padding: 8px;
}
.log-controls {
gap: 5px;
}
.log-wiki-link img {
max-width: 82px;
}
.log-select {
max-width: 88px;
}
.log-btn {
padding: 4px 6px;
}
.log-switch-text {
font-size: 11px;
}
}
</style>
@@ -291,11 +472,16 @@ local translate = i18n.translate
<button class="log-tab" data-log="web" onclick="switchLog('web')">
<%=translate("web log")%>
</button>
<a class="log-wiki-link" href="https://deepwiki.com/EasyTier/EasyTier" target="_blank">
<img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
</a>
</div>
<div class="log-controls">
<a href="https://deepwiki.com/EasyTier/EasyTier" target="_blank" style="display:inline-block;height:36px;line-height:36px;margin-right:8px;">
<img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" style="vertical-align:middle;">
</a>
<label class="log-switch" id="auto_refresh_switch" title="<%=translate("Auto Refresh")%>">
<span class="log-switch-text"><%=translate("Auto Refresh")%></span>
<input type="checkbox" id="auto_refresh_toggle" checked onchange="toggleAutoRefresh(true)">
<span class="log-switch-track" aria-hidden="true"></span>
</label>
<select id="log_level_filter" class="log-select" onchange="filterLogLevel()">
<option value="all"><%=translate("All Levels")%></option>
<option value="INFO">INFO</option>
@@ -304,9 +490,6 @@ local translate = i18n.translate
<option value="DEBUG">DEBUG</option>
<option value="TRACE">TRACE</option>
</select>
<button class="log-btn log-btn-primary" id="auto_refresh_btn" onclick="toggleAutoRefresh()">
<%=translate("Stop Auto Refresh")%>
</button>
<button class="log-btn" onclick="clearCurrentLog()"><%=translate("Clear Logs")%></button>
<button class="log-btn" onclick="downloadCurrentLog()"><%=translate("Download Log")%></button>
</div>
@@ -521,18 +704,20 @@ function startPolling() {
}, 2000);
}
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
var btn = document.getElementById('auto_refresh_btn');
function toggleAutoRefresh(refreshNow) {
var toggle = document.getElementById('auto_refresh_toggle');
var wrapper = document.getElementById('auto_refresh_switch');
autoRefreshEnabled = toggle ? toggle.checked : !autoRefreshEnabled;
if (autoRefreshEnabled) {
btn.textContent = '<%=translate("Stop Auto Refresh")%>';
btn.classList.add('log-btn-primary');
if (wrapper) wrapper.title = '<%=translate("Stop Auto Refresh")%>';
if (refreshNow) refreshLog();
} else {
btn.textContent = '<%=translate("Start Auto Refresh")%>';
btn.classList.remove('log-btn-primary');
if (wrapper) wrapper.title = '<%=translate("Start Auto Refresh")%>';
}
}
toggleAutoRefresh(false);
refreshLog();
startPolling();
@@ -15,6 +15,7 @@ local uci = require "luci.model.uci".cursor()
--info: #2196F3;
--warning: #FF9800;
--hover-bg: #f5f5f5;
--field-width: 520px;
}
@media (prefers-color-scheme: dark) {
@@ -33,17 +34,17 @@ local uci = require "luci.model.uci".cursor()
.web-console-container {
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
margin: 12px auto;
padding: 0 12px;
}
.web-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 28px;
margin-bottom: 24px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border-radius: 10px;
padding: 18px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: box-shadow 0.3s ease;
}
@@ -52,47 +53,47 @@ local uci = require "luci.model.uci".cursor()
}
.web-card h3 {
margin: 0 0 24px 0;
margin: 0 0 16px 0;
color: var(--text-primary);
font-size: 20px;
font-size: 16px;
font-weight: 600;
border-bottom: 3px solid var(--info);
padding-bottom: 14px;
border-bottom: 2px solid var(--info);
padding-bottom: 10px;
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
}
.web-card h3::before {
content: "⚙️";
font-size: 24px;
font-size: 18px;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
margin-bottom: 12px;
}
.form-group {
margin-bottom: 24px;
margin-bottom: 14px;
}
.form-group label {
display: block;
margin-bottom: 10px;
margin-bottom: 6px;
color: var(--text-primary);
font-weight: 600;
font-size: 14px;
font-size: 12px;
}
.form-group .description {
display: block;
margin-top: 6px;
margin-top: 4px;
color: var(--text-secondary);
font-size: 12px;
line-height: 1.6;
font-size: 11px;
line-height: 1.45;
padding-left: 0;
opacity: 0.8;
}
@@ -108,13 +109,17 @@ local uci = require "luci.model.uci".cursor()
}
.form-control {
box-sizing: border-box;
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: 8px;
max-width: var(--field-width);
height: 32px;
padding: 6px 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--card-bg);
color: var(--text-primary);
font-size: 14px;
font-size: 12px;
line-height: 1.2;
transition: all 0.2s ease;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
@@ -122,7 +127,7 @@ local uci = require "luci.model.uci".cursor()
.form-control:focus {
outline: none;
border-color: var(--info);
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
}
.form-control:disabled {
@@ -132,13 +137,17 @@ local uci = require "luci.model.uci".cursor()
}
.checkbox-wrapper {
box-sizing: border-box;
display: block;
padding: 12px 16px;
width: 100%;
max-width: var(--field-width);
padding: 8px 10px;
background: var(--hover-bg);
border-radius: 8px;
border-radius: 6px;
transition: background 0.2s;
position: relative;
min-height: 26px;
min-height: 20px;
vertical-align: top;
}
.checkbox-wrapper:hover {
@@ -150,8 +159,11 @@ local uci = require "luci.model.uci".cursor()
cursor: pointer;
user-select: none;
display: block;
line-height: 26px;
padding-right: 60px;
line-height: 20px;
padding-right: 48px;
white-space: nowrap;
word-break: keep-all;
overflow-wrap: normal;
}
.checkbox-wrapper label::before {
@@ -161,10 +173,10 @@ local uci = require "luci.model.uci".cursor()
/* iOS风格开关 */
.switch {
position: absolute;
width: 48px;
height: 26px;
right: 16px;
top: 12px;
width: 38px;
height: 20px;
right: 10px;
top: 8px;
}
.switch input {
@@ -179,21 +191,21 @@ local uci = require "luci.model.uci".cursor()
cursor: pointer;
top: 0;
left: 0;
width: 48px;
height: 26px;
width: 38px;
height: 20px;
background-color: #ccc;
-webkit-transition: 0.3s;
transition: 0.3s;
border-radius: 26px;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
top: 3px;
height: 16px;
width: 16px;
left: 2px;
top: 2px;
background-color: white;
-webkit-transition: 0.3s;
transition: 0.3s;
@@ -206,9 +218,9 @@ input:checked + .slider {
}
input:checked + .slider:before {
-webkit-transform: translateX(22px);
-ms-transform: translateX(22px);
transform: translateX(22px);
-webkit-transform: translateX(18px);
-ms-transform: translateX(18px);
transform: translateX(18px);
}
input:disabled + .slider {
@@ -217,16 +229,16 @@ input:disabled + .slider {
}
.btn {
padding: 12px 28px;
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 15px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 8px;
gap: 6px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
@@ -259,17 +271,17 @@ input:disabled + .slider {
.btn-group {
display: flex;
gap: 16px;
margin-top: 32px;
padding-top: 24px;
border-top: 2px dashed var(--border-color);
gap: 10px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px dashed var(--border-color);
}
.iframe-container {
width: 100%;
height: 800px;
border: 2px solid var(--border-color);
border-radius: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
background: var(--card-bg);
box-shadow: inset 0 2px 8px rgba(0,0,0,0.05);
@@ -284,18 +296,18 @@ input:disabled + .slider {
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
gap: 5px;
padding: 4px 10px;
border-radius: 14px;
font-size: 11px;
font-weight: 600;
margin-left: auto;
}
.status-badge::before {
content: "";
width: 8px;
height: 8px;
width: 6px;
height: 6px;
border-radius: 50%;
animation: pulse 2s infinite;
}
@@ -345,9 +357,9 @@ input:disabled + .slider {
}
.alert {
padding: 14px 20px;
border-radius: 8px;
font-size: 14px;
padding: 10px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
position: fixed;
top: 80px;
@@ -356,7 +368,7 @@ input:disabled + .slider {
-ms-transform: translateX(-50%);
transform: translateX(-50%);
z-index: 10000;
min-width: 320px;
min-width: 260px;
max-width: 500px;
text-align: center;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
@@ -408,39 +420,39 @@ input:disabled + .slider {
.modal-content {
background: var(--card-bg);
border-radius: 12px;
padding: 24px;
border-radius: 8px;
padding: 18px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.modal-title {
font-size: 18px;
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
margin-bottom: 12px;
}
.modal-message {
font-size: 14px;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 24px;
line-height: 1.6;
margin-bottom: 16px;
line-height: 1.5;
}
.modal-buttons {
display: flex;
gap: 12px;
gap: 8px;
justify-content: flex-end;
}
.modal-btn {
padding: 10px 20px;
padding: 7px 14px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-size: 12px;
font-weight: 500;
transition: all 0.2s;
}
@@ -458,6 +470,84 @@ input:disabled + .slider {
background: var(--border-color);
color: var(--text-primary);
}
@media (max-width: 768px) {
.web-console-container {
margin: 8px auto;
padding: 0 8px;
}
.web-card {
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.web-card h3 {
font-size: 15px;
margin-bottom: 12px;
padding-bottom: 8px;
}
.web-card h3::before {
font-size: 16px;
}
.form-row {
grid-template-columns: 1fr;
gap: 8px;
margin-bottom: 8px;
}
.form-group {
margin-bottom: 10px;
}
.form-control {
height: 30px;
padding: 5px 8px;
font-size: 12px;
}
.checkbox-wrapper {
padding: 7px 8px;
}
.checkbox-wrapper label {
font-size: 12px;
padding-right: 44px;
white-space: normal;
word-break: keep-all;
}
.switch {
right: 8px;
top: 7px;
}
.btn {
padding: 7px 12px;
font-size: 12px;
white-space: nowrap;
word-break: keep-all;
}
.btn-group {
gap: 8px;
margin-top: 14px;
padding-top: 12px;
flex-wrap: wrap;
}
.alert {
min-width: 220px;
max-width: calc(100vw - 24px);
}
.modal-content {
padding: 14px;
}
}
</style>
<div class="web-console-container">
@@ -479,7 +569,7 @@ input:disabled + .slider {
<label for="web_db_path"><%:Database File Path%></label>
<div style="display:flex;gap:8px;align-items:flex-start;flex-wrap:wrap;">
<input type="text" class="form-control" id="web_db_path" name="web_db_path" placeholder="/etc/easytier/et.db" style="flex:1;min-width:200px;">
<button type="button" class="btn" onclick="showResetDbModal()" style="background:var(--error);color:white;padding:12px 16px;white-space:nowrap;">
<button type="button" class="btn" onclick="showResetDbModal()" style="background:var(--error);color:white;padding:8px 12px;white-space:nowrap;">
<%:Reset Database%>
</button>
</div>
@@ -602,7 +692,7 @@ input:disabled + .slider {
</div>
<!-- OIDC单点登录配置 -->
<div style="margin-top:32px;padding-top:24px;border-top:2px dashed var(--border-color);">
<div style="margin-top:20px;padding-top:16px;border-top:1px dashed var(--border-color);">
<div class="form-group">
<div class="checkbox-wrapper">
<label for="web_oidc_enabled"><%:Enable OIDC Single Sign-On%></label>
@@ -676,7 +766,7 @@ input:disabled + .slider {
</div>
<!-- Webhook高级配置 -->
<div style="margin-top:32px;padding-top:24px;border-top:2px dashed var(--border-color);">
<div style="margin-top:20px;padding-top:16px;border-top:1px dashed var(--border-color);">
<div class="form-group">
<div class="checkbox-wrapper">
<label for="web_webhook_enabled"><%:Enable Webhook Integration%></label>
@@ -745,12 +835,12 @@ input:disabled + .slider {
<div class="modal-content">
<div class="modal-title" style="color:var(--error);">⚠️ <%:Warning%></div>
<div class="modal-message">
<div style="color:var(--error);font-weight:600;margin-bottom:12px;">
<div style="color:var(--error);font-weight:600;margin-bottom:8px;">
<%:Are you sure you want to reset the database?%>
</div>
<div style="font-size:13px;line-height:1.6;color:var(--text-secondary);">
<div style="font-size:12px;line-height:1.5;color:var(--text-secondary);">
<%:This will delete all database files and cannot be recovered%>:<br>
<code id="reset_db_files" style="display:block;margin-top:8px;padding:8px;background:var(--hover-bg);border-radius:4px;word-break:break-all;font-size:12px;"></code>
<code id="reset_db_files" style="display:block;margin-top:6px;padding:6px;background:var(--hover-bg);border-radius:4px;word-break:break-all;font-size:11px;"></code>
</div>
</div>
<div class="modal-buttons">
@@ -765,7 +855,7 @@ input:disabled + .slider {
<div class="modal-content">
<div class="modal-title"><%:Database Reset Successful%></div>
<div class="modal-message">
<pre id="restart_web_message" style="white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.6;"></pre>
<pre id="restart_web_message" style="white-space:pre-wrap;word-break:break-all;font-size:12px;line-height:1.5;"></pre>
</div>
<div class="modal-buttons">
<button class="modal-btn modal-btn-secondary" onclick="closeRestartModal()"><%:Later%></button>
+2 -2
View File
@@ -466,10 +466,10 @@ start_et() {
fi
if [ ! -z "$proxy_network" ] ; then
if [[ "$(grep "list proxy_network" /etc/config/easytier | awk '{print $3}' | wc -l ) " -eq 1 ]]; then
core_cmd="${core_cmd} -n $proxy_network"
core_cmd="${core_cmd} -n '$proxy_network'"
else
for proxy_networks in $(cat /etc/config/easytier | grep 'list proxy_network' | awk -F 'list proxy_network' '{print $2}' | sed "s/'/\n/g" | tr -d " ' "); do
core_cmd="${core_cmd} -n $proxy_networks"
core_cmd="${core_cmd} -n '$proxy_networks'"
done
fi
fi
+1 -1
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=Information dashboard for 3G/LTE dongle
LUCI_DEPENDS:=+modeminfo
PKG_LICENSE:=GPLv3
PKG_VERSION:=0.4.8
PKG_RELEASE:=10
PKG_RELEASE:=11
include $(TOPDIR)/feeds/luci/luci.mk
@@ -434,8 +434,8 @@ return view.extend({
updateSignalBar(
'sinr', i, modem.sinr, ' dB',
netmode === 'LTE' ? 'sinr' : 'ecio',
netmode === 'LTE' ? -20 : -24
/LTE|NR/.test(netmode) ? 'sinr' : 'ecio',
/LTE|NR/.test(netmode) ? -20 : -24
);
updateSignalBar('rsrp', i, modem.rsrp, ' dBm', 'rsrp', -140);
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.6.2
PKG_RELEASE:=165
PKG_RELEASE:=166
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -49,6 +49,12 @@ o.rewrite_option = o.option
o = s:option(Value, _n("realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o.rewrite_option = o.option
o:depends({ [_n("realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
@@ -351,6 +351,12 @@ end
o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ [_n("hysteria2_realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
@@ -375,6 +375,12 @@ if singbox_tags:find("with_quic") then
o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ [_n("hysteria2_realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
@@ -36,6 +36,12 @@ o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o.rewrite_option = o.option
o:depends({ [_n("realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
@@ -130,6 +130,12 @@ o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ [_n("hysteria2_realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
@@ -163,6 +163,12 @@ if singbox_tags:find("with_quic") then
o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ [_n("hysteria2_realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
+15 -7
View File
@@ -1605,12 +1605,20 @@ function vps_domain_exclude(domain)
end
function parse_realm_uri(uri)
if type(uri) ~= "string" then return nil end
uri = trim(uri)
if uri == "" then return nil end
-- realm[+http]://token@server/realm_id?query
local scheme, token, server_url, realm_id, query = trim(uri):match("^(realm%+http|realm)://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not scheme or not token or not server_url or not realm_id then return nil end
local scheme = (uri:match("^realm%+http://") and "realm+http") or (uri:match("^realm://") and "realm")
if not scheme then return nil end
uri = uri:gsub("^realm%+http://", ""):gsub("^realm://", "")
local token, server_url, realm_id, query = uri:match("^([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not token or not server_url or not realm_id then return nil end
realm_id = realm_id:gsub("/+$", "")
local address, port = server_url:match("^%[?([^%]]+)%]?:?(%d*)$")
local address, port = server_url:match("^%[([^%]]+)%]:(%d+)$") --ipv6:port
if not address then
address, port = server_url:match("^([^:]+):(%d+)$") --ipv4[domain]:port
end
address = address or server_url:match("^%[([^%]]+)%]$") or server_url
port = tonumber(port) or (scheme == "realm+http" and 80 or 443)
local realm = {
scheme = scheme,
@@ -1622,9 +1630,9 @@ function parse_realm_uri(uri)
}
-- 解析 query 中的 stun=
local stun_servers
for value in (query or ""):gmatch("[?&]?[Ss][Tt][Uu][Nn]=([^&]+)") do
if not stun_servers then stun_servers = {} end
stun_servers[#stun_servers + 1] = value
for v in (query or ""):gmatch("[Ss][Tt][Uu][Nn]=([^&]+)") do
stun_servers = stun_servers or {}
stun_servers[#stun_servers + 1] = v
end
realm.stun_servers = stun_servers
return realm
+3
View File
@@ -2167,3 +2167,6 @@ msgstr "并发隧道连接数"
msgid "Processing, please wait…"
msgstr "正在处理,请稍候…"
msgid "Invalid Realm URL."
msgstr "Realm URL 不正确。"
+3 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-run
PKG_VERSION:=1.0.0
PKG_RELEASE:=5
PKG_RELEASE:=6
LUCI_TITLE:=LuCI support for running installers and packages
LUCI_DESCRIPTION:=Upload and execute .run, .sh, .ipk and .apk files from LuCI.
@@ -16,6 +16,8 @@ define Package/luci-app-run/postinst
#!/bin/sh
# 设置版本号到临时文件,用于前端资源缓存刷新
echo "$(PKG_VERSION)-$(PKG_RELEASE)" > /tmp/luci-app-run.version
# 重启 rpcd 服务以加载新插件
/etc/init.d/rpcd restart >/dev/null 2>&1 || true
exit 0
endef
@@ -23,7 +23,12 @@
"running": "安装器正在运行。",
"last_file": "上一次安装包:%s",
"err_no_opkg": "您的系统不支持 ipk 包安装,请选择 apk 包。",
"err_no_apk": "您的系统不支持 apk 包安装,请选择 ipk 包。"
"err_no_apk": "您的系统不支持 apk 包安装,请选择 ipk 包。",
"script_args": "脚本参数(如 -q -h)",
"args_hint": "您可以输入脚本参数或留空",
"cancel": "取消",
"confirm": "确定",
"auto_cleaned": "执行完毕,已自动清理临时文件。"
},
"en": {
"title": "Run Installer",
@@ -49,6 +54,11 @@
"running": "Installer is running.",
"last_file": "Last installer: %s",
"err_no_opkg": "Your system does not support ipk package installation. Please use .apk packages instead.",
"err_no_apk": "Your system does not support apk package installation. Please use .ipk packages instead."
"err_no_apk": "Your system does not support apk package installation. Please use .ipk packages instead.",
"script_args": "Script arguments (e.g., -q -h)",
"args_hint": "You can enter script arguments or leave it blank",
"cancel": "Cancel",
"confirm": "OK",
"auto_cleaned": "Execution complete, temporary files cleaned automatically."
}
}
@@ -48,12 +48,50 @@ function getLang() {
function _(key) {
if (!I18N)
return key;
return getDefaultText(key);
var lang = getLang();
var str = I18N[lang]?.[key] || I18N.zh[key] || key;
var str = (I18N[lang] && I18N[lang][key]) || (I18N.zh && I18N.zh[key]) || getDefaultText(key) || key;
var args = Array.prototype.slice.call(arguments, 1);
return str.format.apply(str, args);
if (args.length > 0) {
return str.format.apply(str, args);
}
return str;
}
function getDefaultText(key) {
var defaults = {
'title': 'Run安装器',
'desc': '在路由器上上传并执行脚本或安装包,注意架构务必匹配。',
'drop_tip': '拖入文件,或从电脑选择。',
'choose_file': '选择 .run 或 .sh 文件',
'choose_ipk': '选择 .ipk 包',
'choose_apk': '选择 .apk 包',
'execute': '执行',
'clean_up': '清理',
'upload_title': '上传文件',
'log_title': '执行日志',
'clean_done': '临时文件与日志已清理。',
'only_supported': '仅支持 .run、.sh、.ipk 和 .apk 文件。',
'prepare_upload': '准备上传:%s (%s)',
'upload_failed': '上传失败。',
'uploading': '正在上传 %s:%d%%',
'upload_err': '上传请求失败。',
'upload_invalid': '上传返回格式无效。',
'upload_done': '上传完成:%s (%s)',
'starting': '正在启动安装器...',
'started': '安装器已启动,PID %d。',
'running': '安装器正在运行。',
'last_file': '上一次安装包:%s',
'err_no_opkg': '您的系统不支持 ipk 包安装,请选择 apk 包。',
'err_no_apk': '您的系统不支持 apk 包安装,请选择 ipk 包。',
'script_args': '脚本参数(如 -q -h)',
'args_hint': '您可以输入脚本参数或留空',
'cancel': '取消',
'confirm': '确定',
'auto_cleaned': '执行完毕,已自动清理临时文件。'
};
return defaults[key] || null;
}
var uploadStart = rpc.declare({
@@ -77,7 +115,7 @@ var uploadFinish = rpc.declare({
var runInstaller = rpc.declare({
object: 'luci-app-run',
method: 'run',
params: ['id']
params: ['id', 'args']
});
var getStatus = rpc.declare({
@@ -134,6 +172,7 @@ return view.extend({
logOffset: 0,
currentUploadId: null,
currentFileType: null,
appVersion: 'unknown',
capabilities: { opkg: 1, apk: 1 },
@@ -257,7 +296,18 @@ return view.extend({
style: 'min-width:140px;margin-left:15px;background:#7B1FA2!important;background-color:#7B1FA2!important;background-image:none!important;color:#fff!important;border-color:#7B1FA2!important;box-shadow:none!important;text-shadow:none!important;opacity:1!important',
click: function (ev) {
ev.preventDefault();
self.startRun(runButton, state);
if (self.currentFileType === '.sh') {
self.showArgsDialog(function (args) {
if (args !== null) {
log.textContent = '';
self.startRun(runButton, state, args.trim());
}
});
} else {
log.textContent = '';
self.startRun(runButton, state, '');
}
}
}, [_('execute')]);
@@ -272,6 +322,8 @@ return view.extend({
self.currentUploadId = null;
self.logOffset = 0;
self.prevRunning = false;
self.autoCleanType = null;
runButton.disabled = true;
log.textContent = '';
progress.style.display = 'none';
@@ -358,6 +410,10 @@ return view.extend({
return Promise.reject();
}
// 记录文件类型
var ext = file.name.match(/\.(run|sh|ipk|apk)$/i);
self.currentFileType = ext ? ext[0].toLowerCase() : null;
progress.style.display = '';
progress.value = 0;
runButton.disabled = true;
@@ -414,15 +470,16 @@ return view.extend({
});
},
startRun: function (runButton, state) {
startRun: function (runButton, state, args) {
var self = this;
if (!this.currentUploadId) return;
runButton.disabled = true;
state.textContent = _('starting');
self.autoCleanType = null;
return runInstaller(this.currentUploadId).then(function (res) {
return runInstaller(this.currentUploadId, args || '').then(function (res) {
if (res && res.error) {
var errorMsg = res.error;
if (res.error === 'ERR_NO_OPKG') {
@@ -434,6 +491,10 @@ return view.extend({
}
self.logOffset = 0;
self.prevRunning = true;
if (self.currentFileType === '.sh' || self.currentFileType === '.run') {
self.autoCleanType = self.currentFileType;
}
state.textContent = _('started', res.pid);
}).catch(function (err) {
runButton.disabled = false;
@@ -441,6 +502,47 @@ return view.extend({
});
},
showArgsDialog: function (callback) {
var dialog = E('div', {
style: 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999'
}, [
E('div', {
style: 'background:#fff;border-radius:8px;padding:20px;width:400px;box-shadow:0 4px 20px rgba(0,0,0,0.3)'
}, [
E('input', {
type: 'text',
placeholder: _('args_hint'),
style: 'width:100%;padding:10px;margin-bottom:15px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;font-size:14px',
id: 'run-args-dialog-input'
}),
E('div', {
style: 'display:flex;justify-content:flex-end;gap:10px'
}, [
E('button', {
class: 'cbi-button cbi-button-reset',
style: 'padding:8px 20px;text-transform:none',
click: function () {
document.body.removeChild(dialog);
callback(null);
}
}, [_('cancel')]),
E('button', {
class: 'cbi-button cbi-button-action',
style: 'padding:8px 20px;text-transform:none',
click: function () {
var args = document.getElementById('run-args-dialog-input').value;
document.body.removeChild(dialog);
callback(args);
}
}, [_('confirm')])
])
])
]);
document.body.appendChild(dialog);
document.getElementById('run-args-dialog-input').focus();
},
refreshLog: function (log, state) {
var self = this;
@@ -454,8 +556,24 @@ return view.extend({
self.logOffset = res.offset || self.logOffset;
if (res.running)
if (res.running) {
state.textContent = _('running');
self.prevRunning = true;
} else if (self.prevRunning) {
// Installer just finished
self.prevRunning = false;
// Auto-cleanup for .sh and .run files
if (self.autoCleanType) {
self.autoCleanType = null;
cleanup().then(function () {
self.currentUploadId = null;
state.textContent = _('auto_cleaned');
}).catch(function () {
state.textContent = _('clean_done');
});
}
}
}).catch(function () { });
}
});
@@ -0,0 +1,6 @@
{
"luci-app-run": {
"description": "LuCI app run service",
"script": "/usr/libexec/rpcd/luci-app-run"
}
}
@@ -89,7 +89,28 @@ read_state() {
is_running() {
local pid="$1"
[ -n "$pid" ] && [ -d "/proc/$pid" ]
[ -n "$pid" ] || return 1
[ -d "/proc/$pid" ] || return 1
local cmdline
cmdline="$(cat "/proc/$pid/cmdline" 2>/dev/null | tr '\0' ' ')"
echo "$cmdline" | grep -q "luci-app-run" && return 0
# Check for .run files (must be followed by space or end of string)
case "$cmdline" in
*".run "*) return 0 ;;
*".run") return 0 ;;
esac
# Check for .sh files (must be followed by space or end of string)
case "$cmdline" in
*".sh "*) return 0 ;;
*".sh") return 0 ;;
esac
return 1
}
method_list() {
@@ -108,7 +129,8 @@ method_list() {
"id": "String"
},
"run": {
"id": "String"
"id": "String",
"args": "String"
},
"status": {},
"read_log": {
@@ -217,6 +239,7 @@ upload_finish() {
run_installer() {
load_input
get_string id
get_string args
valid_id "$id" || { json_error "Invalid upload id"; return; }
@@ -227,6 +250,11 @@ run_installer() {
file="$dir/$name"
read_state
if [ -n "$PID" ] && ! is_running "$PID"; then
rm -f "$STATE_FILE"
unset PID
fi
if is_running "$PID"; then
json_error "Another installer is already running"
return
@@ -279,8 +307,8 @@ run_installer() {
(
cd "$work" || exit 1
printf 'luci-app-run: started %s\n' "$now"
printf 'luci-app-run: executing %s\n\n' "$file"
"$file"
printf 'luci-app-run: executing %s %s\n\n' "$file" "$args"
"$file" $args
rc=$?
printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
exit "$rc"
@@ -356,8 +384,19 @@ cleanup() {
return
fi
rm -rf "$UPLOAD_DIR" "$RUN_DIR" "$STATE_FILE"
mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
rm -rf "$UPLOAD_DIR"
mkdir -p "$UPLOAD_DIR"
# Keep state file but clear PID and FILE, preserve LOG path
if [ -n "$LOG" ]; then
echo "PID=" > "$STATE_FILE"
echo "FILE=" >> "$STATE_FILE"
echo "LOG=$LOG" >> "$STATE_FILE"
echo "STARTED=" >> "$STATE_FILE"
else
> "$STATE_FILE"
fi
json_ok "Cleaned"
}
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=fastfetch
PKG_VERSION:=2.64.2
PKG_RELEASE:=9
PKG_VERSION:=2.65.1
PKG_RELEASE:=10
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/fastfetch-cli/fastfetch/tar.gz/$(PKG_VERSION)?