mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 02:11:19 +08:00
🎨 Sync 2026-04-10 15:34:26
This commit is contained in:
parent
03930f94a3
commit
c2374cbb98
@ -1,16 +1,16 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=hickory-dns
|
||||
PKG_VERSION:=2026.04.02~54a81de
|
||||
PKG_RELEASE:=10
|
||||
PKG_VERSION:=2026.04.09~34a4cca
|
||||
PKG_RELEASE:=11
|
||||
|
||||
HICKORY_COMMIT:=54a81deefb24bb97df466b9032d5928345fffdde
|
||||
HICKORY_COMMIT:=34a4cca1a707b642052b427938b7d1e8aaa71e3a
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/54a81deefb24bb97df466b9032d5928345fffdde?
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/34a4cca1a707b642052b427938b7d1e8aaa71e3a?
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-54a81deefb24bb97df466b9032d5928345fffdde
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-34a4cca1a707b642052b427938b7d1e8aaa71e3a
|
||||
|
||||
PKG_BUILD_DEPENDS:=rust/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=1.1.1
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=1.2.0
|
||||
PKG_RELEASE:=4
|
||||
|
||||
LUCI_TITLE:=LuCI Support for momo
|
||||
LUCI_DEPENDS:=+luci-base +momo
|
||||
|
||||
@ -114,7 +114,7 @@ return view.extend({
|
||||
o = s.option(form.Flag, 'scheduled_restart', _('Scheduled Restart'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'cron_expression', _('Cron Expression'));
|
||||
o = s.option(form.Value, 'scheduled_restart_cron', _('Scheduled Restart Cron'));
|
||||
o.retain = true;
|
||||
o.rmempty = false;
|
||||
o.depends('scheduled_restart', '1');
|
||||
|
||||
@ -24,35 +24,31 @@ return view.extend({
|
||||
|
||||
m = new form.Map('momo');
|
||||
|
||||
s = m.section(form.NamedSection, 'log', 'log', _('Log Cleanup'));
|
||||
s = m.section(form.NamedSection, 'log', 'log', _('Log'));
|
||||
|
||||
o = s.option(form.Flag, 'log_cleanup_enabled', _('Scheduled Log Cleanup'));
|
||||
s.tab('log_config', _('Log Config'));
|
||||
|
||||
o = s.taboption('log_config', form.Flag, 'scheduled_clear', _('Scheduled Clear'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'log_cleanup_cron_expression', _('Log Cleanup Cron Expression'));
|
||||
|
||||
o = s.taboption('log_config', form.Value, 'scheduled_clear_cron', _('Scheduled Clear Cron'));
|
||||
o.retain = true;
|
||||
o.rmempty = false;
|
||||
o.placeholder = '0 4 * * *';
|
||||
o.depends('log_cleanup_enabled', '1');
|
||||
o.description = _('Run unconditional log cleanup at the configured cron schedule.');
|
||||
o.depends('scheduled_clear', '1');
|
||||
|
||||
o = s.option(form.Flag, 'log_cleanup_size_enabled', _('Size-based Log Cleanup'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'log_cleanup_size_check_cron_expression', _('Log Size Check Cron Expression'));
|
||||
o = s.taboption('log_config', form.Value, 'scheduled_clear_size_limit', _('Scheduled Clear Size Limit'));
|
||||
o.retain = true;
|
||||
o.rmempty = false;
|
||||
o.placeholder = '*/30 * * * *';
|
||||
o.depends('log_cleanup_size_enabled', '1');
|
||||
o.description = _('Check log size at the configured cron schedule before cleaning up.');
|
||||
|
||||
o = s.option(form.Value, 'log_cleanup_size_mb', _('Log Cleanup Size Threshold (MB)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '50';
|
||||
o.depends('log_cleanup_size_enabled', '1');
|
||||
o.description = _('Clear app, core and debug logs when their total size reaches this threshold.');
|
||||
o.depends('scheduled_clear', '1');
|
||||
|
||||
s = m.section(form.NamedSection, 'placeholder', 'placeholder', _('Log'));
|
||||
o = s.taboption('log_config', form.ListValue, 'scheduled_clear_size_limit_unit', _('Scheduled Clear Size Limit Unit'));
|
||||
o.retain = true;
|
||||
o.rmempty = false;
|
||||
o.depends('scheduled_clear', '1');
|
||||
o.value('KB', 'KB');
|
||||
o.value('MB', 'MB');
|
||||
o.value('GB', 'GB');
|
||||
|
||||
s.tab('app_log', _('App Log'));
|
||||
|
||||
|
||||
154
luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js
Normal file
154
luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js
Normal file
@ -0,0 +1,154 @@
|
||||
'use strict';
|
||||
'require form';
|
||||
'require view';
|
||||
'require uci';
|
||||
'require fs';
|
||||
'require network';
|
||||
'require poll';
|
||||
'require tools.widgets as widgets';
|
||||
'require tools.momo as momo';
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
uci.load('momo'),
|
||||
]);
|
||||
},
|
||||
render: function (data) {
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('momo');
|
||||
|
||||
s = m.section(form.NamedSection, 'mixin', 'mixin', _('Mixin Option'));
|
||||
|
||||
s.tab('log', _('Log Config'));
|
||||
|
||||
o = s.taboption('log', form.ListValue, 'log_disabled', _('Log Disabled'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
o = s.taboption('log', form.ListValue, 'log_level', _('Log Level'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('panic');
|
||||
o.value('fatal');
|
||||
o.value('error');
|
||||
o.value('warn');
|
||||
o.value('info');
|
||||
o.value('debug');
|
||||
o.value('trace');
|
||||
|
||||
o = s.taboption('log', form.ListValue, 'log_timestamp', _('Log Timestamp'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_output', _('Log Output'));
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
s.tab('dns', _('DNS Config'));
|
||||
|
||||
o = s.taboption('dns', form.ListValue, 'dns_strategy', _('DNS Strategy'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('prefer_ipv4', _('Prefer IPv4'));
|
||||
o.value('prefer_ipv6', _('Prefer IPv6'));
|
||||
o.value('ipv4_only', _('IPv4 Only'));
|
||||
o.value('ipv6_only', _('IPv6 Only'));
|
||||
|
||||
o = s.taboption('dns', form.ListValue, 'dns_disable_cache', _('DNS Disable Cache'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
o = s.taboption('dns', form.ListValue, 'dns_disable_expire', _('DNS Disable Expire'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
o = s.taboption('dns', form.ListValue, 'dns_independent_cache', _('DNS Independent Cache'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
o = s.taboption('dns', form.Value, 'dns_cache_capacity', _('DNS Cache Capacity'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
o = s.taboption('dns', form.ListValue, 'dns_reverse_mapping', _('DNS Reverse Mapping'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
s.tab('ntp', _('NTP Config'));
|
||||
|
||||
o = s.taboption('ntp', form.ListValue, 'ntp_enabled', _('NTP Enabled'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
o = s.taboption('ntp', form.Value, 'ntp_server', _('NTP Server'));
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
o = s.taboption('ntp', form.Value, 'ntp_server_port', _('NTP Server Port'));
|
||||
o.datatype = 'port';
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
o = s.taboption('ntp', form.Value, 'ntp_interval', _('NTP Interval'));
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
s.tab('cache', _('Cache Config'));
|
||||
|
||||
o = s.taboption('cache', form.ListValue, 'cache_enabled', _('Cache Enabled'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
o = s.taboption('cache', form.Value, 'cache_path', _('Cache Path'));
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
o = s.taboption('cache', form.ListValue, 'cache_store_fakeip', _('Cache Store FakeIP'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
o = s.taboption('cache', form.ListValue, 'cache_store_rdrc', _('Cache Store RDRC'));
|
||||
o.optional = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('0', _('Disable'));
|
||||
o.value('1', _('Enable'));
|
||||
|
||||
s.tab('external_control', _('External Control Config'));
|
||||
|
||||
o = s.taboption('external_control', form.Value, 'external_control_ui_path', _('UI Path'));
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
o = s.taboption('external_control', form.Value, 'external_control_ui_download_url', _('UI Download Url'));
|
||||
o.placeholder = _('Unmodified');
|
||||
o.value('https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip', 'Zashboard (CDN Fonts)');
|
||||
o.value('https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip', 'Zashboard');
|
||||
o.value('https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip', 'MetaCubeXD');
|
||||
o.value('https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip', 'YACD');
|
||||
o.value('https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip', 'Razord');
|
||||
|
||||
o = s.taboption('external_control', form.Value, 'external_control_api_listen', _('API Listen'));
|
||||
o.datatype = 'ipaddrport(1)';
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
o = s.taboption('external_control', form.Value, 'external_control_api_secret', _('API Secret'));
|
||||
o.password = true;
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@ -1,6 +1,14 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:144
|
||||
msgid "API Listen"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:148
|
||||
msgid "API Secret"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:69
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:122
|
||||
msgid "Access Control"
|
||||
@ -24,7 +32,7 @@ msgstr ""
|
||||
msgid "App Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:53
|
||||
msgid "App Log"
|
||||
msgstr ""
|
||||
|
||||
@ -56,6 +64,26 @@ msgstr ""
|
||||
msgid "CGroup"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:108
|
||||
msgid "Cache Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:110
|
||||
msgid "Cache Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:116
|
||||
msgid "Cache Path"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:119
|
||||
msgid "Cache Store FakeIP"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:125
|
||||
msgid "Cache Store RDRC"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/editor.js:27
|
||||
msgid "Choose File"
|
||||
msgstr ""
|
||||
@ -64,8 +92,8 @@ msgstr ""
|
||||
msgid "Choose Profile"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:33
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:66
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:57
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:90
|
||||
msgid "Clear Log"
|
||||
msgstr ""
|
||||
|
||||
@ -74,7 +102,7 @@ msgstr ""
|
||||
msgid "Commonly Used Port"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:62
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:86
|
||||
msgid "Core Log"
|
||||
msgstr ""
|
||||
|
||||
@ -90,16 +118,40 @@ msgstr ""
|
||||
msgid "Core Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:117
|
||||
msgid "Cron Expression"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:99
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:165
|
||||
msgid "DNS"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:95
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:80
|
||||
msgid "DNS Cache Capacity"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:52
|
||||
msgid "DNS Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:62
|
||||
msgid "DNS Disable Cache"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:68
|
||||
msgid "DNS Disable Expire"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:74
|
||||
msgid "DNS Independent Cache"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:84
|
||||
msgid "DNS Reverse Mapping"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:54
|
||||
msgid "DNS Strategy"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:119
|
||||
msgid "Debug Log"
|
||||
msgstr ""
|
||||
|
||||
@ -111,6 +163,16 @@ msgstr ""
|
||||
msgid "Destination UDP Port to Proxy"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:29
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:46
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:65
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:71
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:77
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:87
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:95
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:113
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:122
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:128
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:53
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:60
|
||||
msgid "Disable"
|
||||
@ -121,11 +183,21 @@ msgid "Edit Subscription"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/editor.js:25
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:37
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:45
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:96
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:30
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:47
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:66
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:72
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:78
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:88
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:96
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:114
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:123
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:129
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:33
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:66
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:77
|
||||
@ -138,6 +210,10 @@ msgstr ""
|
||||
msgid "Expire At"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:131
|
||||
msgid "External Control Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:48
|
||||
msgid "Fake-IP Ping Hijack"
|
||||
msgstr ""
|
||||
@ -155,7 +231,7 @@ msgstr ""
|
||||
msgid "General Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:99
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:123
|
||||
msgid "Generate & Download"
|
||||
msgstr ""
|
||||
|
||||
@ -183,6 +259,10 @@ msgstr ""
|
||||
msgid "IPv4 DNS Hijack"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:59
|
||||
msgid "IPv4 Only"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:42
|
||||
msgid "IPv4 Proxy"
|
||||
msgstr ""
|
||||
@ -191,6 +271,10 @@ msgstr ""
|
||||
msgid "IPv6 DNS Hijack"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:60
|
||||
msgid "IPv6 Only"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:45
|
||||
msgid "IPv6 Proxy"
|
||||
msgstr ""
|
||||
@ -208,19 +292,68 @@ msgid "Local"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:27
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:45
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:53
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:24
|
||||
msgid "Log Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:26
|
||||
msgid "Log Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:32
|
||||
msgid "Log Level"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:49
|
||||
msgid "Log Output"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:43
|
||||
msgid "Log Timestamp"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:194
|
||||
msgid "Misc"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:29
|
||||
msgid "Mixin Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:22
|
||||
msgid "Mixin Option"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:38
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:3
|
||||
msgid "Momo"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:90
|
||||
msgid "NTP Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:92
|
||||
msgid "NTP Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:105
|
||||
msgid "NTP Interval"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:98
|
||||
msgid "NTP Server"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:101
|
||||
msgid "NTP Server Port"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:15
|
||||
msgid "Not Running"
|
||||
msgstr ""
|
||||
@ -241,6 +374,14 @@ msgstr ""
|
||||
msgid "Prefer"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:57
|
||||
msgid "Prefer IPv4"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:58
|
||||
msgid "Prefer IPv6"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/profile.js:21
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:21
|
||||
msgid "Profile"
|
||||
@ -257,7 +398,7 @@ msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:29
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:31
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:29
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:37
|
||||
msgid "Proxy Config"
|
||||
msgstr ""
|
||||
|
||||
@ -297,12 +438,32 @@ msgstr ""
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:31
|
||||
msgid "Scheduled Clear"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:34
|
||||
msgid "Scheduled Clear Cron"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:39
|
||||
msgid "Scheduled Clear Size Limit"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:45
|
||||
msgid "Scheduled Clear Size Limit Unit"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:114
|
||||
msgid "Scheduled Restart"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:56
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:89
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:117
|
||||
msgid "Scheduled Restart Cron"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:80
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:113
|
||||
msgid "Scroll To Bottom"
|
||||
msgstr ""
|
||||
|
||||
@ -385,6 +546,14 @@ msgstr ""
|
||||
msgid "UDP Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:136
|
||||
msgid "UI Download Url"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:133
|
||||
msgid "UI Path"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:139
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:143
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:147
|
||||
@ -394,6 +563,31 @@ msgstr ""
|
||||
msgid "Unlimited"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:28
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:34
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:45
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:50
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:56
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:64
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:70
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:76
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:82
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:86
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:94
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:99
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:103
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:106
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:112
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:117
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:121
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:127
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:134
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:137
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:146
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:150
|
||||
msgid "Unmodified"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/profile.js:63
|
||||
msgid "Update"
|
||||
msgstr ""
|
||||
@ -425,39 +619,3 @@ msgstr ""
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:128
|
||||
msgid "procd Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:47
|
||||
msgid "Check log size at the configured cron schedule before cleaning up."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:53
|
||||
msgid "Clear app, core and debug logs when their total size reaches this threshold."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:27
|
||||
msgid "Log Cleanup"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:32
|
||||
msgid "Log Cleanup Cron Expression"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:49
|
||||
msgid "Log Cleanup Size Threshold (MB)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:42
|
||||
msgid "Log Size Check Cron Expression"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:37
|
||||
msgid "Run unconditional log cleanup at the configured cron schedule."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
|
||||
msgid "Scheduled Log Cleanup"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:39
|
||||
msgid "Size-based Log Cleanup"
|
||||
msgstr ""
|
||||
|
||||
@ -8,6 +8,14 @@ msgstr ""
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:144
|
||||
msgid "API Listen"
|
||||
msgstr "API 监听"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:148
|
||||
msgid "API Secret"
|
||||
msgstr "API 密码"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:69
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:122
|
||||
msgid "Access Control"
|
||||
@ -31,7 +39,7 @@ msgstr "全部端口"
|
||||
msgid "App Config"
|
||||
msgstr "插件配置"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:53
|
||||
msgid "App Log"
|
||||
msgstr "插件日志"
|
||||
|
||||
@ -63,6 +71,26 @@ msgstr "绕过 FWMark"
|
||||
msgid "CGroup"
|
||||
msgstr "控制组"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:108
|
||||
msgid "Cache Config"
|
||||
msgstr "缓存配置"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:110
|
||||
msgid "Cache Enabled"
|
||||
msgstr "启用缓存"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:116
|
||||
msgid "Cache Path"
|
||||
msgstr "缓存文件路径"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:119
|
||||
msgid "Cache Store FakeIP"
|
||||
msgstr "缓存 FakeIP"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:125
|
||||
msgid "Cache Store RDRC"
|
||||
msgstr "缓存 RDRC"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/editor.js:27
|
||||
msgid "Choose File"
|
||||
msgstr "选择文件"
|
||||
@ -71,8 +99,8 @@ msgstr "选择文件"
|
||||
msgid "Choose Profile"
|
||||
msgstr "选择配置文件"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:33
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:66
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:57
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:90
|
||||
msgid "Clear Log"
|
||||
msgstr "清空日志"
|
||||
|
||||
@ -81,7 +109,7 @@ msgstr "清空日志"
|
||||
msgid "Commonly Used Port"
|
||||
msgstr "常用端口"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:62
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:86
|
||||
msgid "Core Log"
|
||||
msgstr "核心日志"
|
||||
|
||||
@ -97,16 +125,40 @@ msgstr "核心状态"
|
||||
msgid "Core Version"
|
||||
msgstr "核心版本"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:117
|
||||
msgid "Cron Expression"
|
||||
msgstr "Cron 表达式"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:99
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:165
|
||||
msgid "DNS"
|
||||
msgstr "DNS"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:95
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:80
|
||||
msgid "DNS Cache Capacity"
|
||||
msgstr "DNS 缓存容量"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:52
|
||||
msgid "DNS Config"
|
||||
msgstr "DNS 配置"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:62
|
||||
msgid "DNS Disable Cache"
|
||||
msgstr "禁用 DNS 缓存"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:68
|
||||
msgid "DNS Disable Expire"
|
||||
msgstr "禁用 DNS 过期"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:74
|
||||
msgid "DNS Independent Cache"
|
||||
msgstr "DNS 独立缓存"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:84
|
||||
msgid "DNS Reverse Mapping"
|
||||
msgstr "DNS 反向映射"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:54
|
||||
msgid "DNS Strategy"
|
||||
msgstr "DNS 策略"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:119
|
||||
msgid "Debug Log"
|
||||
msgstr "调试日志"
|
||||
|
||||
@ -118,6 +170,16 @@ msgstr "要代理的 TCP 目标端口"
|
||||
msgid "Destination UDP Port to Proxy"
|
||||
msgstr "要代理的 UDP 目标端口"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:29
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:46
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:65
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:71
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:77
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:87
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:95
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:113
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:122
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:128
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:53
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:60
|
||||
msgid "Disable"
|
||||
@ -128,11 +190,21 @@ msgid "Edit Subscription"
|
||||
msgstr "编辑订阅"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/editor.js:25
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:37
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:45
|
||||
msgid "Editor"
|
||||
msgstr "编辑器"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:96
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:30
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:47
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:66
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:72
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:78
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:88
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:96
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:114
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:123
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:129
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:33
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:66
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:77
|
||||
@ -145,6 +217,10 @@ msgstr "启用"
|
||||
msgid "Expire At"
|
||||
msgstr "到期时间"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:131
|
||||
msgid "External Control Config"
|
||||
msgstr "外部控制配置"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:48
|
||||
msgid "Fake-IP Ping Hijack"
|
||||
msgstr "Fake-IP Ping 劫持"
|
||||
@ -162,7 +238,7 @@ msgstr "文件:"
|
||||
msgid "General Config"
|
||||
msgstr "全局配置"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:99
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:123
|
||||
msgid "Generate & Download"
|
||||
msgstr "生成并下载"
|
||||
|
||||
@ -190,6 +266,10 @@ msgstr "使用说明"
|
||||
msgid "IPv4 DNS Hijack"
|
||||
msgstr "IPv4 DNS 劫持"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:59
|
||||
msgid "IPv4 Only"
|
||||
msgstr "仅 IPv4"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:42
|
||||
msgid "IPv4 Proxy"
|
||||
msgstr "IPv4 代理"
|
||||
@ -198,6 +278,10 @@ msgstr "IPv4 代理"
|
||||
msgid "IPv6 DNS Hijack"
|
||||
msgstr "IPv6 DNS 劫持"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:60
|
||||
msgid "IPv6 Only"
|
||||
msgstr "仅 IPv6"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:45
|
||||
msgid "IPv6 Proxy"
|
||||
msgstr "IPv6 代理"
|
||||
@ -215,19 +299,68 @@ msgid "Local"
|
||||
msgstr "本地"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:27
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:45
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:53
|
||||
msgid "Log"
|
||||
msgstr "日志"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:24
|
||||
msgid "Log Config"
|
||||
msgstr "日志配置"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:26
|
||||
msgid "Log Disabled"
|
||||
msgstr "禁用日志"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:32
|
||||
msgid "Log Level"
|
||||
msgstr "日志级别"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:49
|
||||
msgid "Log Output"
|
||||
msgstr "日志输出文件路径"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:43
|
||||
msgid "Log Timestamp"
|
||||
msgstr "打印时间戳"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:194
|
||||
msgid "Misc"
|
||||
msgstr "杂项"
|
||||
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:29
|
||||
msgid "Mixin Config"
|
||||
msgstr "混入配置"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:22
|
||||
msgid "Mixin Option"
|
||||
msgstr "混入选项"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:38
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:3
|
||||
msgid "Momo"
|
||||
msgstr "Momo"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:90
|
||||
msgid "NTP Config"
|
||||
msgstr "NTP 配置"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:92
|
||||
msgid "NTP Enabled"
|
||||
msgstr "启用 NTP"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:105
|
||||
msgid "NTP Interval"
|
||||
msgstr "NTP 时间同步间隔"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:98
|
||||
msgid "NTP Server"
|
||||
msgstr "NTP 服务地址"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:101
|
||||
msgid "NTP Server Port"
|
||||
msgstr "NTP 服务端口"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:15
|
||||
msgid "Not Running"
|
||||
msgstr "未在运行"
|
||||
@ -248,6 +381,14 @@ msgstr "打开面板"
|
||||
msgid "Prefer"
|
||||
msgstr "优先"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:57
|
||||
msgid "Prefer IPv4"
|
||||
msgstr "优先 IPv4"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:58
|
||||
msgid "Prefer IPv6"
|
||||
msgstr "优先 IPv6"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/profile.js:21
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:21
|
||||
msgid "Profile"
|
||||
@ -264,7 +405,7 @@ msgstr "代理"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:29
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/proxy.js:31
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:29
|
||||
#: applications/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json:37
|
||||
msgid "Proxy Config"
|
||||
msgstr "代理配置"
|
||||
|
||||
@ -304,12 +445,32 @@ msgstr "路由器代理"
|
||||
msgid "Running"
|
||||
msgstr "运行中"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:31
|
||||
msgid "Scheduled Clear"
|
||||
msgstr "定时清除""
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:34
|
||||
msgid "Scheduled Clear Cron"
|
||||
msgstr "Cron 表达式"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:39
|
||||
msgid "Scheduled Clear Size Limit"
|
||||
msgstr "日志大小上限"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:45
|
||||
msgid "Scheduled Clear Size Limit Unit"
|
||||
msgstr "日志大小上限单位"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:114
|
||||
msgid "Scheduled Restart"
|
||||
msgstr "定时重启"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:56
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:89
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:117
|
||||
msgid "Scheduled Restart Cron"
|
||||
msgstr "Cron 表达式"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:80
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:113
|
||||
msgid "Scroll To Bottom"
|
||||
msgstr "滚动到底部"
|
||||
|
||||
@ -392,6 +553,14 @@ msgstr "在 OpenWrt 上使用 sing-box 进行透明代理"
|
||||
msgid "UDP Mode"
|
||||
msgstr "UDP 模式"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:136
|
||||
msgid "UI Download Url"
|
||||
msgstr "UI 下载地址"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:133
|
||||
msgid "UI Path"
|
||||
msgstr "UI 路径"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:139
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:143
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js:147
|
||||
@ -401,6 +570,31 @@ msgstr "UDP 模式"
|
||||
msgid "Unlimited"
|
||||
msgstr "无限制"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:28
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:34
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:45
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:50
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:56
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:64
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:70
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:76
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:82
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:86
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:94
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:99
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:103
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:106
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:112
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:117
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:121
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:127
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:134
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:137
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:146
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js:150
|
||||
msgid "Unmodified"
|
||||
msgstr "不修改"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/profile.js:63
|
||||
msgid "Update"
|
||||
msgstr "更新"
|
||||
@ -433,38 +627,34 @@ msgstr "用户代理(UA)"
|
||||
msgid "procd Config"
|
||||
msgstr "procd 配置"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:47
|
||||
msgid "Check log size at the configured cron schedule before cleaning up."
|
||||
msgstr "按设定的 Cron 周期检查日志大小,满足条件后再清理。"
|
||||
#~ msgid "Cron Expression"
|
||||
#~ msgstr "Cron 表达式"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:53
|
||||
msgid "Clear app, core and debug logs when their total size reaches this threshold."
|
||||
msgstr "当插件日志、核心日志和调试日志总大小达到该阈值时自动清理。"
|
||||
#~ msgid "Check log size at the configured cron schedule before cleaning up."
|
||||
#~ msgstr "按设定的 Cron 周期检查日志大小,满足条件后再清理。"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:27
|
||||
msgid "Log Cleanup"
|
||||
msgstr "日志清理"
|
||||
#~ msgid ""
|
||||
#~ "Clear app, core and debug logs when their total size reaches this "
|
||||
#~ "threshold."
|
||||
#~ msgstr "当插件日志、核心日志和调试日志总大小达到该阈值时自动清理。"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:32
|
||||
msgid "Log Cleanup Cron Expression"
|
||||
msgstr "日志清理 Cron 表达式"
|
||||
#~ msgid "Log Cleanup"
|
||||
#~ msgstr "日志清理"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:49
|
||||
msgid "Log Cleanup Size Threshold (MB)"
|
||||
msgstr "日志清理大小阈值(MB)"
|
||||
#~ msgid "Log Cleanup Cron Expression"
|
||||
#~ msgstr "日志清理 Cron 表达式"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:42
|
||||
msgid "Log Size Check Cron Expression"
|
||||
msgstr "日志大小检查 Cron 表达式"
|
||||
#~ msgid "Log Cleanup Size Threshold (MB)"
|
||||
#~ msgstr "日志清理大小阈值(MB)"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:37
|
||||
msgid "Run unconditional log cleanup at the configured cron schedule."
|
||||
msgstr "按设定的 Cron 时间无条件执行日志清理。"
|
||||
#~ msgid "Log Size Check Cron Expression"
|
||||
#~ msgstr "日志大小检查 Cron 表达式"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:29
|
||||
msgid "Scheduled Log Cleanup"
|
||||
msgstr "定时日志清理"
|
||||
#~ msgid "Run unconditional log cleanup at the configured cron schedule."
|
||||
#~ msgstr "按设定的 Cron 时间无条件执行日志清理。"
|
||||
|
||||
#: applications/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js:39
|
||||
msgid "Size-based Log Cleanup"
|
||||
msgstr "按大小触发日志清理"
|
||||
#~ msgid "Scheduled Log Cleanup"
|
||||
#~ msgstr "定时日志清理"
|
||||
|
||||
#~ msgid "Size-based Log Cleanup"
|
||||
#~ msgstr "按大小触发日志清理"
|
||||
|
||||
@ -25,9 +25,17 @@
|
||||
"path": "momo/profile"
|
||||
}
|
||||
},
|
||||
"admin/services/momo/mixin": {
|
||||
"title": "Mixin Config",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "momo/mixin"
|
||||
}
|
||||
},
|
||||
"admin/services/momo/proxy": {
|
||||
"title": "Proxy Config",
|
||||
"order": 30,
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "momo/proxy"
|
||||
@ -35,7 +43,7 @@
|
||||
},
|
||||
"admin/services/momo/editor": {
|
||||
"title": "Editor",
|
||||
"order": 40,
|
||||
"order": 50,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "momo/editor"
|
||||
@ -43,7 +51,7 @@
|
||||
},
|
||||
"admin/services/momo/log": {
|
||||
"title": "Log",
|
||||
"order": 50,
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "momo/log"
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { access, popen, readfile, writefile } from 'fs';
|
||||
import { get_paths, merge_exists, get_users, get_groups, get_cgroups } from '/etc/momo/ucode/include.uc';
|
||||
import { access, popen, readfile } from 'fs';
|
||||
import { get_paths, merge_exists, get_users, get_groups, get_cgroups, load_profile, save_profile } from '/etc/momo/ucode/include.uc';
|
||||
|
||||
const methods = {
|
||||
get_paths: {
|
||||
@ -40,9 +40,8 @@ const methods = {
|
||||
profile: {
|
||||
args: { defaults: {} },
|
||||
call: function(req) {
|
||||
const paths = get_paths();
|
||||
const defaults = req.args?.defaults ?? {};
|
||||
const profile = json(readfile(paths.run_profile_path));
|
||||
const profile = load_profile();
|
||||
return merge_exists(defaults, profile);
|
||||
}
|
||||
},
|
||||
@ -67,8 +66,7 @@ const methods = {
|
||||
const query = req.args?.query;
|
||||
const body = req.args?.body;
|
||||
|
||||
const paths = get_paths();
|
||||
const profile = json(readfile(paths.run_profile_path));
|
||||
const profile = load_profile();
|
||||
const api_listen = profile['experimental']['clash_api']['external_controller'];
|
||||
const api_secret = profile['experimental']['clash_api']['secret'];
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=1.25.3
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=1.26.0
|
||||
PKG_RELEASE:=4
|
||||
|
||||
LUCI_TITLE:=LuCI Support for nikki
|
||||
LUCI_DEPENDS:=+luci-base +nikki
|
||||
|
||||
@ -13,7 +13,6 @@ return view.extend({
|
||||
return Promise.all([
|
||||
uci.load('nikki'),
|
||||
network.getNetworks(),
|
||||
|
||||
]);
|
||||
},
|
||||
render: function (data) {
|
||||
|
||||
@ -4,6 +4,124 @@
|
||||
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
|
||||
|
||||
## [2.0.4] - 2026-04-10
|
||||
|
||||
### 适配 OpenClaw v2026.4.9
|
||||
|
||||
本次更新适配 OpenClaw 最新稳定版本 (2026.4.9),包含多项破坏性配置变更处理和安全策略适配。
|
||||
|
||||
#### 版本变更
|
||||
- **OC_TESTED_VERSION**: 从 2026.3.28 更新到 2026.4.9 (跨 12 个版本)
|
||||
- **Node.js 版本**: 保持 22.16.0 (v2026.4.x 最低要求 >= 22.14.0)
|
||||
|
||||
#### 更新日志 UI 深度优化
|
||||
|
||||
参照 GitHub Releases 和 App Store 更新页面的设计规范,重构「检测升级」按钮触发的更新日志显示区域:
|
||||
|
||||
- **Markdown 渲染引擎重构** (`ocMarkdownToHtml` 函数):
|
||||
- 中英文混排字体栈优化:`PingFang SC` / `Microsoft YaHei` / `Noto Sans SC` + 系统字体回退
|
||||
- 字体渲染增强:`text-rendering: optimizeLegibility` + 抗锯齿平滑
|
||||
- 标题层级比例重构:一级标题 (20px) → 二级 (17px) → 三级 (15px) → 四级 (14px)
|
||||
- 代码块样式:圆角边框 + 等宽字体栈 (`SF Mono`, Consolas, Menlo)
|
||||
- 链接悬停效果:底部边框渐显动画
|
||||
- 列表标记美化:统一的 `list-style-position: inside` + 行高优化 (1.75)
|
||||
|
||||
- **版本状态徽章设计**:
|
||||
- 「有新版本」状态:渐变金色背景 + 橙色边框
|
||||
- 「已是最新」状态:渐变绿色背景 + 绿色边框
|
||||
- 「无法检查」状态:灰色背景 + 中性边框
|
||||
- 版本号使用等宽字体徽章样式,增强可读性
|
||||
|
||||
- **更新日志容器设计**:
|
||||
- 卡片式布局:圆角 + 微阴影 + 白色背景
|
||||
- 版本标题栏:渐变背景 + 蓝色版本徽章 + GitHub 跳转链接
|
||||
- 内容区滚动条美化:`scrollbar-width: thin` + 自定义颜色
|
||||
- 操作按钮区:分组设计 + 阴影增强
|
||||
|
||||
- **视觉细节优化**:
|
||||
- 段落间距:12px 统一间距
|
||||
- 行高优化:中文场景 1.75-1.8
|
||||
- 字间距微调:标题 `-0.02em`,正文 `0.01em`
|
||||
- 颜色层级:主标题 `#1f2328` → 正文 `#32383f` → 次要文本 `#656d76`
|
||||
|
||||
#### 破坏性变更处理 (v2026.4.5)
|
||||
|
||||
OpenClaw v2026.4.5 移除了多项旧版配置别名,本版本新增自动清理逻辑:
|
||||
|
||||
- **已废弃配置清理**: `sync_uci_to_json()` 新增清理以下废弃字段
|
||||
- `talk.voiceId` / `talk.apiKey` — 语音功能配置迁移
|
||||
- `browser.ssrfPolicy.allowPrivateNetwork` — SSRF 策略重构
|
||||
- `hooks.internal.handlers` — 内部钩子迁移
|
||||
- `channel.*.allow` / `group.*.allow` / `room.*.allow` — 迁移到 `enabled` 字段
|
||||
- `agents.defaults.cliBackends` — CLI 后端配置废弃
|
||||
|
||||
- **配置迁移工具集成**: 服务启动时自动调用 `openclaw doctor --fix` 迁移遗留配置
|
||||
|
||||
#### 权限模型适配 (v2026.4.7/4.9)
|
||||
|
||||
OpenClaw 新增了严格的环境变量安全检查,阻止危险环境变量覆盖:
|
||||
|
||||
- **环境变量安全审计**: `start_service()` 注入环境变量前进行路径安全验证
|
||||
- 阻止包含特殊字符 (`$`, `` ` ``, `|`, `&`) 的路径
|
||||
- 阻止 `/proc/*`, `/sys/*`, `/dev/*` 等危险目录
|
||||
- 确保 `JAVA_HOME`, `RUST_*`, `GIT_*`, `KUBECONFIG`, `AWS_*` 等危险变量不被意外注入
|
||||
|
||||
- **命令授权策略更新**: 适配 v2026.4.7 的权限收紧
|
||||
- `/allowlist add/remove` 现需要所有者授权 (LuCI 会话验证已满足)
|
||||
- `gateway config.apply` 阻止修改 exec 审批路径
|
||||
|
||||
#### 安全更新
|
||||
|
||||
- **依赖安全**: `basic-ftp` 强制升级到 5.2.1 (修复 CRLF 命令注入漏洞 CVE)
|
||||
- **浏览器 SSRF**: 新增安全策略配置选项 (默认不启用内网访问白名单)
|
||||
|
||||
#### 改进
|
||||
|
||||
- **配置验证增强**: 服务启动时先执行配置 schema 验证,失败时自动尝试修复
|
||||
- **iframe 安全头修补**: 适配 v2026.4.x 新的安全头位置
|
||||
- v2026.4.x 将 `X-Frame-Options` 和 `frame-ancestors` 设置从 `gateway-cli-*.js` 迁移到 `server.impl-*.js`
|
||||
- `patch_iframe_headers()` 函数现已支持两个位置的安全头修补
|
||||
- 解决 Web 控制台 iframe 嵌入被阻止的问题
|
||||
- **错误提示优化**: 配置迁移失败时输出详细的诊断信息
|
||||
- **移除 Gemini CLI 安装**: OpenClaw v2026.4.x 已废弃 `google-gemini-cli-auth` 插件
|
||||
- 节省约 155MB 磁盘空间
|
||||
- 减少安装时间约 30~60 秒
|
||||
- 用户配置 Google Gemini 请使用 API Key 方式 (推荐)
|
||||
- **修复微信插件加载失败**: 清理 jiti 缓存目录解决权限冲突
|
||||
- jiti 编译 TypeScript 时在 `/tmp/jiti` 创建缓存
|
||||
- 服务启动前自动清理,避免 openclaw 用户无法写入
|
||||
|
||||
#### 修复 (2026-04-10)
|
||||
|
||||
- **权限修复 EACCES 错误**: `doctor --fix` 以 root 运行后创建的文件导致 Gateway 无法写入
|
||||
- 根因: `doctor --fix` 在服务启动时以 root 身份执行,可能创建 root 所有的配置文件
|
||||
- 症状: `EACCES: permission denied, open '.../agents/main/agent/models.json.xxx.tmp'`
|
||||
- 修复: `init.d/openclaw` 在 `doctor --fix` 后自动修复非 extensions 目录权限
|
||||
- 同时修复 `openclaw-env factory-reset` 后的权限问题
|
||||
|
||||
- **OpenWrt 兼容性**: 无 `ss` 命令时 Gateway 重启检测失败
|
||||
- 根因: `oc-config-interactive.js` 直接调用 `ss` 命令,部分精简固件未安装
|
||||
- 修复: 优先使用 `ss`,不存在时回退到 `netstat`
|
||||
|
||||
- **新增交互式菜单引擎**: `oc-menu-engine.js`
|
||||
- 方向键导航、搜索过滤、粘贴支持 (Bracketed Paste Mode)
|
||||
- 纯 Node.js 实现,零外部依赖
|
||||
- 用于 SSH 命令行配置体验优化
|
||||
|
||||
#### 技术细节
|
||||
|
||||
**新增函数**:
|
||||
- `_validate_path()`: 环境变量路径安全验证
|
||||
- `_run_config_migration()`: 执行 OpenClaw 官方配置迁移工具
|
||||
|
||||
**修改文件**:
|
||||
- `root/etc/init.d/openclaw`: 配置同步逻辑、环境变量注入、权限修复
|
||||
- `root/usr/bin/openclaw-env`: OC_TESTED_VERSION 版本号、factory-reset 权限修复
|
||||
- `root/usr/share/openclaw/oc-config-interactive.js`: ss/netstat 兼容性
|
||||
- `root/usr/share/openclaw/oc-menu-engine.js`: 新增交互式菜单引擎
|
||||
|
||||
---
|
||||
|
||||
## [2.0.3] - 2026-04-03
|
||||
|
||||
### 修复
|
||||
|
||||
@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-openclaw
|
||||
PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0"))
|
||||
PKG_RELEASE:=9
|
||||
PKG_RELEASE:=10
|
||||
|
||||
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
|
||||
@ -30,15 +30,19 @@
|
||||
无需 SDK,适用于已安装好的系统。
|
||||
|
||||
```bash
|
||||
wget https://github.com/10000ge10000/luci-app-openclaw/releases/latest/download/luci-app-openclaw.run
|
||||
sh luci-app-openclaw.run
|
||||
# 下载最新版本(自动获取版本号)
|
||||
VER=$(curl -sI "https://github.com/10000ge10000/luci-app-openclaw/releases/latest" 2>/dev/null | grep -i "location:" | sed 's/.*tag\/v\{0,1\}//' | tr -d '\r\n')
|
||||
wget "https://github.com/10000ge10000/luci-app-openclaw/releases/download/v${VER}/luci-app-openclaw_${VER}.run"
|
||||
sh "luci-app-openclaw_${VER}.run"
|
||||
```
|
||||
|
||||
### 方式二:.ipk 安装
|
||||
|
||||
```bash
|
||||
wget https://github.com/10000ge10000/luci-app-openclaw/releases/latest/download/luci-app-openclaw.ipk
|
||||
opkg install luci-app-openclaw.ipk
|
||||
# 下载最新版本(自动获取版本号)
|
||||
VER=$(curl -sI "https://github.com/10000ge10000/luci-app-openclaw/releases/latest" 2>/dev/null | grep -i "location:" | sed 's/.*tag\/v\{0,1\}//' | tr -d '\r\n')
|
||||
wget "https://github.com/10000ge10000/luci-app-openclaw/releases/download/v${VER}/luci-app-openclaw_${VER}-1_all.ipk"
|
||||
opkg install "luci-app-openclaw_${VER}-1_all.ipk"
|
||||
```
|
||||
|
||||
### 方式三:集成到固件编译
|
||||
@ -72,23 +76,6 @@ make package/luci-app-openclaw/compile V=s
|
||||
find bin/ -name "luci-app-openclaw*.ipk"
|
||||
```
|
||||
|
||||
### 方式四:手动安装
|
||||
|
||||
```bash
|
||||
git clone https://github.com/10000ge10000/luci-app-openclaw.git
|
||||
cd luci-app-openclaw
|
||||
|
||||
cp -r root/* /
|
||||
mkdir -p /usr/lib/lua/luci/controller /usr/lib/lua/luci/model/cbi/openclaw /usr/lib/lua/luci/view/openclaw
|
||||
cp luasrc/controller/openclaw.lua /usr/lib/lua/luci/controller/
|
||||
cp luasrc/model/cbi/openclaw/*.lua /usr/lib/lua/luci/model/cbi/openclaw/
|
||||
cp luasrc/view/openclaw/*.htm /usr/lib/lua/luci/view/openclaw/
|
||||
|
||||
chmod +x /etc/init.d/openclaw /usr/bin/openclaw-env /usr/share/openclaw/oc-config.sh
|
||||
sh /etc/uci-defaults/99-openclaw
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/*
|
||||
```
|
||||
|
||||
|
||||
## 🔰 首次使用
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
2.0.3
|
||||
2.0.4
|
||||
|
||||
@ -188,11 +188,27 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = '});'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 轮询安装日志
|
||||
-- 轮询安装日志 (智能滚动: 用户向上滚动时暂停自动滚动,滚动到底部时恢复)
|
||||
html[#html+1] = 'var _lastLogLen=0;'
|
||||
html[#html+1] = 'var _autoScrollEnabled=true;' -- 智能滚动状态标志
|
||||
html[#html+1] = 'function ocPollSetupLog(){'
|
||||
html[#html+1] = 'if(_setupTimer)clearInterval(_setupTimer);'
|
||||
html[#html+1] = '_lastLogLen=0;'
|
||||
html[#html+1] = '_autoScrollEnabled=true;' -- 初始状态: 启用自动滚动
|
||||
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
|
||||
-- 绑定滚动事件监听器 (只绑定一次)
|
||||
html[#html+1] = 'if(!logEl._scrollListenerAttached){'
|
||||
html[#html+1] = 'logEl.addEventListener("scroll",function(){'
|
||||
html[#html+1] = 'var el=this;'
|
||||
html[#html+1] = 'var atBottom=el.scrollHeight-el.scrollTop-el.clientHeight<5;'
|
||||
html[#html+1] = 'if(atBottom){'
|
||||
html[#html+1] = '_autoScrollEnabled=true;' -- 滚动到底部: 恢复自动滚动
|
||||
html[#html+1] = '}else{'
|
||||
html[#html+1] = '_autoScrollEnabled=false;' -- 用户向上滚动: 暂停自动滚动
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = '});'
|
||||
html[#html+1] = 'logEl._scrollListenerAttached=true;'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = '_setupTimer=setInterval(function(){'
|
||||
html[#html+1] = '(new XHR()).get("' .. log_url .. '",null,function(x){'
|
||||
html[#html+1] = 'try{'
|
||||
@ -204,7 +220,10 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = 'logEl.textContent+=newLog;'
|
||||
html[#html+1] = '_lastLogLen=r.log.length;'
|
||||
html[#html+1] = '}'
|
||||
-- 智能滚动: 仅在自动滚动启用时滚动到底部
|
||||
html[#html+1] = 'if(_autoScrollEnabled){'
|
||||
html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'if(r.state==="running"){'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
|
||||
html[#html+1] = '}else if(r.state==="success"){'
|
||||
@ -295,35 +314,38 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 错误</span>";}'
|
||||
html[#html+1] = '});}'
|
||||
|
||||
-- 简单的 Markdown 转 HTML 函数 (用于渲染 GitHub Release Notes)
|
||||
-- Markdown 转 HTML 函数 (优化版 - 用于渲染 GitHub Release Notes)
|
||||
-- 特性:中英文字体栈、代码块样式、链接悬停效果、列表美化
|
||||
html[#html+1] = 'function ocMarkdownToHtml(md){'
|
||||
html[#html+1] = 'if(!md)return "";'
|
||||
-- 转义 HTML 特殊字符
|
||||
html[#html+1] = 'var html=md.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");'
|
||||
-- 代码块 (```code```)
|
||||
html[#html+1] = 'html=html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(m,lang,code){return"<pre style=\\"background:#f6f8fa;padding:10px 14px;border-radius:6px;overflow-x:auto;font-size:13px;line-height:1.5;\\"><code>"+code.trim()+"</code></pre>";});'
|
||||
-- 代码块 (```code```) - 圆角边框 + 等宽字体栈
|
||||
html[#html+1] = 'html=html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(m,lang,code){return"<pre style=\\"background:#f6f8fa;padding:12px 16px;border-radius:8px;overflow-x:auto;font-size:13px;line-height:1.6;font-family:SF Mono,Consolas,Menlo,monospace;border:1px solid #d0d7de;\\"><code>"+code.trim()+"</code></pre>";});'
|
||||
-- 行内代码 (`code`)
|
||||
html[#html+1] = 'html=html.replace(/`([^`]+)`/g,"<code style=\\"background:#f6f8fa;padding:2px 6px;border-radius:3px;font-size:13px;\\">$1</code>");'
|
||||
-- 标题 (### ## #)
|
||||
html[#html+1] = 'html=html.replace(/^### (.+)$/gm,"<h4 style=\\"margin:14px 0 8px;font-size:15px;font-weight:600;color:#24292f;\\">$1</h4>");'
|
||||
html[#html+1] = 'html=html.replace(/^## (.+)$/gm,"<h3 style=\\"margin:16px 0 10px;font-size:16px;font-weight:600;color:#24292f;\\">$1</h3>");'
|
||||
html[#html+1] = 'html=html.replace(/^# (.+)$/gm,"<h2 style=\\"margin:18px 0 12px;font-size:17px;font-weight:600;color:#24292f;border-bottom:1px solid #d0d7de;padding-bottom:6px;\\">$1</h2>");'
|
||||
html[#html+1] = 'html=html.replace(/`([^`]+)`/g,"<code style=\\"background:#f6f8fa;padding:2px 6px;border-radius:4px;font-size:13px;font-family:SF Mono,Consolas,Menlo,monospace;border:1px solid #e1e4e8;\\">$1</code>");'
|
||||
-- 标题层级 (优化比例: h1=20px, h2=17px, h3=15px, h4=14px)
|
||||
html[#html+1] = 'html=html.replace(/^#### (.+)$/gm,"<h5 style=\\"margin:12px 0 6px;font-size:14px;font-weight:600;color:#1f2328;letter-spacing:-0.02em;\\">$1</h5>");'
|
||||
html[#html+1] = 'html=html.replace(/^### (.+)$/gm,"<h4 style=\\"margin:14px 0 8px;font-size:15px;font-weight:600;color:#1f2328;letter-spacing:-0.02em;\\">$1</h4>");'
|
||||
html[#html+1] = 'html=html.replace(/^## (.+)$/gm,"<h3 style=\\"margin:16px 0 10px;font-size:17px;font-weight:600;color:#1f2328;letter-spacing:-0.02em;\\">$1</h3>");'
|
||||
html[#html+1] = 'html=html.replace(/^# (.+)$/gm,"<h2 style=\\"margin:18px 0 12px;font-size:20px;font-weight:600;color:#1f2328;border-bottom:1px solid #d0d7de;padding-bottom:8px;letter-spacing:-0.02em;\\">$1</h2>");'
|
||||
-- 粗体和斜体
|
||||
html[#html+1] = 'html=html.replace(/\\*\\*([^*]+)\\*\\*/g,"<strong>$1</strong>");'
|
||||
html[#html+1] = 'html=html.replace(/\\*\\*([^*]+)\\*\\*/g,"<strong style=\\"font-weight:600;\\">$1</strong>");'
|
||||
html[#html+1] = 'html=html.replace(/\\*([^*]+)\\*/g,"<em>$1</em>");'
|
||||
-- 链接 [text](url)
|
||||
html[#html+1] = 'html=html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,"<a href=\\"$2\\" target=\\"_blank\\" rel=\\"noopener\\" style=\\"color:#0969da;text-decoration:none;\\">$1</a>");'
|
||||
-- 链接 [text](url) - 悬停效果
|
||||
html[#html+1] = 'html=html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,"<a href=\\"$2\\" target=\\"_blank\\" rel=\\"noopener\\" style=\\"color:#0969da;text-decoration:none;border-bottom:1px solid transparent;transition:border-color 0.2s;\\">$1</a>");'
|
||||
-- 无序列表 (- 或 *)
|
||||
html[#html+1] = 'html=html.replace(/^[*-] (.+)$/gm,"<li style=\\"margin:6px 0 6px 20px;line-height:1.7;\\">$1</li>");'
|
||||
html[#html+1] = 'html=html.replace(/^[*-] (.+)$/gm,"<li style=\\"margin:4px 0 4px 0;padding-left:4px;line-height:1.75;list-style-position:inside;color:#32383f;\\">$1</li>");'
|
||||
-- 有序列表 (1. 2. 等)
|
||||
html[#html+1] = 'html=html.replace(/^\\d+\\. (.+)$/gm,"<li style=\\"margin:6px 0 6px 20px;list-style-type:decimal;line-height:1.7;\\">$1</li>");'
|
||||
html[#html+1] = 'html=html.replace(/^(\\d+)\\. (.+)$/gm,"<li style=\\"margin:4px 0 4px 0;padding-left:4px;line-height:1.75;list-style-position:inside;color:#32383f;\\"><span style=\\"color:#656d76;margin-right:4px;\\">$1.</span>$2</li>");'
|
||||
-- 水平线 (--- 或 ***)
|
||||
html[#html+1] = 'html=html.replace(/^(---|\\*\\*\\*)$/gm,"<hr style=\\"border:none;border-top:1px solid #d0d7de;margin:14px 0;\\"/>");'
|
||||
-- 段落 (连续的非空行合并)
|
||||
html[#html+1] = 'html=html.replace(/\\n\\n/g,"</p><p style=\\"margin:10px 0;line-height:1.7;\\">");'
|
||||
html[#html+1] = 'html=html.replace(/^(---|\\*\\*\\*)$/gm,"<hr style=\\"border:none;border-top:1px solid #d0d7de;margin:16px 0;\\"/>");'
|
||||
-- 段落: 连续空行合并为段落分隔 (简化处理,避免生成未闭合标签)
|
||||
html[#html+1] = 'html=html.replace(/\\n\\n+/g,"<br/><br/>");'
|
||||
-- 换行
|
||||
html[#html+1] = 'html=html.replace(/\\n/g,"<br/>");'
|
||||
html[#html+1] = 'return"<div style=\\"font-size:14px;color:#24292f;line-height:1.7;\\">"+html+"</div>";'
|
||||
-- 外层容器 - 中英文字体栈
|
||||
html[#html+1] = 'return"<div style=\\"font-size:14px;line-height:1.75;color:#32383f;font-family:PingFang SC,Microsoft YaHei,Noto Sans SC,sans-serif;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;\\">"+html+"</div>";'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 检测升级 (只检查插件版本,有新版本时显示更新内容)
|
||||
@ -337,29 +359,31 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = 'var dot=document.getElementById("update-dot");if(dot)dot.style.display="none";'
|
||||
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
|
||||
html[#html+1] = 'var msgs=[];'
|
||||
-- 插件版本检查
|
||||
-- 版本状态徽章样式
|
||||
html[#html+1] = 'var badgeNew="display:inline-flex;align-items:center;padding:3px 10px;border-radius:16px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#fff7e6 0%,#ffe7ba 100%);color:#9a6700;border:1px solid #f5a623;";'
|
||||
html[#html+1] = 'var badgeLatest="display:inline-flex;align-items:center;padding:3px 10px;border-radius:16px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#e6f7e6 0%,#c8f7c8 100%);color:#1a7f1a;border:1px solid #28a745;";'
|
||||
html[#html+1] = 'var badgeUnknown="display:inline-flex;align-items:center;padding:3px 10px;border-radius:16px;font-size:12px;font-weight:600;background:#f6f8fa;color:#656d76;border:1px solid #d0d7de;";'
|
||||
html[#html+1] = 'var verBadge="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-family:SF Mono,Consolas,Menlo,monospace;background:#e1e4e8;color:#24292f;margin-left:4px;";'
|
||||
-- 插件版本检查 (带渐变徽章)
|
||||
html[#html+1] = 'if(r.plugin_current){'
|
||||
html[#html+1] = 'if(r.plugin_has_update){msgs.push("<span style=\\"color:#e36209\\">🔌 插件: v"+r.plugin_current+" → v"+r.plugin_latest+" (有新版本)</span>");}'
|
||||
html[#html+1] = 'else if(r.plugin_latest){msgs.push("<span style=\\"color:green\\">✅ 插件: v"+r.plugin_current+" (已是最新)</span>");}'
|
||||
html[#html+1] = 'else{msgs.push("<span style=\\"color:#999\\">🔌 插件: v"+r.plugin_current+" (无法检查最新版本)</span>");}'
|
||||
html[#html+1] = 'if(r.plugin_has_update){msgs.push("<span style=\\""+badgeNew+"\\">🔌 有新版本</span> v"+r.plugin_current+" → <span style=\\""+verBadge+"\\">v"+r.plugin_latest+"</span>");}'
|
||||
html[#html+1] = 'else if(r.plugin_latest){msgs.push("<span style=\\""+badgeLatest+"\\">✅ 已是最新</span> v"+r.plugin_current);}'
|
||||
html[#html+1] = 'else{msgs.push("<span style=\\""+badgeUnknown+"\\">🔌 无法检查</span> v"+r.plugin_current);}'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'if(msgs.length===0)msgs.push("<span style=\\"color:#999\\">无法获取版本信息</span>");'
|
||||
html[#html+1] = 'if(msgs.length===0)msgs.push("<span style=\\""+badgeUnknown+"\\">无法获取版本信息</span>");'
|
||||
html[#html+1] = 'el.innerHTML=msgs.join("<br/>");'
|
||||
-- 插件有更新时: release notes + 一键升级按钮 + GitHub 下载链接
|
||||
-- 插件有更新时: 卡片式更新日志 + 操作按钮
|
||||
html[#html+1] = 'if(r.plugin_has_update){'
|
||||
html[#html+1] = 'act.style.display="block";'
|
||||
html[#html+1] = 'window._pluginLatestVer=r.plugin_latest;'
|
||||
html[#html+1] = 'var notesHtml="";'
|
||||
html[#html+1] = 'if(r.release_notes){'
|
||||
html[#html+1] = 'var rendered=ocMarkdownToHtml(r.release_notes);'
|
||||
html[#html+1] = 'notesHtml=\'<div style="margin:10px 0 8px;padding:12px 16px;background:#f6f8fa;border:1px solid #d0d7de;border-radius:8px;max-height:400px;overflow-y:auto;">\''
|
||||
html[#html+1] = '+\'<div style="font-size:13px;font-weight:600;color:#24292f;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid #d0d7de;">📋 v\'+r.plugin_latest+\' 更新内容</div>\''
|
||||
html[#html+1] = '+rendered'
|
||||
html[#html+1] = '+\'</div>\';'
|
||||
-- 卡片式容器: 圆角边框 + 微阴影 + 版本标题栏
|
||||
html[#html+1] = 'notesHtml="<div style=\\"margin:12px 0;border:1px solid #d0d7de;border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06);\\"><div style=\\"background:linear-gradient(135deg,#f6f8fa 0%,#ffffff 100%);padding:12px 16px;border-bottom:1px solid #d0d7de;display:flex;align-items:center;justify-content:space-between;\\"><span style=\\"font-size:14px;font-weight:600;color:#24292f;\\">📋 更新日志</span><span style=\\"display:inline-flex;align-items:center;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#e3f2fd 0%,#bbdefb 100%);color:#1565c0;border:1px solid #64b5f6;\\">v"+r.plugin_latest+"</span></div><div style=\\"padding:16px;max-height:450px;overflow-y:auto;background:#fff;\\">"+rendered+"</div></div>";'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'act.innerHTML=notesHtml'
|
||||
html[#html+1] = '+\'<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocPluginUpgrade()" id="btn-plugin-upgrade">⬆️ 升级插件 v\'+r.plugin_latest+\'</button>\''
|
||||
html[#html+1] = '+\' <a href="https://github.com/10000ge10000/luci-app-openclaw/releases/latest" target="_blank" rel="noopener" class="btn cbi-button cbi-button-action" style="text-decoration:none;">📥 手动下载</a>\';'
|
||||
-- 操作按钮区: 分组设计
|
||||
html[#html+1] = 'act.innerHTML=notesHtml+"<div style=\\"margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;\\"><button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"ocPluginUpgrade()\\" id=\\"btn-plugin-upgrade\\" style=\\"box-shadow:0 2px 4px rgba(0,0,0,0.1);\\">⬆️ 一键升级 v"+r.plugin_latest+"</button><a href=\\"https://github.com/10000ge10000/luci-app-openclaw/releases/latest\\" target=\\"_blank\\" rel=\\"noopener\\" class=\\"btn cbi-button cbi-button-action\\" style=\\"text-decoration:none;\\">📥 GitHub 下载</a></div>";'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 检测失败</span>";}'
|
||||
html[#html+1] = '});}'
|
||||
@ -390,10 +414,12 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 轮询插件升级日志 (带容错: 安装时文件被替换可能导致API暂时不可用)
|
||||
-- 复用安装日志的智能滚动机制
|
||||
html[#html+1] = 'var _pluginPollErrors=0;'
|
||||
html[#html+1] = 'function ocPollPluginUpgradeLog(){'
|
||||
html[#html+1] = 'if(_pluginUpgradeTimer)clearInterval(_pluginUpgradeTimer);'
|
||||
html[#html+1] = '_pluginPollErrors=0;'
|
||||
html[#html+1] = '_autoScrollEnabled=true;' -- 重置: 启用自动滚动
|
||||
html[#html+1] = '_pluginUpgradeTimer=setInterval(function(){'
|
||||
html[#html+1] = '(new XHR()).get("' .. plugin_upgrade_log_url .. '",null,function(x){'
|
||||
html[#html+1] = 'try{'
|
||||
@ -402,7 +428,10 @@ act.cfgvalue = function(self, section)
|
||||
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
|
||||
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
|
||||
html[#html+1] = 'if(r.log)logEl.textContent=r.log;'
|
||||
-- 智能滚动: 仅在自动滚动启用时滚动到底部
|
||||
html[#html+1] = 'if(_autoScrollEnabled){'
|
||||
html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'if(r.state==="running"){'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 插件升级中...</span>";'
|
||||
html[#html+1] = '}else if(r.state==="success"){'
|
||||
|
||||
@ -77,8 +77,20 @@ rm -f "$_tmpf"
|
||||
|
||||
patch_iframe_headers() {
|
||||
# 移除 OpenClaw 网关的 X-Frame-Options 和 frame-ancestors 限制,允许 LuCI iframe 嵌入
|
||||
# v2026.3.8: 资产路径可能通过符号链接解析,需同时搜索 OC_GLOBAL 和 NODE_BASE
|
||||
local gw_js
|
||||
# v2026.4.x: 安全头设置迁移到 server.impl-*.js 文件
|
||||
local patched=0
|
||||
|
||||
# 1. 搜索 server.impl-*.js (v2026.4.x 新位置)
|
||||
for f in $(find "${OC_GLOBAL}/lib/node_modules/openclaw/dist" -name "server.impl-*.js" -type f 2>/dev/null); do
|
||||
if grep -q "X-Frame-Options.*DENY\|frame-ancestors.*none" "$f" 2>/dev/null; then
|
||||
sed -i 's|res\.setHeader("X-Frame-Options", "DENY")|res.setHeader("X-Frame-Options", "ALLOW-FROM *") // patched by luci-app-openclaw|g' "$f"
|
||||
sed -i "s|\"frame-ancestors 'none'\"|\"frame-ancestors *\"|g" "$f"
|
||||
logger -t openclaw "Patched iframe headers in $f"
|
||||
patched=1
|
||||
fi
|
||||
done
|
||||
|
||||
# 2. 兼容旧版本: 搜索 gateway-cli-*.js (v2026.3.x)
|
||||
for search_root in "${OC_GLOBAL}" "${NODE_BASE}/lib"; do
|
||||
[ -d "$search_root" ] || continue
|
||||
for f in $(find "$search_root" -name "gateway-cli-*.js" -type f 2>/dev/null); do
|
||||
@ -86,14 +98,17 @@ for search_root in "${OC_GLOBAL}" "${NODE_BASE}/lib"; do
|
||||
sed -i "s|res.setHeader(\"X-Frame-Options\", \"DENY\");|// res.setHeader(\"X-Frame-Options\", \"DENY\"); // patched by luci-app-openclaw|g" "$f"
|
||||
sed -i "s|\"frame-ancestors 'none'\"|\"frame-ancestors *\"|g" "$f"
|
||||
logger -t openclaw "Patched iframe headers in $f"
|
||||
patched=1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
[ $patched -eq 1 ] && logger -t openclaw "iframe headers patched successfully"
|
||||
}
|
||||
|
||||
sync_uci_to_json() {
|
||||
# 将 UCI 配置同步到 openclaw.json,同时确保 token 双向同步
|
||||
local port bind token
|
||||
local port bind token json_token
|
||||
port=$(uci -q get openclaw.main.port || echo "18789")
|
||||
bind=$(uci -q get openclaw.main.bind || echo "lan")
|
||||
token=$(uci -q get openclaw.main.token || echo "")
|
||||
@ -104,14 +119,28 @@ if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo '{}' > "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# UCI 没有 token 时,尝试从已有的 JSON 读取
|
||||
if [ -z "$token" ] && [ -x "$NODE_BIN" ]; then
|
||||
token=$("$NODE_BIN" -e "
|
||||
# 尝试从 JSON 读取 token (用于双向同步检测)
|
||||
if [ -x "$NODE_BIN" ]; then
|
||||
json_token=$("$NODE_BIN" -e "
|
||||
try{const d=JSON.parse(require('fs').readFileSync('${CONFIG_FILE}','utf8'));
|
||||
if(d.gateway&&d.gateway.auth&&d.gateway.auth.token)process.stdout.write(d.gateway.auth.token);}catch(e){}
|
||||
" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# v2026.4.10: 双向同步策略 - JSON 中的 token 优先
|
||||
# 原因: doctor --fix 或其他操作可能修改 JSON 中的 token
|
||||
# 如果 JSON 有 token 且与 UCI 不同,以 JSON 为准
|
||||
if [ -n "$json_token" ] && [ "$json_token" != "$token" ]; then
|
||||
token="$json_token"
|
||||
logger -t openclaw "Token 同步: JSON -> UCI"
|
||||
fi
|
||||
|
||||
# UCI 和 JSON 都没有 token 时,生成一个新的
|
||||
if [ -z "$token" ]; then
|
||||
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || dd if=/dev/urandom bs=24 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48)
|
||||
logger -t openclaw "Token 生成: 新 token"
|
||||
fi
|
||||
|
||||
# 如果仍然没有 token,生成一个新的
|
||||
if [ -z "$token" ]; then
|
||||
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || dd if=/dev/urandom bs=24 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48)
|
||||
@ -162,6 +191,37 @@ d.update.checkOnStart=false;
|
||||
if(d.models&&d.models.providers&&d.models.providers.ollama){const ol=d.models.providers.ollama;if(ol.api==='openai-chat-completions'||ol.api==='openai-completions')ol.api='ollama';if(ol.baseUrl&&ol.baseUrl.endsWith('/v1'))ol.baseUrl=ol.baseUrl.replace(/\/v1$/,'');if(ol.apiKey==='ollama')ol.apiKey='ollama-local';}
|
||||
// v2026.3.7: 清理已废弃的顶层配置键 (loadConfig() 现在会严格校验)
|
||||
['cli','commands.native','commands.nativeSkills','commands.ownerDisplay'].forEach(k=>{const ks=k.split('.');let o=d;for(let i=0;i<ks.length-1;i++){if(!o[ks[i]])return;o=o[ks[i]];}delete o[ks[ks.length-1]];});
|
||||
// v2026.4.5: 清理已废弃的配置别名 (BREAKING CHANGE)
|
||||
// 1. talk.* 命名空间废弃 (voiceId, apiKey)
|
||||
if(d.talk){delete d.talk.voiceId;delete d.talk.apiKey;}
|
||||
// 2. browser.ssrfPolicy.allowPrivateNetwork 废弃
|
||||
if(d.browser&&d.browser.ssrfPolicy){delete d.browser.ssrfPolicy.allowPrivateNetwork;}
|
||||
// 3. hooks.internal.handlers 废弃
|
||||
if(d.hooks&&d.hooks.internal){delete d.hooks.internal.handlers;}
|
||||
// 4. channel/group/room 的 allow 字段迁移到 enabled
|
||||
['channel','group','room'].forEach(k=>{if(d[k]){Object.keys(d[k]).forEach(id=>{if(d[k][id]&&typeof d[k][id].allow!=='undefined'){d[k][id].enabled=d[k][id].allow;delete d[k][id].allow;}});}});
|
||||
// 5. agents.defaults.cliBackends 废弃
|
||||
if(d.agents&&d.agents.defaults){delete d.agents.defaults.cliBackends;}
|
||||
// v2026.4.9: 自动同步 plugins.allow (解决 "plugins.allow is empty" 警告)
|
||||
// 扫描已安装的插件并添加到 allow 列表
|
||||
if(!d.plugins)d.plugins={};
|
||||
if(!Array.isArray(d.plugins.allow))d.plugins.allow=[];
|
||||
// 从 plugins.installs 中提取已安装插件
|
||||
// 注意: 使用 installPath 中的目录名作为插件 ID (与 openclaw.plugin.json 中的 id 一致)
|
||||
if(d.plugins.installs){
|
||||
Object.keys(d.plugins.installs).forEach(key=>{
|
||||
const inst=d.plugins.installs[key];
|
||||
if(inst&&inst.installPath){
|
||||
// 从 installPath 提取插件目录名 (如 openclaw-weixin)
|
||||
const match=inst.installPath.match(/\/([^\/]+)\/?$/);
|
||||
if(match&&match[1]&&!d.plugins.allow.includes(match[1])){
|
||||
d.plugins.allow.push(match[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// 始终信任 copilot-proxy (内置插件)
|
||||
if(!d.plugins.allow.includes('copilot-proxy'))d.plugins.allow.push('copilot-proxy');
|
||||
fs.writeFileSync(f,JSON.stringify(d,null,2));
|
||||
" 2>/dev/null
|
||||
fi
|
||||
@ -252,6 +312,98 @@ fi
|
||||
# 修复数据目录权限 (防止 root 用户操作后留下无法读取的文件)
|
||||
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
|
||||
|
||||
# v2026.4.9: 修复插件目录权限 (OpenClaw 要求插件目录属主为 root)
|
||||
# 详见: https://github.com/nicepkg/openclaw/releases/tag/v2026.4.9
|
||||
local ext_dir="${OC_DATA}/.openclaw/extensions"
|
||||
if [ -d "$ext_dir" ]; then
|
||||
chown -R root:root "$ext_dir" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# v2026.4.x: 清理 jiti 缓存目录 (修复微信插件加载权限问题)
|
||||
# jiti 编译 TypeScript 时会在 /tmp/jiti 创建缓存,如果由 root 创建则 openclaw 用户无法写入
|
||||
rm -rf /tmp/jiti 2>/dev/null || true
|
||||
|
||||
# v2026.4.5: 执行 OpenClaw 官方配置迁移工具
|
||||
# 自动迁移遗留配置到新版本格式
|
||||
_run_config_migration() {
|
||||
local oc_entry="$1"
|
||||
[ -z "$oc_entry" ] && return
|
||||
logger -t openclaw "执行配置迁移 (doctor --fix)..."
|
||||
OPENCLAW_HOME="$OC_DATA" OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
|
||||
"$NODE_BIN" "$oc_entry" doctor --fix 2>/dev/null && \
|
||||
logger -t openclaw "配置迁移完成" || \
|
||||
logger -t openclaw "配置迁移失败,请手动检查"
|
||||
}
|
||||
_run_config_migration "$oc_entry"
|
||||
|
||||
# doctor --fix 后再次修复权限 (防止 root 创建的文件导致 EACCES)
|
||||
# 注意: extensions 目录保持 root 权限 (OpenClaw 安全要求)
|
||||
find "$OC_DATA" -user root ! -path "${OC_DATA}/.openclaw/extensions*" -exec chown openclaw:openclaw {} \; 2>/dev/null || true
|
||||
|
||||
# v2026.4.10: doctor --fix 后确保关键配置字段存在
|
||||
# doctor --fix 可能会删除或遗漏某些必要字段,需要补充
|
||||
_ensure_critical_config() {
|
||||
[ ! -f "$CONFIG_FILE" ] && return
|
||||
[ ! -x "$NODE_BIN" ] && return
|
||||
"$NODE_BIN" -e "
|
||||
const fs = require('fs');
|
||||
const f = '${CONFIG_FILE}';
|
||||
let d = {};
|
||||
try { d = JSON.parse(fs.readFileSync(f, 'utf8')); } catch(e) {}
|
||||
|
||||
// 确保 gateway.mode 存在
|
||||
if (!d.gateway) d.gateway = {};
|
||||
if (!d.gateway.mode) d.gateway.mode = 'local';
|
||||
|
||||
// 确保端口和绑定配置
|
||||
if (!d.gateway.port) d.gateway.port = 18789;
|
||||
if (!d.gateway.bind) d.gateway.bind = 'lan';
|
||||
|
||||
// 确保 controlUi 安全配置存在 (允许非 loopback 绑定和 LuCI iframe 嵌入)
|
||||
if (!d.gateway.controlUi) d.gateway.controlUi = {};
|
||||
if (d.gateway.controlUi.allowInsecureAuth !== true) d.gateway.controlUi.allowInsecureAuth = true;
|
||||
if (d.gateway.controlUi.dangerouslyDisableDeviceAuth !== true) d.gateway.controlUi.dangerouslyDisableDeviceAuth = true;
|
||||
if (d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback !== true) d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true;
|
||||
|
||||
// 确保认证配置
|
||||
if (!d.gateway.auth) d.gateway.auth = {};
|
||||
if (!d.gateway.auth.mode) d.gateway.auth.mode = 'token';
|
||||
|
||||
// 禁用 ACP dispatch (路由器内存有限)
|
||||
if (!d.acp) d.acp = {};
|
||||
if (!d.acp.dispatch) d.acp.dispatch = {};
|
||||
d.acp.dispatch.enabled = false;
|
||||
|
||||
// 工具配置
|
||||
if (!d.tools) d.tools = {};
|
||||
d.tools.profile = 'coding';
|
||||
|
||||
// 禁用更新检查
|
||||
if (!d.update) d.update = {};
|
||||
d.update.checkOnStart = false;
|
||||
|
||||
fs.writeFileSync(f, JSON.stringify(d, null, 2));
|
||||
" 2>/dev/null
|
||||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||||
}
|
||||
_ensure_critical_config
|
||||
|
||||
# v2026.4.10: 最终权限修复 - 确保 Gateway 启动前所有数据目录权限正确
|
||||
# doctor --fix 或 _ensure_critical_config 可能创建新的目录/文件
|
||||
# 关键: agents 目录下的 agent 子目录必须可被 openclaw 用户写入
|
||||
find "${OC_STATE_DIR}" -user root -type d -exec chown openclaw:openclaw {} \; 2>/dev/null || true
|
||||
find "${OC_STATE_DIR}" -user root -type f -exec chown openclaw:openclaw {} \; 2>/dev/null || true
|
||||
# 特别确保 agents/main/agent 目录权限 (模型配置写入目录)
|
||||
local agent_dir="${OC_STATE_DIR}/agents/main/agent"
|
||||
if [ -d "$agent_dir" ]; then
|
||||
chown openclaw:openclaw "$agent_dir" 2>/dev/null || true
|
||||
chmod 755 "$agent_dir" 2>/dev/null || true
|
||||
fi
|
||||
# extensions 目录除外 - 必须保持 root 权限 (OpenClaw 安全要求)
|
||||
if [ -d "${OC_STATE_DIR}/extensions" ]; then
|
||||
chown -R root:root "${OC_STATE_DIR}/extensions" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Patch iframe 安全头,允许 LuCI 嵌入
|
||||
patch_iframe_headers
|
||||
|
||||
@ -305,19 +457,32 @@ _ensure_port_free() {
|
||||
_ensure_port_free "$port"
|
||||
|
||||
# 启动 OpenClaw Gateway (主服务, 前台运行)
|
||||
procd_open_instance "gateway"
|
||||
procd_set_param command "$NODE_BIN" "$oc_entry" gateway run \
|
||||
--port "$port" --bind "$gw_bind"
|
||||
procd_set_param env \
|
||||
HOME="$OC_DATA" \
|
||||
OPENCLAW_HOME="$OC_DATA" \
|
||||
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
||||
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
|
||||
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
|
||||
NODE_BASE="$NODE_BASE" \
|
||||
OC_GLOBAL="$OC_GLOBAL" \
|
||||
OC_DATA="$OC_DATA" \
|
||||
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
# v2026.4.7/4.9: 环境变量安全验证 (阻止危险变量注入)
|
||||
_validate_path() {
|
||||
local p="$1"
|
||||
case "$p" in
|
||||
*'$'*|*'`'*|*'|'*|*'&'*) return 1 ;;
|
||||
/proc/*|/sys/*|/dev/*) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
for _dir in "$NODE_BASE" "$OC_GLOBAL" "$OC_DATA"; do
|
||||
_validate_path "$_dir" || { logger -t openclaw "安全警告: 路径验证失败: $_dir"; return 1; }
|
||||
done
|
||||
|
||||
procd_open_instance "gateway"
|
||||
procd_set_param command "$NODE_BIN" "$oc_entry" gateway run \
|
||||
--port "$port" --bind "$gw_bind"
|
||||
procd_set_param env \
|
||||
HOME="$OC_DATA" \
|
||||
OPENCLAW_HOME="$OC_DATA" \
|
||||
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
||||
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
|
||||
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
|
||||
NODE_BASE="$NODE_BASE" \
|
||||
OC_GLOBAL="$OC_GLOBAL" \
|
||||
OC_DATA="$OC_DATA" \
|
||||
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
procd_set_param user openclaw
|
||||
procd_set_param respawn 3600 10 5
|
||||
procd_set_param stdout 1
|
||||
|
||||
@ -90,4 +90,16 @@ if [ -z "$CURRENT_PTY_TOKEN" ]; then
|
||||
uci commit openclaw
|
||||
fi
|
||||
|
||||
# 启用服务自启动 (创建 /etc/rc.d/S99openclaw 符号链接)
|
||||
# 这一步确保安装/升级后服务能随系统启动
|
||||
# 注意: 仅当 UCI 中 enabled='1' 或尚未设置 enabled 时才启用
|
||||
# 如果用户明确设置 enabled='0' (禁用),则尊重用户选择
|
||||
if [ -x /etc/init.d/openclaw ]; then
|
||||
OC_ENABLED=$(uci -q get openclaw.main.enabled)
|
||||
# enabled='1' 或未设置时启用自启动;enabled='0' 时不操作
|
||||
if [ "$OC_ENABLED" != "0" ]; then
|
||||
/etc/init.d/openclaw enable 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@ -19,7 +19,7 @@ NODE_VERSION_V1="22.15.1"
|
||||
# 默认使用 V2 版本 (可通过 NODE_VERSION 环境变量覆盖)
|
||||
NODE_VERSION="${NODE_VERSION:-${NODE_VERSION_V2}}"
|
||||
# 经过验证的 OpenClaw 稳定版本 (更新此值需同步测试)
|
||||
OC_TESTED_VERSION="2026.3.28"
|
||||
OC_TESTED_VERSION="2026.4.9"
|
||||
# 用户可通过 OC_VERSION 环境变量覆盖安装版本
|
||||
OC_VERSION="${OC_VERSION:-}"
|
||||
|
||||
@ -375,18 +375,6 @@ install_openclaw() {
|
||||
log_error "OpenClaw 安装验证失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装 Gemini CLI (官方模型配置向导的 Google Gemini OAuth 依赖)
|
||||
if [ -x "$NPM_BIN" ]; then
|
||||
echo ""
|
||||
echo "=== 安装 Gemini CLI (Google OAuth 依赖) ==="
|
||||
"$NPM_BIN" install -g @google/gemini-cli --prefix="$OC_GLOBAL" $install_flags 2>&1 | tail -3
|
||||
if command -v gemini >/dev/null 2>&1 || [ -x "$OC_GLOBAL/bin/gemini" ]; then
|
||||
log_info "Gemini CLI 安装成功"
|
||||
else
|
||||
log_warn "Gemini CLI 安装失败 (不影响核心功能,仅影响 Google Gemini OAuth 登录)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
init_openclaw() {
|
||||
@ -635,7 +623,10 @@ do_factory_reset() {
|
||||
log_info "新认证令牌: $new_token"
|
||||
fi
|
||||
|
||||
# 7. 重启服务
|
||||
# 7. 确保数据目录权限正确 (防止 root 操作留下的文件)
|
||||
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
|
||||
|
||||
# 8. 重启服务
|
||||
/etc/init.d/openclaw start >/dev/null 2>&1 &
|
||||
log_info "出厂设置已恢复,Gateway 重启中..."
|
||||
}
|
||||
|
||||
2031
luci-app-openclaw/root/usr/share/openclaw/oc-config-interactive.js
Normal file
2031
luci-app-openclaw/root/usr/share/openclaw/oc-config-interactive.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,10 @@
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
|
||||
|
||||
# ── 交互式菜单引擎路径 ──
|
||||
OC_MENU_ENGINE="/usr/share/openclaw/oc-menu-engine.js"
|
||||
OC_INTERACTIVE="/usr/share/openclaw/oc-config-interactive.js"
|
||||
|
||||
# ── 端口检查兼容函数 (ss 或 netstat) ──
|
||||
# check_port_listening <port> — 检查端口是否在监听,返回 0/1
|
||||
check_port_listening() {
|
||||
@ -200,10 +204,21 @@ enable_auth_plugins() {
|
||||
const d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));
|
||||
if(!d.plugins)d.plugins={};if(!d.plugins.entries)d.plugins.entries={};
|
||||
const e=d.plugins.entries;
|
||||
['qwen-portal-auth','copilot-proxy','google-gemini-cli-auth','minimax-portal-auth'].forEach(p=>{
|
||||
// 启用有效的认证插件
|
||||
['copilot-proxy'].forEach(p=>{
|
||||
if(!e[p])e[p]={};e[p].enabled=true;
|
||||
});
|
||||
// 清理已废弃或未安装的插件配置 (v2026.4.9: 这些插件已移除或更名)
|
||||
delete e['qwen-portal-auth'];
|
||||
delete e['google-gemini-cli-auth'];
|
||||
delete e['minimax-portal-auth'];
|
||||
delete e['google-antigravity-auth'];
|
||||
delete e['openclaw-weixin']; // 微信插件有独立安装流程,不应在 entries 中
|
||||
// 清理过时的 installs 配置
|
||||
if(d.plugins && d.plugins.installs){
|
||||
delete d.plugins.installs['google-gemini-cli-auth'];
|
||||
delete d.plugins.installs['minimax-portal-auth'];
|
||||
}
|
||||
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
|
||||
}catch(e){}
|
||||
" 2>/dev/null
|
||||
@ -641,45 +656,48 @@ show_current_config() {
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
configure_model() {
|
||||
echo ""
|
||||
echo -e " ${BOLD}🤖 配置 AI 模型提供商${NC}"
|
||||
echo -e " ${BOLD}🤖 配置 AI 模型和提供商${NC}"
|
||||
echo ""
|
||||
echo -e " ${GREEN}${BOLD}--- 推荐 ---${NC}"
|
||||
echo -e " ${CYAN}1)${NC} 🌟 官方完整模型配置向导 ${GREEN}(推荐,支持所有提供商)${NC}"
|
||||
echo -e " ${GREEN}${BOLD}🌟 ── 推荐 ──${NC}"
|
||||
echo -e " ${CYAN}w)${NC} 🌟 官方完整模型配置向导 ${GREEN}(推荐,支持所有提供商)${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}--- 快速配置 ---${NC}"
|
||||
echo -e " ${CYAN}2)${NC} OpenAI (GPT-5.2, GPT-5 mini, GPT-4.1)"
|
||||
echo -e " ${CYAN}3)${NC} Anthropic (Claude Sonnet 4, Opus 4, Haiku)"
|
||||
echo -e " ${CYAN}4)${NC} Google Gemini (Gemini 2.5 Pro/Flash, Gemini 3)"
|
||||
echo -e " ${CYAN}5)${NC} OpenRouter (聚合多家模型)"
|
||||
echo -e " ${CYAN}6)${NC} DeepSeek (DeepSeek-V3/R1)"
|
||||
echo -e " ${CYAN}7)${NC} GitHub Copilot (需要 Copilot 订阅)"
|
||||
echo -e " ${CYAN}8)${NC} 阿里云通义千问 Qwen (Portal/API/Coding Plan)"
|
||||
echo -e " ${CYAN}9)${NC} xAI Grok (Grok-3/3-mini)"
|
||||
echo -e " ${CYAN}10)${NC} Groq (Llama 4, Llama 3.3)"
|
||||
echo -e " ${CYAN}11)${NC} 硅基流动 SiliconFlow"
|
||||
echo -e " ${CYAN}12)${NC} Ollama (本地模型,无需 API Key)"
|
||||
echo -e " ${CYAN}13)${NC} 腾讯云 Coding Plan (HY T1/TurboS/GLM-5/Kimi)"
|
||||
echo -e " ${CYAN}14)${NC} Mistral AI (Mistral Large, Codestral)"
|
||||
echo -e " ${CYAN}15)${NC} 百度千帆 (ERNIE-4.0, ERNIE-3.5)"
|
||||
echo -e " ${CYAN}16)${NC} 自定义 OpenAI 兼容 API"
|
||||
echo -e " ${CYAN}0)${NC} 返回"
|
||||
echo -e " ${BOLD}🌍 ── 国外模型提供商 ──${NC}"
|
||||
echo -e " ${CYAN}a)${NC} OpenAI (GPT-5.2, GPT-5 mini, GPT-4.1)"
|
||||
echo -e " ${CYAN}b)${NC} Anthropic (Claude Sonnet 4, Opus 4, Haiku)"
|
||||
echo -e " ${CYAN}c)${NC} Google Gemini (Gemini 2.5 Pro/Flash, Gemini 3)"
|
||||
echo -e " ${CYAN}d)${NC} OpenRouter (聚合多家模型)"
|
||||
echo -e " ${CYAN}e)${NC} GitHub Copilot (需要 Copilot 订阅)"
|
||||
echo -e " ${CYAN}f)${NC} xAI Grok (Grok-4/3)"
|
||||
echo ""
|
||||
prompt_with_default "请选择" "1" choice
|
||||
echo -e " ${BOLD}🇨🇳 ── 国内模型提供商 ──${NC}"
|
||||
echo -e " ${CYAN}g)${NC} 阿里云通义千问 Qwen (Portal/API/Coding Plan)"
|
||||
echo -e " ${CYAN}h)${NC} 硅基流动 SiliconFlow"
|
||||
echo -e " ${CYAN}i)${NC} 腾讯云 Coding Plan (HY T1/TurboS/GLM-5/Kimi)"
|
||||
echo -e " ${CYAN}j)${NC} 百度千帆 (ERNIE-4.0, ERNIE-3.5)"
|
||||
echo -e " ${CYAN}k)${NC} 智谱 GLM / Z.AI (GLM-5.1, GLM-5)"
|
||||
echo ""
|
||||
echo -e " ${BOLD}🏠 ── 本地模型 / 自定义 API ──${NC}"
|
||||
echo -e " ${CYAN}l)${NC} Ollama (本地模型,无需 API Key)"
|
||||
echo -e " ${CYAN}m)${NC} 自定义 OpenAI 兼容 API"
|
||||
echo ""
|
||||
echo -e " ${CYAN}q)${NC} 返回"
|
||||
echo ""
|
||||
prompt_with_default "请选择" "w" choice
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
w)
|
||||
echo ""
|
||||
echo -e " ${CYAN}启动官方完整模型配置向导...${NC}"
|
||||
echo -e " ${YELLOW}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}预启用模型认证插件...${NC}"
|
||||
echo -e " ${CYAN}清理过时插件配置...${NC}"
|
||||
enable_auth_plugins
|
||||
echo ""
|
||||
(oc_cmd configure --section model) || echo -e " ${YELLOW}配置向导已退出${NC}"
|
||||
echo ""
|
||||
ask_restart
|
||||
;;
|
||||
2)
|
||||
a)
|
||||
echo ""
|
||||
echo -e " ${BOLD}OpenAI 配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://platform.openai.com/api-keys${NC}"
|
||||
@ -712,7 +730,7 @@ configure_model() {
|
||||
echo -e " ${GREEN}✅ OpenAI 已配置,活跃模型: openai/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
b)
|
||||
echo ""
|
||||
echo -e " ${BOLD}Anthropic 配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://console.anthropic.com/settings/keys${NC}"
|
||||
@ -743,7 +761,7 @@ configure_model() {
|
||||
echo -e " ${GREEN}✅ Anthropic 已配置,活跃模型: anthropic/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
c)
|
||||
echo ""
|
||||
echo -e " ${BOLD}Google Gemini 配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://aistudio.google.com/apikey${NC}"
|
||||
@ -774,7 +792,7 @@ configure_model() {
|
||||
echo -e " ${GREEN}✅ Google Gemini 已配置,活跃模型: google/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
5)
|
||||
d)
|
||||
echo ""
|
||||
echo -e " ${BOLD}OpenRouter 配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://openrouter.ai/keys${NC}"
|
||||
@ -808,33 +826,7 @@ configure_model() {
|
||||
echo -e " ${GREEN}✅ OpenRouter 已配置,活跃模型: openrouter/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
6)
|
||||
echo ""
|
||||
echo -e " ${BOLD}DeepSeek 配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://platform.deepseek.com/api_keys${NC}"
|
||||
echo ""
|
||||
prompt_with_default "请输入 DeepSeek API Key" "" api_key
|
||||
if [ -n "$api_key" ]; then
|
||||
echo ""
|
||||
echo -e " ${CYAN}可用模型:${NC}"
|
||||
echo -e " ${CYAN}a)${NC} deepseek-chat — DeepSeek-V3 (通用对话)"
|
||||
echo -e " ${CYAN}b)${NC} deepseek-reasoner — DeepSeek-R1 (深度推理)"
|
||||
echo -e " ${CYAN}c)${NC} 手动输入模型名"
|
||||
echo ""
|
||||
prompt_with_default "请选择模型" "a" model_choice
|
||||
case "$model_choice" in
|
||||
a) model_name="deepseek-chat" ;;
|
||||
b) model_name="deepseek-reasoner" ;;
|
||||
c) prompt_with_default "请输入模型名称" "deepseek-chat" model_name ;;
|
||||
*) model_name="deepseek-chat" ;;
|
||||
esac
|
||||
auth_set_apikey deepseek "$api_key"
|
||||
register_custom_provider deepseek "https://api.deepseek.com/v1" "$api_key" "$model_name" "$model_name"
|
||||
register_and_set_model "deepseek/${model_name}"
|
||||
echo -e " ${GREEN}✅ DeepSeek 已配置,活跃模型: deepseek/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
7)
|
||||
e)
|
||||
echo ""
|
||||
echo -e " ${BOLD}GitHub Copilot 配置${NC}"
|
||||
echo -e " ${YELLOW}需要有效的 GitHub Copilot 订阅 (Free/Pro/Business 均可)${NC}"
|
||||
@ -889,7 +881,7 @@ configure_model() {
|
||||
echo -e " ${YELLOW}OAuth 授权已退出或失败${NC}"
|
||||
fi
|
||||
;;
|
||||
8)
|
||||
g)
|
||||
echo ""
|
||||
echo -e " ${BOLD}阿里云通义千问 Qwen 配置${NC}"
|
||||
echo ""
|
||||
@ -1016,7 +1008,7 @@ configure_model() {
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
9)
|
||||
f)
|
||||
echo ""
|
||||
echo -e " ${BOLD}xAI Grok 配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://console.x.ai${NC}"
|
||||
@ -1049,41 +1041,7 @@ configure_model() {
|
||||
echo -e " ${GREEN}✅ xAI Grok 已配置,活跃模型: xai/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
10)
|
||||
echo ""
|
||||
echo -e " ${BOLD}Groq 配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://console.groq.com/keys${NC}"
|
||||
echo -e " ${YELLOW}Groq 提供超快推理速度${NC}"
|
||||
echo ""
|
||||
prompt_with_default "请输入 Groq API Key" "" api_key
|
||||
if [ -n "$api_key" ]; then
|
||||
echo ""
|
||||
echo -e " ${CYAN}可用模型:${NC}"
|
||||
echo -e " ${CYAN}a)${NC} meta-llama/llama-4-maverick-17b-128e-instruct — Llama 4 Maverick (推荐)"
|
||||
echo -e " ${CYAN}b)${NC} meta-llama/llama-4-scout-17b-16e-instruct — Llama 4 Scout"
|
||||
echo -e " ${CYAN}c)${NC} moonshotai/kimi-k2-instruct — Kimi K2"
|
||||
echo -e " ${CYAN}d)${NC} qwen/qwen3-32b — 通义千问 Qwen3 32B"
|
||||
echo -e " ${CYAN}e)${NC} llama-3.3-70b-versatile — Llama 3.3 70B"
|
||||
echo -e " ${CYAN}f)${NC} llama-3.1-8b-instant — Llama 3.1 8B (极速)"
|
||||
echo -e " ${CYAN}g)${NC} 手动输入模型名"
|
||||
echo ""
|
||||
prompt_with_default "请选择模型" "a" model_choice
|
||||
case "$model_choice" in
|
||||
a) model_name="meta-llama/llama-4-maverick-17b-128e-instruct" ;;
|
||||
b) model_name="meta-llama/llama-4-scout-17b-16e-instruct" ;;
|
||||
c) model_name="moonshotai/kimi-k2-instruct" ;;
|
||||
d) model_name="qwen/qwen3-32b" ;;
|
||||
e) model_name="llama-3.3-70b-versatile" ;;
|
||||
f) model_name="llama-3.1-8b-instant" ;;
|
||||
g) prompt_with_default "请输入模型名称" "meta-llama/llama-4-maverick-17b-128e-instruct" model_name ;;
|
||||
*) model_name="meta-llama/llama-4-maverick-17b-128e-instruct" ;;
|
||||
esac
|
||||
auth_set_apikey groq "$api_key"
|
||||
register_and_set_model "groq/${model_name}"
|
||||
echo -e " ${GREEN}✅ Groq 已配置,活跃模型: groq/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
11)
|
||||
h)
|
||||
echo ""
|
||||
echo -e " ${BOLD}硅基流动 SiliconFlow 配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://cloud.siliconflow.cn/account/ak${NC}"
|
||||
@ -1130,7 +1088,7 @@ configure_model() {
|
||||
echo -e " ${GREEN}✅ SiliconFlow 已配置,活跃模型: siliconflow/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
12)
|
||||
l)
|
||||
echo ""
|
||||
echo -e " ${BOLD}🦙 Ollama 本地模型配置${NC}"
|
||||
echo -e " ${YELLOW}Ollama 在本地或局域网运行大模型,无需 API Key${NC}"
|
||||
@ -1246,7 +1204,7 @@ configure_model() {
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
13)
|
||||
i)
|
||||
echo ""
|
||||
echo -e " ${BOLD}腾讯云大模型 Coding Plan 套餐配置${NC}"
|
||||
echo ""
|
||||
@ -1295,37 +1253,7 @@ configure_model() {
|
||||
echo -e " ${DIM}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${NC}"
|
||||
fi
|
||||
;;
|
||||
14)
|
||||
echo ""
|
||||
echo -e " ${BOLD}Mistral AI 配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://console.mistral.ai/api-keys/${NC}"
|
||||
echo ""
|
||||
prompt_with_default "请输入 Mistral API Key" "" api_key
|
||||
if [ -n "$api_key" ]; then
|
||||
auth_set_apikey mistral "$api_key"
|
||||
echo ""
|
||||
echo -e " ${CYAN}可用模型:${NC}"
|
||||
echo -e " ${CYAN}a)${NC} mistral-large-latest — 旗舰模型,最强性能 (推荐)"
|
||||
echo -e " ${CYAN}b)${NC} mistral-medium-latest — 均衡模型"
|
||||
echo -e " ${CYAN}c)${NC} codestral-latest — 代码专用,极速补全"
|
||||
echo -e " ${CYAN}d)${NC} mistral-small-latest — 轻量快速"
|
||||
echo -e " ${CYAN}e)${NC} 手动输入模型名"
|
||||
echo ""
|
||||
prompt_with_default "请选择模型" "a" model_choice
|
||||
case "$model_choice" in
|
||||
a) model_name="mistral-large-latest" ;;
|
||||
b) model_name="mistral-medium-latest" ;;
|
||||
c) model_name="codestral-latest" ;;
|
||||
d) model_name="mistral-small-latest" ;;
|
||||
e) prompt_with_default "请输入模型名称" "mistral-large-latest" model_name ;;
|
||||
*) model_name="mistral-large-latest" ;;
|
||||
esac
|
||||
register_custom_provider mistral "https://api.mistral.ai/v1" "$api_key" "$model_name" "$model_name"
|
||||
register_and_set_model "mistral/${model_name}"
|
||||
echo -e " ${GREEN}✅ Mistral AI 已配置,活跃模型: mistral/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
15)
|
||||
j)
|
||||
echo ""
|
||||
echo -e " ${BOLD}百度千帆大模型配置${NC}"
|
||||
echo -e " ${YELLOW}获取 API Key: https://console.bce.baidu.com/qianfan/ais/console/onlineService${NC}"
|
||||
@ -1359,7 +1287,70 @@ configure_model() {
|
||||
echo -e " ${GREEN}✅ 百度千帆已配置,活跃模型: qianfan/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
16)
|
||||
k)
|
||||
echo ""
|
||||
echo -e " ${BOLD}智谱 GLM / Z.AI 配置${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}认证方式:${NC}"
|
||||
echo -e " ${CYAN}a)${NC} CN (open.bigmodel.cn) ${GREEN}★ 国内用户推荐${NC}"
|
||||
echo -e " ${CYAN}b)${NC} Coding-Plan-CN (智谱 Coding Plan 套餐)"
|
||||
echo -e " ${CYAN}c)${NC} Global (api.z.ai)"
|
||||
echo -e " ${CYAN}d)${NC} 手动输入 API Key"
|
||||
echo ""
|
||||
prompt_with_default "请选择认证方式" "a" zai_method
|
||||
case "$zai_method" in
|
||||
a)
|
||||
zai_base_url="https://open.bigmodel.cn/api/paas/v4"
|
||||
echo -e " ${YELLOW}获取 API Key: https://open.bigmodel.cn/api-key${NC}"
|
||||
;;
|
||||
b)
|
||||
zai_base_url="https://open.bigmodel.cn/api/coding/paas/v4"
|
||||
echo -e " ${YELLOW}Coding Plan 套餐 API Key (sk-...)${NC}"
|
||||
;;
|
||||
c)
|
||||
zai_base_url="https://api.z.ai/api/paas/v4"
|
||||
echo -e " ${YELLOW}全球版 API Key${NC}"
|
||||
;;
|
||||
d)
|
||||
prompt_with_default "请输入 Base URL" "https://open.bigmodel.cn/api/paas/v4" zai_base_url
|
||||
;;
|
||||
*)
|
||||
zai_base_url="https://open.bigmodel.cn/api/paas/v4"
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
prompt_with_default "请输入智谱 API Key" "" api_key
|
||||
if [ -n "$api_key" ]; then
|
||||
echo ""
|
||||
echo -e " ${CYAN}可用模型:${NC}"
|
||||
echo -e " ${CYAN}a)${NC} glm-5.1 — GLM-5.1 (推荐)"
|
||||
echo -e " ${CYAN}b)${NC} glm-5 — GLM-5"
|
||||
echo -e " ${CYAN}c)${NC} glm-4.7 — GLM-4.7"
|
||||
echo -e " ${CYAN}d)${NC} glm-4.7-flash — GLM-4.7 Flash"
|
||||
echo -e " ${CYAN}e)${NC} glm-4.5 — GLM-4.5"
|
||||
echo -e " ${CYAN}f)${NC} glm-4.5-flash — GLM-4.5 Flash (免费额度)"
|
||||
echo -e " ${CYAN}g)${NC} 手动输入模型名"
|
||||
echo ""
|
||||
prompt_with_default "请选择模型" "a" model_choice
|
||||
case "$model_choice" in
|
||||
a) model_name="glm-5.1" ;;
|
||||
b) model_name="glm-5" ;;
|
||||
c) model_name="glm-4.7" ;;
|
||||
d) model_name="glm-4.7-flash" ;;
|
||||
e) model_name="glm-4.5" ;;
|
||||
f) model_name="glm-4.5-flash" ;;
|
||||
g) prompt_with_default "请输入模型名称" "glm-5.1" model_name ;;
|
||||
*) model_name="glm-5.1" ;;
|
||||
esac
|
||||
# 智谱 GLM 使用原生 zai provider (OpenClaw 内置支持)
|
||||
auth_set_apikey zai "$api_key"
|
||||
register_custom_provider zai "$zai_base_url" "$api_key" "$model_name" "$model_name" "128000" "4096"
|
||||
register_and_set_model "zai/${model_name}"
|
||||
echo -e " ${GREEN}✅ 智谱 GLM 已配置,活跃模型: zai/${model_name}${NC}"
|
||||
echo -e " ${DIM} Base URL: ${zai_base_url}${NC}"
|
||||
fi
|
||||
;;
|
||||
m)
|
||||
echo ""
|
||||
echo -e " ${BOLD}自定义 OpenAI 兼容 API${NC}"
|
||||
echo -e " ${YELLOW}支持任何兼容 OpenAI API 格式的服务商${NC}"
|
||||
@ -1374,7 +1365,7 @@ configure_model() {
|
||||
echo -e " ${GREEN}✅ 自定义模型已配置,活跃模型: openai-compatible/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
0) return ;;
|
||||
q) return ;;
|
||||
esac
|
||||
|
||||
if [ "$choice" != "0" ] && [ "$choice" != "1" ]; then
|
||||
@ -2227,7 +2218,7 @@ reset_to_defaults() {
|
||||
echo -e " ${CYAN}已取消${NC}"
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
c)
|
||||
echo ""
|
||||
echo -e " ${RED}╔══════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e " ${RED}║ ⚠️ 完全恢复出厂设置 ║${NC}"
|
||||
@ -2409,7 +2400,7 @@ backup_restore_menu() {
|
||||
oc_cmd backup verify "$latest" 2>&1
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
c)
|
||||
echo ""
|
||||
if [ -d "$backup_dir" ]; then
|
||||
local count=$(ls "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | wc -l)
|
||||
@ -2427,7 +2418,7 @@ backup_restore_menu() {
|
||||
echo ""
|
||||
echo -e " ${DIM}备份目录: ${backup_dir}${NC}"
|
||||
;;
|
||||
5)
|
||||
d)
|
||||
local latest=$(ls -t "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)
|
||||
if [ -z "$latest" ]; then
|
||||
echo -e " ${YELLOW}未找到备份文件,请先创建备份${NC}"
|
||||
@ -2479,7 +2470,60 @@ backup_restore_menu() {
|
||||
esac
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# 交互式菜单 (方向键导航)
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# 检测是否支持交互模式 (需要 Node.js + TTY 终端)
|
||||
can_use_interactive() {
|
||||
[ -x "$NODE_BIN" ] || return 1
|
||||
[ -f "$OC_INTERACTIVE" ] || return 1
|
||||
[ -f "$OC_MENU_ENGINE" ] || return 1
|
||||
# 检测是否为真实 TTY (排除某些非交互环境)
|
||||
[ -t 0 ] && [ -t 1 ] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# 启动交互式菜单 (方向键导航版本)
|
||||
launch_interactive_menu() {
|
||||
if ! can_use_interactive; then
|
||||
echo -e " ${YELLOW}⚠️ 当前环境不支持交互模式,使用传统菜单${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 调用 Node.js 交互式前端
|
||||
"$NODE_BIN" "$OC_INTERACTIVE" 2>&1
|
||||
local rc=$?
|
||||
|
||||
# 返回后刷新配置权限
|
||||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||||
return $rc
|
||||
}
|
||||
|
||||
# 启动交互式模型配置
|
||||
launch_interactive_model_config() {
|
||||
if ! can_use_interactive; then
|
||||
echo -e " ${YELLOW}⚠️ 当前环境不支持交互模式,使用传统菜单${NC}"
|
||||
configure_model # 回退到传统菜单
|
||||
return $?
|
||||
fi
|
||||
|
||||
# 调用 Node.js 交互式前端的模型配置模块
|
||||
"$NODE_BIN" "$OC_INTERACTIVE" model 2>&1
|
||||
local rc=$?
|
||||
|
||||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||||
return $rc
|
||||
}
|
||||
|
||||
main_menu() {
|
||||
# 直接启动交互模式 (如果支持)
|
||||
if can_use_interactive && [ "${OC_FORCE_TRADITIONAL:-0}" != "1" ]; then
|
||||
launch_interactive_menu
|
||||
return $?
|
||||
fi
|
||||
|
||||
# 传统菜单 (回退方案)
|
||||
while true; do
|
||||
echo ""
|
||||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
@ -2693,6 +2737,29 @@ advanced_menu() {
|
||||
|
||||
# ── 支持命令行参数 ──
|
||||
case "${1:-}" in
|
||||
--tui|--interactive)
|
||||
# 强制启动交互式菜单 (方向键导航)
|
||||
if can_use_interactive; then
|
||||
launch_interactive_menu
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ 当前环境不支持交互模式${NC}"
|
||||
echo "需要: Node.js + TTY 终端"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
--traditional)
|
||||
# 强制使用传统菜单
|
||||
export OC_FORCE_TRADITIONAL=1
|
||||
main_menu
|
||||
;;
|
||||
--model)
|
||||
# 直接进入模型配置
|
||||
if can_use_interactive; then
|
||||
launch_interactive_model_config
|
||||
else
|
||||
configure_model
|
||||
fi
|
||||
;;
|
||||
--set)
|
||||
if [ -n "${2:-}" ] && [ -n "${3:-}" ]; then
|
||||
json_set "$2" "$3"
|
||||
@ -2727,12 +2794,21 @@ case "${1:-}" in
|
||||
echo "OpenClaw AI Gateway — OpenWrt 配置管理工具"
|
||||
echo ""
|
||||
echo "用法:"
|
||||
echo " oc-config.sh # 进入交互式菜单"
|
||||
echo " oc-config.sh # 进入交互式菜单 (自动检测)"
|
||||
echo " oc-config.sh --tui # 强制使用方向键交互模式"
|
||||
echo " oc-config.sh --traditional # 强制使用传统数字菜单"
|
||||
echo " oc-config.sh --model # 直接进入模型配置"
|
||||
echo " oc-config.sh --set K V # 设置配置项"
|
||||
echo " oc-config.sh --get K # 读取配置项"
|
||||
echo " oc-config.sh --restart # 重启 Gateway"
|
||||
echo " oc-config.sh --status # 查看状态"
|
||||
echo ""
|
||||
echo "交互模式快捷键:"
|
||||
echo " ↑↓ 导航选项"
|
||||
echo " 回车 确认选择"
|
||||
echo " ESC/q 返回上级"
|
||||
echo " 字符 搜索过滤"
|
||||
echo ""
|
||||
;;
|
||||
"")
|
||||
main_menu
|
||||
|
||||
727
luci-app-openclaw/root/usr/share/openclaw/oc-menu-engine.js
Normal file
727
luci-app-openclaw/root/usr/share/openclaw/oc-menu-engine.js
Normal file
@ -0,0 +1,727 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* OpenClaw 配置工具 — 交互式菜单引擎
|
||||
* ============================================================================
|
||||
*
|
||||
* 功能特性:
|
||||
* - 方向键 (↑↓) 导航菜单选项
|
||||
* - 回车/空格 确认选择
|
||||
* - Tab 在多列菜单中切换
|
||||
* - ESC/q 返回上级菜单
|
||||
* - 搜索过滤 (输入字符实时过滤)
|
||||
* - 纯 Node.js 实现,零外部依赖
|
||||
* - 完全兼容 xterm.js / Web PTY 环境
|
||||
*
|
||||
* 使用方式:
|
||||
* const menu = require('./oc-menu-engine');
|
||||
* const choice = await menu.select({
|
||||
* title: '选择模型提供商',
|
||||
* items: [
|
||||
* { key: 'a', label: 'OpenAI', desc: 'GPT-5, GPT-4.1' },
|
||||
* { key: 'b', label: 'Anthropic', desc: 'Claude' },
|
||||
* ]
|
||||
* });
|
||||
*/
|
||||
|
||||
// ANSI 颜色代码
|
||||
const C = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
italic: '\x1b[3m',
|
||||
underline: '\x1b[4m',
|
||||
// 前景色
|
||||
black: '\x1b[30m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m',
|
||||
white: '\x1b[37m',
|
||||
// 亮色
|
||||
brightRed: '\x1b[91m',
|
||||
brightGreen: '\x1b[92m',
|
||||
brightYellow: '\x1b[93m',
|
||||
brightBlue: '\x1b[94m',
|
||||
brightMagenta: '\x1b[95m',
|
||||
brightCyan: '\x1b[96m',
|
||||
// 背景色
|
||||
bgBlack: '\x1b[40m',
|
||||
bgRed: '\x1b[41m',
|
||||
bgGreen: '\x1b[42m',
|
||||
bgYellow: '\x1b[43m',
|
||||
bgBlue: '\x1b[44m',
|
||||
bgMagenta: '\x1b[45m',
|
||||
bgCyan: '\x1b[46m',
|
||||
bgWhite: '\x1b[47m',
|
||||
// 光标控制
|
||||
hide: '\x1b[?25l',
|
||||
show: '\x1b[?25h',
|
||||
save: '\x1b[s',
|
||||
restore: '\x1b[u',
|
||||
clearLine: '\x1b[2K',
|
||||
clearDown: '\x1b[J',
|
||||
// 移动
|
||||
up: (n = 1) => `\x1b[${n}A`,
|
||||
down: (n = 1) => `\x1b[${n}B`,
|
||||
right: (n = 1) => `\x1b[${n}C`,
|
||||
left: (n = 1) => `\x1b[${n}D`,
|
||||
toCol: (n) => `\x1b[${n}G`,
|
||||
toRow: (n) => `\x1b[${n}H`,
|
||||
to: (row, col) => `\x1b[${row};${col}H`,
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 终端输入处理
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 设置终端为原始模式 (逐字节读取)
|
||||
*/
|
||||
function setRawMode(enable) {
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(enable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 stdin 读取单个按键
|
||||
* 支持 Bracketed Paste Mode (粘贴检测)
|
||||
* @returns {Promise<{name: string, sequence: string, char: string, paste?: string}>}
|
||||
*/
|
||||
function readKey() {
|
||||
return new Promise((resolve) => {
|
||||
const onData = (buffer) => {
|
||||
process.stdin.off('data', onData);
|
||||
|
||||
const seq = buffer.toString('utf8');
|
||||
|
||||
// 解析按键
|
||||
let key = { sequence: seq, char: '', name: '' };
|
||||
|
||||
// 检测 Bracketed Paste (粘贴操作)
|
||||
// 终端在粘贴时发送: ESC[200~ <粘贴内容> ESC[201~
|
||||
if (seq.startsWith('\x1b[200~') && seq.endsWith('\x1b[201~')) {
|
||||
// 提取粘贴的内容 (去掉首尾的转义序列)
|
||||
const pasteContent = seq.slice(6, -6);
|
||||
key.name = 'paste';
|
||||
key.paste = pasteContent;
|
||||
key.char = '';
|
||||
resolve(key);
|
||||
return;
|
||||
}
|
||||
|
||||
if (seq === '\r' || seq === '\n') {
|
||||
key.name = 'return';
|
||||
} else if (seq === '\t') {
|
||||
key.name = 'tab';
|
||||
} else if (seq === '\x1b') {
|
||||
key.name = 'escape';
|
||||
} else if (seq === '\x7f' || seq === '\x08') {
|
||||
key.name = 'backspace';
|
||||
} else if (seq === '\x1b[A') {
|
||||
key.name = 'up';
|
||||
} else if (seq === '\x1b[B') {
|
||||
key.name = 'down';
|
||||
} else if (seq === '\x1b[C') {
|
||||
key.name = 'right';
|
||||
} else if (seq === '\x1b[D') {
|
||||
key.name = 'left';
|
||||
} else if (seq === '\x1b[1;2A' || seq === '\x1b[1;2B') {
|
||||
// Shift+Up/Down - 可用于快速滚动
|
||||
key.name = seq.endsWith('A') ? 'shift-up' : 'shift-down';
|
||||
} else if (seq === '\x1b[5~') {
|
||||
key.name = 'pageup';
|
||||
} else if (seq === '\x1b[6~') {
|
||||
key.name = 'pagedown';
|
||||
} else if (seq === '\x1b[H') {
|
||||
key.name = 'home';
|
||||
} else if (seq === '\x1b[F') {
|
||||
key.name = 'end';
|
||||
} else if (seq.length === 1 && seq >= ' ' && seq <= '~') {
|
||||
key.char = seq;
|
||||
key.name = seq.toLowerCase();
|
||||
} else if (seq.startsWith('\x1b[') && seq.endsWith('~')) {
|
||||
// 功能键 F1-F12
|
||||
const code = seq.slice(2, -1);
|
||||
if (code >= '11' && code <= '24') {
|
||||
key.name = `f${parseInt(code) - 10}`;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(key);
|
||||
};
|
||||
|
||||
process.stdin.once('data', onData);
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 交互式选择菜单
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// 记录上一次渲染的菜单行数,用于正确清除
|
||||
let lastRenderLines = 0;
|
||||
|
||||
/**
|
||||
* 重置渲染计数器 (在打印非菜单内容前调用)
|
||||
*/
|
||||
function resetRenderCount() {
|
||||
lastRenderLines = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染菜单选项
|
||||
* @param {Array} items 菜单项数组
|
||||
* @param {number} selected 当前选中索引
|
||||
* @param {string} searchFilter 搜索过滤字符串
|
||||
* @param {object} options 配置选项
|
||||
* @returns {Array} 过滤后的菜单项
|
||||
*/
|
||||
function renderMenu(items, selected, searchFilter = '', options = {}) {
|
||||
const {
|
||||
title = '请选择',
|
||||
header = '',
|
||||
footer = '',
|
||||
showSearch = true,
|
||||
showHelp = true,
|
||||
maxWidth = 80,
|
||||
} = options;
|
||||
|
||||
// 过滤项目
|
||||
let filteredItems = items;
|
||||
if (searchFilter) {
|
||||
const filter = searchFilter.toLowerCase();
|
||||
filteredItems = items.filter(item =>
|
||||
(item.label || '').toLowerCase().includes(filter) ||
|
||||
(item.desc || '').toLowerCase().includes(filter) ||
|
||||
(item.key || '').toLowerCase().includes(filter)
|
||||
);
|
||||
}
|
||||
|
||||
// 输出缓冲
|
||||
const lines = [];
|
||||
|
||||
// 标题
|
||||
if (title) {
|
||||
lines.push(`${C.bold}${C.cyan} ${title}${C.reset}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// 头部信息
|
||||
if (header) {
|
||||
lines.push(` ${header}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// 搜索框
|
||||
if (showSearch && searchFilter !== undefined) {
|
||||
const searchPrompt = searchFilter
|
||||
? `${C.dim}搜索: ${C.reset}${C.yellow}${searchFilter}${C.reset}${C.dim}█${C.reset}`
|
||||
: `${C.dim}搜索: ${C.reset}${C.dim}(输入字符过滤,ESC 清空)${C.reset}`;
|
||||
lines.push(` ${searchPrompt}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// 菜单项
|
||||
filteredItems.forEach((item, idx) => {
|
||||
const isSelected = idx === selected;
|
||||
const isDisabled = item.disabled;
|
||||
|
||||
// 选中态样式 - 使用背景色高亮整行
|
||||
const cursor = isSelected ? `${C.cyan}▸${C.reset} ` : ' ';
|
||||
const labelStyle = isSelected ? `${C.bold}${C.cyan}` : (isDisabled ? C.dim : '');
|
||||
const descStyle = isSelected ? C.cyan : C.dim;
|
||||
|
||||
// 构建行
|
||||
const keyLabel = item.key ? `${C.yellow}${item.key}${C.reset}` : '';
|
||||
const mainLabel = `${labelStyle}${item.label || ''}${C.reset}`;
|
||||
const descLabel = item.desc ? `${descStyle}— ${item.desc}${C.reset}` : '';
|
||||
|
||||
// 拼接
|
||||
let line = cursor;
|
||||
if (keyLabel) line += `${C.dim}[${C.reset}${keyLabel}${C.dim}]${C.reset} `;
|
||||
line += mainLabel;
|
||||
if (descLabel && !isDisabled) line += ` ${descLabel}`;
|
||||
|
||||
lines.push(line);
|
||||
});
|
||||
|
||||
// 空状态
|
||||
if (filteredItems.length === 0) {
|
||||
lines.push(` ${C.dim}(没有匹配的项目)${C.reset}`);
|
||||
}
|
||||
|
||||
// 底部帮助
|
||||
if (showHelp) {
|
||||
lines.push('');
|
||||
lines.push(` ${C.dim}↑↓ 导航 回车 确认 ESC/q 返回${showSearch ? ' 输入 搜索' : ''}${C.reset}`);
|
||||
}
|
||||
|
||||
// 自定义底部
|
||||
if (footer) {
|
||||
lines.push('');
|
||||
lines.push(` ${footer}`);
|
||||
}
|
||||
|
||||
// 计算需要的清除行数
|
||||
const totalLines = lines.length + 1;
|
||||
|
||||
// 清除之前的内容: 移动到开头,清除到屏幕底部
|
||||
let clearSeq = '\r';
|
||||
if (lastRenderLines > 0) {
|
||||
// 移动到上一次渲染的第一行
|
||||
clearSeq += `\x1b[${lastRenderLines}A`;
|
||||
// 清除从此行到屏幕底部
|
||||
clearSeq += C.clearDown;
|
||||
}
|
||||
lastRenderLines = totalLines;
|
||||
|
||||
// 输出: 清除旧内容 + 新内容
|
||||
process.stdout.write(clearSeq + lines.join('\n') + '\n');
|
||||
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 交互式单选菜单
|
||||
* @param {object} options 配置选项
|
||||
* @returns {Promise<object|null>} 选中的项目或 null (取消)
|
||||
*/
|
||||
async function select(options = {}) {
|
||||
const {
|
||||
items = [],
|
||||
title = '请选择',
|
||||
header = '',
|
||||
footer = '',
|
||||
defaultIndex = 0,
|
||||
showSearch = true,
|
||||
allowCancel = true,
|
||||
exitKeys = ['escape', 'q'],
|
||||
onSelect = null, // 选择回调 (可用于预览)
|
||||
} = options;
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 重置渲染行计数
|
||||
lastRenderLines = 0;
|
||||
|
||||
// 初始化状态
|
||||
let selected = Math.min(defaultIndex, items.length - 1);
|
||||
let searchFilter = '';
|
||||
let filteredItems = items;
|
||||
let done = false;
|
||||
let result = null;
|
||||
|
||||
// 保存终端状态
|
||||
setRawMode(true);
|
||||
process.stdout.write(C.hide);
|
||||
process.stdout.write(C.save);
|
||||
|
||||
try {
|
||||
// 初始渲染
|
||||
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
|
||||
|
||||
while (!done) {
|
||||
const key = await readKey();
|
||||
|
||||
// 处理按键
|
||||
if (key.name === 'return') {
|
||||
// 确认选择
|
||||
if (filteredItems.length > 0 && !filteredItems[selected].disabled) {
|
||||
result = filteredItems[selected];
|
||||
done = true;
|
||||
}
|
||||
} else if (key.name === 'up') {
|
||||
// 上移
|
||||
if (selected > 0) {
|
||||
selected--;
|
||||
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
|
||||
}
|
||||
} else if (key.name === 'down') {
|
||||
// 下移
|
||||
if (selected < filteredItems.length - 1) {
|
||||
selected++;
|
||||
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
|
||||
}
|
||||
} else if (key.name === 'pageup') {
|
||||
// 上翻页
|
||||
selected = Math.max(0, selected - 10);
|
||||
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
|
||||
} else if (key.name === 'pagedown') {
|
||||
// 下翻页
|
||||
selected = Math.min(filteredItems.length - 1, selected + 10);
|
||||
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
|
||||
} else if (key.name === 'home') {
|
||||
selected = 0;
|
||||
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
|
||||
} else if (key.name === 'end') {
|
||||
selected = filteredItems.length - 1;
|
||||
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
|
||||
} else if (exitKeys.includes(key.name) && allowCancel) {
|
||||
// 取消/退出
|
||||
done = true;
|
||||
} else if (key.name === 'backspace') {
|
||||
// 删除搜索字符
|
||||
if (searchFilter.length > 0) {
|
||||
searchFilter = searchFilter.slice(0, -1);
|
||||
selected = 0; // 重置选中
|
||||
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
|
||||
}
|
||||
} else if (key.char && key.char.length === 1) {
|
||||
// 输入字符 -> 搜索
|
||||
if (showSearch) {
|
||||
searchFilter += key.char;
|
||||
selected = 0;
|
||||
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
|
||||
}
|
||||
}
|
||||
|
||||
// 直接按键选择 (如果菜单项有 key 属性)
|
||||
if (key.char && !showSearch) {
|
||||
const matchByChar = items.find(item => (item.key || '').toLowerCase() === key.name);
|
||||
if (matchByChar) {
|
||||
result = matchByChar;
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// 恢复终端状态
|
||||
process.stdout.write(C.restore);
|
||||
process.stdout.write(C.clearDown);
|
||||
process.stdout.write(C.show);
|
||||
setRawMode(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 其他交互组件
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 文本输入框
|
||||
* 支持粘贴操作 (Bracketed Paste Mode)
|
||||
* @param {object} options 配置选项
|
||||
* @returns {Promise<string|null>} 输入的文本或 null (取消)
|
||||
*/
|
||||
async function input(options = {}) {
|
||||
const {
|
||||
prompt = '请输入',
|
||||
defaultValue = '',
|
||||
placeholder = '',
|
||||
validate = null,
|
||||
allowCancel = true,
|
||||
hidden = false, // 密码输入模式
|
||||
} = options;
|
||||
|
||||
let value = '';
|
||||
let done = false;
|
||||
let cancelled = false;
|
||||
|
||||
// 启用 Bracketed Paste Mode
|
||||
process.stdout.write('\x1b[?2004h');
|
||||
setRawMode(true);
|
||||
process.stdout.write(C.hide);
|
||||
|
||||
try {
|
||||
// 初始渲染
|
||||
const renderInput = () => {
|
||||
const displayValue = hidden ? '*'.repeat(value.length) : value;
|
||||
const promptText = `${C.bold}${C.cyan}${prompt}:${C.reset} `;
|
||||
const valueText = displayValue || `${C.dim}${placeholder}${C.reset}`;
|
||||
|
||||
process.stdout.write(`\r${C.clearLine}${promptText}${valueText}`);
|
||||
|
||||
// 光标定位
|
||||
if (displayValue) {
|
||||
process.stdout.write(C.left(displayValue.length - value.length));
|
||||
}
|
||||
};
|
||||
|
||||
renderInput();
|
||||
|
||||
while (!done) {
|
||||
const key = await readKey();
|
||||
|
||||
if (key.name === 'return') {
|
||||
// 确认
|
||||
const finalValue = value || defaultValue;
|
||||
if (validate) {
|
||||
const error = validate(finalValue);
|
||||
if (error) {
|
||||
process.stdout.write(`\n${C.red} ✗ ${error}${C.reset}\n`);
|
||||
renderInput();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
done = true;
|
||||
} else if ((key.name === 'escape' || key.name === 'q') && allowCancel) {
|
||||
done = true;
|
||||
cancelled = true;
|
||||
} else if (key.name === 'backspace') {
|
||||
if (value.length > 0) {
|
||||
value = value.slice(0, -1);
|
||||
renderInput();
|
||||
}
|
||||
} else if (key.name === 'paste' && key.paste) {
|
||||
// 处理粘贴操作 (Bracketed Paste Mode)
|
||||
// 清理粘贴内容中的控制字符和换行符
|
||||
const cleanPaste = key.paste
|
||||
.replace(/[\r\n]+/g, ' ') // 换行符替换为空格
|
||||
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '') // 移除 ANSI 转义序列
|
||||
.replace(/[\x00-\x1f]/g, ''); // 移除控制字符
|
||||
value += cleanPaste;
|
||||
renderInput();
|
||||
} else if (key.char && key.char.length === 1) {
|
||||
value += key.char;
|
||||
renderInput();
|
||||
} else if (key.sequence && key.sequence.length > 1 && !key.name.startsWith('page') &&
|
||||
!['up', 'down', 'left', 'right', 'home', 'end'].includes(key.name)) {
|
||||
// 回退处理: 如果收到未知的多字符序列,可能是粘贴内容
|
||||
// 尝试提取可打印字符
|
||||
const extracted = key.sequence
|
||||
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.replace(/[\x00-\x1f]/g, '')
|
||||
.replace(/[^\x20-\x7e]/g, '');
|
||||
if (extracted.length > 0) {
|
||||
value += extracted;
|
||||
renderInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// 禁用 Bracketed Paste Mode
|
||||
process.stdout.write('\x1b[?2004l');
|
||||
process.stdout.write(C.show);
|
||||
setRawMode(false);
|
||||
}
|
||||
|
||||
if (cancelled) return null;
|
||||
|
||||
// 显示最终值 (非隐藏模式)
|
||||
if (!hidden) {
|
||||
process.stdout.write(`\n`);
|
||||
}
|
||||
|
||||
return value || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认对话框
|
||||
* @param {object} options 配置选项
|
||||
* @returns {Promise<boolean>} 用户选择
|
||||
*/
|
||||
async function confirm(options = {}) {
|
||||
const {
|
||||
prompt = '确认吗?',
|
||||
defaultYes = true,
|
||||
} = options;
|
||||
|
||||
let selected = defaultYes; // true = Yes, false = No
|
||||
|
||||
setRawMode(true);
|
||||
process.stdout.write(C.hide);
|
||||
|
||||
const renderConfirm = () => {
|
||||
const yesStyle = selected ? `${C.bold}${C.green}` : C.dim;
|
||||
const noStyle = selected ? C.dim : `${C.bold}${C.red}`;
|
||||
|
||||
process.stdout.write(
|
||||
`\r${C.clearLine}${C.bold}${C.cyan}${prompt}${C.reset} ` +
|
||||
`${yesStyle}[Y] 是${C.reset} / ` +
|
||||
`${noStyle}[N] 否${C.reset} ` +
|
||||
`${C.dim}(←→ 切换, 回车 确认)${C.reset}`
|
||||
);
|
||||
};
|
||||
|
||||
renderConfirm();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const key = await readKey();
|
||||
|
||||
if (key.name === 'return') {
|
||||
process.stdout.write('\n');
|
||||
return selected;
|
||||
} else if (key.name === 'left' || key.name === 'y') {
|
||||
selected = true;
|
||||
renderConfirm();
|
||||
} else if (key.name === 'right' || key.name === 'n') {
|
||||
selected = false;
|
||||
renderConfirm();
|
||||
} else if (key.name === 'escape') {
|
||||
process.stdout.write('\n');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process.stdout.write(C.show);
|
||||
setRawMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多选菜单 (复选框)
|
||||
* @param {object} options 配置选项
|
||||
* @returns {Promise<Array>} 选中的项目数组
|
||||
*/
|
||||
async function multiselect(options = {}) {
|
||||
const {
|
||||
items = [],
|
||||
title = '请选择 (空格 切换选中)',
|
||||
defaultSelected = [],
|
||||
} = options;
|
||||
|
||||
if (items.length === 0) return [];
|
||||
|
||||
const selectedSet = new Set(defaultSelected);
|
||||
let cursor = 0;
|
||||
let done = false;
|
||||
let cancelled = false;
|
||||
|
||||
setRawMode(true);
|
||||
process.stdout.write(C.hide);
|
||||
|
||||
const render = () => {
|
||||
const lines = [`\r${C.bold}${C.cyan}${title}${C.reset}\n`];
|
||||
|
||||
items.forEach((item, idx) => {
|
||||
const isCursor = idx === cursor;
|
||||
const isChecked = selectedSet.has(idx);
|
||||
|
||||
const cursorStyle = isCursor ? `${C.bold}${C.cyan}▸` : ' ';
|
||||
const checkChar = isChecked ? `${C.green}✓${C.reset}` : `${C.dim}○${C.reset}`;
|
||||
const labelStyle = isCursor ? C.bold : '';
|
||||
|
||||
lines.push(`\r${cursorStyle} ${checkChar} ${labelStyle}${item.label}${C.reset}`);
|
||||
});
|
||||
|
||||
lines.push(`\r\n${C.dim}↑↓ 移动 空格 选中 回车 确认 ESC 取消${C.reset}`);
|
||||
|
||||
process.stdout.write(C.clearDown + lines.join('\n') + '\n');
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
try {
|
||||
while (!done) {
|
||||
const key = await readKey();
|
||||
|
||||
if (key.name === 'return') {
|
||||
done = true;
|
||||
} else if (key.name === 'escape' || key.name === 'q') {
|
||||
done = true;
|
||||
cancelled = true;
|
||||
} else if (key.name === 'up' && cursor > 0) {
|
||||
cursor--;
|
||||
render();
|
||||
} else if (key.name === 'down' && cursor < items.length - 1) {
|
||||
cursor++;
|
||||
render();
|
||||
} else if (key.name === ' ' || key.name === 'tab') {
|
||||
if (selectedSet.has(cursor)) {
|
||||
selectedSet.delete(cursor);
|
||||
} else {
|
||||
selectedSet.add(cursor);
|
||||
}
|
||||
render();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process.stdout.write(C.show);
|
||||
setRawMode(false);
|
||||
}
|
||||
|
||||
if (cancelled) return [];
|
||||
|
||||
return Array.from(selectedSet).map(idx => items[idx]);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 进度指示器
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 加载动画 (Spinner)
|
||||
*/
|
||||
const spinners = {
|
||||
dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
||||
line: ['-', '\\', '|', '/'],
|
||||
circle: ['◜', '◠', '◝', '◞', '◡', '◟'],
|
||||
};
|
||||
|
||||
function spinner(options = {}) {
|
||||
const {
|
||||
text = '加载中...',
|
||||
style = 'dots',
|
||||
} = options;
|
||||
|
||||
const frames = spinners[style] || spinners.dots;
|
||||
let frame = 0;
|
||||
let interval = null;
|
||||
let stopped = false;
|
||||
|
||||
return {
|
||||
start() {
|
||||
process.stdout.write(C.hide);
|
||||
interval = setInterval(() => {
|
||||
if (stopped) return;
|
||||
const symbol = `${C.cyan}${frames[frame]}${C.reset}`;
|
||||
process.stdout.write(`\r${C.clearLine}${symbol} ${text}`);
|
||||
frame = (frame + 1) % frames.length;
|
||||
}, 80);
|
||||
},
|
||||
update(newText) {
|
||||
if (interval && !stopped) {
|
||||
const symbol = `${C.cyan}${frames[frame]}${C.reset}`;
|
||||
process.stdout.write(`\r${C.clearLine}${symbol} ${newText}`);
|
||||
}
|
||||
},
|
||||
stop(finalText = '') {
|
||||
stopped = true;
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
interval = null;
|
||||
}
|
||||
process.stdout.write(`\r${C.clearLine}${finalText}\n`);
|
||||
process.stdout.write(C.show);
|
||||
},
|
||||
succeed(text) {
|
||||
this.stop(`${C.green}✓${C.reset} ${text}`);
|
||||
},
|
||||
fail(text) {
|
||||
this.stop(`${C.red}✗${C.reset} ${text}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 导出
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
module.exports = {
|
||||
// 颜色常量
|
||||
C,
|
||||
|
||||
// 核心交互
|
||||
select,
|
||||
input,
|
||||
confirm,
|
||||
multiselect,
|
||||
|
||||
// 工具
|
||||
spinner,
|
||||
readKey,
|
||||
setRawMode,
|
||||
resetRenderCount,
|
||||
|
||||
// 底层渲染
|
||||
renderMenu,
|
||||
};
|
||||
@ -120,14 +120,6 @@ if #ss_type > 0 or #trojan_type > 0 or #vmess_type > 0 or #vless_type > 0 or #hy
|
||||
translate("The configured type also applies to the core specified when manually importing nodes."))
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "domain_strategy", "Sing-box " .. translate("Domain Strategy"), translate("Set the default domain resolution strategy for the sing-box node."))
|
||||
o.default = ""
|
||||
o:value("", translate("Auto"))
|
||||
o:value("prefer_ipv4", translate("Prefer IPv4"))
|
||||
o:value("prefer_ipv6", translate("Prefer IPv6"))
|
||||
o:value("ipv4_only", translate("IPv4 Only"))
|
||||
o:value("ipv6_only", translate("IPv6 Only"))
|
||||
|
||||
---- Subscribe Delete All
|
||||
o = s:option(DummyValue, "_stop", translate("Delete All Subscribe Node"))
|
||||
o.rawhtml = true
|
||||
|
||||
@ -133,6 +133,35 @@ o.validate = function(self, value)
|
||||
return value:gsub("%s+", ""):gsub("%z", "")
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
|
||||
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" ..
|
||||
translate("Supports only Xray or Sing-box node types.") .. "<br>" .. string.format('<font color="red">%s</font>',
|
||||
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("https", "DoH")
|
||||
|
||||
o = s:option(Value, "domain_resolver_dns", "DNS")
|
||||
o.datatype = "or(ipaddr,ipaddrport)"
|
||||
o:value("114.114.114.114")
|
||||
o:value("223.5.5.5:53")
|
||||
o.default = o.keylist[1]
|
||||
o:depends({ domain_resolver = "tcp" })
|
||||
o:depends({ domain_resolver = "udp" })
|
||||
|
||||
o = s:option(Value, "domain_resolver_dns_https", "DNS")
|
||||
o:value("https://120.53.53.53/dns-query", "DNSPod")
|
||||
o:value("https://223.5.5.5/dns-query", "AliDNS")
|
||||
o.default = o.keylist[1]
|
||||
o:depends({ domain_resolver = "https" })
|
||||
|
||||
o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
|
||||
o.default = ""
|
||||
o:value("", translate("Auto"))
|
||||
o:value("UseIPv4", translate("IPv4 Only"))
|
||||
o:value("UseIPv6", translate("IPv6 Only"))
|
||||
|
||||
o = s:option(Flag, "allowInsecure", translate("allowInsecure"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
@ -203,15 +232,6 @@ if #hysteria2_type > 0 then
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "domain_strategy", "Sing-box " .. translate("Domain Strategy"), translate("Set the default domain resolution strategy for the sing-box node."))
|
||||
o.default = "global"
|
||||
o:value("global", translate("Use global config"))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("prefer_ipv4", translate("Prefer IPv4"))
|
||||
o:value("prefer_ipv6", translate("Prefer IPv6"))
|
||||
o:value("ipv4_only", translate("IPv4 Only"))
|
||||
o:value("ipv6_only", translate("IPv6 Only"))
|
||||
|
||||
o = s:option(Flag, "boot_update", translate("Update Once on Boot"), translate("Updates the subscription the first time PassWall runs automatically after each system boot."))
|
||||
o.default = 0
|
||||
|
||||
|
||||
@ -240,16 +240,6 @@ o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
|
||||
o = s:option(Value, _n("port"), translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
local protocols = s.fields[_n("protocol")].keylist
|
||||
if #protocols > 0 then
|
||||
for index, value in ipairs(protocols) do
|
||||
if not value:find("^_") then
|
||||
s.fields[_n("address")]:depends({ [_n("protocol")] = value })
|
||||
s.fields[_n("port")]:depends({ [_n("protocol")] = value })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Value, _n("uuid"), translate("ID"))
|
||||
o.password = true
|
||||
o:depends({ [_n("protocol")] = "vmess" })
|
||||
@ -735,10 +725,48 @@ o.datatype = "uinteger"
|
||||
o.placeholder = 0
|
||||
o:depends({ [_n("protocol")] = "vless" })
|
||||
|
||||
for i, v in ipairs(s.fields[_n("protocol")].keylist) do
|
||||
if not v:find("^_") and v ~= "hysteria2" then
|
||||
s.fields[_n("tcp_fast_open")]:depends({ [_n("protocol")] = v })
|
||||
s.fields[_n("tcpMptcp")]:depends({ [_n("protocol")] = v })
|
||||
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"))
|
||||
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
|
||||
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("https", "DoH")
|
||||
|
||||
o = s:option(Value, _n("domain_resolver_dns"), "DNS")
|
||||
o.datatype = "or(ipaddr,ipaddrport)"
|
||||
o:value("114.114.114.114")
|
||||
o:value("223.5.5.5:53")
|
||||
o.default = o.keylist[1]
|
||||
o:depends({ [_n("domain_resolver")] = "tcp" })
|
||||
o:depends({ [_n("domain_resolver")] = "udp" })
|
||||
|
||||
o = s:option(Value, _n("domain_resolver_dns_https"), "DNS")
|
||||
o:value("https://120.53.53.53/dns-query", "DNSPod")
|
||||
o:value("https://223.5.5.5/dns-query", "AliDNS")
|
||||
o.default = o.keylist[1]
|
||||
o:depends({ [_n("domain_resolver")] = "https" })
|
||||
|
||||
o = s:option(ListValue, _n("domain_strategy"), translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
|
||||
o.default = ""
|
||||
o:value("", translate("Auto"))
|
||||
o:value("UseIPv4", translate("IPv4 Only"))
|
||||
o:value("UseIPv6", translate("IPv6 Only"))
|
||||
|
||||
local protocols = s.fields[_n("protocol")].keylist
|
||||
if #protocols > 0 then
|
||||
for i, v in ipairs(protocols) do
|
||||
if not v:find("^_") then
|
||||
s.fields[_n("address")]:depends({ [_n("protocol")] = v })
|
||||
s.fields[_n("port")]:depends({ [_n("protocol")] = v })
|
||||
s.fields[_n("domain_resolver")]:depends({ [_n("protocol")] = v })
|
||||
s.fields[_n("domain_strategy")]:depends({ [_n("protocol")] = v })
|
||||
|
||||
if v ~= "hysteria2" then
|
||||
s.fields[_n("tcp_fast_open")]:depends({ [_n("protocol")] = v })
|
||||
s.fields[_n("tcpMptcp")]:depends({ [_n("protocol")] = v })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -206,16 +206,6 @@ o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
|
||||
o = s:option(Value, _n("port"), translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
local protocols = s.fields[_n("protocol")].keylist
|
||||
if #protocols > 0 then
|
||||
for index, value in ipairs(protocols) do
|
||||
if not value:find("^_") then
|
||||
s.fields[_n("address")]:depends({ [_n("protocol")] = value })
|
||||
s.fields[_n("port")]:depends({ [_n("protocol")] = value })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Value, _n("uuid"), translate("ID"))
|
||||
o.password = true
|
||||
o:depends({ [_n("protocol")] = "vmess" })
|
||||
@ -742,6 +732,28 @@ o:value("v2ray-plugin")
|
||||
o = s:option(Value, _n("plugin_opts"), translate("opts"))
|
||||
o:depends({ [_n("plugin_enabled")] = true })
|
||||
|
||||
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"))
|
||||
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
|
||||
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
o:value("https", "DoH")
|
||||
|
||||
o = s:option(Value, _n("domain_resolver_dns"), "DNS")
|
||||
o.datatype = "or(ipaddr,ipaddrport)"
|
||||
o:value("114.114.114.114")
|
||||
o:value("223.5.5.5:53")
|
||||
o.default = o.keylist[1]
|
||||
o:depends({ [_n("domain_resolver")] = "tcp" })
|
||||
o:depends({ [_n("domain_resolver")] = "udp" })
|
||||
|
||||
o = s:option(Value, _n("domain_resolver_dns_https"), "DNS")
|
||||
o:value("https://120.53.53.53/dns-query", "DNSPod")
|
||||
o:value("https://223.5.5.5/dns-query", "AliDNS")
|
||||
o.default = o.keylist[1]
|
||||
o:depends({ [_n("domain_resolver")] = "https" })
|
||||
|
||||
o = s:option(ListValue, _n("domain_strategy"), translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
|
||||
o.default = ""
|
||||
o:value("", translate("Auto"))
|
||||
@ -749,17 +761,19 @@ o:value("prefer_ipv4", translate("Prefer IPv4"))
|
||||
o:value("prefer_ipv6", translate("Prefer IPv6"))
|
||||
o:value("ipv4_only", translate("IPv4 Only"))
|
||||
o:value("ipv6_only", translate("IPv6 Only"))
|
||||
o:depends({ [_n("protocol")] = "socks" })
|
||||
o:depends({ [_n("protocol")] = "http" })
|
||||
o:depends({ [_n("protocol")] = "shadowsocks" })
|
||||
o:depends({ [_n("protocol")] = "vmess" })
|
||||
o:depends({ [_n("protocol")] = "trojan" })
|
||||
o:depends({ [_n("protocol")] = "wireguard" })
|
||||
o:depends({ [_n("protocol")] = "hysteria" })
|
||||
o:depends({ [_n("protocol")] = "vless" })
|
||||
o:depends({ [_n("protocol")] = "tuic" })
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
o:depends({ [_n("protocol")] = "anytls" })
|
||||
|
||||
|
||||
local protocols = s.fields[_n("protocol")].keylist
|
||||
if #protocols > 0 then
|
||||
for i, v in ipairs(protocols) do
|
||||
if not v:find("^_") then
|
||||
s.fields[_n("address")]:depends({ [_n("protocol")] = v })
|
||||
s.fields[_n("port")]:depends({ [_n("protocol")] = v })
|
||||
s.fields[_n("domain_resolver")]:depends({ [_n("protocol")] = v })
|
||||
s.fields[_n("domain_strategy")]:depends({ [_n("protocol")] = v })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- [[ Normal single node End ]]
|
||||
|
||||
|
||||
@ -11,6 +11,10 @@ local ech_domain = {}
|
||||
local local_version = api.get_app_version("sing-box"):match("[^v]+")
|
||||
local version_ge_1_13_0 = api.compare_versions(local_version, ">=", "1.13.0")
|
||||
|
||||
local GLOBAL = {
|
||||
DNS_SERVER = {}
|
||||
}
|
||||
|
||||
local GEO_VAR = {
|
||||
OK = nil,
|
||||
DIR = nil,
|
||||
@ -154,6 +158,47 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
detour = node.detour,
|
||||
}
|
||||
|
||||
if api.datatypes.hostname(node.address) and node.domain_resolver and (node.domain_resolver_dns or node.domain_resolver_dns_https) then
|
||||
local dns_tag = node_id .. "_dns"
|
||||
local dns_proto = node.domain_resolver
|
||||
local server_address
|
||||
local server_port
|
||||
local server_path
|
||||
if dns_proto == "https" then
|
||||
local _a = api.parseURL(node.domain_resolver_dns_https)
|
||||
if _a then
|
||||
server_address = _a.hostname
|
||||
if _a.port then
|
||||
server_port = _a.port
|
||||
else
|
||||
server_port = 443
|
||||
end
|
||||
server_path = _a.pathname
|
||||
end
|
||||
else
|
||||
server_address = node.domain_resolver_dns
|
||||
server_port = 53
|
||||
local split = api.split(server_address, ":")
|
||||
if #split > 1 then
|
||||
server_address = split[1]
|
||||
server_port = tonumber(split[#split])
|
||||
end
|
||||
end
|
||||
GLOBAL.DNS_SERVER[node_id] = {
|
||||
server = {
|
||||
tag = dns_tag,
|
||||
type = dns_proto,
|
||||
server = server_address,
|
||||
server_port = server_port,
|
||||
path = server_path,
|
||||
domain_resolver = "direct",
|
||||
detour = "direct"
|
||||
},
|
||||
domain = node.address
|
||||
}
|
||||
result.domain_resolver.server = dns_tag
|
||||
end
|
||||
|
||||
local tls = nil
|
||||
if node.protocol == "hysteria" or node.protocol == "hysteria2" or node.protocol == "tuic" or node.protocol == "naive" then
|
||||
node.tls = "1"
|
||||
@ -1922,6 +1967,16 @@ function gen_config(var)
|
||||
}
|
||||
end
|
||||
|
||||
for i, v in pairs(GLOBAL.DNS_SERVER) do
|
||||
table.insert(dns.servers, v.server)
|
||||
if not dns.rules then dns.rules = {} end
|
||||
table.insert(dns.rules, {
|
||||
action = "route",
|
||||
server = v.server.tag,
|
||||
domain = v.domain,
|
||||
})
|
||||
end
|
||||
|
||||
if next(ech_domain) ~= nil then
|
||||
table.insert(dns.servers, {
|
||||
tag = "ech-dns",
|
||||
@ -1983,6 +2038,12 @@ function gen_config(var)
|
||||
if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and (value.server_port or value.server_ports) and not no_run then
|
||||
sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list"))
|
||||
end
|
||||
if not value.detour and value.server then
|
||||
value.detour = "direct"
|
||||
end
|
||||
if value.server and not api.datatypes.hostname(value.server) then
|
||||
value.domain_resolver = nil
|
||||
end
|
||||
for k, v in pairs(config.outbounds[index]) do
|
||||
if k:find("_") == 1 then
|
||||
config.outbounds[index][k] = nil
|
||||
|
||||
@ -6,6 +6,11 @@ local jsonc = api.jsonc
|
||||
local appname = "passwall"
|
||||
local fs = api.fs
|
||||
|
||||
local GLOBAL = {
|
||||
DNS_SERVER = {},
|
||||
DNS_HOSTNAME = {}
|
||||
}
|
||||
|
||||
local xray_version = api.get_app_version("xray")
|
||||
|
||||
local function get_domain_excluded()
|
||||
@ -134,6 +139,7 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
streamSettings = (node.streamSettings or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria") and {
|
||||
sockopt = {
|
||||
mark = 255,
|
||||
domainStrategy = node.domain_strategy or "UseIP",
|
||||
tcpFastOpen = (node.tcp_fast_open == "1") and true or nil,
|
||||
tcpMptcp = (node.tcpMptcp == "1") and true or nil
|
||||
},
|
||||
@ -397,6 +403,48 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
result.streamSettings.tlsSettings.alpn = alpn
|
||||
end
|
||||
end
|
||||
|
||||
if api.datatypes.hostname(node.address) and node.domain_resolver and (node.domain_resolver_dns or node.domain_resolver_dns_https) then
|
||||
local dns_tag = node_id .. "_dns"
|
||||
local dns_proto = node.domain_resolver
|
||||
local config_address
|
||||
local config_port
|
||||
if dns_proto == "https" then
|
||||
local _a = api.parseURL(node.domain_resolver_dns_https)
|
||||
if _a then
|
||||
config_address = node.domain_resolver_dns_https
|
||||
if _a.port then
|
||||
config_port = _a.port
|
||||
else
|
||||
config_port = 443
|
||||
end
|
||||
if _a.hostname then
|
||||
if api.datatypes.hostname(_a.hostname) then
|
||||
GLOBAL.DNS_HOSTNAME[_a.hostname] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
local server_address = node.domain_resolver_dns
|
||||
local config_port = 53
|
||||
local split = api.split(server_address, ":")
|
||||
if #split > 1 then
|
||||
server_address = split[1]
|
||||
config_port = tonumber(split[#split])
|
||||
end
|
||||
config_address = server_address
|
||||
if dns_proto == "tcp" then
|
||||
config_address = dns_proto .. "://" .. server_address .. ":" .. config_port
|
||||
end
|
||||
end
|
||||
GLOBAL.DNS_SERVER[node_id] = {
|
||||
tag = dns_tag,
|
||||
queryStrategy = node.domain_strategy or "UseIP",
|
||||
address = config_address,
|
||||
port = config_port,
|
||||
domains = {"full:" .. node.address}
|
||||
}
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
@ -1437,7 +1485,15 @@ function gen_config(var)
|
||||
end
|
||||
end
|
||||
|
||||
if (remote_dns_udp_server and remote_dns_udp_port) or (remote_dns_tcp_server and remote_dns_tcp_port) then
|
||||
local node_dns = {}
|
||||
for i, v in pairs(GLOBAL.DNS_SERVER) do
|
||||
table.insert(node_dns, {
|
||||
server = v,
|
||||
outboundTag = "direct"
|
||||
})
|
||||
end
|
||||
|
||||
if (remote_dns_udp_server and remote_dns_udp_port) or (remote_dns_tcp_server and remote_dns_tcp_port) or #node_dns > 0 then
|
||||
if not routing then
|
||||
routing = {
|
||||
domainStrategy = "IPOnDemand",
|
||||
@ -1461,6 +1517,8 @@ function gen_config(var)
|
||||
queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
|
||||
}
|
||||
|
||||
direct_dns_udp_server = (direct_dns_udp_server and direct_dns_udp_server ~= "") and direct_dns_udp_server or nil
|
||||
|
||||
if direct_dns_udp_server or direct_dns_tcp_server then
|
||||
local domain = {}
|
||||
local nodes_domain_text = sys.exec('uci show passwall | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
|
||||
@ -1489,14 +1547,12 @@ function gen_config(var)
|
||||
end
|
||||
end
|
||||
|
||||
local _remote_dns = {
|
||||
--tag = "dns-global-remote",
|
||||
queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4",
|
||||
}
|
||||
local _remote_dns = {}
|
||||
|
||||
if remote_dns_udp_server then
|
||||
_remote_dns.address = remote_dns_udp_server
|
||||
_remote_dns.port = tonumber(remote_dns_udp_port) or 53
|
||||
else
|
||||
elseif remote_dns_tcp_server then
|
||||
_remote_dns.address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
|
||||
end
|
||||
|
||||
@ -1510,7 +1566,11 @@ function gen_config(var)
|
||||
_remote_dns.port = tonumber(remote_dns_doh_port)
|
||||
end
|
||||
|
||||
table.insert(dns.servers, _remote_dns)
|
||||
if next(_remote_dns) then
|
||||
-- _remote_dns.tag = "dns-global-remote"
|
||||
_remote_dns.queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4"
|
||||
table.insert(dns.servers, _remote_dns)
|
||||
end
|
||||
|
||||
local _remote_fakedns = {
|
||||
--tag = "dns-global-remote-fakedns",
|
||||
@ -1649,7 +1709,7 @@ function gen_config(var)
|
||||
dns_server = nil
|
||||
end
|
||||
if dns_server then
|
||||
dns_server.finalQuery = true
|
||||
--dns_server.finalQuery = true
|
||||
dns_server.domains = value.domain
|
||||
if value.shunt_rule_name then
|
||||
dns_server.tag = "dns-in-" .. value.shunt_rule_name
|
||||
@ -1666,7 +1726,7 @@ function gen_config(var)
|
||||
end
|
||||
|
||||
local _outboundTag
|
||||
if not api.is_local_ip(_remote_dns.address) or dns_outbound_tag == "blackhole" then --dns为本地ip,不走代理
|
||||
if _remote_dns.address and not api.is_local_ip(_remote_dns.address) or dns_outbound_tag == "blackhole" then --dns为本地ip,不走代理
|
||||
_outboundTag = dns_outbound_tag
|
||||
else
|
||||
_outboundTag = "direct"
|
||||
@ -1698,6 +1758,37 @@ function gen_config(var)
|
||||
if dns_hosts_len == 0 then
|
||||
dns.hosts = nil
|
||||
end
|
||||
|
||||
-- 自定义节点 DNS
|
||||
if #node_dns > 0 and #dns.servers < 1 then
|
||||
dns.servers = { "localhost" }
|
||||
end
|
||||
for i = #node_dns, 1, -1 do
|
||||
local value = node_dns[i]
|
||||
table.insert(routing.rules, 1, {
|
||||
inboundTag = {
|
||||
value.server.tag
|
||||
},
|
||||
outboundTag = value.outboundTag,
|
||||
})
|
||||
table.insert(dns.servers, 2, value.server)
|
||||
end
|
||||
if next(GLOBAL.DNS_HOSTNAME) then
|
||||
local hostname = {}
|
||||
for line, _ in pairs(GLOBAL.DNS_HOSTNAME) do
|
||||
table.insert(hostname, line)
|
||||
end
|
||||
table.insert(dns.servers, 2, {
|
||||
tag = "bootstrap",
|
||||
address = "223.5.5.5",
|
||||
queryStrategy = "UseIPv4",
|
||||
domains = hostname
|
||||
})
|
||||
table.insert(routing.rules, 1, {
|
||||
inboundTag = { "bootstrap" },
|
||||
outboundTag = "direct"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if inbounds or outbounds then
|
||||
|
||||
@ -1972,6 +1972,18 @@ msgstr "延迟(ms)"
|
||||
msgid "If is domain name, The requested domain name will be resolved to IP before connect."
|
||||
msgstr "如果是域名,域名将在请求发出之前解析为 IP。"
|
||||
|
||||
msgid "Domain DNS Resolve"
|
||||
msgstr "域名 DNS 解析"
|
||||
|
||||
msgid "If the node address is a domain name, this DNS will be used for resolution."
|
||||
msgstr "如果节点地址是域名,则将使用此 DNS 进行解析。"
|
||||
|
||||
msgid "Note: For node-specific DNS only. Keep Auto to avoid extra overhead."
|
||||
msgstr "注意:仅用于节点专用 DNS,通常请保持自动,以免增加开销。"
|
||||
|
||||
msgid "Supports only Xray or Sing-box node types."
|
||||
msgstr "仅支持 Xray 或 Sing-box 类型节点。"
|
||||
|
||||
msgid "Chain Proxy"
|
||||
msgstr "链式代理"
|
||||
|
||||
@ -1985,15 +1997,12 @@ msgid ""
|
||||
"Chained proxy works only with Xray or Sing-box nodes.<br>"
|
||||
"You can only use manual or imported nodes as chained nodes."
|
||||
msgstr ""
|
||||
"链式代理仅支持 Xray 与 Sing-box 节点。<br>"
|
||||
"链式代理仅支持 Xray 或 Sing-box 节点。<br>"
|
||||
"仅支持手动添加或导入的节点用作链式节点。"
|
||||
|
||||
msgid "Only work with using the %s node."
|
||||
msgstr "与使用 %s 节点时生效。"
|
||||
|
||||
msgid "Set the default domain resolution strategy for the sing-box node."
|
||||
msgstr "为 sing-box 节点设置默认的域名解析策略。"
|
||||
|
||||
msgid "Total Lines"
|
||||
msgstr "总行数:"
|
||||
|
||||
|
||||
@ -186,7 +186,18 @@ run_singbox() {
|
||||
[ -n "$no_run" ] && json_add_string "no_run" "1"
|
||||
local _json_arg="$(json_dump)"
|
||||
lua $UTIL_SINGBOX gen_config "${_json_arg}" > $config_file
|
||||
[ -n "$no_run" ] || ln_run "$SINGBOX_BIN" "sing-box" $log_file run -c "$config_file"
|
||||
[ -n "$no_run" ] && return
|
||||
|
||||
local test_log_file=$log_file
|
||||
[ "$test_log_file" = "/dev/null" ] && test_log_file="${TMP_PATH}/${config_file##*/}_test.log"
|
||||
$SINGBOX_BIN check -c "$config_file" > $test_log_file 2>&1; local status=$?
|
||||
if [ "${status}" = 0 ]; then
|
||||
ln_run "$SINGBOX_BIN" "sing-box" "${log_file}" run -c "$config_file"
|
||||
else
|
||||
echolog "Sing-box 配置文件 $config_file 校验有误,进程启动失败,错误信息:"
|
||||
cat ${test_log_file} >> ${LOG_FILE}
|
||||
fi
|
||||
[ "$test_log_file" != "$log_file" ] && rm -f "${test_log_file}"
|
||||
}
|
||||
|
||||
run_xray() {
|
||||
@ -275,7 +286,18 @@ run_xray() {
|
||||
[ -n "$no_run" ] && json_add_string "no_run" "1"
|
||||
local _json_arg="$(json_dump)"
|
||||
lua $UTIL_XRAY gen_config "${_json_arg}" > $config_file
|
||||
[ -n "$no_run" ] || ln_run "$XRAY_BIN" "xray" $log_file run -c "$config_file"
|
||||
[ -n "$no_run" ] && return
|
||||
|
||||
local test_log_file=$log_file
|
||||
[ "$test_log_file" = "/dev/null" ] && test_log_file="${TMP_PATH}/${config_file##*/}_test.log"
|
||||
$XRAY_BIN run -test -c "$config_file" > $test_log_file; local status=$?
|
||||
if [ "${status}" = 0 ]; then
|
||||
ln_run "$XRAY_BIN" "xray" "${log_file}" run -c "$config_file"
|
||||
else
|
||||
echolog "Xray 配置文件 $config_file 校验有误,进程启动失败,错误信息:"
|
||||
cat ${test_log_file} >> ${LOG_FILE}
|
||||
fi
|
||||
[ "$test_log_file" != "$log_file" ] && rm -f "${test_log_file}"
|
||||
}
|
||||
|
||||
run_dns2socks() {
|
||||
|
||||
@ -42,8 +42,7 @@ local core_has = {
|
||||
["trojan-plus"] = has_trojan_plus, ["hysteria2"] = has_hysteria2
|
||||
}
|
||||
----
|
||||
local domain_strategy_default = uci:get(appname, "@global_subscribe[0]", "domain_strategy") or ""
|
||||
local domain_strategy_node = ""
|
||||
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
|
||||
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
|
||||
-- 判断是否过滤节点关键字
|
||||
local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
|
||||
@ -1855,9 +1854,22 @@ local function update_node(manual)
|
||||
if kkk ~= "group" or vvv ~= "default" then
|
||||
uci:set(appname, cfgid, kkk, vvv)
|
||||
end
|
||||
-- sing-box 域名解析策略
|
||||
if kkk == "type" and vvv == "sing-box" then
|
||||
uci:set(appname, cfgid, "domain_strategy", domain_strategy_node)
|
||||
-- sing-box/xray 节点域名解析
|
||||
if kkk == "type" and (vvv == "Xray" or vvv == "sing-box") then
|
||||
if domain_resolver then
|
||||
uci:set(appname, cfgid, "domain_resolver", domain_resolver)
|
||||
if domain_resolver_dns then
|
||||
uci:set(appname, cfgid, "domain_resolver_dns", domain_resolver_dns)
|
||||
elseif domain_resolver_dns_https then
|
||||
uci:set(appname, cfgid, "domain_resolver_dns_https", domain_resolver_dns_https)
|
||||
end
|
||||
end
|
||||
if domain_strategy then
|
||||
if vvv == "sing-box" then
|
||||
domain_strategy = (domain_strategy == "UseIPv4" and "ipv4_only") or (domain_strategy == "UseIPv6" and "ipv6_only") or domain_strategy
|
||||
end
|
||||
uci:set(appname, cfgid, "domain_strategy", domain_strategy)
|
||||
end
|
||||
end
|
||||
-- 订阅组链式代理
|
||||
if chain_node_type ~= "" and kkk == "type" and (vvv == "Xray" or vvv == "sing-box") then
|
||||
@ -2069,12 +2081,11 @@ local execute = function()
|
||||
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
|
||||
hysteria2_type_default = hysteria2_type
|
||||
end
|
||||
local domain_strategy = value.domain_strategy or "global"
|
||||
if domain_strategy ~= "global" then
|
||||
domain_strategy_node = domain_strategy
|
||||
else
|
||||
domain_strategy_node = domain_strategy_default
|
||||
end
|
||||
|
||||
domain_resolver = value.domain_resolver
|
||||
domain_resolver_dns = value.domain_resolver_dns
|
||||
domain_resolver_dns_https = value.domain_resolver_dns_https
|
||||
domain_strategy = (value.domain_strategy == "UseIPv4" or value.domain_strategy == "UseIPv6") and value.domain_strategy or nil
|
||||
|
||||
-- 订阅组链式代理
|
||||
local function valid_chain_node(node)
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.4.5
|
||||
PKG_VERSION:=26.4.10
|
||||
PKG_RELEASE:=1
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
|
||||
@ -168,6 +168,19 @@ node_socks_bind_local:depends({ node = "", ["!reverse"] = true })
|
||||
|
||||
s:tab("DNS", translate("DNS"))
|
||||
|
||||
o = s:taboption("DNS", TextValue, "direct_dns_shunt", translate("Direct domain DNS routing"))
|
||||
o.description = "<br /><ul>"
|
||||
.. "<li>" .. translate("Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'.") .. "</li>"
|
||||
.. "<li>" .. translate("Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'.") .. "</li>"
|
||||
.. "<li>" .. translate("Such as:") .. "</li>"
|
||||
.. "<li>" .. "domain:my-nodes.com tcp://223.5.5.5" .. "</li>"
|
||||
.. "<li>" .. "domain:vpn.com udp://119.29.29.29:53" .. "</li>"
|
||||
.. "<li>" .. "full:www.dnspod.com https://120.53.53.53/dns-query" .. "</li>"
|
||||
.. "<li>" .. '<a style="color:red">' .. translate("Please note that the program will not start if the format is incorrect!") .. '</a>' .. "</li>"
|
||||
.. "</ul>"
|
||||
o.rows = 3
|
||||
o.wrap = "off"
|
||||
|
||||
o = s:taboption("DNS", ListValue, "direct_dns_query_strategy", translate("Direct Query Strategy"))
|
||||
o.default = "UseIP"
|
||||
o:value("UseIP")
|
||||
|
||||
@ -91,6 +91,34 @@ local function convert_geofile()
|
||||
convert(GEO_VAR.IP_PATH, "geoip", GEO_VAR.IP_TAGS)
|
||||
end
|
||||
|
||||
function parseDNS(str)
|
||||
local result_dns_server
|
||||
-- [proto]://[ip]
|
||||
-- [proto]://[ip]:[port]
|
||||
-- https://[ip]/[path]
|
||||
-- https://[ip]:[port]/[path]
|
||||
local _a = api.parseURL(str)
|
||||
if _a then
|
||||
if _a.protocol == "tcp" or _a.protocol == "udp" or _a.protocol == "https" then
|
||||
result_dns_server = {
|
||||
type = _a.protocol,
|
||||
server = _a.hostname
|
||||
}
|
||||
if _a.port then
|
||||
result_dns_server.server_port = _a.port
|
||||
else
|
||||
if _a.protocol == "https" then
|
||||
result_dns_server.server_port = 443
|
||||
else
|
||||
result_dns_server.server_port = 53
|
||||
end
|
||||
end
|
||||
result_dns_server.path = _a.pathname
|
||||
end
|
||||
end
|
||||
return result_dns_server
|
||||
end
|
||||
|
||||
function gen_outbound(flag, node, tag, proxy_table)
|
||||
local result = nil
|
||||
if node then
|
||||
@ -1754,6 +1782,13 @@ function gen_config(var)
|
||||
tag = "local"
|
||||
})
|
||||
|
||||
local direct_strategy = "prefer_ipv6"
|
||||
if direct_dns_query_strategy == "UseIPv4" then
|
||||
direct_strategy = "ipv4_only"
|
||||
elseif direct_dns_query_strategy == "UseIPv6" then
|
||||
direct_strategy = "ipv6_only"
|
||||
end
|
||||
|
||||
if dns_listen_port then
|
||||
local dns_host = ""
|
||||
if flag == "global" then
|
||||
@ -1891,13 +1926,6 @@ function gen_config(var)
|
||||
})
|
||||
end
|
||||
|
||||
direct_strategy = "prefer_ipv6"
|
||||
if direct_dns_query_strategy == "UseIPv4" then
|
||||
direct_strategy = "ipv4_only"
|
||||
elseif direct_dns_query_strategy == "UseIPv6" then
|
||||
direct_strategy = "ipv6_only"
|
||||
end
|
||||
|
||||
local server_port = tonumber(direct_dns_udp_port) or 53
|
||||
|
||||
table.insert(dns.servers, {
|
||||
@ -2229,7 +2257,155 @@ function gen_proto_config(var)
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
function gen_front_dns_config(var)
|
||||
local dns_listen_port = var["dns_listen_port"]
|
||||
local direct_dns_udp_server = var["direct_dns_udp_server"]
|
||||
local direct_dns_udp_port = var["direct_dns_udp_port"]
|
||||
local direct_dns_query_strategy = var["direct_dns_query_strategy"]
|
||||
local default_dns_udp_server = var["default_dns_udp_server"]
|
||||
local default_dns_udp_port = var["default_dns_udp_port"]
|
||||
|
||||
local dns = {
|
||||
servers = {},
|
||||
rules = {}
|
||||
}
|
||||
local inbounds = {}
|
||||
local outbounds = {}
|
||||
local route = {}
|
||||
|
||||
local direct_strategy = "prefer_ipv6"
|
||||
if direct_dns_query_strategy == "UseIPv4" then
|
||||
direct_strategy = "ipv4_only"
|
||||
elseif direct_dns_query_strategy == "UseIPv6" then
|
||||
direct_strategy = "ipv6_only"
|
||||
end
|
||||
|
||||
table.insert(outbounds, {
|
||||
type = "direct",
|
||||
tag = "direct",
|
||||
routing_mark = 255,
|
||||
domain_resolver = {
|
||||
server = "direct",
|
||||
strategy = direct_strategy
|
||||
}
|
||||
})
|
||||
|
||||
local direct_dns_shunt = uci:get(appname, "@global[0]", "direct_dns_shunt") or ""
|
||||
if #direct_dns_shunt > 0 then
|
||||
local dns_server = {}
|
||||
string.gsub(direct_dns_shunt, '[^' .. "\r\n" .. ']+', function(w)
|
||||
if w:find("#") == 1 then return end
|
||||
local domain = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $1}')", w))
|
||||
local dns = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $2}')", w))
|
||||
if domain ~= "" and dns ~= "" then
|
||||
local new_dns_server = parseDNS(dns)
|
||||
if new_dns_server then
|
||||
if not dns_server[dns] then
|
||||
dns_server[dns] = {}
|
||||
end
|
||||
if not dns_server[dns].server then
|
||||
dns_server[dns].server = new_dns_server
|
||||
dns_server[dns].server.tag = dns
|
||||
dns_server[dns].server.detour = "direct"
|
||||
end
|
||||
if not dns_server[dns].rule then
|
||||
dns_server[dns].rule = {
|
||||
action = "route",
|
||||
server = dns,
|
||||
domain = {},
|
||||
domain_suffix = {},
|
||||
domain_keyword = {}
|
||||
}
|
||||
end
|
||||
if domain:find("full:") == 1 then
|
||||
table.insert(dns_server[dns].rule.domain, domain:sub(1 + #"full:"))
|
||||
elseif domain:find("domain:") == 1 then
|
||||
table.insert(dns_server[dns].rule.domain_suffix, domain:sub(1 + #"domain:"))
|
||||
else
|
||||
table.insert(dns_server[dns].rule.domain_keyword, domain)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
for k, v in pairs(dns_server) do
|
||||
table.insert(dns.servers, v.server)
|
||||
if #v.rule.domain == 0 then v.rule.domain = nil end
|
||||
if #v.rule.domain_suffix == 0 then v.rule.domain_suffix = nil end
|
||||
if #v.rule.domain_keyword == 0 then v.rule.domain_keyword = nil end
|
||||
table.insert(dns.rules, v.rule)
|
||||
end
|
||||
end
|
||||
|
||||
if direct_dns_udp_server then
|
||||
table.insert(dns.servers, {
|
||||
tag = "direct",
|
||||
type = "udp",
|
||||
server = direct_dns_udp_server,
|
||||
server_port = tonumber(direct_dns_udp_port) or 53,
|
||||
detour = "direct"
|
||||
})
|
||||
local node_domain = {}
|
||||
local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
|
||||
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
|
||||
table.insert(node_domain, w)
|
||||
end)
|
||||
if #node_domain > 0 then
|
||||
table.insert(dns.rules, {
|
||||
action = "route",
|
||||
server = "direct",
|
||||
domain = node_domain
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if default_dns_udp_server then
|
||||
table.insert(dns.servers, {
|
||||
tag = "default",
|
||||
type = "udp",
|
||||
server = default_dns_udp_server,
|
||||
server_port = tonumber(default_dns_udp_port) or 53,
|
||||
detour = "direct"
|
||||
})
|
||||
dns.final = "default"
|
||||
else
|
||||
dns.final = "direct"
|
||||
end
|
||||
|
||||
local dns_in_inbound = {
|
||||
type = "direct",
|
||||
tag = "dns-in",
|
||||
listen = "127.0.0.1",
|
||||
listen_port = tonumber(dns_listen_port),
|
||||
}
|
||||
table.insert(inbounds, dns_in_inbound)
|
||||
|
||||
route.rules = {}
|
||||
table.insert(route.rules, {
|
||||
action = "hijack-dns",
|
||||
inbound = dns_in_inbound.tag
|
||||
})
|
||||
table.insert(route.rules, {
|
||||
action = "sniff",
|
||||
inbound = dns_in_inbound.tag
|
||||
})
|
||||
route.final = "direct"
|
||||
|
||||
local config = {
|
||||
log = {
|
||||
disabled = true,
|
||||
level = "debug",
|
||||
timestamp = true,
|
||||
},
|
||||
dns = dns,
|
||||
inbounds = inbounds,
|
||||
outbounds = outbounds,
|
||||
route = route
|
||||
}
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
_G.gen_config = gen_config
|
||||
_G.gen_front_dns_config = gen_front_dns_config
|
||||
_G.gen_proto_config = gen_proto_config
|
||||
_G.geo_convert_srs = geo_convert_srs
|
||||
|
||||
|
||||
@ -28,6 +28,35 @@ local function get_domain_excluded()
|
||||
return hosts
|
||||
end
|
||||
|
||||
function parseDNS(str)
|
||||
local result_dns_server
|
||||
-- [proto]://[ip]
|
||||
-- [proto]://[ip]:[port]
|
||||
-- https://[ip]/[path]
|
||||
-- https://[ip]:[port]/[path]
|
||||
local _a = api.parseURL(str)
|
||||
if _a then
|
||||
if _a.protocol == "tcp" or _a.protocol == "udp" or _a.protocol == "https" then
|
||||
result_dns_server = {
|
||||
address = str
|
||||
}
|
||||
if _a.protocol == "udp" then
|
||||
result_dns_server.address = _a.hostname
|
||||
end
|
||||
if _a.port then
|
||||
result_dns_server.port = _a.port
|
||||
else
|
||||
if _a.protocol == "https" then
|
||||
result_dns_server.port = 443
|
||||
else
|
||||
result_dns_server.port = 53
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return result_dns_server
|
||||
end
|
||||
|
||||
function gen_outbound(flag, node, tag, proxy_table)
|
||||
local result = nil
|
||||
if node then
|
||||
@ -1451,12 +1480,22 @@ function gen_config(var)
|
||||
queryStrategy = "UseIP"
|
||||
}
|
||||
|
||||
table.insert(dns_servers, {
|
||||
server = {
|
||||
tag = "local",
|
||||
address = "localhost"
|
||||
local _direct_dns = nil
|
||||
if direct_dns_udp_server then
|
||||
_direct_dns = {
|
||||
tag = direct_dns_tag,
|
||||
address = direct_dns_udp_server,
|
||||
port = tonumber(direct_dns_udp_port) or 53,
|
||||
queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
|
||||
}
|
||||
})
|
||||
|
||||
if _direct_dns.address then
|
||||
table.insert(dns_servers, {
|
||||
outboundTag = "direct",
|
||||
server = _direct_dns
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if dns_listen_port then
|
||||
local _remote_dns_proto = "tcp"
|
||||
@ -1556,8 +1595,7 @@ function gen_config(var)
|
||||
server = _remote_fakedns
|
||||
})
|
||||
end
|
||||
|
||||
local _direct_dns = nil
|
||||
|
||||
if direct_dns_udp_server then
|
||||
local domain = {}
|
||||
local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
|
||||
@ -1565,24 +1603,10 @@ function gen_config(var)
|
||||
table.insert(domain, "full:" .. w)
|
||||
end)
|
||||
if #domain > 0 then
|
||||
table.insert(dns_domain_rules, 1, {
|
||||
shunt_rule_name = "logic-vpslist",
|
||||
outboundTag = "direct",
|
||||
domain = domain
|
||||
})
|
||||
end
|
||||
|
||||
_direct_dns = {
|
||||
tag = direct_dns_tag,
|
||||
address = direct_dns_udp_server,
|
||||
port = tonumber(direct_dns_udp_port) or 53,
|
||||
queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP",
|
||||
}
|
||||
|
||||
if _direct_dns.address then
|
||||
table.insert(dns_servers, {
|
||||
outboundTag = "direct",
|
||||
server = _direct_dns
|
||||
table.insert(dns.servers, 1, {
|
||||
tag = "local",
|
||||
address = "localhost",
|
||||
domains = domain
|
||||
})
|
||||
end
|
||||
end
|
||||
@ -1773,6 +1797,21 @@ function gen_config(var)
|
||||
end
|
||||
end
|
||||
|
||||
local has_default_dns_server = "0"
|
||||
for index, value in ipairs(dns.servers) do
|
||||
if value.domains == nil then
|
||||
has_default_dns_server = "1"
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if #dns.servers == 0 or not has_default_server == "0" then
|
||||
table.insert(dns.servers, 1, {
|
||||
tag = "local",
|
||||
address = "localhost"
|
||||
})
|
||||
end
|
||||
|
||||
if redir_port then
|
||||
local inbound = {
|
||||
port = tonumber(redir_port),
|
||||
@ -1994,250 +2033,154 @@ function gen_proto_config(var)
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
function gen_dns_config(var)
|
||||
function gen_front_dns_config(var)
|
||||
local dns_listen_port = var["dns_listen_port"]
|
||||
local dns_out_tag = var["dns_out_tag"]
|
||||
local direct_dns_udp_server = var["direct_dns_udp_server"]
|
||||
local direct_dns_udp_port = var["direct_dns_udp_port"]
|
||||
local direct_dns_tcp_server = var["direct_dns_tcp_server"]
|
||||
local direct_dns_tcp_port = var["direct_dns_tcp_port"]
|
||||
local direct_dns_doh_url = var["direct_dns_doh_url"]
|
||||
local direct_dns_doh_host = var["direct_dns_doh_host"]
|
||||
local direct_dns_doh_ip = var["direct_dns_doh_ip"]
|
||||
local direct_dns_doh_port = var["direct_dns_doh_port"]
|
||||
local direct_dns_query_strategy = var["direct_dns_query_strategy"]
|
||||
local remote_dns_udp_server = var["remote_dns_udp_server"]
|
||||
local remote_dns_udp_port = var["remote_dns_udp_port"]
|
||||
local remote_dns_tcp_server = var["remote_dns_tcp_server"]
|
||||
local remote_dns_tcp_port = var["remote_dns_tcp_port"]
|
||||
local remote_dns_doh_url = var["remote_dns_doh_url"]
|
||||
local remote_dns_doh_host = var["remote_dns_doh_host"]
|
||||
local remote_dns_doh_ip = var["remote_dns_doh_ip"]
|
||||
local remote_dns_doh_port = var["remote_dns_doh_port"]
|
||||
local remote_dns_query_strategy = var["remote_dns_query_strategy"]
|
||||
local remote_dns_detour = var["remote_dns_detour"]
|
||||
local remote_dns_client_ip = var["remote_dns_client_ip"]
|
||||
local remote_dns_outbound_socks_address = var["remote_dns_outbound_socks_address"]
|
||||
local remote_dns_outbound_socks_port = var["remote_dns_outbound_socks_port"]
|
||||
local dns_cache = var["dns_cache"]
|
||||
local loglevel = var["loglevel"] or "warning"
|
||||
|
||||
local default_dns_udp_server = var["default_dns_udp_server"]
|
||||
local default_dns_udp_port = var["default_dns_udp_port"]
|
||||
|
||||
local queryStrategy = "UseIP"
|
||||
local dns = {
|
||||
tag = "dns-global-direct",
|
||||
disableCache = false,
|
||||
disableFallback = true,
|
||||
disableFallbackIfMatch = true,
|
||||
queryStrategy = queryStrategy,
|
||||
servers = {}
|
||||
}
|
||||
local inbounds = {}
|
||||
local outbounds = {}
|
||||
local dns = nil
|
||||
local routing = nil
|
||||
local routing = {
|
||||
rules = {}
|
||||
}
|
||||
|
||||
if dns_listen_port then
|
||||
routing = {
|
||||
domainStrategy = "IPOnDemand",
|
||||
rules = {}
|
||||
}
|
||||
|
||||
dns = {
|
||||
tag = "dns-global",
|
||||
hosts = {},
|
||||
disableCache = (dns_cache == "1") and false or true,
|
||||
disableFallback = true,
|
||||
disableFallbackIfMatch = true,
|
||||
servers = {},
|
||||
clientIp = (remote_dns_client_ip and remote_dns_client_ip ~= "") and remote_dns_client_ip or nil,
|
||||
}
|
||||
|
||||
local other_type_dns_proto, other_type_dns_server, other_type_dns_port
|
||||
|
||||
if dns_out_tag == "remote" then
|
||||
dns.queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4"
|
||||
if remote_dns_detour == "direct" then
|
||||
dns_out_tag = "direct"
|
||||
table.insert(outbounds, 1, {
|
||||
tag = dns_out_tag,
|
||||
protocol = "freedom",
|
||||
settings = {
|
||||
domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
|
||||
},
|
||||
streamSettings = {
|
||||
sockopt = {
|
||||
mark = 255
|
||||
}
|
||||
}
|
||||
})
|
||||
else
|
||||
if remote_dns_outbound_socks_address and remote_dns_outbound_socks_port then
|
||||
table.insert(outbounds, 1, {
|
||||
tag = dns_out_tag,
|
||||
protocol = "socks",
|
||||
streamSettings = {
|
||||
network = "tcp",
|
||||
security = "none"
|
||||
},
|
||||
settings = {
|
||||
servers = {
|
||||
{
|
||||
address = remote_dns_outbound_socks_address,
|
||||
port = tonumber(remote_dns_outbound_socks_port)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local _remote_dns = {
|
||||
tag = "dns-in-remote"
|
||||
table.insert(outbounds, {
|
||||
tag = "direct",
|
||||
protocol = "freedom",
|
||||
settings = {
|
||||
domainStrategy = queryStrategy
|
||||
},
|
||||
streamSettings = {
|
||||
sockopt = {
|
||||
mark = 255
|
||||
}
|
||||
|
||||
if remote_dns_udp_server then
|
||||
_remote_dns.address = remote_dns_udp_server
|
||||
_remote_dns.port = tonumber(remote_dns_udp_port) or 53
|
||||
|
||||
other_type_dns_proto = "udp"
|
||||
other_type_dns_server = remote_dns_udp_server
|
||||
other_type_dns_port = _remote_dns.port
|
||||
end
|
||||
|
||||
if remote_dns_tcp_server then
|
||||
_remote_dns.address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
|
||||
_remote_dns.port = tonumber(remote_dns_tcp_port) or 53
|
||||
|
||||
other_type_dns_proto = "tcp"
|
||||
other_type_dns_server = remote_dns_tcp_server
|
||||
other_type_dns_port = _remote_dns.port
|
||||
end
|
||||
|
||||
if remote_dns_doh_url and remote_dns_doh_host then
|
||||
if remote_dns_doh_ip and remote_dns_doh_host ~= remote_dns_doh_ip and not api.is_ip(remote_dns_doh_host) then
|
||||
dns.hosts[remote_dns_doh_host] = remote_dns_doh_ip
|
||||
}
|
||||
})
|
||||
|
||||
if default_dns_udp_server then
|
||||
table.insert(dns.servers, {
|
||||
tag = "default",
|
||||
address = default_dns_udp_server,
|
||||
port = tonumber(default_dns_udp_port) or 53,
|
||||
queryStrategy = queryStrategy,
|
||||
})
|
||||
end
|
||||
|
||||
local direct_dns_shunt = uci:get(appname, "@global[0]", "direct_dns_shunt") or ""
|
||||
if #direct_dns_shunt > 0 then
|
||||
local dns_server = {}
|
||||
string.gsub(direct_dns_shunt, '[^' .. "\r\n" .. ']+', function(w)
|
||||
if w:find("#") == 1 then return end
|
||||
local domain = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $1}')", w))
|
||||
local dns = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $2}')", w))
|
||||
if domain ~= "" and dns ~= "" then
|
||||
local new_dns_server = parseDNS(dns)
|
||||
if new_dns_server then
|
||||
if not dns_server[dns] then
|
||||
dns_server[dns] = {}
|
||||
end
|
||||
dns_server[dns].tag = dns
|
||||
dns_server[dns].queryStrategy = queryStrategy
|
||||
dns_server[dns].address = new_dns_server.address
|
||||
dns_server[dns].port = new_dns_server.port
|
||||
dns_server[dns].finalQuery = true
|
||||
if not dns_server[dns].domains then
|
||||
dns_server[dns].domains = {}
|
||||
end
|
||||
table.insert(dns_server[dns].domains, domain)
|
||||
end
|
||||
_remote_dns.address = remote_dns_doh_url
|
||||
_remote_dns.port = tonumber(remote_dns_doh_port) or 443
|
||||
end
|
||||
|
||||
table.insert(dns.servers, _remote_dns)
|
||||
elseif dns_out_tag == "direct" then
|
||||
dns.queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
|
||||
table.insert(outbounds, 1, {
|
||||
tag = dns_out_tag,
|
||||
protocol = "freedom",
|
||||
settings = {
|
||||
domainStrategy = dns.queryStrategy
|
||||
end)
|
||||
for k, v in pairs(dns_server) do
|
||||
table.insert(dns.servers, v)
|
||||
table.insert(routing.rules, {
|
||||
inboundTag = {
|
||||
v.tag
|
||||
},
|
||||
streamSettings = {
|
||||
sockopt = {
|
||||
mark = 255
|
||||
}
|
||||
}
|
||||
outboundTag = "direct"
|
||||
})
|
||||
|
||||
local _direct_dns = {
|
||||
tag = "dns-in-direct"
|
||||
}
|
||||
|
||||
if direct_dns_udp_server then
|
||||
_direct_dns.address = direct_dns_udp_server
|
||||
_direct_dns.port = tonumber(direct_dns_udp_port) or 53
|
||||
table.insert(routing.rules, 1, {
|
||||
ip = {
|
||||
direct_dns_udp_server
|
||||
},
|
||||
port = tonumber(direct_dns_udp_port) or 53,
|
||||
network = "udp",
|
||||
outboundTag = "direct"
|
||||
})
|
||||
|
||||
other_type_dns_proto = "udp"
|
||||
other_type_dns_server = direct_dns_udp_server
|
||||
other_type_dns_port = _direct_dns.port
|
||||
end
|
||||
|
||||
if direct_dns_tcp_server then
|
||||
_direct_dns.address = "tcp+local://" .. direct_dns_tcp_server
|
||||
_direct_dns.port = tonumber(direct_dns_tcp_port) or 53
|
||||
|
||||
other_type_dns_proto = "tcp"
|
||||
other_type_dns_server = direct_dns_tcp_server
|
||||
other_type_dns_port = _direct_dns.port
|
||||
end
|
||||
|
||||
if direct_dns_doh_url and direct_dns_doh_host then
|
||||
if direct_dns_doh_ip and direct_dns_doh_host ~= direct_dns_doh_ip and not api.is_ip(direct_dns_doh_host) then
|
||||
dns.hosts[direct_dns_doh_host] = direct_dns_doh_ip
|
||||
end
|
||||
_direct_dns.address = direct_dns_doh_url:gsub("https://", "https+local://")
|
||||
_direct_dns.port = tonumber(direct_dns_doh_port) or 443
|
||||
end
|
||||
|
||||
table.insert(dns.servers, _direct_dns)
|
||||
end
|
||||
|
||||
local dns_hosts_len = 0
|
||||
for key, value in pairs(dns.hosts) do
|
||||
dns_hosts_len = dns_hosts_len + 1
|
||||
end
|
||||
|
||||
if dns_hosts_len == 0 then
|
||||
dns.hosts = nil
|
||||
end
|
||||
|
||||
table.insert(inbounds, {
|
||||
listen = "127.0.0.1",
|
||||
port = tonumber(dns_listen_port),
|
||||
protocol = "dokodemo-door",
|
||||
tag = "dns-in",
|
||||
settings = {
|
||||
address = other_type_dns_server or "1.1.1.1",
|
||||
port = other_type_dns_port or 53,
|
||||
network = "tcp,udp"
|
||||
}
|
||||
})
|
||||
|
||||
table.insert(outbounds, {
|
||||
tag = "dns-out",
|
||||
protocol = "dns",
|
||||
proxySettings = {
|
||||
tag = dns_out_tag
|
||||
},
|
||||
settings = {
|
||||
address = other_type_dns_server or "1.1.1.1",
|
||||
port = other_type_dns_port or 53,
|
||||
network = other_type_dns_proto or "tcp",
|
||||
nonIPQuery = "reject"
|
||||
}
|
||||
})
|
||||
|
||||
table.insert(routing.rules, 1, {
|
||||
inboundTag = {
|
||||
"dns-in"
|
||||
},
|
||||
outboundTag = "dns-out"
|
||||
})
|
||||
|
||||
table.insert(routing.rules, {
|
||||
inboundTag = {
|
||||
"dns-global"
|
||||
},
|
||||
outboundTag = dns_out_tag
|
||||
})
|
||||
end
|
||||
|
||||
if inbounds or outbounds then
|
||||
local config = {
|
||||
log = {
|
||||
--dnsLog = true,
|
||||
loglevel = loglevel
|
||||
},
|
||||
dns = dns,
|
||||
inbounds = inbounds,
|
||||
outbounds = outbounds,
|
||||
routing = routing
|
||||
|
||||
if direct_dns_udp_server then
|
||||
local node_domain = {}
|
||||
local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
|
||||
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
|
||||
table.insert(node_domain, "full:" .. w)
|
||||
end)
|
||||
if #node_domain > 0 then
|
||||
table.insert(dns.servers, {
|
||||
tag = "direct",
|
||||
address = direct_dns_udp_server,
|
||||
port = tonumber(direct_dns_udp_port) or 53,
|
||||
queryStrategy = queryStrategy,
|
||||
domains = node_domain
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(inbounds, {
|
||||
tag = "dns-in",
|
||||
listen = "127.0.0.1",
|
||||
port = tonumber(dns_listen_port),
|
||||
protocol = "dokodemo-door",
|
||||
settings = {
|
||||
address = "0.0.0.0",
|
||||
network = "tcp,udp"
|
||||
}
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
})
|
||||
|
||||
table.insert(outbounds, {
|
||||
tag = "dns-out",
|
||||
protocol = "dns",
|
||||
proxySettings = {
|
||||
tag = "direct"
|
||||
},
|
||||
settings = {
|
||||
address = direct_dns_udp_server,
|
||||
port = tonumber(direct_dns_udp_port) or 53,
|
||||
network = "udp",
|
||||
nonIPQuery = "skip",
|
||||
blockTypes = {
|
||||
65
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
table.insert(routing.rules, {
|
||||
inboundTag = {
|
||||
"dns-in"
|
||||
},
|
||||
outboundTag = "dns-out"
|
||||
})
|
||||
|
||||
local config = {
|
||||
log = {
|
||||
dnsLog = true,
|
||||
loglevel = "debug"
|
||||
},
|
||||
dns = dns,
|
||||
inbounds = inbounds,
|
||||
outbounds = outbounds,
|
||||
routing = routing
|
||||
}
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
_G.gen_config = gen_config
|
||||
_G.gen_front_dns_config = gen_front_dns_config
|
||||
_G.gen_proto_config = gen_proto_config
|
||||
_G.gen_dns_config = gen_dns_config
|
||||
|
||||
if arg[1] then
|
||||
local func =_G[arg[1]]
|
||||
|
||||
@ -121,6 +121,12 @@ msgstr "0 به معنای عدم استفاده است"
|
||||
msgid "Current node: %s"
|
||||
msgstr "گره فعلی: %s"
|
||||
|
||||
msgid "Direct domain DNS routing"
|
||||
msgstr "مسیریابی مستقیم DNS دامنه"
|
||||
|
||||
msgid "Please note that the program will not start if the format is incorrect!"
|
||||
msgstr "لطفا توجه داشته باشید که اگر قالب نادرست باشد، برنامه شروع نخواهد شد!"
|
||||
|
||||
msgid "IP:Port mode acceptable, multi value split with english comma."
|
||||
msgstr "حالت IP:Port قابل قبول است، مقادیر متعدد را با کاما انگلیسی جدا کنید."
|
||||
|
||||
|
||||
@ -118,6 +118,12 @@ msgstr "0 — отключить функцию"
|
||||
msgid "Current node: %s"
|
||||
msgstr "Текущий узел: %s"
|
||||
|
||||
msgid "Direct domain DNS routing"
|
||||
msgstr "Прямая маршрутизация DNS домена"
|
||||
|
||||
msgid "Please note that the program will not start if the format is incorrect!"
|
||||
msgstr "Обратите внимание, что программа не запустится, если формат указан неверно!"
|
||||
|
||||
msgid "IP:Port mode acceptable, multi value split with english comma."
|
||||
msgstr "Допустим формат IP:Port. Несколько значений указывайте через запятую."
|
||||
|
||||
|
||||
@ -118,6 +118,12 @@ msgstr "0为不使用"
|
||||
msgid "Current node: %s"
|
||||
msgstr "当前节点:%s"
|
||||
|
||||
msgid "Direct domain DNS routing"
|
||||
msgstr "直连域名 DNS 分流"
|
||||
|
||||
msgid "Please note that the program will not start if the format is incorrect!"
|
||||
msgstr "请注意,格式不正确将无法启动!"
|
||||
|
||||
msgid "IP:Port mode acceptable, multi value split with english comma."
|
||||
msgstr "接受 IP:Port 形式的输入,多个以英文逗号分隔。"
|
||||
|
||||
|
||||
@ -118,6 +118,12 @@ msgstr "0為不使用"
|
||||
msgid "Current node: %s"
|
||||
msgstr "當前節點:%s"
|
||||
|
||||
msgid "Direct domain DNS routing"
|
||||
msgstr "直連網域 DNS 分流"
|
||||
|
||||
msgid "Please note that the program will not start if the format is incorrect!"
|
||||
msgstr "請注意,格式不正確將無法啟動!"
|
||||
|
||||
msgid "IP:Port mode acceptable, multi value split with english comma."
|
||||
msgstr "接受 IP:Port 形式的輸入,多個以英文逗号分隔。"
|
||||
|
||||
|
||||
@ -106,14 +106,6 @@ run_xray() {
|
||||
[ -n "$dns_listen_port" ] && {
|
||||
json_add_string "dns_listen_port" "${dns_listen_port}"
|
||||
[ -n "$dns_cache" ] && json_add_string "dns_cache" "${dns_cache}"
|
||||
|
||||
local _dns=$(get_first_dns AUTO_DNS 53 | sed 's/#/:/g')
|
||||
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
|
||||
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
|
||||
|
||||
DIRECT_DNS_UDP_SERVER=${_dns_address}
|
||||
DIRECT_DNS_UDP_PORT=${_dns_port}
|
||||
|
||||
[ "${node_protocol}" = "_shunt" ] && local write_ipset_direct=$(config_n_get $node write_ipset_direct 0)
|
||||
[ "${write_ipset_direct}" = "1" ] && {
|
||||
direct_dnsmasq_listen_port=$(get_new_port $(expr $dns_listen_port + 1) udp)
|
||||
@ -188,38 +180,17 @@ run_xray() {
|
||||
[ -n "$remote_dns_client_ip" ] && json_add_string "remote_dns_client_ip" "${remote_dns_client_ip}"
|
||||
local _json2_arg="$(json_dump)"
|
||||
|
||||
local independent_dns
|
||||
if [ -z "${independent_dns}" ]; then
|
||||
local _json2_keys key
|
||||
json_load "${_json2_arg}"
|
||||
json_get_keys _json2_keys
|
||||
for key in ${_json2_keys}; do
|
||||
json_get_var "_json2_$key" "$key"
|
||||
done
|
||||
json_load "${_json1_arg}"
|
||||
for key in ${_json2_keys}; do
|
||||
eval "local _v=\$_json2_$key"
|
||||
json_add_string "$key" "$_v"
|
||||
done
|
||||
else
|
||||
dns_remote_listen_port=$(get_new_port $(expr ${direct_dnsmasq_listen_port:-${dns_listen_port}} + 1) udp)
|
||||
V2RAY_DNS_REMOTE_CONFIG="${TMP_PATH}/${flag}_dns_remote.json"
|
||||
V2RAY_DNS_REMOTE_LOG="${TMP_PATH}/${flag}_dns_remote.log"
|
||||
V2RAY_DNS_REMOTE_LOG="/dev/null"
|
||||
json_load "${_json2_arg}"
|
||||
json_add_string "dns_out_tag" "remote"
|
||||
json_add_string "dns_listen_port" "${dns_remote_listen_port}"
|
||||
json_add_string "remote_dns_outbound_socks_address" "127.0.0.1"
|
||||
json_add_string "remote_dns_outbound_socks_port" "${socks_port}"
|
||||
_json2_arg="$(json_dump)"
|
||||
lua $UTIL_XRAY gen_dns_config "${_json2_arg}" > $V2RAY_DNS_REMOTE_CONFIG
|
||||
ln_run "$XRAY_BIN" "xray" $V2RAY_DNS_REMOTE_LOG run -c "$V2RAY_DNS_REMOTE_CONFIG"
|
||||
|
||||
json_load "${_json1_arg}"
|
||||
json_add_string "remote_dns_udp_port" "${dns_remote_listen_port}"
|
||||
json_add_string "remote_dns_udp_server" "127.0.0.1"
|
||||
json_add_string "remote_dns_query_strategy" "${remote_dns_query_strategy}"
|
||||
fi
|
||||
local _json2_keys key
|
||||
json_load "${_json2_arg}"
|
||||
json_get_keys _json2_keys
|
||||
for key in ${_json2_keys}; do
|
||||
json_get_var "_json2_$key" "$key"
|
||||
done
|
||||
json_load "${_json1_arg}"
|
||||
for key in ${_json2_keys}; do
|
||||
eval "local _v=\$_json2_$key"
|
||||
json_add_string "$key" "$_v"
|
||||
done
|
||||
}
|
||||
|
||||
[ -n "${redir_port}" ] && {
|
||||
@ -290,13 +261,6 @@ run_singbox() {
|
||||
}
|
||||
}
|
||||
[ -n "$dns_listen_port" ] && {
|
||||
local _dns=$(get_first_dns AUTO_DNS 53 | sed 's/#/:/g')
|
||||
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
|
||||
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
|
||||
|
||||
DIRECT_DNS_UDP_SERVER=${_dns_address}
|
||||
DIRECT_DNS_UDP_PORT=${_dns_port}
|
||||
|
||||
[ "${node_protocol}" = "_shunt" ] && local write_ipset_direct=$(config_n_get $node write_ipset_direct 0)
|
||||
[ "${write_ipset_direct}" = "1" ] && {
|
||||
direct_dnsmasq_listen_port=$(get_new_port $(expr $dns_listen_port + 1) udp)
|
||||
@ -633,8 +597,6 @@ run_global() {
|
||||
TYPE=$(echo $(config_n_get $NODE type) | tr 'A-Z' 'a-z')
|
||||
[ -z "$TYPE" ] && return 1
|
||||
|
||||
mkdir -p ${GLOBAL_ACL_PATH}
|
||||
|
||||
if [ $PROXY_IPV6 == "1" ]; then
|
||||
log_i18n 0 "To enable experimental IPv6 transparent proxy (TProxy), please ensure your node and type support IPv6!"
|
||||
fi
|
||||
@ -712,8 +674,62 @@ run_global() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
set_cache_var "ACL_GLOBAL_node" "$NODE"
|
||||
set_cache_var "ACL_GLOBAL_redir_port" "$REDIR_PORT"
|
||||
}
|
||||
|
||||
run_front_dns() {
|
||||
local switch=0
|
||||
direct_dns_shunt=$(config_t_get global direct_dns_shunt)
|
||||
direct_dns_shunt=$(echo "${direct_dns_shunt}" | grep -v "^#")
|
||||
[ -n "${direct_dns_shunt}" ] && switch=1
|
||||
[ "${switch}" == "1" ] && {
|
||||
local config_file="${TMP_PATH}/direct_dns.json"
|
||||
local log_file="${TMP_PATH}/direct_dns.log"
|
||||
log_file="/dev/null"
|
||||
local listen_port=$(get_new_port 10553)
|
||||
json_init
|
||||
json_add_string "dns_listen_port" "${listen_port}"
|
||||
json_add_string "direct_dns_udp_server" "${DIRECT_DNS_UDP_SERVER}"
|
||||
json_add_string "direct_dns_udp_port" "${DIRECT_DNS_UDP_PORT}"
|
||||
json_add_string "direct_dns_query_strategy" "${DIRECT_DNS_QUERY_STRATEGY}"
|
||||
[ -n "${ACL_GLOBAL_node}" ] && [ -n "${TUN_DNS_PORT}" ] && {
|
||||
json_add_string "default_dns_udp_server" "127.0.0.1"
|
||||
json_add_string "default_dns_udp_port" "${TUN_DNS_PORT}"
|
||||
}
|
||||
local _json_arg="$(json_dump)"
|
||||
|
||||
local preferred_core=""
|
||||
[ -n "${XRAY_BIN}" ] && preferred_core="xray"
|
||||
[ -n "${SINGBOX_BIN}" ] && preferred_core="sing-box"
|
||||
|
||||
if [ "${preferred_core}" = "xray" ]; then
|
||||
lua $UTIL_XRAY gen_front_dns_config "${_json_arg}" > $config_file
|
||||
ln_run "$XRAY_BIN" "xray" "${log_file}" run -c "$config_file"
|
||||
elif [ "${preferred_core}" = "sing-box" ]; then
|
||||
lua $UTIL_SINGBOX gen_front_dns_config "${_json_arg}" > $config_file
|
||||
ln_run "$SINGBOX_BIN" "sing-box" "${log_file}" run -c "$config_file"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
FRONT_DNS_SERVER="127.0.0.1"
|
||||
FRONT_DNS_PORT="${listen_port}"
|
||||
}
|
||||
}
|
||||
|
||||
run_global_dnsmasq() {
|
||||
[ -z "${ACL_GLOBAL_node}" ] && [ -z "${FRONT_DNS_PORT}" ] && return
|
||||
local RUN_NEW_DNSMASQ=1
|
||||
RUN_NEW_DNSMASQ=${DNS_REDIRECT}
|
||||
DNSMASQ_DEFAULT_DNS="${AUTO_DNS}"
|
||||
DNSMASQ_LOCAL_DNS="${LOCAL_DNS:-${AUTO_DNS}}"
|
||||
DNSMASQ_TUN_DNS="${TUN_DNS}"
|
||||
[ -n "${FRONT_DNS_PORT}" ] && {
|
||||
DNSMASQ_DEFAULT_DNS="${FRONT_DNS_SERVER}#${FRONT_DNS_PORT}"
|
||||
DNSMASQ_LOCAL_DNS="${DNSMASQ_DEFAULT_DNS}"
|
||||
DNSMASQ_TUN_DNS="${DNSMASQ_DEFAULT_DNS}"
|
||||
}
|
||||
if [ "${RUN_NEW_DNSMASQ}" == "0" ]; then
|
||||
#The old logic will be removed in the future.
|
||||
#Run a copy dnsmasq instance, DNS hijack that don't need a proxy devices.
|
||||
@ -738,9 +754,9 @@ run_global() {
|
||||
json_add_string "FLAG" "default"
|
||||
json_add_string "TMP_DNSMASQ_PATH" "${GLOBAL_DNSMASQ_CONF_PATH}"
|
||||
json_add_string "DNSMASQ_CONF_FILE" "${GLOBAL_DNSMASQ_CONF}"
|
||||
json_add_string "DEFAULT_DNS" "${AUTO_DNS}"
|
||||
json_add_string "LOCAL_DNS" "${LOCAL_DNS:-${AUTO_DNS}}"
|
||||
json_add_string "TUN_DNS" "${TUN_DNS}"
|
||||
json_add_string "DEFAULT_DNS" "${DNSMASQ_DEFAULT_DNS}"
|
||||
json_add_string "LOCAL_DNS" "${DNSMASQ_LOCAL_DNS}"
|
||||
json_add_string "TUN_DNS" "${DNSMASQ_TUN_DNS}"
|
||||
json_add_string "NFTFLAG" "${nftflag:-0}"
|
||||
json_add_string "NO_LOGIC_LOG" "${NO_LOGIC_LOG:-0}"
|
||||
lua $APP_PATH/helper_dnsmasq.lua add_rule "$(json_dump)"
|
||||
@ -753,14 +769,11 @@ run_global() {
|
||||
else
|
||||
#Run a copy dnsmasq instance, DNS hijack for that need proxy devices.
|
||||
GLOBAL_DNSMASQ_PORT=$(get_new_port 11400)
|
||||
run_copy_dnsmasq flag="default" listen_port=$GLOBAL_DNSMASQ_PORT tun_dns="${TUN_DNS}"
|
||||
run_copy_dnsmasq flag="default" listen_port=$GLOBAL_DNSMASQ_PORT tun_dns="${DNSMASQ_TUN_DNS}"
|
||||
DNS_REDIRECT_PORT=${GLOBAL_DNSMASQ_PORT}
|
||||
#dhcp.leases to hosts
|
||||
$APP_PATH/lease2hosts.sh > /dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
set_cache_var "ACL_GLOBAL_node" "$NODE"
|
||||
set_cache_var "ACL_GLOBAL_redir_port" "$REDIR_PORT"
|
||||
}
|
||||
|
||||
start_socks() {
|
||||
@ -1188,7 +1201,10 @@ start() {
|
||||
lua $APP_PATH/helper_dnsmasq.lua restart "$(json_dump)"
|
||||
}
|
||||
fi
|
||||
mkdir -p ${GLOBAL_ACL_PATH}
|
||||
[ "$ENABLED_DEFAULT_ACL" == 1 ] && run_global
|
||||
run_front_dns
|
||||
run_global_dnsmasq
|
||||
[ -n "$USE_TABLES" ] && source $APP_PATH/${USE_TABLES}.sh start
|
||||
set_cache_var "USE_TABLES" "$USE_TABLES"
|
||||
if [ "$ENABLED_DEFAULT_ACL" == 1 ] || [ "$ENABLED_ACLS" == 1 ]; then
|
||||
@ -1312,6 +1328,12 @@ get_config() {
|
||||
[ -z "${DEFAULT_DNS}" ] && DEFAULT_DNS=$(echo -n $ISP_DNS | tr ' ' '\n' | head -2 | tr '\n' ',' | sed 's/,$//')
|
||||
AUTO_DNS=${DEFAULT_DNS:-119.29.29.29}
|
||||
|
||||
local AUTO_DNS2=$(get_first_dns AUTO_DNS 53 | sed 's/#/:/g')
|
||||
local AUTO_DNS_ADDRESS=$(echo ${AUTO_DNS2} | awk -F ':' '{print $1}')
|
||||
local AUTO_DNS_PORT=$(echo ${AUTO_DNS2} | awk -F ':' '{print $2}')
|
||||
DIRECT_DNS_UDP_SERVER=${AUTO_DNS_ADDRESS}
|
||||
DIRECT_DNS_UDP_PORT=${AUTO_DNS_PORT}
|
||||
|
||||
DNSMASQ_CONF_DIR=/tmp/dnsmasq.d
|
||||
DEFAULT_DNSMASQ_CFGID="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')"
|
||||
if [ -f "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID" ]; then
|
||||
|
||||
@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-ssr-plus
|
||||
PKG_VERSION:=190
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
|
||||
@ -5,6 +5,7 @@ require "nixio.fs"
|
||||
require "luci.sys"
|
||||
require "luci.http"
|
||||
require "luci.jsonc"
|
||||
local nixio = require "nixio"
|
||||
require "luci.model.uci"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
@ -146,11 +147,11 @@ local function set_apply_on_parse(map)
|
||||
end
|
||||
end
|
||||
|
||||
local has_xray = is_finded("xray")
|
||||
local has_hysteria2 = is_finded("hysteria")
|
||||
|
||||
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
|
||||
local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
|
||||
local has_trojan = is_finded("trojan")
|
||||
local has_xray = is_finded("xray")
|
||||
local has_hysteria2 = is_finded("hysteria")
|
||||
|
||||
local server_table = {}
|
||||
local encrypt_methods = {
|
||||
@ -277,25 +278,63 @@ o.value = sid
|
||||
-- 新增一个选择框,用于选择 Xray 或 Hysteria2 核心
|
||||
o = s:option(ListValue, "_xray_hy2_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Hysteria2")))
|
||||
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
|
||||
-- 设置默认 Xray 或 Hysteria2 核心
|
||||
-- 动态添加选项
|
||||
if has_xray then
|
||||
o:value("v2ray", translate("Xray (Hysteria2)"))
|
||||
end
|
||||
-- 注意:Auto 选项使用特殊字符串 "__auto__" 而不是空字符串
|
||||
o:value("__auto__", translate("Auto"))
|
||||
if has_hysteria2 then
|
||||
o:value("hysteria2", translate("Hysteria2"))
|
||||
o:value("hysteria2", translate("Hysteria2"))
|
||||
end
|
||||
if has_xray then
|
||||
o:value("v2ray", translate("Xray (Hysteria2)"))
|
||||
end
|
||||
-- 读取全局 xray_hy2_type
|
||||
o.cfgvalue = function(self, section)
|
||||
return self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type") or "hysteria2"
|
||||
local val = uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type")
|
||||
if val == nil or val == "" then
|
||||
return "__auto__" -- 对应 Auto 选项
|
||||
end
|
||||
return val
|
||||
end
|
||||
o.rmempty = true
|
||||
-- 保存时更新全局配置
|
||||
o.write = function(self, section, value)
|
||||
-- 更新 server_subscribe 的 xray_hy2_type
|
||||
local old_value = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type")
|
||||
if old_value ~= value then
|
||||
self.map.uci:set("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type", value)
|
||||
end
|
||||
if value == "__auto__" then
|
||||
-- 删除全局配置
|
||||
uci:delete("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type")
|
||||
else
|
||||
-- 设置具体值
|
||||
uci:set("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type", value)
|
||||
end
|
||||
end
|
||||
|
||||
-- 新增一个选择框,用于选择 Xray 或 Trojan 核心
|
||||
o = s:option(ListValue, "_xray_tj_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Trojan")))
|
||||
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
|
||||
-- 注意:Auto 选项使用特殊字符串 "__auto__" 而不是空字符串
|
||||
o:value("__auto__", translate("Auto"))
|
||||
if has_hysteria2 then
|
||||
o:value("trojan", translate("Trojan"))
|
||||
end
|
||||
if has_xray then
|
||||
o:value("v2ray", translate("Xray (Trojan)"))
|
||||
end
|
||||
-- 读取全局 xray_tj_type
|
||||
o.cfgvalue = function(self, section)
|
||||
local val = uci:get("shadowsocksr", "@server_subscribe[0]", "xray_tj_type")
|
||||
if val == nil or val == "" then
|
||||
return "__auto__" -- 对应 Auto 选项
|
||||
end
|
||||
return val
|
||||
end
|
||||
o.rmempty = true
|
||||
-- 保存时更新全局配置
|
||||
o.write = function(self, section, value)
|
||||
if value == "__auto__" then
|
||||
-- 删除全局配置
|
||||
uci:delete("shadowsocksr", "@server_subscribe[0]", "xray_tj_type")
|
||||
else
|
||||
-- 设置具体值
|
||||
uci:set("shadowsocksr", "@server_subscribe[0]", "xray_tj_type", value)
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "type", translate("Server Node Type"))
|
||||
@ -329,6 +368,29 @@ end
|
||||
if is_finded("redsocks2") then
|
||||
o:value("tun", translate("Network Tunnel"))
|
||||
end
|
||||
local old_cfgvalue = o.cfgvalue
|
||||
o.cfgvalue = function(self, section)
|
||||
local val = self.map.uci:get("shadowsocksr", section, "type")
|
||||
if val == "ss-rust" or val == "ss-libev" then
|
||||
return "ss"
|
||||
end
|
||||
if old_cfgvalue then
|
||||
return old_cfgvalue(self, section)
|
||||
end
|
||||
return val
|
||||
end
|
||||
-- 重写 write,当用户选择 "ss" 时不写入(由 _ss_core 负责写入具体核心)
|
||||
local old_write = o.write
|
||||
o.write = function(self, section, value)
|
||||
if value == "ss" then
|
||||
return -- 不做任何写入,等待 _ss_core 写入
|
||||
end
|
||||
if old_write then
|
||||
old_write(self, section, value)
|
||||
else
|
||||
self.map.uci:set("shadowsocksr", section, "type", value)
|
||||
end
|
||||
end
|
||||
|
||||
o.description = translate("Using incorrect encryption mothod may causes service fail to start")
|
||||
|
||||
@ -343,29 +405,37 @@ end
|
||||
o:depends("type", "tun")
|
||||
o.description = translate("Redirect traffic to this network interface")
|
||||
|
||||
-- 新增一个选择框,用于选择 Shadowsocks 版本
|
||||
o = s:option(ListValue, "_has_ss_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
|
||||
-- 新增一个选择框,用于选择 Shadowsocks 具体版本(仅当节点类型为 ss 或其具体子类型时显示)
|
||||
o = s:option(ListValue, "_ss_core", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
|
||||
o.description = translate("Selection ShadowSocks Node Use Version.")
|
||||
-- 设置默认 Shadowsocks 版本
|
||||
-- 动态添加选项
|
||||
if has_ss_rust then
|
||||
o:value("ss-rust", translate("ShadowSocks-rust Version"))
|
||||
o:value("ss-rust", translate("ShadowSocks-rust Version"))
|
||||
end
|
||||
if has_ss_libev then
|
||||
o:value("ss-libev", translate("ShadowSocks-libev Version"))
|
||||
o:value("ss-libev", translate("ShadowSocks-libev Version"))
|
||||
end
|
||||
-- 读取全局 ss_type
|
||||
o.cfgvalue = function(self, section)
|
||||
return self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type") or "ss-rust"
|
||||
-- 读取当前节点的 type 值,如果已经是具体核心则显示对应的选项
|
||||
local node_type = self.map.uci:get("shadowsocksr", section, "type")
|
||||
if node_type == "ss-rust" or node_type == "ss-libev" then
|
||||
return node_type
|
||||
end
|
||||
-- 如果全局 ss_type 有值且为具体核心则返回该值
|
||||
local ss_type = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type")
|
||||
if ss_type == "ss-rust" or ss_type == "ss-libev" then
|
||||
return ss_type
|
||||
end
|
||||
-- 如果节点 type 是旧的 "ss",则返回空,手动选择
|
||||
return nil
|
||||
end
|
||||
-- 显示条件:当节点类型为 "ss" 或其具体核心时显示
|
||||
o:depends("type", "ss")
|
||||
o.rmempty = true
|
||||
-- 保存时,将选择的值直接写入当前节点的 type 字段
|
||||
o.write = function(self, section, value)
|
||||
-- 更新 server_subscribe 的 ss_type
|
||||
local old_value = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type")
|
||||
if old_value ~= value then
|
||||
self.map.uci:set("shadowsocksr", "@server_subscribe[0]", "ss_type", value)
|
||||
end
|
||||
if value and value ~= "" then
|
||||
self.map.uci:set("shadowsocksr", section, "type", value)
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "v2ray_protocol", translate("V2Ray/XRay protocol"))
|
||||
@ -539,7 +609,7 @@ o = s:option(Value, "port_range", translate("Port hopping range"))
|
||||
o.description = translate("Format as 10000:20000 or 10000-20000 Multiple groups are separated by commas (,).")
|
||||
o:depends({type = "hysteria2", flag_port_hopping = true})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_port_hopping = true})
|
||||
--o.datatype = "portrange"
|
||||
o.datatype = "or(uinteger,portrange)"
|
||||
o.rmempty = true
|
||||
|
||||
o = s:option(Flag, "flag_transport", translate("Enable Transport Protocol Settings"))
|
||||
@ -554,9 +624,10 @@ o.default = "udp"
|
||||
o.rmempty = true
|
||||
|
||||
o = s:option(Value, "hopinterval", translate("Port Hopping Interval(Unit:Second)"))
|
||||
o.description = translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")
|
||||
o:depends({type = "hysteria2", flag_transport = true, flag_port_hopping = true})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_port_hopping = true})
|
||||
o.datatype = "uinteger"
|
||||
o.datatype = "or(uinteger,portrange)"
|
||||
o.rmempty = true
|
||||
o.default = "30"
|
||||
|
||||
|
||||
@ -295,4 +295,3 @@ end
|
||||
|
||||
m:section(SimpleSection).template = "shadowsocksr/status_bottom"
|
||||
return m
|
||||
|
||||
|
||||
@ -70,9 +70,14 @@ local function set_apply_on_parse(map)
|
||||
end
|
||||
end
|
||||
|
||||
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
|
||||
local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
|
||||
local has_trojan = is_finded("trojan")
|
||||
local has_xray = is_finded("xray")
|
||||
local has_hysteria2 = is_finded("hysteria")
|
||||
|
||||
local ss_type_list = {}
|
||||
local tj_type_list = {}
|
||||
local hy2_type_list = {}
|
||||
|
||||
if has_hysteria2 then
|
||||
@ -82,19 +87,12 @@ if has_xray then
|
||||
table.insert(hy2_type_list, { id = "v2ray", name = translate("Xray (Hysteria2)") })
|
||||
end
|
||||
|
||||
-- 如果用户没有手动设置,则自动选择
|
||||
if not xray_hy2_type or xray_hy2_type == "" then
|
||||
if has_hysteria2 then
|
||||
xray_hy2_type = "hysteria2"
|
||||
elseif has_xray then
|
||||
xray_hy2_type = "v2ray"
|
||||
end
|
||||
if has_trojan then
|
||||
table.insert(tj_type_list, { id = "trojan", name = translate("Trojan") })
|
||||
end
|
||||
if has_xray then
|
||||
table.insert(tj_type_list, { id = "v2ray", name = translate("Xray (Trojan)") })
|
||||
end
|
||||
|
||||
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
|
||||
local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
|
||||
|
||||
local ss_type_list = {}
|
||||
|
||||
if has_ss_rust then
|
||||
table.insert(ss_type_list, { id = "ss-rust", name = translate("ShadowSocks-rust Version") })
|
||||
@ -106,17 +104,6 @@ if has_xray then
|
||||
table.insert(ss_type_list, { id = "v2ray", name = translate("Xray (ShadowSocks)") })
|
||||
end
|
||||
|
||||
-- 如果用户没有手动设置,则自动选择
|
||||
if not ss_type or ss_type == "" then
|
||||
if has_ss_rust then
|
||||
ss_type = "ss-rust"
|
||||
elseif has_ss_libev then
|
||||
ss_type = "ss-libev"
|
||||
elseif has_xray then
|
||||
ss_type = "v2ray"
|
||||
end
|
||||
end
|
||||
|
||||
uci:foreach("shadowsocksr", "servers", function(s)
|
||||
server_count = server_count + 1
|
||||
end)
|
||||
@ -162,22 +149,87 @@ o:depends("auto_update", "1")
|
||||
|
||||
-- 确保 hy2_type_list 不为空
|
||||
if #hy2_type_list > 0 then
|
||||
local sid = uci:get_first("shadowsocksr", "server_subscribe")
|
||||
if not sid then
|
||||
uci:foreach("shadowsocksr", "server_subscribe", function(section)
|
||||
sid = section[".name"]
|
||||
return false
|
||||
end)
|
||||
end
|
||||
if sid then
|
||||
local old_val = uci:get("shadowsocksr", sid, "xray_hy2_type")
|
||||
if old_val and old_val ~= "" then
|
||||
if (old_val == "hysteria2" and not has_hysteria2) or
|
||||
(old_val == "v2ray" and not has_xray) then
|
||||
-- 核心不可用,设置为空(删除配置)
|
||||
uci:set("shadowsocksr", sid, "xray_hy2_type", "")
|
||||
uci:commit("shadowsocksr")
|
||||
end
|
||||
end
|
||||
end
|
||||
o = s:option(ListValue, "xray_hy2_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Hysteria2")))
|
||||
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
|
||||
o:value("", translate("Auto"))
|
||||
for _, v in ipairs(hy2_type_list) do
|
||||
o:value(v.id, v.name) -- 存储 "Xray" / "Hysteria2",但 UI 显示完整名称
|
||||
end
|
||||
o.default = xray_hy2_type -- 设置默认值
|
||||
end
|
||||
|
||||
-- 确保 tj_type_list 不为空
|
||||
if #tj_type_list > 0 then
|
||||
local sid = uci:get_first("shadowsocksr", "server_subscribe")
|
||||
if not sid then
|
||||
uci:foreach("shadowsocksr", "server_subscribe", function(section)
|
||||
sid = section[".name"]
|
||||
return false
|
||||
end)
|
||||
end
|
||||
if sid then
|
||||
local old_val = uci:get("shadowsocksr", sid, "xray_tj_type")
|
||||
if old_val and old_val ~= "" then
|
||||
if (old_val == "trojan" and not has_trojan) or
|
||||
(old_val == "v2ray" and not has_xray) then
|
||||
-- 核心不可用,设置为空(删除配置)
|
||||
uci:set("shadowsocksr", sid, "xray_tj_type", "")
|
||||
uci:commit("shadowsocksr")
|
||||
end
|
||||
end
|
||||
end
|
||||
o = s:option(ListValue, "xray_tj_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Trojan")))
|
||||
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
|
||||
o:value("", translate("Auto"))
|
||||
for _, v in ipairs(tj_type_list) do
|
||||
o:value(v.id, v.name) -- 存储 "Xray" / "Trojan",但 UI 显示完整名称
|
||||
end
|
||||
end
|
||||
|
||||
-- 确保 ss_type_list 不为空
|
||||
if #ss_type_list > 0 then
|
||||
local sid = uci:get_first("shadowsocksr", "server_subscribe")
|
||||
if not sid then
|
||||
uci:foreach("shadowsocksr", "server_subscribe", function(section)
|
||||
sid = section[".name"]
|
||||
return false
|
||||
end)
|
||||
end
|
||||
if sid then
|
||||
local old_val = uci:get("shadowsocksr", sid, "ss_type")
|
||||
if old_val and old_val ~= "" then
|
||||
if (old_val == "ss-rust" and not has_ss_rust) or
|
||||
(old_val == "ss-libev" and not has_ss_libev) or
|
||||
(old_val == "v2ray" and not has_xray) then
|
||||
-- 核心不可用,设置为空(删除配置)
|
||||
uci:set("shadowsocksr", sid, "ss_type", "")
|
||||
uci:commit("shadowsocksr")
|
||||
end
|
||||
end
|
||||
end
|
||||
o = s:option(ListValue, "ss_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
|
||||
o.description = translate("Selection ShadowSocks Node Use Version.")
|
||||
o:value("", translate("Auto"))
|
||||
for _, v in ipairs(ss_type_list) do
|
||||
o:value(v.id, v.name) -- 存储 "ss-libev" / "ss-rust",但 UI 显示完整名称
|
||||
end
|
||||
o.default = ss_type -- 设置默认值
|
||||
end
|
||||
|
||||
o = s:option(DynamicList, "subscribe_url", translate("Subscribe URL"))
|
||||
|
||||
@ -3,11 +3,24 @@
|
||||
local map = self.map
|
||||
local ss_type = map:get("@server_subscribe[0]", "ss_type")
|
||||
local xray_hy2_type = map:get("@server_subscribe[0]", "xray_hy2_type")
|
||||
local xray_tj_type = map:get("@server_subscribe[0]", "xray_tj_type")
|
||||
|
||||
local has_ss_rust = luci.sys.exec('type -t -p sslocal 2>/dev/null || type -t -p ssserver 2>/dev/null') ~= ""
|
||||
local has_ss_libev = luci.sys.exec('type -t -p ss-redir 2>/dev/null || type -t -p ss-local 2>/dev/null') ~= ""
|
||||
local has_hysteria = luci.sys.exec('type -t -p hysteria 2>/dev/null') ~= ""
|
||||
local has_trojan = luci.sys.exec('type -t -p trojan 2>/dev/null') ~= ""
|
||||
local has_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= ""
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
let ss_type = "<%=ss_type%>"
|
||||
let xray_hy2_type = "<%=xray_hy2_type%>"
|
||||
let xray_tj_type = "<%=xray_tj_type%>"
|
||||
let has_ss_rust = "<%=has_ss_rust%>"
|
||||
let has_ss_libev = "<%=has_ss_libev%>"
|
||||
let has_hysteria = "<%=has_hysteria%>"
|
||||
let has_trojan = "<%=has_trojan%>"
|
||||
let has_xray = "<%=has_xray%>"
|
||||
|
||||
function padright(str, cnt, pad) {
|
||||
return str + Array(cnt + 1).join(pad);
|
||||
@ -112,48 +125,105 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (xray_hy2_type === "hysteria2") {
|
||||
// 普通 Hysteria2 导入函数
|
||||
function importAsNormalHy2() {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
|
||||
|
||||
if (params.get("protocol") && params.get("protocol").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].checked = true; // 设置 flag_transport 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].dispatchEvent(event); // 触发事件
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.transport_protocol')[0].value = params.get("protocol") || "udp";
|
||||
}
|
||||
if (params.get("lazy") === "1") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].checked = true;
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].dispatchEvent(event);
|
||||
}
|
||||
if (params.get("sni") || params.get("alpn")) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; // 设置 tls 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event); // 触发事件
|
||||
if (params.get("sni")) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("sni") || "";
|
||||
}
|
||||
if (params.get("alpn") && params.get("alpn").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_alpn')[0].value = params.get("alpn") || "";
|
||||
}
|
||||
}
|
||||
if (params.get("pinSHA256") && params.get("pinSHA256").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.pinsha256')[0].value = params.get("pinSHA256") || "";
|
||||
}
|
||||
}
|
||||
|
||||
// Xray Hysteria2 导入函数
|
||||
function importAsXrayHy2() {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray"
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].dispatchEvent(event);
|
||||
if (params.get("security") === "tls" || params.get("sni") || params.get("alpn") || (params.get("pcs") || params.get("vcn"))) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; // 设置 tls 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event); // 触发事件
|
||||
if (params.get("sni")) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("sni") || "";
|
||||
}
|
||||
if (params.get("alpn") && params.get("alpn").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_alpn')[0].value = params.get("alpn") || "";
|
||||
}
|
||||
if (params.get("pcs") && params.get("pcs").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertSha')[0].value = params.get("pcs") || "";
|
||||
}
|
||||
if (params.get("vcn") && params.get("vcn").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertByName')[0].value = params.get("vcn") || "";
|
||||
}
|
||||
}
|
||||
if (params.get("fm") && params.get("fm").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].checked = true; // 设置 enable_finalmask 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].dispatchEvent(event); // 触发事件
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.finalmask')[0].value = params.get("fm") || "";
|
||||
}
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
|
||||
} else {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray"
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].dispatchEvent(event);
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
|
||||
}
|
||||
|
||||
// 主逻辑:智能核心选择(支持回退)
|
||||
function hasCore(core) {
|
||||
if (core === "hysteria") return has_hysteria === "true";
|
||||
if (core === "xray") return has_xray === "true";
|
||||
return false;
|
||||
}
|
||||
let finalMode = null; // true=Xray, false=普通, null=无核心
|
||||
if (xray_hy2_type === "v2ray") {
|
||||
if (hasCore("xray")) finalMode = true;
|
||||
else if (hasCore("hysteria")) finalMode = false;
|
||||
else finalMode = null;
|
||||
} else if (xray_hy2_type === "hysteria2") {
|
||||
if (hasCore("hysteria")) finalMode = false;
|
||||
else if (hasCore("xray")) finalMode = true;
|
||||
else finalMode = null;
|
||||
} else {
|
||||
// auto 或空:Hysteria2 无 type 参数,优先普通 Hysteria2
|
||||
if (hasCore("hysteria")) finalMode = false;
|
||||
else if (hasCore("xray")) finalMode = true;
|
||||
else finalMode = null;
|
||||
}
|
||||
if (finalMode === null) {
|
||||
s.innerHTML = "<font style=\'color:red\'><%:No available core (Hysteria2 or Xray) to import this node.%></font>";
|
||||
return false;
|
||||
}
|
||||
if (finalMode === true) importAsXrayHy2();
|
||||
else importAsNormalHy2();
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port || "443";
|
||||
if (params.get("lazy") === "1") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].checked = true;
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].dispatchEvent(event);
|
||||
}
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].value = decodeURIComponent(url.username);
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].dispatchEvent(event);
|
||||
|
||||
if (params.get("mport")) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_port_hopping')[0].checked = true; // 设置 flag_port_hopping 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_port_hopping')[0].dispatchEvent(event); // 触发事件
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.port_range')[0].value = params.get("mport") || "";
|
||||
}
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].value = decodeURIComponent(url.username);
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].dispatchEvent(event);
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.uplink_capacity')[0].value =
|
||||
(params.get("upmbps") && params.get("upmbps").match(/\d+/)) ? params.get("upmbps").match(/\d+/)[0] : "";
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.downlink_capacity')[0].value =
|
||||
@ -165,25 +235,6 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.obfs_type')[0].value = params.get("obfs");
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.salamander')[0].value = params.get("obfs-password") || params.get("obfs_password");
|
||||
}
|
||||
if (params.get("security") === "tls" || params.get("sni") || params.get("alpn")
|
||||
|| (xray_hy2_type !== "hysteria2" && (params.get("pcs") || params.get("vcn")))) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; // 设置 tls 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event); // 触发事件
|
||||
if (params.get("sni") && params.get("protocol").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("sni") || "";
|
||||
}
|
||||
if (params.get("alpn") && params.get("alpn").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_alpn')[0].value = params.get("alpn") || "";
|
||||
}
|
||||
if (xray_hy2_type !== "hysteria2") {
|
||||
if (params.get("pcs") && params.get("pcs").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertSha')[0].value = params.get("pcs") || "";
|
||||
}
|
||||
if (params.get("vcn") && params.get("vcn").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertByName')[0].value = params.get("vcn") || "";
|
||||
}
|
||||
}
|
||||
}
|
||||
var allowInsecureValue = params.get("allowInsecure") || params.get("insecure");
|
||||
if (allowInsecureValue === "1" || allowInsecureValue === "true") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.insecure')[0].checked = true; // 设置 insecure 为 true
|
||||
@ -218,7 +269,7 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
|
||||
var params = Object.fromEntries(new URLSearchParams(query));
|
||||
|
||||
if ( ss_type !== "v2ray") {
|
||||
function importAsNormalSS() {
|
||||
// 普通 SS 导入逻辑
|
||||
// 判断是否 SIP002 格式(即含 @)
|
||||
if (url0.indexOf("@") !== -1) {
|
||||
@ -226,15 +277,28 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
var sipIndex = url0.indexOf("@");
|
||||
// 先 URL 解码 base64 再解码
|
||||
var userInfoB64 = decodeURIComponent(url0.substring(0, sipIndex));
|
||||
// console.log("userInfoB64:", userInfoB64);
|
||||
var userInfo = b64decsafe(userInfoB64);
|
||||
if (userInfo && userInfo.indexOf(":") !== -1) {
|
||||
var userInfo = userInfo;
|
||||
} else {
|
||||
var userInfo = userInfoB64;
|
||||
}
|
||||
// 如果没有冒号,且不是 base64 格式,我们可以尝试补齐它
|
||||
if (userInfo.indexOf(":") === -1) {
|
||||
userInfo = "none:" + userInfo;
|
||||
}
|
||||
// console.log("userInfo after decode:", userInfo);
|
||||
var userInfoSplitIndex = userInfo.indexOf(":");
|
||||
if(userInfoSplitIndex < 0) {
|
||||
// 格式错误
|
||||
s.innerHTML = "<font style='color:red'><%:Userinfo format error.%></font>";
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
var method = userInfo.substring(0, userInfoSplitIndex);
|
||||
// console.log("method:", method);
|
||||
var password = userInfo.substring(userInfoSplitIndex + 1);
|
||||
// console.log("password:", password);
|
||||
var serverPart = url0.substring(sipIndex + 1);
|
||||
var serverInfo = serverPart.split(":");
|
||||
|
||||
@ -254,7 +318,7 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
var sstr = b64decsafe(decodedUrl0);
|
||||
if (!sstr) {
|
||||
s.innerHTML = "<font style='color:red'><%:Base64 sstr failed.%></font>";
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 支持 SS2022 / 普通格式
|
||||
@ -276,7 +340,7 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
var port = mNormal[4];
|
||||
} else {
|
||||
s.innerHTML = "<font style='color:red'><%:SS URL base64 sstr format not recognized.%></font>";
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
|
||||
var plugin = "", pluginOpts = "";
|
||||
@ -314,9 +378,17 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
|
||||
// === 填充配置项 ===
|
||||
//var has_ss_type = (ss_type === "ss-rust") ? "ss-rust" : "ss-libev";
|
||||
// 设置 _ss_core 的值
|
||||
var ssCore = (ss_type === "ss-rust" || ss_type === "ss-libev")
|
||||
? ss_type
|
||||
: (has_ss_rust ? "ss-rust" : (has_ss_libev ? "ss-libev" : "ss-libev"));
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = ssu[0];
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '._ss_core')[0].value = ssCore;
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '._ss_core')[0].dispatchEvent(event);
|
||||
|
||||
//document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].value = has_ss_type;
|
||||
//document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].dispatchEvent(event);
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = server;
|
||||
@ -351,8 +423,10 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = decodeURIComponent(param);
|
||||
}
|
||||
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
function importAsXraySS() {
|
||||
try {
|
||||
// Xray SS 导入逻辑
|
||||
// 拆分 @,判断是否是 base64 userinfo 的格式
|
||||
@ -366,7 +440,14 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
password = userinfo.slice(sepIndex + 1); //一些链接用明文uuid做密码
|
||||
}
|
||||
}
|
||||
var url = new URL("http://" + url0 + (param ? "#" + encodeURIComponent(param) : ""));
|
||||
// 已编码的别名
|
||||
// var url = new URL("http://" + url0 + (param ? "#" + encodeURIComponent(param) : ""));
|
||||
// 未编码的别名
|
||||
// var url = new URL("http://" + url0 + (param ? "#" + param : ""));
|
||||
// 兼容已编码和未编码的别名
|
||||
var remarks = param ? decodeURIComponent(param) : "";
|
||||
var encodedremarks = encodeURIComponent(remarks);
|
||||
var url = new URL("http://" + url0 + (encodedremarks ? "#" + encodedremarks : ""));
|
||||
|
||||
} catch(e) {
|
||||
alert(e);
|
||||
@ -411,7 +492,7 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
}
|
||||
if (params.fm && params.fm.trim() !== "") {
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.enable_finalmask', true); // 设置 enable_finalmask 为 true
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.enable_finalmask', event); // 触发事件
|
||||
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_finalmask', event); // 触发事件
|
||||
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.finalmask', params.fm || "");
|
||||
}
|
||||
@ -514,8 +595,46 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
break;
|
||||
}
|
||||
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
|
||||
return true;
|
||||
}
|
||||
|
||||
// 主逻辑:智能核心选择(支持回退)
|
||||
function hasSSCore() { return has_ss_rust === "true" || has_ss_libev === "true"; }
|
||||
function hasXrayCore() { return has_xray === "true"; }
|
||||
let finalSSType = null; // true=Xray, false=普通SS, null=无核心
|
||||
if (ss_type === "v2ray") {
|
||||
if (hasXrayCore()) finalSSType = true;
|
||||
else if (hasSSCore()) finalSSType = false;
|
||||
else finalSSType = null;
|
||||
} else if (ss_type === "ss-rust" || ss_type === "ss-libev") {
|
||||
let userSSCore = (ss_type === "ss-rust" && has_ss_rust === "true") || (ss_type === "ss-libev" && has_ss_libev === "true");
|
||||
if (userSSCore) finalSSType = false;
|
||||
else {
|
||||
let otherSSCore = (ss_type === "ss-rust" && has_ss_libev === "true") || (ss_type === "ss-libev" && has_ss_rust === "true");
|
||||
if (otherSSCore) finalSSType = false;
|
||||
else if (hasXrayCore()) finalSSType = true;
|
||||
else finalSSType = null;
|
||||
}
|
||||
} else {
|
||||
// auto 或空:根据 type 参数决定
|
||||
let hasType = params.type && params.type !== "";
|
||||
if (hasType) {
|
||||
if (hasXrayCore()) finalSSType = true;
|
||||
else if (hasSSCore()) finalSSType = false;
|
||||
else finalSSType = null;
|
||||
} else {
|
||||
if (hasSSCore()) finalSSType = false;
|
||||
else if (hasXrayCore()) finalSSType = true;
|
||||
else finalSSType = null;
|
||||
}
|
||||
}
|
||||
if (finalSSType === null) {
|
||||
s.innerHTML = "<font style=\'color:red\'><%:No available core (Shadowsocks or Xray) to import this node.%></font>";
|
||||
return false;
|
||||
}
|
||||
if (finalSSType === true) importAsXraySS();
|
||||
else importAsNormalSS();
|
||||
return false;
|
||||
case "ssr":
|
||||
var sstr = b64decsafe((ssu[1] || "").replace(/#.*/, "").trim());
|
||||
var ploc = sstr.indexOf("/?");
|
||||
@ -561,8 +680,8 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!params.get("type")) {
|
||||
// 普通 Trojan 导入逻辑
|
||||
// 普通 Trojan 导入逻辑
|
||||
function importAsNormalTrojan() {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "trojan";
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
|
||||
@ -585,9 +704,11 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
}
|
||||
|
||||
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
|
||||
return false;
|
||||
} else {
|
||||
// Xray Trojan 导入逻辑
|
||||
return true;
|
||||
}
|
||||
|
||||
// Xray Trojan 导入逻辑
|
||||
function importAsXrayTrojan() {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray";
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
|
||||
@ -690,8 +811,40 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
}
|
||||
|
||||
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
|
||||
return true;
|
||||
}
|
||||
|
||||
// 主逻辑:智能核心选择(支持回退)
|
||||
function hasTrojanCore() { return has_trojan === "true"; }
|
||||
function hasXrayCore() { return has_xray === "true"; }
|
||||
let finalTrojanMode = null;
|
||||
if (xray_tj_type === "v2ray") {
|
||||
if (hasXrayCore()) finalTrojanMode = true;
|
||||
else if (hasTrojanCore()) finalTrojanMode = false;
|
||||
else finalTrojanMode = null;
|
||||
} else if (xray_tj_type === "trojan") {
|
||||
if (hasTrojanCore()) finalTrojanMode = false;
|
||||
else if (hasXrayCore()) finalTrojanMode = true;
|
||||
else finalTrojanMode = null;
|
||||
} else {
|
||||
let hasType = params.get("type") && params.get("type") !== "";
|
||||
if (hasType) {
|
||||
if (hasXrayCore()) finalTrojanMode = true;
|
||||
else if (hasTrojanCore()) finalTrojanMode = false;
|
||||
else finalTrojanMode = null;
|
||||
} else {
|
||||
if (hasTrojanCore()) finalTrojanMode = false;
|
||||
else if (hasXrayCore()) finalTrojanMode = true;
|
||||
else finalTrojanMode = null;
|
||||
}
|
||||
}
|
||||
if (finalTrojanMode === null) {
|
||||
s.innerHTML = "<font style=\'color:red\'><%:No available core (Trojan or Xray) to import this node.%></font>";
|
||||
return false;
|
||||
}
|
||||
if (finalTrojanMode === true) importAsXrayTrojan();
|
||||
else importAsNormalTrojan();
|
||||
return false;
|
||||
case "vmess":
|
||||
var sstr = b64DecodeUnicode((ssu[1] || "").replace(/#.*/, "").trim());
|
||||
var ploc = sstr.indexOf("/?");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -611,15 +611,18 @@ gen_config_file() { #server1 type2 code3 local_port4 socks_port5 chain6 threads5
|
||||
}
|
||||
|
||||
start_udp() {
|
||||
local type=$(uci_get_by_name $UDP_RELAY_SERVER type)
|
||||
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
|
||||
local udp_relay_server_type=$(uci_get_by_name $UDP_RELAY_SERVER type)
|
||||
local type=$udp_relay_server_type
|
||||
if [ "$udp_relay_server_type" = "ss-rust" ] || [ "$udp_relay_server_type" = "ss-libev" ]; then
|
||||
type="ss"
|
||||
fi
|
||||
redir_udp=1
|
||||
case "$type" in
|
||||
ss | ssr)
|
||||
gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_port
|
||||
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
|
||||
if [ "$udp_relay_server_type" = "ss-libev" ] || [ "$udp_relay_server_type" = "ssr" ]; then
|
||||
ss_program="$(first_type ${type}-redir)"
|
||||
elif [ "$has_ss_type" = "ss-rust" ]; then
|
||||
elif [ "$udp_relay_server_type" = "ss-rust" ]; then
|
||||
ss_program="$(first_type ${type}local)"
|
||||
fi
|
||||
echolog "$(get_name $type) program is: $ss_program"
|
||||
@ -858,14 +861,17 @@ shunt_dns_config_file_port() {
|
||||
}
|
||||
|
||||
start_shunt() {
|
||||
local type=$(uci_get_by_name $SHUNT_SERVER type)
|
||||
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
|
||||
local shunt_server_type=$(uci_get_by_name $SHUNT_SERVER type)
|
||||
local type=$shunt_server_type
|
||||
if [ "$shunt_server_type" = "ss-rust" ] || [ "$shunt_server_type" = "ss-libev" ]; then
|
||||
type="ss"
|
||||
fi
|
||||
case "$type" in
|
||||
ss | ssr)
|
||||
gen_config_file $SHUNT_SERVER $type 3 $tmp_shunt_port
|
||||
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
|
||||
if [ "$shunt_server_type" = "ss-libev" ] || [ "$shunt_server_type" = "ssr" ]; then
|
||||
ss_program="$(first_type ${type}-redir)"
|
||||
elif [ "$has_ss_type" = "ss-rust" ]; then
|
||||
elif [ "$shunt_server_type" = "ss-rust" ]; then
|
||||
ss_program="$(first_type ${type}local)"
|
||||
fi
|
||||
echolog "$(get_name $type) program is: $ss_program"
|
||||
@ -880,9 +886,9 @@ start_shunt() {
|
||||
local tmp_port=$tmp_local_port
|
||||
else
|
||||
local tmp_port=$tmp_shunt_local_port
|
||||
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
|
||||
if [ "$shunt_server_type" = "ss-libev" ] || [ "$shunt_server_type" = "ssr" ]; then
|
||||
dns_ss_program="$(first_type ${type}-local)"
|
||||
elif [ "$has_ss_type" = "ss-rust" ]; then
|
||||
elif [ "$shunt_server_type" = "ss-rust" ]; then
|
||||
dns_ss_program="$(first_type ${type}local)"
|
||||
fi
|
||||
# 获取当前软链接指向的执行文件路径
|
||||
@ -1009,14 +1015,17 @@ start_local() {
|
||||
[ "$LOCAL_SERVER" = "nil" ] && return 1
|
||||
local local_port=$(uci_get_by_type socks5_proxy local_port)
|
||||
[ "$LOCAL_SERVER" == "$SHUNT_SERVER" ] && tmp_local_port=$local_port
|
||||
local type=$(uci_get_by_name $LOCAL_SERVER type)
|
||||
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
|
||||
local local_server_type=$(uci_get_by_name $LOCAL_SERVER type)
|
||||
local type=$local_server_type
|
||||
if [ "$local_server_type" = "ss-rust" ] || [ "$local_server_type" = "ss-libev" ]; then
|
||||
type="ss"
|
||||
fi
|
||||
case "$type" in
|
||||
ss | ssr)
|
||||
gen_config_file $LOCAL_SERVER $type 4 $local_port
|
||||
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
|
||||
if [ "$local_server_type" = "ss-libev" ] || [ "$local_server_type" = "ssr" ]; then
|
||||
ss_program="$(first_type ${type}-local)"
|
||||
elif [ "$has_ss_type" = "ss-rust" ]; then
|
||||
elif [ "$local_server_type" = "ss-rust" ]; then
|
||||
ss_program="$(first_type ${type}local)"
|
||||
fi
|
||||
echolog "$(get_name $type) program is: $ss_program"
|
||||
@ -1124,14 +1133,17 @@ Start_Run() {
|
||||
#}
|
||||
fi
|
||||
local tcp_port=$(uci_get_by_name $GLOBAL_SERVER local_port)
|
||||
local type=$(uci_get_by_name $GLOBAL_SERVER type)
|
||||
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
|
||||
local global_server_type=$(uci_get_by_name $GLOBAL_SERVER type)
|
||||
local type=$global_server_type
|
||||
if [ "$global_server_type" = "ss-rust" ] || [ "$global_server_type" = "ss-libev" ]; then
|
||||
type="ss"
|
||||
fi
|
||||
case "$type" in
|
||||
ss | ssr)
|
||||
gen_config_file $GLOBAL_SERVER $type 1 $tcp_port
|
||||
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
|
||||
if [ "$global_server_type" = "ss-libev" ] || [ "$global_server_type" = "ssr" ]; then
|
||||
ss_program="$(first_type ${type}-redir)"
|
||||
elif [ "$has_ss_type" = "ss-rust" ]; then
|
||||
elif [ "$global_server_type" = "ss-rust" ]; then
|
||||
ss_program="$(first_type ${type}local)"
|
||||
fi
|
||||
echolog "$(get_name $type) program is: $ss_program"
|
||||
@ -1367,14 +1379,17 @@ start_server() {
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
local type=$(uci_get_by_name $1 type)
|
||||
local has_ss_type=$(uci_get_by_type server_subscribe ss_type)
|
||||
local node_type=$(uci_get_by_name $1 type)
|
||||
local type=$node_type
|
||||
if [ "$node_type" = "ss-rust" ] || [ "$node_type" = "ss-libev" ]; then
|
||||
type="ss"
|
||||
fi
|
||||
case "$type" in
|
||||
ss | ssr)
|
||||
gen_service_file ${type} $1 $TMP_PATH/ssr-server$server_count.json
|
||||
if [ "$has_ss_type" = "ss-libev" -o "$type" = "ssr" ]; then
|
||||
if [ "$node_type" = "ss-libev" ] || [ "$node_type" = "ssr" ]; then
|
||||
ss_program="$(first_type ${type}-server)"
|
||||
elif [ "$has_ss_type" = "ss-rust" ]; then
|
||||
elif [ "$node_type" = "ss-rust" ]; then
|
||||
ss_program="$(first_type ${type}server)"
|
||||
fi
|
||||
# 获取当前软链接指向的执行文件路径
|
||||
@ -1506,7 +1521,7 @@ start_xhttp_addr() {
|
||||
# MD5 不同更新 download_address 地址文件
|
||||
if [ "$md5_new" != "$md5_old" ]; then
|
||||
mv -f "$tmp_file" "$xhttp_addr_file"
|
||||
logger -t ssrplus-xhttp "xhttp_address.txt updated"
|
||||
logger -t ssrplus-xhttp "download_address.txt updated"
|
||||
else
|
||||
rm -f "$tmp_file"
|
||||
fi
|
||||
|
||||
@ -1241,7 +1241,6 @@ compare_rules() {
|
||||
fi
|
||||
|
||||
# Generate temporary file for current rules
|
||||
local temp_file=$(mktemp)
|
||||
local rules_file=$(mktemp)
|
||||
loger 7 "DEBUG: Temporary file path: $rules_file"
|
||||
|
||||
@ -1255,18 +1254,18 @@ compare_rules() {
|
||||
# Check if current rules were exported successfully
|
||||
if [ ! -s "$rules_file" ] || ! grep -q "table" "$rules_file" 2>/dev/null; then
|
||||
loger 4 "Failed to export current rules"
|
||||
rm -f "$temp_file" "$rules_file"
|
||||
rm -f "$rules_file"
|
||||
return 1 # Export failed, need update
|
||||
fi
|
||||
|
||||
# Compare current rules with rules in persistence file
|
||||
if ! cmp -s "$rules_file" "$NFTABLES_RULES_FILE"; then
|
||||
loger 6 "Rules differ, update needed"
|
||||
rm -f "$temp_file" "$rules_file"
|
||||
rm -f "$rules_file"
|
||||
return 1 # Need update
|
||||
fi
|
||||
|
||||
rm -f "$temp_file" "$rules_file"
|
||||
rm -f "$rules_file"
|
||||
loger 6 "Rules unchanged, no update needed"
|
||||
return 0 # No update needed
|
||||
}
|
||||
@ -1277,21 +1276,17 @@ persist_nftables_rules() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# If mode unchanged and persistence file exists, skip update
|
||||
if [ "$MODE_CHANGED" = "0" ] && [ -f "$NFTABLES_RULES_FILE" ]; then
|
||||
loger 6 "Mode unchanged and persistence file exists, skipping update"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Force update: skip comparison check and delete old file
|
||||
if [ "$FORCE_UPDATE" = "1" ]; then
|
||||
loger 6 "Force update requested, removing old persistence file"
|
||||
rm -f "$NFTABLES_RULES_FILE" 2>/dev/null
|
||||
# Non-force update: compare rules
|
||||
# Otherwise, if persistence file exists, compare rules
|
||||
elif [ -f "$NFTABLES_RULES_FILE" ]; then
|
||||
if compare_rules; then
|
||||
loger 6 "Rules unchanged, skipping persistence update"
|
||||
return 0
|
||||
else
|
||||
loger 6 "Rules changed, updating persistence"
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -1299,7 +1294,7 @@ persist_nftables_rules() {
|
||||
mkdir -p "$NFTABLES_RULES_DIR" 2>/dev/null
|
||||
|
||||
# Generate nftables rule file
|
||||
cat <<-'EOF' >>$NFTABLES_RULES_FILE
|
||||
cat <<-'EOF' > "$NFTABLES_RULES_FILE"
|
||||
#!/usr/sbin/nft -f
|
||||
|
||||
# ShadowsocksR nftables rules
|
||||
@ -1374,17 +1369,18 @@ start_auto_update_daemon() {
|
||||
echo $$ > "/var/run/ssr-rules-daemon.pid"
|
||||
|
||||
while true; do
|
||||
sleep 300
|
||||
sleep "$AUTO_UPDATE_INTERVAL"
|
||||
if [ -x "/usr/bin/ssr-rules" ]; then
|
||||
# -C returns 0 if rules are OK, non-zero if need update
|
||||
if /usr/bin/ssr-rules -C >/dev/null 2>&1; then
|
||||
logger -t ssr-rules[daemon] "Rules status OK, no update needed"
|
||||
else
|
||||
logger -t ssr-rules[daemon] "Rules changed or missing, updating persistence"
|
||||
if /usr/bin/ssr-rules -P >/dev/null 2>&1; then
|
||||
logger -t ssr-rules[daemon] "Persistence rules updated successfully"
|
||||
else
|
||||
logger -t ssr-rules[daemon] "Failed to update persistence"
|
||||
fi
|
||||
else
|
||||
logger -t ssr-rules[daemon] "Rules status OK, no update needed"
|
||||
fi
|
||||
else
|
||||
logger -t ssr-rules[daemon] "Script not found, exiting daemon"
|
||||
|
||||
@ -25,6 +25,10 @@ local remarks = server.alias or ""
|
||||
local b64decode = nixio.bin.b64decode
|
||||
local b64encode = nixio.bin.b64encode
|
||||
|
||||
if server.type == "ss-rust" or server.type == "ss-libev" then
|
||||
server.type = "ss"
|
||||
end
|
||||
|
||||
-- base64 解码
|
||||
local function base64Decode(text)
|
||||
local raw = text
|
||||
@ -527,6 +531,18 @@ end
|
||||
udpHop = (server.flag_port_hopping == "1") and {
|
||||
ports = string.gsub(server.port_range, ":", "-"),
|
||||
interval = (function(v)
|
||||
if not v then return 30 end
|
||||
if v:find("-", 1, true) then
|
||||
local min, max = v:match("^(%d+)%-(%d+)$")
|
||||
min = tonumber(min)
|
||||
max = tonumber(max)
|
||||
if min and max then
|
||||
min = (min >= 5) and min or 5
|
||||
max = (max >= min) and max or min
|
||||
return min .. "-" .. max
|
||||
end
|
||||
return 30
|
||||
end
|
||||
v = tonumber((v or "30"):match("^%d+"))
|
||||
return (v and v >= 5) and v or 30
|
||||
end)(server.hopinterval)
|
||||
@ -736,12 +752,30 @@ local hysteria2 = {
|
||||
listen = "0.0.0.0:" .. tonumber(socks_port),
|
||||
disableUDP = false
|
||||
} or nil,
|
||||
transport = server.transport_protocol and {
|
||||
transport = {
|
||||
type = server.transport_protocol or "udp",
|
||||
udp = (server.port_range and (server.hopinterval) and {
|
||||
hopInterval = (server.port_range and (tonumber(server.hopinterval) .. "s") or nil)
|
||||
} or nil)
|
||||
} or nil,
|
||||
udp = server.port_range and (function()
|
||||
local udp = {}
|
||||
local t = server.hopinterval
|
||||
if not t then return nil end
|
||||
if t:find("-", 1, true) then
|
||||
local min, max = t:match("^(%d+)%-(%d+)$")
|
||||
min = tonumber(min)
|
||||
max = tonumber(max)
|
||||
if min and max then
|
||||
min = (min >= 5) and min or 5
|
||||
max = (max >= min) and max or min
|
||||
udp.minHopInterval = min .. "s"
|
||||
udp.maxHopInterval = max .. "s"
|
||||
return udp
|
||||
end
|
||||
end
|
||||
t = tonumber((t or "30"):match("^%d+"))
|
||||
t = (t and t >= 5) and t or 30
|
||||
udp.hopInterval = t .. "s"
|
||||
return udp
|
||||
end)() or nil
|
||||
},
|
||||
--[[
|
||||
tcpTProxy = (proto:find("tcp") and local_port ~= "0") and {
|
||||
listen = "0.0.0.0:" .. tonumber(local_port)
|
||||
|
||||
@ -27,10 +27,8 @@ config server_subscribe
|
||||
option auto_update_week_time '*'
|
||||
option auto_update_day_time '2'
|
||||
option auto_update_min_time '0'
|
||||
option ss_type 'ss-rust'
|
||||
option url_test_url 'https://www.google.com/generate_204'
|
||||
option user_agent 'v2rayN/9.99'
|
||||
option xray_hy2_type 'hysteria2'
|
||||
option filter_words '过期/套餐/剩余/QQ群/官网/防失联/回国'
|
||||
|
||||
config access_control
|
||||
|
||||
@ -30,54 +30,20 @@ local subscribe_url = ucic:get_first(name, 'server_subscribe', 'subscribe_url',
|
||||
local filter_words = ucic:get_first(name, 'server_subscribe', 'filter_words', '过期时间/剩余流量')
|
||||
local save_words = ucic:get_first(name, 'server_subscribe', 'save_words', '')
|
||||
local user_agent = ucic:get_first(name, 'server_subscribe', 'user_agent', 'v2rayN/9.99')
|
||||
|
||||
-- 读取 ss_type 设置
|
||||
local ss_type = ucic:get_first(name, 'server_subscribe', 'ss_type', 'ss-rust')
|
||||
-- 根据 ss_type 选择对应的程序
|
||||
local ss_program = "sslocal"
|
||||
if ss_type == "ss-rust" then
|
||||
ss_program = "sslocal"
|
||||
elseif ss_type == "ss-libev" then
|
||||
ss_program = "ss-redir"
|
||||
elseif ss_type == "v2ray" then
|
||||
ss_program = "xray"
|
||||
end
|
||||
-- 从 UCI 配置读取 xray_hy2_type 设置
|
||||
local xray_hy2_type = ucic:get_first(name, 'server_subscribe', 'xray_hy2_type', 'hysteria2')
|
||||
local xray_hy2_program = "hysteria"
|
||||
if xray_hy2_type == "v2ray" then
|
||||
xray_hy2_program = "xray" -- Hysteria2 使用 Xray
|
||||
elseif xray_hy2_type == "hysteria2" then
|
||||
xray_hy2_program = "hysteria" -- Hysteria2 使用 Hysteria
|
||||
end
|
||||
local v2_ss_exists = luci.sys.exec('type -t -p ' .. ss_program .. ' 2>/dev/null') ~= ""
|
||||
-- 初始化变量
|
||||
local v2_ss = nil
|
||||
local has_v2_ss_type = nil
|
||||
if v2_ss_exists then
|
||||
if ss_type == "v2ray" then
|
||||
-- 使用 Xray
|
||||
v2_ss = "v2ray"
|
||||
has_v2_ss_type = "shadowsocks"
|
||||
else
|
||||
-- 使用 SS (rust 或 libev)
|
||||
v2_ss = "ss"
|
||||
end
|
||||
end
|
||||
local v2_tj = luci.sys.exec('type -t -p trojan') ~= "" and "trojan" or "v2ray"
|
||||
-- 检查程序是否存在
|
||||
local program_exists = luci.sys.exec('type -t -p ' .. xray_hy2_program .. ' 2>/dev/null') ~= ""
|
||||
-- 初始化变量
|
||||
local hy2_type = nil
|
||||
local has_xray_hy2_type = nil
|
||||
if program_exists then
|
||||
-- 设置节点类型
|
||||
if xray_hy2_type == "hysteria2" then
|
||||
hy2_type = "hysteria2"
|
||||
else
|
||||
hy2_type = "v2ray" -- 当使用 Xray 时,节点类型是 "v2ray"
|
||||
has_xray_hy2_type = "hysteria2" -- 可用的协议类型是 Hysteria2
|
||||
end
|
||||
end
|
||||
local ss_type = ucic:get_first(name, 'server_subscribe', 'ss_type')
|
||||
-- 读取 xray_hy2_type 设置
|
||||
local xray_hy2_type = ucic:get_first(name, 'server_subscribe', 'xray_hy2_type')
|
||||
-- 读取 xray_tj_type 设置
|
||||
local xray_tj_type = ucic:get_first(name, 'server_subscribe', 'xray_tj_type')
|
||||
|
||||
local has_ss_rust = luci.sys.exec('type -t -p sslocal 2>/dev/null || type -t -p ssserver 2>/dev/null') ~= ""
|
||||
local has_ss_libev = luci.sys.exec('type -t -p ss-redir 2>/dev/null || type -t -p ss-local 2>/dev/null') ~= ""
|
||||
local has_hysteria = luci.sys.exec('type -t -p hysteria 2>/dev/null') ~= ""
|
||||
local has_trojan = luci.sys.exec('type -t -p trojan 2>/dev/null') ~= ""
|
||||
local has_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= ""
|
||||
|
||||
local tuic_type = luci.sys.exec('type -t -p tuic-client') ~= "" and "tuic"
|
||||
local log = function(...)
|
||||
print(os.date("%Y-%m-%d %H:%M:%S ") .. table.concat({...}, " "))
|
||||
@ -246,67 +212,66 @@ local function processData(szType, content, cfgid)
|
||||
-- log(k.."="..v)
|
||||
-- end
|
||||
|
||||
-- 如果 hy2 或 Xray 程序未安装则跳过订阅
|
||||
if not hy2_type then
|
||||
-- 自动决定模式(true=Xray, false=普通)
|
||||
local xray_hy2_mode = false -- 默认普通模式
|
||||
if xray_hy2_type == "v2ray" then
|
||||
-- Xray 模式
|
||||
if has_xray then
|
||||
xray_hy2_mode = true
|
||||
elseif has_hysteria then
|
||||
xray_hy2_mode = false -- 回退到普通 Hysteria2
|
||||
else
|
||||
xray_hy2_mode = nil
|
||||
end
|
||||
elseif xray_hy2_type == "hysteria2" then
|
||||
-- 普通 Hysteria2 模式
|
||||
if has_hysteria then
|
||||
xray_hy2_mode = false
|
||||
elseif has_xray then
|
||||
xray_hy2_mode = true -- 回退到 Xray
|
||||
else
|
||||
xray_hy2_mode = nil
|
||||
end
|
||||
else
|
||||
-- auto 或空:优先普通 Hysteria2,若不存在则使用 Xray
|
||||
if has_hysteria then
|
||||
xray_hy2_mode = false
|
||||
elseif has_xray then
|
||||
xray_hy2_mode = true -- 回退到 Xray
|
||||
else
|
||||
xray_hy2_mode = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- 如果无法确定模式,跳过该订阅
|
||||
if xray_hy2_mode == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
if xray_hy2_type == "hysteria2" then
|
||||
if params.protocol and params.protocol ~= "" then
|
||||
result.flag_transport = "1"
|
||||
result.transport_protocol = params.protocol
|
||||
else
|
||||
result.flag_transport = "1"
|
||||
result.transport_protocol = "udp"
|
||||
end
|
||||
if xray_hy2_mode then
|
||||
result.type = "v2ray"
|
||||
result.v2ray_protocol = "hysteria2"
|
||||
if params.fm and params.fm ~= "" then
|
||||
result.enable_finalmask = "1"
|
||||
result.finalmask = base64Encode(params.fm)
|
||||
end
|
||||
if params.pinSHA256 and params.pinSHA256 ~= "" then
|
||||
result.pinsha256 = params.pinSHA256
|
||||
end
|
||||
else
|
||||
result.v2ray_protocol = has_xray_hy2_type
|
||||
end
|
||||
|
||||
local raw_alias = url.fragment and UrlDecode(url.fragment) or nil
|
||||
result.raw_alias = raw_alias -- 新增
|
||||
result.alias = raw_alias -- 临时赋值(后面会被覆盖)
|
||||
result.type = hy2_type
|
||||
result.server = url.host
|
||||
result.server_port = url.port or 443
|
||||
|
||||
result.hy2_auth = url.user
|
||||
result.uplink_capacity = tonumber((params.upmbps or ""):match("^(%d+)")) or nil
|
||||
result.downlink_capacity = tonumber((params.downmbps or ""):match("^(%d+)")) or nil
|
||||
if params.mport then
|
||||
result.flag_port_hopping = "1"
|
||||
result.port_range = params.mport
|
||||
end
|
||||
if params.obfs and params.obfs ~= "none" then
|
||||
result.flag_obfs = "1"
|
||||
result.obfs_type = params.obfs
|
||||
result.salamander = params["obfs-password"] or params["obfs_password"]
|
||||
end
|
||||
if (params.security and params.security:lower() == "tls")
|
||||
or (params.sni and params.sni ~= "")
|
||||
or (params.alpn and params.alpn ~= "")
|
||||
or (xray_hy2_type == "hysteria2" and (params.pcs or params.vcn)) then
|
||||
result.tls = "1"
|
||||
if params.sni then
|
||||
result.tls_host = params.sni
|
||||
end
|
||||
if params.alpn and params.alpn ~= "" then
|
||||
local alpn = {}
|
||||
for v in params.alpn:gmatch("[^,;|%s]+") do
|
||||
table.insert(alpn, v)
|
||||
if (params.security and params.security:lower() == "tls")
|
||||
or (params.sni and params.sni ~= "")
|
||||
or (params.alpn and params.alpn ~= "")
|
||||
or (params.pcs or params.vcn) then
|
||||
result.tls = "1"
|
||||
if params.sni then
|
||||
result.tls_host = params.sni
|
||||
end
|
||||
if #alpn > 0 then
|
||||
result.tls_alpn = table.concat(alpn, ",") -- 确保为字符串
|
||||
if params.alpn and params.alpn ~= "" then
|
||||
local alpn = {}
|
||||
for v in params.alpn:gmatch("[^,;|%s]+") do
|
||||
table.insert(alpn, v)
|
||||
end
|
||||
if #alpn > 0 then
|
||||
result.tls_alpn = table.concat(alpn, ",") -- 确保为字符串
|
||||
end
|
||||
end
|
||||
end
|
||||
if xray_hy2_type ~= "hysteria2" then
|
||||
if params.pcs then
|
||||
result.tls_CertSha = params.pcs
|
||||
end
|
||||
@ -314,6 +279,55 @@ local function processData(szType, content, cfgid)
|
||||
result.tls_CertByName = params.vcn
|
||||
end
|
||||
end
|
||||
else
|
||||
result.type = "hysteria2"
|
||||
if params.protocol and params.protocol ~= "" then
|
||||
result.flag_transport = "1"
|
||||
result.transport_protocol = params.protocol
|
||||
else
|
||||
result.flag_transport = "1"
|
||||
result.transport_protocol = "udp"
|
||||
end
|
||||
if params.lazy and params.lazy ~= "" then
|
||||
result.lazy_mode = "1"
|
||||
end
|
||||
if (params.sni and params.sni ~= "") or (params.alpn and params.alpn ~= "") then
|
||||
result.tls = "1"
|
||||
if params.sni then
|
||||
result.tls_host = params.sni
|
||||
end
|
||||
if params.alpn and params.alpn ~= "" then
|
||||
local alpn = {}
|
||||
for v in params.alpn:gmatch("[^,;|%s]+") do
|
||||
table.insert(alpn, v)
|
||||
end
|
||||
if #alpn > 0 then
|
||||
result.tls_alpn = table.concat(alpn, ",") -- 确保为字符串
|
||||
end
|
||||
end
|
||||
end
|
||||
if params.pinSHA256 and params.pinSHA256 ~= "" then
|
||||
result.pinsha256 = params.pinSHA256
|
||||
end
|
||||
end
|
||||
|
||||
local raw_alias = url.fragment and UrlDecode(url.fragment) or nil
|
||||
result.raw_alias = raw_alias -- 新增
|
||||
result.alias = raw_alias -- 临时赋值(后面会被覆盖)
|
||||
result.server = url.host
|
||||
result.server_port = url.port or 443
|
||||
result.hy2_auth = url.user
|
||||
|
||||
if params.mport then
|
||||
result.flag_port_hopping = "1"
|
||||
result.port_range = params.mport
|
||||
end
|
||||
result.uplink_capacity = tonumber((params.upmbps or ""):match("^(%d+)")) or nil
|
||||
result.downlink_capacity = tonumber((params.downmbps or ""):match("^(%d+)")) or nil
|
||||
if params.obfs and params.obfs ~= "none" then
|
||||
result.flag_obfs = "1"
|
||||
result.obfs_type = params.obfs
|
||||
result.salamander = params["obfs-password"] or params["obfs_password"]
|
||||
end
|
||||
if params.allowInsecure or params.insecure then
|
||||
local insecure = params.allowInsecure or params.insecure
|
||||
@ -543,153 +557,69 @@ local function processData(szType, content, cfgid)
|
||||
result.fast_open = params.tfo
|
||||
end
|
||||
|
||||
if v2_ss ~= "v2ray" then
|
||||
local is_old_format = find_index:find("@") and not find_index:find("://.*@")
|
||||
local old_base64, host_port, userinfo, server, port, method, password
|
||||
|
||||
if is_old_format then
|
||||
-- 旧格式:base64(method:pass)@host:port
|
||||
old_base64, host_port = find_index:match("^([^@]+)@(.-)$")
|
||||
log("SS 节点旧格式解析:", old_base64)
|
||||
if not old_base64 or not host_port then
|
||||
log("SS 节点旧格式解析失败:", find_index)
|
||||
return nil
|
||||
end
|
||||
local decoded = base64Decode(UrlDecode(old_base64))
|
||||
if not decoded then
|
||||
log("SS base64 解码失败(旧格式):", old_base64)
|
||||
return nil
|
||||
end
|
||||
userinfo = decoded
|
||||
-- 自动决定模式(true=Xray, false=普通 SS)
|
||||
local xray_ss_mode = false
|
||||
if ss_type == "v2ray" then
|
||||
-- Xray 模式
|
||||
if has_xray then
|
||||
xray_ss_mode = true
|
||||
elseif has_ss_rust or has_ss_libev then
|
||||
xray_ss_mode = false -- 回退到普通 SS
|
||||
else
|
||||
-- 新格式:base64(method:pass@host:port)
|
||||
local decoded = base64Decode(UrlDecode(find_index))
|
||||
if not decoded then
|
||||
log("SS base64 解码失败(新格式):", find_index)
|
||||
return nil
|
||||
end
|
||||
userinfo, host_port = decoded:match("^(.-)@(.-)$")
|
||||
if not userinfo or not host_port then
|
||||
log("SS 解码内容缺失 @ 分隔:", decoded)
|
||||
return nil
|
||||
end
|
||||
xray_ss_mode = nil
|
||||
end
|
||||
|
||||
-- 解析加密方式和密码(允许密码包含冒号)
|
||||
local meth_pass = userinfo:find(":")
|
||||
if not meth_pass then
|
||||
log("SS 用户信息格式错误:", userinfo)
|
||||
return nil
|
||||
end
|
||||
method = userinfo:sub(1, meth_pass - 1)
|
||||
password = userinfo:sub(meth_pass + 1)
|
||||
|
||||
-- 判断密码是否经过url编码
|
||||
local function isURLEncodedPassword(pwd)
|
||||
if not pwd:find("%%[0-9A-Fa-f][0-9A-Fa-f]") then
|
||||
return false
|
||||
end
|
||||
local ok, decoded = pcall(UrlDecode, pwd)
|
||||
return ok and urlEncode(decoded) == pwd
|
||||
end
|
||||
|
||||
local decoded = UrlDecode(password)
|
||||
if isURLEncodedPassword(password) and decoded then
|
||||
password = decoded
|
||||
end
|
||||
|
||||
-- 解析服务器地址和端口(兼容 IPv6)
|
||||
if host_port:find("^%[.*%]:%d+$") then
|
||||
server, port = host_port:match("^%[(.*)%]:(%d+)$")
|
||||
elseif ss_type == "ss-rust" or ss_type == "ss-libev" then
|
||||
-- 普通 SS 模式
|
||||
local user_core = (ss_type == "ss-rust" and has_ss_rust) or (ss_type == "ss-libev" and has_ss_libev)
|
||||
if user_core then
|
||||
xray_ss_mode = false -- 否则普通 SS
|
||||
else
|
||||
server, port = host_port:match("^(.-):(%d+)$")
|
||||
end
|
||||
if not server or not port then
|
||||
log("SS 节点服务器信息格式错误:", host_port)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 如果 SS 程序未安装则跳过订阅
|
||||
if not v2_ss then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 填充 result
|
||||
result.type = v2_ss
|
||||
result.encrypt_method_ss = method
|
||||
result.password = password
|
||||
result.server = server
|
||||
result.server_port = port
|
||||
|
||||
-- 插件处理
|
||||
if params.plugin then
|
||||
local plugin_info = UrlDecode(params.plugin)
|
||||
local idx_pn = plugin_info:find(";")
|
||||
if idx_pn then
|
||||
result.plugin = plugin_info:sub(1, idx_pn - 1)
|
||||
result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
|
||||
-- 指定的核心不存在,尝试另一个 SS 核心
|
||||
local other_core = (ss_type == "ss-rust" and has_ss_libev) or (ss_type == "ss-libev" and has_ss_rust)
|
||||
if other_core then
|
||||
xray_ss_mode = false -- 使用存在的另一个 SS 核心
|
||||
elseif has_xray then
|
||||
xray_ss_mode = true -- 回退到 Xray
|
||||
else
|
||||
result.plugin = plugin_info
|
||||
result.plugin_opts = ""
|
||||
xray_ss_mode = nil
|
||||
end
|
||||
-- 部分机场下发的插件名为 simple-obfs,这里应该改为 obfs-local
|
||||
if result.plugin == "simple-obfs" then
|
||||
result.plugin = "obfs-local"
|
||||
end
|
||||
-- 如果插件不为 none,确保 enable_plugin 为 1
|
||||
if result.plugin ~= "none" and result.plugin ~= "" then
|
||||
result.enable_plugin = 1
|
||||
end
|
||||
elseif has_ss_type and has_ss_type ~= "ss-libev" then
|
||||
if params["shadow-tls"] then
|
||||
-- 特别处理 shadow-tls 作为插件
|
||||
-- log("原始 shadow-tls 参数:", params["shadow-tls"])
|
||||
local decoded_tls = base64Decode(UrlDecode(params["shadow-tls"]))
|
||||
--log("SS 节点 shadow-tls 解码后:", decoded_tls or "nil")
|
||||
if decoded_tls then
|
||||
local ok, st = pcall(jsonParse, decoded_tls)
|
||||
if ok and st then
|
||||
result.plugin = "shadow-tls"
|
||||
result.enable_plugin = 1
|
||||
local version_flag = ""
|
||||
if st.version and tonumber(st.version) then
|
||||
version_flag = string.format("v%s=1;", st.version)
|
||||
end
|
||||
|
||||
-- 合成 plugin_opts 格式:v%s=1;host=xxx;password=xxx
|
||||
result.plugin_opts = string.format("%shost=%s;passwd=%s",
|
||||
version_flag,
|
||||
st.host or "",
|
||||
st.password or "")
|
||||
else
|
||||
log("shadow-tls JSON 解析失败")
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
if params["shadow-tls"] then
|
||||
log("错误:ShadowSocks-libev 不支持使用 shadow-tls 插件")
|
||||
return nil, "ShadowSocks-libev 不支持使用 shadow-tls 插件"
|
||||
end
|
||||
end
|
||||
|
||||
-- 检查加密方法是否受支持
|
||||
if not checkTabValue(encrypt_methods_ss)[method] then
|
||||
-- 1202 年了还不支持 SS AEAD 的屑机场
|
||||
-- log("不支持的SS加密方法:", method)
|
||||
result.server = nil
|
||||
end
|
||||
else
|
||||
-- ss_type 为空或 auto:根据链接中是否有 type 参数决定
|
||||
local has_type = params.type and params.type ~= ""
|
||||
if has_type then
|
||||
-- 有 type 参数,优先 Xray
|
||||
if has_xray then
|
||||
xray_ss_mode = true
|
||||
elseif has_ss_rust or has_ss_libev then
|
||||
xray_ss_mode = false -- 回退到普通 SS
|
||||
else
|
||||
xray_ss_mode = nil
|
||||
end
|
||||
else
|
||||
-- 无 type 参数,优先普通 SS
|
||||
if has_ss_rust or has_ss_libev then
|
||||
-- 普通 SS 模式
|
||||
xray_ss_mode = false
|
||||
elseif has_xray then
|
||||
xray_ss_mode = true -- 回退到 Xray
|
||||
else
|
||||
xray_ss_mode = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 如果最终无可用核心,跳过该订阅
|
||||
if xray_ss_mode == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
if xray_ss_mode then
|
||||
local url = URL.parse("http://" .. info)
|
||||
local params = url.query
|
||||
|
||||
-- 如果 Xray 程序未安装则跳过订阅
|
||||
if not v2_ss then
|
||||
return nil
|
||||
end
|
||||
|
||||
result.type = v2_ss
|
||||
result.v2ray_protocol = has_v2_ss_type
|
||||
result.type = "v2ray"
|
||||
result.v2ray_protocol = "shadowsocks"
|
||||
result.server = url.host
|
||||
result.server_port = url.port
|
||||
|
||||
@ -816,6 +746,143 @@ local function processData(szType, content, cfgid)
|
||||
result.tcp_path = params.path and UrlDecode(params.path) or nil
|
||||
end
|
||||
end
|
||||
else
|
||||
local is_old_format = find_index:find("@") and not find_index:find("://.*@")
|
||||
local old_base64, host_port, userinfo, server, port, method, password
|
||||
|
||||
if is_old_format then
|
||||
-- 旧格式:base64(method:pass)@host:port
|
||||
old_base64, host_port = find_index:match("^([^@]+)@(.-)$")
|
||||
log("SS 节点旧格式解析:", old_base64)
|
||||
if not old_base64 or not host_port then
|
||||
log("SS 节点旧格式解析失败:", find_index)
|
||||
return nil
|
||||
end
|
||||
local decoded = base64Decode(UrlDecode(old_base64))
|
||||
if not decoded then
|
||||
log("SS base64 解码失败(旧格式):", old_base64)
|
||||
return nil
|
||||
end
|
||||
userinfo = decoded
|
||||
else
|
||||
-- 新格式:base64(method:pass@host:port)
|
||||
local decoded = base64Decode(UrlDecode(find_index))
|
||||
if not decoded then
|
||||
log("SS base64 解码失败(新格式):", find_index)
|
||||
return nil
|
||||
end
|
||||
userinfo, host_port = decoded:match("^(.-)@(.-)$")
|
||||
if not userinfo or not host_port then
|
||||
log("SS 解码内容缺失 @ 分隔:", decoded)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- 解析加密方式和密码(允许密码包含冒号)
|
||||
local meth_pass = userinfo:find(":")
|
||||
if not meth_pass then
|
||||
log("SS 用户信息格式错误:", userinfo)
|
||||
return nil
|
||||
end
|
||||
method = userinfo:sub(1, meth_pass - 1)
|
||||
password = userinfo:sub(meth_pass + 1)
|
||||
|
||||
-- 判断密码是否经过url编码
|
||||
local function isURLEncodedPassword(pwd)
|
||||
if not pwd:find("%%[0-9A-Fa-f][0-9A-Fa-f]") then
|
||||
return false
|
||||
end
|
||||
local ok, decoded = pcall(UrlDecode, pwd)
|
||||
return ok and urlEncode(decoded) == pwd
|
||||
end
|
||||
|
||||
local decoded = UrlDecode(password)
|
||||
if isURLEncodedPassword(password) and decoded then
|
||||
password = decoded
|
||||
end
|
||||
|
||||
-- 解析服务器地址和端口(兼容 IPv6)
|
||||
if host_port:find("^%[.*%]:%d+$") then
|
||||
server, port = host_port:match("^%[(.*)%]:(%d+)$")
|
||||
else
|
||||
server, port = host_port:match("^(.-):(%d+)$")
|
||||
end
|
||||
if not server or not port then
|
||||
log("SS 节点服务器信息格式错误:", host_port)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 填充 result
|
||||
local xray_ss_type
|
||||
if ss_type == "ss-rust" or ss_type == "ss-libev" then
|
||||
xray_ss_type = ss_type
|
||||
else
|
||||
xray_ss_type = has_ss_rust and "ss-rust" or "ss-libev"
|
||||
end
|
||||
result.type = xray_ss_type
|
||||
result.encrypt_method_ss = method
|
||||
result.password = password
|
||||
result.server = server
|
||||
result.server_port = port
|
||||
|
||||
-- 插件处理
|
||||
if params.plugin then
|
||||
local plugin_info = UrlDecode(params.plugin)
|
||||
local idx_pn = plugin_info:find(";")
|
||||
if idx_pn then
|
||||
result.plugin = plugin_info:sub(1, idx_pn - 1)
|
||||
result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
|
||||
else
|
||||
result.plugin = plugin_info
|
||||
result.plugin_opts = ""
|
||||
end
|
||||
-- 部分机场下发的插件名为 simple-obfs,这里应该改为 obfs-local
|
||||
if result.plugin == "simple-obfs" then
|
||||
result.plugin = "obfs-local"
|
||||
end
|
||||
-- 如果插件不为 none,确保 enable_plugin 为 1
|
||||
if result.plugin ~= "none" and result.plugin ~= "" then
|
||||
result.enable_plugin = 1
|
||||
end
|
||||
elseif has_ss_type and has_ss_type ~= "ss-libev" then
|
||||
if params["shadow-tls"] then
|
||||
-- 特别处理 shadow-tls 作为插件
|
||||
-- log("原始 shadow-tls 参数:", params["shadow-tls"])
|
||||
local decoded_tls = base64Decode(UrlDecode(params["shadow-tls"]))
|
||||
--log("SS 节点 shadow-tls 解码后:", decoded_tls or "nil")
|
||||
if decoded_tls then
|
||||
local ok, st = pcall(jsonParse, decoded_tls)
|
||||
if ok and st then
|
||||
result.plugin = "shadow-tls"
|
||||
result.enable_plugin = 1
|
||||
local version_flag = ""
|
||||
if st.version and tonumber(st.version) then
|
||||
version_flag = string.format("v%s=1;", st.version)
|
||||
end
|
||||
|
||||
-- 合成 plugin_opts 格式:v%s=1;host=xxx;password=xxx
|
||||
result.plugin_opts = string.format("%shost=%s;passwd=%s",
|
||||
version_flag,
|
||||
st.host or "",
|
||||
st.password or "")
|
||||
else
|
||||
log("shadow-tls JSON 解析失败")
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
if params["shadow-tls"] then
|
||||
log("错误:ShadowSocks-libev 不支持使用 shadow-tls 插件")
|
||||
return nil, "ShadowSocks-libev 不支持使用 shadow-tls 插件"
|
||||
end
|
||||
end
|
||||
|
||||
-- 检查加密方法是否受支持
|
||||
if not checkTabValue(encrypt_methods_ss)[method] then
|
||||
-- 1202 年了还不支持 SS AEAD 的屑机场
|
||||
-- log("不支持的SS加密方法:", method)
|
||||
result.server = nil
|
||||
end
|
||||
end
|
||||
elseif szType == "sip008" then
|
||||
result.type = v2_ss
|
||||
@ -936,102 +1003,143 @@ local function processData(szType, content, cfgid)
|
||||
result.server_port = port
|
||||
end
|
||||
|
||||
-- 如果 Tojan 程序未安装则跳过订阅
|
||||
if not v2_tj or v2_tj == "" then
|
||||
-- 自动决定模式(true=Xray, false=普通 Trojan)
|
||||
local xray_tj_mode = false
|
||||
if xray_tj_type == "v2ray" then
|
||||
-- Xray 模式
|
||||
if has_xray then
|
||||
xray_tj_mode = true
|
||||
elseif has_trojan then
|
||||
xray_tj_mode = false -- 回退到普通 Trojan
|
||||
else
|
||||
xray_tj_mode = nil -- 两类核心均不存在,停止订阅
|
||||
end
|
||||
elseif xray_tj_type == "trojan" then
|
||||
-- 普通 Trojan 模式
|
||||
if has_trojan then
|
||||
xray_tj_mode = false
|
||||
elseif has_xray then
|
||||
xray_tj_mode = true -- 回退到 Xray
|
||||
else
|
||||
xray_tj_mode = nil -- 两类核心均不存在,停止订阅
|
||||
end
|
||||
else
|
||||
-- 全局配置为空或 auto,根据链接中是否有 type 参数决定
|
||||
local has_type = params.type and params.type ~= ""
|
||||
-- 有 type 参数,优先 Xray
|
||||
if has_type then
|
||||
if has_xray then
|
||||
xray_tj_mode = true -- 有 type 参数使用 Xray
|
||||
elseif has_trojan then
|
||||
xray_tj_mode = false -- 否则普通 Trojan
|
||||
else
|
||||
xray_tj_mode = nil -- 两类核心均不存在,停止订阅
|
||||
end
|
||||
else
|
||||
-- 无 type 参数,优先普通 Trojan
|
||||
if has_trojan then
|
||||
xray_tj_mode = false -- 普通 Trojan
|
||||
elseif has_xray then
|
||||
xray_tj_mode = true -- 否则使用 Xray
|
||||
else
|
||||
xray_tj_mode = nil -- 两类核心均不存在,停止订阅
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 如果最终无可用核心,跳过该订阅
|
||||
if xray_tj_mode == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
if params.type and params.type ~= "" then
|
||||
v2_tj = "v2ray"
|
||||
result.type = v2_tj
|
||||
if xray_tj_mode then
|
||||
result.type = "v2ray"
|
||||
result.v2ray_protocol = "trojan"
|
||||
if v2_tj ~= "trojan" then
|
||||
if params.fp then
|
||||
-- 处理 fingerprint 参数
|
||||
result.fingerprint = params.fp
|
||||
end
|
||||
-- 处理 ech 参数
|
||||
if params.ech and params.ech ~= "" then
|
||||
result.enable_ech = "1"
|
||||
result.ech_config = params.ech
|
||||
end
|
||||
-- 检查 finalmaskg 参数是否存在且非空
|
||||
if params.fm and params.fm ~= "" then
|
||||
result.enable_finalmask = "1"
|
||||
result.finalmaskg = base64Encode(params.fm)
|
||||
end
|
||||
-- 处理传输协议
|
||||
result.transport = params.type or "raw" -- 默认传输协议为 raw
|
||||
if result.transport == "tcp" then
|
||||
result.transport = "raw"
|
||||
end
|
||||
if result.transport == "splithttp" then
|
||||
result.transport = "xhttp"
|
||||
end
|
||||
if params.pcs and params.pcs ~= "" then
|
||||
result.tls_CertSha = params.pcs
|
||||
end
|
||||
if params.vcn and params.vcn ~= "" then
|
||||
result.tls_CertByName = params.vcn
|
||||
end
|
||||
if result.transport == "ws" then
|
||||
result.ws_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
|
||||
result.ws_path = params.path and UrlDecode(params.path) or "/"
|
||||
elseif result.transport == "httpupgrade" then
|
||||
result.httpupgrade_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
|
||||
result.httpupgrade_path = params.path and UrlDecode(params.path) or "/"
|
||||
elseif result.transport == "xhttp" or result.transport == "splithttp" then
|
||||
result.xhttp_mode = params.mode or "auto"
|
||||
result.xhttp_host = params.host and UrlDecode(params.host) or nil
|
||||
result.xhttp_path = params.path and UrlDecode(params.path) or "/"
|
||||
-- 检查 extra 参数是否存在且非空
|
||||
if params.extra and params.extra ~= "" then
|
||||
result.enable_xhttp_extra = "1"
|
||||
result.xhttp_extra = base64Encode(params.extra)
|
||||
end
|
||||
-- 尝试解析 JSON 数据
|
||||
local success, Data = pcall(jsonParse, params.extra or "")
|
||||
if success and type(Data) == "table" then
|
||||
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
|
||||
or (Data.downloadSettings and Data.downloadSettings.address)
|
||||
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "")
|
||||
else
|
||||
-- 如果解析失败,清空下载地址
|
||||
result.download_address = nil
|
||||
end
|
||||
elseif result.transport == "http" or result.transport == "h2" then
|
||||
result.transport = "h2"
|
||||
result.h2_host = params.host and UrlDecode(params.host) or nil
|
||||
result.h2_path = params.path and UrlDecode(params.path) or nil
|
||||
elseif result.transport == "kcp" then
|
||||
result.kcp_guise = params.headerType or "none"
|
||||
if params.headerType and params.headerType == "dns" then
|
||||
result.kcp_domain = params.host or ""
|
||||
end
|
||||
result.seed = params.seed
|
||||
result.mtu = 1350
|
||||
result.tti = 50
|
||||
result.uplink_capacity = 5
|
||||
result.downlink_capacity = 20
|
||||
result.read_buffer_size = 2
|
||||
result.write_buffer_size = 2
|
||||
elseif result.transport == "quic" then
|
||||
result.quic_guise = params.headerType or "none"
|
||||
result.quic_security = params.quicSecurity or "none"
|
||||
result.quic_key = params.key
|
||||
elseif result.transport == "grpc" then
|
||||
result.serviceName = params.serviceName
|
||||
result.grpc_mode = params.mode or "gun"
|
||||
elseif result.transport == "tcp" or result.transport == "raw" then
|
||||
result.tcp_guise = params.headerType and params.headerType ~= "" and params.headerType or "none"
|
||||
if result.tcp_guise == "http" then
|
||||
result.tcp_host = params.host and UrlDecode(params.host) or nil
|
||||
result.tcp_path = params.path and UrlDecode(params.path) or nil
|
||||
end
|
||||
end
|
||||
else
|
||||
result.type = v2_tj
|
||||
if params.fp then
|
||||
-- 处理 fingerprint 参数
|
||||
result.fingerprint = params.fp
|
||||
end
|
||||
-- 处理 ech 参数
|
||||
if params.ech and params.ech ~= "" then
|
||||
result.enable_ech = "1"
|
||||
result.ech_config = params.ech
|
||||
end
|
||||
-- 检查 finalmaskg 参数是否存在且非空
|
||||
if params.fm and params.fm ~= "" then
|
||||
result.enable_finalmask = "1"
|
||||
result.finalmaskg = base64Encode(params.fm)
|
||||
end
|
||||
-- 处理传输协议
|
||||
result.transport = params.type or "raw" -- 默认传输协议为 raw
|
||||
if result.transport == "tcp" then
|
||||
result.transport = "raw"
|
||||
end
|
||||
if result.transport == "splithttp" then
|
||||
result.transport = "xhttp"
|
||||
end
|
||||
if params.pcs and params.pcs ~= "" then
|
||||
result.tls_CertSha = params.pcs
|
||||
end
|
||||
if params.vcn and params.vcn ~= "" then
|
||||
result.tls_CertByName = params.vcn
|
||||
end
|
||||
if result.transport == "ws" then
|
||||
result.ws_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
|
||||
result.ws_path = params.path and UrlDecode(params.path) or "/"
|
||||
elseif result.transport == "httpupgrade" then
|
||||
result.httpupgrade_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
|
||||
result.httpupgrade_path = params.path and UrlDecode(params.path) or "/"
|
||||
elseif result.transport == "xhttp" or result.transport == "splithttp" then
|
||||
result.xhttp_mode = params.mode or "auto"
|
||||
result.xhttp_host = params.host and UrlDecode(params.host) or nil
|
||||
result.xhttp_path = params.path and UrlDecode(params.path) or "/"
|
||||
-- 检查 extra 参数是否存在且非空
|
||||
if params.extra and params.extra ~= "" then
|
||||
result.enable_xhttp_extra = "1"
|
||||
result.xhttp_extra = base64Encode(params.extra)
|
||||
end
|
||||
-- 尝试解析 JSON 数据
|
||||
local success, Data = pcall(jsonParse, params.extra or "")
|
||||
if success and type(Data) == "table" then
|
||||
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
|
||||
or (Data.downloadSettings and Data.downloadSettings.address)
|
||||
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "")
|
||||
else
|
||||
-- 如果解析失败,清空下载地址
|
||||
result.download_address = nil
|
||||
end
|
||||
elseif result.transport == "http" or result.transport == "h2" then
|
||||
result.transport = "h2"
|
||||
result.h2_host = params.host and UrlDecode(params.host) or nil
|
||||
result.h2_path = params.path and UrlDecode(params.path) or nil
|
||||
elseif result.transport == "kcp" then
|
||||
result.kcp_guise = params.headerType or "none"
|
||||
if params.headerType and params.headerType == "dns" then
|
||||
result.kcp_domain = params.host or ""
|
||||
end
|
||||
result.seed = params.seed
|
||||
result.mtu = 1350
|
||||
result.tti = 50
|
||||
result.uplink_capacity = 5
|
||||
result.downlink_capacity = 20
|
||||
result.read_buffer_size = 2
|
||||
result.write_buffer_size = 2
|
||||
elseif result.transport == "quic" then
|
||||
result.quic_guise = params.headerType or "none"
|
||||
result.quic_security = params.quicSecurity or "none"
|
||||
result.quic_key = params.key
|
||||
elseif result.transport == "grpc" then
|
||||
result.serviceName = params.serviceName
|
||||
result.grpc_mode = params.mode or "gun"
|
||||
elseif result.transport == "tcp" or result.transport == "raw" then
|
||||
result.tcp_guise = params.headerType and params.headerType ~= "" and params.headerType or "none"
|
||||
if result.tcp_guise == "http" then
|
||||
result.tcp_host = params.host and UrlDecode(params.host) or nil
|
||||
result.tcp_path = params.path and UrlDecode(params.path) or nil
|
||||
end
|
||||
end
|
||||
else
|
||||
result.type = "trojan"
|
||||
end
|
||||
elseif szType == "vless" then
|
||||
local url = URL.parse("http://" .. content)
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=momo
|
||||
PKG_VERSION:=2026.03.29
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=2026.04.09
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_LICENSE:=GPL-3.0+
|
||||
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com>
|
||||
@ -35,6 +35,7 @@ define Package/momo/install
|
||||
$(INSTALL_DIR) $(1)/etc/momo/run
|
||||
|
||||
$(INSTALL_BIN) $(CURDIR)/files/ucode/include.uc $(1)/etc/momo/ucode/include.uc
|
||||
$(INSTALL_BIN) $(CURDIR)/files/ucode/mixin.uc $(1)/etc/momo/ucode/mixin.uc
|
||||
$(INSTALL_BIN) $(CURDIR)/files/ucode/hijack.ut $(1)/etc/momo/ucode/hijack.ut
|
||||
|
||||
$(INSTALL_BIN) $(CURDIR)/files/scripts/include.sh $(1)/etc/momo/scripts/include.sh
|
||||
@ -52,6 +53,7 @@ define Package/momo/install
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) $(CURDIR)/files/uci-defaults/firewall.sh $(1)/etc/uci-defaults/99_firewall_momo
|
||||
$(INSTALL_BIN) $(CURDIR)/files/uci-defaults/init.sh $(1)/etc/uci-defaults/99_init_momo
|
||||
$(INSTALL_BIN) $(CURDIR)/files/uci-defaults/migrate.sh $(1)/etc/uci-defaults/99_migrate_momo
|
||||
|
||||
$(INSTALL_DIR) $(1)/lib/upgrade/keep.d
|
||||
|
||||
@ -4,17 +4,10 @@ config config 'config'
|
||||
option 'profile' 'file:example.json'
|
||||
option 'start_delay' '0'
|
||||
option 'scheduled_restart' '0'
|
||||
option 'cron_expression' '0 3 * * *'
|
||||
option 'scheduled_restart_cron' '0 3 * * *'
|
||||
option 'test_profile' '1'
|
||||
option 'core_only' '0'
|
||||
|
||||
config log 'log'
|
||||
option 'log_cleanup_enabled' '0'
|
||||
option 'log_cleanup_cron_expression' '0 4 * * *'
|
||||
option 'log_cleanup_size_enabled' '0'
|
||||
option 'log_cleanup_size_check_cron_expression' '*/30 * * * *'
|
||||
option 'log_cleanup_size_mb' '50'
|
||||
|
||||
config procd 'procd'
|
||||
option 'fast_reload' '0'
|
||||
|
||||
@ -25,6 +18,18 @@ config core 'core'
|
||||
option 'dns_inbound_tag' 'dns-in'
|
||||
option 'fake_ip_dns_server_tag' 'fake-ip-dns-server'
|
||||
|
||||
config mixin 'mixin'
|
||||
option 'log_disabled' '0'
|
||||
option 'log_level' 'info'
|
||||
option 'log_timestamp' '1'
|
||||
option 'dns_independent_cache' '1'
|
||||
option 'dns_reverse_mapping' '1'
|
||||
option 'cache_enabled' '1'
|
||||
option 'cache_store_fakeip' '1'
|
||||
option 'cache_store_rdrc' '1'
|
||||
option 'external_control_ui_path' 'ui'
|
||||
option 'external_control_ui_download_url' 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip'
|
||||
|
||||
config proxy 'proxy'
|
||||
option 'enabled' '1'
|
||||
option 'ipv4_dns_hijack' '1'
|
||||
@ -115,4 +120,10 @@ config routing 'routing'
|
||||
option 'cgroup_name' 'momo'
|
||||
option 'dummy_device' 'momo-dummy'
|
||||
|
||||
config log 'log'
|
||||
option 'scheduled_clear' '1'
|
||||
option 'scheduled_clear_cron' '*/5 * * * *'
|
||||
option 'scheduled_clear_size_limit' '1'
|
||||
option 'scheduled_clear_size_limit_unit' 'MB'
|
||||
|
||||
config placeholder 'placeholder'
|
||||
|
||||
@ -9,8 +9,7 @@ PROG="/usr/bin/sing-box"
|
||||
. "$IPKG_INSTROOT/etc/momo/scripts/include.sh"
|
||||
|
||||
extra_command 'update_subscription' 'Update subscription by section id'
|
||||
extra_command 'cleanup_logs' 'Cleanup logs immediately'
|
||||
extra_command 'check_log_cleanup' 'Cleanup logs if size threshold is reached'
|
||||
extra_command 'clear_logs' 'Clear logs if size exceeds the limit'
|
||||
|
||||
boot_func() {
|
||||
# prepare files
|
||||
@ -30,7 +29,7 @@ boot_func() {
|
||||
}
|
||||
|
||||
boot() {
|
||||
boot_func &
|
||||
boot_func < /dev/null > /dev/null 2>&1 &
|
||||
}
|
||||
|
||||
start_service() {
|
||||
@ -64,11 +63,6 @@ start_service() {
|
||||
config_get profile "config" "profile"
|
||||
config_get_bool test_profile "config" "test_profile" 0
|
||||
config_get_bool core_only "config" "core_only" 0
|
||||
config_get_bool log_cleanup_enabled "log" "log_cleanup_enabled" 0
|
||||
config_get log_cleanup_cron_expression "log" "log_cleanup_cron_expression"
|
||||
config_get_bool log_cleanup_size_enabled "log" "log_cleanup_size_enabled" 0
|
||||
config_get log_cleanup_size_check_cron_expression "log" "log_cleanup_size_check_cron_expression"
|
||||
config_get log_cleanup_size_mb "log" "log_cleanup_size_mb" 0
|
||||
## procd config
|
||||
### general
|
||||
local fast_reload
|
||||
@ -97,6 +91,10 @@ start_service() {
|
||||
config_get_bool ipv6_dns_hijack "proxy" "ipv6_dns_hijack" 0
|
||||
config_get tcp_mode "proxy" "tcp_mode"
|
||||
config_get udp_mode "proxy" "udp_mode"
|
||||
## log config
|
||||
local log_scheduled_clear log_scheduled_clear_cron
|
||||
config_get_bool log_scheduled_clear "log" "scheduled_clear" 0
|
||||
config_get log_scheduled_clear_cron "log" "scheduled_clear_cron"
|
||||
# get profile
|
||||
local profile_type; profile_type=$(echo "$profile" | cut -d ':' -f 1)
|
||||
local profile_id; profile_id=$(echo "$profile" | cut -d ':' -f 2)
|
||||
@ -131,6 +129,11 @@ start_service() {
|
||||
log "App" "Exit."
|
||||
return
|
||||
fi
|
||||
# mixin
|
||||
if [ "$core_only" = 0 ]; then
|
||||
log "Mixin" "Mixin config."
|
||||
ucode -S "$MIXIN_UC"
|
||||
fi
|
||||
# check profile
|
||||
if [ "$core_only" = 0 ] && [ "$proxy_enabled" = 1 ]; then
|
||||
log "Profile" "Checking..."
|
||||
@ -202,23 +205,18 @@ start_service() {
|
||||
|
||||
procd_close_instance
|
||||
# cron
|
||||
local cron_reload; cron_reload=0
|
||||
if [ "$scheduled_restart" = 1 ] && [ -n "$cron_expression" ]; then
|
||||
local reload_cron; reload_cron=0
|
||||
if [ "$scheduled_restart" = 1 ] && [ -n "$scheduled_restart_cron" ]; then
|
||||
log "App" "Set scheduled restart."
|
||||
echo "$cron_expression /etc/init.d/momo restart #momo_scheduled_restart" >> "/etc/crontabs/root"
|
||||
cron_reload=1
|
||||
echo "$scheduled_restart_cron /etc/init.d/momo restart #momo scheduled restart" >> "/etc/crontabs/root"
|
||||
reload_cron=1
|
||||
fi
|
||||
if [ "$log_cleanup_enabled" = 1 ] && [ -n "$log_cleanup_cron_expression" ]; then
|
||||
log "Log" "Set scheduled log cleanup."
|
||||
echo "$log_cleanup_cron_expression /etc/init.d/momo cleanup_logs > /dev/null 2>&1 #momo_log_cleanup_time" >> "/etc/crontabs/root"
|
||||
cron_reload=1
|
||||
if [ "$log_scheduled_clear" = 1 ] && [ -n "$log_scheduled_clear_cron" ]; then
|
||||
log "App" "Set log scheduled clear."
|
||||
echo "$log_scheduled_clear_cron /etc/init.d/momo clear_logs #momo log scheduled clear" >> "/etc/crontabs/root"
|
||||
reload_cron=1
|
||||
fi
|
||||
if [ "$log_cleanup_size_enabled" = 1 ] && [ -n "$log_cleanup_size_check_cron_expression" ] && [ "${log_cleanup_size_mb:-0}" -gt 0 ]; then
|
||||
log "Log" "Set size-based log cleanup check."
|
||||
echo "$log_cleanup_size_check_cron_expression /etc/init.d/momo check_log_cleanup > /dev/null 2>&1 #momo_log_cleanup_size" >> "/etc/crontabs/root"
|
||||
cron_reload=1
|
||||
fi
|
||||
if [ "$cron_reload" = 1 ]; then
|
||||
if [ "$reload_cron" = 1 ]; then
|
||||
/etc/init.d/cron restart
|
||||
fi
|
||||
# set started flag
|
||||
@ -379,8 +377,6 @@ service_triggers() {
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
# clear log
|
||||
clear_log
|
||||
# load config
|
||||
config_load momo
|
||||
# get config
|
||||
@ -427,44 +423,39 @@ cleanup() {
|
||||
/etc/init.d/cron restart
|
||||
}
|
||||
|
||||
cleanup_logs() {
|
||||
prepare_files
|
||||
run_log_cleanup "scheduled trigger"
|
||||
}
|
||||
|
||||
check_log_cleanup() {
|
||||
prepare_files
|
||||
clear_logs() {
|
||||
# load config
|
||||
config_load momo
|
||||
|
||||
local log_cleanup_size_enabled log_cleanup_size_mb total_size_bytes threshold_size_bytes
|
||||
config_get_bool log_cleanup_size_enabled "log" "log_cleanup_size_enabled" 0
|
||||
config_get log_cleanup_size_mb "log" "log_cleanup_size_mb" 0
|
||||
|
||||
if [ "$log_cleanup_size_enabled" != 1 ] || [ "${log_cleanup_size_mb:-0}" -le 0 ]; then
|
||||
return
|
||||
# get config
|
||||
## log config
|
||||
local log_scheduled_clear_size_limit log_scheduled_clear_size_limit_unit
|
||||
config_get log_scheduled_clear_size_limit "log" "scheduled_clear_size_limit" 1
|
||||
config_get log_scheduled_clear_size_limit_unit "log" "scheduled_clear_size_limit_unit" "MB"
|
||||
# prepare config
|
||||
local log_scheduled_clear_size_limit_in_bytes; log_scheduled_clear_size_limit_in_bytes=0
|
||||
case "$log_scheduled_clear_size_limit_unit" in
|
||||
B)
|
||||
log_scheduled_clear_size_limit_in_bytes=$((log_scheduled_clear_size_limit))
|
||||
;;
|
||||
KB)
|
||||
log_scheduled_clear_size_limit_in_bytes=$((log_scheduled_clear_size_limit * 1024))
|
||||
;;
|
||||
MB)
|
||||
log_scheduled_clear_size_limit_in_bytes=$((log_scheduled_clear_size_limit * 1024 * 1024))
|
||||
;;
|
||||
GB)
|
||||
log_scheduled_clear_size_limit_in_bytes=$((log_scheduled_clear_size_limit * 1024 * 1024 * 1024))
|
||||
;;
|
||||
esac
|
||||
# clear logs
|
||||
if [ -f "$APP_LOG_PATH" ] && [ "$(wc -c < "$APP_LOG_PATH")" -ge "$log_scheduled_clear_size_limit_in_bytes" ]; then
|
||||
echo -n > "$APP_LOG_PATH"
|
||||
log "Log" "App log is cleared due to scheduled clear and exceeding size limits."
|
||||
fi
|
||||
|
||||
total_size_bytes=$(get_total_log_size)
|
||||
threshold_size_bytes=$((log_cleanup_size_mb * 1024 * 1024))
|
||||
|
||||
if [ "$total_size_bytes" -lt "$threshold_size_bytes" ]; then
|
||||
return
|
||||
if [ -f "$CORE_LOG_PATH" ] && [ "$(wc -c < "$CORE_LOG_PATH")" -ge "$log_scheduled_clear_size_limit_in_bytes" ]; then
|
||||
echo -n > "$CORE_LOG_PATH"
|
||||
log "Log" "Core log is cleared due to scheduled clear and exceeding size limits."
|
||||
fi
|
||||
|
||||
run_log_cleanup "size threshold ${log_cleanup_size_mb} MB reached" "$total_size_bytes"
|
||||
}
|
||||
|
||||
run_log_cleanup() {
|
||||
local reason previous_size
|
||||
reason="$1"
|
||||
previous_size="$2"
|
||||
|
||||
if [ -z "$previous_size" ]; then
|
||||
previous_size=$(get_total_log_size)
|
||||
fi
|
||||
|
||||
clear_log
|
||||
log "Log" "Cleanup executed: ${reason}. Previous total size: $(format_filesize "$previous_size")."
|
||||
}
|
||||
|
||||
update_subscription() {
|
||||
|
||||
@ -118,39 +118,11 @@ prepare_files() {
|
||||
if [ ! -f "$CORE_LOG_PATH" ]; then
|
||||
touch "$CORE_LOG_PATH"
|
||||
fi
|
||||
if [ ! -f "$DEBUG_LOG_PATH" ]; then
|
||||
touch "$DEBUG_LOG_PATH"
|
||||
fi
|
||||
if [ ! -d "$TEMP_DIR" ]; then
|
||||
mkdir -p "$TEMP_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
clear_log() {
|
||||
echo -n > "$APP_LOG_PATH"
|
||||
echo -n > "$CORE_LOG_PATH"
|
||||
echo -n > "$DEBUG_LOG_PATH"
|
||||
}
|
||||
|
||||
get_filesize() {
|
||||
local path; path="$1"
|
||||
if [ ! -f "$path" ]; then
|
||||
echo 0
|
||||
return
|
||||
fi
|
||||
wc -c < "$path" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
get_total_log_size() {
|
||||
local total_size path path_size
|
||||
total_size=0
|
||||
for path in "$APP_LOG_PATH" "$CORE_LOG_PATH" "$DEBUG_LOG_PATH"; do
|
||||
path_size=$(get_filesize "$path")
|
||||
total_size=$((total_size + path_size))
|
||||
done
|
||||
echo "$total_size"
|
||||
}
|
||||
|
||||
log() {
|
||||
echo "[$(date "+%Y-%m-%d %H:%M:%S")] [$1] $2" >> "$APP_LOG_PATH"
|
||||
}
|
||||
|
||||
21
momo/files/uci-defaults/init.sh
Normal file
21
momo/files/uci-defaults/init.sh
Normal file
@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
. "$IPKG_INSTROOT/etc/momo/scripts/include.sh"
|
||||
|
||||
# check momo.config.init
|
||||
init=$(uci -q get momo.config.init); [ -z "$init" ] && return
|
||||
|
||||
# generate random string for api secret and authentication password
|
||||
random=$(awk 'BEGIN{srand(); printf "%06d", int(rand() * 1000000)}')
|
||||
|
||||
# set momo.mixin.api_secret
|
||||
uci set momo.mixin.external_control_api_secret="$random"
|
||||
|
||||
# remove momo.config.init
|
||||
uci del momo.config.init
|
||||
|
||||
# commit
|
||||
uci commit momo
|
||||
|
||||
# exit with 0
|
||||
exit 0
|
||||
@ -27,42 +27,29 @@ dummy_device=$(uci -q get momo.routing.dummy_device); [ -z "$dummy_device" ] &&
|
||||
|
||||
# since v1.1.2
|
||||
|
||||
section_log=$(uci -q get momo.log); [ -z "$section_log" ] && uci set momo.log=log
|
||||
config_scheduled_restart_cron=$(uci -q get momo.config.scheduled_restart_cron); [ -z "$config_scheduled_restart_cron" ] && uci rename momo.config.cron_expression="scheduled_restart_cron"
|
||||
|
||||
log_cleanup_enabled=$(uci -q get momo.log.log_cleanup_enabled)
|
||||
[ -z "$log_cleanup_enabled" ] && {
|
||||
log_cleanup_enabled=$(uci -q get momo.config.log_cleanup_enabled)
|
||||
[ -n "$log_cleanup_enabled" ] && uci set momo.log.log_cleanup_enabled=$log_cleanup_enabled || uci set momo.log.log_cleanup_enabled=0
|
||||
section_log=$(uci -q get momo.log); [ -z "$section_log" ] && {
|
||||
uci set momo.log=log
|
||||
uci set momo.log.scheduled_clear=1
|
||||
uci set momo.log.scheduled_clear_cron="*/5 * * * *"
|
||||
uci set momo.log.scheduled_clear_size_limit=1
|
||||
uci set momo.log.scheduled_clear_size_limit_unit=MB
|
||||
}
|
||||
uci -q del momo.config.log_cleanup_enabled
|
||||
|
||||
log_cleanup_cron_expression=$(uci -q get momo.log.log_cleanup_cron_expression)
|
||||
[ -z "$log_cleanup_cron_expression" ] && {
|
||||
log_cleanup_cron_expression=$(uci -q get momo.config.log_cleanup_cron_expression)
|
||||
[ -n "$log_cleanup_cron_expression" ] && uci set momo.log.log_cleanup_cron_expression="$log_cleanup_cron_expression" || uci set momo.log.log_cleanup_cron_expression='0 4 * * *'
|
||||
section_mixin=$(uci -q get momo.mixin); [ -z "$section_mixin" ] && {
|
||||
uci set momo.mixin=mixin
|
||||
uci set momo.mixin.log_disabled='0'
|
||||
uci set momo.mixin.log_level='info'
|
||||
uci set momo.mixin.log_timestamp='1'
|
||||
uci set momo.mixin.dns_independent_cache='1'
|
||||
uci set momo.mixin.dns_reverse_mapping='1'
|
||||
uci set momo.mixin.cache_enabled='1'
|
||||
uci set momo.mixin.cache_store_fakeip='1'
|
||||
uci set momo.mixin.cache_store_rdrc='1'
|
||||
uci set momo.mixin.external_control_ui_path='ui'
|
||||
uci set momo.mixin.external_control_ui_download_url='https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip'
|
||||
}
|
||||
uci -q del momo.config.log_cleanup_cron_expression
|
||||
|
||||
log_cleanup_size_enabled=$(uci -q get momo.log.log_cleanup_size_enabled)
|
||||
[ -z "$log_cleanup_size_enabled" ] && {
|
||||
log_cleanup_size_enabled=$(uci -q get momo.config.log_cleanup_size_enabled)
|
||||
[ -n "$log_cleanup_size_enabled" ] && uci set momo.log.log_cleanup_size_enabled=$log_cleanup_size_enabled || uci set momo.log.log_cleanup_size_enabled=0
|
||||
}
|
||||
uci -q del momo.config.log_cleanup_size_enabled
|
||||
|
||||
log_cleanup_size_check_cron_expression=$(uci -q get momo.log.log_cleanup_size_check_cron_expression)
|
||||
[ -z "$log_cleanup_size_check_cron_expression" ] && {
|
||||
log_cleanup_size_check_cron_expression=$(uci -q get momo.config.log_cleanup_size_check_cron_expression)
|
||||
[ -n "$log_cleanup_size_check_cron_expression" ] && uci set momo.log.log_cleanup_size_check_cron_expression="$log_cleanup_size_check_cron_expression" || uci set momo.log.log_cleanup_size_check_cron_expression='*/30 * * * *'
|
||||
}
|
||||
uci -q del momo.config.log_cleanup_size_check_cron_expression
|
||||
|
||||
log_cleanup_size_mb=$(uci -q get momo.log.log_cleanup_size_mb)
|
||||
[ -z "$log_cleanup_size_mb" ] && {
|
||||
log_cleanup_size_mb=$(uci -q get momo.config.log_cleanup_size_mb)
|
||||
[ -n "$log_cleanup_size_mb" ] && uci set momo.log.log_cleanup_size_mb=$log_cleanup_size_mb || uci set momo.log.log_cleanup_size_mb=50
|
||||
}
|
||||
uci -q del momo.config.log_cleanup_size_mb
|
||||
|
||||
# commit
|
||||
uci commit momo
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { readfile, popen } from 'fs';
|
||||
import { readfile, writefile, popen } from 'fs';
|
||||
|
||||
export function get_paths() {
|
||||
let result = {};
|
||||
@ -120,4 +120,14 @@ export function get_cgroups() {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export function load_profile() {
|
||||
const paths = get_paths();
|
||||
return json(readfile(paths.run_profile_path));
|
||||
};
|
||||
|
||||
export function save_profile(obj) {
|
||||
const paths = get_paths();
|
||||
return writefile(paths.run_profile_path, obj);
|
||||
};
|
||||
48
momo/files/ucode/mixin.uc
Normal file
48
momo/files/ucode/mixin.uc
Normal file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import { uci_bool, uci_int, uci_array, merge, trim_all, load_profile, save_profile } from '/etc/momo/ucode/include.uc';
|
||||
|
||||
const uci = cursor();
|
||||
|
||||
const config = {};
|
||||
|
||||
config['log'] = {};
|
||||
config['log']['disabled'] = uci_bool(uci.get('momo', 'mixin', 'log_disabled'));
|
||||
config['log']['level'] = uci.get('momo', 'mixin', 'log_level');
|
||||
config['log']['timestamp'] = uci_bool(uci.get('momo', 'mixin', 'log_timestamp'));
|
||||
config['log']['output'] = uci.get('momo', 'mixin', 'log_output');
|
||||
|
||||
config['dns'] = {};
|
||||
config['dns']['strategy'] = uci.get('momo', 'mixin', 'dns_strategy');
|
||||
config['dns']['disable_cache'] = uci_bool(uci.get('momo', 'mixin', 'dns_disable_cache'));
|
||||
config['dns']['disable_expire'] = uci_bool(uci.get('momo', 'mixin', 'dns_disable_expire'));
|
||||
config['dns']['independent_cache'] = uci_bool(uci.get('momo', 'mixin', 'dns_independent_cache'));
|
||||
config['dns']['cache_capacity'] = uci_int(uci.get('momo', 'mixin', 'dns_cache_capacity'));
|
||||
config['dns']['reverse_mapping'] = uci_bool(uci.get('momo', 'mixin', 'dns_reverse_mapping'));
|
||||
|
||||
config['ntp'] = {};
|
||||
config['ntp']['enabled'] = uci_bool(uci.get('momo', 'mixin', 'ntp_enabled'));
|
||||
config['ntp']['server'] = uci.get('momo', 'mixin', 'ntp_server');
|
||||
config['ntp']['server_port'] = uci_int(uci.get('momo', 'mixin', 'ntp_server_port'));
|
||||
config['ntp']['interval'] = uci.get('momo', 'mixin', 'ntp_interval');
|
||||
|
||||
config['experimental'] = {};
|
||||
|
||||
config['experimental']['cache_file'] = {};
|
||||
config['experimental']['cache_file']['enabled'] = uci_bool(uci.get('momo', 'mixin', 'cache_enabled'));
|
||||
config['experimental']['cache_file']['path'] = uci.get('momo', 'mixin', 'cache_path');
|
||||
config['experimental']['cache_file']['store_fakeip'] = uci_bool(uci.get('momo', 'mixin', 'cache_store_fakeip'));
|
||||
config['experimental']['cache_file']['store_rdrc'] = uci_bool(uci.get('momo', 'mixin', 'cache_store_rdrc'));
|
||||
|
||||
config['experimental']['clash_api'] = {};
|
||||
config['experimental']['clash_api']['external_ui'] = uci.get('momo', 'mixin', 'external_control_ui_path');
|
||||
config['experimental']['clash_api']['external_ui_download_url'] = uci.get('momo', 'mixin', 'external_control_ui_download_url');
|
||||
config['experimental']['clash_api']['external_controller'] = uci.get('momo', 'mixin', 'external_control_api_listen');
|
||||
config['experimental']['clash_api']['secret'] = uci.get('momo', 'mixin', 'external_control_api_secret');
|
||||
|
||||
const profile = load_profile();
|
||||
|
||||
save_profile(merge(profile, trim_all(config)));
|
||||
@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mptcp
|
||||
PKG_VERSION:=6.1
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
|
||||
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2018 Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
|
||||
# Copyright (C) 2018-2026 Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
|
||||
# Released under GPL 3. See LICENSE for the full terms.
|
||||
|
||||
START=50
|
||||
@ -41,10 +41,10 @@ global_multipath_settings() {
|
||||
# Global MPTCP configuration
|
||||
if [ -f /proc/sys/net/mptcp/mptcp_enabled ]; then
|
||||
sysctl -qew net.mptcp.mptcp_enabled="$multipath_status"
|
||||
if [ -z "$(grep net.mptcp.mptcp_enabled /etc/sysctl.d/zzz_openmptcprouter.conf)" ]; then
|
||||
echo "net.mptcp.mptcp_enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf
|
||||
else
|
||||
if grep -q net.mptcp.mptcp_enabled /etc/sysctl.d/zzz_openmptcprouter.conf; then
|
||||
sed -i "s:^net.mptcp.mptcp_enabled=[0-1]*:net.mptcp.mptcp_enabled=${multipath_status}:" /etc/sysctl.d/zzz_openmptcprouter.conf
|
||||
else
|
||||
echo "net.mptcp.mptcp_enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf
|
||||
fi
|
||||
[ -z "$mptcp_path_manager" ] || sysctl -qew net.mptcp.mptcp_path_manager="$mptcp_path_manager"
|
||||
[ -z "$mptcp_scheduler" ] || sysctl -qew net.mptcp.mptcp_scheduler="$mptcp_scheduler"
|
||||
@ -59,10 +59,10 @@ global_multipath_settings() {
|
||||
[ -z "$mptcp_version" ] || sysctl -qew net.mptcp.mptcp_version="$mptcp_version"
|
||||
elif [ -f /proc/sys/net/mptcp/enabled ]; then
|
||||
sysctl -qew net.mptcp.enabled="$multipath_status"
|
||||
if [ -z "$(grep net.mptcp.enabled /etc/sysctl.d/zzz_openmptcprouter.conf)" ]; then
|
||||
echo "net.mptcp.enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf
|
||||
else
|
||||
if grep -q net.mptcp.enabled /etc/sysctl.d/zzz_openmptcprouter.conf; then
|
||||
sed -i "s:^net.mptcp.enabled=[0-1]*:net.mptcp.enabled=${multipath_status}:" /etc/sysctl.d/zzz_openmptcprouter.conf
|
||||
else
|
||||
echo "net.mptcp.enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf
|
||||
fi
|
||||
ip mptcp limits set add_addr_accepted $mptcp_add_addr_accepted subflows $mptcp_subflows >/dev/null 2>&1
|
||||
#[ -z "$mptcp_debug" ] || sysctl -qew net.mptcp.mptcp_debug="$mptcp_debug"
|
||||
@ -70,15 +70,16 @@ global_multipath_settings() {
|
||||
[ -z "$mptcp_checksum" ] || sysctl -qew net.mptcp.checksum_enabled="$mptcp_checksum"
|
||||
[ -z "$mptcp_stale_loss_cnt" ] || sysctl -qew net.mptcp.stale_loss_cnt="$mptcp_stale_loss_cnt"
|
||||
[ -z "$mptcp_pm_type" ] || sysctl -qew net.mptcp.pm_type="$mptcp_pm_type"
|
||||
[ -z "$mptcp_allow_join_initial_addr_port" ] || sysctl -qew net.mptcp.allow_join_initial_addr_port="$mptcp_allow_initial_addr_port"
|
||||
[ -z "$mptcp_allow_join_initial_addr_port" ] || sysctl -qew net.mptcp.allow_join_initial_addr_port="$mptcp_allow_join_initial_addr_port"
|
||||
[ -z "$mptcp_close_timeout" ] || sysctl -qew net.mptcp.close_timeout="$mptcp_close_timeout"
|
||||
[ -z "$mptcp_syn_retrans_before_tcp_fallback" ] || sysctl -qew net.mptcp.syn_retrans_before_tcp_fallback="$mptcp_syn_retrans_before_tcp_fallback"
|
||||
[ -z "$mptcp_blackhole_timeout" ] || sysctl -qew net.mptcp.blackhole_timeout="$mptcp_blackhole_timeout"
|
||||
if [ -n "$mptcp_scheduler" ] && [ -d /usr/share/bpf/scheduler ]; then
|
||||
for scheduler in $(ls -1 /usr/share/bpf/scheduler/*.o); do
|
||||
bpftool struct_ops register $scheduler >/dev/null 2>&1
|
||||
for scheduler in /usr/share/bpf/scheduler/*.o; do
|
||||
[ -f "$scheduler" ] && bpftool struct_ops register "$scheduler" >/dev/null 2>&1
|
||||
done
|
||||
sysctl -qew net.mptcp.scheduler="$(echo $mptcp_scheduler | sed -e 's/mptcp_//' -e 's/.o//')" >/dev/null 2>&1
|
||||
local sched="${mptcp_scheduler#mptcp_}"; sched="${sched%.o}"
|
||||
sysctl -qew net.mptcp.scheduler="$sched" >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
[ -z "$congestion" ] || sysctl -qew net.ipv4.tcp_congestion_control="$congestion"
|
||||
@ -88,44 +89,52 @@ interface_macaddr_count() {
|
||||
local intf="$1"
|
||||
local dmacaddr="$2"
|
||||
config_get macaddr "$intf" macaddr
|
||||
[ "$macaddr" = "$dmacaddr" ] && [ -z "$(echo $intf | grep '\.')" ] && nbmac=$((nbmac+1))
|
||||
[ "$macaddr" = "$dmacaddr" ] && [ "${intf%.*}" = "$intf" ] && nbmac=$((nbmac+1))
|
||||
}
|
||||
|
||||
interface_max_metric() {
|
||||
local config="$1"
|
||||
if [ "$1" != "omrvpn" ] && [ "$1" != "omr6in4" ] && [ "$1" != "lan" ] && [ "$1" != "loopback" ]; then
|
||||
config_get metric "$config" ip4table
|
||||
if [ "$metric" -gt "$count" ] && [ "$metric" -lt "1000" ]; then
|
||||
count=$metric
|
||||
fi
|
||||
elif [ "$1" = "omrvpn" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.${config}.metric=1500
|
||||
set network.${config}.ip4table=1500
|
||||
set network.${config}.ip6table=61500
|
||||
commit network
|
||||
set openmptcprouter.${config}.metric=1500
|
||||
commit openmptcprouter
|
||||
EOF
|
||||
elif [ "$1" = "omr6in4" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.${config}.metric=1201
|
||||
set network.${config}.ip4table=1201
|
||||
set network.${config}.ip6table=61201
|
||||
commit network
|
||||
set openmptcprouter.${config}.metric=1201
|
||||
commit openmptcprouter
|
||||
EOF
|
||||
elif [ "$1" = "lan" ]; then
|
||||
sed -i 's:50 lan:9999 lan:g' /etc/iproute2/rt_tables
|
||||
fi
|
||||
case "$1" in
|
||||
omrvpn)
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.${config}.metric=1500
|
||||
set network.${config}.ip4table=1500
|
||||
set network.${config}.ip6table=61500
|
||||
commit network
|
||||
set openmptcprouter.${config}.metric=1500
|
||||
commit openmptcprouter
|
||||
EOF
|
||||
;;
|
||||
omr6in4)
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.${config}.metric=1201
|
||||
set network.${config}.ip4table=1201
|
||||
set network.${config}.ip6table=61201
|
||||
commit network
|
||||
set openmptcprouter.${config}.metric=1201
|
||||
commit openmptcprouter
|
||||
EOF
|
||||
;;
|
||||
lan)
|
||||
sed -i 's:50 lan:9999 lan:g' /etc/iproute2/rt_tables
|
||||
;;
|
||||
loopback)
|
||||
;;
|
||||
*)
|
||||
config_get metric "$config" ip4table
|
||||
if [ "$metric" -gt "$count" ] && [ "$metric" -lt "1000" ]; then
|
||||
count=$metric
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
interface_multipath_settings() {
|
||||
local mode iface proto metric ip4table
|
||||
local config="$1"
|
||||
local intf="$2"
|
||||
local enabled
|
||||
local enabled uci_route
|
||||
uci_route=$(uci -q get openmptcprouter.settings.uci_route)
|
||||
|
||||
network_get_device iface $config
|
||||
[ -z "$iface" ] && network_get_physdev iface $config
|
||||
@ -135,7 +144,7 @@ interface_multipath_settings() {
|
||||
|
||||
config_get enabled "$config" auto "1"
|
||||
config_get txqueuelen "$config" txqueuelen
|
||||
[ -n "$(echo $iface | grep '@')" ] && iface=$(ifstatus "$config" | jsonfilter -q -e '@["device"]')
|
||||
case "$iface" in *@*) iface=$(ifstatus "$config" | jsonfilter -q -e '@["device"]') ;; esac
|
||||
if [ "$(uci -q get openmptcprouter.${config}.metric)" = "" ] || [ "$(uci -q get openmptcprouter.${config}.metric)" = "1" ]; then
|
||||
count=$((count+1))
|
||||
metric=$count
|
||||
@ -164,21 +173,12 @@ interface_multipath_settings() {
|
||||
ip4table=$metric
|
||||
ip6table=6$metric
|
||||
[ -n "$iface" ] && {
|
||||
gro=$(uci -q get network.${config}.gro)
|
||||
[ "$gro" = "1" ] && ethtool -K $iface gro on >/dev/null 2>&1
|
||||
[ "$gro" = "0" ] && ethtool -K $iface gro off >/dev/null 2>&1
|
||||
gso=$(uci -q get network.${config}.gso)
|
||||
[ "$gso" = "1" ] && ethtool -K $iface gso on >/dev/null 2>&1
|
||||
[ "$gso" = "0" ] && ethtool -K $iface gso off >/dev/null 2>&1
|
||||
lro=$(uci -q get network.${config}.lro)
|
||||
[ "$lro" = "1" ] && ethtool -K $iface lro on >/dev/null 2>&1
|
||||
[ "$lro" = "0" ] && ethtool -K $iface lro off >/dev/null 2>&1
|
||||
ufo=$(uci -q get network.${config}.ufo)
|
||||
[ "$ufo" = "1" ] && ethtool -K $iface ufo on >/dev/null 2>&1
|
||||
[ "$ufo" = "0" ] && ethtool -K $iface ufo off >/dev/null 2>&1
|
||||
tso=$(uci -q get network.${config}.tso)
|
||||
[ "$tso" = "1" ] && ethtool -K $iface tso on >/dev/null 2>&1
|
||||
[ "$tso" = "0" ] && ethtool -K $iface tso off >/dev/null 2>&1
|
||||
local offload val
|
||||
for offload in gro gso lro ufo tso; do
|
||||
val=$(uci -q get network.${config}.${offload})
|
||||
[ "$val" = "1" ] && ethtool -K $iface $offload on >/dev/null 2>&1
|
||||
[ "$val" = "0" ] && ethtool -K $iface $offload off >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
[ "$mode" = "" ] && {
|
||||
mode="$(uci -q get openmptcprouter.${config}.multipath)"
|
||||
@ -199,7 +199,7 @@ interface_multipath_settings() {
|
||||
[ -z "$mptcpmintf" ] && mptcpmintf="$config"
|
||||
uci -q set network.${config}.defaultroute=0
|
||||
uci -q set network.${config}.peerdns=0
|
||||
uci -q delete network.${config}.dns=0
|
||||
uci -q delete network.${config}.dns
|
||||
echo '' > /etc/resolv.conf >/dev/null 2>&1
|
||||
}
|
||||
[ "$mode" = "master" ] && {
|
||||
@ -233,9 +233,9 @@ interface_multipath_settings() {
|
||||
#[ "$config" = "omrvpn" ] && return 0
|
||||
[ "$config" = "omrvpn" ] && mode="off"
|
||||
#[ -n "$(ifconfig | grep $iface)" ] || return 0
|
||||
[ -n "$(ip link show dev $iface)" ] || return 0
|
||||
[ "$(echo $iface | grep _dev)" != "" ] && return 0
|
||||
[ "$(echo $iface | grep '^if')" != "" ] && return 0
|
||||
ip link show dev "$iface" >/dev/null 2>&1 || return 0
|
||||
case "$iface" in *_dev*) return 0 ;; esac
|
||||
case "$iface" in if*) return 0 ;; esac
|
||||
[ "$iface" = "lo" ] && return 0
|
||||
status=`ifstatus $config | jsonfilter -q -e "@.up" | tr -d "\n"`
|
||||
[ "$status" = "false" ] && return 0
|
||||
@ -293,7 +293,7 @@ interface_multipath_settings() {
|
||||
[ -n "$ipaddr" ] && [ -n "$netmask" ] && netmask=`ipcalc.sh $ipaddr/$netmask | sed -n '/PREFIX=/{;s/.*=//;s/ .*//;p;}'`
|
||||
[ -n "$ipaddr" ] && [ -n "$netmask" ] && network=`ipcalc.sh $ipaddr/$netmask | sed -n '/NETWORK=/{;s/.*=//;s/ .*//;p;}'`
|
||||
fi
|
||||
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
|
||||
if [ "$uci_route" = "1" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete network.${config}_rule
|
||||
delete network.${config}_route
|
||||
@ -301,12 +301,12 @@ interface_multipath_settings() {
|
||||
commit network
|
||||
EOF
|
||||
else
|
||||
[ -n "$(ip rule list | grep $id)" ] && ip rule del table $ip4table > /dev/null 2>&1
|
||||
ip rule show | grep -q " $id " && ip rule del table $ip4table > /dev/null 2>&1
|
||||
ip route flush $id > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [ -n "$gateway" ] && [ -n "$network" ]; then
|
||||
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
|
||||
if [ "$uci_route" = "1" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete network.${config}_rule
|
||||
set network.${config}_rule=rule
|
||||
@ -360,7 +360,7 @@ interface_multipath_settings() {
|
||||
fi
|
||||
fi
|
||||
if [ -z "$gateway" ] && [ -n "$network" ]; then
|
||||
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
|
||||
if [ "$uci_route" = "1" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete network.${config}_rule
|
||||
set network.${config}_rule=rule
|
||||
@ -424,11 +424,11 @@ interface_multipath_settings() {
|
||||
#fi
|
||||
gateway6=$(echo $gateway6 | cut -d/ -f1 | tr -d "\n")
|
||||
netmask6=$(ubus call network.interface.$config status | jsonfilter -q -l 1 -e '@["ipv6-prefix"][0].mask' | tr -d "\n")
|
||||
network6=$(ubus call network.interface.$config status | jsonfilter -q -l 1 -e '@[".ipv6-prefix"][0].address' | tr -d "\n")
|
||||
network6=$(ubus call network.interface.$config status | jsonfilter -q -l 1 -e '@["ipv6-prefix"][0].address' | tr -d "\n")
|
||||
[ -z "$netmask6" ] && [ -n "$ip6addr" ] && netmask6=$(ip -6 addr show dev $iface | grep $ip6addr | awk '/inet6/ {print $2; exit}' | cut -d/ -f2 | tr -d "\n")
|
||||
[ -z "$network6" ] && [ -n "$ip6addr" ] && [ -n "$netmask6" ] && network6=`/usr/sbin/ipcalc -n ${ip6addr}/${netmask6} | sed -n '/NETWORK=/{;s/.*=//;s/ .*//;p;}'`
|
||||
fi
|
||||
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
|
||||
if [ "$uci_route" = "1" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete network.${config}_rule6
|
||||
delete network.${config}_route6
|
||||
@ -436,12 +436,12 @@ interface_multipath_settings() {
|
||||
commit network
|
||||
EOF
|
||||
else
|
||||
[ -n "$(ip -6 rule list | grep 6$id)" ] && ip -6 rule del table $ip6table > /dev/null 2>&1
|
||||
ip -6 rule show | grep -q " 6$id " && ip -6 rule del table $ip6table > /dev/null 2>&1
|
||||
ip -6 route flush 6$id > /dev/null 2>&1
|
||||
fi
|
||||
if [ -n "$gateway6" ] && [ -n "$network6" ]; then
|
||||
#echo "gateway6: $gateway6 - network6: $network6 -> ok"
|
||||
if [ "$(uci -q get openmptcprouter.settings.uci_route)" = "1" ]; then
|
||||
if [ "$uci_route" = "1" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete network.${config}_rule6
|
||||
set network.${config}_rule6=rule6
|
||||
@ -466,7 +466,7 @@ interface_multipath_settings() {
|
||||
EOF
|
||||
else
|
||||
[ -n "$ip6addr" ] && ip -6 rule add from $ip6addr table $ip6table pref 0 >/dev/null 2>&1
|
||||
[ -z "$(ip rule show pref 0 table $ip6table oif $iface)" ] && ip rule add oif $iface table $ip6table pref 0
|
||||
[ -z "$(ip -6 rule show pref 0 table $ip6table oif $iface)" ] && ip -6 rule add oif $iface table $ip6table pref 0
|
||||
ip -6 route replace $network6/$netmask6 dev $iface scope link metric 6$id $initcwrwnd >/dev/null 2>&1
|
||||
ip -6 route replace $network6/$netmask6 dev $iface scope link table $ip6table $initcwrwnd >/dev/null 2>&1
|
||||
ip -6 route replace $gateway6 dev $iface table $ip6table $initcwrwnd >/dev/null 2>&1
|
||||
@ -516,17 +516,16 @@ load_interfaces() {
|
||||
}
|
||||
|
||||
set_multipath() {
|
||||
ls -1 /sys/class/net/ | while read iface; do
|
||||
exist=0
|
||||
for ifacemptcp in $mptcpintf; do
|
||||
if [ "$iface" = "$ifacemptcp" ]; then
|
||||
exist=1
|
||||
fi
|
||||
done
|
||||
[ "$exist" = "0" ] && [ -z "$(multipath $iface | grep deactivated)" ] && [ "$iface" != "bonding_master" ] && [ -n "$(multipath $iface)" ] && {
|
||||
logger -t "MPTCP" "Disabling MPTCP on interface $iface not found in enabled multipath list"
|
||||
multipath $iface off >/dev/null 2>&1
|
||||
}
|
||||
local iface mp
|
||||
for iface in /sys/class/net/*; do
|
||||
iface="${iface##*/}"
|
||||
case " $mptcpintf " in *" $iface "*) continue ;; esac
|
||||
mp=$(multipath "$iface" 2>/dev/null)
|
||||
[ "$iface" = "bonding_master" ] && continue
|
||||
[ -z "$mp" ] && continue
|
||||
case "$mp" in *deactivated*) continue ;; esac
|
||||
logger -t "MPTCP" "Disabling MPTCP on interface $iface not found in enabled multipath list"
|
||||
multipath "$iface" off >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
@ -550,10 +549,10 @@ add_route() {
|
||||
config_get type "$1" type
|
||||
[ -n "$type" ] && routeset="$routeset type $type"
|
||||
config_get table "$1" table
|
||||
[ -n "$table" ] && routeset="table $table"
|
||||
[ -n "$table" ] && routeset="$routeset table $table"
|
||||
config_get interface "$1" interface
|
||||
iface=$(ifstatus "$interface" | jsonfilter -q -e '@["l3_device"]')
|
||||
[ -n "$(echo $iface | grep '@')" ] && iface=$(ifstatus "$interface" | jsonfilter -q -e '@["device"]')
|
||||
case "$iface" in *@*) iface=$(ifstatus "$interface" | jsonfilter -q -e '@["device"]') ;; esac
|
||||
[ -n "$iface" ] && routeset="$routeset dev $iface"
|
||||
logger -t "MPTCP" "Add route $routeset"
|
||||
[ -n "$routeset" ] && {
|
||||
@ -575,10 +574,10 @@ add_route6() {
|
||||
config_get type "$1" type
|
||||
[ -n "$type" ] && routeset="$routeset type $type"
|
||||
config_get table "$1" table
|
||||
[ -n "$table" ] && routeset="table $table"
|
||||
[ -n "$table" ] && routeset="$routeset table $table"
|
||||
config_get interface "$1" interface
|
||||
iface=$(ifstatus "$interface" | jsonfilter -q -e '@["l3_device"]')
|
||||
[ -n "$(echo $iface | grep '@')" ] && iface=$(ifstatus "$interface" | jsonfilter -q -e '@["device"]')
|
||||
case "$iface" in *@*) iface=$(ifstatus "$interface" | jsonfilter -q -e '@["device"]') ;; esac
|
||||
[ -n "$iface" ] && routeset="$routeset dev $iface"
|
||||
logger -t "MPTCP" "Add IPv6 route $routeset"
|
||||
[ -n "$routeset" ] && {
|
||||
@ -599,13 +598,18 @@ start_service() {
|
||||
#[ -n "$intf" ] && multipath "${intf}" off >/dev/null 2>&1
|
||||
[ -z "$intf" ] && global_multipath_settings
|
||||
|
||||
[ -n "$(ubus call system board | jsonfilter -e '@.board_name' | grep '3-model-b')" ] && [ "$(ip link show eth0 | grep UP)" = "" ] && {
|
||||
# RPI 3 workaround no network at boot
|
||||
ethtool eth0 > /dev/null 2>&1
|
||||
ethtool -s eth0 autoneg off > /dev/null 2>&1
|
||||
ip link set eth0 up > /dev/null 2>&1
|
||||
ethtool -s eth0 autoneg on > /dev/null 2>&1
|
||||
}
|
||||
local board_name
|
||||
board_name=$(ubus call system board | jsonfilter -e '@.board_name')
|
||||
|
||||
case "$board_name" in *3-model-b*)
|
||||
[ "$(ip link show eth0 | grep UP)" = "" ] && {
|
||||
# RPI 3 workaround no network at boot
|
||||
ethtool eth0 > /dev/null 2>&1
|
||||
ethtool -s eth0 autoneg off > /dev/null 2>&1
|
||||
ip link set eth0 up > /dev/null 2>&1
|
||||
ethtool -s eth0 autoneg on > /dev/null 2>&1
|
||||
}
|
||||
;; esac
|
||||
|
||||
mptcpintf=""
|
||||
mptcpmintf=""
|
||||
@ -637,7 +641,7 @@ start_service() {
|
||||
# If no master is defined, one interface is defined as master
|
||||
if [ "$master" = "" ] && [ -z "$intf" ]; then
|
||||
intfmaster="$mptcpmintf"
|
||||
[ "$intfmaster" != "" ] && [ "$(uci -q show network | grep \"multipath='master'\")" ] && {
|
||||
[ "$intfmaster" != "" ] && [ -z "$(uci -q show network | grep \"multipath='master'\")" ] && {
|
||||
logger -t "MPTCP" "No master multipath defined, setting it to $intfmaster"
|
||||
uci -q set network.${intfmaster}.multipath="master"
|
||||
uci -q set openmptcprouter.${intfmaster}.multipath="master"
|
||||
@ -646,13 +650,15 @@ start_service() {
|
||||
fi
|
||||
[ -n "$(uci -q changes network)" ] && uci -q commit network
|
||||
[ -n "$(uci -q changes openmptcprouter)" ] && uci -q commit openmptcprouter
|
||||
[ -n "$(ubus call system board | jsonfilter -e '@.board_name' | grep raspberry)" ] && [ -z "$(ubus call system board | jsonfilter -e '@.board_name' | grep '4-model-b')" ] && [ -z "$(ubus call system board | jsonfilter -e '@.board_name' | grep '5')" ] && {
|
||||
ethtool --offload eth0 rx off tx off > /dev/null 2>&1
|
||||
}
|
||||
[ -n "$(ubus call system board | jsonfilter -e '@.board_name' | grep -i r2s)" ] && {
|
||||
case "$board_name" in
|
||||
*raspberry*) case "$board_name" in *4-model-b*|*5*) ;; *)
|
||||
ethtool --offload eth0 rx off tx off > /dev/null 2>&1
|
||||
;; esac ;;
|
||||
esac
|
||||
case "$board_name" in *[Rr]2[Ss]*)
|
||||
ethtool -K eth0 rx off tx off > /dev/null 2>&1
|
||||
ethtool -K eth1 rx off tx off > /dev/null 2>&1
|
||||
}
|
||||
;; esac
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
|
||||
@ -65,7 +65,7 @@ case $1 in
|
||||
fi
|
||||
if [ -f /proc/sys/net/mptcp/mptcp_checksum ]; then
|
||||
echo Use checksum: `cat /proc/sys/net/mptcp/mptcp_checksum`
|
||||
else
|
||||
elif [ -f /proc/sys/net/mptcp/checksum_enabled ]; then
|
||||
echo Use checksum: `cat /proc/sys/net/mptcp/checksum_enabled`
|
||||
fi
|
||||
if [ -f /proc/sys/net/mptcp/mptcp_scheduler ]; then
|
||||
@ -98,7 +98,12 @@ case $1 in
|
||||
echo "jsonfilter or jq are required"
|
||||
exit 1
|
||||
fi
|
||||
[ -n "$IP" ] && [ -n "$endpoint" ] && ID=$(echo "${endpoint}" show | grep "$IP " | awk '{print $3}')
|
||||
if [ -n "$IP" ] && [ -n "$endpoint" ]; then
|
||||
for i in $IP; do
|
||||
ID=$(echo "${endpoint}" | grep -F "$i " | awk '{print $3}')
|
||||
[ -n "$ID" ] && break
|
||||
done
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ID" ]; then
|
||||
echo "OK"
|
||||
@ -159,18 +164,18 @@ else
|
||||
endpoint="$(ip mptcp endpoint show)"
|
||||
if [ -n "$TYPE" ]; then
|
||||
# Remove not needed if* interfaces in MPTCP
|
||||
[ -n "$endpoint" ] && oldintfs=$(echo "${endpoint}" | grep "dev if" | awk '{ print $3 }')
|
||||
[ -n "$oldintfs" ] && {
|
||||
for oldintf in $oldintfs; do
|
||||
ip mptcp endpoint delete id $oldintf >/dev/null 2>&1
|
||||
[ -n "$endpoint" ] && oldids=$(echo "${endpoint}" | grep "dev if" | awk '{ print $3 }')
|
||||
[ -n "$oldids" ] && {
|
||||
for oldid in $oldids; do
|
||||
ip mptcp endpoint delete id $oldid >/dev/null 2>&1
|
||||
done
|
||||
endpoint="$(ip mptcp endpoint show)"
|
||||
}
|
||||
fi
|
||||
if [ -n "$endpoint" ]; then
|
||||
ID=$(echo "${endpoint}" | sort | grep "dev $DEVICE " | awk '{print $3}')
|
||||
IFF=$(echo "${endpoint}" | sort | grep -m 1 -E "dev $DEVICE " | awk '{print $4; exit}')
|
||||
IFF2=$(echo "${endpoint}" | sort | grep -m 1 -E "dev $DEVICE " | awk '{print $5; exit}')
|
||||
ID=$(echo "${endpoint}" | sort | grep -F "dev $DEVICE " | awk '{print $3}')
|
||||
IFF=$(echo "${endpoint}" | sort | grep -F -m 1 "dev $DEVICE " | awk '{print $4; exit}')
|
||||
IFF2=$(echo "${endpoint}" | sort | grep -F -m 1 "dev $DEVICE " | awk '{print $5; exit}')
|
||||
fi
|
||||
if [ -n "$TYPE" ]; then
|
||||
#IP=$(ip a show $DEVICE | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p')
|
||||
@ -182,7 +187,12 @@ else
|
||||
echo "jsonfilter or jq are required"
|
||||
exit 1
|
||||
fi
|
||||
[ -z "$ID" ] && [ -n "$IP" ] && [ -n "$endpoint" ] && ID=$(echo "${endpoint}" show | grep "$IP " | awk '{print $3}')
|
||||
if [ -z "$ID" ] && [ -n "$IP" ] && [ -n "$endpoint" ]; then
|
||||
for i in $IP; do
|
||||
ID=$(echo "${endpoint}" | grep -F "$i " | awk '{print $3}')
|
||||
[ -n "$ID" ] && break
|
||||
done
|
||||
fi
|
||||
[ -n "$endpoint" ] && RMID=$(echo "${endpoint}" | grep '::ffff' | awk '{ print $3 }')
|
||||
[ -n "$RMID" ] && ip mptcp endpoint delete id $RMID >/dev/null 2>&1
|
||||
fi
|
||||
@ -195,6 +205,10 @@ else
|
||||
}
|
||||
exit 0;;
|
||||
"on")
|
||||
[ -z "$IP" ] && {
|
||||
echo "No global IP found on $DEVICE"
|
||||
exit 1
|
||||
}
|
||||
[ -n "$ID" ] && {
|
||||
for i in $ID; do
|
||||
ip mptcp endpoint delete id $i >/dev/null 2>&1
|
||||
@ -205,6 +219,10 @@ else
|
||||
done
|
||||
exit 0;;
|
||||
"signal")
|
||||
[ -z "$IP" ] && {
|
||||
echo "No global IP found on $DEVICE"
|
||||
exit 1
|
||||
}
|
||||
[ -n "$ID" ] && {
|
||||
for i in $ID; do
|
||||
ip mptcp endpoint delete id $i >/dev/null 2>&1
|
||||
@ -216,6 +234,10 @@ else
|
||||
done
|
||||
exit 0;;
|
||||
"backup")
|
||||
[ -z "$IP" ] && {
|
||||
echo "No global IP found on $DEVICE"
|
||||
exit 1
|
||||
}
|
||||
[ -n "$ID" ] && {
|
||||
for i in $ID; do
|
||||
ip mptcp endpoint delete id $i >/dev/null 2>&1
|
||||
|
||||
@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
|
||||
include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=natflow
|
||||
PKG_VERSION:=20251024
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=20260313
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_SOURCE:=$(PKG_VERSION).tar.xz
|
||||
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=netdata
|
||||
PKG_VERSION:=2.9.0
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=2.10.0
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_MAINTAINER:=Josef Schlehofer <pepe.schlehofer@gmail.com>, Daniel Engberg <daniel.engberg.lists@pyret.net>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=openlist2
|
||||
PKG_VERSION:=4.2.0
|
||||
PKG_WEB_VERSION:=4.1.10
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=4.2.1
|
||||
PKG_WEB_VERSION:=4.2.1
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_SOURCE:=openlist-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/OpenListTeam/OpenList/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
Loading…
Reference in New Issue
Block a user