diff --git a/hickory-dns/Makefile b/hickory-dns/Makefile
index f9a5f8af..f179dda9 100644
--- a/hickory-dns/Makefile
+++ b/hickory-dns/Makefile
@@ -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
diff --git a/luci-app-momo/Makefile b/luci-app-momo/Makefile
index c063ebef..c612361f 100644
--- a/luci-app-momo/Makefile
+++ b/luci-app-momo/Makefile
@@ -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
diff --git a/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js b/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js
index 7d95b660..27840a3c 100644
--- a/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js
+++ b/luci-app-momo/htdocs/luci-static/resources/view/momo/app.js
@@ -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');
diff --git a/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js b/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js
index 22641512..7fdc9c5c 100644
--- a/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js
+++ b/luci-app-momo/htdocs/luci-static/resources/view/momo/log.js
@@ -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'));
diff --git a/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js b/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js
new file mode 100644
index 00000000..3bdc5e28
--- /dev/null
+++ b/luci-app-momo/htdocs/luci-static/resources/view/momo/mixin.js
@@ -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();
+ }
+});
\ No newline at end of file
diff --git a/luci-app-momo/po/templates/momo.pot b/luci-app-momo/po/templates/momo.pot
index 3b4e7bde..e84f1adc 100644
--- a/luci-app-momo/po/templates/momo.pot
+++ b/luci-app-momo/po/templates/momo.pot
@@ -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 ""
diff --git a/luci-app-momo/po/zh_Hans/momo.po b/luci-app-momo/po/zh_Hans/momo.po
index 6ee9ae13..8e834d95 100644
--- a/luci-app-momo/po/zh_Hans/momo.po
+++ b/luci-app-momo/po/zh_Hans/momo.po
@@ -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 "按大小触发日志清理"
diff --git a/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json b/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json
index af050de1..b5feb632 100644
--- a/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json
+++ b/luci-app-momo/root/usr/share/luci/menu.d/luci-app-momo.json
@@ -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"
diff --git a/luci-app-momo/root/usr/share/rpcd/ucode/luci.momo b/luci-app-momo/root/usr/share/rpcd/ucode/luci.momo
index 9dce3295..667c616b 100644
--- a/luci-app-momo/root/usr/share/rpcd/ucode/luci.momo
+++ b/luci-app-momo/root/usr/share/rpcd/ucode/luci.momo
@@ -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'];
diff --git a/luci-app-nikki/Makefile b/luci-app-nikki/Makefile
index c4faa169..86db9331 100644
--- a/luci-app-nikki/Makefile
+++ b/luci-app-nikki/Makefile
@@ -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
diff --git a/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js b/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js
index c1e51d79..943b7c01 100644
--- a/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js
+++ b/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js
@@ -13,7 +13,6 @@ return view.extend({
return Promise.all([
uci.load('nikki'),
network.getNetworks(),
-
]);
},
render: function (data) {
diff --git a/luci-app-openclaw/CHANGELOG.md b/luci-app-openclaw/CHANGELOG.md
index 4a11911e..f87e69d9 100644
--- a/luci-app-openclaw/CHANGELOG.md
+++ b/luci-app-openclaw/CHANGELOG.md
@@ -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
### 修复
diff --git a/luci-app-openclaw/Makefile b/luci-app-openclaw/Makefile
index 00de4423..e22cfd25 100644
--- a/luci-app-openclaw/Makefile
+++ b/luci-app-openclaw/Makefile
@@ -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
diff --git a/luci-app-openclaw/README.md b/luci-app-openclaw/README.md
index 7752d065..df1a9802 100644
--- a/luci-app-openclaw/README.md
+++ b/luci-app-openclaw/README.md
@@ -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/*
-```
-
## 🔰 首次使用
diff --git a/luci-app-openclaw/VERSION b/luci-app-openclaw/VERSION
index 50ffc5aa..2165f8f9 100644
--- a/luci-app-openclaw/VERSION
+++ b/luci-app-openclaw/VERSION
@@ -1 +1 @@
-2.0.3
+2.0.4
diff --git a/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua b/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua
index 4b903bbb..c630b915 100644
--- a/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua
+++ b/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua
@@ -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="⏳ 安装进行中...";'
html[#html+1] = '}else if(r.state==="success"){'
@@ -295,35 +314,38 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}catch(e){el.innerHTML="❌ 错误";}'
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,">");'
- -- 代码块 (```code```)
- html[#html+1] = 'html=html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(m,lang,code){return"
"+code.trim()+"
";});'
+ -- 代码块 (```code```) - 圆角边框 + 等宽字体栈
+ html[#html+1] = 'html=html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(m,lang,code){return""+code.trim()+"
";});'
-- 行内代码 (`code`)
- html[#html+1] = 'html=html.replace(/`([^`]+)`/g,"$1");'
- -- 标题 (### ## #)
- html[#html+1] = 'html=html.replace(/^### (.+)$/gm,"$1
");'
- html[#html+1] = 'html=html.replace(/^## (.+)$/gm,"$1
");'
- html[#html+1] = 'html=html.replace(/^# (.+)$/gm,"$1
");'
+ html[#html+1] = 'html=html.replace(/`([^`]+)`/g,"$1");'
+ -- 标题层级 (优化比例: h1=20px, h2=17px, h3=15px, h4=14px)
+ html[#html+1] = 'html=html.replace(/^#### (.+)$/gm,"$1
");'
+ html[#html+1] = 'html=html.replace(/^### (.+)$/gm,"$1
");'
+ html[#html+1] = 'html=html.replace(/^## (.+)$/gm,"$1
");'
+ html[#html+1] = 'html=html.replace(/^# (.+)$/gm,"$1
");'
-- 粗体和斜体
- html[#html+1] = 'html=html.replace(/\\*\\*([^*]+)\\*\\*/g,"$1");'
+ html[#html+1] = 'html=html.replace(/\\*\\*([^*]+)\\*\\*/g,"$1");'
html[#html+1] = 'html=html.replace(/\\*([^*]+)\\*/g,"$1");'
- -- 链接 [text](url)
- html[#html+1] = 'html=html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,"$1");'
+ -- 链接 [text](url) - 悬停效果
+ html[#html+1] = 'html=html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,"$1");'
-- 无序列表 (- 或 *)
- html[#html+1] = 'html=html.replace(/^[*-] (.+)$/gm,"$1");'
+ html[#html+1] = 'html=html.replace(/^[*-] (.+)$/gm,"$1");'
-- 有序列表 (1. 2. 等)
- html[#html+1] = 'html=html.replace(/^\\d+\\. (.+)$/gm,"$1");'
+ html[#html+1] = 'html=html.replace(/^(\\d+)\\. (.+)$/gm,"$1.$2");'
-- 水平线 (--- 或 ***)
- html[#html+1] = 'html=html.replace(/^(---|\\*\\*\\*)$/gm,"
");'
- -- 段落 (连续的非空行合并)
- html[#html+1] = 'html=html.replace(/\\n\\n/g,"");'
+ html[#html+1] = 'html=html.replace(/^(---|\\*\\*\\*)$/gm,"
");'
+ -- 段落: 连续空行合并为段落分隔 (简化处理,避免生成未闭合标签)
+ html[#html+1] = 'html=html.replace(/\\n\\n+/g,"
");'
-- 换行
html[#html+1] = 'html=html.replace(/\\n/g,"
");'
- html[#html+1] = 'return""+html+"
";'
+ -- 外层容器 - 中英文字体栈
+ html[#html+1] = 'return""+html+"
";'
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("🔌 插件: v"+r.plugin_current+" → v"+r.plugin_latest+" (有新版本)");}'
- html[#html+1] = 'else if(r.plugin_latest){msgs.push("✅ 插件: v"+r.plugin_current+" (已是最新)");}'
- html[#html+1] = 'else{msgs.push("🔌 插件: v"+r.plugin_current+" (无法检查最新版本)");}'
+ html[#html+1] = 'if(r.plugin_has_update){msgs.push("🔌 有新版本 v"+r.plugin_current+" → v"+r.plugin_latest+"");}'
+ html[#html+1] = 'else if(r.plugin_latest){msgs.push("✅ 已是最新 v"+r.plugin_current);}'
+ html[#html+1] = 'else{msgs.push("🔌 无法检查 v"+r.plugin_current);}'
html[#html+1] = '}'
- html[#html+1] = 'if(msgs.length===0)msgs.push("无法获取版本信息");'
+ html[#html+1] = 'if(msgs.length===0)msgs.push("无法获取版本信息");'
html[#html+1] = 'el.innerHTML=msgs.join("
");'
- -- 插件有更新时: 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=\'\''
- html[#html+1] = '+\'
📋 v\'+r.plugin_latest+\' 更新内容
\''
- html[#html+1] = '+rendered'
- html[#html+1] = '+\'
\';'
+ -- 卡片式容器: 圆角边框 + 微阴影 + 版本标题栏
+ html[#html+1] = 'notesHtml="📋 更新日志v"+r.plugin_latest+"
"+rendered+"
";'
html[#html+1] = '}'
- html[#html+1] = 'act.innerHTML=notesHtml'
- html[#html+1] = '+\'\''
- html[#html+1] = '+\' 📥 手动下载\';'
+ -- 操作按钮区: 分组设计
+ html[#html+1] = 'act.innerHTML=notesHtml+"";'
html[#html+1] = '}'
html[#html+1] = '}catch(e){el.innerHTML="❌ 检测失败";}'
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="⏳ 插件升级中...";'
html[#html+1] = '}else if(r.state==="success"){'
diff --git a/luci-app-openclaw/root/etc/init.d/openclaw b/luci-app-openclaw/root/etc/init.d/openclaw
index 1cd8fabb..b3f6c6ca 100755
--- a/luci-app-openclaw/root/etc/init.d/openclaw
+++ b/luci-app-openclaw/root/etc/init.d/openclaw
@@ -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{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
diff --git a/luci-app-openclaw/root/etc/uci-defaults/99-openclaw b/luci-app-openclaw/root/etc/uci-defaults/99-openclaw
index 1aef4a68..a953d077 100755
--- a/luci-app-openclaw/root/etc/uci-defaults/99-openclaw
+++ b/luci-app-openclaw/root/etc/uci-defaults/99-openclaw
@@ -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
diff --git a/luci-app-openclaw/root/usr/bin/openclaw-env b/luci-app-openclaw/root/usr/bin/openclaw-env
index 3fbaca73..bcabeec0 100755
--- a/luci-app-openclaw/root/usr/bin/openclaw-env
+++ b/luci-app-openclaw/root/usr/bin/openclaw-env
@@ -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 重启中..."
}
diff --git a/luci-app-openclaw/root/usr/share/openclaw/oc-config-interactive.js b/luci-app-openclaw/root/usr/share/openclaw/oc-config-interactive.js
new file mode 100644
index 00000000..0143e735
--- /dev/null
+++ b/luci-app-openclaw/root/usr/share/openclaw/oc-config-interactive.js
@@ -0,0 +1,2031 @@
+#!/usr/bin/env node
+/**
+ * ============================================================================
+ * OpenClaw 配置工具 — 交互式菜单前端
+ * ============================================================================
+ *
+ * 功能特性:
+ * - 方向键 (↑↓) 导航菜单选项 (使用 oc-menu-engine 引擎)
+ * - 回车/空格 确认选择
+ * - 实时搜索过滤
+ * - 与 oc-config.sh 业务逻辑完全一致
+ * - 纯 Node.js 实现,零外部依赖
+ * - 完全兼容 xterm.js / Web PTY 环境
+ */
+
+const path = require('path');
+const fs = require('fs');
+const { spawn, execSync } = require('child_process');
+const menu = require('./oc-menu-engine');
+const { C, select, input, confirm, spinner, resetRenderCount } = menu;
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 配置路径 (与 oc-config.sh 保持一致)
+// ═══════════════════════════════════════════════════════════════════════════
+
+const OC_BASE_PATH = process.env.OC_BASE_PATH || '/opt';
+const OC_INSTALL_PATH = process.env.OC_INSTALL_PATH || `${OC_BASE_PATH}/openclaw`;
+const NODE_BASE = process.env.NODE_BASE || `${OC_INSTALL_PATH}/node`;
+const OC_GLOBAL = process.env.OC_GLOBAL || `${OC_INSTALL_PATH}/global`;
+const OC_DATA = process.env.OC_DATA || `${OC_INSTALL_PATH}/data`;
+const OC_STATE_DIR = process.env.OPENCLAW_STATE_DIR || `${OC_DATA}/.openclaw`;
+const CONFIG_FILE = process.env.OPENCLAW_CONFIG_PATH || `${OC_STATE_DIR}/openclaw.json`;
+const NODE_BIN = `${NODE_BASE}/bin/node`;
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 辅助函数 (与 oc-config.sh 逻辑对应)
+// ═══════════════════════════════════════════════════════════════════════════
+
+/**
+ * 执行命令并返回结果
+ */
+function runCommand(cmd, args = [], options = {}) {
+ return new Promise((resolve, reject) => {
+ const child = spawn(cmd, args, {
+ stdio: ['inherit', 'pipe', 'pipe'],
+ shell: true,
+ env: { ...process.env, ...options.env },
+ });
+
+ let stdout = '';
+ let stderr = '';
+
+ child.stdout.on('data', (data) => {
+ stdout += data;
+ process.stdout.write(data);
+ });
+
+ child.stderr.on('data', (data) => {
+ stderr += data;
+ process.stderr.write(data);
+ });
+
+ child.on('close', (code) => {
+ if (code === 0) {
+ resolve({ stdout, stderr, code });
+ } else {
+ reject(new Error(`Command failed with code ${code}: ${stderr}`));
+ }
+ });
+ });
+}
+
+/**
+ * 读取 JSON 配置文件
+ */
+function readConfig() {
+ try {
+ if (fs.existsSync(CONFIG_FILE)) {
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
+ }
+ } catch (e) {
+ console.error(`${C.red}读取配置失败:${C.reset} ${e.message}`);
+ }
+ return {};
+}
+
+/**
+ * 写入 JSON 配置文件
+ */
+function writeConfig(config) {
+ const dir = path.dirname(CONFIG_FILE);
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
+ try {
+ execSync(`chown openclaw:openclaw "${CONFIG_FILE}"`, { stdio: 'ignore' });
+ } catch {}
+}
+
+/**
+ * 获取 JSON 配置值 (对应 json_get)
+ */
+function jsonGet(keyPath) {
+ const config = readConfig();
+ const keys = keyPath.split('.');
+ let value = config;
+ for (const key of keys) {
+ if (value && typeof value === 'object' && key in value) {
+ value = value[key];
+ } else {
+ return null;
+ }
+ }
+ return value;
+}
+
+/**
+ * 设置 JSON 配置值 (对应 json_set)
+ */
+function jsonSet(keyPath, value) {
+ const config = readConfig();
+ const keys = keyPath.split('.');
+ let obj = config;
+ for (let i = 0; i < keys.length - 1; i++) {
+ if (!obj[keys[i]]) obj[keys[i]] = {};
+ obj = obj[keys[i]];
+ }
+ obj[keys[keys.length - 1]] = value;
+ writeConfig(config);
+}
+
+/**
+ * 获取当前活跃模型
+ */
+function getCurrentModel() {
+ const primary = jsonGet('agents.defaults.model.primary');
+ if (primary) return primary;
+ const config = readConfig();
+ if (config.models?.defaultModel) return config.models.defaultModel;
+ return null;
+}
+
+/**
+ * 查找 OpenClaw 入口文件
+ */
+function findOpenClawEntry() {
+ const searchPaths = [
+ `${OC_GLOBAL}/lib/node_modules/openclaw/openclaw.mjs`,
+ `${OC_GLOBAL}/lib/node_modules/openclaw/dist/cli.js`,
+ `${OC_GLOBAL}/node_modules/openclaw/openclaw.mjs`,
+ `${OC_GLOBAL}/node_modules/openclaw/dist/cli.js`,
+ ];
+ for (const p of searchPaths) {
+ if (fs.existsSync(p)) return p;
+ }
+ try {
+ const dirs = fs.readdirSync(OC_GLOBAL);
+ for (const dir of dirs) {
+ const p = `${OC_GLOBAL}/${dir}/node_modules/openclaw/openclaw.mjs`;
+ if (fs.existsSync(p)) return p;
+ }
+ } catch {}
+ return null;
+}
+
+/**
+ * 执行 OpenClaw CLI 命令 (对应 oc_cmd)
+ */
+async function ocCmd(...args) {
+ const ocEntry = findOpenClawEntry();
+ if (!ocEntry) throw new Error('OpenClaw 未安装');
+ return runCommand(NODE_BIN, [ocEntry, ...args], {
+ env: {
+ OPENCLAW_HOME: OC_DATA,
+ OPENCLAW_CONFIG_PATH: CONFIG_FILE,
+ OPENCLAW_STATE_DIR: OC_STATE_DIR,
+ HOME: OC_DATA,
+ }
+ });
+}
+
+/**
+ * 注册模型并设为默认 (对应 register_and_set_model)
+ */
+function registerAndSetModel(modelId) {
+ const config = readConfig();
+ if (!config.agents) config.agents = {};
+ if (!config.agents.defaults) config.agents.defaults = {};
+ if (!config.agents.defaults.models) config.agents.defaults.models = {};
+ if (!config.agents.defaults.model) config.agents.defaults.model = {};
+ config.agents.defaults.models[modelId] = {};
+ config.agents.defaults.model.primary = modelId;
+ writeConfig(config);
+}
+
+/**
+ * 写入 API Key 到 auth-profiles.json (对应 auth_set_apikey)
+ */
+function authSetApikey(provider, apiKey, profileId) {
+ const authDir = `${OC_STATE_DIR}/agents/main/agent`;
+ const authFile = `${authDir}/auth-profiles.json`;
+ try {
+ fs.mkdirSync(authDir, { recursive: true });
+ } catch {}
+
+ let authData = { version: 1, profiles: {}, usageStats: {} };
+ try {
+ if (fs.existsSync(authFile)) {
+ authData = JSON.parse(fs.readFileSync(authFile, 'utf8'));
+ }
+ } catch {}
+
+ if (!authData.profiles) authData.profiles = {};
+ authData.profiles[profileId || `${provider}:manual`] = {
+ type: 'api_key',
+ provider: provider,
+ key: apiKey,
+ };
+
+ fs.writeFileSync(authFile, JSON.stringify(authData, null, 2));
+ try {
+ execSync(`chown openclaw:openclaw "${authFile}"`, { stdio: 'ignore' });
+ } catch {}
+}
+
+/**
+ * 注册自定义提供商 (对应 register_custom_provider)
+ */
+function registerCustomProvider(providerName, baseUrl, apiKey, modelId, modelDisplay, ctxWindow, maxTok) {
+ const config = readConfig();
+ if (!config.models) config.models = {};
+ if (!config.models.providers) config.models.providers = {};
+ config.models.mode = 'merge';
+ config.models.providers[providerName] = {
+ baseUrl: baseUrl,
+ apiKey: apiKey,
+ api: 'openai-completions',
+ models: [{
+ id: modelId,
+ name: modelDisplay || modelId,
+ reasoning: false,
+ input: ['text', 'image'],
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: parseInt(ctxWindow) || 128000,
+ maxTokens: parseInt(maxTok) || 32000,
+ }]
+ };
+ writeConfig(config);
+}
+
+/**
+ * 注册 Coding Plan 提供商 (对应 register_codingplan_provider)
+ */
+function registerCodingPlanProvider(apiKey) {
+ const config = readConfig();
+ if (!config.models) config.models = {};
+ if (!config.models.providers) config.models.providers = {};
+ config.models.mode = 'merge';
+ config.models.providers['bailian'] = {
+ baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
+ apiKey: apiKey,
+ api: 'openai-completions',
+ models: [
+ { id: 'qwen3.5-plus', name: 'qwen3.5-plus', reasoning: false, input: ['text', 'image'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 65536 },
+ { id: 'qwen3-coder-plus', name: 'qwen3-coder-plus', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 65536 },
+ { id: 'qwen3-coder-next', name: 'qwen3-coder-next', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 65536 },
+ { id: 'qwen3-max-2026-01-23', name: 'qwen3-max-2026-01-23', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 65536 },
+ { id: 'MiniMax-M2.5', name: 'MiniMax-M2.5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 204800, maxTokens: 131072 },
+ { id: 'glm-5', name: 'glm-5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202752, maxTokens: 16384 },
+ { id: 'glm-4.7', name: 'glm-4.7', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202752, maxTokens: 16384 },
+ { id: 'kimi-k2.5', name: 'kimi-k2.5', reasoning: false, input: ['text', 'image'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 32768 },
+ ]
+ };
+
+ if (!config.agents) config.agents = {};
+ if (!config.agents.defaults) config.agents.defaults = {};
+ if (!config.agents.defaults.models) config.agents.defaults.models = {};
+ ['qwen3.5-plus', 'qwen3-coder-plus', 'qwen3-coder-next', 'qwen3-max-2026-01-23', 'MiniMax-M2.5', 'glm-5', 'glm-4.7', 'kimi-k2.5'].forEach(m => {
+ config.agents.defaults.models[`bailian/${m}`] = {};
+ });
+
+ writeConfig(config);
+}
+
+/**
+ * 注册腾讯云 Coding Plan 提供商 (对应 register_lkeap_codingplan_provider)
+ */
+function registerLkeapCodingPlanProvider(apiKey) {
+ const config = readConfig();
+ if (!config.models) config.models = {};
+ if (!config.models.providers) config.models.providers = {};
+ config.models.mode = 'merge';
+ config.models.providers['lkeap'] = {
+ baseUrl: 'https://api.lkeap.cloud.tencent.com/coding/v3',
+ apiKey: apiKey,
+ api: 'openai-completions',
+ models: [
+ { id: 'tc-code-latest', name: 'Auto (智能匹配最优模型)', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
+ { id: 'hunyuan-2.0-instruct', name: 'Tencent HY 2.0 Instruct', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 16000 },
+ { id: 'hunyuan-2.0-thinking', name: 'Tencent HY 2.0 Think', reasoning: true, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 64000 },
+ { id: 'hunyuan-t1', name: 'Hunyuan-T1', reasoning: true, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 64000 },
+ { id: 'hunyuan-turbos', name: 'Hunyuan-TurboS', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 16000 },
+ { id: 'minimax-m2.5', name: 'MiniMax-M2.5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 204800, maxTokens: 131072 },
+ { id: 'kimi-k2.5', name: 'Kimi-K2.5', reasoning: false, input: ['text', 'image'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 32768 },
+ { id: 'glm-5', name: 'GLM-5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202752, maxTokens: 8192 },
+ ]
+ };
+
+ if (!config.agents) config.agents = {};
+ if (!config.agents.defaults) config.agents.defaults = {};
+ if (!config.agents.defaults.models) config.agents.defaults.models = {};
+ ['tc-code-latest', 'hunyuan-2.0-instruct', 'hunyuan-2.0-thinking', 'hunyuan-t1', 'hunyuan-turbos', 'minimax-m2.5', 'kimi-k2.5', 'glm-5'].forEach(m => {
+ config.agents.defaults.models[`lkeap/${m}`] = {};
+ });
+
+ writeConfig(config);
+}
+
+/**
+ * 重启 Gateway
+ */
+async function restartGateway() {
+ resetRenderCount();
+ console.log(`\n${C.yellow}正在重启 Gateway...${C.reset}`);
+
+ try {
+ await runCommand('/etc/init.d/openclaw', ['restart_gateway']);
+ } catch {}
+
+ const spin = spinner({ text: 'Gateway 启动中,请稍候 (约 15-30 秒)...' });
+ spin.start();
+
+ const gwPort = jsonGet('gateway.port') || '18789';
+ let waited = 0;
+ const maxWait = 30;
+
+ while (waited < maxWait) {
+ await new Promise(r => setTimeout(r, 3000));
+ waited += 3;
+ try {
+ // 兼容 OpenWrt: 优先 ss,回退 netstat
+ let stdout = '';
+ try {
+ const result = await runCommand('ss', ['-tulnp']);
+ stdout = result.stdout;
+ } catch {
+ // ss 不存在,使用 netstat
+ const result = await runCommand('netstat', ['-tulnp']);
+ stdout = result.stdout;
+ }
+ if (stdout.includes(`:${gwPort} `)) {
+ spin.succeed(`Gateway 已重启成功 (${waited}秒)`);
+ return;
+ }
+ } catch {}
+ }
+
+ spin.stop(`${C.yellow}Gateway 仍在启动中,请稍后确认${C.reset}`);
+ console.log(`${C.cyan} 查看日志: logread -e openclaw${C.reset}\n`);
+}
+
+/**
+ * 询问是否重启
+ */
+async function askRestart() {
+ const ok = await confirm({ prompt: '是否立即重启服务以应用配置?', defaultYes: true });
+ if (ok) {
+ await restartGateway();
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 主菜单 (对应 main_menu)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function showMainMenu() {
+ const currentModel = getCurrentModel();
+ const statusLine = currentModel
+ ? `${C.green}当前模型: ${currentModel}${C.reset}`
+ : `${C.yellow}未配置模型${C.reset}`;
+
+ const result = await select({
+ title: 'OpenClaw AI Gateway — OpenWrt 配置管理',
+ header: statusLine,
+ showSearch: false,
+ items: [
+ { label: `${C.dim}━━━ AI 模型配置 ━━━${C.reset}`, disabled: true },
+ { key: '1', label: '配置 AI 模型和提供商', desc: '', value: 'model' },
+ { key: '2', label: '设置活动模型', desc: '', value: 'set-active-model' },
+
+ { label: `${C.dim}━━━ 消息渠道 ━━━${C.reset}`, disabled: true },
+ { key: '3', label: '配置消息渠道', desc: '电报/QQ/飞书', value: 'channels' },
+
+ { label: `${C.dim}━━━ 系统管理 ━━━${C.reset}`, disabled: true },
+ { key: '4', label: '健康检查与状态', desc: '', value: 'health' },
+ { key: '5', label: '查看日志', desc: '', value: 'logs' },
+ { key: '6', label: '重启 Gateway', desc: '', value: 'restart' },
+
+ { label: `${C.dim}━━━ 高级选项 ━━━${C.reset}`, disabled: true },
+ { key: '7', label: '高级配置', desc: '', value: 'advanced' },
+ { key: '8', label: '重置配置', desc: '', value: 'reset' },
+ { key: '9', label: '显示当前配置概览', desc: '', value: 'show-config' },
+ { key: '10', label: '备份/还原配置', desc: '', value: 'backup' },
+
+ { label: '', disabled: true },
+ { key: '0', label: '退出', desc: '', value: 'quit' },
+ ],
+ });
+
+ return result;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 模型配置菜单 (对应 configure_model)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function showModelMenu() {
+ const result = await select({
+ title: '配置 AI 模型和提供商',
+ showSearch: true,
+ items: [
+ { label: `${C.green}${C.bold}── 推荐 ──${C.reset}`, disabled: true },
+ { key: 'w', label: '官方完整模型配置向导', desc: `${C.green}(推荐,支持所有提供商)${C.reset}`, value: 'wizard' },
+
+ { label: `${C.bold}── 国外模型提供商 ──${C.reset}`, disabled: true },
+ { key: 'a', label: 'OpenAI', desc: 'GPT-5.2, GPT-5 mini, GPT-4.1', value: 'openai' },
+ { key: 'b', label: 'Anthropic', desc: 'Claude Sonnet 4, Opus 4, Haiku', value: 'anthropic' },
+ { key: 'c', label: 'Google Gemini', desc: 'Gemini 2.5 Pro/Flash, Gemini 3', value: 'google' },
+ { key: 'd', label: 'OpenRouter', desc: '聚合多家模型', value: 'openrouter' },
+ { key: 'e', label: 'GitHub Copilot', desc: '需要 Copilot 订阅', value: 'copilot' },
+ { key: 'f', label: 'xAI Grok', desc: 'Grok-4/3', value: 'xai' },
+
+ { label: `${C.bold}── 国内模型提供商 ──${C.reset}`, disabled: true },
+ { key: 'g', label: '阿里云通义千问 Qwen', desc: 'Portal/API/Coding Plan', value: 'qwen' },
+ { key: 'h', label: '硅基流动 SiliconFlow', desc: '', value: 'siliconflow' },
+ { key: 'i', label: '腾讯云 Coding Plan', desc: 'HY T1/TurboS/GLM-5/Kimi', value: 'tencent' },
+ { key: 'j', label: '百度千帆', desc: 'ERNIE-4.0, ERNIE-3.5', value: 'baidu' },
+ { key: 'k', label: '智谱 GLM / Z.AI', desc: 'GLM-5.1, GLM-5, GLM-4.7', value: 'zhipu' },
+
+ { label: `${C.bold}── 本地模型 / 自定义 API ──${C.reset}`, disabled: true },
+ { key: 'l', label: 'Ollama', desc: '本地模型,无需 API Key', value: 'ollama' },
+ { key: 'm', label: '自定义 OpenAI 兼容 API', desc: '', value: 'custom' },
+ { key: 'n', label: '自定义 Anthropic 兼容 API', desc: '', value: 'custom-anthropic' },
+
+ { label: '', disabled: true },
+ { key: '0', label: '返回', desc: '', value: 'back' },
+ ],
+ });
+
+ return result;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 消息渠道菜单 (对应 configure_channels)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function showChannelsMenu() {
+ const result = await select({
+ title: '配置消息渠道',
+ showSearch: false,
+ items: [
+ { label: `${C.dim}提示: 微信配置请使用 LuCI 界面「微信配置」菜单${C.reset}`, disabled: true },
+ { label: '', disabled: true },
+ { key: '1', label: 'QQ 机器人', desc: '腾讯QQ', value: 'qq' },
+ { key: '2', label: 'Telegram', desc: `${C.green}最常用${C.reset}`, value: 'telegram' },
+ { key: '3', label: 'Discord', desc: '', value: 'discord' },
+ { key: '4', label: '飞书 (Feishu)', desc: '', value: 'feishu' },
+ { key: '5', label: 'Slack', desc: '', value: 'slack' },
+ { key: '6', label: 'WhatsApp', desc: '需通过 Web 控制台扫码', value: 'whatsapp' },
+ { key: '7', label: 'Telegram 配对助手', desc: '', value: 'telegram-pairing' },
+ { key: '8', label: '官方完整渠道配置向导', desc: '', value: 'wizard' },
+ { label: '', disabled: true },
+ { key: '0', label: '返回', desc: '', value: 'back' },
+ ],
+ });
+
+ return result;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 高级配置菜单 (对应 advanced_menu)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function showAdvancedMenu() {
+ const gwPort = jsonGet('gateway.port') || '18789';
+ const gwBind = jsonGet('gateway.bind') || 'lan';
+ const gwMode = jsonGet('gateway.mode') || 'local';
+ const logLevel = jsonGet('gateway.logLevel') || '未设置';
+ const acpDispatch = jsonGet('acp.dispatch.enabled') || 'false';
+
+ const result = await select({
+ title: '高级配置',
+ showSearch: false,
+ items: [
+ { key: '1', label: 'Gateway 端口', desc: `当前: ${gwPort}`, value: 'port' },
+ { key: '2', label: 'Gateway 绑定地址', desc: `当前: ${gwBind}`, value: 'bind' },
+ { key: '3', label: 'Gateway 运行模式', desc: `当前: ${gwMode}`, value: 'mode' },
+ { key: '4', label: '日志级别', desc: `当前: ${logLevel}`, value: 'loglevel' },
+ { key: '5', label: 'ACP Dispatch 设置', desc: `当前: ${acpDispatch}`, value: 'acp' },
+ { key: '6', label: '官方完整配置向导', desc: 'oc configure', value: 'wizard' },
+ { key: '7', label: '查看原始配置 JSON', desc: '', value: 'view-json' },
+ { key: '8', label: '编辑配置文件', desc: 'vi / nano', value: 'edit' },
+ { key: '9', label: '导出配置备份', desc: '', value: 'backup' },
+ { key: '10', label: '导入配置', desc: '', value: 'import' },
+ { label: '', disabled: true },
+ { key: '0', label: '返回', desc: '', value: 'back' },
+ ],
+ });
+
+ return result;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 重置配置菜单 (对应 reset_to_defaults)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function showResetMenu() {
+ const result = await select({
+ title: '恢复默认配置',
+ showSearch: false,
+ items: [
+ { label: `${C.yellow}请选择恢复级别:${C.reset}`, disabled: true },
+ { label: '', disabled: true },
+ { key: '1', label: '仅重置网关设置', desc: '端口/绑定/模式恢复默认,保留模型和渠道', value: 'gateway' },
+ { key: '2', label: '清除模型配置', desc: '移除所有 AI 模型和 API Key', value: 'models' },
+ { key: '3', label: '清除渠道配置', desc: '移除所有消息渠道配置', value: 'channels' },
+ { key: '4', label: '完全恢复出厂', desc: '删除所有配置,重新初始化', value: 'full' },
+ { label: '', disabled: true },
+ { key: '0', label: '返回', desc: '', value: 'back' },
+ ],
+ });
+
+ return result;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 备份/还原菜单 (对应 backup_restore_menu)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function showBackupMenu() {
+ const result = await select({
+ title: '备份/还原配置',
+ showSearch: false,
+ items: [
+ { key: '1', label: '创建配置备份', desc: '仅配置文件', value: 'create-config' },
+ { key: '2', label: '创建完整备份', desc: '配置 + 状态数据', value: 'create-full' },
+ { key: '3', label: '验证最新备份', desc: '', value: 'verify' },
+ { key: '4', label: '查看备份列表', desc: '', value: 'list' },
+ { key: '5', label: '从最新备份恢复配置', desc: '', value: 'restore' },
+ { label: '', disabled: true },
+ { key: '0', label: '返回', desc: '', value: 'back' },
+ ],
+ });
+
+ return result;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 模型配置处理函数 (与 oc-config.sh 完全对应)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function configureOpenAI() {
+ resetRenderCount();
+ console.log(`\n${C.bold}OpenAI 配置${C.reset}`);
+ console.log(`${C.yellow}获取 API Key: https://platform.openai.com/api-keys${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入 OpenAI API Key (sk-...)', placeholder: 'sk-...' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: 'OpenAI 模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'gpt-5.2', desc: '最强编程与代理旗舰 (推荐)', value: 'gpt-5.2' },
+ { key: 'b', label: 'gpt-5-mini', desc: '高性价比推理', value: 'gpt-5-mini' },
+ { key: 'c', label: 'gpt-5-nano', desc: '极速低成本', value: 'gpt-5-nano' },
+ { key: 'd', label: 'gpt-4.1', desc: '最强非推理模型', value: 'gpt-4.1' },
+ { key: 'e', label: 'o3', desc: '推理模型', value: 'o3' },
+ { key: 'f', label: 'o4-mini', desc: '推理轻量', value: 'o4-mini' },
+ { key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'gpt-5.2' });
+ if (!modelName) return false;
+ }
+
+ authSetApikey('openai', apiKey);
+ registerAndSetModel(`openai/${modelName}`);
+ console.log(`\n${C.green}✅ OpenAI 已配置,活跃模型: openai/${modelName}${C.reset}\n`);
+ return true;
+}
+
+async function configureAnthropic() {
+ resetRenderCount();
+ console.log(`\n${C.bold}Anthropic 配置${C.reset}`);
+ console.log(`${C.yellow}获取 API Key: https://console.anthropic.com/settings/keys${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入 Anthropic API Key (sk-ant-...)', placeholder: 'sk-ant-...' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: 'Anthropic 模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'claude-sonnet-4-20250514', desc: 'Claude Sonnet 4 (推荐)', value: 'claude-sonnet-4-20250514' },
+ { key: 'b', label: 'claude-opus-4-20250514', desc: 'Claude Opus 4 顶级推理', value: 'claude-opus-4-20250514' },
+ { key: 'c', label: 'claude-haiku-4-5', desc: 'Claude Haiku 4.5 轻量快速', value: 'claude-haiku-4-5' },
+ { key: 'd', label: 'claude-sonnet-4.5', desc: 'Claude Sonnet 4.5', value: 'claude-sonnet-4.5' },
+ { key: 'e', label: 'claude-sonnet-4.6', desc: 'Claude Sonnet 4.6', value: 'claude-sonnet-4.6' },
+ { key: 'f', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'claude-sonnet-4-20250514' });
+ if (!modelName) return false;
+ }
+
+ authSetApikey('anthropic', apiKey);
+ registerAndSetModel(`anthropic/${modelName}`);
+ console.log(`\n${C.green}✅ Anthropic 已配置,活跃模型: anthropic/${modelName}${C.reset}\n`);
+ return true;
+}
+
+async function configureGoogle() {
+ resetRenderCount();
+ console.log(`\n${C.bold}Google Gemini 配置${C.reset}`);
+ console.log(`${C.yellow}获取 API Key: https://aistudio.google.com/apikey${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入 Google AI API Key', placeholder: 'AIza...' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: 'Google Gemini 模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'gemini-2.5-pro', desc: '旗舰推理 (推荐)', value: 'gemini-2.5-pro' },
+ { key: 'b', label: 'gemini-2.5-flash', desc: '快速均衡', value: 'gemini-2.5-flash' },
+ { key: 'c', label: 'gemini-2.5-flash-lite', desc: '极速低成本', value: 'gemini-2.5-flash-lite' },
+ { key: 'd', label: 'gemini-3-flash-preview', desc: 'Gemini 3 Flash 预览', value: 'gemini-3-flash-preview' },
+ { key: 'e', label: 'gemini-3-pro-preview', desc: 'Gemini 3 Pro 预览', value: 'gemini-3-pro-preview' },
+ { key: 'f', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'gemini-2.5-pro' });
+ if (!modelName) return false;
+ }
+
+ authSetApikey('google', apiKey);
+ registerAndSetModel(`google/${modelName}`);
+ console.log(`\n${C.green}✅ Google Gemini 已配置,活跃模型: google/${modelName}${C.reset}\n`);
+ return true;
+}
+
+async function configureOpenRouter() {
+ resetRenderCount();
+ console.log(`\n${C.bold}OpenRouter 配置${C.reset}`);
+ console.log(`${C.yellow}获取 API Key: https://openrouter.ai/keys${C.reset}`);
+ console.log(`${C.dim}聚合多家模型,一个 Key 可调用所有主流模型${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入 OpenRouter API Key', placeholder: 'sk-or-...' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: 'OpenRouter 常用模型',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'anthropic/claude-sonnet-4', desc: 'Claude Sonnet 4 (推荐)', value: 'anthropic/claude-sonnet-4' },
+ { key: 'b', label: 'anthropic/claude-opus-4', desc: 'Claude Opus 4', value: 'anthropic/claude-opus-4' },
+ { key: 'c', label: 'openai/gpt-5.2', desc: 'GPT-5.2', value: 'openai/gpt-5.2' },
+ { key: 'd', label: 'google/gemini-2.5-pro', desc: 'Gemini 2.5 Pro', value: 'google/gemini-2.5-pro' },
+ { key: 'e', label: 'deepseek/deepseek-r1', desc: 'DeepSeek R1', value: 'deepseek/deepseek-r1' },
+ { key: 'f', label: 'meta-llama/llama-4-maverick', desc: 'Meta Llama 4', value: 'meta-llama/llama-4-maverick' },
+ { key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称 (格式: provider/model)', defaultValue: 'anthropic/claude-sonnet-4' });
+ if (!modelName) return false;
+ }
+
+ authSetApikey('openrouter', apiKey);
+ registerCustomProvider('openrouter', 'https://openrouter.ai/api/v1', apiKey, modelName, modelName);
+ registerAndSetModel(`openrouter/${modelName}`);
+ console.log(`\n${C.green}✅ OpenRouter 已配置,活跃模型: openrouter/${modelName}${C.reset}\n`);
+ return true;
+}
+
+async function configureCopilot() {
+ resetRenderCount();
+ console.log(`\n${C.bold}GitHub Copilot 配置${C.reset}`);
+ console.log(`${C.yellow}需要有效的 GitHub Copilot 订阅 (Free/Pro/Business 均可)${C.reset}\n`);
+
+ console.log(`${C.cyan}启动 GitHub Copilot OAuth 登录...${C.reset}`);
+ console.log(`${C.dim}请在浏览器中打开显示的 URL,输入授权码完成登录${C.reset}\n`);
+
+ try {
+ await ocCmd('models', 'auth', 'login-github-copilot', '--yes');
+ console.log(`\n${C.green}✅ GitHub Copilot OAuth 认证成功${C.reset}\n`);
+
+ const modelChoice = await select({
+ title: 'GitHub Copilot 模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'gpt-4.1', desc: 'GPT-4.1 (推荐)', value: 'gpt-4.1' },
+ { key: 'b', label: 'gpt-4o', desc: 'GPT-4o', value: 'gpt-4o' },
+ { key: 'c', label: 'gpt-5', desc: 'GPT-5', value: 'gpt-5' },
+ { key: 'd', label: 'gpt-5-mini', desc: 'GPT-5 mini', value: 'gpt-5-mini' },
+ { key: 'e', label: 'gpt-5.1', desc: 'GPT-5.1', value: 'gpt-5.1' },
+ { key: 'f', label: 'gpt-5.2', desc: 'GPT-5.2', value: 'gpt-5.2' },
+ { key: 'g', label: 'gpt-5.2-codex', desc: 'GPT-5.2 Codex', value: 'gpt-5.2-codex' },
+ { key: 'h', label: 'claude-sonnet-4', desc: 'Claude Sonnet 4', value: 'claude-sonnet-4' },
+ { key: 'i', label: 'claude-sonnet-4.5', desc: 'Claude Sonnet 4.5', value: 'claude-sonnet-4.5' },
+ { key: 'j', label: 'claude-sonnet-4.6', desc: 'Claude Sonnet 4.6', value: 'claude-sonnet-4.6' },
+ { key: 'k', label: 'gemini-2.5-pro', desc: 'Gemini 2.5 Pro', value: 'gemini-2.5-pro' },
+ { key: 'm', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+
+ if (modelChoice) {
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'gpt-4.1' });
+ if (!modelName) return false;
+ }
+ registerAndSetModel(`github-copilot/${modelName}`);
+ console.log(`\n${C.green}✅ 活跃模型已设置: github-copilot/${modelName}${C.reset}\n`);
+ }
+ return true;
+ } catch (e) {
+ console.log(`\n${C.yellow}OAuth 授权已退出或失败${C.reset}\n`);
+ return false;
+ }
+}
+
+async function configureXAI() {
+ resetRenderCount();
+ console.log(`\n${C.bold}xAI Grok 配置${C.reset}`);
+ console.log(`${C.yellow}获取 API Key: https://console.x.ai${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入 xAI API Key', placeholder: '' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: 'xAI Grok 模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'grok-4', desc: 'Grok 4 旗舰 (推荐)', value: 'grok-4' },
+ { key: 'b', label: 'grok-4-fast', desc: 'Grok 4 Fast', value: 'grok-4-fast' },
+ { key: 'c', label: 'grok-3', desc: 'Grok 3', value: 'grok-3' },
+ { key: 'd', label: 'grok-3-fast', desc: 'Grok 3 Fast', value: 'grok-3-fast' },
+ { key: 'e', label: 'grok-3-mini', desc: 'Grok 3 Mini', value: 'grok-3-mini' },
+ { key: 'f', label: 'grok-3-mini-fast', desc: 'Grok 3 Mini Fast', value: 'grok-3-mini-fast' },
+ { key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'grok-4' });
+ if (!modelName) return false;
+ }
+
+ authSetApikey('xai', apiKey);
+ registerAndSetModel(`xai/${modelName}`);
+ console.log(`\n${C.green}✅ xAI Grok 已配置,活跃模型: xai/${modelName}${C.reset}\n`);
+ return true;
+}
+
+async function configureQwen() {
+ resetRenderCount();
+ console.log(`\n${C.bold}阿里云通义千问 Qwen 配置${C.reset}\n`);
+
+ const modeChoice = await select({
+ title: '配置方式',
+ showSearch: false,
+ items: [
+ { key: 'a', label: '通过官方向导配置', desc: 'Qwen Portal OAuth', value: 'portal' },
+ { key: 'b', label: '百炼按量付费 API Key', desc: 'sk-xxx, 按 token 计费', value: 'bailian' },
+ { key: 'c', label: 'Coding Plan 套餐', desc: `${C.green}★ 推荐${C.reset}`, value: 'codingplan' },
+ ],
+ });
+ if (!modeChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ if (modeChoice.value === 'portal') {
+ console.log(`\n${C.cyan}启动 Qwen OAuth 授权...${C.reset}\n`);
+ try {
+ await ocCmd('models', 'auth', 'login', '--provider', 'qwen-portal', '--set-default');
+ } catch {}
+ console.log(`\n${C.yellow}OAuth 授权已退出${C.reset}\n`);
+ return true;
+ }
+
+ if (modeChoice.value === 'bailian') {
+ console.log(`\n${C.bold}百炼按量付费配置${C.reset}`);
+ console.log(`${C.yellow}获取 API Key: https://dashscope.console.aliyun.com/apiKey${C.reset}`);
+ console.log(`${C.dim}Base URL: https://dashscope.aliyuncs.com/compatible-mode/v1${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入百炼 API Key (sk-...)', placeholder: 'sk-...' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: '百炼模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'qwen-max', desc: '千问Max 旗舰模型 (推荐)', value: 'qwen-max' },
+ { key: 'b', label: 'qwen-plus', desc: '千问Plus 均衡之选', value: 'qwen-plus' },
+ { key: 'c', label: 'qwen-flash', desc: '千问Flash 速度最快', value: 'qwen-flash' },
+ { key: 'd', label: 'qwen-turbo', desc: '千问Turbo 经济实惠', value: 'qwen-turbo' },
+ { key: 'e', label: 'qwen-long', desc: '千问Long 超长上下文', value: 'qwen-long' },
+ { key: 'f', label: 'qwen3-coder-plus', desc: '代码专用旗舰', value: 'qwen3-coder-plus' },
+ { key: 'g', label: 'qwq-plus', desc: 'QwQ推理模型', value: 'qwq-plus' },
+ { key: 'h', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'qwen-max' });
+ if (!modelName) return false;
+ }
+
+ authSetApikey('dashscope', apiKey);
+ registerCustomProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode/v1', apiKey, modelName, modelName);
+ registerAndSetModel(`dashscope/${modelName}`);
+ console.log(`\n${C.green}✅ 通义千问已配置 (按量付费),活跃模型: dashscope/${modelName}${C.reset}\n`);
+ return true;
+ }
+
+ if (modeChoice.value === 'codingplan') {
+ console.log(`\n${C.bold}Coding Plan 套餐配置${C.reset}`);
+ console.log(`${C.yellow}订阅套餐: https://bailian.console.aliyun.com/cn-beijing/?tab=model#/efm/coding_plan${C.reset}`);
+ console.log(`${C.dim}Base URL: https://coding.dashscope.aliyuncs.com/v1${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入 Coding Plan 专属 API Key (sk-sp-...)', placeholder: 'sk-sp-...' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: 'Coding Plan 模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'qwen3.5-plus', desc: 'Qwen3.5 Plus (推荐, 100万上下文)', value: 'qwen3.5-plus' },
+ { key: 'b', label: 'qwen3-coder-plus', desc: 'Qwen3 Coder Plus', value: 'qwen3-coder-plus' },
+ { key: 'c', label: 'qwen3-coder-next', desc: 'Qwen3 Coder Next', value: 'qwen3-coder-next' },
+ { key: 'd', label: 'qwen3-max-2026-01-23', desc: 'Qwen3 Max', value: 'qwen3-max-2026-01-23' },
+ { key: 'e', label: 'MiniMax-M2.5', desc: 'MiniMax M2.5', value: 'MiniMax-M2.5' },
+ { key: 'f', label: 'glm-5', desc: '智谱 GLM-5', value: 'glm-5' },
+ { key: 'g', label: 'kimi-k2.5', desc: 'Kimi K2.5', value: 'kimi-k2.5' },
+ { key: 'h', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'qwen3.5-plus' });
+ if (!modelName) return false;
+ }
+
+ console.log(`\n${C.cyan}正在注册 Coding Plan 提供商 (含全部可用模型)...${C.reset}`);
+ authSetApikey('bailian', apiKey);
+ registerCodingPlanProvider(apiKey);
+ registerAndSetModel(`bailian/${modelName}`);
+ console.log(`\n${C.green}✅ Coding Plan 已配置,活跃模型: bailian/${modelName}${C.reset}`);
+ console.log(`${C.dim}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${C.reset}\n`);
+ return true;
+ }
+
+ return false;
+}
+
+async function configureSiliconFlow() {
+ resetRenderCount();
+ console.log(`\n${C.bold}硅基流动 SiliconFlow 配置${C.reset}`);
+ console.log(`${C.yellow}获取 API Key: https://cloud.siliconflow.cn/account/ak${C.reset}`);
+ console.log(`${C.yellow}国内推理平台,支持多种开源模型${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入 SiliconFlow API Key', placeholder: '' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ console.log(`\n${C.cyan}可用模型分类说明:${C.reset}`);
+ console.log(`${C.yellow}* Pro模型: 仅支持充值余额支付${C.reset}`);
+ console.log(`${C.yellow}* 非Pro模型: 支持代金券/免费额度${C.reset}\n`);
+
+ const modelChoice = await select({
+ title: 'SiliconFlow 模型选择',
+ showSearch: false,
+ items: [
+ { label: `${C.cyan}── 非Pro模型 (支持代金券) ──${C.reset}`, disabled: true },
+ { key: '1', label: 'deepseek-ai/DeepSeek-V3', desc: 'DeepSeek-V3 (推荐)', value: 'deepseek-ai/DeepSeek-V3' },
+ { key: '2', label: 'deepseek-ai/DeepSeek-R1', desc: 'DeepSeek-R1 推理模型', value: 'deepseek-ai/DeepSeek-R1' },
+ { key: '3', label: 'Qwen/Qwen2.5-72B-Instruct', desc: '通义千问 2.5 72B', value: 'Qwen/Qwen2.5-72B-Instruct' },
+ { key: '4', label: 'Qwen/Qwen2.5-7B-Instruct', desc: '通义千问 2.5 7B', value: 'Qwen/Qwen2.5-7B-Instruct' },
+ { key: '5', label: 'THUDM/glm-4-9b-chat', desc: '智谱 GLM-4 9B', value: 'THUDM/glm-4-9b-chat' },
+ { key: '6', label: '01-ai/Yi-1.5-34B-Chat-16K', desc: '零一万物 Yi-1.5', value: '01-ai/Yi-1.5-34B-Chat-16K' },
+ { label: `${C.cyan}── Pro模型 (仅支持充值余额) ──${C.reset}`, disabled: true },
+ { key: '7', label: 'Pro/deepseek-ai/DeepSeek-V3', desc: 'DeepSeek-V3 (Pro)', value: 'Pro/deepseek-ai/DeepSeek-V3' },
+ { key: '8', label: 'Pro/zai-org/GLM-5', desc: '智谱 GLM-5', value: 'Pro/zai-org/GLM-5' },
+ { key: '0', label: '手动输入其他模型名称', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型详细名称', defaultValue: 'deepseek-ai/DeepSeek-V3' });
+ if (!modelName) return false;
+ }
+
+ authSetApikey('siliconflow', apiKey);
+ registerCustomProvider('siliconflow', 'https://api.siliconflow.cn/v1', apiKey, modelName, modelName);
+ registerAndSetModel(`siliconflow/${modelName}`);
+ console.log(`\n${C.green}✅ SiliconFlow 已配置,活跃模型: siliconflow/${modelName}${C.reset}\n`);
+ return true;
+}
+
+async function configureTencent() {
+ resetRenderCount();
+ console.log(`\n${C.bold}腾讯云大模型 Coding Plan 套餐配置${C.reset}`);
+ console.log(`${C.yellow}订阅/管理套餐: https://hunyuan.cloud.tencent.com/#/app/subscription${C.reset}`);
+ console.log(`${C.dim}文档: https://cloud.tencent.com/document/product/1772/128947${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入 Coding Plan API Key (sk-sp-...)', placeholder: 'sk-sp-...' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: '腾讯云 Coding Plan 模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'tc-code-latest', desc: '自动路由 (推荐)', value: 'tc-code-latest' },
+ { key: 'b', label: 'hunyuan-t1', desc: '混元 T1 深度推理', value: 'hunyuan-t1' },
+ { key: 'c', label: 'hunyuan-2.0-thinking', desc: '混元 2.0 Thinking', value: 'hunyuan-2.0-thinking' },
+ { key: 'd', label: 'hunyuan-turbos', desc: '混元 TurboS 旗舰', value: 'hunyuan-turbos' },
+ { key: 'e', label: 'hunyuan-2.0-instruct', desc: '混元 2.0 Instruct', value: 'hunyuan-2.0-instruct' },
+ { key: 'f', label: 'glm-5', desc: '智谱 GLM-5', value: 'glm-5' },
+ { key: 'g', label: 'kimi-k2.5', desc: 'Moonshot Kimi K2.5', value: 'kimi-k2.5' },
+ { key: 'h', label: 'minimax-m2.5', desc: 'MiniMax M2.5', value: 'minimax-m2.5' },
+ { key: 'z', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'tc-code-latest' });
+ if (!modelName) return false;
+ }
+
+ console.log(`\n${C.cyan}正在注册腾讯云 Coding Plan 提供商 (含全部套餐模型)...${C.reset}`);
+ authSetApikey('lkeap', apiKey);
+ registerLkeapCodingPlanProvider(apiKey);
+ registerAndSetModel(`lkeap/${modelName}`);
+ console.log(`\n${C.green}✅ 腾讯云 Coding Plan 已配置,活跃模型: lkeap/${modelName}${C.reset}`);
+ console.log(`${C.dim}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${C.reset}\n`);
+ return true;
+}
+
+async function configureBaidu() {
+ resetRenderCount();
+ console.log(`\n${C.bold}百度千帆大模型配置${C.reset}`);
+ console.log(`${C.yellow}获取 API Key: https://console.bce.baidu.com/qianfan/ais/console/onlineService${C.reset}\n`);
+
+ const apiKey = await input({ prompt: '请输入百度千帆 API Key (Access Token)', placeholder: '' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: '百度千帆模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'ernie-4.0-8k', desc: '文心一言 4.0 (推荐)', value: 'ernie-4.0-8k' },
+ { key: 'b', label: 'ernie-3.5-8k', desc: '文心一言 3.5', value: 'ernie-3.5-8k' },
+ { key: 'c', label: 'ernie-4.0-turbo-8k', desc: '文心一言 4.0 Turbo', value: 'ernie-4.0-turbo-8k' },
+ { key: 'd', label: 'ernie-speed-8k', desc: '文心一言 Speed 极速', value: 'ernie-speed-8k' },
+ { key: 'e', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'ernie-4.0-8k' });
+ if (!modelName) return false;
+ }
+
+ authSetApikey('qianfan', apiKey);
+ registerCustomProvider('qianfan', 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop', apiKey, modelName, modelName);
+ registerAndSetModel(`qianfan/${modelName}`);
+ console.log(`\n${C.green}✅ 百度千帆已配置,活跃模型: qianfan/${modelName}${C.reset}\n`);
+ return true;
+}
+
+async function configureZhipu() {
+ resetRenderCount();
+ console.log(`\n${C.bold}智谱 GLM / Z.AI 配置${C.reset}\n`);
+
+ const methodChoice = await select({
+ title: '认证方式',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'CN (open.bigmodel.cn)', desc: `${C.green}★ 国内用户推荐${C.reset}`, value: 'cn' },
+ { key: 'b', label: 'Coding-Plan-CN', desc: '智谱 Coding Plan 套餐', value: 'coding-plan-cn' },
+ { key: 'c', label: 'Global (api.z.ai)', desc: '全球版', value: 'global' },
+ { key: 'd', label: '手动输入', desc: '', value: 'custom' },
+ ],
+ });
+ if (!methodChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let baseUrl = 'https://open.bigmodel.cn/api/paas/v4';
+ switch (methodChoice.value) {
+ case 'cn':
+ console.log(`${C.yellow}获取 API Key: https://open.bigmodel.cn/api-key${C.reset}`);
+ break;
+ case 'coding-plan-cn':
+ baseUrl = 'https://open.bigmodel.cn/api/coding/paas/v4';
+ console.log(`${C.yellow}Coding Plan 套餐 API Key${C.reset}`);
+ break;
+ case 'global':
+ baseUrl = 'https://api.z.ai/api/paas/v4';
+ console.log(`${C.yellow}全球版 API Key${C.reset}`);
+ break;
+ case 'custom':
+ const customUrl = await input({ prompt: '请输入 Base URL', defaultValue: 'https://open.bigmodel.cn/api/paas/v4' });
+ if (customUrl) baseUrl = customUrl;
+ break;
+ }
+
+ const apiKey = await input({ prompt: '请输入智谱 API Key', placeholder: '' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: '智谱 GLM 模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'glm-5.1', desc: 'GLM-5.1 (推荐)', value: 'glm-5.1' },
+ { key: 'b', label: 'glm-5', desc: 'GLM-5', value: 'glm-5' },
+ { key: 'c', label: 'glm-4.7', desc: 'GLM-4.7', value: 'glm-4.7' },
+ { key: 'd', label: 'glm-4.7-flash', desc: 'GLM-4.7 Flash', value: 'glm-4.7-flash' },
+ { key: 'e', label: 'glm-4.5', desc: 'GLM-4.5', value: 'glm-4.5' },
+ { key: 'f', label: 'glm-4.5-flash', desc: 'GLM-4.5 Flash (免费)', value: 'glm-4.5-flash' },
+ { key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'glm-5.1' });
+ if (!modelName) return false;
+ }
+
+ // 智谱 GLM 使用原生 zai provider (OpenClaw 内置支持)
+ registerCustomProvider('zai', baseUrl, apiKey, modelName, modelName, 128000, 4096);
+ authSetApikey('zai', apiKey);
+ registerAndSetModel(`zai/${modelName}`);
+ console.log(`\n${C.green}✅ 智谱 GLM 已配置,活跃模型: zai/${modelName}${C.reset}`);
+ console.log(`${C.dim} Base URL: ${baseUrl}${C.reset}\n`);
+ return true;
+}
+
+async function configureOllama() {
+ resetRenderCount();
+ console.log(`\n${C.bold}Ollama 本地模型配置${C.reset}`);
+ console.log(`${C.yellow}Ollama 在本地或局域网运行大模型,无需 API Key${C.reset}`);
+ console.log(`${C.yellow}安装 Ollama: https://ollama.com${C.reset}\n`);
+
+ const modeChoice = await select({
+ title: '连接方式',
+ showSearch: false,
+ items: [
+ { key: 'a', label: '本机运行', desc: 'localhost:11434', value: 'local' },
+ { key: 'b', label: '局域网其他设备', desc: '', value: 'remote' },
+ ],
+ });
+ if (!modeChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let ollamaUrl = 'http://127.0.0.1:11434';
+ if (modeChoice.value === 'remote') {
+ const host = await input({ prompt: 'Ollama 地址 (如 192.168.1.100:11434)', placeholder: '' });
+ if (!host) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+ ollamaUrl = host.startsWith('http') ? host : `http://${host}`;
+ ollamaUrl = ollamaUrl.replace(/\/v1$/, '').replace(/\/$/, '');
+ }
+
+ // 尝试检测 Ollama 连通性
+ console.log(`\n${C.cyan}检测 Ollama 连通性...${C.reset}`);
+ let modelList = [];
+ try {
+ const result = await runCommand('curl', ['-sf', '--connect-timeout', '3', '--max-time', '5', `${ollamaUrl}/api/tags`]);
+ const data = JSON.parse(result.stdout);
+ modelList = data.models || [];
+ console.log(`${C.green}✅ Ollama 已连接${C.reset}`);
+ } catch {
+ console.log(`${C.yellow}⚠️ 无法连接 Ollama (${ollamaUrl})${C.reset}`);
+ const continueAnyway = await confirm({ prompt: '仍要继续配置?', defaultYes: false });
+ if (!continueAnyway) return false;
+ }
+
+ let modelName = '';
+ if (modelList.length > 0) {
+ const items = modelList.map((m, i) => ({
+ key: String(i + 1),
+ label: m.name,
+ desc: '',
+ value: m.name,
+ }));
+ items.push({ key: 'm', label: '手动输入模型名', desc: '', value: 'custom' });
+
+ const modelChoice = await select({
+ title: '已安装的模型',
+ showSearch: false,
+ items,
+ });
+
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+ modelName = modelChoice.value === 'custom'
+ ? await input({ prompt: '请输入模型名称', defaultValue: 'llama3.3' })
+ : modelChoice.value;
+ } else {
+ modelName = await input({ prompt: '请输入模型名称', placeholder: 'llama3.3, qwen2.5...' });
+ }
+
+ if (!modelName) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ // 注册 Ollama 提供商
+ const config = readConfig();
+ if (!config.models) config.models = {};
+ if (!config.models.providers) config.models.providers = {};
+ config.models.mode = 'merge';
+ config.models.providers['ollama'] = {
+ baseUrl: ollamaUrl,
+ apiKey: 'ollama-local',
+ api: 'ollama',
+ models: [{
+ id: modelName,
+ name: modelName,
+ reasoning: false,
+ input: ['text'],
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 128000,
+ maxTokens: 32000,
+ }]
+ };
+ writeConfig(config);
+
+ authSetApikey('ollama', 'ollama-local', 'ollama:local');
+ registerAndSetModel(`ollama/${modelName}`);
+ console.log(`\n${C.green}✅ Ollama 已配置,活跃模型: ollama/${modelName}${C.reset}`);
+ console.log(`${C.cyan} Ollama 地址: ${ollamaUrl}${C.reset}\n`);
+ return true;
+}
+
+async function configureCustomAPI() {
+ resetRenderCount();
+ console.log(`\n${C.bold}自定义 OpenAI 兼容 API 配置${C.reset}`);
+ console.log(`${C.yellow}支持任何兼容 OpenAI API 格式的服务商${C.reset}\n`);
+
+ const baseUrl = await input({ prompt: 'API Base URL (如 https://api.example.com/v1)', placeholder: '' });
+ if (!baseUrl) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const apiKey = await input({ prompt: 'API Key', placeholder: 'sk-...' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelName = await input({ prompt: '模型名称', placeholder: '' });
+ if (!modelName) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ authSetApikey('openai-compatible', apiKey, 'openai-compatible:manual');
+ registerCustomProvider('openai-compatible', baseUrl.replace(/\/$/, ''), apiKey, modelName, modelName);
+ registerAndSetModel(`openai-compatible/${modelName}`);
+ console.log(`\n${C.green}✅ 自定义 API 已配置,活跃模型: openai-compatible/${modelName}${C.reset}\n`);
+ return true;
+}
+
+async function configureCustomAnthropic() {
+ resetRenderCount();
+ console.log(`\n${C.bold}自定义 Anthropic 兼容 API 配置${C.reset}`);
+ console.log(`${C.yellow}支持任何兼容 Anthropic Messages API 格式的服务商${C.reset}\n`);
+
+ const baseUrl = await input({ prompt: 'API Base URL', placeholder: 'https://api.anthropic.com' });
+ if (!baseUrl) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const apiKey = await input({ prompt: 'API Key', placeholder: 'sk-ant-...' });
+ if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ const modelChoice = await select({
+ title: 'Anthropic 模型选择',
+ showSearch: false,
+ items: [
+ { key: 'a', label: 'claude-sonnet-4-20250514', desc: 'Claude Sonnet 4 (推荐)', value: 'claude-sonnet-4-20250514' },
+ { key: 'b', label: 'claude-opus-4-20250514', desc: 'Claude Opus 4', value: 'claude-opus-4-20250514' },
+ { key: 'c', label: '手动输入模型名', desc: '', value: 'custom' },
+ ],
+ });
+ if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
+
+ let modelName = modelChoice.value;
+ if (modelChoice.value === 'custom') {
+ modelName = await input({ prompt: '请输入模型名称', defaultValue: 'claude-sonnet-4-20250514' });
+ if (!modelName) return false;
+ }
+
+ const config = readConfig();
+ if (!config.models) config.models = {};
+ if (!config.models.providers) config.models.providers = {};
+ config.models.mode = 'merge';
+ config.models.providers['anthropic-compatible'] = {
+ baseUrl: baseUrl.replace(/\/$/, ''),
+ apiKey: apiKey,
+ api: 'anthropic-messages',
+ models: [{
+ id: modelName,
+ name: modelName,
+ reasoning: false,
+ input: ['text', 'image'],
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 200000,
+ maxTokens: 16000,
+ }]
+ };
+ writeConfig(config);
+
+ authSetApikey('anthropic-compatible', apiKey, 'anthropic-compatible:manual');
+ registerAndSetModel(`anthropic-compatible/${modelName}`);
+ console.log(`\n${C.green}✅ 自定义 Anthropic API 已配置,活跃模型: anthropic-compatible/${modelName}${C.reset}\n`);
+ return true;
+}
+
+async function launchWizard() {
+ resetRenderCount();
+ console.log(`\n${C.cyan}启动官方完整模型配置向导...${C.reset}\n`);
+ console.log(`${C.yellow}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${C.reset}\n`);
+ try {
+ await ocCmd('configure', '--section', 'model');
+ } catch (e) {
+ console.log(`${C.yellow}配置向导已退出${C.reset}\n`);
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 模型配置处理入口
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function handleModelConfig() {
+ while (true) {
+ const choice = await showModelMenu();
+ if (!choice || choice.value === 'back') break;
+
+ resetRenderCount();
+ let configured = false;
+
+ switch (choice.value) {
+ case 'wizard': await launchWizard(); break;
+ case 'openai': configured = await configureOpenAI(); break;
+ case 'anthropic': configured = await configureAnthropic(); break;
+ case 'google': configured = await configureGoogle(); break;
+ case 'openrouter': configured = await configureOpenRouter(); break;
+ case 'copilot': configured = await configureCopilot(); break;
+ case 'xai': configured = await configureXAI(); break;
+ case 'qwen': configured = await configureQwen(); break;
+ case 'siliconflow': configured = await configureSiliconFlow(); break;
+ case 'tencent': configured = await configureTencent(); break;
+ case 'baidu': configured = await configureBaidu(); break;
+ case 'zhipu': configured = await configureZhipu(); break;
+ case 'ollama': configured = await configureOllama(); break;
+ case 'custom': configured = await configureCustomAPI(); break;
+ case 'custom-anthropic': configured = await configureCustomAnthropic(); break;
+ }
+
+ if (configured) await askRestart();
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 设置活动模型 (对应 set_active_model)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function handleSetActiveModel() {
+ resetRenderCount();
+ console.log(`\n${C.bold}设定当前活跃模型${C.reset}\n`);
+
+ const currentModel = getCurrentModel();
+ console.log(` 当前活跃模型: ${C.green}${C.bold}${currentModel || '未设置'}${C.reset}\n`);
+
+ try {
+ const result = await ocCmd('models', 'list', '--json');
+ const modelsData = JSON.parse(result.stdout);
+ const models = modelsData.models || [];
+
+ if (models.length > 0) {
+ const items = models.map((m, i) => ({
+ key: String(i + 1),
+ label: m.key,
+ desc: m.name && m.name !== m.key ? `(${m.name})` : '',
+ value: m.key,
+ selected: m.key === currentModel,
+ }));
+
+ items.push({ label: '', disabled: true });
+ items.push({ key: 'm', label: '手动输入模型 ID', desc: '', value: 'manual' });
+ items.push({ key: '0', label: '返回', desc: '', value: 'back' });
+
+ const choice = await select({
+ title: '已配置的模型',
+ showSearch: true,
+ items,
+ });
+
+ if (!choice || choice.value === 'back') return;
+
+ if (choice.value === 'manual') {
+ const manualModel = await input({
+ prompt: '请输入模型 ID',
+ placeholder: 'openai/gpt-4o',
+ defaultValue: currentModel || '',
+ });
+ if (manualModel) {
+ registerAndSetModel(manualModel);
+ console.log(`\n${C.green}✅ 活跃模型已设为: ${manualModel}${C.reset}\n`);
+ await askRestart();
+ }
+ } else {
+ registerAndSetModel(choice.value);
+ console.log(`\n${C.green}✅ 活跃模型已切换为: ${choice.value}${C.reset}\n`);
+ await askRestart();
+ }
+ } else {
+ console.log(`${C.yellow}尚未配置任何模型。${C.reset}`);
+ console.log(`${C.yellow}请先通过「配置 AI 模型提供商」添加模型。${C.reset}\n`);
+
+ const manualModel = await input({
+ prompt: '直接输入模型 ID 设置 (留空返回)',
+ defaultValue: '',
+ });
+ if (manualModel) {
+ registerAndSetModel(manualModel);
+ console.log(`\n${C.green}✅ 活跃模型已设为: ${manualModel}${C.reset}\n`);
+ await askRestart();
+ }
+ }
+ } catch (e) {
+ console.log(`${C.red}获取模型列表失败: ${e.message}${C.reset}\n`);
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 消息渠道处理 (对应 configure_channels 等函数)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function handleChannels() {
+ while (true) {
+ const choice = await showChannelsMenu();
+ if (!choice || choice.value === 'back') break;
+
+ resetRenderCount();
+
+ switch (choice.value) {
+ case 'qq': {
+ console.log(`\n${C.bold}QQ 机器人配置${C.reset}\n`);
+ console.log(`${C.yellow}获取 App ID 和 App Secret:${C.reset}`);
+ console.log(` 1. 前往 ${C.cyan}https://q.qq.com/qqbot/openclaw/login.html${C.reset}`);
+ console.log(` 2. 用手机 QQ 扫码注册/登录`);
+ console.log(` 3. 创建机器人后复制 App ID 和 App Secret\n`);
+
+ const appId = await input({ prompt: '请输入 QQ 机器人 App ID', placeholder: '' });
+ if (!appId) { console.log(`${C.yellow}已取消${C.reset}`); break; }
+
+ const appSecret = await input({ prompt: '请输入 QQ 机器人 App Secret', placeholder: '' });
+ if (!appSecret) { console.log(`${C.yellow}已取消${C.reset}`); break; }
+
+ // 写入配置
+ const cfg = readConfig();
+ if (!cfg.channels) cfg.channels = {};
+ if (!cfg.channels.qqbot) cfg.channels.qqbot = {};
+ cfg.channels.qqbot.appId = appId;
+ cfg.channels.qqbot.clientSecret = appSecret;
+ cfg.channels.qqbot.enabled = true;
+ writeConfig(cfg);
+
+ console.log(`\n${C.green}✅ QQ 机器人配置已保存${C.reset}\n`);
+ await askRestart();
+ break;
+ }
+ case 'telegram': {
+ console.log(`\n${C.bold}Telegram Bot 配置${C.reset}\n`);
+ console.log(`${C.yellow}获取 Bot Token:${C.reset}`);
+ console.log(` 1. 打开 Telegram → 搜索 ${C.cyan}@BotFather${C.reset}`);
+ console.log(` 2. 发送 ${C.cyan}/newbot${C.reset} → 按提示创建`);
+ console.log(` 3. 复制生成的 Token\n`);
+
+ const token = await input({ prompt: '请输入 Telegram Bot Token', placeholder: '123456:ABC...' });
+ if (!token) { console.log(`${C.yellow}已取消${C.reset}`); break; }
+
+ // 验证格式
+ if (!/^[0-9]+:[A-Za-z0-9_-]+$/.test(token)) {
+ console.log(`${C.red}✗ Token 格式错误${C.reset}`);
+ console.log(`${C.yellow}正确格式: 123456789:ABCdefGHIjklMNOpqr${C.reset}\n`);
+ break;
+ }
+
+ const cfg = readConfig();
+ if (!cfg.channels) cfg.channels = {};
+ if (!cfg.channels.telegram) cfg.channels.telegram = {};
+ cfg.channels.telegram.botToken = token;
+ writeConfig(cfg);
+
+ console.log(`\n${C.green}✅ Telegram Bot Token 已保存${C.reset}\n`);
+ await askRestart();
+ break;
+ }
+ case 'discord': {
+ console.log(`\n${C.bold}Discord Bot 配置${C.reset}\n`);
+ console.log(`${C.yellow}获取 Bot Token:${C.cyan} https://discord.com/developers/applications${C.reset}\n`);
+
+ const token = await input({ prompt: '请输入 Discord Bot Token', placeholder: '' });
+ if (!token) { console.log(`${C.yellow}已取消${C.reset}`); break; }
+
+ const cfg = readConfig();
+ if (!cfg.channels) cfg.channels = {};
+ if (!cfg.channels.discord) cfg.channels.discord = {};
+ cfg.channels.discord.botToken = token;
+ writeConfig(cfg);
+
+ console.log(`\n${C.green}✅ Discord Bot Token 已保存${C.reset}\n`);
+ await askRestart();
+ break;
+ }
+ case 'feishu': {
+ console.log(`\n${C.bold}飞书 Bot 配置${C.reset}\n`);
+ console.log(`${C.cyan}即将执行飞书官方安装向导...${C.reset}\n`);
+
+ const doInstall = await confirm({ prompt: '是否开始安装?', defaultYes: true });
+ if (!doInstall) break;
+
+ console.log(`\n${C.cyan}正在启动飞书安装向导...${C.reset}\n`);
+ try {
+ await runCommand('npx', ['-y', '@larksuite/openclaw-lark-tools', 'install'], { env: { HOME: OC_DATA } });
+ } catch (e) {
+ console.log(`${C.yellow}安装向导已退出${C.reset}\n`);
+ }
+ break;
+ }
+ case 'slack': {
+ console.log(`\n${C.bold}Slack Bot 配置${C.reset}\n`);
+ console.log(`${C.yellow}获取 Bot Token:${C.cyan} https://api.slack.com/apps${C.reset}\n`);
+
+ const token = await input({ prompt: '请输入 Slack Bot Token (xoxb-...)', placeholder: '' });
+ if (!token) { console.log(`${C.yellow}已取消${C.reset}`); break; }
+
+ const cfg = readConfig();
+ if (!cfg.channels) cfg.channels = {};
+ if (!cfg.channels.slack) cfg.channels.slack = {};
+ cfg.channels.slack.botToken = token;
+ writeConfig(cfg);
+
+ console.log(`\n${C.green}✅ Slack Bot Token 已保存${C.reset}\n`);
+ await askRestart();
+ break;
+ }
+ case 'whatsapp': {
+ const gwToken = jsonGet('gateway.auth.token') || '';
+ const gwPort = jsonGet('gateway.port') || '18789';
+ console.log(`\n${C.yellow}WhatsApp 需要通过 Web 控制台扫码配对:${C.reset}`);
+ console.log(`${C.cyan}http://<你的路由器IP>:${gwPort}/?token=${gwToken}${C.reset}`);
+ console.log(`打开后进入 Channels → WhatsApp 扫码即可。\n`);
+ await input({ prompt: '按回车继续', defaultValue: '' });
+ break;
+ }
+ case 'telegram-pairing': {
+ console.log(`\n${C.cyan}启动 Telegram 配对助手...${C.reset}\n`);
+ try {
+ await ocCmd('models', 'auth', 'login-telegram-bot');
+ } catch (e) {}
+ break;
+ }
+ case 'wizard': {
+ console.log(`\n${C.cyan}启动官方渠道配置向导...${C.reset}\n`);
+ try {
+ await ocCmd('configure', '--section', 'channels');
+ } catch (e) {}
+ break;
+ }
+ }
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 健康检查 (对应 health_check)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function handleHealthCheck() {
+ resetRenderCount();
+ console.log(`\n${C.bold}健康检查${C.reset}\n`);
+
+ // 验证配置文件
+ console.log(`${C.cyan}验证配置文件格式...${C.reset}`);
+ try {
+ const result = await ocCmd('config', 'validate', '--json');
+ const validateData = JSON.parse(result.stdout);
+ if (validateData.valid) {
+ console.log(`${C.green}✅ 配置文件格式有效${C.reset}\n`);
+ } else if (validateData.errors && validateData.errors.length > 0) {
+ console.log(`${C.red}❌ 配置文件存在错误:${C.reset}`);
+ validateData.errors.forEach(e => console.log(` ${C.yellow}• ${e.message}${C.reset}`));
+ console.log('');
+ }
+ } catch (e) {
+ console.log(`${C.yellow}⚠️ 无法验证配置文件${C.reset}\n`);
+ }
+
+ // 检查端口
+ console.log(`${C.cyan}检查服务状态...${C.reset}`);
+ try {
+ await runCommand('/etc/init.d/openclaw', ['status_service']);
+ } catch (e) {}
+
+ console.log(`\n${C.cyan}提示: 查看详细日志请运行 logread -e openclaw${C.reset}`);
+ await input({ prompt: '按回车继续', defaultValue: '' });
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 显示当前配置 (对应 show_current_config)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function handleShowConfig() {
+ resetRenderCount();
+
+ const gwPort = jsonGet('gateway.port') || '18789';
+ const gwBind = jsonGet('gateway.bind') || 'lan';
+ const gwMode = jsonGet('gateway.mode') || 'local';
+ const currentModel = getCurrentModel();
+
+ console.log(`\n${C.green}┌──────────────────────────────────────────────────────────┐${C.reset}`);
+ console.log(`${C.green}│${C.reset} 📋 ${C.bold}当前配置概览${C.reset}`);
+ console.log(`${C.green}├──────────────────────────────────────────────────────────┤${C.reset}`);
+ console.log(`${C.green}│${C.reset} 网关端口 ............ ${C.cyan}${gwPort}${C.reset}`);
+ console.log(`${C.green}│${C.reset} 绑定模式 ............ ${C.cyan}${gwBind}${C.reset}`);
+ console.log(`${C.green}│${C.reset} 运行模式 ............ ${C.cyan}${gwMode}${C.reset}`);
+ console.log(`${C.green}│${C.reset} 活跃模型 ............ ${currentModel ? C.cyan + currentModel : C.yellow + '未配置'}${C.reset}`);
+
+ // 渠道状态
+ console.log(`${C.green}├──────────────────────────────────────────────────────────┤${C.reset}`);
+ console.log(`${C.green}│${C.reset} ${C.bold}渠道配置状态${C.reset}`);
+
+ const tgToken = jsonGet('channels.telegram.botToken');
+ const dcToken = jsonGet('channels.discord.botToken');
+ const fsAppId = jsonGet('channels.feishu.appId');
+ const skToken = jsonGet('channels.slack.botToken');
+ const qqAppId = jsonGet('channels.qqbot.appId');
+
+ if (qqAppId) {
+ console.log(`${C.green}│${C.reset} QQ (qqbot) ......... ${C.green}✅ 已配置${C.reset} (AppID: ${qqAppId.slice(0, 8)}...)`);
+ } else {
+ console.log(`${C.green}│${C.reset} QQ (qqbot) ......... ${C.yellow}❌ 未配置${C.reset}`);
+ }
+ if (tgToken) {
+ console.log(`${C.green}│${C.reset} Telegram ........... ${C.green}✅ 已配置${C.reset} (${tgToken.slice(0, 12)}...)`);
+ } else {
+ console.log(`${C.green}│${C.reset} Telegram ........... ${C.yellow}❌ 未配置${C.reset}`);
+ }
+ if (dcToken) {
+ console.log(`${C.green}│${C.reset} Discord ............ ${C.green}✅ 已配置${C.reset}`);
+ } else {
+ console.log(`${C.green}│${C.reset} Discord ............ ${C.yellow}❌ 未配置${C.reset}`);
+ }
+ if (fsAppId) {
+ console.log(`${C.green}│${C.reset} 飞书 ............... ${C.green}✅ 已配置${C.reset} (AppID: ${fsAppId.slice(0, 6)}...)`);
+ } else {
+ console.log(`${C.green}│${C.reset} 飞书 ............... ${C.yellow}❌ 未配置${C.reset}`);
+ }
+ if (skToken) {
+ console.log(`${C.green}│${C.reset} Slack .............. ${C.green}✅ 已配置${C.reset}`);
+ } else {
+ console.log(`${C.green}│${C.reset} Slack .............. ${C.yellow}❌ 未配置${C.reset}`);
+ }
+
+ console.log(`${C.green}└──────────────────────────────────────────────────────────┘${C.reset}\n`);
+
+ await input({ prompt: '按回车继续', defaultValue: '' });
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 高级配置处理 (对应 advanced_menu)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function handleAdvancedConfig() {
+ while (true) {
+ const choice = await showAdvancedMenu();
+ if (!choice || choice.value === 'back') break;
+
+ resetRenderCount();
+
+ switch (choice.value) {
+ case 'port': {
+ const newPort = await input({
+ prompt: '请输入 Gateway 端口',
+ defaultValue: String(jsonGet('gateway.port') || '18789'),
+ });
+ if (newPort) {
+ jsonSet('gateway.port', parseInt(newPort));
+ try { execSync(`uci set openclaw.main.port="${newPort}" && uci commit openclaw`, { stdio: 'ignore' }); } catch {}
+ console.log(`\n${C.green}✅ 端口已设置为 ${newPort}${C.reset}\n`);
+ await askRestart();
+ }
+ break;
+ }
+ case 'bind': {
+ const bindChoice = await select({
+ title: '绑定地址选项',
+ showSearch: false,
+ items: [
+ { key: '1', label: 'lan', desc: '仅 LAN 接口 (推荐)', value: 'lan' },
+ { key: '2', label: 'loopback', desc: '仅本机访问', value: 'loopback' },
+ { key: '3', label: 'all', desc: '所有接口 (0.0.0.0)', value: 'all' },
+ ],
+ });
+ if (bindChoice) {
+ jsonSet('gateway.bind', bindChoice.value);
+ try { execSync(`uci set openclaw.main.bind="${bindChoice.value}" && uci commit openclaw`, { stdio: 'ignore' }); } catch {}
+ console.log(`\n${C.green}✅ 绑定地址已设置为 ${bindChoice.value}${C.reset}\n`);
+ await askRestart();
+ }
+ break;
+ }
+ case 'mode': {
+ const modeChoice = await select({
+ title: '运行模式选项',
+ showSearch: false,
+ items: [
+ { key: '1', label: 'local', desc: '本地模式 (推荐)', value: 'local' },
+ { key: '2', label: 'remote', desc: '远程模式', value: 'remote' },
+ ],
+ });
+ if (modeChoice) {
+ jsonSet('gateway.mode', modeChoice.value);
+ console.log(`\n${C.green}✅ 运行模式已设置为 ${modeChoice.value}${C.reset}\n`);
+ await askRestart();
+ }
+ break;
+ }
+ case 'loglevel': {
+ const levelChoice = await select({
+ title: '日志级别选项',
+ showSearch: false,
+ items: [
+ { key: '1', label: 'debug', desc: '详细调试', value: 'debug' },
+ { key: '2', label: 'info', desc: '常规信息', value: 'info' },
+ { key: '3', label: 'warn', desc: '警告及以上', value: 'warn' },
+ { key: '4', label: 'error', desc: '仅错误', value: 'error' },
+ ],
+ });
+ if (levelChoice) {
+ jsonSet('gateway.logLevel', levelChoice.value);
+ console.log(`\n${C.green}✅ 日志级别已设置为 ${levelChoice.value}${C.reset}\n`);
+ await askRestart();
+ }
+ break;
+ }
+ case 'acp': {
+ const acpChoice = await select({
+ title: 'ACP Dispatch 选项',
+ showSearch: false,
+ items: [
+ { key: '1', label: 'false', desc: '禁用 (推荐路由器使用)', value: 'false' },
+ { key: '2', label: 'true', desc: '启用 (可能占用大量内存)', value: 'true' },
+ ],
+ });
+ if (acpChoice) {
+ jsonSet('acp.dispatch.enabled', acpChoice.value === 'true');
+ console.log(`\n${C.green}✅ ACP Dispatch 已设置为 ${acpChoice.value}${C.reset}\n`);
+ await askRestart();
+ }
+ break;
+ }
+ case 'wizard':
+ console.log(`\n${C.cyan}启动官方配置向导...${C.reset}\n`);
+ try {
+ await ocCmd('configure');
+ } catch (e) {}
+ break;
+ case 'view-json':
+ console.log(`\n${C.cyan}配置文件路径: ${CONFIG_FILE}${C.reset}\n`);
+ try {
+ const content = fs.readFileSync(CONFIG_FILE, 'utf8');
+ console.log(content);
+ } catch (e) {
+ console.log(`${C.red}无法读取配置文件${C.reset}`);
+ }
+ await input({ prompt: '按回车继续', defaultValue: '' });
+ break;
+ case 'edit':
+ console.log(`\n${C.yellow}请在 SSH 终端中手动编辑配置文件:${C.reset}`);
+ console.log(` vi ${CONFIG_FILE}\n`);
+ await input({ prompt: '按回车继续', defaultValue: '' });
+ break;
+ case 'backup':
+ await handleBackup();
+ break;
+ case 'import': {
+ const importPath = await input({
+ prompt: '请输入备份文件路径',
+ defaultValue: '',
+ });
+ if (importPath && fs.existsSync(importPath)) {
+ try {
+ fs.copyFileSync(importPath, CONFIG_FILE);
+ console.log(`\n${C.green}✅ 配置已导入${C.reset}\n`);
+ await askRestart();
+ } catch (e) {
+ console.log(`${C.red}导入失败: ${e.message}${C.reset}\n`);
+ }
+ } else {
+ console.log(`${C.yellow}文件不存在${C.reset}\n`);
+ }
+ break;
+ }
+ }
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 重置配置处理 (对应 reset_to_defaults)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function handleReset() {
+ while (true) {
+ const choice = await showResetMenu();
+ if (!choice || choice.value === 'back') break;
+
+ resetRenderCount();
+
+ switch (choice.value) {
+ case 'gateway': {
+ console.log(`\n${C.yellow}将重置: 网关端口→18789, 绑定→lan, 模式→local${C.reset}`);
+ console.log(`${C.yellow}保留: 认证令牌、模型配置、消息渠道${C.reset}\n`);
+ const ok = await confirm({ prompt: '确认恢复网关默认设置?', defaultYes: false });
+ if (ok) {
+ jsonSet('gateway.port', 18789);
+ jsonSet('gateway.bind', 'lan');
+ jsonSet('gateway.mode', 'local');
+ jsonSet('gateway.controlUi.allowInsecureAuth', true);
+ jsonSet('gateway.controlUi.dangerouslyDisableDeviceAuth', true);
+ jsonSet('gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback', true);
+ jsonSet('gateway.tailscale.mode', 'off');
+ console.log(`\n${C.green}✅ 网关设置已恢复默认${C.reset}\n`);
+ await askRestart();
+ }
+ break;
+ }
+ case 'models': {
+ console.log(`\n${C.red}⚠️ 将清除: 所有模型配置、API Key、活跃模型设置${C.reset}\n`);
+ const ok = await confirm({ prompt: '确认清除所有模型配置?', defaultYes: false });
+ if (ok) {
+ const cfg = readConfig();
+ delete cfg.models;
+ if (cfg.agents?.defaults?.model) delete cfg.agents.defaults.model;
+ if (cfg.agents?.defaults?.models) delete cfg.agents.defaults.models;
+ writeConfig(cfg);
+ // 清除 auth-profiles.json
+ const authFile = `${OC_STATE_DIR}/agents/main/agent/auth-profiles.json`;
+ try {
+ fs.writeFileSync(authFile, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }, null, 2));
+ } catch {}
+ console.log(`\n${C.green}✅ 模型配置已清除${C.reset}`);
+ console.log(`${C.yellow}请通过菜单 [1] 重新配置 AI 模型${C.reset}\n`);
+ await askRestart();
+ }
+ break;
+ }
+ case 'channels': {
+ console.log(`\n${C.red}⚠️ 将清除: 所有消息渠道配置 (Telegram/Discord/飞书等)${C.reset}\n`);
+ const ok = await confirm({ prompt: '确认清除所有渠道配置?', defaultYes: false });
+ if (ok) {
+ const cfg = readConfig();
+ delete cfg.channels;
+ writeConfig(cfg);
+ console.log(`\n${C.green}✅ 渠道配置已清除${C.reset}\n`);
+ await askRestart();
+ }
+ break;
+ }
+ case 'full': {
+ console.log(`\n${C.red}╔══════════════════════════════════════════════════════╗${C.reset}`);
+ console.log(`${C.red}║ ⚠️ 完全恢复出厂设置 ║${C.reset}`);
+ console.log(`${C.red}║ 此操作将删除所有配置并重新初始化 ║${C.reset}`);
+ console.log(`${C.red}╚══════════════════════════════════════════════════════╝${C.reset}\n`);
+
+ const confirmStr = await input({ prompt: '输入 RESET 确认恢复出厂设置', defaultValue: '' });
+ if (confirmStr !== 'RESET') {
+ console.log(`${C.cyan}已取消${C.reset}\n`);
+ break;
+ }
+
+ // 执行恢复
+ console.log(`\n${C.cyan}[1/5] 停止 Gateway...${C.reset}`);
+ try { await runCommand('/etc/init.d/openclaw', ['stop']); } catch {}
+
+ console.log(`${C.cyan}[2/5] 备份当前配置...${C.reset}`);
+ const backupDir = `${OC_STATE_DIR}/backups`;
+ const backupTs = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
+ try {
+ if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
+ fs.copyFileSync(CONFIG_FILE, `${backupDir}/openclaw_${backupTs}.json`);
+ console.log(`${C.green} 备份已保存: backups/openclaw_${backupTs}.json${C.reset}`);
+ } catch {}
+
+ console.log(`${C.cyan}[3/5] 重置配置...${C.reset}`);
+ writeConfig({});
+
+ console.log(`${C.cyan}[4/5] 重新初始化...${C.reset}`);
+ // 生成新 token
+ const crypto = require('crypto');
+ const newToken = crypto.randomBytes(24).toString('hex');
+
+ console.log(`${C.cyan}[5/5] 应用 OpenWrt 适配配置...${C.reset}`);
+ jsonSet('gateway.port', 18789);
+ jsonSet('gateway.bind', 'lan');
+ jsonSet('gateway.mode', 'local');
+ jsonSet('gateway.auth.mode', 'token');
+ jsonSet('gateway.auth.token', newToken);
+ jsonSet('gateway.controlUi.allowInsecureAuth', true);
+ jsonSet('gateway.controlUi.dangerouslyDisableDeviceAuth', true);
+ jsonSet('gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback', true);
+ jsonSet('gateway.tailscale.mode', 'off');
+ jsonSet('acp.dispatch.enabled', false);
+ jsonSet('tools.profile', 'coding');
+
+ try { execSync(`uci set openclaw.main.token="${newToken}" && uci commit openclaw`, { stdio: 'ignore' }); } catch {}
+
+ console.log(`\n${C.green}✅ 出厂设置已恢复!${C.reset}\n`);
+ console.log(`${C.cyan}新认证令牌: ${newToken}${C.reset}\n`);
+
+ await restartGateway();
+ break;
+ }
+ }
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 备份/还原处理 (对应 backup_restore_menu)
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function handleBackup() {
+ while (true) {
+ const choice = await showBackupMenu();
+ if (!choice || choice.value === 'back') break;
+
+ resetRenderCount();
+ const backupDir = `${OC_STATE_DIR}/backups`;
+ try { if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true }); } catch {}
+
+ switch (choice.value) {
+ case 'create-config': {
+ console.log(`\n${C.cyan}正在创建配置备份...${C.reset}`);
+ try {
+ await ocCmd('backup', 'create', '--only-config', '--no-include-workspace');
+ console.log(`${C.green}✅ 配置备份已创建${C.reset}\n`);
+ } catch (e) {
+ console.log(`${C.yellow}⚠️ 备份功能需要 OpenClaw v2026.3.8+${C.reset}\n`);
+ }
+ await input({ prompt: '按回车继续', defaultValue: '' });
+ break;
+ }
+ case 'create-full': {
+ console.log(`\n${C.cyan}正在创建完整备份...${C.reset}`);
+ try {
+ await ocCmd('backup', 'create', '--no-include-workspace');
+ console.log(`${C.green}✅ 完整备份已创建${C.reset}\n`);
+ } catch (e) {
+ console.log(`${C.yellow}⚠️ 备份失败${C.reset}\n`);
+ }
+ await input({ prompt: '按回车继续', defaultValue: '' });
+ break;
+ }
+ case 'verify': {
+ const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.tar.gz')).sort().reverse();
+ if (files.length === 0) {
+ console.log(`${C.yellow}未找到备份文件${C.reset}\n`);
+ } else {
+ const latest = `${backupDir}/${files[0]}`;
+ console.log(`${C.cyan}验证备份: ${latest}${C.reset}`);
+ try {
+ await ocCmd('backup', 'verify', latest);
+ } catch {}
+ }
+ await input({ prompt: '按回车继续', defaultValue: '' });
+ break;
+ }
+ case 'list': {
+ const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.tar.gz') || f.endsWith('.json')).sort().reverse();
+ if (files.length === 0) {
+ console.log(`${C.yellow}暂无备份文件${C.reset}\n`);
+ } else {
+ console.log(`\n${C.bold}备份文件列表:${C.reset}`);
+ files.slice(0, 10).forEach(f => {
+ const stat = fs.statSync(`${backupDir}/${f}`);
+ console.log(` ${C.dim}${f} (${(stat.size / 1024).toFixed(1)} KB)${C.reset}`);
+ });
+ console.log(`${C.dim}\n备份目录: ${backupDir}${C.reset}\n`);
+ }
+ await input({ prompt: '按回车继续', defaultValue: '' });
+ break;
+ }
+ case 'restore': {
+ const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.json')).sort().reverse();
+ if (files.length === 0) {
+ console.log(`${C.yellow}未找到配置备份文件${C.reset}\n`);
+ await input({ prompt: '按回车继续', defaultValue: '' });
+ break;
+ }
+
+ const fileChoice = await select({
+ title: '选择备份文件',
+ showSearch: false,
+ items: files.slice(0, 10).map((f, i) => ({
+ key: String(i + 1),
+ label: f,
+ desc: '',
+ value: f,
+ })),
+ });
+
+ if (!fileChoice) break;
+
+ const ok = await confirm({ prompt: '确认恢复此备份?', defaultYes: false });
+ if (ok) {
+ try {
+ fs.copyFileSync(`${backupDir}/${fileChoice.value}`, CONFIG_FILE);
+ console.log(`\n${C.green}✅ 配置已恢复${C.reset}\n`);
+ await askRestart();
+ } catch (e) {
+ console.log(`${C.red}恢复失败: ${e.message}${C.reset}\n`);
+ }
+ }
+ break;
+ }
+ }
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// 主函数
+// ═══════════════════════════════════════════════════════════════════════════
+
+async function main() {
+ const command = process.argv[2];
+
+ if (command === 'model') {
+ await handleModelConfig();
+ return;
+ }
+
+ if (command === 'status') {
+ await runCommand('/etc/init.d/openclaw', ['status_service']);
+ return;
+ }
+
+ if (command === 'restart') {
+ const ok = await confirm({ prompt: '确认重启 OpenClaw 服务?' });
+ if (ok) await runCommand('/etc/init.d/openclaw', ['restart']);
+ return;
+ }
+
+ // 交互式主菜单
+ while (true) {
+ const choice = await showMainMenu();
+
+ if (!choice || choice.value === 'quit') {
+ console.log(`\n${C.green}再见!${C.reset}\n`);
+ break;
+ }
+
+ switch (choice.value) {
+ case 'model':
+ await handleModelConfig();
+ break;
+ case 'set-active-model':
+ await handleSetActiveModel();
+ break;
+ case 'channels':
+ await handleChannels();
+ break;
+ case 'health':
+ await handleHealthCheck();
+ break;
+ case 'logs':
+ resetRenderCount();
+ console.log(`\n${C.cyan}=== OpenClaw 日志 ===${C.reset}\n`);
+ await runCommand('logread', ['-e', 'openclaw']);
+ console.log('');
+ await input({ prompt: '按回车继续', defaultValue: '' });
+ break;
+ case 'restart':
+ resetRenderCount();
+ await restartGateway();
+ break;
+ case 'advanced':
+ await handleAdvancedConfig();
+ break;
+ case 'reset':
+ await handleReset();
+ break;
+ case 'show-config':
+ await handleShowConfig();
+ break;
+ case 'backup':
+ await handleBackup();
+ break;
+ }
+ }
+}
+
+main().catch(e => {
+ console.error(`${C.red}错误:${C.reset}`, e.message);
+ process.exit(1);
+});
diff --git a/luci-app-openclaw/root/usr/share/openclaw/oc-config.sh b/luci-app-openclaw/root/usr/share/openclaw/oc-config.sh
index edbcf925..3c06136b 100755
--- a/luci-app-openclaw/root/usr/share/openclaw/oc-config.sh
+++ b/luci-app-openclaw/root/usr/share/openclaw/oc-config.sh
@@ -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 — 检查端口是否在监听,返回 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
diff --git a/luci-app-openclaw/root/usr/share/openclaw/oc-menu-engine.js b/luci-app-openclaw/root/usr/share/openclaw/oc-menu-engine.js
new file mode 100644
index 00000000..c2449383
--- /dev/null
+++ b/luci-app-openclaw/root/usr/share/openclaw/oc-menu-engine.js
@@ -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