update 2026-05-10 09:48:30

This commit is contained in:
action committed 2026-05-10 09:48:30 +08:00
1 parent 95ac96413e
commit 0b362674f1
6244 files changed
+2992248

No files matched your search

@@ -0,0 +1,307 @@
'use strict';
'require view';
'require fs';
'require ui';
'require uci';
'require rpc';
'require form';
'require poll';
return view.extend({
callHostHints: rpc.declare({
object: 'luci-rpc',
method: 'getHostHints',
expect: { '': {} }
}),
load: function () {
return Promise.all([
this.callHostHints(),
fs.read('/proc/net/arp')
]);
},
parseArp: function (data) {
var lines = data.split('\n'),
hosts = [];
for (var i = 1; i < lines.length; i++) {
var columns = lines[i].replace(/ +/g, ' ').split(' ');
if (columns.length >= 6) {
hosts.push({
ip: columns[0],
mac: columns[3]
});
}
}
// Sort hosts array by IP address
hosts.sort(function (a, b) {
var ipA = a.ip.split('.').map(Number);
var ipB = b.ip.split('.').map(Number);
for (var i = 0; i < 4; i++) {
if (ipA[i] !== ipB[i]) {
return ipA[i] - ipB[i];
}
}
return 0;
});
return hosts;
},
formatHostIPMAC: function (host) {
return host.ip + ' (' + host.mac + ')';
},
formatHostMACIP: function (host) {
return host.mac + ' (' + host.ip + ')';
},
render: function (data) {
var arpData = data[1],
hosts = this.parseArp(arpData),
m, s, o,
programPath = '/usr/share/wechatpush/wechatpush';
m = new form.Map('wechatpush', _(''))
m.description = _("If you are not familiar with the meanings of these options, please do not modify them.<br/><br/>")
s = m.section(form.NamedSection, 'config', 'wechatpush', _(''));
s.anonymous = true
s.addremove = false
o = s.option(form.Flag, "passive_mode", _("Disable active detection"))
o.default = 0
o.rmempty = true
o.description = _("Disable active detection of client online status. Enabling this feature will no longer prompt device online/offline events.<br/>Suitable for users who are not sensitive to online devices but need other features.")
o = s.option(form.Value, "thread_num", _('Maximum concurrent processes'))
o.placeholder = "3"
o.datatype = "uinteger"
o.rmempty = false;
o.description = _("Do not change the setting value for low-performance devices, or reduce the parameters as appropriate.")
o = s.option(form.ListValue, "defaultSortColumn", _("Client list sorting method"))
o.default = "ip"
o.value("ip", _("IP"))
o.value("uptime", _("Online time"))
o.description = _("This will change the sorting method for both the online device list page and the sorting order in the push content.")
o = s.option(form.Value, "soc_code", _('Custom temperature reading command'))
o.rmempty = true
o.value("", _("Default"))
o.value("pve", _("Proxmox Virtual Environment"))
o.description = _("If you need to use special symbols such as quotes, $, !, etc. in custom commands, you need to escape them yourself.<br/>You can use the command eval echo $(uci get wechatpush.wechatpush.soc_code) to view command output and error information.<br/>The execution result should be a pure number (including decimals) for temperature comparison.<br/>Here is an example that does not require escaping:<br/>cat /sys/class/thermal/thermal_zone0/temp|sort -nr|head -n1|cut -c-2")
o = s.option(form.Value, "server_host", _("Host machine address"))
o.rmempty = true
o.default = "10.0.0.2"
o.depends('soc_code', 'pve');
o = s.option(form.Value, "server_port", _("Host machine SSH port"))
o.rmempty = true
o.default = "22"
o.description = _('The default SSH port is 22. If you have a custom port, please fill in the custom SSH port.<br/>Please make sure you have set up key-based login, otherwise it may cause script errors.<br/>Install the sensors command on PVE by searching on the internet.<br/>Example for key-based login (modify the address and port number accordingly):<br/>opkg update # Update package list<br/>opkg install openssh-client openssh-keygen # Install openssh client<br/>echo -e \"\\n\" | ssh-keygen -t rsa # Generate key file (no passphrase)<br/>pve_host=`uci get wechatpush.config.server_host` || pve_host=\"10.0.0.3\" # Read the PVE host address from the configuration file, If not saved, please fill in by yourself.<br/>pve_port=`uci get wechatpush.config.server_port` || pve_host=\"22\" # Read the PVE host SSH port number from the configuration file, If not saved, please fill in by yourself.<br/>ssh -o StrictHostKeyChecking=yes root@${pve_host} -p ${pve_port} \"tee -a ~/.ssh/OpenWrt_id_rsa.pub\" < ~/.ssh/id_rsa.pub # Transfer public key to PVE<br/>ssh root@${pve_host} -p ${pve_port} \"cat ~/.ssh/OpenWrt_id_rsa.pub >> ~/.ssh/authorized_keys\" # Write public key to PVE<br/>ssh -i /root/.ssh/id_rsa root@${pve_host} -p ${pve_port} sensors # To avoid script errors during the initial connection, please use a private key to connect to PVE and test the temperature command for its proper functioning.<br/>For users who frequently flash firmware, please add /root/.ssh/ to the backup list to avoid duplicate operations.');
// cat ~/.ssh/id_rsa.pub | ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@${pve_host} -p ${pve_port} "cat >> ~/.ssh/authorized_keys"
o.depends('soc_code', 'pve');
o = s.option(form.Button, '_soc', _('Test temperature command'), _('You may need to save the configuration before sending.'));
o.inputstyle = 'action';
o.onclick = function () {
var _this = this;
return fs.exec(programPath, ['soc']).then(function (res) {
if (!res.stdout) {
throw new Error(_('Returned value is empty'));
}
_this.description = res.stdout.trim();
return _this.map.reset();
}).catch(function (err) {
_this.description = _('Fetch failed:') + err.message;
return _this.map.reset();
});
};
o = s.option(form.Value, 'up_timeout', _('Device online detection timeout (s)'));
o.placeholder = "2"
o.optional = false
o.datatype = "uinteger"
o.rmempty = false;
o.depends('passive_mode', '0');
o = s.option(form.Value, "down_timeout", _('Device offline detection timeout (s)'))
o.placeholder = "10"
o.optional = false
o.datatype = "uinteger"
o.rmempty = false;
o.depends('passive_mode', '0');
o = s.option(form.Value, "timeout_retry_count", _('Offline detection count'))
o.placeholder = "2"
o.optional = false
o.datatype = "uinteger"
o.rmempty = false;
o.description = _("If the device has good signal strength and no Wi-Fi sleep issues, you can reduce the above values.<br/>Due to the mysterious nature of Wi-Fi sleep during the night, if you encounter frequent disconnections, please adjust the parameters accordingly.<br/>..╮(╯_╰)╭..")
o.depends('passive_mode', '0');
o = s.option(form.Flag, "only_timeout_push", _("Offline timeout applies only to the devices that receive push notifications"))
o.default = 0
o.rmempty = true
o.description = _("When this option is selected, the offline timeout and offline detection count apply only to the devices that require push notifications. Other devices will use default values, which can significantly reduce the time required for detection. However, it may result in inaccurate online time displayed in the online devices list. It is recommended to enable this option only when there are many devices and frequent offline occurrences are observed for specific devices of interest.")
o.depends('passive_mode', '0');
o = s.option(form.DynamicList, 'always_check_ip_list', _('IP address to always scan'));
o.datatype = 'ipaddr';
o.description = _('The IPs in the list are always subjected to online detection regardless of whether they exist in the ARP list, suitable for secondary routing scenarios.');
hosts.forEach(function (host) {
o.value(host.ip, this.formatHostIPMAC(host));
}, this);
o.depends('passive_mode', '0');
o = s.option(form.MultiValue, 'device_info_helper', _('Assist in obtaining device information'));
o.value('gateway_info', _('Retrieve hostname list from modem'));
o.value('miwifi_info', _('Get wireless band information and hostname from MiWiFi'));
o.value('mikrotik_info', _('Retrieve hostname list from modem MikroTik Router'));
o.value('openwrt_info', _('Get wireless band information and hostname from other OpenWrt'));
o.value('scan_local_ip', _('Scan local IP'));
o.modalonly = true;
o.description = _('When OpenWrt is used as a bypass gateway and cannot obtain device hostnames or a complete list of local network devices.<br/>the \"Retrieve hostname list from modem\" option has only been tested with HG5143F/HN8145V China Telecom gateways and may not be universally applicable.<br/>The \"Scan local IP\" option may not retrieve hostnames, so please use device name annotations in conjunction with it.');
o.depends('passive_mode', '0');
o = s.option(form.Value, "gateway_host_url", _('Optical modem login URL'));
o.rmempty = true;
o.default = "http://192.168.1.1/cgi-bin/luci";
o.depends({ device_info_helper: "gateway_info", '!contains': true });
o = s.option(form.Value, "gateway_info_url", _('Device list JSON URL'));
o.rmempty = true;
o.default = "http://192.168.1.1/cgi-bin/luci/admin/allInfo";
o.description = _('Use F12 console to capture<br/>ip, devName, model are mandatory fields. Example JSON file information:<br/>{\"pc1\":{\"devName\":\"RouterOS\",\"model\":\"\",\"type\":\"pc\",\"ip\":\"192.168.1.7\"}}');
o.depends({ device_info_helper: "gateway_info", '!contains': true });
o = s.option(form.Value, "gateway_logout_url", _('Optical modem logout URL'))
o.rmempty = true
o.default = "http://192.168.1.1/cgi-bin/luci/admin/logout"
o.description = _("Not a mandatory field, but it may affect other users logging into the web management page, e.g., HG5143F")
o.depends({ device_info_helper: "gateway_info", '!contains': true });
o = s.option(form.Value, "gateway_username_id", _('Login page account input box ID'))
o.rmempty = true
o.default = "username"
o.depends({ device_info_helper: "gateway_info", '!contains': true });
o = s.option(form.Value, "gateway_password_id", _('Login page password input box ID'))
o.rmempty = true
o.default = "psd"
o.description = _("Right-click in the browser and select 'Inspect Element'")
o.depends({ device_info_helper: "gateway_info", '!contains': true });
o = s.option(form.Value, "gateway_username", _('Optical modem login account'))
o.rmempty = true
o.default = "useradmin"
o.depends({ device_info_helper: "gateway_info", '!contains': true });
o = s.option(form.Value, "gateway_password", _('Optical modem login password'))
o.rmempty = true
o.description = _("Use a regular account, no need for super password")
o.depends({ device_info_helper: "gateway_info", '!contains': true });
o = s.option(form.Value, "miwifi_ip", _('MiWiFi IP Address'));
o.rmempty = true;
o.description = _("The main router address is all that is needed in the Mesh wireless network topology.")
o.depends({ device_info_helper: "miwifi_info", '!contains': true });
o = s.option(form.Value, "miwifi_password", _('MiWiFi Login Password'))
o.rmempty = true
o.depends({ device_info_helper: "miwifi_info", '!contains': true });
o = s.option(form.Value, "mikrotik_ip", _('MikroTik Routers IP Address'));
o.rmempty = true;
o.description = _('echo -e "\\n" | ssh-keygen -t rsa -f /root/.ssh/id_rsa -N ""<br/>scp /root/.ssh/id_rsa.pub your_username@mikrotik_ip:/id_rsa.pub<br/>ssh your_username@mikrotik_ip<br/>/user ssh-keys import public-key-file=id_rsa.pub user=your_username')
o.depends({ device_info_helper: "mikrotik_info", '!contains': true });
o = s.option(form.Value, "mikrotik_username", _('MikroTik Router Account'))
o.rmempty = true
o.depends({ device_info_helper: "mikrotik_info", '!contains': true });
o = s.option(form.DynamicList, "op_host_ips", _('OpenWrt IP Address'));
o.rmempty = true;
o.description = _('echo -e "\\n" | ssh-keygen -t rsa -f /root/.ssh/id_rsa -N ""<br/>ssh root@your_openwrt_ip "mkdir -p /root/.ssh && chmod 700 /root/.ssh && echo $(cat /root/.ssh/id_rsa.pub) >> /etc/dropbear/authorized_keys && chmod 600 /etc/dropbear/authorized_keys"')
o.depends({ device_info_helper: "openwrt_info", '!contains': true });
o = s.option(form.Value, "scan_ip_range", _('IP range to be scanned'))
o.rmempty = true
o.placeholder = _('192.168.1.1-192.168.1.100');
o.depends({ device_info_helper: "scan_local_ip", '!contains': true });
o = s.option(form.Value, 'device_info_helper_sleeptime', _('Interval for capturing info'));
o.rmempty = false;
o.placeholder = '600';
o.datatype = 'and(uinteger,min(60))'
o.description = _("Generally, frequent capturing is not necessary. Adjust it as needed.")
o.depends({ device_info_helper: "gateway_info", '!contains': true });
o.depends({ device_info_helper: "miwifi_info", '!contains': true });
o.depends({ device_info_helper: "mikrotik_info", '!contains': true });
o.depends({ device_info_helper: "openwrt_info", '!contains': true });
o.depends({ device_info_helper: "scan_local_ip", '!contains': true });
o = s.option(form.Flag, "unattended_enable", _("Unattended tasks"))
o.default = 0
o.rmempty = true
o.description = _("Please make sure the script can run properly, otherwise it may cause frequent restarts and other errors!")
o = s.option(form.Flag, 'zerotier_helper', _('Restart zerotier after IP change'));
o.description = _('An old issue with zerotier<br/>Cannot reconnect after disconnection, emmm, I don\'t know if it has been fixed now.');
o.depends('unattended_enable', '1');
o = s.option(form.Flag, "unattended_only_on_disturb_time", _("Redial only during Do-Not-Disturb period"))
o.default = 0
o.rmempty = true
o.description = _("Avoid redialing network during the day to prevent waiting for DDNS domain resolution. This feature does not affect disconnection detection.<br/>Due to the issue of certain apps consuming excessive data at night, this feature may be unstable.")
o.depends('unattended_enable', '1');
o = s.option(form.DynamicList, 'unattended_device_aliases', _('Followed device list'));
o.datatype = 'macaddr';
o.description = _("Will only be executed when none of the devices in the list are online.<br/>After an hour of Do-Not-Disturb period, if the devices in the focus list have low traffic (around 100kb/m) for five minutes, they will be considered offline.")
o.depends('unattended_enable', '1');
hosts.forEach(function (host) {
o.value(host.mac, this.formatHostMACIP(host));
}, this);
o = s.option(form.ListValue, "network_disconnect_event", _("When the network is disconnected"))
o.default = ""
o.value("", _("No operation"))
o.value("1", _("Restart the router"))
o.value("2", _("Redialing network"))
o.description = _("The restart operation will occur ten minutes after the network disconnection and will be attempted a maximum of two times. If the option to log in to the optical modem is available, this operation will attempt to restart the optical modem.<br/>【!!This feature cannot guarantee compatibility!!】")
o.depends('unattended_enable', '1');
o = s.option(form.ListValue, "unattended_autoreboot_mode", _("Scheduled reboot"))
o.default = ""
o.value("", _("No operation"))
o.value("1", _("Restart the router"))
o.value("2", _("Redialing network"))
o.depends('unattended_enable', '1');
o = s.option(form.Value, "autoreboot_system_uptime", _("System uptime greater than"))
o.rmempty = true
o.default = "24"
o.datatype = "uinteger"
o.description = _("Unit: hours")
o.depends('unattended_autoreboot_mode', '1');
o = s.option(form.Value, "autoreboot_network_uptime", _("Network uptime greater than"))
o.rmempty = true
o.default = "24"
o.datatype = "uinteger"
o.description = _("Unit: hours")
o.depends('unattended_autoreboot_mode', '2');
return m.render();
}
});
@@ -0,0 +1,515 @@
'use strict';
'require view';
'require fs';
'require ui';
'require poll';
'require uci';
return view.extend({
load: function () {
var self = this;
// 清除 localStorage 中的排序设置
localStorage.removeItem('sortColumn');
localStorage.removeItem('sortDirection');
uci.load('wechatpush');
return this.fetchAndRenderDevices().then(function () {
self.setupAutoRefresh();
});
},
fetchAndRenderDevices: function () {
var self = this;
return this.fetchDevices().then(function (data) {
var container = self.render(data);
self.switchContent(container);
}).catch(function (error) {
console.error('Error fetching or rendering devices:', error);
});
},
fetchDevices: function () {
var devices_path = '/tmp/wechatpush/devices.json';
return fs.read(devices_path).then(function (content) {
try {
var data = JSON.parse(content);
var wlanMap = {};
// 如果存在无线接口信息,解析为频段
if (data.wlan && Array.isArray(data.wlan)) {
data.wlan.forEach(function (wlan) {
wlanMap[wlan.interface] = wlan.band;
});
}
// 解析设备的接口信息
data.devices.forEach(function (device) {
if (device.type) {
device.interface = device.type;
} else if (wlanMap[device.interface]) {
device.interface = wlanMap[device.interface];
} else {
device.interface = "LAN";
}
});
return { devices: data.devices };
} catch (e) {
console.error('Error parsing JSON:', e);
return { devices: [] };
}
});
},
render: function (data) {
if (!data || !data.devices || !Array.isArray(data.devices)) {
return document.createElement('div');
}
var devices = data.devices.filter(device => device.status === 'online' || device.status === 'unknown');
var totalDevices = devices.length;
var headers = [_('Hostname'), _('IPv4 address'), _('MAC address'), _('Interfaces'), _('Connection Point'), _('Online time'), _('Details')];
var columns = ['name', 'ip', 'mac', 'interface', 'parent', 'uptime', 'usage'];
var visibleColumns = [];
var hasData = false;
// 获取配置中的默认排序列
var defaultSortColumn = uci.get('wechatpush', 'config', 'defaultSortColumn') || 'ip';
var defaultSortDirection = (defaultSortColumn === 'uptime') ? 'desc' : 'asc';
// 获取存储的排序设置,如果没有则使用默认设置
var storedSortColumn = localStorage.getItem('sortColumn');
var storedSortDirection = localStorage.getItem('sortDirection');
var currentSortColumn = storedSortColumn || defaultSortColumn;
var currentSortDirection = storedSortDirection || defaultSortDirection;
devices.sort(function (a, b) {
return compareDevices(a, b, currentSortColumn, currentSortDirection);
});
// 根据数据源决定可见列
for (var i = 0; i < columns.length; i++) {
var column = columns[i];
var hasColumnData = false;
// 特殊处理 parent 列
if (column === 'parent') {
var hasNonLocalParent = false;
for (var j = 0; j < devices.length; j++) {
var parentValue = devices[j][column];
if (parentValue && parentValue !== "Local") {
hasNonLocalParent = true;
break;
}
}
// 如果存在非 "Local" 的 parent 值,则显示该列
if (hasNonLocalParent) {
visibleColumns.push(i);
}
} else {
// 其他列的正常逻辑
for (var j = 0; j < devices.length; j++) {
if (devices[j][column] !== undefined && devices[j][column] !== '') {
hasColumnData = true;
hasData = true;
break;
}
}
if (hasColumnData) {
visibleColumns.push(i);
}
}
}
var style = `
/* 设备表格样式 */
.device-table {
width: 80%; /* 表格宽度占满父容器 */
border-collapse: collapse; /* 合并边框 */
margin-top: 10px; /* 顶部外边距 */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* 阴影效果 */
border-radius: 8px; /* 圆角边框 */
overflow: hidden; /* 内容溢出隐藏 */
}
.device-table th,
.device-table td {
padding: 10px; /* 单元格内边距 */
text-align: center; /* 文本居中 */
border: 1px solid #ddd; /* 边框样式 */
}
.device-table th {
background-color: rgba(0, 0, 0, 0.05); /* 表头背景色,透明 */
font-weight: bold; /* 粗体字体 */
cursor: pointer; /* 鼠标指针样式为指针 */
position: relative; /* 相对定位 */
}
.device-table th.sortable::after {
right: 10px; /* 右侧距离 */
top: 50%; /* 顶部偏移50% */
transform: translateY(-50%); /* 垂直居中 */
border-width: 5px 5px 0; /* 边框宽度 */
border-style: solid; /* 边框样式为实线 */
opacity: 0.6; /* 透明度 */
}
.device-table th.asc::after {
content: '';
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
border-width: 6px;
border-style: solid;
border-color: #666 transparent transparent transparent; /* 向上箭头颜色 */
}
.device-table th.desc::after {
content: '';
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
border-width: 6px;
border-style: solid;
border-color: transparent transparent #666 transparent; /* 向下箭头颜色 */
}
.device-table tbody tr:nth-child(even) {
background-color: rgba(0, 0, 0, 0.05); /* 偶数行背景色,透明 */
}
.device-table td:first-child {
text-align: left; /* 第一列文本左对齐 */
padding-left: 20px; /* 第一列左侧内边距 */
}
.device-table td a {
color: #007bff; /* 链接颜色 */
text-decoration: none; /* 去掉下划线 */
}
.device-table td a:hover {
text-decoration: underline; /* 鼠标悬停下划线 */
}
.device-table .hide {
display: none; /* 隐藏元素 */
}
@media (max-width: 767px) {
.device-table {
width: 100%; /* 表格宽度占满父容器 */
overflow: hidden; /* 内容溢出隐藏 */
}
.device-table th,
.device-table td {
padding: 3px; /* 单元格内边距 */
text-align: center; /* 文本居中 */
border: 0.35px solid #ddd; /* 边框样式 */
}
.device-table td:first-child {
max-width: 80px;
}
.device-table td:first-child {
text-align: left; /* 第一列文本左对齐 */
padding-left: 2px; /* 第一列左侧内边距 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 显示省略号 */
}
/* 隐藏特定列 */
.device-table th[data-column="parent"],
.device-table td[data-column="parent"] {
display: none;
}
/* 隐藏接口列的文本部分 */
.device-table td[data-column="interface"] span:not(.iface-icon) {
display: none;
}
/* 调整图标样式 */
.device-table td[data-column="interface"] .iface-icon {
margin-right: 0; /* 去掉图标右侧的间距 */
}
}
`;
function createTable() {
var table = document.createElement('table');
table.classList.add('device-table');
var thead = document.createElement('thead');
var tr = document.createElement('tr');
for (var i = 0; i < headers.length; i++) {
var th = document.createElement('th');
th.textContent = headers[i];
if (visibleColumns.includes(i)) {
th.classList.add('sortable');
th.dataset.column = columns[i];
if (columns[i] === currentSortColumn) {
th.classList.add(currentSortDirection === 'asc' ? 'asc' : 'desc');
}
} else {
th.classList.add('hide');
}
tr.appendChild(th);
}
thead.appendChild(tr);
table.appendChild(thead);
var tbody = document.createElement('tbody');
devices.forEach(function (device) {
var row = document.createElement('tr');
for (var i = 0; i < columns.length; i++) {
if (visibleColumns.includes(i)) {
var cell = document.createElement('td');
cell.dataset.column = columns[i];
if (columns[i] === 'uptime') {
cell.textContent = calculateUptime(device['uptime'], window.innerWidth <= 767);
} else if (columns[i] === 'ip' && device['http_access']) {
var link = document.createElement('a');
link.href = `${device['http_access']}://${device['ip']}`;
link.textContent = device['ip'];
link.target = '_blank';
cell.appendChild(link);
} else if (columns[i] === 'interface') {
var icon = document.createElement('span');
icon.classList.add('iface-icon');
if (device['interface'] === '2.4G') {
icon.innerHTML = '📶';
} else if (device['interface'] === '5G') {
icon.innerHTML = '🛜';
} else if (device['interface'] === 'WiFi') {
icon.innerHTML = '🛜';
}
var text = document.createElement('span');
text.textContent = device['interface'];
cell.appendChild(icon);
cell.appendChild(text);
} else if (columns[i] === 'parent') {
if (device['parent']) {
var parentDevice = devices.find(d => {
// 统一转换为大写比较
var deviceMac = (d.mac || '').toUpperCase();
var parentMac = (device['parent'] || '').toUpperCase();
return deviceMac === parentMac || d.ip === device['parent'];
});
if (parentDevice) {
cell.textContent = parentDevice.name || parentDevice.ip;
} else {
cell.textContent = device['parent'];
}
} else {
cell.textContent = '';
}
} else {
cell.textContent = device[columns[i]];
}
row.appendChild(cell);
}
}
tbody.appendChild(row);
});
table.appendChild(tbody);
return table;
}
function calculateUptime(uptime, simpleFormat = false) {
var startTimeStamp = parseInt(uptime);
var currentTimeStamp = Math.floor(Date.now() / 1000);
var uptimeInSeconds = currentTimeStamp - startTimeStamp;
var days = Math.floor(uptimeInSeconds / (3600 * 24));
var hours = Math.floor((uptimeInSeconds % (3600 * 24)) / 3600);
var minutes = Math.floor((uptimeInSeconds % 3600) / 60);
var seconds = uptimeInSeconds % 60;
if (simpleFormat) {
return days > 0 ? `${days}d ${hours}h` :
hours > 0 ? `${hours}h ${minutes}m` :
minutes > 0 ? `${minutes}m ${seconds}s` :
`${seconds}s`;
} else {
if (days > 0) {
return _('%dd %dh').replace(/%d/g, match =>
match === '%d' ? days : hours);
} else if (hours > 0) {
return _('%dh %dm').replace(/%d/g, match =>
match === '%d' ? hours : minutes);
} else if (minutes > 0) {
return _('%dm %ds').replace(/%d/g, match =>
match === '%d' ? minutes : seconds);
} else {
return _('%ds').replace('%d', seconds);
}
}
}
function compareDevices(a, b, column, direction) {
var value1 = getValueForSorting(a, column);
var value2 = getValueForSorting(b, column);
// 处理 name/mac 列的 "unknown" 优先级
if (column === 'name' || column === 'mac') {
const isUnknown1 = (value1 === "unknown");
const isUnknown2 = (value2 === "unknown");
if (isUnknown1 !== isUnknown2) {
return direction === 'asc'
// 升序时 unknown 排最后(视为最大值),降序时排最前
//? (isUnknown1 ? 1 : -1)
//: (isUnknown1 ? -1 : 1);
// 升序时 unknown 排最前(视为最小值)
? (isUnknown1 ? -1 : 1)
: (isUnknown1 ? 1 : -1);
}
}
// 处理 parent 列的优先级
if (column === 'parent') {
const aHasValue = a.parent ? 1 : 0;
const bHasValue = b.parent ? 1 : 0;
if (aHasValue !== bHasValue) {
return direction === 'desc'
? (bHasValue - aHasValue)
: (aHasValue - bHasValue);
}
value1 = a.parent || '';
value2 = b.parent || '';
}
// 通用比较逻辑
if (value1 < value2) {
return direction === 'asc' ? -1 : 1;
} else if (value1 > value2) {
return direction === 'asc' ? 1 : -1;
}
return 0;
}
var interfaceDisplayMap = {
'2.4G': '2.4G',
'5G': '5G',
'WiFi': 'WiFi'
};
// 排序
function getValueForSorting(device, column) {
if (column === 'uptime') {
return parseInt(device['uptime']);
} else if (column === 'ip') {
return ipToNumber(device['ip']);
} else if (column === 'interface') {
return interfaceDisplayMap[device['interface']] || 'LAN';
} else if (column === 'parent') {
// 使用 parent 列的实际显示值进行排序
if (device['parent']) {
var parentDevice = devices.find(d => {
var deviceMac = (d.mac || '').toUpperCase();
var parentMac = (device['parent'] || '').toUpperCase();
return deviceMac === parentMac || d.ip === device['parent'];
});
if (parentDevice) {
return parentDevice.name || parentDevice.ip;
} else {
return device['parent'];
}
} else {
return '';
}
}
return device[column];
}
function ipToNumber(ipAddress) {
var parts = ipAddress.split('.');
var number = 0;
for (var i = 0; i < parts.length; i++) {
number = number * 256 + parseInt(parts[i]);
}
return number;
}
var container = document.createElement('div');
refreshContainer();
container.addEventListener('click', function (event) {
if (event.target.tagName === 'TH' && event.target.parentNode.rowIndex === 0) {
var columnIndex = event.target.cellIndex;
var column = columns[columnIndex];
var direction = 'asc';
// 使在线时间第一次点击方向为倒序
if (column === 'uptime' || column === 'parent') {
if (currentSortColumn !== column) {
// 首次点击该列,默认方向为 desc
direction = 'desc';
} else {
// 切换方向
direction = currentSortDirection === 'desc' ? 'asc' : 'desc';
}
} else if (column === currentSortColumn) {
direction = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
direction = 'asc';
}
sortTable(column, direction);
}
});
function refreshContainer() {
container.innerHTML = '';
container.appendChild(document.createElement('h2')).textContent = _('Currently %s devices online').replace('%s', totalDevices);
container.appendChild(createTable());
container.appendChild(document.createElement('style')).textContent = style;
}
function sortTable(column, direction) {
devices.sort(function (a, b) {
return compareDevices(a, b, column, direction);
});
currentSortColumn = column;
currentSortDirection = direction;
// 存储排序设置
localStorage.setItem('sortColumn', currentSortColumn);
localStorage.setItem('sortDirection', currentSortDirection);
refreshContainer();
}
return container;
},
setupAutoRefresh: function () {
var self = this;
poll.add(L.bind(function () {
self.fetchAndRenderDevices();
}));
},
switchContent: function (newContent) {
var existingContainer = document.querySelector('#view');
if (!existingContainer) {
console.error('Table container not found.');
return;
}
existingContainer.innerHTML = '';
existingContainer.appendChild(newContent);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});
@@ -0,0 +1,786 @@
'use strict';
'require view';
'require fs';
'require ui';
'require uci';
'require rpc';
'require form';
'require poll';
'require tools.widgets as widgets';
'require tools.firewall as fwtool';
var callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { '': {} }
});
function getServiceStatus() {
return L.resolveDefault(callServiceList('wechatpush'), {}).then(function (res) {
console.log(res);
var isRunning = false;
try {
isRunning = res['wechatpush']['instances']['instance1']['running'];
} catch (e) { }
return isRunning;
});
}
function renderStatus(isRunning) {
var spanTemp = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
var renderHTML;
if (isRunning) {
renderHTML = String.format(spanTemp, 'green', _('wechatpush'), _('RUNNING'));
} else {
renderHTML = String.format(spanTemp, 'red', _('wechatpush'), _('NOT RUNNING'));
}
return renderHTML;
}
var cbiRichListValue = form.ListValue.extend({
renderWidget: function (section_id, option_index, cfgvalue) {
var choices = this.transformChoices();
var widget = new ui.Dropdown((cfgvalue != null) ? cfgvalue : this.default, choices, {
id: this.cbid(section_id),
sort: this.keylist,
optional: true,
select_placeholder: this.select_placeholder || this.placeholder,
custom_placeholder: this.custom_placeholder || this.placeholder,
validate: L.bind(this.validate, this, section_id),
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
return widget.render();
},
value: function (value, title, description) {
if (description) {
form.ListValue.prototype.value.call(this, value, E([], [
E('span', { 'class': 'hide-open' }, [title]),
E('div', { 'class': 'hide-close', 'style': 'min-width:25vw' }, [
E('strong', [title]),
E('br'),
E('span', { 'style': 'white-space:normal' }, description)
])
]));
}
else {
form.ListValue.prototype.value.call(this, value, title);
}
}
});
return view.extend({
callHostHints: rpc.declare({
object: 'luci-rpc',
method: 'getHostHints',
expect: { '': {} }
}),
load: function () {
return Promise.all([
this.callHostHints()
]);
},
render: function (data) {
if (fwtool.checkLegacySNAT())
return fwtool.renderMigration();
else
return this.renderForwards(data);
},
renderForwards: function (data) {
var hosts = data[0],
m, s, o,
programPath = '/usr/share/wechatpush/wechatpush';
m = new form.Map('wechatpush', _('WeChat push'), _('A tool that can push device messages from OpenWrt to a mobile phone via WeChat or Telegram.<br /><br />If you encounter any issues while using it, please submit them here:') + '<a href="https://github.com/tty228/luci-app-wechatpush" target="_blank">' + _('GitHub Project Address') + '</a>');
s = m.section(form.TypedSection);
s.anonymous = true;
s.render = function () {
var statusView = E('p', { id: 'service_status' }, _('Collecting data ...'));
poll.add(function () {
return L.resolveDefault(getServiceStatus()).then(function (res) {
statusView.innerHTML = renderStatus(res);
});
});
setTimeout(function () {
poll.start();
}, 100);
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
statusView
]);
}
s = m.section(form.NamedSection, 'config', 'wechatpush', _(''));
s.tab('basic', _('Basic Settings'));
s.tab('content', _('Push Content'));
s.tab('ipset', _('Auto Ban'));
s.tab('crontab', _('Scheduled Push'));
s.tab('disturb', _('Do Not Disturb'));
s.addremove = false;
s.anonymous = true;
// 基本设置
o = s.taboption('basic', form.Flag, 'enable', _('Enabled'));
o = s.taboption('basic', cbiRichListValue, 'jsonpath', _('Push Mode'));
o.value('', _('Close'),
_('Do not use push notifications, only use other features.'));
o.value('/usr/share/wechatpush/api/serverchan.json', _('WeChat serverchan'),
_('Using serverchan API, simple configuration, supports multiple push methods'));
o.value('/usr/share/wechatpush/api/serverchan3.json', _('Serverchan3 APP'),
_('Using serverchan3 API, simple configuration, support push to ServerChan App'));
o.value('/usr/share/wechatpush/api/qywx_mpnews.json', _('WeChat Work Image Message'),
_('Using WeChat Work application message, more complex configuration, and starting from June 20, 2022, additional configuration for trusted IP is required. Trusted IP cannot be shared. This channel is no longer recommended.'));
o.value('/usr/share/wechatpush/api/qywx_markdown.json', _('WeChat Work Markdown Version'),
_('WeChat Work application message in plain text format, no need to click the title to view the content, same as above'));
o.value('/usr/share/wechatpush/api/wxpusher.json', _('wxpusher'),
_('Another channel for WeChat push, the configuration is relatively simple, and only supports official accounts'));
o.value('/usr/share/wechatpush/api/pushplus.json', _('pushplus'),
_('Another channel for WeChat push, the configuration is relatively simple, and it supports multiple push methods'));
o.value('/usr/share/wechatpush/api/telegram.json', _('Telegram'),
_('Telegram Bot Push'));
o.value('/usr/share/wechatpush/api/msmtp.json', _('msmtp'),
_('To send emails using msmtp, you can manually install msmtp and configure `/etc/msmtprc`, or configure the SMTP service here to override the default msmtp configuration file.'));
o.value('/usr/share/wechatpush/api/diy.json', _('Custom Push'),
_('By modifying the JSON file, you can use a custom API'));
o = s.taboption('basic', form.Value, 'sckey', _('「wechatpush」sendkey'));
o.description = _('Get Instructions') + ' <a href="https://sct.ftqq.com/r/13285" target="_blank">' + _('Click here') + '</a>';
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/serverchan.json');
o = s.taboption('basic', form.Value, 'sc3key', _('「Serverchan3」sendkey'));
o.description = _('Get Instructions') + ' <a href="https://sc3.ft07.com/" target="_blank">' + _('Click here') + '</a>';
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/serverchan3.json');
o = s.taboption('basic', form.Value, 'sc3uid', _('「Serverchan3」uid'));
o.description = _('ServerChan3 uid');
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/serverchan3.json');
o = s.taboption('basic', form.Value, 'sc3tags', _('「Serverchan3」tags'));
o.description = _('ServerChan3 message tags, split with |');
o.depends('jsonpath', '/usr/share/wechatpush/api/serverchan3.json');
o = s.taboption('basic', form.Value, 'corpid', _('corpid'));
o.description = _('Get Instructions') + ' <a href="https://work.weixin.qq.com/api/doc/10013" target="_blank">' + _('Click here') + '</a>';
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_mpnews.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_markdown.json');
o = s.taboption('basic', form.Value, 'userid', _('userid'));
o.rmempty = false;
o.description = _('Send to All App Users, enter @all');
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_mpnews.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_markdown.json');
o = s.taboption('basic', form.Value, 'agentid', _('agentid'));
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_mpnews.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_markdown.json');
o = s.taboption('basic', form.Value, 'corpsecret', _('Secret'));
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_mpnews.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_markdown.json');
o = s.taboption('basic', form.Value, 'mediapath', _('Thumbnail Image File Path'))
o.rmempty = false;
o.default = '/usr/share/wechatpush/api/logo.jpg';
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_mpnews.json');
o.description = _('Supports JPG and PNG formats within 2MB <br> Optimal size: 900383 or 2.35:1');
o = s.taboption('basic', form.Value, 'proxy_ip', _('Trusted IP address'));
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_mpnews.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_markdown.json');
o.description = _('If the application was created after June 20, 2022, you need to set the trusted IP. This option should be used in conjunction with a proxy server.');
o = s.taboption('basic', form.Value, 'wxpusher_apptoken', _('appToken'));
o.description = _('Get Instructions') + ' <a href="https://wxpusher.zjiecode.com/docs/#/?id=%e5%bf%ab%e9%80%9f%e6%8e%a5%e5%85%a5" target="_blank">' + _('Click here') + '</a>';
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/wxpusher.json');
o = s.taboption('basic', form.Value, 'wxpusher_uids', _('uids'));
o.depends('jsonpath', '/usr/share/wechatpush/api/wxpusher.json');
o = s.taboption('basic', form.Value, 'wxpusher_topicIds', _('topicIds(Mass sending)'));
o.description = _('Get Instructions') + ' <a href="https://wxpusher.zjiecode.com/docs/#/?id=%e5%8f%91%e9%80%81%e6%b6%88%e6%81%af-1" target="_blank">' + _('Click here') + '</a>';
o.depends('jsonpath', '/usr/share/wechatpush/api/wxpusher.json');
o = s.taboption('basic', form.Value, 'pushplus_token', _('pushplus_token'));
o.description = _('Get Instructions') + ' <a href="http://www.pushplus.plus/" target="_blank">' + _('Click here') + '</a>';
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/pushplus.json');
o = s.taboption('basic', form.Value, 'tg_token', _('Bot Token'));
o.description = _('Get Bot') + ' <a href="https://t.me/BotFather" target="_blank">' + _('Click here') + '</a>' + _('<br />Send a message to the created bot to initiate a conversation.');
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/telegram.json');
o = s.taboption('basic', form.Value, 'tg_chat_id', _('Chat ID'));
o.description = _('Get chat_id') + ' <a href="https://t.me/getuserIDbot" target="_blank">' + _('Click here') + '</a>' + _('<br />If you want to send to a group/channel, please create a non-Chinese group/channel (for easier chatid lookup, you can rename it later).<br />Add the bot to the group, send a message, and use https://api.telegram.org/bot token /getUpdates to obtain the chatid.');
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/telegram.json');
o = s.taboption('basic', form.Value, 'tg_thread_id', _('Message Thread ID'));
o.description = _('Optional: specify a thread (topic) ID for forum-style Telegram chats.');
o.rmempty = false;
o.optional = true;
o.depends('jsonpath', '/usr/share/wechatpush/api/telegram.json');
o = s.taboption('basic', form.Value, 'tg_api_server', _('API Server URL'));
o.placeholder = 'https://api.telegram.org';
o.rmempty = false;
o.depends('jsonpath', '/usr/share/wechatpush/api/telegram.json');
o = s.taboption('basic', form.Value, 'recipient_email', _('Recipient Email Address'));
o.depends('jsonpath', '/usr/share/wechatpush/api/msmtp.json');
o.rmempty = false;
o.description = _('opkg update<br />opkg install msmtp');
o = s.taboption('basic', form.Value, 'smtp_host', _('SMTP Host'));
o.depends('jsonpath', '/usr/share/wechatpush/api/msmtp.json');
o.placeholder = "mail.oursite.example";
o = s.taboption('basic', form.Value, 'smtp_port', _('SMTP Port'));
o.depends('jsonpath', '/usr/share/wechatpush/api/msmtp.json');
o.placeholder = "25";
o = s.taboption('basic', form.ListValue, 'smtp_tls', _('Enable/Disable TLS'));
o.depends('jsonpath', '/usr/share/wechatpush/api/msmtp.json');
o.widget = 'radio';
o.value('on', _('Enable'));
o.value('off', _('Disable'));
o.value('', _('Ignore'));
o.default = '';
o = s.taboption('basic', form.ListValue, 'smtp_starttls', _('Enable/Disable STARTTLS'));
o.depends('jsonpath', '/usr/share/wechatpush/api/msmtp.json');
o.widget = 'radio';
o.value('on', _('Enable'));
o.value('off', _('Disable'));
o.value('', _('Ignore'));
o.default = '';
o = s.taboption('basic', form.Value, 'smtp_user', _('User'));
o.depends('jsonpath', '/usr/share/wechatpush/api/msmtp.json');
o = s.taboption('basic', form.Value, 'smtp_passwordeval', _('Password Command'));
o.depends('jsonpath', '/usr/share/wechatpush/api/msmtp.json');
o.description = _('Set the password for authentication to the output (stdout) of the command.');
o = s.taboption('basic', form.Value, 'smtp_from', _('From Address'));
o.depends('jsonpath', '/usr/share/wechatpush/api/msmtp.json');
o.placeholder = "user@oursite.example";
o = s.taboption('basic', form.Value, 'smtp_from_full_name', _('From Full Name'));
o.depends('jsonpath', '/usr/share/wechatpush/api/msmtp.json');
o.description = _('If set, it is usually displayed as a nickname of the sender in email clients.');
o = s.taboption('basic', form.TextValue, 'diy_json', _('Custom Push'));
o.rows = 28;
o.wrap = 'oft';
o.cfgvalue = function (section_id) {
return fs.trimmed('/usr/share/wechatpush/api/diy.json');
};
o.write = function (section_id, formvalue) {
return this.cfgvalue(section_id).then(function (value) {
if (value == formvalue) {
return
}
return fs.write('/usr/share/wechatpush/api/diy.json', formvalue.trim().replace(/\r\n/g, '\n') + '\n');
});
};
o.description = _('Please refer to the comments and other interface files for modifications. Limited resources, no longer supporting more interfaces, please debug on your own.<br />You can use a similar website to check the JSON file format: https://www.google.com/search?q=JSON+Parser+Online<br />Please use the 「Save」 button in the text box.');
o.depends('jsonpath', '/usr/share/wechatpush/api/diy.json');
o = s.taboption('basic', form.Value, 'proxy_address', _('Proxy Address'));
o.description = _('When you want to use a proxy to push information, you can use this option.<br/>This may be helpful for scenarios like trusted IPs for WeChat Work.Using special characters may cause sending failure.<br/>Example:<br/>http://username:password@127.0.0.1:1080<br/>socks5://username:password@127.0.0.1:1080');
o.depends('jsonpath', '/usr/share/wechatpush/api/serverchan.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/serverchan3.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_mpnews.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/wxpusher.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/pushplus.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/telegram.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/diy.json');
o = s.taboption('basic', form.Button, '_test', _('Send Test'), _('You may need to save the configuration before sending.'));
o.inputstyle = 'add';
o.onclick = function () {
var _this = this;
return fs.exec(programPath, ['test']).then(function (res) {
if (res.code === 0)
_this.description = _('Message sent successfully. If you don\'t receive the message, please check the logs for manual processing.');
else if (res.code === 1)
_this.description = _('Sending failed');
return _this.map.reset();
}).catch(function (err) {
ui.addNotification(null, E('p', [_('Unknown error: %s.').format(err)]));
_this.description = _('Sending failed');
return _this.map.reset();
});
}
o.depends('jsonpath', '/usr/share/wechatpush/api/serverchan.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/serverchan3.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/qywx_mpnews.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/wxpusher.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/pushplus.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/telegram.json');
o.depends('jsonpath', '/usr/share/wechatpush/api/diy.json');
o = s.taboption('basic', form.Value, 'device_name', _('Device Name'));
o.description = _('The device name will be displayed in the push message title to identify the source device of the message.');
o = s.taboption('basic', form.Value, 'sleeptime', _('Check Interval (s)'));
o.rmempty = false;
o.placeholder = '60';
o.datatype = 'and(uinteger,min(10))';
o.description = _('Shorter intervals provide quicker response but consume more system resources.');
o = s.taboption('basic', cbiRichListValue, 'oui_data', _('MAC Device Database'));
o.value('', _('Close'),
_('Do not use MAC device database'));
o.value('1', _('Simplified Version'),
_('Includes common device manufacturers, occupies approximately 200Kb of space'));
o.value('2', _('Full Version'),
_('Download the complete database, processed size is approximately 1.3Mb'));
o.value('3', _('Network Query'),
_('Enables network query, slower response. Only use if space is limited.'));
o = s.taboption('basic', form.Button, '_update_oui', _('Update MAC Device Database'));
o.inputstyle = 'add';
o.onclick = function () {
var _this = this;
return fs.exec('/usr/libexec/wechatpush-call', ['down_oui']).then(function (res) {
if (res.code === 2) {
_this.description = _('Database is up to date, skipping update');
}
return _this.map.reset();
}).catch(function (err) {
ui.addNotification(null, E('p', [_('Unknown error: %s.').format(err)]));
_this.description = _('Browser timeout or unknown error. If the log shows that the update process has been established, please ignore this error: %s.');
return _this.map.reset();
});
};
o.depends('oui_data', '1');
o.depends('oui_data', '2');
o = s.taboption('basic', form.Flag, 'reset_regularly', _('Reset Traffic Data Every Day at Midnight'));
o = s.taboption('basic', form.Flag, 'prefer_client_page', _('Prefer displaying online devices'));
o.default = '1';
o.description = _('Only effective when service is running');
o = s.taboption('basic', form.Flag, 'debuglevel', _('Enable Logging'));
o = s.taboption('basic', form.TextValue, '_device_aliases', _('Device Alias'));
o.rows = 20;
o.wrap = 'oft';
o.cfgvalue = function (section_id) {
return fs.trimmed('/usr/share/wechatpush/api/device_aliases.list');
};
o.write = function (section_id, formvalue) {
return this.cfgvalue(section_id).then(function (value) {
if (value == formvalue) {
return
}
return fs.write('/usr/share/wechatpush/api/device_aliases.list', formvalue.trim().replace(/\r\n/g, '\n') + '\n');
});
};
o.description = _('Please enter the device MAC and device alias separated by a space, such as:<br/> XX:XX:XX:XX:XX:XX My Phone<br/> 192.168.1.2 My PC<br />Please use the 「Save」 button in the text box.');
// 推送内容
o = s.taboption('content', form.MultiValue, 'lite_enable', _('Simplified mode'));
o.value('device', _('Simplify the current device list'));
o.value('nowtime', _('Simplify the current time'));
o.value('content', _('Push only the title'));
o = s.taboption('content', form.Button, '_getip', _('Test IP acquisition command'), _('You may need to save the configuration before sending.'));
o.inputstyle = 'action';
o.onclick = function () {
var _this = this;
return fs.exec(programPath, ['getip']).then(function (res) {
if (!res.stdout) {
throw new Error(_('Returned value is empty'));
}
_this.description = res.stdout.trim();
return _this.map.reset();
}).catch(function (err) {
_this.description = _('Fetch failed:') + err.message;
return _this.map.reset();
});
};
o = s.taboption('content', cbiRichListValue, 'get_ipv4_mode', _('IPv4 Dynamic Notification'));
o.value('', _('Close'),
_(' '));
o.value('1', _('Obtain through interface'),
_(' '));
o.value('2', _('Obtain through URL'),
_('May fail due to server stability and frequent connections.<br/>If the interface can obtain the IP address properly, it is not recommended to use this method.'));
o = s.taboption('content', widgets.DeviceSelect, 'ipv4_interface', _("Device"));
o.description = _('Generally, it should be the pppoe-wan or WAN interface. For multi-dial environments, please select the appropriate interface yourself.');
o.modalonly = true;
o.multiple = false;
o.default = 'pppoe-wan';
o.depends('get_ipv4_mode', '1');
o = s.taboption('content', form.TextValue, 'ipv4_list', _('IPv4 API List'));
o.depends('get_ipv4_mode', '2');
o.optional = false;
o.rows = 8;
o.wrap = 'oft';
o.cfgvalue = function (section_id) {
return fs.trimmed('/usr/share/wechatpush/api/ipv4.list');
};
o.write = function (section_id, formvalue) {
return this.cfgvalue(section_id).then(function (value) {
if (value == formvalue) {
return
}
return fs.write('/usr/share/wechatpush/api/ipv4.list', formvalue.trim().replace(/\r\n/g, '\n') + '\n');
});
};
o.description = _('Access a random address from the list above,URLs in the list are specific to Chinese websites. If you need to use this feature, please replace the URLs with the ones available to you.<br/>Please use the 「Save」 button in the text box.');
o = s.taboption('content', cbiRichListValue, 'get_ipv6_mode', _('IPv6 Dynamic Notification'));
o.value('', _('Close'),
_(' '));
o.value('1', _('Obtain through interface'),
_(' '));
o.value('2', _('Obtain through URL'),
_('May fail due to server stability and frequent connections.<br/>If the interface can obtain the IP address properly, it is not recommended to use this method.'));
o = s.taboption('content', widgets.DeviceSelect, 'ipv6_interface', _("Device"));
o.description = _('Generally, it should be the pppoe-wan or WAN interface. For multi-dial environments, please select the appropriate interface yourself.');
o.modalonly = true;
o.multiple = false;
o.default = 'WAN';
o.depends('get_ipv6_mode', '1');
o = s.taboption('content', form.TextValue, 'ipv6_list', _('IPv6 API List'));
o.depends('get_ipv6_mode', '2')
o.optional = false;
o.rows = 8;
o.wrap = 'oft';
o.cfgvalue = function (section_id) {
return fs.trimmed('/usr/share/wechatpush/api/ipv6.list');
};
o.write = function (section_id, formvalue) {
return this.cfgvalue(section_id).then(function (value) {
if (value == formvalue) {
return
}
return fs.write('/usr/share/wechatpush/api/ipv6.list', formvalue.trim().replace(/\r\n/g, '\n') + '\n');
});
};
o.description = _('Access a random address from the list above,URLs in the list are specific to Chinese websites. If you need to use this feature, please replace the URLs with the ones available to you.<br/>Please use the 「Save」 button in the text box.');
o = s.taboption('content', form.MultiValue, 'device_notification', _('Device Online/Offline Notification'));
o.value('online', _('Online Notification'));
o.value('offline', _('Offline Notification'));
o.modalonly = true;
o = s.taboption('content', form.MultiValue, 'cpu_notification', _('CPU Alert'));
o.value('load', _('Load Alert'));
o.value('temp', _('Temperature Alert'));
o.modalonly = true;
o.description = _('Device alert will be triggered only if it exceeds the set value continuously for five minutes, and there won\'t be a second reminder within an hour.');
o = s.taboption('content', form.Value, 'cpu_load_threshold', _('Load alert threshold'));
o.rmempty = false;
o.placeholder = '2';
o.depends({ cpu_notification: "load", '!contains': true });
o.validate = function (section_id, value) {
var floatValue = parseFloat(value);
if (!isNaN(floatValue) && floatValue.toString() === value) {
return true;
}
return _('Please enter a numeric value only');
};
o.description = _('In general, when the load value is lower than the number of logical cores, you typically don\'t need to pay much attention to it.');
o = s.taboption('content', form.Value, 'temperature_threshold', _('Temperature alert threshold'));
o.rmempty = false;
o.placeholder = '80';
o.datatype = 'and(uinteger,min(1))';
o.depends({ cpu_notification: "temp", '!contains': true });
o.description = _('Please confirm that the device can retrieve temperature. If you need to modify the command, please go to advanced settings.');
o = s.taboption('content', form.MultiValue, 'login_notification', _('Login Notification'));
o.value('web_logged', _('Web Login'));
o.value('ssh_logged', _('SSH Login'));
o.value('web_login_failed', _('Frequent Web Login Errors'));
o.value('ssh_login_failed', _('Frequent SSH Login Errors'));
o.modalonly = true;
o = s.taboption('content', form.Value, 'login_max_num', _('Login failure count'));
o.default = '3';
o.rmempty = false;
o.datatype = 'and(uinteger,min(1))';
o.depends({ login_notification: "web_login_failed", '!contains': true });
o.depends({ login_notification: "ssh_login_failed", '!contains': true });
o.description = _('Send notification after exceeding the count, and optionally auto-ban IP');
o = s.taboption('content', form.Flag, 'client_usage', _('Device abnormal traffic alert'));
o.description = _('Please ensure that you can retrieve device traffic information correctly, otherwise this feature will not work properly');
o.default = '0';
o = s.taboption('content', form.Value, 'client_usage_max', _('Per-minute traffic limit'));
o.placeholder = '10M';
o.rmempty = false;
o.depends('client_usage', '1');
o.description = _('Abnormal traffic alert (byte), you can append K or M');
o = s.taboption('content', form.Flag, 'client_usage_disturb', _('Abnormal traffic do not disturb'));
o.default = '0';
o.depends('client_usage', '1');
o = fwtool.addMACOption(s, 'content', 'client_usage_whitelist', _('Abnormal traffic monitoring list'),
_('Please select device MAC'), hosts);
o.rmempty = true;
o.datatype = 'list(neg(macaddr))';
o.depends('client_usage_disturb', '1');
// 自动封禁
o = s.taboption('ipset', form.Flag, 'login_web_black', _('Auto-ban unauthorized login devices'));
o.default = '0';
o.depends({ login_notification: "web_login_failed", '!contains': true });
o.depends({ login_notification: "ssh_login_failed", '!contains': true });
o = s.taboption('ipset', form.Value, 'login_ip_black_timeout', _('Blacklisting time (s)'));
o.default = '86400';
o.rmempty = false;
o.datatype = 'and(uinteger,min(0))';
o.depends('login_web_black', '1');
o.description = _('\"0\" in ipset means permanent blacklist, use with caution. If misconfigured, change the device IP and clear rules in LUCI.<br/>Note: The whitelist for bans is located under the \"Do Not Disturb\" tab.');
o = s.taboption('ipset', form.Flag, 'port_knocking_enable', _('Port knocking'));
o.default = '0';
o.description = _('If you have disabled LAN port inbound and forwarding in Firewall - Zone Settings, it won\'t work.');
o.depends({ login_notification: "web_login_failed", '!contains': true });
o.depends({ login_notification: "ssh_login_failed", '!contains': true });
o = s.taboption('ipset', form.Value, 'login_port_white', _('Port'));
o.default = '';
o.description = _('Open port after successful login<br/>example:\"22\"、\"21:25\"、\"21:25,135:139\"');
o.depends('port_knocking_enable', '1');
o = s.taboption('ipset', form.DynamicList, 'login_port_forward_list', _('Port Forwards'));
o.default = '';
o.description = _('Example: Forward port 13389 of this device (IPv4:10.0.0.1 / IPv6:fe80::10:0:0:2) to port 3389 of (IPv4:10.0.0.2 / IPv6:fe80::10:0:0:8)<br/>\"10.0.0.1,13389,10.0.0.2,3389\"<br/>\"fe80::10:0:0:1,13389,fe80::10:0:0:2,3389\"');
o.depends('port_knocking_enable', '1');
o = s.taboption('ipset', form.Value, 'login_ip_white_timeout', _('Release time (s)'));
o.default = '86400';
o.datatype = 'and(uinteger,min(0))';
o.description = _('\"0\" in ipset means permanent release, use with caution');
o.depends('port_knocking_enable', '1');
o = s.taboption('ipset', form.TextValue, 'ip_black_list', _('IP blacklist'));
o.rows = 8;
o.wrap = 'soft';
o.cfgvalue = function (section_id) {
return fs.trimmed('/usr/share/wechatpush/api/ip_blacklist');
};
o.write = function (section_id, formvalue) {
return this.cfgvalue(section_id).then(function (value) {
if (value == formvalue) {
return
}
return fs.write('/usr/share/wechatpush/api/ip_blacklist', formvalue.trim().replace(/\r\n/g, '\n') + '\n');
});
};
o.depends('login_web_black', '1');
o.description = _('You can add or delete here, the numbers after represent the remaining time. When adding, only the IP needs to be entered.<br/>Due to limitations on the web interface, please keep one empty line if you need to clear the content; otherwise, it will not be possible to submit. ╮(╯_╰)╭<br/>Please use the 「Save」 button in the text box.');
// 定时推送
o = s.taboption('crontab', cbiRichListValue, 'crontab_mode', _('Scheduled Tasks'));
o.value('', _('Close'),
_(' '));
o.value('1', _('Scheduled sending'),
_('Send at the same time every day'));
o.value('2', _('Interval sending'),
_('Starting from 00:00, send every * hours'));
o = s.taboption('crontab', form.MultiValue, 'crontab_regular_time', _('Sending time'));
for (var t = 0; t <= 23; t++) {
o.value(t, _('Every day') + t + _('clock'));
}
o.modalonly = true;
o.depends("crontab_mode", "1");
o = s.taboption('crontab', form.ListValue, 'crontab_interval_time', _('Interval sending'));
o.default = "6"
for (var t = 0; t <= 12; t++) {
o.value(t, _("") + t + _("Hour"));
}
o.default = '';
o.datatype = "uinteger";
o.depends('crontab_mode', '2');
o.description = _('Starting from 00:00, send every * hours');
o = s.taboption('crontab', form.Value, 'send_title', _('Push title'));
o.depends('crontab_mode', '1');
o.depends('crontab_mode', '2');
o.placeholder = _('OpenWrt Router Status:');
o.description = _('Using special characters may cause sending failure');
o = s.taboption('crontab', form.MultiValue, 'send_notification', _('Push content'));
o.value('router_status', _('System running status'));
o.value('router_temp', _('Device temperature'));
o.value('wan_info', _('WAN info'));
o.value('client_list', _('Client list'));
o.modalonly = true;
o.depends('crontab_mode', '1');
o.depends('crontab_mode', '2');
o = s.taboption('crontab', form.Button, '_send', _('Manual sending'), _('You may need to save the configuration before sending.<br/>Due to browser timeout limitations, if the program is not running, it may exit due to timeout during device list initialization'));
o.inputstyle = 'add';
o.onclick = function () {
var _this = this;
return fs.exec(programPath, ['send']).then(function (res) {
if (res.code === 0)
_this.description = _('Message sent successfully. If you don\'t receive the message, please check the logs for manual processing.');
else if (res.code === 1)
_this.description = _('Sending failed');
return _this.map.reset();
}).catch(function (err) {
ui.addNotification(null, E('p', [_('Unknown error: %s.').format(err)]));
_this.description = _('Sending failed');
return _this.map.reset();
});
}
// 免打扰
o = s.taboption('disturb', cbiRichListValue, 'do_not_disturb_mode', _('Do Not Disturb time setting'));
o.value('', _('Close'),
_(' '));
o.value('1', _('Mode 1: Script Suspension'),
_('Suspend all script actions, including unattended tasks, until the end of the time period'));
o.value('2', _('Mode 2: Silent Mode'),
_('Stop sending notifications, but log normally'));
o = s.taboption('disturb', form.ListValue, 'do_not_disturb_starttime', _('Do Not Disturb start time'));
for (var t = 0; t <= 23; t++) {
o.value(t, _('Every day') + t + _('clock'));
}
o.default = '0';
o.datatype = "uinteger"
o.depends('do_not_disturb_mode', '1');
o.depends('do_not_disturb_mode', '2');
o = s.taboption('disturb', form.ListValue, 'do_not_disturb_endtime', _('Do Not Disturb end time'));
for (var t = 0; t <= 23; t++) {
o.value(t, _('Every day') + t + _('clock'));
}
o.default = 8
o.datatype = "uinteger"
o.depends('do_not_disturb_mode', '1');
o.depends('do_not_disturb_mode', '2');
o = s.taboption('disturb', cbiRichListValue, 'mac_filtering_mode_1', _('MAC Filtering Mode 1'));
o.value('', _('Close'),
_(' '));
o.value('allow', _('Ignore devices in the list'),
_('Ignored devices will not receive notifications or be logged'));
o.value('block', _('Notify only devices in the list'),
_('Ignored devices will not receive notifications or be logged'));
o.value('interface', _('Notify only devices using this interface'),
_('Multiple selections are not supported at the moment'));
o = fwtool.addMACOption(s, 'disturb', 'up_down_push_whitelist', _('Ignored device list'),
_('Please select device MAC'), hosts);
o.datatype = 'list(neg(macaddr))';
o.depends('mac_filtering_mode_1', 'allow');
o = fwtool.addMACOption(s, 'disturb', 'up_down_push_blacklist', _('Followed device list'),
_('Please select device MAC'), hosts);
o.datatype = 'list(neg(macaddr))';
o.depends('mac_filtering_mode_1', 'block');
o.description = _('AA:AA:AA:AA:AA:AA\\|BB:BB:BB:BB:BB:BB Multiple MAC addresses can be treated as the same user.<br/>Notifications will not be sent once any device is online, and notifications will only be sent when all devices are offline to avoid frequent notifications in dual Wi-Fi scenarios.'); // 有点问题,待修复
o = s.taboption('disturb', widgets.DeviceSelect, 'up_down_push_interface', _("Device"));
o.description = _('Notify only devices using this interface');
o.modalonly = true;
o.multiple = false;
o.depends('mac_filtering_mode_1', 'interface');
o = s.taboption('disturb', cbiRichListValue, 'mac_filtering_mode_2', _('MAC Filtering Mode 2'));
o.value('', _('Close'),
_(' '));
o.value('mac_online', _('Do Not Disturb when devices are online'),
_('No notifications will be sent when any device in the list is online'));
o.value('mac_offline', _('Do Not Disturb when devices are offline'),
_('No notifications will be sent when any device in the list is offline'));
o = fwtool.addMACOption(s, 'disturb', 'mac_online_list', _('Do Not Disturb device online list'),
_('Please select device MAC'), hosts);
o.datatype = 'list(neg(macaddr))';
o.depends('mac_filtering_mode_2', 'mac_online');
o = fwtool.addMACOption(s, 'disturb', 'mac_offline_list', _('Do Not Disturb device offline list'),
_('Please select device MAC'), hosts);
o.datatype = 'list(neg(macaddr))';
o.depends('mac_filtering_mode_2', 'mac_offline');
o = s.taboption('disturb', form.Value, 'cpu_threshold_duration', _('When CPU temperature or load exceeds the threshold continuously for (s) seconds.'));
o.rmempty = false;
o.default = '300';
o.datatype = 'and(uinteger)';
o.description = _('If set to 0, it\'s a single check without considering duration.');
o.depends({ cpu_notification: "temp", '!contains': true });
o.depends({ cpu_notification: "load", '!contains': true });
o = s.taboption('disturb', form.Value, 'cpu_notification_delay', _('CPU alarm quiet time (seconds)'));
o.rmempty = false;
o.default = '3600';
o.datatype = 'and(uinteger)';
o.description = _('No repeat notifications within the set time after the initial push notification.');
o.depends({ cpu_notification: "temp", '!contains': true });
o.depends({ cpu_notification: "load", '!contains': true });
o = s.taboption('disturb', cbiRichListValue, 'login_disturb', _('Do Not Disturb for Login Reminders'));
o.value('', _('Close'),
_(' '));
o.value('1', _('Only record in the log'),
_('Ignore all login notifications (including failed logins), and only record them in the log.'));
o.value('2', _('Send notification only on the first login'),
_('Send notification only once within the specified time interval.'));
o = fwtool.addIPOption(s, 'disturb', 'login_ip_white_list', _('Login Alert (Auto-Ban) Whitelist'), null, 'ipv4', hosts, true);
o.datatype = 'ipaddr';
o.depends({ login_notification: "web_logged", '!contains': true });
o.depends({ login_notification: "ssh_logged", '!contains': true });
o.depends({ login_notification: "web_login_failed", '!contains': true });
o.depends({ login_notification: "ssh_login_failed", '!contains': true });
o.description = _('Add the IP addresses in the list to the whitelist for the blocking function (if available), and ignore automatic blocking and login event notifications. Only record in the log. Mask notation is currently not supported.');
o = s.taboption('disturb', form.Flag, 'login_log_enable', _('Login reminder log anti-flooding'));
o.description = _('Users in the whitelist or during the undisturbed time period after their first login IP will be exempt from log recording, preventing log flooding.');
o.depends('login_disturb', '1');
o.depends('login_disturb', '2');
o = s.taboption('disturb', form.Value, 'login_notification_delay', _('Login reminder do not disturb time (s)'));
o.rmempty = false;
o.placeholder = '3600';
o.datatype = 'and(uinteger,min(10))';
o.description = _('Send notification after the first login and do not repeat within the specified time<br/>Take a shortcut and read the login time from the log');
o.depends('login_disturb', '2');
return m.render();
}
});
@@ -0,0 +1,110 @@
'use strict';
'require dom';
'require fs';
'require poll';
'require uci';
'require view';
'require form';
return view.extend({
render: function () {
var css = `
/* 日志框文本区域 */
#log_textarea {
margin-top: 10px;
}
#log_textarea pre {
padding: 10px; /* 内边距 */
border: 1px solid #ddd; /* 边框颜色 */
border-radius: 6px; /* 边框圆角 */
font-family: Consolas, Menlo, Monaco, monospace;
font-size: 14px;
line-height: 1.6; /* 行高 */
white-space: pre-wrap;
word-wrap: break-word;
overflow-y: auto;
max-height: 600px;
}
/* 5s 自动刷新文字 */
.cbi-section small {
margin-left: 5px;
font-size: 12px;
color: #666; /* 深灰色文字 */
}
`;
var log_textarea = E('div', { 'id': 'log_textarea' },
E('img', {
'src': L.resource(['icons/loading.gif']),
'alt': _('Loading...'),
'style': 'vertical-align:middle'
}, _('Collecting data ...'))
);
var log_path = '/tmp/wechatpush/wechatpush.log';
var lastLogContent = '';
var clear_log_button = E('div', {}, [
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': function (ev) {
ev.preventDefault();
var button = ev.target;
button.disabled = true;
button.textContent = _('Clear Logs...');
fs.exec_direct('/usr/libexec/wechatpush-call', ['clear_log'])
.then(function () {
button.textContent = _('Logs cleared successfully!');
button.disabled = false;
button.textContent = _('Clear Logs');
// 立即刷新日志显示框
var log = E('pre', { 'wrap': 'pre' }, [_('Log is clean.')]);
dom.content(log_textarea, log);
lastLogContent = '';
})
.catch(function () {
button.textContent = _('Failed to clear log.');
button.disabled = false;
button.textContent = _('Clear Logs');
});
}
}, _('Clear Logs'))
]);
poll.add(L.bind(function () {
return fs.read_direct(log_path, 'text')
.then(function (res) {
var newContent = res.trim() || _('Log is clean.');
if (newContent !== lastLogContent) {
var log = E('pre', { 'wrap': 'pre' }, [newContent]);
dom.content(log_textarea, log);
log.scrollTop = log.scrollHeight;
lastLogContent = newContent;
}
}).catch(function (err) {
var log;
if (err.toString().includes('NotFoundError')) {
log = E('pre', { 'wrap': 'pre' }, [_('Log file does not exist.')]);
} else {
log = E('pre', { 'wrap': 'pre' }, [_('Unknown error: %s').format(err)]);
}
dom.content(log_textarea, log);
});
}));
return E('div', { 'class': 'cbi-map' }, [
E('style', [css]),
E('div', { 'class': 'cbi-section' }, [
clear_log_button,
log_textarea,
E('small', {}, _('Refresh every 5 seconds.').format(L.env.pollinterval)),
E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,48 @@
'use strict';
'require view';
'require rpc';
'require uci';
var callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { '': {} }
});
function getServiceStatus() {
return L.resolveDefault(callServiceList('wechatpush'), {}).then(function (res) {
console.log(res);
var isRunning = false;
try {
isRunning = res['wechatpush']['instances']['instance1']['running'];
} catch (e) { }
return isRunning;
});
}
return view.extend({
load: function () {
return Promise.all([
getServiceStatus(),
uci.load('wechatpush')
]);
},
render: function (data) {
var status = data[0];
// 检查配置,默认为 '1' (开启)
var preferClient = uci.get('wechatpush', 'config', 'prefer_client_page') !== '0';
// 如果服务正在运行 且 优先展示在线设备开关开启,跳转到 client 页面
if (status && preferClient) {
window.location.replace('/cgi-bin/luci/admin/services/wechatpush/client');
} else {
window.location.replace('/cgi-bin/luci/admin/services/wechatpush/config');
}
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});