mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 00:05:13 +08:00
💋 Sync 2026-04-08 20:31:38
This commit is contained in:
1 parent
0d0d3bb21b
commit
a12e7a5d23
29 files changed
+3458
-1106
No files matched your search
@@ -967,18 +967,18 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
o = s.taboption('field_transport', form.ListValue, 'transport_type', _('Transport type'));
|
o = s.taboption('field_transport', form.ListValue, 'transport_type', _('Transport type'));
|
||||||
o.value('grpc', _('gRPC'));
|
o.value('grpc', _('gRPC'));
|
||||||
o.value('ws', _('WebSocket'));
|
o.value('ws', _('WebSocket'));
|
||||||
|
o.value('xhttp', _('XHTTP'));
|
||||||
o.validate = function(section_id, value) {
|
o.validate = function(section_id, value) {
|
||||||
const type = this.section.getOption('type').formvalue(section_id);
|
const type = this.section.getOption('type').formvalue(section_id);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'vmess':
|
|
||||||
case 'vless':
|
case 'vless':
|
||||||
if (!['http', 'h2', 'grpc', 'ws'].includes(value))
|
if (!['grpc', 'ws', 'xhttp'].includes(value))
|
||||||
return _('Expecting: only support %s.').format(_('HTTP') +
|
return _('Expecting: only support %s.').format(_('gRPC') +
|
||||||
' / ' + _('HTTPUpgrade') +
|
' / ' + _('WebSocket') +
|
||||||
' / ' + _('gRPC') +
|
' / ' + _('XHTTP'));
|
||||||
' / ' + _('WebSocket'));
|
|
||||||
break;
|
break;
|
||||||
|
case 'vmess':
|
||||||
case 'trojan':
|
case 'trojan':
|
||||||
if (!['grpc', 'ws'].includes(value))
|
if (!['grpc', 'ws'].includes(value))
|
||||||
return _('Expecting: only support %s.').format(_('gRPC') +
|
return _('Expecting: only support %s.').format(_('gRPC') +
|
||||||
@@ -993,11 +993,17 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
o.depends('transport_enabled', '1');
|
o.depends('transport_enabled', '1');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
|
o = s.taboption('field_transport', form.Value, 'transport_host', _('Server hostname'));
|
||||||
|
o.datatype = 'hostname';
|
||||||
|
o.placeholder = 'example.com';
|
||||||
|
o.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
o = s.taboption('field_transport', form.Value, 'transport_path', _('Request path'));
|
o = s.taboption('field_transport', form.Value, 'transport_path', _('Request path'));
|
||||||
o.placeholder = '/';
|
o.placeholder = '/';
|
||||||
o.default = '/';
|
o.default = '/';
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
o.depends({transport_enabled: '1', transport_type: 'ws'});
|
o.depends({transport_enabled: '1', transport_type: /^(ws|xhttp)$/});
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
o = s.taboption('field_transport', form.Value, 'transport_grpc_servicename', _('gRPC service name'));
|
o = s.taboption('field_transport', form.Value, 'transport_grpc_servicename', _('gRPC service name'));
|
||||||
@@ -1005,6 +1011,30 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
o.depends({transport_enabled: '1', transport_type: 'grpc'});
|
o.depends({transport_enabled: '1', transport_type: 'grpc'});
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
|
o = s.taboption('field_transport', form.ListValue, 'transport_xhttp_mode', _('XHTTP mode'));
|
||||||
|
o.value('auto', _('Auto'));
|
||||||
|
o.value('stream-one', _('stream-one'));
|
||||||
|
o.value('stream-up', _('stream-up'));
|
||||||
|
o.value('packet-up', _('packet-up'));
|
||||||
|
o.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
|
o = s.taboption('field_transport', form.Flag, 'transport_xhttp_no_sse_header', _('No SSE header'));
|
||||||
|
o.default = o.disabled;
|
||||||
|
o.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
|
o = s.taboption('field_transport', form.Value, 'transport_xhttp_sc_stream_up_server_secs', _('stream-up server seconds'));
|
||||||
|
o.placeholder = '20-80';
|
||||||
|
o.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
|
o = s.taboption('field_transport', form.Value, 'transport_xhttp_sc_max_each_post_bytes', _('Max each POST bytes'));
|
||||||
|
o.datatype = 'uinteger';
|
||||||
|
o.placeholder = '1000000';
|
||||||
|
o.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||||
|
o.modalonly = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseclass.extend({
|
return baseclass.extend({
|
||||||
|
|||||||
@@ -1170,7 +1170,7 @@ return view.extend({
|
|||||||
so.depends({transport_enabled: '1', transport_type: 'h2'});
|
so.depends({transport_enabled: '1', transport_type: 'h2'});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_transport', form.DynamicList, 'transport_host', _('Server hostname'));
|
so = ss.taboption('field_transport', form.Value, 'transport_host', _('Server hostname'));
|
||||||
so.datatype = 'hostname';
|
so.datatype = 'hostname';
|
||||||
so.placeholder = 'example.com';
|
so.placeholder = 'example.com';
|
||||||
so.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
so.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||||
@@ -1261,6 +1261,42 @@ return view.extend({
|
|||||||
so.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
so.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_transport', form.Value, 'transport_xhttp_sc_max_each_post_bytes', _('Max each POST bytes'));
|
||||||
|
so.datatype = 'uinteger';
|
||||||
|
so.placeholder = '1000000';
|
||||||
|
so.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_transport', form.Flag, 'transport_xhttp_xmux', _('XMUX'));
|
||||||
|
so.default = so.disabled;
|
||||||
|
so.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_transport', form.Value, 'transport_xhttp_xmux_max_connections', _('XMUX: ') + _('Max connections'));
|
||||||
|
so.placeholder = '16-32';
|
||||||
|
so.depends('transport_xhttp_xmux', '1');
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_transport', form.Value, 'transport_xhttp_xmux_max_concurrency', _('XMUX: ') + _('Max concurrency'));
|
||||||
|
so.placeholder = '0';
|
||||||
|
so.depends({transport_xhttp_xmux: '1', transport_xhttp_xmux_max_connections: ''});
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_transport', form.Value, 'transport_xhttp_xmux_max_reuse_times', _('XMUX: ') + _('Max reuse times'));
|
||||||
|
so.placeholder = '0';
|
||||||
|
so.depends('transport_xhttp_xmux', '1');
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_transport', form.Value, 'transport_xhttp_xmux_max_request_times', _('XMUX: ') + _('Max request times'));
|
||||||
|
so.placeholder = '600-900';
|
||||||
|
so.depends('transport_xhttp_xmux', '1');
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_transport', form.Value, 'transport_xhttp_xmux_max_reusable_secs', _('XMUX: ') + _('Max reusable seconds'));
|
||||||
|
so.placeholder = '1800-3000';
|
||||||
|
so.depends('transport_xhttp_xmux', '1');
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
/* Multiplex fields */ // TCP protocol only
|
/* Multiplex fields */ // TCP protocol only
|
||||||
so = ss.taboption('field_general', form.Flag, 'smux_enabled', _('Multiplex'));
|
so = ss.taboption('field_general', form.Flag, 'smux_enabled', _('Multiplex'));
|
||||||
so.default = so.disabled;
|
so.default = so.disabled;
|
||||||
@@ -1280,6 +1316,8 @@ return view.extend({
|
|||||||
so.datatype = 'uinteger';
|
so.datatype = 'uinteger';
|
||||||
so.placeholder = '4';
|
so.placeholder = '4';
|
||||||
so.depends('smux_enabled', '1');
|
so.depends('smux_enabled', '1');
|
||||||
|
so.depends({transport_enabled: '1', transport_type: 'grpc'});
|
||||||
|
so.depends('type', 'trusttunnel');
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_multiplex', form.Value, 'smux_min_streams', _('Minimum streams'),
|
so = ss.taboption('field_multiplex', form.Value, 'smux_min_streams', _('Minimum streams'),
|
||||||
@@ -1287,6 +1325,8 @@ return view.extend({
|
|||||||
so.datatype = 'uinteger';
|
so.datatype = 'uinteger';
|
||||||
so.placeholder = '4';
|
so.placeholder = '4';
|
||||||
so.depends('smux_enabled', '1');
|
so.depends('smux_enabled', '1');
|
||||||
|
so.depends({transport_enabled: '1', transport_type: 'grpc'});
|
||||||
|
so.depends('type', 'trusttunnel');
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_multiplex', form.Value, 'smux_max_streams', _('Maximum streams'),
|
so = ss.taboption('field_multiplex', form.Value, 'smux_max_streams', _('Maximum streams'),
|
||||||
@@ -1296,6 +1336,8 @@ return view.extend({
|
|||||||
so.datatype = 'uinteger';
|
so.datatype = 'uinteger';
|
||||||
so.placeholder = '0';
|
so.placeholder = '0';
|
||||||
so.depends({smux_enabled: '1', smux_max_connections: '', smux_min_streams: ''});
|
so.depends({smux_enabled: '1', smux_max_connections: '', smux_min_streams: ''});
|
||||||
|
so.depends({transport_enabled: '1', transport_type: 'grpc', smux_max_connections: '', smux_min_streams: ''});
|
||||||
|
so.depends({type: 'trusttunnel', smux_max_connections: '', smux_min_streams: ''});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_multiplex', form.Flag, 'smux_padding', _('Enable padding'));
|
so = ss.taboption('field_multiplex', form.Flag, 'smux_padding', _('Enable padding'));
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -649,7 +649,10 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
|||||||
"grpc-opts": cfg.transport_type === 'grpc' ? {
|
"grpc-opts": cfg.transport_type === 'grpc' ? {
|
||||||
"grpc-service-name": cfg.transport_grpc_servicename,
|
"grpc-service-name": cfg.transport_grpc_servicename,
|
||||||
"grpc-user-agent": cfg.transport_grpc_user_agent,
|
"grpc-user-agent": cfg.transport_grpc_user_agent,
|
||||||
"ping-interval": strToInt(cfg.transport_grpc_ping_interval) || null
|
"ping-interval": strToInt(cfg.transport_grpc_ping_interval) || null,
|
||||||
|
"max-connections": strToInt(cfg.smux_max_connections) || null,
|
||||||
|
"min-streams": strToInt(cfg.smux_min_streams) || null,
|
||||||
|
"max-streams": strToInt(cfg.smux_max_streams) || null,
|
||||||
} : null,
|
} : null,
|
||||||
"ws-opts": cfg.transport_type === 'ws' ? {
|
"ws-opts": cfg.transport_type === 'ws' ? {
|
||||||
path: cfg.transport_path || '/',
|
path: cfg.transport_path || '/',
|
||||||
@@ -665,11 +668,24 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
|||||||
headers: cfg.transport_http_headers ? json(cfg.transport_http_headers) : null,
|
headers: cfg.transport_http_headers ? json(cfg.transport_http_headers) : null,
|
||||||
mode: cfg.transport_xhttp_mode,
|
mode: cfg.transport_xhttp_mode,
|
||||||
"no-grpc-header": strToBool(cfg.transport_xhttp_no_grpc_header),
|
"no-grpc-header": strToBool(cfg.transport_xhttp_no_grpc_header),
|
||||||
"x-padding-bytes": cfg.transport_xhttp_x_padding_bytes
|
"x-padding-bytes": cfg.transport_xhttp_x_padding_bytes,
|
||||||
|
"sc-max-each-post-bytes": strToInt(cfg.transport_xhttp_sc_max_each_post_bytes) || null,
|
||||||
|
"reuse-settings": cfg.transport_xhttp_xmux ? {
|
||||||
|
"max-connections": cfg.transport_xhttp_xmux_max_connections,
|
||||||
|
"max-concurrency": cfg.transport_xhttp_xmux_max_concurrency,
|
||||||
|
"c-max-reuse-times": cfg.transport_xhttp_xmux_max_reuse_times,
|
||||||
|
"h-max-request-times": cfg.transport_xhttp_xmux_max_request_times,
|
||||||
|
"h-max-reusable-secs": cfg.transport_xhttp_xmux_max_reusable_secs
|
||||||
|
} : null
|
||||||
} : null
|
} : null
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
/* Multiplex fields */
|
/* Multiplex fields */
|
||||||
|
...(cfg.type in ['trusttunnel'] ? {
|
||||||
|
"max-connections": strToInt(cfg.smux_max_connections) || null,
|
||||||
|
"min-streams": strToInt(cfg.smux_min_streams) || null,
|
||||||
|
"max-streams": strToInt(cfg.smux_max_streams) || null
|
||||||
|
} : {}),
|
||||||
smux: cfg.smux_enabled === '1' ? {
|
smux: cfg.smux_enabled === '1' ? {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
protocol: cfg.smux_protocol,
|
protocol: cfg.smux_protocol,
|
||||||
|
|||||||
@@ -345,8 +345,16 @@ export function parseListener(cfg, isClient, label) {
|
|||||||
|
|
||||||
/* Transport fields */
|
/* Transport fields */
|
||||||
...(cfg.transport_enabled === '1' ? {
|
...(cfg.transport_enabled === '1' ? {
|
||||||
"grpc-service-name": cfg.transport_grpc_servicename,
|
"grpc-service-name": cfg.transport_type === 'grpc' ? cfg.transport_grpc_servicename : null,
|
||||||
"ws-path": cfg.transport_path
|
"ws-path": cfg.transport_type === 'ws' ? cfg.transport_path : null,
|
||||||
|
"xhttp-config": cfg.transport_type === 'xhttp' ? {
|
||||||
|
path: cfg.transport_path,
|
||||||
|
host: cfg.transport_host,
|
||||||
|
mode: cfg.transport_xhttp_mode,
|
||||||
|
"no-sse-header": strToBool(cfg.transport_xhttp_no_sse_header),
|
||||||
|
"sc-stream-up-server-secs": cfg.transport_xhttp_sc_stream_up_server_secs,
|
||||||
|
"sc-max-each-post-bytes": strToInt(cfg.transport_xhttp_sc_max_each_post_bytes) || null,
|
||||||
|
} : null
|
||||||
} : {})
|
} : {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_VERSION:=0.1.9
|
PKG_VERSION:=0.1.10
|
||||||
PKG_RELEASE:=11
|
PKG_RELEASE:=12
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI support for OpenClaw Launcher
|
LUCI_TITLE:=LuCI support for OpenClaw Launcher
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
|
|||||||
@@ -327,6 +327,73 @@
|
|||||||
color: var(--oclm-primary);
|
color: var(--oclm-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oclm-button-ghost {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--oclm-subtle);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-button-ghost:hover {
|
||||||
|
background: rgba(47, 103, 246, 0.08);
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--oclm-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-button-ghost-danger {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--oclm-danger);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-button-ghost-danger:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
border-color: transparent;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-icon-ghost-button {
|
||||||
|
appearance: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--oclm-subtle);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-icon-ghost-button:hover {
|
||||||
|
background: rgba(47, 103, 246, 0.08);
|
||||||
|
color: var(--oclm-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-icon-ghost-button:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-icon-ghost-danger {
|
||||||
|
color: var(--oclm-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-icon-ghost-danger:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-row-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
.oclm-form-grid {
|
.oclm-form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
@@ -564,6 +631,295 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oclm-security-hero {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-hero-title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-card,
|
||||||
|
.oclm-security-info-card {
|
||||||
|
border: 1px solid var(--oclm-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--oclm-card);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-title-row {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-title-row .oclm-hint {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-title-row h3,
|
||||||
|
.oclm-security-info-card h3,
|
||||||
|
.oclm-dialog-card h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-input-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-input-wrap {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#oclm-security-add {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-control-danger {
|
||||||
|
border-color: rgba(239, 68, 68, 0.7);
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-error {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--oclm-danger);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-list-box {
|
||||||
|
margin-top: 16px;
|
||||||
|
border: 1px solid var(--oclm-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--oclm-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-row + .oclm-security-row {
|
||||||
|
border-top: 1px solid var(--oclm-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-row-main {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-item-path {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-item-path code,
|
||||||
|
.oclm-dialog-code {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(148, 163, 184, 0.14);
|
||||||
|
font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-badge.is-active {
|
||||||
|
background: rgba(18, 185, 129, 0.12);
|
||||||
|
color: var(--oclm-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-badge.is-danger {
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
color: var(--oclm-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-badge.is-muted {
|
||||||
|
background: rgba(148, 163, 184, 0.16);
|
||||||
|
color: var(--oclm-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-item-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px 16px;
|
||||||
|
color: var(--oclm-subtle);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-key {
|
||||||
|
color: var(--oclm-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-row-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-empty {
|
||||||
|
margin-top: 16px;
|
||||||
|
border: 1px dashed var(--oclm-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 28px 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--oclm-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-empty-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-info-card {
|
||||||
|
background: rgba(47, 103, 246, 0.06);
|
||||||
|
border-color: rgba(96, 165, 250, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-info-card h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-info-card ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
color: var(--oclm-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-info-card li + li {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9998;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(15, 23, 42, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-card {
|
||||||
|
width: min(560px, 100%);
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid var(--oclm-border);
|
||||||
|
background: var(--oclm-card);
|
||||||
|
box-shadow: var(--oclm-shadow);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-text {
|
||||||
|
color: var(--oclm-subtle);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-code {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-warning {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.24);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(245, 158, 11, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-warning-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--oclm-text);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-warning ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
color: var(--oclm-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-warning li + li {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-actions {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-option {
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 58px;
|
||||||
|
height: auto;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-option-title {
|
||||||
|
color: var(--oclm-text);
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-option-note {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--oclm-subtle);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-footer .oclm-button {
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
.oclm-banner {
|
.oclm-banner {
|
||||||
color: var(--oclm-danger);
|
color: var(--oclm-danger);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
@@ -646,6 +1002,27 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oclm-security-item-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-input-row,
|
||||||
|
.oclm-security-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-security-row-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-footer {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oclm-dialog-footer .oclm-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.oclm-log-modal {
|
.oclm-log-modal {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var root = container.attachShadow ? (container.shadowRoot || container.attachShadow({ mode: "open" })) : container;
|
var root = container.attachShadow ? (container.shadowRoot || container.attachShadow({ mode: "open" })) : container;
|
||||||
|
|
||||||
|
function defaultProtectedDirectories() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
var state = {
|
var state = {
|
||||||
status: null,
|
status: null,
|
||||||
form: null,
|
form: null,
|
||||||
options: null,
|
options: null,
|
||||||
newOrigin: "",
|
newOrigin: "",
|
||||||
|
security: {
|
||||||
|
runningUser: "openclawmgr",
|
||||||
|
newDirectoryPath: "",
|
||||||
|
directoryError: "",
|
||||||
|
pendingAddPath: "",
|
||||||
|
deleteTargetId: null,
|
||||||
|
deleteTargetPath: "",
|
||||||
|
protectedDirectories: defaultProtectedDirectories()
|
||||||
|
},
|
||||||
activeTab: "basic",
|
activeTab: "basic",
|
||||||
savingSection: "",
|
savingSection: "",
|
||||||
lastAppliedAt: "",
|
lastAppliedAt: "",
|
||||||
@@ -102,60 +116,32 @@
|
|||||||
.replace(/"/g, """);
|
.replace(/"/g, """);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelForAgent(agent) {
|
function defaultModelForAgent(agent) {
|
||||||
return {
|
return {
|
||||||
openai: "openai/gpt-5.2",
|
openai: "gpt-5.2",
|
||||||
anthropic: "anthropic/claude-sonnet-4-6",
|
anthropic: "claude-sonnet-4-6",
|
||||||
"minimax-cn": "minimax-cn/MiniMax-M2.5",
|
"minimax-cn": "MiniMax-M2.5",
|
||||||
moonshot: "moonshot/kimi-k2.5",
|
moonshot: "kimi-k2.5",
|
||||||
"custom-provider": "custom-provider/custom-model"
|
"custom-provider": "custom-model"
|
||||||
}[agent] || "anthropic/claude-sonnet-4-6";
|
}[agent] || "claude-sonnet-4-6";
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelPrefixForAgent(agent) {
|
function normalizeModelValue(value, fallback) {
|
||||||
return {
|
|
||||||
openai: "openai",
|
|
||||||
anthropic: "anthropic",
|
|
||||||
"minimax-cn": "minimax-cn",
|
|
||||||
moonshot: "moonshot",
|
|
||||||
"custom-provider": "custom-provider"
|
|
||||||
}[agent] || "anthropic";
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeModelValue(agent, value) {
|
|
||||||
var prefix = modelPrefixForAgent(agent);
|
|
||||||
value = String(value || "").replace(/^\s+|\s+$/g, "");
|
value = String(value || "").replace(/^\s+|\s+$/g, "");
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return modelForAgent(agent);
|
return String(fallback || "");
|
||||||
}
|
}
|
||||||
if (value.indexOf(prefix + "/") === 0) {
|
return value.replace(/^[^/]+\//, "");
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (/^[^/]+\/.+/.test(value)) {
|
|
||||||
return prefix + "/" + value.replace(/^[^/]+\//, "");
|
|
||||||
}
|
|
||||||
return prefix + "/" + value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function modelMatchesAgent(agent, model) {
|
|
||||||
model = String(model || "");
|
|
||||||
return model.indexOf(modelPrefixForAgent(agent) + "/") === 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveModelValue(form) {
|
function resolveModelValue(form) {
|
||||||
var agent = form && form.default_agent ? form.default_agent : "anthropic";
|
var agent = form && form.default_agent ? form.default_agent : "anthropic";
|
||||||
var value = form && form.default_model ? String(form.default_model) : "";
|
var value = form && form.default_model ? String(form.default_model) : "";
|
||||||
if (!value || !modelMatchesAgent(agent, value)) {
|
return normalizeModelValue(value, defaultModelForAgent(agent));
|
||||||
return normalizeModelValue(agent, value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelPlaceholder(agent) {
|
function modelPlaceholder() {
|
||||||
if (agent === "custom-provider") {
|
return "这里只填写模型名;例如 gpt-5.1";
|
||||||
return "例如 gpt-5.1 或 custom-provider/gpt-5.1";
|
|
||||||
}
|
|
||||||
return "请按照<provider>/<model-id>格式填写";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusText(status) {
|
function statusText(status) {
|
||||||
@@ -181,6 +167,101 @@
|
|||||||
return escapeHtml(value).replace(/'/g, "'");
|
return escapeHtml(value).replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSecurityState() {
|
||||||
|
if (!state.security) {
|
||||||
|
state.security = {
|
||||||
|
runningUser: "openclawmgr",
|
||||||
|
newDirectoryPath: "",
|
||||||
|
directoryError: "",
|
||||||
|
pendingAddPath: "",
|
||||||
|
deleteTargetId: null,
|
||||||
|
deleteTargetPath: "",
|
||||||
|
protectedDirectories: defaultProtectedDirectories()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!Array.isArray(state.security.protectedDirectories)) {
|
||||||
|
state.security.protectedDirectories = defaultProtectedDirectories();
|
||||||
|
}
|
||||||
|
if (state.security.newDirectoryPath == null) {
|
||||||
|
state.security.newDirectoryPath = "";
|
||||||
|
}
|
||||||
|
if (state.security.directoryError == null) {
|
||||||
|
state.security.directoryError = "";
|
||||||
|
}
|
||||||
|
if (state.security.pendingAddPath == null) {
|
||||||
|
state.security.pendingAddPath = "";
|
||||||
|
}
|
||||||
|
if (state.security.deleteTargetPath == null) {
|
||||||
|
state.security.deleteTargetPath = "";
|
||||||
|
}
|
||||||
|
return state.security;
|
||||||
|
}
|
||||||
|
|
||||||
|
function securityDataDirectory() {
|
||||||
|
var formBaseDir = state.form && state.form.base_dir ? String(state.form.base_dir) : "";
|
||||||
|
var statusBaseDir = state.status && state.status.base_dir ? String(state.status.base_dir) : "";
|
||||||
|
var suggestedBaseDir = state.options && state.options.suggested_base_dir ? String(state.options.suggested_base_dir) : "";
|
||||||
|
return formBaseDir || statusBaseDir || suggestedBaseDir || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateProtectedDirectory(path) {
|
||||||
|
var security = getSecurityState();
|
||||||
|
var value = String(path || "").replace(/^\s+|\s+$/g, "");
|
||||||
|
var dataDirectory = securityDataDirectory();
|
||||||
|
if (!value) {
|
||||||
|
return "请输入目录路径";
|
||||||
|
}
|
||||||
|
if (value.charAt(0) !== "/") {
|
||||||
|
return "请输入绝对路径";
|
||||||
|
}
|
||||||
|
if (value === "/") {
|
||||||
|
return "不能添加根目录 /";
|
||||||
|
}
|
||||||
|
if (dataDirectory && (value === dataDirectory || value.indexOf(dataDirectory + "/") === 0)) {
|
||||||
|
return "不能添加 OpenClaw 的运行目录";
|
||||||
|
}
|
||||||
|
if (dataDirectory && dataDirectory.indexOf(value + "/") === 0) {
|
||||||
|
return "不能添加 OpenClaw 运行目录的父目录";
|
||||||
|
}
|
||||||
|
if (security.protectedDirectories.some(function(item) { return item.path === value; })) {
|
||||||
|
return "该目录已在列表中";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function securityProtectionMode(level) {
|
||||||
|
return level === "strict" ? "仅允许 root 访问" : "仅允许 root 和 root 组访问";
|
||||||
|
}
|
||||||
|
|
||||||
|
function securityStatusBadge(status) {
|
||||||
|
var map = {
|
||||||
|
active: ["已生效", "is-active"],
|
||||||
|
inactive: ["未生效", "is-danger"],
|
||||||
|
"not-found": ["目录不存在", "is-muted"],
|
||||||
|
denied: ["已拒绝", "is-active"],
|
||||||
|
"check-failed": ["检测失败", "is-danger"]
|
||||||
|
};
|
||||||
|
var item = map[status] || [status || "未知", "is-muted"];
|
||||||
|
return '<span class="oclm-security-badge ' + item[1] + '">' + escapeHtml(item[0]) + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSecurityData(done) {
|
||||||
|
if (!config.securityDataUrl) {
|
||||||
|
if (typeof done === "function") done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request(config.securityDataUrl).then(function(rv) {
|
||||||
|
var security = getSecurityState();
|
||||||
|
security.protectedDirectories = rv && rv.ok && Array.isArray(rv.items) ? rv.items : [];
|
||||||
|
render();
|
||||||
|
if (typeof done === "function") done(rv);
|
||||||
|
}).catch(function() {
|
||||||
|
getSecurityState().protectedDirectories = [];
|
||||||
|
render();
|
||||||
|
if (typeof done === "function") done(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function maskedTokenUrl(url) {
|
function maskedTokenUrl(url) {
|
||||||
var value = String(url || "");
|
var value = String(url || "");
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -335,6 +416,26 @@
|
|||||||
'</svg>';
|
'</svg>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshIcon(className) {
|
||||||
|
return '' +
|
||||||
|
'<svg class="' + escapeAttr(className || "") + '" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
|
||||||
|
'<path d="M21 12a9 9 0 0 1-15.4 6.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>' +
|
||||||
|
'<path d="M3 12a9 9 0 0 1 15.4-6.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>' +
|
||||||
|
'<path d="M3 4v5h5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path>' +
|
||||||
|
'<path d="M21 20v-5h-5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path>' +
|
||||||
|
'</svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function trashIcon(className) {
|
||||||
|
return '' +
|
||||||
|
'<svg class="' + escapeAttr(className || "") + '" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
|
||||||
|
'<path d="M3 6h18" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>' +
|
||||||
|
'<path d="M8 6V4.5A1.5 1.5 0 0 1 9.5 3h5A1.5 1.5 0 0 1 16 4.5V6" stroke="currentColor" stroke-width="1.8"></path>' +
|
||||||
|
'<path d="M19 6l-1 13a2 2 0 0 1-2 1H8a2 2 0 0 1-2-1L5 6" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"></path>' +
|
||||||
|
'<path d="M10 11v5M14 11v5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>' +
|
||||||
|
'</svg>';
|
||||||
|
}
|
||||||
|
|
||||||
function taskWindowAvailable() {
|
function taskWindowAvailable() {
|
||||||
return !!(window.taskd && window.taskd.show_log);
|
return !!(window.taskd && window.taskd.show_log);
|
||||||
}
|
}
|
||||||
@@ -448,6 +549,7 @@
|
|||||||
var status = state.status || {};
|
var status = state.status || {};
|
||||||
var form = state.form || {};
|
var form = state.form || {};
|
||||||
var options = state.options || { base_dir_choices: [] };
|
var options = state.options || { base_dir_choices: [] };
|
||||||
|
var security = getSecurityState();
|
||||||
var allowedOrigins = Array.isArray(form.allowed_origins) ? form.allowed_origins : [];
|
var allowedOrigins = Array.isArray(form.allowed_origins) ? form.allowed_origins : [];
|
||||||
var baseDirOptions = (options.base_dir_choices || []).map(function(path) {
|
var baseDirOptions = (options.base_dir_choices || []).map(function(path) {
|
||||||
return '<option value="' + escapeHtml(path) + '"></option>';
|
return '<option value="' + escapeHtml(path) + '"></option>';
|
||||||
@@ -472,6 +574,24 @@
|
|||||||
var savingAi = state.savingSection === "ai";
|
var savingAi = state.savingSection === "ai";
|
||||||
var savingAccess = state.savingSection === "access";
|
var savingAccess = state.savingSection === "access";
|
||||||
var noteText = statusNoteText(status);
|
var noteText = statusNoteText(status);
|
||||||
|
var securityList = security.protectedDirectories.map(function(item) {
|
||||||
|
return '' +
|
||||||
|
'<div class="oclm-security-row">' +
|
||||||
|
'<div class="oclm-security-row-main">' +
|
||||||
|
'<div class="oclm-security-item-path"><code>' + escapeHtml(item.path) + '</code>' + securityStatusBadge(item.status) + '</div>' +
|
||||||
|
'<div class="oclm-security-item-grid">' +
|
||||||
|
'<div><span class="oclm-security-key">保护方式:</span>' + escapeHtml(item.protectionMode) + '</div>' +
|
||||||
|
'<div><span class="oclm-security-key">检测结果:</span>' + escapeHtml(item.checkResult) + '</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="oclm-security-row-actions">' +
|
||||||
|
'<button class="oclm-icon-ghost-button" type="button" data-security-recheck="' + escapeAttr(item.id) + '" title="重新检测" aria-label="重新检测">' + refreshIcon("oclm-row-icon") + '</button>' +
|
||||||
|
'<button class="oclm-icon-ghost-button oclm-icon-ghost-danger" type="button" data-security-delete="' + escapeAttr(item.id) + '" title="删除" aria-label="删除">' + trashIcon("oclm-row-icon") + '</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}).join("");
|
||||||
|
var addDialogOpen = !!security.pendingAddPath;
|
||||||
|
var deleteDialogOpen = security.deleteTargetId != null;
|
||||||
|
|
||||||
var canStartService = !status.running && !status.reachable;
|
var canStartService = !status.running && !status.reachable;
|
||||||
var canStopService = !!status.running;
|
var canStopService = !!status.running;
|
||||||
@@ -515,6 +635,7 @@
|
|||||||
'<button class="oclm-tab' + (activeTab === "basic" ? ' is-active' : '') + '" type="button" data-tab="basic">基础配置</button>' +
|
'<button class="oclm-tab' + (activeTab === "basic" ? ' is-active' : '') + '" type="button" data-tab="basic">基础配置</button>' +
|
||||||
'<button class="oclm-tab' + (activeTab === "ai" ? ' is-active' : '') + '" type="button" data-tab="ai">AI配置</button>' +
|
'<button class="oclm-tab' + (activeTab === "ai" ? ' is-active' : '') + '" type="button" data-tab="ai">AI配置</button>' +
|
||||||
'<button class="oclm-tab' + (activeTab === "access" ? ' is-active' : '') + '" type="button" data-tab="access">访问控制</button>' +
|
'<button class="oclm-tab' + (activeTab === "access" ? ' is-active' : '') + '" type="button" data-tab="access">访问控制</button>' +
|
||||||
|
'<button class="oclm-tab' + (activeTab === "security" ? ' is-active' : '') + '" type="button" data-tab="security">安全设置</button>' +
|
||||||
'<button class="oclm-tab' + (activeTab === "version" ? ' is-active' : '') + '" type="button" data-tab="version">版本管理</button>' +
|
'<button class="oclm-tab' + (activeTab === "version" ? ' is-active' : '') + '" type="button" data-tab="version">版本管理</button>' +
|
||||||
'<button class="oclm-tab' + (activeTab === "cleanup" ? ' is-active' : '') + '" type="button" data-tab="cleanup">卸载清理</button>' +
|
'<button class="oclm-tab' + (activeTab === "cleanup" ? ' is-active' : '') + '" type="button" data-tab="cleanup">卸载清理</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
@@ -553,7 +674,7 @@
|
|||||||
: "如果你的供应商兼容 OpenAI 协议,可选择“自定义供应商”并在下方填写地址与模型。") + '</div>') +
|
: "如果你的供应商兼容 OpenAI 协议,可选择“自定义供应商”并在下方填写地址与模型。") + '</div>') +
|
||||||
fieldInput("API 密钥", passwordHtml("oclm-api-key", form.provider_api_key || "", "sk-...")) +
|
fieldInput("API 密钥", passwordHtml("oclm-api-key", form.provider_api_key || "", "sk-...")) +
|
||||||
fieldInput(form.default_agent === "custom-provider" ? "服务地址" : "中转地址(可选)", '<input class="oclm-control" type="text" id="oclm-base-url" value="' + escapeHtml(form.provider_base_url || "") + '" placeholder="https://api.example.com" />') +
|
fieldInput(form.default_agent === "custom-provider" ? "服务地址" : "中转地址(可选)", '<input class="oclm-control" type="text" id="oclm-base-url" value="' + escapeHtml(form.provider_base_url || "") + '" placeholder="https://api.example.com" />') +
|
||||||
fieldInput("默认模型", '<input class="oclm-control" type="text" id="oclm-model" value="' + escapeAttr(resolveModelValue(form)) + '" placeholder="' + escapeAttr(modelPlaceholder(form.default_agent)) + '" /><div class="oclm-hint">将按照“供应商名称/模型名称”的格式保存</div>') +
|
fieldInput("默认模型", '<input class="oclm-control" type="text" id="oclm-model" value="' + escapeAttr(resolveModelValue(form)) + '" placeholder="' + escapeAttr(modelPlaceholder(form.default_agent)) + '" /><div class="oclm-hint">这里只填写模型名;例如 gpt-5.1</div>') +
|
||||||
'<div class="oclm-section-submit"><button class="oclm-button oclm-button-primary" type="button" id="oclm-save-ai"' + (savingAi ? ' disabled' : '') + '>' + (savingAi ? '应用中…' : '保存 AI配置') + '</button>' + (state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') + '</div>' +
|
'<div class="oclm-section-submit"><button class="oclm-button oclm-button-primary" type="button" id="oclm-save-ai"' + (savingAi ? ' disabled' : '') + '>' + (savingAi ? '应用中…' : '保存 AI配置') + '</button>' + (state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') + '</div>' +
|
||||||
'</div></div>' +
|
'</div></div>' +
|
||||||
|
|
||||||
@@ -569,6 +690,37 @@
|
|||||||
'<div class="oclm-section-submit"><button class="oclm-button oclm-button-primary" type="button" id="oclm-save-access"' + (savingAccess ? ' disabled' : '') + '>' + (savingAccess ? '应用中…' : '保存访问控制设置') + '</button>' + (state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') + '</div>' +
|
'<div class="oclm-section-submit"><button class="oclm-button oclm-button-primary" type="button" id="oclm-save-access"' + (savingAccess ? ' disabled' : '') + '>' + (savingAccess ? '应用中…' : '保存访问控制设置') + '</button>' + (state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') + '</div>' +
|
||||||
'</div></div>' +
|
'</div></div>' +
|
||||||
|
|
||||||
|
'<div class="' + (activeTab === "security" ? '' : 'oclm-hidden') + '">' +
|
||||||
|
'<div class="oclm-security-hero">' +
|
||||||
|
'<h3 class="oclm-security-hero-title">🔒 禁止访问目录</h3>' +
|
||||||
|
'<div class="oclm-hint">在 OpenClaw 以 ' + escapeHtml(security.runningUser) + ' 用户运行时禁止读取本地敏感目录。可能导致影响其他程序访问,请谨慎使用。</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="oclm-security-stack">' +
|
||||||
|
'<div class="oclm-security-card">' +
|
||||||
|
'<div class="oclm-security-input-row">' +
|
||||||
|
'<div class="oclm-security-input-wrap">' +
|
||||||
|
'<input class="oclm-control' + (security.directoryError ? ' oclm-control-danger' : '') + '" type="text" id="oclm-security-path" value="' + escapeAttr(security.newDirectoryPath || "") + '" placeholder="/mnt/vio3-1/no-claw" />' +
|
||||||
|
'<div id="oclm-security-error" class="oclm-security-error' + (security.directoryError ? '' : ' oclm-hidden') + '">' + escapeHtml(security.directoryError || "") + '</div>' +
|
||||||
|
'<div class="oclm-hint">请输入绝对路径。不建议添加整个磁盘根目录或 OpenClaw 自身工作目录。</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<button class="oclm-button oclm-button-primary" type="button" id="oclm-security-add">添加目录</button>' +
|
||||||
|
'</div>' +
|
||||||
|
(security.protectedDirectories.length
|
||||||
|
? '<div class="oclm-security-list-box"><div class="oclm-security-list">' + securityList + '</div></div>'
|
||||||
|
: '<div class="oclm-security-empty"><div class="oclm-security-empty-icon">🔒</div><div>暂无受保护的目录</div><div class="oclm-hint">添加目录以限制 OpenClaw 访问</div></div>') +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="oclm-security-info-card">' +
|
||||||
|
'<h3>ℹ 使用说明</h3>' +
|
||||||
|
'<ul>' +
|
||||||
|
'<li>本功能通过 Linux 文件权限限制 openclawmgr 用户访问目录。</li>' +
|
||||||
|
'<li>本功能不是容器或沙箱隔离。</li>' +
|
||||||
|
'<li>修改目录权限后,可能影响其他服务或用户访问这些目录。</li>' +
|
||||||
|
'<li>请勿将 OpenClaw 的数据目录、程序目录加入禁止访问列表。</li>' +
|
||||||
|
'</ul>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
|
||||||
'<div class="' + (activeTab === "version" ? '' : 'oclm-hidden') + '">' +
|
'<div class="' + (activeTab === "version" ? '' : 'oclm-hidden') + '">' +
|
||||||
'<div class="oclm-section-heading"><h2>版本管理</h2><div class="oclm-hint">openclaw 官方正在快速迭代,最新版本可能不兼容旧版本配置导致无法启动。请<a class="oclm-inline-link" href="https://www.koolcenter.com/t/topic/19042" target="_blank" rel="noreferrer noopener">加入交流群</a>了解最新稳定版本号</div></div>' +
|
'<div class="oclm-section-heading"><h2>版本管理</h2><div class="oclm-hint">openclaw 官方正在快速迭代,最新版本可能不兼容旧版本配置导致无法启动。请<a class="oclm-inline-link" href="https://www.koolcenter.com/t/topic/19042" target="_blank" rel="noreferrer noopener">加入交流群</a>了解最新稳定版本号</div></div>' +
|
||||||
'<div class="oclm-form-grid">' +
|
'<div class="oclm-form-grid">' +
|
||||||
@@ -594,6 +746,41 @@
|
|||||||
'<button class="oclm-button oclm-button-danger" type="button" data-op="purge">彻底清理</button>' +
|
'<button class="oclm-button oclm-button-danger" type="button" data-op="purge">彻底清理</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
'<div class="oclm-dialog' + (deleteDialogOpen ? '' : ' oclm-hidden') + '">' +
|
||||||
|
'<div class="oclm-dialog-card">' +
|
||||||
|
'<h3>删除禁止访问目录</h3>' +
|
||||||
|
'<div class="oclm-dialog-text">确定要从保护列表中移除以下目录吗?</div>' +
|
||||||
|
'<code class="oclm-dialog-code">' + escapeHtml(security.deleteTargetPath || "") + '</code>' +
|
||||||
|
'<div class="oclm-dialog-text">请选择删除方式:</div>' +
|
||||||
|
'<div class="oclm-dialog-actions">' +
|
||||||
|
'<button class="oclm-button oclm-dialog-option" type="button" id="oclm-security-delete-direct"><span class="oclm-dialog-option-title">直接删除</span><span class="oclm-dialog-option-note">仅从列表移除,不改变目录权限</span></button>' +
|
||||||
|
'<button class="oclm-button oclm-dialog-option" type="button" id="oclm-security-delete-restore"><span class="oclm-dialog-option-title">恢复其他用户组可访问(drwxr-xr-x)</span><span class="oclm-dialog-option-note">移除并恢复权限</span></button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="oclm-dialog-footer">' +
|
||||||
|
'<button class="oclm-button oclm-button-danger" type="button" id="oclm-security-delete-cancel">取消</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="oclm-dialog' + (addDialogOpen ? '' : ' oclm-hidden') + '">' +
|
||||||
|
'<div class="oclm-dialog-card">' +
|
||||||
|
'<h3>确认添加禁止访问目录</h3>' +
|
||||||
|
'<div class="oclm-dialog-text">添加后将尝试修改该目录权限,限制 OpenClaw 对其访问:</div>' +
|
||||||
|
'<code class="oclm-dialog-code">' + escapeHtml(security.pendingAddPath || "") + '</code>' +
|
||||||
|
'<div class="oclm-dialog-warning">' +
|
||||||
|
'<div class="oclm-dialog-warning-title">请确认你已知晓以下风险:</div>' +
|
||||||
|
'<ul>' +
|
||||||
|
'<li>该目录的 Linux 权限会被修改为仅允许 root 和 root 组访问。</li>' +
|
||||||
|
'<li>这可能影响其他服务、用户或脚本对该目录的正常访问。</li>' +
|
||||||
|
'<li>本功能不是容器或沙箱隔离,只是基于系统文件权限限制。</li>' +
|
||||||
|
'<li>请勿将 OpenClaw 自身的数据目录、程序目录加入禁止访问列表。</li>' +
|
||||||
|
'</ul>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="oclm-dialog-footer">' +
|
||||||
|
'<button class="oclm-button" type="button" id="oclm-security-add-cancel">取消</button>' +
|
||||||
|
'<button class="oclm-button oclm-button-danger" type="button" id="oclm-security-add-confirm">确认添加</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
'</section>' +
|
'</section>' +
|
||||||
'</div></div></div>';
|
'</div></div></div>';
|
||||||
|
|
||||||
@@ -821,6 +1008,10 @@
|
|||||||
|
|
||||||
var newOriginEl = getEl("oclm-new-origin");
|
var newOriginEl = getEl("oclm-new-origin");
|
||||||
if (newOriginEl) state.newOrigin = newOriginEl.value;
|
if (newOriginEl) state.newOrigin = newOriginEl.value;
|
||||||
|
|
||||||
|
var securityPathEl = getEl("oclm-security-path");
|
||||||
|
if (securityPathEl) getSecurityState().newDirectoryPath = securityPathEl.value;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
@@ -1060,11 +1251,11 @@
|
|||||||
if (agent) {
|
if (agent) {
|
||||||
agent.onchange = function() {
|
agent.onchange = function() {
|
||||||
syncDraftFromDom();
|
syncDraftFromDom();
|
||||||
state.form.default_agent = agent.value;
|
state.form.default_agent = agent.value;
|
||||||
state.form.default_model = modelForAgent(agent.value);
|
state.form.default_model = defaultModelForAgent(agent.value);
|
||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiKey = root.getElementById("oclm-api-key");
|
var apiKey = root.getElementById("oclm-api-key");
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
@@ -1125,6 +1316,158 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var securityPath = root.getElementById("oclm-security-path");
|
||||||
|
if (securityPath) {
|
||||||
|
securityPath.oninput = function() {
|
||||||
|
var security = getSecurityState();
|
||||||
|
security.newDirectoryPath = securityPath.value;
|
||||||
|
if (security.directoryError) {
|
||||||
|
security.directoryError = "";
|
||||||
|
securityPath.classList.remove("oclm-control-danger");
|
||||||
|
var errorEl = root.getElementById("oclm-security-error");
|
||||||
|
if (errorEl) {
|
||||||
|
errorEl.textContent = "";
|
||||||
|
errorEl.classList.add("oclm-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var securityAdd = root.getElementById("oclm-security-add");
|
||||||
|
if (securityAdd) {
|
||||||
|
securityAdd.onclick = function() {
|
||||||
|
var security = getSecurityState();
|
||||||
|
var path = securityPath && securityPath.value ? securityPath.value.replace(/^\s+|\s+$/g, "") : "";
|
||||||
|
var error = validateProtectedDirectory(path);
|
||||||
|
if (error) {
|
||||||
|
security.directoryError = error;
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
security.pendingAddPath = path;
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var securityAddCancel = root.getElementById("oclm-security-add-cancel");
|
||||||
|
if (securityAddCancel) {
|
||||||
|
securityAddCancel.onclick = function() {
|
||||||
|
var security = getSecurityState();
|
||||||
|
security.pendingAddPath = "";
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var securityAddConfirm = root.getElementById("oclm-security-add-confirm");
|
||||||
|
if (securityAddConfirm) {
|
||||||
|
securityAddConfirm.onclick = function() {
|
||||||
|
var security = getSecurityState();
|
||||||
|
var path = security.pendingAddPath || "";
|
||||||
|
if (!path) {
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postJson(config.securityAddUrl, { path: path }).then(function(rv) {
|
||||||
|
if (!rv || !rv.ok) {
|
||||||
|
security.pendingAddPath = "";
|
||||||
|
security.directoryError = (rv && rv.error) || "添加目录失败";
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
security.pendingAddPath = "";
|
||||||
|
security.newDirectoryPath = "";
|
||||||
|
security.directoryError = "";
|
||||||
|
loadSecurityData();
|
||||||
|
}).catch(function() {
|
||||||
|
security.pendingAddPath = "";
|
||||||
|
security.directoryError = "添加目录失败";
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(root.querySelectorAll("[data-security-recheck]"), function(el) {
|
||||||
|
el.onclick = function() {
|
||||||
|
var id = el.getAttribute("data-security-recheck");
|
||||||
|
if (!id) return;
|
||||||
|
postJson(config.securityRecheckUrl, { id: id }).then(function(rv) {
|
||||||
|
if (!rv || !rv.ok) {
|
||||||
|
window.alert((rv && rv.error) || "重新检测失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadSecurityData();
|
||||||
|
}).catch(function() {
|
||||||
|
window.alert("重新检测失败");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(root.querySelectorAll("[data-security-delete]"), function(el) {
|
||||||
|
el.onclick = function() {
|
||||||
|
var id = el.getAttribute("data-security-delete");
|
||||||
|
if (!id) return;
|
||||||
|
var security = getSecurityState();
|
||||||
|
var target = null;
|
||||||
|
security.protectedDirectories.forEach(function(item) {
|
||||||
|
if (item.id === id) {
|
||||||
|
target = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!target) return;
|
||||||
|
security.deleteTargetId = id;
|
||||||
|
security.deleteTargetPath = target.path;
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var securityDeleteDirect = root.getElementById("oclm-security-delete-direct");
|
||||||
|
if (securityDeleteDirect) {
|
||||||
|
securityDeleteDirect.onclick = function() {
|
||||||
|
var security = getSecurityState();
|
||||||
|
if (security.deleteTargetId == null) return;
|
||||||
|
postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: "direct" }).then(function(rv) {
|
||||||
|
if (!rv || !rv.ok) {
|
||||||
|
window.alert((rv && rv.error) || "删除失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
security.deleteTargetId = null;
|
||||||
|
security.deleteTargetPath = "";
|
||||||
|
loadSecurityData();
|
||||||
|
}).catch(function() {
|
||||||
|
window.alert("删除失败");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var securityDeleteRestore = root.getElementById("oclm-security-delete-restore");
|
||||||
|
if (securityDeleteRestore) {
|
||||||
|
securityDeleteRestore.onclick = function() {
|
||||||
|
var security = getSecurityState();
|
||||||
|
if (security.deleteTargetId == null) return;
|
||||||
|
postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: "restore" }).then(function(rv) {
|
||||||
|
if (!rv || !rv.ok) {
|
||||||
|
window.alert((rv && rv.error) || "恢复并删除失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
security.deleteTargetId = null;
|
||||||
|
security.deleteTargetPath = "";
|
||||||
|
loadSecurityData();
|
||||||
|
}).catch(function() {
|
||||||
|
window.alert("恢复并删除失败");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var securityDeleteCancel = root.getElementById("oclm-security-delete-cancel");
|
||||||
|
if (securityDeleteCancel) {
|
||||||
|
securityDeleteCancel.onclick = function() {
|
||||||
|
var security = getSecurityState();
|
||||||
|
security.deleteTargetId = null;
|
||||||
|
security.deleteTargetPath = "";
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var saveBasic = root.getElementById("oclm-save-basic");
|
var saveBasic = root.getElementById("oclm-save-basic");
|
||||||
if (saveBasic) {
|
if (saveBasic) {
|
||||||
saveBasic.onclick = function() {
|
saveBasic.onclick = function() {
|
||||||
@@ -1148,7 +1491,7 @@
|
|||||||
if (saveAi) {
|
if (saveAi) {
|
||||||
saveAi.onclick = function() {
|
saveAi.onclick = function() {
|
||||||
var agentValue = root.getElementById("oclm-agent").value;
|
var agentValue = root.getElementById("oclm-agent").value;
|
||||||
var modelValue = normalizeModelValue(agentValue, root.getElementById("oclm-model").value);
|
var modelValue = normalizeModelValue(root.getElementById("oclm-model").value, defaultModelForAgent(agentValue));
|
||||||
var baseUrlValue = root.getElementById("oclm-base-url").value;
|
var baseUrlValue = root.getElementById("oclm-base-url").value;
|
||||||
if (agentValue === "custom-provider" && !String(baseUrlValue || "").replace(/^\s+|\s+$/g, "")) {
|
if (agentValue === "custom-provider" && !String(baseUrlValue || "").replace(/^\s+|\s+$/g, "")) {
|
||||||
window.alert("自定义供应商必须填写服务地址");
|
window.alert("自定义供应商必须填写服务地址");
|
||||||
@@ -1282,11 +1625,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
|
loadSecurityData();
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
}).catch(function() {
|
}).catch(function() {
|
||||||
state.form = state.form || {};
|
state.form = state.form || {};
|
||||||
state.options = state.options || { base_dir_choices: [] };
|
state.options = state.options || { base_dir_choices: [] };
|
||||||
render();
|
render();
|
||||||
|
loadSecurityData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ function index()
|
|||||||
entry({"admin", "services", "openclawmgr", "check_update"}, call("action_check_update")).leaf = true
|
entry({"admin", "services", "openclawmgr", "check_update"}, call("action_check_update")).leaf = true
|
||||||
entry({"admin", "services", "openclawmgr", "config_data"}, call("action_config_data")).leaf = true
|
entry({"admin", "services", "openclawmgr", "config_data"}, call("action_config_data")).leaf = true
|
||||||
entry({"admin", "services", "openclawmgr", "apply_config"}, call("action_apply_config")).leaf = true
|
entry({"admin", "services", "openclawmgr", "apply_config"}, call("action_apply_config")).leaf = true
|
||||||
|
entry({"admin", "services", "openclawmgr", "security_data"}, call("action_security_data")).leaf = true
|
||||||
|
entry({"admin", "services", "openclawmgr", "security_add"}, call("action_security_add")).leaf = true
|
||||||
|
entry({"admin", "services", "openclawmgr", "security_remove"}, call("action_security_remove")).leaf = true
|
||||||
|
entry({"admin", "services", "openclawmgr", "security_recheck"}, call("action_security_recheck")).leaf = true
|
||||||
|
|
||||||
entry({"admin", "services", "openclawmgr", "logs_api"}, call("action_logs")).leaf = true
|
entry({"admin", "services", "openclawmgr", "logs_api"}, call("action_logs")).leaf = true
|
||||||
|
|
||||||
@@ -68,6 +72,51 @@ local function write_json(obj)
|
|||||||
http.write_json(obj)
|
http.write_json(obj)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function read_json_file(path)
|
||||||
|
local jsonc = require "luci.jsonc"
|
||||||
|
local f = io.open(path, "r")
|
||||||
|
if not f then return nil end
|
||||||
|
local raw = f:read("*a")
|
||||||
|
f:close()
|
||||||
|
if not raw or raw == "" then return nil end
|
||||||
|
return jsonc.parse(raw)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function openclaw_config_path(base_dir)
|
||||||
|
base_dir = tostring(base_dir or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
if base_dir == "" then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
return base_dir .. "/data/.openclaw/openclaw.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_runtime_gateway_config(base_dir, fallback)
|
||||||
|
fallback = fallback or {}
|
||||||
|
local cfg = read_json_file(openclaw_config_path(base_dir)) or {}
|
||||||
|
local gateway = cfg.gateway or {}
|
||||||
|
local auth = gateway.auth or {}
|
||||||
|
local port = tostring(gateway.port or fallback.port or "18789")
|
||||||
|
if not port:match("^%d+$") then
|
||||||
|
port = tostring(fallback.port or "18789")
|
||||||
|
end
|
||||||
|
if not port:match("^%d+$") then
|
||||||
|
port = "18789"
|
||||||
|
end
|
||||||
|
local bind = tostring(gateway.bind or fallback.bind or "lan")
|
||||||
|
if bind ~= "loopback" and bind ~= "lan" and bind ~= "auto" and bind ~= "tailnet" and bind ~= "custom" then
|
||||||
|
bind = tostring(fallback.bind or "lan")
|
||||||
|
if bind ~= "loopback" and bind ~= "lan" and bind ~= "auto" and bind ~= "tailnet" and bind ~= "custom" then
|
||||||
|
bind = "lan"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local token = tostring(auth.token or fallback.token or "")
|
||||||
|
return {
|
||||||
|
port = port,
|
||||||
|
bind = bind,
|
||||||
|
token = token,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
local function read_json_body()
|
local function read_json_body()
|
||||||
local http = require "luci.http"
|
local http = require "luci.http"
|
||||||
local jsonc = require "luci.jsonc"
|
local jsonc = require "luci.jsonc"
|
||||||
@@ -273,6 +322,213 @@ local function get_host()
|
|||||||
return host
|
return host
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function security_config_path(base_dir)
|
||||||
|
base_dir = trim(base_dir)
|
||||||
|
if base_dir == "" then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
return base_dir .. "/data/.openclawmgr/openclawmgr-security.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function load_security_config(base_dir)
|
||||||
|
local data = read_json_file(security_config_path(base_dir))
|
||||||
|
if type(data) ~= "table" or type(data.items) ~= "table" then
|
||||||
|
return { items = {} }
|
||||||
|
end
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
local function save_security_config(base_dir, data)
|
||||||
|
local jsonc = require "luci.jsonc"
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local util = require "luci.util"
|
||||||
|
local path = security_config_path(base_dir)
|
||||||
|
local dir = base_dir .. "/data/.openclawmgr"
|
||||||
|
local tmp = path .. ".tmp"
|
||||||
|
local f
|
||||||
|
|
||||||
|
if trim(path) == "" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
sys.call("mkdir -p " .. util.shellquote(dir) .. " >/dev/null 2>&1")
|
||||||
|
f = io.open(tmp, "w")
|
||||||
|
if not f then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
f:write(jsonc.stringify(data) or "{\"items\":[]}")
|
||||||
|
f:close()
|
||||||
|
if not os.rename(tmp, path) then
|
||||||
|
os.remove(tmp)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function security_stat(path)
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local util = require "luci.util"
|
||||||
|
local raw = sys.exec("stat -c '%u:%g:%a' " .. util.shellquote(path) .. " 2>/dev/null"):gsub("%s+$", "")
|
||||||
|
local uid, gid, mode = raw:match("^(%d+):(%d+):(%d+)$")
|
||||||
|
if not uid then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
uid = tonumber(uid),
|
||||||
|
gid = tonumber(gid),
|
||||||
|
mode = mode,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function security_path_exists(path)
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local util = require "luci.util"
|
||||||
|
return sys.call("[ -e " .. util.shellquote(path) .. " ] >/dev/null 2>&1") == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function security_path_is_dir(path)
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local util = require "luci.util"
|
||||||
|
return sys.call("[ -d " .. util.shellquote(path) .. " ] >/dev/null 2>&1") == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function security_probe(path)
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local util = require "luci.util"
|
||||||
|
local raw = sys.exec("stat -Lc '%F|%u|%g|%a' " .. util.shellquote(path) .. " 2>/dev/null"):gsub("%s+$", "")
|
||||||
|
local kind, uid, gid, mode = raw:match("^(.-)|(%d+)|(%d+)|(%d+)$")
|
||||||
|
if kind then
|
||||||
|
return {
|
||||||
|
kind = kind,
|
||||||
|
uid = tonumber(uid),
|
||||||
|
gid = tonumber(gid),
|
||||||
|
mode = mode,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if security_path_is_dir(path) then
|
||||||
|
return { kind = "directory" }
|
||||||
|
end
|
||||||
|
if security_path_exists(path) then
|
||||||
|
return { kind = "other" }
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function security_apply(path)
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local util = require "luci.util"
|
||||||
|
local quoted = util.shellquote(path)
|
||||||
|
if sys.call("chown root:root " .. quoted .. " >/dev/null 2>&1") ~= 0 then
|
||||||
|
return false, "chown failed"
|
||||||
|
end
|
||||||
|
if sys.call("chmod 0750 " .. quoted .. " >/dev/null 2>&1") ~= 0 then
|
||||||
|
return false, "chmod failed"
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function security_restore(path, uid, gid, mode)
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local util = require "luci.util"
|
||||||
|
local quoted = util.shellquote(path)
|
||||||
|
if sys.call("chown " .. tostring(uid) .. ":" .. tostring(gid) .. " " .. quoted .. " >/dev/null 2>&1") ~= 0 then
|
||||||
|
return false, "restore chown failed"
|
||||||
|
end
|
||||||
|
if sys.call("chmod " .. tostring(mode) .. " " .. quoted .. " >/dev/null 2>&1") ~= 0 then
|
||||||
|
return false, "restore chmod failed"
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function security_protection_mode()
|
||||||
|
return "仅允许 root 和 root 组访问"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function security_check_item(item)
|
||||||
|
if security_path_is_dir(item.path) then
|
||||||
|
local st = security_stat(item.path)
|
||||||
|
if not st then
|
||||||
|
return {
|
||||||
|
id = tostring(item.id or ""),
|
||||||
|
path = tostring(item.path or ""),
|
||||||
|
protectionMode = security_protection_mode(),
|
||||||
|
status = "check-failed",
|
||||||
|
checkResult = "检测失败",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if st.uid == 0 and st.gid == 0 and (st.mode == "750" or st.mode == "0750") then
|
||||||
|
return {
|
||||||
|
id = tostring(item.id or ""),
|
||||||
|
path = tostring(item.path or ""),
|
||||||
|
protectionMode = security_protection_mode(),
|
||||||
|
status = "active",
|
||||||
|
checkResult = "openclawmgr 无法进入该目录",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
id = tostring(item.id or ""),
|
||||||
|
path = tostring(item.path or ""),
|
||||||
|
protectionMode = security_protection_mode(),
|
||||||
|
status = "inactive",
|
||||||
|
checkResult = "目录权限与预期不一致",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if not security_path_exists(item.path) then
|
||||||
|
return {
|
||||||
|
id = tostring(item.id or ""),
|
||||||
|
path = tostring(item.path or ""),
|
||||||
|
protectionMode = security_protection_mode(),
|
||||||
|
status = "not-found",
|
||||||
|
checkResult = "目录不存在",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
id = tostring(item.id or ""),
|
||||||
|
path = tostring(item.path or ""),
|
||||||
|
protectionMode = security_protection_mode(),
|
||||||
|
status = "check-failed",
|
||||||
|
checkResult = "目标不是目录",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function validate_security_path(path, base_dir, items)
|
||||||
|
path = trim(path)
|
||||||
|
base_dir = trim(base_dir)
|
||||||
|
if path == "" then
|
||||||
|
return false, "请输入目录路径"
|
||||||
|
end
|
||||||
|
if not path:match("^/") then
|
||||||
|
return false, "请输入绝对路径"
|
||||||
|
end
|
||||||
|
if path == "/" then
|
||||||
|
return false, "不能添加根目录 /"
|
||||||
|
end
|
||||||
|
for _, item in ipairs(items or {}) do
|
||||||
|
if tostring(item.path or "") == path then
|
||||||
|
return false, "该目录已在列表中"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if base_dir ~= "" then
|
||||||
|
local esc_base = base_dir:gsub("([^%w])", "%%%1")
|
||||||
|
local esc_path = path:gsub("([^%w])", "%%%1")
|
||||||
|
if path == base_dir or path:match("^" .. esc_base .. "/") then
|
||||||
|
return false, "不能添加 OpenClaw 的运行目录"
|
||||||
|
end
|
||||||
|
if base_dir:match("^" .. esc_path .. "/") then
|
||||||
|
return false, "不能添加 OpenClaw 运行目录的父目录"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function find_security_item(items, id)
|
||||||
|
for index, item in ipairs(items or {}) do
|
||||||
|
if tostring(item.id or "") == tostring(id or "") then
|
||||||
|
return item, index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
function action_logs()
|
function action_logs()
|
||||||
local http = require "luci.http"
|
local http = require "luci.http"
|
||||||
local sys = require "luci.sys"
|
local sys = require "luci.sys"
|
||||||
@@ -472,10 +728,15 @@ function action_status()
|
|||||||
local task = get_task_state("openclawmgr")
|
local task = get_task_state("openclawmgr")
|
||||||
|
|
||||||
local enabled = uci:get("openclawmgr", "main", "enabled") or "0"
|
local enabled = uci:get("openclawmgr", "main", "enabled") or "0"
|
||||||
local port = uci:get("openclawmgr", "main", "port") or "18789"
|
|
||||||
local bind = uci:get("openclawmgr", "main", "bind") or "lan"
|
|
||||||
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
||||||
local token = uci:get("openclawmgr", "main", "token") or ""
|
local runtime_gateway = get_runtime_gateway_config(base_dir, {
|
||||||
|
port = uci:get("openclawmgr", "main", "port") or "18789",
|
||||||
|
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
||||||
|
token = uci:get("openclawmgr", "main", "token") or "",
|
||||||
|
})
|
||||||
|
local port = runtime_gateway.port
|
||||||
|
local bind = runtime_gateway.bind
|
||||||
|
local token = runtime_gateway.token
|
||||||
|
|
||||||
if not port:match("^%d+$") then port = "18789" end
|
if not port:match("^%d+$") then port = "18789" end
|
||||||
|
|
||||||
@@ -549,11 +810,13 @@ end
|
|||||||
function action_ready()
|
function action_ready()
|
||||||
local uci = require "luci.model.uci".cursor()
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
|
||||||
local port = uci:get("openclawmgr", "main", "port") or "18789"
|
|
||||||
local bind = uci:get("openclawmgr", "main", "bind") or "lan"
|
|
||||||
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
||||||
|
local runtime_gateway = get_runtime_gateway_config(base_dir, {
|
||||||
if not port:match("^%d+$") then port = "18789" end
|
port = uci:get("openclawmgr", "main", "port") or "18789",
|
||||||
|
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
||||||
|
})
|
||||||
|
local port = runtime_gateway.port
|
||||||
|
local bind = runtime_gateway.bind
|
||||||
|
|
||||||
if base_dir == "" then
|
if base_dir == "" then
|
||||||
write_json({ ok = true, ready = false, reason = "base_dir missing" })
|
write_json({ ok = true, ready = false, reason = "base_dir missing" })
|
||||||
@@ -654,25 +917,255 @@ end
|
|||||||
function action_config_data()
|
function action_config_data()
|
||||||
local http = require "luci.http"
|
local http = require "luci.http"
|
||||||
local jsonc = require "luci.jsonc"
|
local jsonc = require "luci.jsonc"
|
||||||
|
local sys = require "luci.sys"
|
||||||
|
local util = require "luci.util"
|
||||||
local uci = require "luci.model.uci".cursor()
|
local uci = require "luci.model.uci".cursor()
|
||||||
local model = require "luci.model.openclawmgr"
|
local model = require "luci.model.openclawmgr"
|
||||||
|
|
||||||
local function read_json_file(path)
|
local function write_json_file(path, obj)
|
||||||
local f = io.open(path, "r")
|
local dir = path:match("^(.+)/[^/]+$")
|
||||||
if not f then return nil end
|
if dir and dir ~= "" then
|
||||||
local raw = f:read("*a")
|
sys.call("mkdir -p " .. util.shellquote(dir) .. " >/dev/null 2>&1")
|
||||||
|
end
|
||||||
|
local encoded = jsonc.stringify(obj, true) or "{}"
|
||||||
|
encoded = encoded:gsub("\\/", "/")
|
||||||
|
local f = io.open(path, "w")
|
||||||
|
if not f then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
f:write(encoded)
|
||||||
|
f:write("\n")
|
||||||
f:close()
|
f:close()
|
||||||
if not raw or raw == "" then return nil end
|
return true
|
||||||
return jsonc.parse(raw)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function infer_custom_provider_model(base_dir)
|
local function config_path(base_dir)
|
||||||
|
return openclaw_config_path(base_dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensure_table(parent, key)
|
||||||
|
if type(parent[key]) ~= "table" then
|
||||||
|
parent[key] = {}
|
||||||
|
end
|
||||||
|
return parent[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
local infer_custom_provider_model
|
||||||
|
local display_model_name
|
||||||
|
|
||||||
|
local function env_key_for_agent(agent)
|
||||||
|
if agent == "openai" then return "OPENAI_API_KEY" end
|
||||||
|
if agent == "anthropic" then return "ANTHROPIC_API_KEY" end
|
||||||
|
if agent == "minimax-cn" then return "MINIMAX_API_KEY" end
|
||||||
|
if agent == "moonshot" then return "MOONSHOT_API_KEY" end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function default_model_for_agent(agent, current_custom_model)
|
||||||
|
if agent == "openai" then return "gpt-5.2" end
|
||||||
|
if agent == "anthropic" then return "claude-sonnet-4-6" end
|
||||||
|
if agent == "minimax-cn" then return "MiniMax-M2.5" end
|
||||||
|
if agent == "moonshot" then return "kimi-k2.5" end
|
||||||
|
if agent == "custom-provider" then return trim(current_custom_model) ~= "" and trim(current_custom_model) or "custom-model" end
|
||||||
|
return "claude-sonnet-4-6"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function normalized_model_path(agent, value, current_custom_model)
|
||||||
|
local model_name = display_model_name(value)
|
||||||
|
if model_name == "" then
|
||||||
|
model_name = default_model_for_agent(agent, current_custom_model)
|
||||||
|
end
|
||||||
|
return agent .. "/" .. model_name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function infer_agent_from_cfg(cfg)
|
||||||
|
local primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary
|
||||||
|
if type(primary) == "string" and primary:match("^[^/]+/.+") then
|
||||||
|
return primary:match("^([^/]+)/") or ""
|
||||||
|
end
|
||||||
|
local providers = cfg.models and cfg.models.providers
|
||||||
|
if type(providers) == "table" then
|
||||||
|
for _, agent in ipairs({ "openai", "anthropic", "minimax-cn", "moonshot", "custom-provider" }) do
|
||||||
|
if type(providers[agent]) == "table" then
|
||||||
|
return agent
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_runtime_config(base_dir)
|
||||||
|
local cfg = read_json_file(config_path(base_dir)) or {}
|
||||||
|
local gateway_cfg = get_runtime_gateway_config(base_dir, {
|
||||||
|
port = uci:get("openclawmgr", "main", "port") or "18789",
|
||||||
|
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
||||||
|
token = uci:get("openclawmgr", "main", "token") or "",
|
||||||
|
})
|
||||||
|
local gateway = cfg.gateway or {}
|
||||||
|
local control = gateway.controlUi or {}
|
||||||
|
local auth = gateway.auth or {}
|
||||||
|
local primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary
|
||||||
|
local agent = infer_agent_from_cfg(cfg)
|
||||||
|
if agent == "" then
|
||||||
|
agent = uci:get("openclawmgr", "main", "default_agent") or "anthropic"
|
||||||
|
end
|
||||||
|
|
||||||
|
local default_model = ""
|
||||||
|
if type(primary) == "string" and primary ~= "" then
|
||||||
|
default_model = display_model_name(primary)
|
||||||
|
elseif agent == "custom-provider" then
|
||||||
|
default_model = infer_custom_provider_model(base_dir)
|
||||||
|
else
|
||||||
|
default_model = display_model_name(uci:get("openclawmgr", "main", "default_model") or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
local provider = cfg.models and cfg.models.providers and cfg.models.providers[agent] or {}
|
||||||
|
local env = cfg.env or {}
|
||||||
|
local provider_api_key = ""
|
||||||
|
local env_key = env_key_for_agent(agent)
|
||||||
|
if env_key ~= "" and type(env[env_key]) == "string" then
|
||||||
|
provider_api_key = env[env_key]
|
||||||
|
elseif type(provider.apiKey) == "string" then
|
||||||
|
provider_api_key = provider.apiKey
|
||||||
|
else
|
||||||
|
provider_api_key = uci:get("openclawmgr", "main", "provider_api_key") or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local allowed_origins = {}
|
||||||
|
if type(control.allowedOrigins) == "table" then
|
||||||
|
for _, item in ipairs(control.allowedOrigins) do
|
||||||
|
item = trim(item)
|
||||||
|
if item ~= "" then
|
||||||
|
allowed_origins[#allowed_origins + 1] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif #allowed_origins == 0 then
|
||||||
|
for _, item in ipairs(uci:get_list("openclawmgr", "main", "allowed_origins") or {}) do
|
||||||
|
item = trim(item)
|
||||||
|
if item ~= "" then
|
||||||
|
allowed_origins[#allowed_origins + 1] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local allow_insecure_auth = control.allowInsecureAuth
|
||||||
|
if type(allow_insecure_auth) ~= "boolean" then
|
||||||
|
allow_insecure_auth = (uci:get("openclawmgr", "main", "allow_insecure_auth") or "1") == "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
local disable_device_auth = control.dangerouslyDisableDeviceAuth
|
||||||
|
if type(disable_device_auth) ~= "boolean" then
|
||||||
|
disable_device_auth = (uci:get("openclawmgr", "main", "disable_device_auth") or "1") == "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
local provider_base_url = ""
|
||||||
|
if type(provider.baseUrl) == "string" then
|
||||||
|
provider_base_url = provider.baseUrl
|
||||||
|
else
|
||||||
|
provider_base_url = uci:get("openclawmgr", "main", "provider_base_url") or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
port = gateway_cfg.port,
|
||||||
|
bind = gateway_cfg.bind,
|
||||||
|
token = gateway_cfg.token,
|
||||||
|
allowed_origins = allowed_origins,
|
||||||
|
allow_insecure_auth = allow_insecure_auth,
|
||||||
|
disable_device_auth = disable_device_auth,
|
||||||
|
default_agent = agent,
|
||||||
|
default_model = default_model,
|
||||||
|
provider_api_key = provider_api_key,
|
||||||
|
provider_base_url = provider_base_url,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write_runtime_config(base_dir, service_cfg, runtime_cfg)
|
||||||
|
local path = config_path(base_dir)
|
||||||
|
if path == "" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local cfg = read_json_file(path) or {}
|
||||||
|
local gateway = ensure_table(cfg, "gateway")
|
||||||
|
gateway.mode = "local"
|
||||||
|
gateway.port = tonumber(service_cfg.port) or 18789
|
||||||
|
gateway.bind = service_cfg.bind or "lan"
|
||||||
|
local auth = ensure_table(gateway, "auth")
|
||||||
|
auth.mode = "token"
|
||||||
|
auth.token = runtime_cfg.token or ""
|
||||||
|
local control = ensure_table(gateway, "controlUi")
|
||||||
|
control.enabled = true
|
||||||
|
control.allowedOrigins = runtime_cfg.allowed_origins or {}
|
||||||
|
control.allowInsecureAuth = runtime_cfg.allow_insecure_auth == true
|
||||||
|
control.dangerouslyDisableDeviceAuth = runtime_cfg.disable_device_auth == true
|
||||||
|
control.dangerouslyAllowHostHeaderOriginFallback = service_cfg.bind ~= "loopback"
|
||||||
|
|
||||||
|
local env = type(cfg.env) == "table" and cfg.env or {}
|
||||||
|
for _, key in ipairs({ "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "MINIMAX_API_KEY", "MOONSHOT_API_KEY" }) do
|
||||||
|
env[key] = nil
|
||||||
|
end
|
||||||
|
local env_key = env_key_for_agent(runtime_cfg.default_agent)
|
||||||
|
if env_key ~= "" and trim(runtime_cfg.provider_api_key) ~= "" then
|
||||||
|
env[env_key] = runtime_cfg.provider_api_key
|
||||||
|
end
|
||||||
|
cfg.env = next(env) and env or nil
|
||||||
|
|
||||||
|
local models = ensure_table(cfg, "models")
|
||||||
|
if type(models.mode) ~= "string" or models.mode == "" then
|
||||||
|
models.mode = "merge"
|
||||||
|
end
|
||||||
|
local providers = {}
|
||||||
|
models.providers = providers
|
||||||
|
local provider = {}
|
||||||
|
providers[runtime_cfg.default_agent] = provider
|
||||||
|
local model_name = display_model_name(runtime_cfg.default_model)
|
||||||
|
if model_name == "" then
|
||||||
|
model_name = default_model_for_agent(runtime_cfg.default_agent, infer_custom_provider_model(base_dir))
|
||||||
|
end
|
||||||
|
if runtime_cfg.default_agent == "openai" then
|
||||||
|
provider.api = "openai-completions"
|
||||||
|
provider.baseUrl = trim(runtime_cfg.provider_base_url) ~= "" and runtime_cfg.provider_base_url or "https://api.openai.com/v1"
|
||||||
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
||||||
|
provider.authHeader = nil
|
||||||
|
elseif runtime_cfg.default_agent == "anthropic" then
|
||||||
|
provider.api = "anthropic-messages"
|
||||||
|
provider.baseUrl = trim(runtime_cfg.provider_base_url) ~= "" and runtime_cfg.provider_base_url or "https://api.anthropic.com"
|
||||||
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
||||||
|
provider.authHeader = nil
|
||||||
|
elseif runtime_cfg.default_agent == "minimax-cn" then
|
||||||
|
provider.api = "anthropic-messages"
|
||||||
|
provider.baseUrl = trim(runtime_cfg.provider_base_url) ~= "" and runtime_cfg.provider_base_url or "https://api.minimaxi.com/anthropic"
|
||||||
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
||||||
|
provider.authHeader = true
|
||||||
|
elseif runtime_cfg.default_agent == "moonshot" then
|
||||||
|
provider.api = "openai-completions"
|
||||||
|
provider.baseUrl = trim(runtime_cfg.provider_base_url) ~= "" and runtime_cfg.provider_base_url or "https://api.moonshot.cn/v1"
|
||||||
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
||||||
|
provider.authHeader = nil
|
||||||
|
else
|
||||||
|
provider.api = "openai-completions"
|
||||||
|
provider.baseUrl = runtime_cfg.provider_base_url or ""
|
||||||
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
||||||
|
provider.authHeader = nil
|
||||||
|
end
|
||||||
|
provider.models = {
|
||||||
|
{ id = model_name, name = model_name }
|
||||||
|
}
|
||||||
|
|
||||||
|
local agents = ensure_table(cfg, "agents")
|
||||||
|
local defaults = ensure_table(agents, "defaults")
|
||||||
|
local model_cfg = ensure_table(defaults, "model")
|
||||||
|
model_cfg.primary = normalized_model_path(runtime_cfg.default_agent, model_name, infer_custom_provider_model(base_dir))
|
||||||
|
|
||||||
|
return write_json_file(path, cfg)
|
||||||
|
end
|
||||||
|
|
||||||
|
infer_custom_provider_model = function(base_dir)
|
||||||
base_dir = tostring(base_dir or "")
|
base_dir = tostring(base_dir or "")
|
||||||
if base_dir == "" then return "" end
|
if base_dir == "" then return "" end
|
||||||
local cfg = read_json_file(base_dir .. "/data/.openclaw/openclaw.json") or {}
|
local cfg = read_json_file(base_dir .. "/data/.openclaw/openclaw.json") or {}
|
||||||
local primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary
|
local primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary
|
||||||
if type(primary) == "string" and primary:match("^custom%-provider/.+") then
|
if type(primary) == "string" and primary:match("^custom%-provider/.+") then
|
||||||
return primary
|
return primary:gsub("^[^/]+/", "")
|
||||||
end
|
end
|
||||||
local providers = cfg.models and cfg.models.providers
|
local providers = cfg.models and cfg.models.providers
|
||||||
local custom = providers and providers["custom-provider"]
|
local custom = providers and providers["custom-provider"]
|
||||||
@@ -680,11 +1173,19 @@ function action_config_data()
|
|||||||
local first = type(models) == "table" and models[1] or nil
|
local first = type(models) == "table" and models[1] or nil
|
||||||
local id = first and first.id
|
local id = first and first.id
|
||||||
if type(id) == "string" and id ~= "" then
|
if type(id) == "string" and id ~= "" then
|
||||||
return "custom-provider/" .. id
|
return id
|
||||||
end
|
end
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
display_model_name = function(value)
|
||||||
|
value = tostring(value or "")
|
||||||
|
if value == "" then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
return value:gsub("^[^/]+/", "")
|
||||||
|
end
|
||||||
|
|
||||||
if (http.getenv("REQUEST_METHOD") or "GET") == "POST" then
|
if (http.getenv("REQUEST_METHOD") or "GET") == "POST" then
|
||||||
if not require_csrf() then
|
if not require_csrf() then
|
||||||
return
|
return
|
||||||
@@ -701,7 +1202,8 @@ function action_config_data()
|
|||||||
return body[key] ~= nil
|
return body[key] ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local requested_default_agent = tostring(has("default_agent") and body.default_agent or (uci:get("openclawmgr", section, "default_agent") or "anthropic"))
|
local current = get_runtime_config(uci:get("openclawmgr", section, "base_dir") or "")
|
||||||
|
local requested_default_agent = tostring(has("default_agent") and body.default_agent or current.default_agent or "anthropic")
|
||||||
|
|
||||||
if has("enabled") then
|
if has("enabled") then
|
||||||
uci:set("openclawmgr", section, "enabled", bool_to_uci(body.enabled == true or body.enabled == "1"))
|
uci:set("openclawmgr", section, "enabled", bool_to_uci(body.enabled == true or body.enabled == "1"))
|
||||||
@@ -718,7 +1220,6 @@ function action_config_data()
|
|||||||
write_json({ ok = false, error = "invalid port (must be 1025-65535)" })
|
write_json({ ok = false, error = "invalid port (must be 1025-65535)" })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
uci:set("openclawmgr", section, "port", port)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if has("bind") then
|
if has("bind") then
|
||||||
@@ -727,7 +1228,6 @@ function action_config_data()
|
|||||||
write_json({ ok = false, error = "invalid bind" })
|
write_json({ ok = false, error = "invalid bind" })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
uci:set("openclawmgr", section, "bind", bind)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if has("base_dir") then
|
if has("base_dir") then
|
||||||
@@ -745,21 +1245,12 @@ function action_config_data()
|
|||||||
write_json({ ok = false, error = "invalid default_agent" })
|
write_json({ ok = false, error = "invalid default_agent" })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
uci:set("openclawmgr", section, "default_agent", agent)
|
|
||||||
end
|
|
||||||
|
|
||||||
if has("default_model") then
|
|
||||||
uci:set("openclawmgr", section, "default_model", tostring(body.default_model or ""))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if has("install_accelerated") then
|
if has("install_accelerated") then
|
||||||
uci:set("openclawmgr", section, "install_accelerated", bool_to_uci(body.install_accelerated == true or body.install_accelerated == "1"))
|
uci:set("openclawmgr", section, "install_accelerated", bool_to_uci(body.install_accelerated == true or body.install_accelerated == "1"))
|
||||||
end
|
end
|
||||||
|
|
||||||
if has("provider_api_key") then
|
|
||||||
uci:set("openclawmgr", section, "provider_api_key", tostring(body.provider_api_key or ""))
|
|
||||||
end
|
|
||||||
|
|
||||||
if has("provider_base_url") then
|
if has("provider_base_url") then
|
||||||
local value = tostring(body.provider_base_url or "")
|
local value = tostring(body.provider_base_url or "")
|
||||||
if requested_default_agent == "custom-provider" and value == "" then
|
if requested_default_agent == "custom-provider" and value == "" then
|
||||||
@@ -770,45 +1261,78 @@ function action_config_data()
|
|||||||
write_json({ ok = false, error = "invalid provider_base_url" })
|
write_json({ ok = false, error = "invalid provider_base_url" })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
uci:set("openclawmgr", section, "provider_base_url", value)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if has("token") then
|
if has("token") then
|
||||||
uci:set("openclawmgr", section, "token", tostring(body.token or ""))
|
body.token = tostring(body.token or "")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local effective_base_dir = tostring(has("base_dir") and body.base_dir or (uci:get("openclawmgr", section, "base_dir") or ""))
|
||||||
|
local effective_port = tostring(has("port") and body.port or current.port or "18789")
|
||||||
|
local effective_bind = tostring(has("bind") and body.bind or current.bind or "lan")
|
||||||
|
local runtime_cfg = {
|
||||||
|
token = tostring(has("token") and body.token or current.token or ""),
|
||||||
|
allowed_origins = current.allowed_origins,
|
||||||
|
allow_insecure_auth = current.allow_insecure_auth,
|
||||||
|
disable_device_auth = current.disable_device_auth,
|
||||||
|
default_agent = requested_default_agent,
|
||||||
|
default_model = tostring(has("default_model") and body.default_model or current.default_model or ""),
|
||||||
|
provider_api_key = tostring(has("provider_api_key") and body.provider_api_key or current.provider_api_key or ""),
|
||||||
|
provider_base_url = tostring(has("provider_base_url") and body.provider_base_url or current.provider_base_url or ""),
|
||||||
|
}
|
||||||
|
|
||||||
if has("allowed_origins") then
|
if has("allowed_origins") then
|
||||||
local origins = {}
|
local origins = {}
|
||||||
if type(body.allowed_origins) == "table" then
|
if type(body.allowed_origins) == "table" then
|
||||||
for _, item in ipairs(body.allowed_origins) do
|
for _, item in ipairs(body.allowed_origins) do
|
||||||
item = tostring(item or ""):match("^%s*(.-)%s*$")
|
item = trim(item)
|
||||||
if item and item ~= "" then
|
if item ~= "" then
|
||||||
table.insert(origins, item)
|
origins[#origins + 1] = item
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if #origins > 0 then
|
runtime_cfg.allowed_origins = origins
|
||||||
uci:set_list("openclawmgr", section, "allowed_origins", origins)
|
|
||||||
else
|
|
||||||
uci:delete("openclawmgr", section, "allowed_origins")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if has("allow_insecure_auth") then
|
if has("allow_insecure_auth") then
|
||||||
uci:set("openclawmgr", section, "allow_insecure_auth", bool_to_uci(body.allow_insecure_auth == true or body.allow_insecure_auth == "1"))
|
runtime_cfg.allow_insecure_auth = (body.allow_insecure_auth == true or body.allow_insecure_auth == "1")
|
||||||
end
|
end
|
||||||
|
|
||||||
if has("disable_device_auth") then
|
if has("disable_device_auth") then
|
||||||
uci:set("openclawmgr", section, "disable_device_auth", bool_to_uci(body.disable_device_auth == true or body.disable_device_auth == "1"))
|
runtime_cfg.disable_device_auth = (body.disable_device_auth == true or body.disable_device_auth == "1")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if runtime_cfg.default_agent == "custom-provider" and trim(runtime_cfg.provider_base_url) == "" then
|
||||||
|
write_json({ ok = false, error = "provider_base_url required for custom-provider" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if trim(runtime_cfg.provider_base_url) ~= "" and not tostring(runtime_cfg.provider_base_url):match("^https?://") then
|
||||||
|
write_json({ ok = false, error = "invalid provider_base_url" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
uci:commit("openclawmgr")
|
||||||
|
if effective_base_dir == "" then
|
||||||
|
write_json({ ok = false, error = "base_dir required" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ok = write_runtime_config(effective_base_dir, {
|
||||||
|
port = effective_port,
|
||||||
|
bind = effective_bind,
|
||||||
|
}, runtime_cfg)
|
||||||
|
if not ok then
|
||||||
|
write_json({ ok = false, error = "write openclaw.json failed" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for _, key in ipairs({ "port", "bind", "token", "allowed_origins", "allow_insecure_auth", "disable_device_auth", "default_agent", "default_model", "provider_api_key", "provider_base_url" }) do
|
||||||
|
uci:delete("openclawmgr", section, key)
|
||||||
|
end
|
||||||
uci:commit("openclawmgr")
|
uci:commit("openclawmgr")
|
||||||
write_json({ ok = true })
|
write_json({ ok = true })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
||||||
local port = uci:get("openclawmgr", "main", "port") or "18789"
|
|
||||||
local blocks = model.blocks()
|
local blocks = model.blocks()
|
||||||
local home = model.home()
|
local home = model.home()
|
||||||
local paths, default_path = model.find_paths(blocks, home, "Configs")
|
local paths, default_path = model.find_paths(blocks, home, "Configs")
|
||||||
@@ -830,42 +1354,165 @@ function action_config_data()
|
|||||||
end
|
end
|
||||||
add_choice(default_path or "/root/Configs/OpenClawMgr")
|
add_choice(default_path or "/root/Configs/OpenClawMgr")
|
||||||
|
|
||||||
local allowed_origins = {}
|
local runtime_cfg = get_runtime_config(base_dir)
|
||||||
for _, item in ipairs(uci:get_list("openclawmgr", "main", "allowed_origins") or {}) do
|
|
||||||
allowed_origins[#allowed_origins + 1] = item
|
|
||||||
end
|
|
||||||
|
|
||||||
local current_default_agent = uci:get("openclawmgr", "main", "default_agent") or "anthropic"
|
|
||||||
local current_default_model = uci:get("openclawmgr", "main", "default_model") or ""
|
|
||||||
if current_default_agent == "custom-provider" and current_default_model == "" then
|
|
||||||
current_default_model = infer_custom_provider_model(base_dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
write_json({
|
write_json({
|
||||||
ok = true,
|
ok = true,
|
||||||
config = {
|
config = {
|
||||||
enabled = (uci:get("openclawmgr", "main", "enabled") or "0") == "1",
|
enabled = (uci:get("openclawmgr", "main", "enabled") or "0") == "1",
|
||||||
port = port,
|
port = runtime_cfg.port,
|
||||||
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
bind = runtime_cfg.bind,
|
||||||
base_dir = base_dir,
|
base_dir = base_dir,
|
||||||
token = uci:get("openclawmgr", "main", "token") or "",
|
token = runtime_cfg.token,
|
||||||
allowed_origins = allowed_origins,
|
allowed_origins = runtime_cfg.allowed_origins,
|
||||||
allow_insecure_auth = (uci:get("openclawmgr", "main", "allow_insecure_auth") or "0") == "1",
|
allow_insecure_auth = runtime_cfg.allow_insecure_auth,
|
||||||
disable_device_auth = (uci:get("openclawmgr", "main", "disable_device_auth") or "0") == "1",
|
disable_device_auth = runtime_cfg.disable_device_auth,
|
||||||
default_agent = current_default_agent,
|
default_agent = runtime_cfg.default_agent,
|
||||||
default_model = current_default_model,
|
default_model = runtime_cfg.default_model,
|
||||||
install_accelerated = (uci:get("openclawmgr", "main", "install_accelerated") or "1") == "1",
|
install_accelerated = (uci:get("openclawmgr", "main", "install_accelerated") or "1") == "1",
|
||||||
provider_api_key = uci:get("openclawmgr", "main", "provider_api_key") or "",
|
provider_api_key = runtime_cfg.provider_api_key,
|
||||||
provider_base_url = uci:get("openclawmgr", "main", "provider_base_url") or "",
|
provider_base_url = runtime_cfg.provider_base_url,
|
||||||
},
|
},
|
||||||
options = {
|
options = {
|
||||||
base_dir_choices = choices,
|
base_dir_choices = choices,
|
||||||
suggested_base_dir = default_path or "",
|
suggested_base_dir = default_path or "",
|
||||||
default_origin = default_allowed_origin(port),
|
default_origin = default_allowed_origin(runtime_cfg.port),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function action_security_data()
|
||||||
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
local base_dir = trim(uci:get("openclawmgr", "main", "base_dir") or "")
|
||||||
|
local data, items = nil, {}
|
||||||
|
if base_dir == "" then
|
||||||
|
write_json({ ok = true, items = items })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
data = load_security_config(base_dir)
|
||||||
|
for _, item in ipairs(data.items or {}) do
|
||||||
|
items[#items + 1] = security_check_item(item)
|
||||||
|
end
|
||||||
|
write_json({ ok = true, items = items })
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_security_add()
|
||||||
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
if not require_csrf() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local base_dir = trim(uci:get("openclawmgr", "main", "base_dir") or "")
|
||||||
|
local body = read_json_body() or {}
|
||||||
|
local path = trim(body.path or "")
|
||||||
|
local data, ok, err, item, st, probe = nil, nil, nil, nil, nil, nil
|
||||||
|
if base_dir == "" then
|
||||||
|
write_json({ ok = false, error = "base_dir required" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
data = load_security_config(base_dir)
|
||||||
|
ok, err = validate_security_path(path, base_dir, data.items)
|
||||||
|
if not ok then
|
||||||
|
write_json({ ok = false, error = err })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
item = {
|
||||||
|
id = "dir_" .. tostring(os.time()) .. tostring(math.random(1000, 9999)),
|
||||||
|
path = path,
|
||||||
|
orig_uid = 0,
|
||||||
|
orig_gid = 0,
|
||||||
|
orig_mode = "755",
|
||||||
|
}
|
||||||
|
probe = security_probe(path)
|
||||||
|
if not probe then
|
||||||
|
write_json({ ok = false, error = "目录不存在" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if probe.kind ~= "directory" then
|
||||||
|
write_json({ ok = false, error = "目标不是目录" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
st = security_stat(path)
|
||||||
|
if not st then
|
||||||
|
write_json({ ok = false, error = "读取目录权限失败" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
item.orig_uid = st.uid
|
||||||
|
item.orig_gid = st.gid
|
||||||
|
item.orig_mode = st.mode
|
||||||
|
ok, err = security_apply(path)
|
||||||
|
if not ok then
|
||||||
|
write_json({ ok = false, error = err or "apply failed" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
data.items[#data.items + 1] = item
|
||||||
|
if not save_security_config(base_dir, data) then
|
||||||
|
write_json({ ok = false, error = "save security config failed" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
write_json({ ok = true, item = security_check_item(item) })
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_security_remove()
|
||||||
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
if not require_csrf() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local base_dir = trim(uci:get("openclawmgr", "main", "base_dir") or "")
|
||||||
|
local body = read_json_body() or {}
|
||||||
|
local id = tostring(body.id or "")
|
||||||
|
local mode = tostring(body.mode or "direct")
|
||||||
|
local data, item, index = nil, nil, nil
|
||||||
|
if base_dir == "" then
|
||||||
|
write_json({ ok = false, error = "base_dir required" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if mode ~= "direct" and mode ~= "restore" then
|
||||||
|
write_json({ ok = false, error = "invalid mode" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
data = load_security_config(base_dir)
|
||||||
|
item, index = find_security_item(data.items, id)
|
||||||
|
if not item then
|
||||||
|
write_json({ ok = false, error = "item not found" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if mode == "restore" and security_path_exists(item.path) then
|
||||||
|
local ok, err = security_restore(item.path, item.orig_uid, item.orig_gid, item.orig_mode)
|
||||||
|
if not ok then
|
||||||
|
write_json({ ok = false, error = err or "restore failed" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.remove(data.items, index)
|
||||||
|
if not save_security_config(base_dir, data) then
|
||||||
|
write_json({ ok = false, error = "save security config failed" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
write_json({ ok = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_security_recheck()
|
||||||
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
if not require_csrf() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local base_dir = trim(uci:get("openclawmgr", "main", "base_dir") or "")
|
||||||
|
local body = read_json_body() or {}
|
||||||
|
local id = tostring(body.id or "")
|
||||||
|
local data, item = nil, nil
|
||||||
|
if base_dir == "" then
|
||||||
|
write_json({ ok = false, error = "base_dir required" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
data = load_security_config(base_dir)
|
||||||
|
item = find_security_item(data.items, id)
|
||||||
|
if not item then
|
||||||
|
write_json({ ok = false, error = "item not found" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
write_json({ ok = true, item = security_check_item(item) })
|
||||||
|
end
|
||||||
|
|
||||||
function action_op()
|
function action_op()
|
||||||
local http = require "luci.http"
|
local http = require "luci.http"
|
||||||
local i18n = require "luci.i18n"
|
local i18n = require "luci.i18n"
|
||||||
@@ -980,11 +1627,13 @@ function action_diag_info()
|
|||||||
local uci = require "luci.model.uci".cursor()
|
local uci = require "luci.model.uci".cursor()
|
||||||
|
|
||||||
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
||||||
local port = uci:get("openclawmgr", "main", "port") or "18789"
|
|
||||||
local bind = uci:get("openclawmgr", "main", "bind") or "lan"
|
|
||||||
local enabled = uci:get("openclawmgr", "main", "enabled") or "0"
|
local enabled = uci:get("openclawmgr", "main", "enabled") or "0"
|
||||||
|
local runtime_gateway = get_runtime_gateway_config(base_dir, {
|
||||||
if not port:match("^%d+$") then port = "18789" end
|
port = uci:get("openclawmgr", "main", "port") or "18789",
|
||||||
|
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
||||||
|
})
|
||||||
|
local port = runtime_gateway.port
|
||||||
|
local bind = runtime_gateway.bind
|
||||||
|
|
||||||
local avail_mb = 0
|
local avail_mb = 0
|
||||||
if base_dir ~= "" then
|
if base_dir ~= "" then
|
||||||
|
|||||||
@@ -32,13 +32,14 @@ local data_dir = base_dir .. "/data"
|
|||||||
s = m:section(SimpleSection)
|
s = m:section(SimpleSection)
|
||||||
|
|
||||||
o = s:option(Value, "mode", translate("Mode"))
|
o = s:option(Value, "mode", translate("Mode"))
|
||||||
|
o:value("cli-env", translate("CLI终端 (仅注入环境)"))
|
||||||
o:value("configure", translate("官方配置向导 (openclaw configure)"))
|
o:value("configure", translate("官方配置向导 (openclaw configure)"))
|
||||||
o:value("backup", translate("备份配置"))
|
o:value("backup", translate("备份配置"))
|
||||||
o:value("restore", translate("恢复配置"))
|
o:value("restore", translate("恢复配置"))
|
||||||
o.default = "configure"
|
o.default = "cli-env"
|
||||||
o.forcewrite = true
|
o.forcewrite = true
|
||||||
|
|
||||||
o = s:option(DummyValue, "_tip", translate("CLI Config"))
|
o = s:option(DummyValue, "_tip", translate("CLI终端"))
|
||||||
o.rawhtml = true
|
o.rawhtml = true
|
||||||
o.cfgvalue = function()
|
o.cfgvalue = function()
|
||||||
return translate("Starts a web terminal (LAN only) for common OpenClaw maintenance tasks.")
|
return translate("Starts a web terminal (LAN only) for common OpenClaw maintenance tasks.")
|
||||||
@@ -93,7 +94,7 @@ o.render = function(self, section, scope)
|
|||||||
end
|
end
|
||||||
o.write = function(self, section)
|
o.write = function(self, section)
|
||||||
local mode = m:formvalue("mode") or m:formvalue("cbid.OpenClawCLI.1.mode") or "configure"
|
local mode = m:formvalue("mode") or m:formvalue("cbid.OpenClawCLI.1.mode") or "configure"
|
||||||
if mode ~= "configure" and mode ~= "backup" and mode ~= "restore" then
|
if mode ~= "configure" and mode ~= "cli-env" and mode ~= "backup" and mode ~= "restore" then
|
||||||
mode = "configure"
|
mode = "configure"
|
||||||
end
|
end
|
||||||
start_ttyd(mode)
|
start_ttyd(mode)
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
checkUpdateUrl: "<%=url('admin/services/openclawmgr/check_update')%>",
|
checkUpdateUrl: "<%=url('admin/services/openclawmgr/check_update')%>",
|
||||||
configUrl: "<%=url('admin/services/openclawmgr/config_data')%>",
|
configUrl: "<%=url('admin/services/openclawmgr/config_data')%>",
|
||||||
applyUrl: "<%=url('admin/services/openclawmgr/apply_config')%>",
|
applyUrl: "<%=url('admin/services/openclawmgr/apply_config')%>",
|
||||||
|
securityDataUrl: "<%=url('admin/services/openclawmgr/security_data')%>",
|
||||||
|
securityAddUrl: "<%=url('admin/services/openclawmgr/security_add')%>",
|
||||||
|
securityRemoveUrl: "<%=url('admin/services/openclawmgr/security_remove')%>",
|
||||||
|
securityRecheckUrl: "<%=url('admin/services/openclawmgr/security_recheck')%>",
|
||||||
staticBase: "<%=resource%>/openclawmgr"
|
staticBase: "<%=resource%>/openclawmgr"
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
config openclawmgr 'main'
|
config openclawmgr 'main'
|
||||||
option enabled '0'
|
option enabled '0'
|
||||||
option port '18789'
|
option base_dir ''
|
||||||
option bind 'lan'
|
|
||||||
option token ''
|
|
||||||
option allow_insecure_auth '1'
|
|
||||||
option disable_device_auth '1'
|
|
||||||
option install_accelerated '1'
|
option install_accelerated '1'
|
||||||
option default_agent 'anthropic'
|
|
||||||
option default_model ''
|
|
||||||
option provider_api_key ''
|
|
||||||
option provider_base_url ''
|
|
||||||
@@ -14,16 +14,34 @@ _find_entry() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_json_get() {
|
||||||
|
local file="$1" expr="$2"
|
||||||
|
[ -f "$file" ] || return 0
|
||||||
|
command -v jsonfilter >/dev/null 2>&1 || return 0
|
||||||
|
jsonfilter -i "$file" -e "$expr" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
start_service() {
|
start_service() {
|
||||||
config_load openclawmgr
|
config_load openclawmgr
|
||||||
config_get_bool enabled main enabled 0
|
config_get_bool enabled main enabled 0
|
||||||
config_get port main port 18789
|
|
||||||
config_get bind main bind lan
|
|
||||||
config_get base_dir main base_dir ""
|
config_get base_dir main base_dir ""
|
||||||
config_get token main token ""
|
local port="18789"
|
||||||
|
local bind="lan"
|
||||||
|
local token=""
|
||||||
|
|
||||||
[ "$enabled" -eq 1 ] || return 0
|
[ "$enabled" -eq 1 ] || return 0
|
||||||
[ -n "$base_dir" ] || return 1
|
[ -n "$base_dir" ] || return 1
|
||||||
|
|
||||||
|
local data_dir="${base_dir}/data"
|
||||||
|
local config_file="${data_dir}/.openclaw/openclaw.json"
|
||||||
|
local json_port json_bind json_token
|
||||||
|
json_port="$(_json_get "$config_file" '@.gateway.port')"
|
||||||
|
json_bind="$(_json_get "$config_file" '@.gateway.bind')"
|
||||||
|
json_token="$(_json_get "$config_file" '@.gateway.auth.token')"
|
||||||
|
[ -n "$json_port" ] && port="$json_port"
|
||||||
|
[ -n "$json_bind" ] && bind="$json_bind"
|
||||||
|
[ -n "$json_token" ] && token="$json_token"
|
||||||
|
|
||||||
if echo "$port" | grep -Eq '^[0-9]+$'; then
|
if echo "$port" | grep -Eq '^[0-9]+$'; then
|
||||||
if [ "$port" -le 1024 ] 2>/dev/null; then
|
if [ "$port" -le 1024 ] 2>/dev/null; then
|
||||||
logger -t openclawmgr "Refusing to start: unsafe port ${port} (must be >1024)"
|
logger -t openclawmgr "Refusing to start: unsafe port ${port} (must be >1024)"
|
||||||
@@ -35,7 +53,6 @@ start_service() {
|
|||||||
|
|
||||||
local node_bin="${base_dir}/node/bin/node"
|
local node_bin="${base_dir}/node/bin/node"
|
||||||
local global_dir="${base_dir}/global"
|
local global_dir="${base_dir}/global"
|
||||||
local data_dir="${base_dir}/data"
|
|
||||||
local env_file="${data_dir}/.openclaw/openclaw.env"
|
local env_file="${data_dir}/.openclaw/openclaw.env"
|
||||||
local entry="$(_find_entry "$global_dir")"
|
local entry="$(_find_entry "$global_dir")"
|
||||||
local openai_api_key=""
|
local openai_api_key=""
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
default_base_dir() {
|
||||||
|
local main_dir conf_dir
|
||||||
|
|
||||||
|
main_dir="$(uci -q get quickstart.main.main_dir 2>/dev/null || true)"
|
||||||
|
conf_dir="$(uci -q get quickstart.main.conf_dir 2>/dev/null || true)"
|
||||||
|
|
||||||
|
[ -n "$main_dir" ] || main_dir="/root"
|
||||||
|
[ -n "$conf_dir" ] || conf_dir="${main_dir%/}/Configs"
|
||||||
|
|
||||||
|
printf '%s/OpenClawMgr\n' "${conf_dir%/}"
|
||||||
|
}
|
||||||
|
|
||||||
# Ensure section exists
|
# Ensure section exists
|
||||||
if ! uci -q get openclawmgr.main >/dev/null 2>&1; then
|
if ! uci -q get openclawmgr.main >/dev/null 2>&1; then
|
||||||
uci -q set openclawmgr.main=openclawmgr >/dev/null 2>&1 || true
|
uci -q set openclawmgr.main=openclawmgr >/dev/null 2>&1 || true
|
||||||
@@ -7,16 +19,8 @@ fi
|
|||||||
|
|
||||||
# Fill defaults (only if missing)
|
# Fill defaults (only if missing)
|
||||||
[ -n "$(uci -q get openclawmgr.main.enabled 2>/dev/null)" ] || uci -q set openclawmgr.main.enabled='0'
|
[ -n "$(uci -q get openclawmgr.main.enabled 2>/dev/null)" ] || uci -q set openclawmgr.main.enabled='0'
|
||||||
[ -n "$(uci -q get openclawmgr.main.port 2>/dev/null)" ] || uci -q set openclawmgr.main.port='18789'
|
[ -n "$(uci -q get openclawmgr.main.base_dir 2>/dev/null)" ] || uci -q set openclawmgr.main.base_dir="$(default_base_dir)"
|
||||||
[ -n "$(uci -q get openclawmgr.main.bind 2>/dev/null)" ] || uci -q set openclawmgr.main.bind='lan'
|
|
||||||
[ -n "$(uci -q get openclawmgr.main.install_accelerated 2>/dev/null)" ] || uci -q set openclawmgr.main.install_accelerated='1'
|
[ -n "$(uci -q get openclawmgr.main.install_accelerated 2>/dev/null)" ] || uci -q set openclawmgr.main.install_accelerated='1'
|
||||||
[ -n "$(uci -q get openclawmgr.main.token 2>/dev/null)" ] || uci -q set openclawmgr.main.token=''
|
|
||||||
[ -n "$(uci -q get openclawmgr.main.allow_insecure_auth 2>/dev/null)" ] || uci -q set openclawmgr.main.allow_insecure_auth='1'
|
|
||||||
[ -n "$(uci -q get openclawmgr.main.disable_device_auth 2>/dev/null)" ] || uci -q set openclawmgr.main.disable_device_auth='1'
|
|
||||||
[ -n "$(uci -q get openclawmgr.main.default_agent 2>/dev/null)" ] || uci -q set openclawmgr.main.default_agent='anthropic'
|
|
||||||
[ -n "$(uci -q get openclawmgr.main.default_model 2>/dev/null)" ] || uci -q set openclawmgr.main.default_model=''
|
|
||||||
[ -n "$(uci -q get openclawmgr.main.provider_api_key 2>/dev/null)" ] || uci -q set openclawmgr.main.provider_api_key=''
|
|
||||||
[ -n "$(uci -q get openclawmgr.main.provider_base_url 2>/dev/null)" ] || uci -q set openclawmgr.main.provider_base_url=''
|
|
||||||
|
|
||||||
uci -q commit openclawmgr >/dev/null 2>&1 || true
|
uci -q commit openclawmgr >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,25 @@ uci_get_list() {
|
|||||||
sed -n "s/^${UCI_NS}\\.main\\.${key}='\\(.*\\)'$/\\1/p"
|
sed -n "s/^${UCI_NS}\\.main\\.${key}='\\(.*\\)'$/\\1/p"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json_get() {
|
||||||
|
local file="$1" expr="$2"
|
||||||
|
[ -f "$file" ] || return 0
|
||||||
|
command -v jsonfilter >/dev/null 2>&1 || return 0
|
||||||
|
jsonfilter -i "$file" -e "$expr" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
load_gateway_runtime_from_json() {
|
||||||
|
local cfg="${DATA_DIR}/.openclaw/openclaw.json"
|
||||||
|
[ -f "$cfg" ] || return 0
|
||||||
|
local json_port json_bind json_token
|
||||||
|
json_port="$(json_get "$cfg" '@.gateway.port')"
|
||||||
|
json_bind="$(json_get "$cfg" '@.gateway.bind')"
|
||||||
|
json_token="$(json_get "$cfg" '@.gateway.auth.token')"
|
||||||
|
[ -n "$json_port" ] && PORT="$json_port"
|
||||||
|
[ -n "$json_bind" ] && BIND="$json_bind"
|
||||||
|
[ -n "$json_token" ] && TOKEN="$json_token"
|
||||||
|
}
|
||||||
|
|
||||||
fmt_elapsed() {
|
fmt_elapsed() {
|
||||||
local total="${1:-0}" h m s
|
local total="${1:-0}" h m s
|
||||||
case "$total" in ''|*[!0-9]*) total=0 ;; esac
|
case "$total" in ''|*[!0-9]*) total=0 ;; esac
|
||||||
@@ -254,6 +273,12 @@ gen_token() {
|
|||||||
(date +%s | md5sum 2>/dev/null | awk '{print $1}')
|
(date +%s | md5sum 2>/dev/null | awk '{print $1}')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_token() {
|
||||||
|
if [ -z "${TOKEN:-}" ]; then
|
||||||
|
TOKEN="$(gen_token)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
is_musl() {
|
is_musl() {
|
||||||
ldd --version 2>&1 | grep -qi musl && return 0
|
ldd --version 2>&1 | grep -qi musl && return 0
|
||||||
[ -e /lib/ld-musl-*.so.1 ] 2>/dev/null && return 0
|
[ -e /lib/ld-musl-*.so.1 ] 2>/dev/null && return 0
|
||||||
@@ -573,46 +598,58 @@ ensure_gateway_config() {
|
|||||||
host_header_fallback="true"
|
host_header_fallback="true"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local allow_insecure="false"
|
local allow_insecure=""
|
||||||
if [ "${ALLOW_INSECURE_AUTH:-0}" = "1" ]; then
|
if [ -n "${ALLOW_INSECURE_AUTH:-}" ]; then
|
||||||
allow_insecure="true"
|
if [ "$ALLOW_INSECURE_AUTH" = "1" ]; then
|
||||||
|
allow_insecure="true"
|
||||||
|
else
|
||||||
|
allow_insecure="false"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local disable_device_auth="false"
|
local disable_device_auth=""
|
||||||
if [ "${DISABLE_DEVICE_AUTH:-0}" = "1" ]; then
|
if [ -n "${DISABLE_DEVICE_AUTH:-}" ]; then
|
||||||
disable_device_auth="true"
|
if [ "$DISABLE_DEVICE_AUTH" = "1" ]; then
|
||||||
|
disable_device_auth="true"
|
||||||
|
else
|
||||||
|
disable_device_auth="false"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local allowed_origins="$ALLOWED_ORIGINS"
|
local allowed_origins="$ALLOWED_ORIGINS"
|
||||||
if [ -z "$allowed_origins" ]; then
|
|
||||||
allowed_origins="$(default_allowed_origins)"
|
|
||||||
fi
|
|
||||||
local selected_key selected_model selected_base_url override_base_url base_url_mode
|
local selected_key selected_model selected_base_url override_base_url base_url_mode
|
||||||
selected_key="$(agent_key_name)"
|
selected_key=""
|
||||||
selected_model="$(agent_default_model)"
|
selected_model=""
|
||||||
selected_base_url="$(agent_default_base_url)"
|
selected_base_url=""
|
||||||
override_base_url=""
|
override_base_url=""
|
||||||
base_url_mode="default"
|
base_url_mode=""
|
||||||
if [ -n "${PROVIDER_BASE_URL:-}" ]; then
|
if [ -n "${DEFAULT_AGENT:-}" ]; then
|
||||||
if is_valid_http_url "$PROVIDER_BASE_URL"; then
|
selected_key="$(agent_key_name)"
|
||||||
override_base_url="$PROVIDER_BASE_URL"
|
selected_model="$(agent_default_model)"
|
||||||
base_url_mode="override"
|
selected_base_url="$(agent_default_base_url)"
|
||||||
else
|
base_url_mode="default"
|
||||||
write_installer_log "Ignoring invalid relay URL for ${DEFAULT_AGENT:-anthropic}: ${PROVIDER_BASE_URL}"
|
if [ -n "${PROVIDER_BASE_URL:-}" ]; then
|
||||||
base_url_mode="preserve"
|
if is_valid_http_url "$PROVIDER_BASE_URL"; then
|
||||||
|
override_base_url="$PROVIDER_BASE_URL"
|
||||||
|
base_url_mode="override"
|
||||||
|
else
|
||||||
|
write_installer_log "Ignoring invalid relay URL for ${DEFAULT_AGENT}: ${PROVIDER_BASE_URL}"
|
||||||
|
base_url_mode="preserve"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$(dirname "$cfg")" 2>/dev/null || true
|
mkdir -p "$(dirname "$cfg")" 2>/dev/null || true
|
||||||
CFG_PATH="$cfg" \
|
CFG_PATH="$cfg" \
|
||||||
ALLOWED_ORIGINS_RAW="$allowed_origins" \
|
ALLOWED_ORIGINS_RAW="$allowed_origins" \
|
||||||
|
DEFAULT_ALLOWED_ORIGINS_RAW="$(default_allowed_origins)" \
|
||||||
GATEWAY_PORT="$PORT" \
|
GATEWAY_PORT="$PORT" \
|
||||||
GATEWAY_BIND="$BIND" \
|
GATEWAY_BIND="$BIND" \
|
||||||
GATEWAY_TOKEN="$TOKEN" \
|
GATEWAY_TOKEN="$TOKEN" \
|
||||||
GATEWAY_ALLOW_INSECURE="$allow_insecure" \
|
GATEWAY_ALLOW_INSECURE="$allow_insecure" \
|
||||||
GATEWAY_DISABLE_DEVICE_AUTH="$disable_device_auth" \
|
GATEWAY_DISABLE_DEVICE_AUTH="$disable_device_auth" \
|
||||||
GATEWAY_HOST_HEADER_FALLBACK="$host_header_fallback" \
|
GATEWAY_HOST_HEADER_FALLBACK="$host_header_fallback" \
|
||||||
DEFAULT_AGENT_NAME="${DEFAULT_AGENT:-anthropic}" \
|
DEFAULT_AGENT_NAME="${DEFAULT_AGENT:-}" \
|
||||||
DEFAULT_AGENT_KEY="$selected_key" \
|
DEFAULT_AGENT_KEY="$selected_key" \
|
||||||
DEFAULT_AGENT_MODEL="$selected_model" \
|
DEFAULT_AGENT_MODEL="$selected_model" \
|
||||||
DEFAULT_AGENT_BASE_URL="$selected_base_url" \
|
DEFAULT_AGENT_BASE_URL="$selected_base_url" \
|
||||||
@@ -635,6 +672,25 @@ local function bool_env(name)
|
|||||||
return os.getenv(name) == "true"
|
return os.getenv(name) == "true"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function optional_env(name)
|
||||||
|
local value = os.getenv(name)
|
||||||
|
if value == nil or value == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function optional_bool_env(name)
|
||||||
|
local value = optional_env(name)
|
||||||
|
if value == "true" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if value == "false" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
local function split_lines(raw)
|
local function split_lines(raw)
|
||||||
local out = {}
|
local out = {}
|
||||||
raw = raw or ""
|
raw = raw or ""
|
||||||
@@ -685,123 +741,166 @@ local gateway = ensure_table(cfg, "gateway")
|
|||||||
gateway.mode = "local"
|
gateway.mode = "local"
|
||||||
gateway.port = tonumber(os.getenv("GATEWAY_PORT")) or 18789
|
gateway.port = tonumber(os.getenv("GATEWAY_PORT")) or 18789
|
||||||
gateway.bind = os.getenv("GATEWAY_BIND") or "lan"
|
gateway.bind = os.getenv("GATEWAY_BIND") or "lan"
|
||||||
gateway.auth = { mode = "token", token = os.getenv("GATEWAY_TOKEN") or "" }
|
local auth = ensure_table(gateway, "auth")
|
||||||
|
auth.mode = "token"
|
||||||
|
auth.token = os.getenv("GATEWAY_TOKEN") or ""
|
||||||
|
|
||||||
local control = ensure_table(gateway, "controlUi")
|
local control = ensure_table(gateway, "controlUi")
|
||||||
control.enabled = true
|
control.enabled = true
|
||||||
control.basePath = nil
|
local configured_origins = optional_env("ALLOWED_ORIGINS_RAW")
|
||||||
control.allowedOrigins = split_lines(os.getenv("ALLOWED_ORIGINS_RAW"))
|
if configured_origins then
|
||||||
control.allowInsecureAuth = bool_env("GATEWAY_ALLOW_INSECURE")
|
control.allowedOrigins = split_lines(configured_origins)
|
||||||
control.dangerouslyDisableDeviceAuth = bool_env("GATEWAY_DISABLE_DEVICE_AUTH")
|
elseif type(control.allowedOrigins) ~= "table" or next(control.allowedOrigins) == nil then
|
||||||
|
control.allowedOrigins = split_lines(os.getenv("DEFAULT_ALLOWED_ORIGINS_RAW") or "")
|
||||||
|
end
|
||||||
|
local allow_insecure = optional_bool_env("GATEWAY_ALLOW_INSECURE")
|
||||||
|
if allow_insecure ~= nil then
|
||||||
|
control.allowInsecureAuth = allow_insecure
|
||||||
|
elseif type(control.allowInsecureAuth) ~= "boolean" then
|
||||||
|
control.allowInsecureAuth = true
|
||||||
|
end
|
||||||
|
local disable_device_auth = optional_bool_env("GATEWAY_DISABLE_DEVICE_AUTH")
|
||||||
|
if disable_device_auth ~= nil then
|
||||||
|
control.dangerouslyDisableDeviceAuth = disable_device_auth
|
||||||
|
elseif type(control.dangerouslyDisableDeviceAuth) ~= "boolean" then
|
||||||
|
control.dangerouslyDisableDeviceAuth = true
|
||||||
|
end
|
||||||
control.dangerouslyAllowHostHeaderOriginFallback = bool_env("GATEWAY_HOST_HEADER_FALLBACK")
|
control.dangerouslyAllowHostHeaderOriginFallback = bool_env("GATEWAY_HOST_HEADER_FALLBACK")
|
||||||
|
|
||||||
local env = {}
|
local env = ensure_table(cfg, "env")
|
||||||
local selected_env_key = os.getenv("DEFAULT_AGENT_KEY")
|
local selected_env_key = optional_env("DEFAULT_AGENT_KEY")
|
||||||
local selected_api_key = os.getenv("DEFAULT_AGENT_API_KEY") or ""
|
local selected_api_key = optional_env("DEFAULT_AGENT_API_KEY")
|
||||||
if selected_env_key and selected_env_key ~= "" and selected_api_key ~= "" then
|
if selected_env_key and selected_api_key then
|
||||||
env[selected_env_key] = selected_api_key
|
env[selected_env_key] = selected_api_key
|
||||||
end
|
end
|
||||||
cfg.env = next(env) and env or nil
|
cfg.env = next(env) and env or nil
|
||||||
|
|
||||||
|
local function update_api_key(provider)
|
||||||
|
if selected_api_key ~= nil then
|
||||||
|
provider.apiKey = selected_api_key ~= "" and selected_api_key or nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function env_key_for_provider(id)
|
||||||
|
if id == "openai" then return "OPENAI_API_KEY" end
|
||||||
|
if id == "anthropic" then return "ANTHROPIC_API_KEY" end
|
||||||
|
if id == "minimax-cn" then return "MINIMAX_API_KEY" end
|
||||||
|
if id == "moonshot" then return "MOONSHOT_API_KEY" end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
local models = ensure_table(cfg, "models")
|
local models = ensure_table(cfg, "models")
|
||||||
if type(models.mode) ~= "string" or models.mode == "" then
|
if type(models.mode) ~= "string" or models.mode == "" then
|
||||||
models.mode = "merge"
|
models.mode = "merge"
|
||||||
end
|
end
|
||||||
local providers = ensure_table(models, "providers")
|
|
||||||
|
|
||||||
local current_provider_id = os.getenv("DEFAULT_AGENT_NAME") or "anthropic"
|
local current_provider_id = optional_env("DEFAULT_AGENT_NAME")
|
||||||
local current_provider = ensure_table(providers, current_provider_id)
|
local primary_model = optional_env("DEFAULT_AGENT_MODEL")
|
||||||
|
local current_primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary
|
||||||
|
if not primary_model and type(current_primary) == "string" and current_primary ~= "" then
|
||||||
|
primary_model = current_primary
|
||||||
|
end
|
||||||
|
if not current_provider_id and type(primary_model) == "string" and primary_model:match("^[^/]+/.+") then
|
||||||
|
current_provider_id = primary_model:match("^([^/]+)/")
|
||||||
|
end
|
||||||
|
if not current_provider_id or current_provider_id == "" then
|
||||||
|
current_provider_id = "anthropic"
|
||||||
|
end
|
||||||
|
if not primary_model or primary_model == "" then
|
||||||
|
if current_provider_id == "openai" then
|
||||||
|
primary_model = "openai/gpt-5.2"
|
||||||
|
elseif current_provider_id == "anthropic" then
|
||||||
|
primary_model = "anthropic/claude-sonnet-4-6"
|
||||||
|
elseif current_provider_id == "minimax-cn" then
|
||||||
|
primary_model = "minimax-cn/MiniMax-M2.5"
|
||||||
|
elseif current_provider_id == "moonshot" then
|
||||||
|
primary_model = "moonshot/kimi-k2.5"
|
||||||
|
elseif current_provider_id == "custom-provider" then
|
||||||
|
primary_model = "custom-provider/custom-model"
|
||||||
|
local custom = models.providers and models.providers["custom-provider"]
|
||||||
|
local first = custom and custom.models and custom.models[1]
|
||||||
|
if type(first) == "table" and type(first.id) == "string" and first.id ~= "" then
|
||||||
|
primary_model = "custom-provider/" .. first.id
|
||||||
|
end
|
||||||
|
else
|
||||||
|
primary_model = "anthropic/claude-sonnet-4-6"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local current_model_id = primary_model:match("^[^/]+/(.+)$") or primary_model
|
||||||
|
if current_model_id == "" then
|
||||||
|
current_model_id = "claude-sonnet-4-6"
|
||||||
|
end
|
||||||
|
|
||||||
|
local old_providers = type(models.providers) == "table" and models.providers or {}
|
||||||
|
local preserved_provider = type(old_providers[current_provider_id]) == "table" and old_providers[current_provider_id] or {}
|
||||||
|
local providers = {}
|
||||||
|
models.providers = providers
|
||||||
|
local current_provider = preserved_provider
|
||||||
|
providers[current_provider_id] = current_provider
|
||||||
|
|
||||||
if current_provider_id == "openai" then
|
if current_provider_id == "openai" then
|
||||||
current_provider.api = "openai-completions"
|
current_provider.api = "openai-completions"
|
||||||
current_provider.baseUrl = "https://api.openai.com/v1"
|
current_provider.baseUrl = "https://api.openai.com/v1"
|
||||||
current_provider.apiKey = selected_api_key
|
update_api_key(current_provider)
|
||||||
current_provider.models = {
|
current_provider.models = {
|
||||||
{ id = "gpt-5.2", name = "GPT-5.2" },
|
{ id = current_model_id, name = current_model_id },
|
||||||
}
|
}
|
||||||
elseif current_provider_id == "anthropic" then
|
elseif current_provider_id == "anthropic" then
|
||||||
current_provider.api = "anthropic-messages"
|
current_provider.api = "anthropic-messages"
|
||||||
current_provider.baseUrl = "https://api.anthropic.com"
|
current_provider.baseUrl = "https://api.anthropic.com"
|
||||||
current_provider.apiKey = selected_api_key
|
update_api_key(current_provider)
|
||||||
current_provider.models = {
|
current_provider.models = {
|
||||||
{ id = "claude-sonnet-4-6", name = "Claude Sonnet 4.6" },
|
{ id = current_model_id, name = current_model_id },
|
||||||
}
|
}
|
||||||
elseif current_provider_id == "minimax-cn" then
|
elseif current_provider_id == "minimax-cn" then
|
||||||
current_provider.api = "anthropic-messages"
|
current_provider.api = "anthropic-messages"
|
||||||
current_provider.baseUrl = "https://api.minimaxi.com/anthropic"
|
current_provider.baseUrl = "https://api.minimaxi.com/anthropic"
|
||||||
current_provider.apiKey = selected_api_key
|
update_api_key(current_provider)
|
||||||
current_provider.authHeader = true
|
current_provider.authHeader = true
|
||||||
current_provider.models = {
|
current_provider.models = {
|
||||||
{ id = "MiniMax-M2.5", name = "MiniMax M2.5" },
|
{ id = current_model_id, name = current_model_id },
|
||||||
}
|
}
|
||||||
elseif current_provider_id == "moonshot" then
|
elseif current_provider_id == "moonshot" then
|
||||||
current_provider.api = "openai-completions"
|
current_provider.api = "openai-completions"
|
||||||
current_provider.baseUrl = "https://api.moonshot.cn/v1"
|
current_provider.baseUrl = "https://api.moonshot.cn/v1"
|
||||||
current_provider.apiKey = selected_api_key
|
update_api_key(current_provider)
|
||||||
current_provider.models = {
|
current_provider.models = {
|
||||||
{ id = "kimi-k2.5", name = "Kimi K2.5" },
|
{ id = current_model_id, name = current_model_id },
|
||||||
}
|
}
|
||||||
elseif current_provider_id == "custom-provider" then
|
elseif current_provider_id == "custom-provider" then
|
||||||
local primary_model = os.getenv("DEFAULT_AGENT_MODEL") or "custom-provider/custom-model"
|
if current_model_id == "" then
|
||||||
local custom_model_id = primary_model:match("^[^/]+/(.+)$") or primary_model
|
current_model_id = "custom-model"
|
||||||
if custom_model_id == "" then
|
|
||||||
custom_model_id = "custom-model"
|
|
||||||
end
|
end
|
||||||
current_provider.api = "openai-completions"
|
current_provider.api = "openai-completions"
|
||||||
current_provider.baseUrl = os.getenv("DEFAULT_AGENT_OVERRIDE_BASE_URL") or current_provider.baseUrl
|
current_provider.baseUrl = optional_env("DEFAULT_AGENT_OVERRIDE_BASE_URL") or current_provider.baseUrl
|
||||||
current_provider.apiKey = selected_api_key
|
update_api_key(current_provider)
|
||||||
current_provider.authHeader = nil
|
current_provider.authHeader = nil
|
||||||
current_provider.models = {
|
current_provider.models = {
|
||||||
{
|
{ id = current_model_id, name = current_model_id },
|
||||||
reasoning = false,
|
|
||||||
name = custom_model_id .. " (Custom Provider)",
|
|
||||||
cost = {
|
|
||||||
input = 0,
|
|
||||||
cacheRead = 0,
|
|
||||||
cacheWrite = 0,
|
|
||||||
output = 0,
|
|
||||||
},
|
|
||||||
id = custom_model_id,
|
|
||||||
maxTokens = 4096,
|
|
||||||
contextWindow = 16000,
|
|
||||||
input = { "text" },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local override_base = os.getenv("DEFAULT_AGENT_OVERRIDE_BASE_URL") or ""
|
local override_base = optional_env("DEFAULT_AGENT_OVERRIDE_BASE_URL") or ""
|
||||||
local base_url_mode = os.getenv("DEFAULT_AGENT_BASE_URL_MODE") or "default"
|
local base_url_mode = optional_env("DEFAULT_AGENT_BASE_URL_MODE") or ""
|
||||||
if current_provider_id == "custom-provider" and override_base ~= "" then
|
if current_provider_id == "custom-provider" and override_base ~= "" then
|
||||||
current_provider.baseUrl = override_base
|
current_provider.baseUrl = override_base
|
||||||
elseif base_url_mode == "override" and override_base ~= "" then
|
elseif base_url_mode == "override" and override_base ~= "" then
|
||||||
current_provider.baseUrl = override_base
|
current_provider.baseUrl = override_base
|
||||||
elseif base_url_mode == "default" then
|
elseif base_url_mode == "default" then
|
||||||
current_provider.baseUrl = os.getenv("DEFAULT_AGENT_BASE_URL") or current_provider.baseUrl
|
current_provider.baseUrl = optional_env("DEFAULT_AGENT_BASE_URL") or current_provider.baseUrl
|
||||||
end
|
|
||||||
|
|
||||||
-- luci.jsonc encodes empty Lua tables as JSON arrays ([]). To avoid producing
|
|
||||||
-- invalid provider entries like {"deepseek/deepseek-chat":[]}, drop provider
|
|
||||||
-- records that are empty or look like a provider/model string.
|
|
||||||
do
|
|
||||||
local to_del = {}
|
|
||||||
for k, v in pairs(providers) do
|
|
||||||
if type(k) == "string" and k:find("/", 1, true) then
|
|
||||||
table.insert(to_del, k)
|
|
||||||
elseif type(v) == "table" and next(v) == nil then
|
|
||||||
table.insert(to_del, k)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for _, k in ipairs(to_del) do
|
|
||||||
providers[k] = nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local agents = ensure_table(cfg, "agents")
|
local agents = ensure_table(cfg, "agents")
|
||||||
local defaults = ensure_table(agents, "defaults")
|
local defaults = ensure_table(agents, "defaults")
|
||||||
local model = ensure_table(defaults, "model")
|
local model = ensure_table(defaults, "model")
|
||||||
model.primary = os.getenv("DEFAULT_AGENT_MODEL") or "anthropic/claude-sonnet-4-6"
|
model.primary = primary_model or "anthropic/claude-sonnet-4-6"
|
||||||
|
|
||||||
prune_empty_tables(cfg)
|
local current_env_key = env_key_for_provider(current_provider_id)
|
||||||
|
local next_env = {}
|
||||||
|
if current_env_key and type(current_provider.apiKey) == "string" and current_provider.apiKey ~= "" then
|
||||||
|
next_env[current_env_key] = current_provider.apiKey
|
||||||
|
end
|
||||||
|
cfg.env = next(next_env) and next_env or nil
|
||||||
|
|
||||||
local encoded = json.stringify(cfg, true)
|
local encoded = json.stringify(cfg, true)
|
||||||
if encoded then
|
if encoded then
|
||||||
@@ -814,7 +913,7 @@ f:close()
|
|||||||
|
|
||||||
local env_path = cfg_path:gsub("openclaw%.json$", "openclaw.env")
|
local env_path = cfg_path:gsub("openclaw%.json$", "openclaw.env")
|
||||||
local envf = assert(io.open(env_path, "w"))
|
local envf = assert(io.open(env_path, "w"))
|
||||||
if selected_env_key and selected_env_key ~= "" and selected_api_key ~= "" then
|
if selected_env_key and selected_env_key ~= "" and selected_api_key and selected_api_key ~= "" then
|
||||||
local val = tostring(selected_api_key)
|
local val = tostring(selected_api_key)
|
||||||
val = val:gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub('"', '\\"')
|
val = val:gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub('"', '\\"')
|
||||||
envf:write(selected_env_key .. '="' .. val .. '"\n')
|
envf:write(selected_env_key .. '="' .. val .. '"\n')
|
||||||
@@ -1147,11 +1246,7 @@ upgrade_openclaw() {
|
|||||||
uci -q commit "$UCI_NS" >/dev/null 2>&1 || true
|
uci -q commit "$UCI_NS" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# Ensure token exists even if runtime is already installed (service start requires it).
|
# Ensure token exists even if runtime is already installed (service start requires it).
|
||||||
TOKEN="$(uci_get token)"
|
ensure_token
|
||||||
if [ -z "$TOKEN" ]; then
|
|
||||||
TOKEN="$(gen_token)"
|
|
||||||
uci -q set "${UCI_NS}.main.token=$TOKEN" && uci -q commit "$UCI_NS" || true
|
|
||||||
fi
|
|
||||||
ensure_gateway_config || true
|
ensure_gateway_config || true
|
||||||
|
|
||||||
if have_openclaw_runtime; then
|
if have_openclaw_runtime; then
|
||||||
@@ -1233,6 +1328,7 @@ do_start() {
|
|||||||
write_installer_log "== start begin =="
|
write_installer_log "== start begin =="
|
||||||
ensure_dirs
|
ensure_dirs
|
||||||
fix_data_permissions || true
|
fix_data_permissions || true
|
||||||
|
ensure_token
|
||||||
ensure_safe_port_for_start || exit 1
|
ensure_safe_port_for_start || exit 1
|
||||||
/etc/init.d/openclawmgr enable >/dev/null 2>&1 || true
|
/etc/init.d/openclawmgr enable >/dev/null 2>&1 || true
|
||||||
uci -q set "${UCI_NS}.main.enabled=1" && uci -q commit "$UCI_NS" || true
|
uci -q set "${UCI_NS}.main.enabled=1" && uci -q commit "$UCI_NS" || true
|
||||||
@@ -1255,6 +1351,7 @@ do_restart() {
|
|||||||
write_installer_log "== restart begin =="
|
write_installer_log "== restart begin =="
|
||||||
ensure_dirs
|
ensure_dirs
|
||||||
fix_data_permissions || true
|
fix_data_permissions || true
|
||||||
|
ensure_token
|
||||||
ensure_safe_port_for_start || exit 1
|
ensure_safe_port_for_start || exit 1
|
||||||
ensure_gateway_config || true
|
ensure_gateway_config || true
|
||||||
/etc/init.d/openclawmgr restart >/dev/null 2>&1 || true
|
/etc/init.d/openclawmgr restart >/dev/null 2>&1 || true
|
||||||
@@ -1266,6 +1363,7 @@ do_restart() {
|
|||||||
write_installer_log "== apply_config begin =="
|
write_installer_log "== apply_config begin =="
|
||||||
ensure_dirs
|
ensure_dirs
|
||||||
fix_data_permissions || true
|
fix_data_permissions || true
|
||||||
|
ensure_token
|
||||||
ensure_gateway_config || true
|
ensure_gateway_config || true
|
||||||
|
|
||||||
local pid=""
|
local pid=""
|
||||||
@@ -1425,14 +1523,8 @@ PROVIDER_BASE_URL="$(uci_get provider_base_url)"
|
|||||||
[ -n "$BIND" ] || BIND="lan"
|
[ -n "$BIND" ] || BIND="lan"
|
||||||
[ -n "$ENABLED" ] || ENABLED="0"
|
[ -n "$ENABLED" ] || ENABLED="0"
|
||||||
NODE_VERSION="24.14.0"
|
NODE_VERSION="24.14.0"
|
||||||
[ -n "$ALLOW_INSECURE_AUTH" ] || ALLOW_INSECURE_AUTH="1"
|
|
||||||
[ -n "$DISABLE_DEVICE_AUTH" ] || DISABLE_DEVICE_AUTH="1"
|
|
||||||
[ -n "$DEFAULT_AGENT" ] || DEFAULT_AGENT="anthropic"
|
|
||||||
[ -n "$DEFAULT_MODEL" ] || DEFAULT_MODEL=""
|
|
||||||
[ -n "$INSTALL_ACCELERATED" ] || INSTALL_ACCELERATED="1"
|
[ -n "$INSTALL_ACCELERATED" ] || INSTALL_ACCELERATED="1"
|
||||||
[ -n "$INSTALL_CHANNEL" ] || INSTALL_CHANNEL="stable"
|
[ -n "$INSTALL_CHANNEL" ] || INSTALL_CHANNEL="stable"
|
||||||
[ -n "$PROVIDER_API_KEY" ] || PROVIDER_API_KEY=""
|
|
||||||
[ -n "$PROVIDER_BASE_URL" ] || PROVIDER_BASE_URL=""
|
|
||||||
|
|
||||||
case "$PORT" in
|
case "$PORT" in
|
||||||
''|*[!0-9]*) PORT="18789" ;;
|
''|*[!0-9]*) PORT="18789" ;;
|
||||||
@@ -1449,14 +1541,17 @@ case "$ENABLED" in
|
|||||||
*) ENABLED="0" ;;
|
*) ENABLED="0" ;;
|
||||||
esac
|
esac
|
||||||
case "$ALLOW_INSECURE_AUTH" in
|
case "$ALLOW_INSECURE_AUTH" in
|
||||||
|
'') ;;
|
||||||
1|true|yes|on) ALLOW_INSECURE_AUTH="1" ;;
|
1|true|yes|on) ALLOW_INSECURE_AUTH="1" ;;
|
||||||
*) ALLOW_INSECURE_AUTH="0" ;;
|
*) ALLOW_INSECURE_AUTH="0" ;;
|
||||||
esac
|
esac
|
||||||
case "$DISABLE_DEVICE_AUTH" in
|
case "$DISABLE_DEVICE_AUTH" in
|
||||||
|
'') ;;
|
||||||
1|true|yes|on) DISABLE_DEVICE_AUTH="1" ;;
|
1|true|yes|on) DISABLE_DEVICE_AUTH="1" ;;
|
||||||
*) DISABLE_DEVICE_AUTH="0" ;;
|
*) DISABLE_DEVICE_AUTH="0" ;;
|
||||||
esac
|
esac
|
||||||
case "$DEFAULT_AGENT" in
|
case "$DEFAULT_AGENT" in
|
||||||
|
'') ;;
|
||||||
openai|anthropic|minimax-cn|moonshot|custom-provider) ;;
|
openai|anthropic|minimax-cn|moonshot|custom-provider) ;;
|
||||||
*) DEFAULT_AGENT="anthropic" ;;
|
*) DEFAULT_AGENT="anthropic" ;;
|
||||||
esac
|
esac
|
||||||
@@ -1496,6 +1591,21 @@ NODE_DIR="${BASE_DIR}/node"
|
|||||||
GLOBAL_DIR="${BASE_DIR}/global"
|
GLOBAL_DIR="${BASE_DIR}/global"
|
||||||
DATA_DIR="${BASE_DIR}/data"
|
DATA_DIR="${BASE_DIR}/data"
|
||||||
|
|
||||||
|
if [ -n "$BASE_DIR" ]; then
|
||||||
|
load_gateway_runtime_from_json
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$PORT" in
|
||||||
|
''|*[!0-9]*) PORT="18789" ;;
|
||||||
|
esac
|
||||||
|
if [ "$PORT" -lt 1 ] 2>/dev/null || [ "$PORT" -gt 65535 ] 2>/dev/null; then
|
||||||
|
PORT="18789"
|
||||||
|
fi
|
||||||
|
case "$BIND" in
|
||||||
|
loopback|lan|auto|tailnet|custom) ;;
|
||||||
|
*) BIND="lan" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
NODE_BIN="${NODE_DIR}/bin/node"
|
NODE_BIN="${NODE_DIR}/bin/node"
|
||||||
NPM_BIN="${NODE_DIR}/bin/npm"
|
NPM_BIN="${NODE_DIR}/bin/npm"
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,45 @@ prompt_default() {
|
|||||||
|
|
||||||
uci_get() { uci -q get "openclawmgr.main.$1" 2>/dev/null || true; }
|
uci_get() { uci -q get "openclawmgr.main.$1" 2>/dev/null || true; }
|
||||||
|
|
||||||
|
is_musl() {
|
||||||
|
ldd --version 2>&1 | grep -qi musl && return 0
|
||||||
|
[ -e /lib/ld-musl-*.so.1 ] 2>/dev/null && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
init_git_transport() {
|
||||||
|
command -v git >/dev/null 2>&1 || return 0
|
||||||
|
mkdir -p "$DATA_DIR" 2>/dev/null || true
|
||||||
|
git config --file "$DATA_DIR/.gitconfig" --add url."https://github.com/".insteadOf "ssh://git@github.com/" 2>/dev/null || true
|
||||||
|
git config --file "$DATA_DIR/.gitconfig" --add url."https://github.com/".insteadOf "ssh://git@github.com" 2>/dev/null || true
|
||||||
|
git config --file "$DATA_DIR/.gitconfig" --add url."https://github.com/".insteadOf "git@github.com:" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
init_npm_env() {
|
||||||
|
INSTALL_ACCELERATED="${INSTALL_ACCELERATED:-$(uci_get install_accelerated)}"
|
||||||
|
[ -n "$INSTALL_ACCELERATED" ] || INSTALL_ACCELERATED="1"
|
||||||
|
case "$INSTALL_ACCELERATED" in
|
||||||
|
1|true|yes|on) INSTALL_ACCELERATED="1" ;;
|
||||||
|
*) INSTALL_ACCELERATED="0" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
NPM_CACHE_DIR="${BASE_DIR}/npm-cache"
|
||||||
|
export npm_config_cache="$NPM_CACHE_DIR"
|
||||||
|
export npm_config_prefix="$GLOBAL_DIR"
|
||||||
|
export npm_config_audit="false"
|
||||||
|
export npm_config_fund="false"
|
||||||
|
export npm_config_progress="false"
|
||||||
|
export npm_config_update_notifier="false"
|
||||||
|
|
||||||
|
if is_musl; then
|
||||||
|
export npm_config_ignore_scripts="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$INSTALL_ACCELERATED" = "1" ]; then
|
||||||
|
export npm_config_registry="https://registry.npmmirror.com"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
init_paths() {
|
init_paths() {
|
||||||
BASE_DIR="${BASE_DIR:-$(uci_get base_dir)}"
|
BASE_DIR="${BASE_DIR:-$(uci_get base_dir)}"
|
||||||
if [ -z "$BASE_DIR" ]; then
|
if [ -z "$BASE_DIR" ]; then
|
||||||
@@ -54,6 +93,8 @@ init_paths() {
|
|||||||
export OPENCLAW_STATE_DIR="${DATA_DIR}/.openclaw"
|
export OPENCLAW_STATE_DIR="${DATA_DIR}/.openclaw"
|
||||||
export OPENCLAW_CONFIG_PATH="$CONFIG_FILE"
|
export OPENCLAW_CONFIG_PATH="$CONFIG_FILE"
|
||||||
export PATH="${NODE_DIR}/bin:${GLOBAL_DIR}/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
export PATH="${NODE_DIR}/bin:${GLOBAL_DIR}/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
|
init_git_transport
|
||||||
|
init_npm_env
|
||||||
}
|
}
|
||||||
|
|
||||||
find_openclaw_entry() {
|
find_openclaw_entry() {
|
||||||
@@ -81,6 +122,51 @@ openclaw_cmd() {
|
|||||||
return 127
|
return 127
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openclaw() {
|
||||||
|
openclaw_cmd "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
first_line_or_empty() {
|
||||||
|
sed -n '1p' 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_openclaw_version() {
|
||||||
|
local v=""
|
||||||
|
v="$(openclaw_cmd --version 2>/dev/null | first_line_or_empty)"
|
||||||
|
if [ -z "$v" ]; then
|
||||||
|
v="$(openclaw_cmd version 2>/dev/null | first_line_or_empty)"
|
||||||
|
fi
|
||||||
|
printf '%s' "$v"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_cli_env_summary() {
|
||||||
|
local node_v npm_v openclaw_v npm_registry musl_state ignore_scripts
|
||||||
|
node_v="$(node -v 2>/dev/null | first_line_or_empty)"
|
||||||
|
npm_v="$(npm -v 2>/dev/null | first_line_or_empty)"
|
||||||
|
openclaw_v="$(detect_openclaw_version)"
|
||||||
|
npm_registry="$(npm config get registry 2>/dev/null | tr -d '\r' | first_line_or_empty)"
|
||||||
|
musl_state="no"
|
||||||
|
ignore_scripts="${npm_config_ignore_scripts:-false}"
|
||||||
|
is_musl && musl_state="yes"
|
||||||
|
|
||||||
|
[ -n "$node_v" ] || node_v="未安装"
|
||||||
|
[ -n "$npm_v" ] || npm_v="未安装"
|
||||||
|
[ -n "$openclaw_v" ] || openclaw_v="未安装"
|
||||||
|
[ -n "$npm_registry" ] || npm_registry="unknown"
|
||||||
|
|
||||||
|
printf '%s\n' "${BOLD}环境摘要${NC}"
|
||||||
|
printf '%s\n' " base_dir ${BASE_DIR}"
|
||||||
|
printf '%s\n' " config ${CONFIG_FILE}"
|
||||||
|
printf '%s\n' " node ${node_v}"
|
||||||
|
printf '%s\n' " npm ${npm_v}"
|
||||||
|
printf '%s\n' " npm prefix ${npm_config_prefix:-${GLOBAL_DIR}}"
|
||||||
|
printf '%s\n' " npm cache ${npm_config_cache:-${NPM_CACHE_DIR:-}}"
|
||||||
|
printf '%s\n' " npm registry ${npm_registry}"
|
||||||
|
printf '%s\n' " musl ${musl_state}"
|
||||||
|
printf '%s\n' " ignore scripts ${ignore_scripts}"
|
||||||
|
printf '%s\n' " openclaw ${openclaw_v}"
|
||||||
|
}
|
||||||
|
|
||||||
action_backup_config() {
|
action_backup_config() {
|
||||||
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
|
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
|
||||||
if [ ! -f "$CONFIG_FILE" ]; then
|
if [ ! -f "$CONFIG_FILE" ]; then
|
||||||
@@ -117,15 +203,35 @@ action_configure() {
|
|||||||
openclaw_cmd configure || true
|
openclaw_cmd configure || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
action_cli_env() {
|
||||||
|
printf '\n'
|
||||||
|
info "=== OpenClaw CLI 环境 ==="
|
||||||
|
printf '%s\n' "${DIM}已注入 BASE_DIR / NODE_DIR / GLOBAL_DIR / DATA_DIR / OPENCLAW_* / PATH / npm_config_*${NC}"
|
||||||
|
printf '%s\n' "${DIM}可直接执行 openclaw doctor / openclaw configure / node -v / npm -v 等命令${NC}"
|
||||||
|
printf '%s\n' "${DIM}此环境下 npm -g 默认安装到 ${GLOBAL_DIR},并复用 OpenClawMgr 的缓存/镜像配置${NC}"
|
||||||
|
printf '%s\n' "${DIM}若系统为 musl,将自动启用 ignore-scripts,并预置 GitHub HTTPS 重写${NC}"
|
||||||
|
printf '%s\n' "${DIM}输入 exit 可退出终端${NC}"
|
||||||
|
printf '\n'
|
||||||
|
print_cli_env_summary
|
||||||
|
if cd "$DATA_DIR" 2>/dev/null; then
|
||||||
|
printf '%s\n' "${DIM}当前目录: ${DATA_DIR}${NC}"
|
||||||
|
else
|
||||||
|
warn "无法切换到数据目录,保持当前目录不变。"
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
exec sh -i
|
||||||
|
}
|
||||||
|
|
||||||
main_menu() {
|
main_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
printf '\n'
|
printf '\n'
|
||||||
printf '%s\n' "${BOLD}OpenClaw AI Gateway — CLI 配置入口(OpenClawMgr)${NC}"
|
printf '%s\n' "${BOLD}OpenClaw AI Gateway — CLI 配置入口(OpenClawMgr)${NC}"
|
||||||
printf '%s\n' "${DIM}base_dir: ${BASE_DIR}${NC}"
|
printf '%s\n' "${DIM}base_dir: ${BASE_DIR}${NC}"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
printf '%s\n' " ${CYAN}1)${NC} 🧭 官方配置向导 ${DIM}(openclaw configure)${NC}"
|
printf '%s\n' " ${CYAN}1)${NC} 💻 CLI环境 ${DIM}(仅注入环境,不执行 configure)${NC}"
|
||||||
printf '%s\n' " ${CYAN}2)${NC} 💾 备份配置"
|
printf '%s\n' " ${CYAN}2)${NC} 🧭 官方配置向导 ${DIM}(openclaw configure)${NC}"
|
||||||
printf '%s\n' " ${CYAN}3)${NC} 📥 恢复配置"
|
printf '%s\n' " ${CYAN}3)${NC} 💾 备份配置"
|
||||||
|
printf '%s\n' " ${CYAN}4)${NC} 📥 恢复配置"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
printf '%s\n' " ${CYAN}0)${NC} 退出"
|
printf '%s\n' " ${CYAN}0)${NC} 退出"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
@@ -133,9 +239,10 @@ main_menu() {
|
|||||||
local c=""
|
local c=""
|
||||||
c="$(prompt_default "请选择" "1")"
|
c="$(prompt_default "请选择" "1")"
|
||||||
case "$c" in
|
case "$c" in
|
||||||
1) action_configure; pause_enter ;;
|
1) action_cli_env ;;
|
||||||
2) action_backup_config || true; pause_enter ;;
|
2) action_configure; pause_enter ;;
|
||||||
3) action_restore_config || true; pause_enter ;;
|
3) action_backup_config || true; pause_enter ;;
|
||||||
|
4) action_restore_config || true; pause_enter ;;
|
||||||
0) ok "再见!"; exit 0 ;;
|
0) ok "再见!"; exit 0 ;;
|
||||||
*) warn "无效选择" ;;
|
*) warn "无效选择" ;;
|
||||||
esac
|
esac
|
||||||
@@ -146,12 +253,13 @@ init_paths
|
|||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
--help|-h)
|
--help|-h)
|
||||||
echo "Usage: openclawmgr-cli.sh [configure|backup|restore]"
|
echo "Usage: openclawmgr-cli.sh [configure|cli-env|backup|restore]"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
case "${1:-menu}" in
|
case "${1:-menu}" in
|
||||||
menu) main_menu ;;
|
menu) main_menu ;;
|
||||||
configure) action_configure; pause_enter ;;
|
configure) action_configure; pause_enter ;;
|
||||||
|
cli-env) action_cli_env ;;
|
||||||
backup) action_backup_config || true; pause_enter ;;
|
backup) action_backup_config || true; pause_enter ;;
|
||||||
restore) action_restore_config || true; pause_enter ;;
|
restore) action_restore_config || true; pause_enter ;;
|
||||||
*) echo "Unknown command: ${1:-}" >&2; exit 2 ;;
|
*) echo "Unknown command: ${1:-}" >&2; exit 2 ;;
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */
|
/*! Sortable 1.15.7 - MIT | git://github.com/SortableJS/Sortable.git */
|
||||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?e(Object(i),!0).forEach(function(t){var e,n;e=o,t=i[n=t],n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach(function(t){Object.defineProperty(o,t,Object.getOwnPropertyDescriptor(i,t))})}return o}function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)Object.prototype.hasOwnProperty.call(o,n)&&(t[n]=o[n])}return t}).apply(this,arguments)}function i(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};for(var n,o={},i=Object.keys(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function r(t){return function(t){if(Array.isArray(t))return l(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return l(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function l(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n<e;n++)o[n]=t[n];return o}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),s=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),c=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),d={capture:!1,passive:!1};function h(t,e,n){t.addEventListener(e,n,!y&&d)}function p(t,e,n){t.removeEventListener(e,n,!y&&d)}function f(t,e){if(e&&(">"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBoundingClientRect();a-=h.top+parseInt(R(i,"border-top-width")),l-=h.left+parseInt(R(i,"border-left-width")),s=a+r.height,c=l+r.width;break}}while(i=i.parentNode);return o&&t!==window&&(o=(e=b(i||t))&&e.a,t=e&&e.d,e&&(s=(a/=t)+(u/=t),c=(l/=o)+(d/=o))),{top:a,left:l,bottom:s,right:c,width:d,height:u}}}function Y(t,e,n){for(var o=M(tLine truncated
|
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n<e;n++)o[n]=t[n];return o}function i(t,e,n){return(e=function(t){t=function(t,e){if("object"!=typeof t||!t)return t;var n=t[Symbol.toPrimitive];if(void 0===n)return("string"===e?String:Number)(t);e=n.call(t,e||"default");if("object"!=typeof e)return e;throw new TypeError("@@toPrimitive must return a primitive value.")}(t,"string");return"symbol"==typeof t?t:t+""}(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function a(){return(a=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)!{}.hasOwnProperty.call(o,n)||(t[n]=o[n])}return t}).apply(null,arguments)}function r(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?r(Object(n),!0).forEach(function(t){i(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):r(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function l(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};var n,o={};for(n in t)if({}.hasOwnProperty.call(t,n)){if(-1!==e.indexOf(n))continue;o[n]=t[n]}return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],-1===e.indexOf(n)&&{}.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function e(t){return function(t){if(Array.isArray(t))return o(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return o(t,e);var n={}.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?o(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(t){return(s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),c=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),d=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),h={capture:!1,passive:!1};function f(t,e,n){t.addEventListener(e,n,!y&&h)}function p(t,e,n){t.removeEventListener(e,n,!y&&h)}function g(t,e){if(e&&(">"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function m(t){return t.host&&t!==document&&t.host.nodeType&&t.host!==t?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&g(t,e)||o&&t===n)return t}while(t!==n&&(t=m(t)))}return null}var v,b=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(b," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(b," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function D(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function E(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBouLine truncated
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<%
|
<%
|
||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
-%>
|
-%>
|
||||||
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js?v=26.1.9"></script>
|
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table .cbi-button-up,
|
table .cbi-button-up,
|
||||||
table .cbi-button-down {
|
table .cbi-button-down,
|
||||||
|
.td.cbi-section-actions .cbi-button-up,
|
||||||
|
.td.cbi-section-actions .cbi-button-down {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ if node then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
-%>
|
-%>
|
||||||
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
|
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table th, .table .th {
|
table th, .table .th {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<%
|
<%
|
||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
-%>
|
-%>
|
||||||
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js?v=26.1.11"></script>
|
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table .cbi-button-up,
|
table .cbi-button-up,
|
||||||
table .cbi-button-down {
|
table .cbi-button-down,
|
||||||
|
.td.cbi-section-actions .cbi-button-up,
|
||||||
|
.td.cbi-section-actions .cbi-button-down {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ local fs = api.fs
|
|||||||
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
|
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
|
||||||
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
|
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
|
||||||
-%>
|
-%>
|
||||||
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js?v=26.1.9"></script>
|
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table .cbi-button-up,
|
table .cbi-button-up,
|
||||||
table .cbi-button-down {
|
table .cbi-button-down,
|
||||||
|
.td.cbi-section-actions .cbi-button-up,
|
||||||
|
.td.cbi-section-actions .cbi-button-down {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -546,7 +546,7 @@ load_acl() {
|
|||||||
$ipt_m -A PSW2 $(comment "${comment_d}") -p udp $(REDIRECT $REDIR_PORT TPROXY)
|
$ipt_m -A PSW2 $(comment "${comment_d}") -p udp $(REDIRECT $REDIR_PORT TPROXY)
|
||||||
|
|
||||||
[ "$PROXY_IPV6" == "1" ] && {
|
[ "$PROXY_IPV6" == "1" ] && {
|
||||||
$ip6t_m -I PSW2 $(comment "${comment_d}") -p udp -d $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE
|
$ip6t_m -I PSW2 $(comment "${comment_d}") -p udp $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE
|
||||||
$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp -d $FAKE_IP_6 -j PSW2_RULE
|
$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp -d $FAKE_IP_6 -j PSW2_RULE
|
||||||
add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS
|
add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS
|
||||||
add_port_rules "$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE"
|
add_port_rules "$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE"
|
||||||
|
|||||||
+3
-2
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=microsocks
|
PKG_NAME:=microsocks
|
||||||
PKG_VERSION:=1.0.5
|
PKG_VERSION:=1.0.5
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/rofl0r/microsocks/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/rofl0r/microsocks/tar.gz/v$(PKG_VERSION)?
|
||||||
@@ -25,7 +25,7 @@ define Package/microsocks
|
|||||||
SECTION:=net
|
SECTION:=net
|
||||||
CATEGORY:=Network
|
CATEGORY:=Network
|
||||||
SUBMENU:=Web Servers/Proxies
|
SUBMENU:=Web Servers/Proxies
|
||||||
TITLE:=Tiny, portable SOCKS5 server
|
TITLE:=Tiny, portable SOCKS5 server. Support forwarding rules
|
||||||
URL:=https://github.com/rofl0r/microsocks
|
URL:=https://github.com/rofl0r/microsocks
|
||||||
DEPENDS:=+libpthread
|
DEPENDS:=+libpthread
|
||||||
endef
|
endef
|
||||||
@@ -33,6 +33,7 @@ endef
|
|||||||
define Package/microsocks/description
|
define Package/microsocks/description
|
||||||
A SOCKS5 service that you can run on your remote boxes to tunnel connections
|
A SOCKS5 service that you can run on your remote boxes to tunnel connections
|
||||||
through them, if for some reason SSH doesn't cut it for you.
|
through them, if for some reason SSH doesn't cut it for you.
|
||||||
|
This version supports forwarding rules.
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/microsocks/install
|
define Package/microsocks/install
|
||||||
|
|||||||
@@ -0,0 +1,428 @@
|
|||||||
|
--- a/sockssrv.c
|
||||||
|
+++ b/sockssrv.c
|
||||||
|
@@ -33,8 +33,10 @@
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
+#include <sys/time.h>
|
||||||
|
#include "server.h"
|
||||||
|
#include "sblist.h"
|
||||||
|
+#define MICROSOCKS_VERSION "1.0.5-forward"
|
||||||
|
|
||||||
|
/* timeout in microseconds on resource exhaustion to prevent excessive
|
||||||
|
cpu usage. */
|
||||||
|
@@ -71,6 +73,7 @@
|
||||||
|
static pthread_rwlock_t auth_ips_lock = PTHREAD_RWLOCK_INITIALIZER;
|
||||||
|
static const struct server* server;
|
||||||
|
static union sockaddr_union bind_addr = {.v4.sin_family = AF_UNSPEC};
|
||||||
|
+static sblist *fwd_rules;
|
||||||
|
|
||||||
|
enum socksstate {
|
||||||
|
SS_1_CONNECTED,
|
||||||
|
@@ -97,6 +100,17 @@
|
||||||
|
EC_ADDRESSTYPE_NOT_SUPPORTED = 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
+struct fwd_rule {
|
||||||
|
+ char *match_name;
|
||||||
|
+ short match_port;
|
||||||
|
+ char *auth_buf; /* Username/Password request buffer (RFC-1929) */
|
||||||
|
+ size_t auth_len;
|
||||||
|
+ char *upstream_name;
|
||||||
|
+ short upstream_port;
|
||||||
|
+ char *req_buf; /* Client Connection Request buffer to send to upstream */
|
||||||
|
+ size_t req_len;
|
||||||
|
+};
|
||||||
|
+
|
||||||
|
struct thread {
|
||||||
|
pthread_t pt;
|
||||||
|
struct client client;
|
||||||
|
@@ -116,6 +130,109 @@
|
||||||
|
static void dolog(const char* fmt, ...) { }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+static int upstream_handshake(const struct fwd_rule* rule, unsigned char *client_buf, size_t client_buf_len,
|
||||||
|
+ int client_fd, int upstream_fd, unsigned short client_port) {
|
||||||
|
+ unsigned char sbuf[512];
|
||||||
|
+ ssize_t r;
|
||||||
|
+
|
||||||
|
+ if(rule->auth_buf) {
|
||||||
|
+ unsigned char handshake[4] = {5, 2, 0, 2};
|
||||||
|
+ if (write(upstream_fd, handshake, 4) != 4) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+ } else {
|
||||||
|
+ unsigned char handshake[3] = {5, 1, 0};
|
||||||
|
+ if (write(upstream_fd, handshake, 3) != 3) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (read(upstream_fd, sbuf, 2) != 2 || sbuf[0] != 5) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (sbuf[1] == 2) {
|
||||||
|
+ if (!rule->auth_buf) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+ if (write(upstream_fd, rule->auth_buf, rule->auth_len) != (ssize_t)rule->auth_len) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+ if (read(upstream_fd, sbuf, 2) != 2 || sbuf[0] != 1 || sbuf[1] != 0) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+ } else if (sbuf[1] != 0) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (write(upstream_fd, client_buf, client_buf_len) != (ssize_t)client_buf_len) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ size_t total = 0;
|
||||||
|
+ size_t need = 4;
|
||||||
|
+
|
||||||
|
+ while (total < need) {
|
||||||
|
+ r = read(upstream_fd, sbuf + total, need - total);
|
||||||
|
+ if (r <= 0) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+ total += r;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (sbuf[1] != 0) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -sbuf[1];
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ size_t need_more = 0;
|
||||||
|
+ switch (sbuf[3]) {
|
||||||
|
+ case 1:
|
||||||
|
+ need_more = 4 + 2;
|
||||||
|
+ break;
|
||||||
|
+ case 4:
|
||||||
|
+ need_more = 16 + 2;
|
||||||
|
+ break;
|
||||||
|
+ case 3:
|
||||||
|
+ r = read(upstream_fd, sbuf + total, 1);
|
||||||
|
+ if (r != 1) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+ total += r;
|
||||||
|
+ need_more = sbuf[4] + 2;
|
||||||
|
+ break;
|
||||||
|
+ default:
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -EC_ADDRESSTYPE_NOT_SUPPORTED;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ while (total < need + need_more) {
|
||||||
|
+ r = read(upstream_fd, sbuf + total, (need + need_more) - total);
|
||||||
|
+ if (r <= 0) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+ total += r;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (write(client_fd, sbuf, total) != (ssize_t)total) {
|
||||||
|
+ close(upstream_fd);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return upstream_fd;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
static struct addrinfo* addr_choose(struct addrinfo* list, union sockaddr_union* bindaddr) {
|
||||||
|
int af = SOCKADDR_UNION_AF(bindaddr);
|
||||||
|
if(af == AF_UNSPEC) return list;
|
||||||
|
@@ -125,7 +242,9 @@
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
-static int connect_socks_target(unsigned char *buf, size_t n, struct client *client) {
|
||||||
|
+static int connect_socks_target(unsigned char *buf, size_t n, struct client *client, int *used_rule) {
|
||||||
|
+ *used_rule = 0;
|
||||||
|
+
|
||||||
|
if(n < 5) return -EC_GENERAL_FAILURE;
|
||||||
|
if(buf[0] != 5) return -EC_GENERAL_FAILURE;
|
||||||
|
if(buf[1] != 1) return -EC_COMMAND_NOT_SUPPORTED; /* we support only CONNECT method */
|
||||||
|
@@ -158,6 +277,29 @@
|
||||||
|
}
|
||||||
|
unsigned short port;
|
||||||
|
port = (buf[minlen-2] << 8) | buf[minlen-1];
|
||||||
|
+
|
||||||
|
+ size_t i;
|
||||||
|
+ struct fwd_rule *rule = NULL;
|
||||||
|
+ char original_name[256];
|
||||||
|
+ unsigned short original_port = port;
|
||||||
|
+ strncpy(original_name, namebuf, sizeof(original_name) - 1);
|
||||||
|
+ original_name[sizeof(original_name) - 1] = '\0';
|
||||||
|
+ if(fwd_rules) {
|
||||||
|
+ for(i=0;i<sblist_getsize(fwd_rules);++i) {
|
||||||
|
+ struct fwd_rule* r = (struct fwd_rule*)sblist_get(fwd_rules, i);
|
||||||
|
+ int name_match = (r->match_name[0]=='\0' || strcmp(r->match_name, namebuf) == 0);
|
||||||
|
+ int port_match = (r->match_port == 0 || r->match_port == port);
|
||||||
|
+ if(name_match && port_match) {
|
||||||
|
+ rule = r;
|
||||||
|
+ *used_rule = 1;
|
||||||
|
+ strncpy(namebuf, r->upstream_name, sizeof(namebuf)-1);
|
||||||
|
+ namebuf[sizeof(namebuf)-1] = '\0';
|
||||||
|
+ port = r->upstream_port;
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
/* there's no suitable errorcode in rfc1928 for dns lookup failure */
|
||||||
|
if(resolve(namebuf, port, &remote)) return -EC_GENERAL_FAILURE;
|
||||||
|
struct addrinfo* raddr = addr_choose(remote, &bind_addr);
|
||||||
|
@@ -186,6 +328,11 @@
|
||||||
|
return -EC_GENERAL_FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+ struct timeval tv = {5, 0};
|
||||||
|
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
|
||||||
|
+ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
|
||||||
|
+
|
||||||
|
if(SOCKADDR_UNION_AF(&bind_addr) == raddr->ai_family &&
|
||||||
|
bindtoip(fd, &bind_addr) == -1)
|
||||||
|
goto eval_errno;
|
||||||
|
@@ -198,9 +345,22 @@
|
||||||
|
af = SOCKADDR_UNION_AF(&client->addr);
|
||||||
|
void *ipdata = SOCKADDR_UNION_ADDRESS(&client->addr);
|
||||||
|
inet_ntop(af, ipdata, clientname, sizeof clientname);
|
||||||
|
- dolog("client[%d] %s: connected to %s:%d\n", client->fd, clientname, namebuf, port);
|
||||||
|
+ if (rule) {
|
||||||
|
+ dolog("client[%d] %s: %s:%d -> via %s:%d\n", client->fd, clientname, original_name, original_port, rule->upstream_name, rule->upstream_port);
|
||||||
|
+ } else {
|
||||||
|
+ dolog("client[%d] %s: connected to %s:%d\n", client->fd, clientname, namebuf, port);
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
- return fd;
|
||||||
|
+
|
||||||
|
+ if (rule) {
|
||||||
|
+ int result = upstream_handshake(rule, buf, n, client->fd, fd, original_port);
|
||||||
|
+ if (result < 0) {
|
||||||
|
+ close(fd);
|
||||||
|
+ return result;
|
||||||
|
+ }
|
||||||
|
+ return result;
|
||||||
|
+ }
|
||||||
|
+ return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int is_authed(union sockaddr_union *client, union sockaddr_union *authedip) {
|
||||||
|
@@ -322,6 +482,7 @@
|
||||||
|
ssize_t n;
|
||||||
|
int ret;
|
||||||
|
enum authmethod am;
|
||||||
|
+ int used_rule = 0;
|
||||||
|
t->state = SS_1_CONNECTED;
|
||||||
|
while((n = recv(t->client.fd, buf, sizeof buf, 0)) > 0) {
|
||||||
|
switch(t->state) {
|
||||||
|
@@ -345,12 +506,14 @@
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SS_3_AUTHED:
|
||||||
|
- ret = connect_socks_target(buf, n, &t->client);
|
||||||
|
+ ret = connect_socks_target(buf, n, &t->client, &used_rule);
|
||||||
|
if(ret < 0) {
|
||||||
|
send_error(t->client.fd, ret*-1);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
- send_error(t->client.fd, EC_SUCCESS);
|
||||||
|
+ if (!used_rule) {
|
||||||
|
+ send_error(t->client.fd, EC_SUCCESS);
|
||||||
|
+ }
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -382,11 +545,131 @@
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+static short host_get_port(char *name) {
|
||||||
|
+ int p,n;
|
||||||
|
+ char *c;
|
||||||
|
+ if((c = strrchr(name, ':')) && sscanf(c+1,"%d%n",&p, &n)==1 && n == (int)(strlen(c + 1)) && p >= 0 && p < USHRT_MAX)
|
||||||
|
+ return (*c='\0'),(short)p;
|
||||||
|
+ else
|
||||||
|
+ return -1;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static int fwd_rules_add(char *str) {
|
||||||
|
+ char *match = NULL, *upstream = NULL, *remote = NULL;
|
||||||
|
+ unsigned short match_port, upstream_port, remote_port;
|
||||||
|
+ int ncred;
|
||||||
|
+
|
||||||
|
+ if(sscanf(str, "%m[^,],%n%m[^,],%ms\n", &match, &ncred, &upstream, &remote) != 3)
|
||||||
|
+ return 1;
|
||||||
|
+
|
||||||
|
+ match_port = host_get_port(match);
|
||||||
|
+ upstream_port = host_get_port(upstream);
|
||||||
|
+ remote_port = host_get_port(remote);
|
||||||
|
+
|
||||||
|
+ if(match_port < 0 || upstream_port <= 0 || remote_port < 0) {
|
||||||
|
+ free(match);
|
||||||
|
+ free(upstream);
|
||||||
|
+ free(remote);
|
||||||
|
+ return 1;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ char *match_copy = strdup(match);
|
||||||
|
+ char *upstream_copy = strdup(upstream);
|
||||||
|
+ char *remote_copy = strdup(remote);
|
||||||
|
+
|
||||||
|
+ struct fwd_rule *rule = (struct fwd_rule*)malloc(sizeof(struct fwd_rule));
|
||||||
|
+ if (!rule) {
|
||||||
|
+ free(match_copy);
|
||||||
|
+ free(upstream_copy);
|
||||||
|
+ free(remote_copy);
|
||||||
|
+ free(match);
|
||||||
|
+ free(upstream);
|
||||||
|
+ free(remote);
|
||||||
|
+ return 1;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if(strcmp(match_copy, "0.0.0.0") == 0 || strcmp(match_copy, "*") == 0) {
|
||||||
|
+ free(match_copy);
|
||||||
|
+ rule->match_name = strdup("");
|
||||||
|
+ } else {
|
||||||
|
+ rule->match_name = match_copy;
|
||||||
|
+ }
|
||||||
|
+ rule->match_port = match_port;
|
||||||
|
+ rule->auth_buf = NULL;
|
||||||
|
+ rule->auth_len = 0;
|
||||||
|
+
|
||||||
|
+ char *at_sign = strchr(upstream_copy, '@');
|
||||||
|
+ if (at_sign) {
|
||||||
|
+ *at_sign = '\0';
|
||||||
|
+ char *auth_part = upstream_copy;
|
||||||
|
+ char *host_part = at_sign + 1;
|
||||||
|
+ char *colon = strchr(auth_part, ':');
|
||||||
|
+ if (!colon) {
|
||||||
|
+ free(rule);
|
||||||
|
+ free(upstream_copy);
|
||||||
|
+ free(remote_copy);
|
||||||
|
+ free(match);
|
||||||
|
+ free(upstream);
|
||||||
|
+ free(remote);
|
||||||
|
+ return 1;
|
||||||
|
+ }
|
||||||
|
+ *colon++ = '\0';
|
||||||
|
+ char *username = auth_part;
|
||||||
|
+ char *password = colon;
|
||||||
|
+ size_t ulen = strlen(username);
|
||||||
|
+ size_t plen = strlen(password);
|
||||||
|
+ if (ulen > 255 || plen > 255) {
|
||||||
|
+ free(rule);
|
||||||
|
+ free(upstream_copy);
|
||||||
|
+ free(remote_copy);
|
||||||
|
+ free(match);
|
||||||
|
+ free(upstream);
|
||||||
|
+ free(remote);
|
||||||
|
+ return 1;
|
||||||
|
+ }
|
||||||
|
+ rule->auth_len = 1 + 1 + ulen + 1 + plen;
|
||||||
|
+ rule->auth_buf = malloc(rule->auth_len);
|
||||||
|
+ rule->auth_buf[0] = 1;
|
||||||
|
+ rule->auth_buf[1] = ulen;
|
||||||
|
+ memcpy(&rule->auth_buf[2], username, ulen);
|
||||||
|
+ rule->auth_buf[2 + ulen] = plen;
|
||||||
|
+ memcpy(&rule->auth_buf[3 + ulen], password, plen);
|
||||||
|
+ rule->upstream_name = strdup(host_part);
|
||||||
|
+ rule->upstream_port = upstream_port;
|
||||||
|
+ /* hide from ps */
|
||||||
|
+ memset(str+ncred, '*', ulen+1+plen);
|
||||||
|
+ } else {
|
||||||
|
+ rule->upstream_name = strdup(upstream_copy);
|
||||||
|
+ rule->upstream_port = upstream_port;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ free(upstream_copy);
|
||||||
|
+ short rlen = strlen(remote_copy);
|
||||||
|
+ rule->req_len = 3 + 1 + 1 + rlen + 2;
|
||||||
|
+ rule->req_buf = (char*)malloc(rule->req_len);
|
||||||
|
+ rule->req_buf[0] = 5;
|
||||||
|
+ rule->req_buf[1] = 1;
|
||||||
|
+ rule->req_buf[2] = 0;
|
||||||
|
+ rule->req_buf[3] = 3;
|
||||||
|
+ rule->req_buf[4] = rlen;
|
||||||
|
+ memcpy(&rule->req_buf[5], remote_copy, rlen);
|
||||||
|
+ unsigned short rport = remote_port ? remote_port : 0;
|
||||||
|
+ rule->req_buf[5 + rlen] = (rport >> 8) & 0xFF;
|
||||||
|
+ rule->req_buf[5 + rlen + 1] = (rport & 0xFF);
|
||||||
|
+ free(remote_copy);
|
||||||
|
+ sblist_add(fwd_rules, rule);
|
||||||
|
+ free(match);
|
||||||
|
+ free(upstream);
|
||||||
|
+ free(remote);
|
||||||
|
+
|
||||||
|
+ return 0;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
static int usage(void) {
|
||||||
|
dprintf(2,
|
||||||
|
"MicroSocks SOCKS5 Server\n"
|
||||||
|
"------------------------\n"
|
||||||
|
- "usage: microsocks -1 -q -i listenip -p port -u user -P pass -b bindaddr -w ips\n"
|
||||||
|
+ "usage: microsocks -1 -q -i listenip -p port -u user -P pass -b bindaddr -w ips -f fwdrule\n"
|
||||||
|
"all arguments are optional.\n"
|
||||||
|
"by default listenip is 0.0.0.0 and port 1080.\n\n"
|
||||||
|
"option -q disables logging.\n"
|
||||||
|
@@ -401,6 +684,12 @@
|
||||||
|
" this is handy for programs like firefox that don't support\n"
|
||||||
|
" user/pass auth. for it to work you'd basically make one connection\n"
|
||||||
|
" with another program that supports it, and then you can use firefox too.\n"
|
||||||
|
+ "option -f specifies a forwarding rule of the form\n"
|
||||||
|
+ " match_name:match_port,[user:password@]upstream_name:upstream_port,remote_name:remote_port\n"
|
||||||
|
+ " this will cause requests that /match/ to be renamed to /remote/\n"
|
||||||
|
+ " and sent to the /upstream/ SOCKS5 proxy server.\n"
|
||||||
|
+ " this option may be specified multiple times.\n"
|
||||||
|
+ "option -V prints version information and exits.\n"
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
@@ -416,7 +705,7 @@
|
||||||
|
const char *listenip = "0.0.0.0";
|
||||||
|
char *p, *q;
|
||||||
|
unsigned port = 1080;
|
||||||
|
- while((ch = getopt(argc, argv, ":1qb:i:p:u:P:w:")) != -1) {
|
||||||
|
+ while((ch = getopt(argc, argv, ":1qb:i:p:u:P:w:f:V")) != -1) {
|
||||||
|
switch(ch) {
|
||||||
|
case 'w': /* fall-through */
|
||||||
|
case '1':
|
||||||
|
@@ -456,11 +745,20 @@
|
||||||
|
case 'p':
|
||||||
|
port = atoi(optarg);
|
||||||
|
break;
|
||||||
|
+ case 'f':
|
||||||
|
+ if(!fwd_rules)
|
||||||
|
+ fwd_rules = sblist_new(sizeof(struct fwd_rule), 16);
|
||||||
|
+ if(fwd_rules_add(optarg))
|
||||||
|
+ return dprintf(2, "error: could not parse forwarding rule %s\n", optarg), 1;
|
||||||
|
+ break;
|
||||||
|
case ':':
|
||||||
|
dprintf(2, "error: option -%c requires an operand\n", optopt);
|
||||||
|
/* fall through */
|
||||||
|
case '?':
|
||||||
|
return usage();
|
||||||
|
+ case 'V':
|
||||||
|
+ dprintf(1, "MicroSocks %s\n", MICROSOCKS_VERSION);
|
||||||
|
+ return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if((auth_user && !auth_pass) || (!auth_user && auth_pass)) {
|
||||||
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=mihomo
|
PKG_NAME:=mihomo
|
||||||
PKG_VERSION:=1.19.22
|
PKG_VERSION:=1.19.23
|
||||||
PKG_RELEASE:=3
|
PKG_RELEASE:=4
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
+3
-3
@@ -1,14 +1,14 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=nikki
|
PKG_NAME:=nikki
|
||||||
PKG_VERSION:=2026.04.01
|
PKG_VERSION:=2026.04.08
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
||||||
PKG_SOURCE_VERSION:=v1.19.22
|
PKG_SOURCE_VERSION:=v1.19.23
|
||||||
PKG_MIRROR_HASH:=skip
|
PKG_MIRROR_HASH:=skip
|
||||||
|
|
||||||
PKG_LICENSE:=GPL3.0+
|
PKG_LICENSE:=GPL3.0+
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ config mixin 'mixin'
|
|||||||
option 'mode' 'rule'
|
option 'mode' 'rule'
|
||||||
option 'match_process' 'off'
|
option 'match_process' 'off'
|
||||||
option 'ipv6' '1'
|
option 'ipv6' '1'
|
||||||
|
option 'ui_path' 'ui'
|
||||||
option 'ui_url' 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip'
|
option 'ui_url' 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip'
|
||||||
option 'api_listen' '[::]:9090'
|
option 'api_listen' '[::]:9090'
|
||||||
option 'selection_cache' '1'
|
option 'selection_cache' '1'
|
||||||
|
|||||||
Reference in new issue
Block a user