mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 10:31:38 +08:00
💋 Sync 2026-04-08 20:31:38
This commit is contained in:
parent
d6e7b3ef08
commit
92a3fac9ca
@ -967,18 +967,18 @@ function renderListeners(s, uciconfig, isClient) {
|
||||
o = s.taboption('field_transport', form.ListValue, 'transport_type', _('Transport type'));
|
||||
o.value('grpc', _('gRPC'));
|
||||
o.value('ws', _('WebSocket'));
|
||||
o.value('xhttp', _('XHTTP'));
|
||||
o.validate = function(section_id, value) {
|
||||
const type = this.section.getOption('type').formvalue(section_id);
|
||||
|
||||
switch (type) {
|
||||
case 'vmess':
|
||||
case 'vless':
|
||||
if (!['http', 'h2', 'grpc', 'ws'].includes(value))
|
||||
return _('Expecting: only support %s.').format(_('HTTP') +
|
||||
' / ' + _('HTTPUpgrade') +
|
||||
' / ' + _('gRPC') +
|
||||
' / ' + _('WebSocket'));
|
||||
if (!['grpc', 'ws', 'xhttp'].includes(value))
|
||||
return _('Expecting: only support %s.').format(_('gRPC') +
|
||||
' / ' + _('WebSocket') +
|
||||
' / ' + _('XHTTP'));
|
||||
break;
|
||||
case 'vmess':
|
||||
case 'trojan':
|
||||
if (!['grpc', 'ws'].includes(value))
|
||||
return _('Expecting: only support %s.').format(_('gRPC') +
|
||||
@ -993,11 +993,17 @@ function renderListeners(s, uciconfig, isClient) {
|
||||
o.depends('transport_enabled', '1');
|
||||
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.placeholder = '/';
|
||||
o.default = '/';
|
||||
o.rmempty = false;
|
||||
o.depends({transport_enabled: '1', transport_type: 'ws'});
|
||||
o.depends({transport_enabled: '1', transport_type: /^(ws|xhttp)$/});
|
||||
o.modalonly = true;
|
||||
|
||||
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.depends({transport_enabled: '1', transport_type: 'grpc'});
|
||||
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({
|
||||
|
||||
@ -1170,7 +1170,7 @@ return view.extend({
|
||||
so.depends({transport_enabled: '1', transport_type: 'h2'});
|
||||
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.placeholder = 'example.com';
|
||||
so.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||
@ -1261,6 +1261,42 @@ return view.extend({
|
||||
so.depends({transport_enabled: '1', transport_type: 'xhttp'});
|
||||
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
|
||||
so = ss.taboption('field_general', form.Flag, 'smux_enabled', _('Multiplex'));
|
||||
so.default = so.disabled;
|
||||
@ -1280,6 +1316,8 @@ return view.extend({
|
||||
so.datatype = 'uinteger';
|
||||
so.placeholder = '4';
|
||||
so.depends('smux_enabled', '1');
|
||||
so.depends({transport_enabled: '1', transport_type: 'grpc'});
|
||||
so.depends('type', 'trusttunnel');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_multiplex', form.Value, 'smux_min_streams', _('Minimum streams'),
|
||||
@ -1287,6 +1325,8 @@ return view.extend({
|
||||
so.datatype = 'uinteger';
|
||||
so.placeholder = '4';
|
||||
so.depends('smux_enabled', '1');
|
||||
so.depends({transport_enabled: '1', transport_type: 'grpc'});
|
||||
so.depends('type', 'trusttunnel');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_multiplex', form.Value, 'smux_max_streams', _('Maximum streams'),
|
||||
@ -1296,6 +1336,8 @@ return view.extend({
|
||||
so.datatype = 'uinteger';
|
||||
so.placeholder = '0';
|
||||
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 = 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-service-name": cfg.transport_grpc_servicename,
|
||||
"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,
|
||||
"ws-opts": cfg.transport_type === 'ws' ? {
|
||||
path: cfg.transport_path || '/',
|
||||
@ -665,11 +668,24 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
||||
headers: cfg.transport_http_headers ? json(cfg.transport_http_headers) : null,
|
||||
mode: cfg.transport_xhttp_mode,
|
||||
"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
|
||||
} : {}),
|
||||
|
||||
/* 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' ? {
|
||||
enabled: true,
|
||||
protocol: cfg.smux_protocol,
|
||||
|
||||
@ -345,8 +345,16 @@ export function parseListener(cfg, isClient, label) {
|
||||
|
||||
/* Transport fields */
|
||||
...(cfg.transport_enabled === '1' ? {
|
||||
"grpc-service-name": cfg.transport_grpc_servicename,
|
||||
"ws-path": cfg.transport_path
|
||||
"grpc-service-name": cfg.transport_type === 'grpc' ? cfg.transport_grpc_servicename : null,
|
||||
"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
|
||||
|
||||
PKG_VERSION:=0.1.9
|
||||
PKG_RELEASE:=11
|
||||
PKG_VERSION:=0.1.10
|
||||
PKG_RELEASE:=12
|
||||
|
||||
LUCI_TITLE:=LuCI support for OpenClaw Launcher
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
@ -327,6 +327,73 @@
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
@ -564,6 +631,295 @@
|
||||
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 {
|
||||
color: var(--oclm-danger);
|
||||
margin-bottom: 12px;
|
||||
@ -646,6 +1002,27 @@
|
||||
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 {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@ -26,11 +26,25 @@
|
||||
}
|
||||
|
||||
var root = container.attachShadow ? (container.shadowRoot || container.attachShadow({ mode: "open" })) : container;
|
||||
|
||||
function defaultProtectedDirectories() {
|
||||
return [];
|
||||
}
|
||||
|
||||
var state = {
|
||||
status: null,
|
||||
form: null,
|
||||
options: null,
|
||||
newOrigin: "",
|
||||
security: {
|
||||
runningUser: "openclawmgr",
|
||||
newDirectoryPath: "",
|
||||
directoryError: "",
|
||||
pendingAddPath: "",
|
||||
deleteTargetId: null,
|
||||
deleteTargetPath: "",
|
||||
protectedDirectories: defaultProtectedDirectories()
|
||||
},
|
||||
activeTab: "basic",
|
||||
savingSection: "",
|
||||
lastAppliedAt: "",
|
||||
@ -102,60 +116,32 @@
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function modelForAgent(agent) {
|
||||
function defaultModelForAgent(agent) {
|
||||
return {
|
||||
openai: "openai/gpt-5.2",
|
||||
anthropic: "anthropic/claude-sonnet-4-6",
|
||||
"minimax-cn": "minimax-cn/MiniMax-M2.5",
|
||||
moonshot: "moonshot/kimi-k2.5",
|
||||
"custom-provider": "custom-provider/custom-model"
|
||||
}[agent] || "anthropic/claude-sonnet-4-6";
|
||||
openai: "gpt-5.2",
|
||||
anthropic: "claude-sonnet-4-6",
|
||||
"minimax-cn": "MiniMax-M2.5",
|
||||
moonshot: "kimi-k2.5",
|
||||
"custom-provider": "custom-model"
|
||||
}[agent] || "claude-sonnet-4-6";
|
||||
}
|
||||
|
||||
function modelPrefixForAgent(agent) {
|
||||
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);
|
||||
function normalizeModelValue(value, fallback) {
|
||||
value = String(value || "").replace(/^\s+|\s+$/g, "");
|
||||
if (!value) {
|
||||
return modelForAgent(agent);
|
||||
return String(fallback || "");
|
||||
}
|
||||
if (value.indexOf(prefix + "/") === 0) {
|
||||
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;
|
||||
return value.replace(/^[^/]+\//, "");
|
||||
}
|
||||
|
||||
function resolveModelValue(form) {
|
||||
var agent = form && form.default_agent ? form.default_agent : "anthropic";
|
||||
var value = form && form.default_model ? String(form.default_model) : "";
|
||||
if (!value || !modelMatchesAgent(agent, value)) {
|
||||
return normalizeModelValue(agent, value);
|
||||
}
|
||||
return value;
|
||||
return normalizeModelValue(value, defaultModelForAgent(agent));
|
||||
}
|
||||
|
||||
function modelPlaceholder(agent) {
|
||||
if (agent === "custom-provider") {
|
||||
return "例如 gpt-5.1 或 custom-provider/gpt-5.1";
|
||||
}
|
||||
return "请按照<provider>/<model-id>格式填写";
|
||||
function modelPlaceholder() {
|
||||
return "这里只填写模型名;例如 gpt-5.1";
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
@ -181,6 +167,101 @@
|
||||
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) {
|
||||
var value = String(url || "");
|
||||
if (!value) {
|
||||
@ -335,6 +416,26 @@
|
||||
'</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() {
|
||||
return !!(window.taskd && window.taskd.show_log);
|
||||
}
|
||||
@ -448,6 +549,7 @@
|
||||
var status = state.status || {};
|
||||
var form = state.form || {};
|
||||
var options = state.options || { base_dir_choices: [] };
|
||||
var security = getSecurityState();
|
||||
var allowedOrigins = Array.isArray(form.allowed_origins) ? form.allowed_origins : [];
|
||||
var baseDirOptions = (options.base_dir_choices || []).map(function(path) {
|
||||
return '<option value="' + escapeHtml(path) + '"></option>';
|
||||
@ -472,6 +574,24 @@
|
||||
var savingAi = state.savingSection === "ai";
|
||||
var savingAccess = state.savingSection === "access";
|
||||
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 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 === "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 === "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 === "cleanup" ? ' is-active' : '') + '" type="button" data-tab="cleanup">卸载清理</button>' +
|
||||
'</div>' +
|
||||
@ -553,7 +674,7 @@
|
||||
: "如果你的供应商兼容 OpenAI 协议,可选择“自定义供应商”并在下方填写地址与模型。") + '</div>') +
|
||||
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("默认模型", '<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></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></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="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">' +
|
||||
@ -594,6 +746,41 @@
|
||||
'<button class="oclm-button oclm-button-danger" type="button" data-op="purge">彻底清理</button>' +
|
||||
'</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>' +
|
||||
'</div></div></div>';
|
||||
|
||||
@ -821,6 +1008,10 @@
|
||||
|
||||
var newOriginEl = getEl("oclm-new-origin");
|
||||
if (newOriginEl) state.newOrigin = newOriginEl.value;
|
||||
|
||||
var securityPathEl = getEl("oclm-security-path");
|
||||
if (securityPathEl) getSecurityState().newDirectoryPath = securityPathEl.value;
|
||||
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
@ -1060,11 +1251,11 @@
|
||||
if (agent) {
|
||||
agent.onchange = function() {
|
||||
syncDraftFromDom();
|
||||
state.form.default_agent = agent.value;
|
||||
state.form.default_model = modelForAgent(agent.value);
|
||||
render();
|
||||
};
|
||||
}
|
||||
state.form.default_agent = agent.value;
|
||||
state.form.default_model = defaultModelForAgent(agent.value);
|
||||
render();
|
||||
};
|
||||
}
|
||||
|
||||
var apiKey = root.getElementById("oclm-api-key");
|
||||
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");
|
||||
if (saveBasic) {
|
||||
saveBasic.onclick = function() {
|
||||
@ -1148,7 +1491,7 @@
|
||||
if (saveAi) {
|
||||
saveAi.onclick = function() {
|
||||
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;
|
||||
if (agentValue === "custom-provider" && !String(baseUrlValue || "").replace(/^\s+|\s+$/g, "")) {
|
||||
window.alert("自定义供应商必须填写服务地址");
|
||||
@ -1282,11 +1625,13 @@
|
||||
}
|
||||
}
|
||||
render();
|
||||
loadSecurityData();
|
||||
refreshStatus();
|
||||
}).catch(function() {
|
||||
state.form = state.form || {};
|
||||
state.options = state.options || { base_dir_choices: [] };
|
||||
render();
|
||||
loadSecurityData();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,10 @@ function index()
|
||||
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", "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
|
||||
|
||||
@ -68,6 +72,51 @@ local function write_json(obj)
|
||||
http.write_json(obj)
|
||||
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 http = require "luci.http"
|
||||
local jsonc = require "luci.jsonc"
|
||||
@ -273,6 +322,213 @@ local function get_host()
|
||||
return host
|
||||
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()
|
||||
local http = require "luci.http"
|
||||
local sys = require "luci.sys"
|
||||
@ -472,10 +728,15 @@ function action_status()
|
||||
local task = get_task_state("openclawmgr")
|
||||
|
||||
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 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
|
||||
|
||||
@ -549,11 +810,13 @@ end
|
||||
function action_ready()
|
||||
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 ""
|
||||
|
||||
if not port:match("^%d+$") then port = "18789" end
|
||||
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",
|
||||
})
|
||||
local port = runtime_gateway.port
|
||||
local bind = runtime_gateway.bind
|
||||
|
||||
if base_dir == "" then
|
||||
write_json({ ok = true, ready = false, reason = "base_dir missing" })
|
||||
@ -654,25 +917,255 @@ end
|
||||
function action_config_data()
|
||||
local http = require "luci.http"
|
||||
local jsonc = require "luci.jsonc"
|
||||
local sys = require "luci.sys"
|
||||
local util = require "luci.util"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local model = require "luci.model.openclawmgr"
|
||||
|
||||
local function read_json_file(path)
|
||||
local f = io.open(path, "r")
|
||||
if not f then return nil end
|
||||
local raw = f:read("*a")
|
||||
local function write_json_file(path, obj)
|
||||
local dir = path:match("^(.+)/[^/]+$")
|
||||
if dir and dir ~= "" then
|
||||
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()
|
||||
if not raw or raw == "" then return nil end
|
||||
return jsonc.parse(raw)
|
||||
return true
|
||||
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 "")
|
||||
if base_dir == "" then return "" end
|
||||
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
|
||||
if type(primary) == "string" and primary:match("^custom%-provider/.+") then
|
||||
return primary
|
||||
return primary:gsub("^[^/]+/", "")
|
||||
end
|
||||
local providers = cfg.models and cfg.models.providers
|
||||
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 id = first and first.id
|
||||
if type(id) == "string" and id ~= "" then
|
||||
return "custom-provider/" .. id
|
||||
return id
|
||||
end
|
||||
return ""
|
||||
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 not require_csrf() then
|
||||
return
|
||||
@ -701,7 +1202,8 @@ function action_config_data()
|
||||
return body[key] ~= nil
|
||||
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
|
||||
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)" })
|
||||
return
|
||||
end
|
||||
uci:set("openclawmgr", section, "port", port)
|
||||
end
|
||||
|
||||
if has("bind") then
|
||||
@ -727,7 +1228,6 @@ function action_config_data()
|
||||
write_json({ ok = false, error = "invalid bind" })
|
||||
return
|
||||
end
|
||||
uci:set("openclawmgr", section, "bind", bind)
|
||||
end
|
||||
|
||||
if has("base_dir") then
|
||||
@ -745,21 +1245,12 @@ function action_config_data()
|
||||
write_json({ ok = false, error = "invalid default_agent" })
|
||||
return
|
||||
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
|
||||
|
||||
if has("install_accelerated") then
|
||||
uci:set("openclawmgr", section, "install_accelerated", bool_to_uci(body.install_accelerated == true or body.install_accelerated == "1"))
|
||||
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
|
||||
local value = tostring(body.provider_base_url or "")
|
||||
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" })
|
||||
return
|
||||
end
|
||||
uci:set("openclawmgr", section, "provider_base_url", value)
|
||||
end
|
||||
|
||||
if has("token") then
|
||||
uci:set("openclawmgr", section, "token", tostring(body.token or ""))
|
||||
body.token = tostring(body.token or "")
|
||||
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
|
||||
local origins = {}
|
||||
if type(body.allowed_origins) == "table" then
|
||||
for _, item in ipairs(body.allowed_origins) do
|
||||
item = tostring(item or ""):match("^%s*(.-)%s*$")
|
||||
if item and item ~= "" then
|
||||
table.insert(origins, item)
|
||||
item = trim(item)
|
||||
if item ~= "" then
|
||||
origins[#origins + 1] = item
|
||||
end
|
||||
end
|
||||
end
|
||||
if #origins > 0 then
|
||||
uci:set_list("openclawmgr", section, "allowed_origins", origins)
|
||||
else
|
||||
uci:delete("openclawmgr", section, "allowed_origins")
|
||||
end
|
||||
runtime_cfg.allowed_origins = origins
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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")
|
||||
write_json({ ok = true })
|
||||
return
|
||||
end
|
||||
|
||||
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
||||
local port = uci:get("openclawmgr", "main", "port") or "18789"
|
||||
local blocks = model.blocks()
|
||||
local home = model.home()
|
||||
local paths, default_path = model.find_paths(blocks, home, "Configs")
|
||||
@ -830,42 +1354,165 @@ function action_config_data()
|
||||
end
|
||||
add_choice(default_path or "/root/Configs/OpenClawMgr")
|
||||
|
||||
local allowed_origins = {}
|
||||
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
|
||||
local runtime_cfg = get_runtime_config(base_dir)
|
||||
|
||||
write_json({
|
||||
ok = true,
|
||||
config = {
|
||||
enabled = (uci:get("openclawmgr", "main", "enabled") or "0") == "1",
|
||||
port = port,
|
||||
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
||||
port = runtime_cfg.port,
|
||||
bind = runtime_cfg.bind,
|
||||
base_dir = base_dir,
|
||||
token = uci:get("openclawmgr", "main", "token") or "",
|
||||
allowed_origins = allowed_origins,
|
||||
allow_insecure_auth = (uci:get("openclawmgr", "main", "allow_insecure_auth") or "0") == "1",
|
||||
disable_device_auth = (uci:get("openclawmgr", "main", "disable_device_auth") or "0") == "1",
|
||||
default_agent = current_default_agent,
|
||||
default_model = current_default_model,
|
||||
token = runtime_cfg.token,
|
||||
allowed_origins = runtime_cfg.allowed_origins,
|
||||
allow_insecure_auth = runtime_cfg.allow_insecure_auth,
|
||||
disable_device_auth = runtime_cfg.disable_device_auth,
|
||||
default_agent = runtime_cfg.default_agent,
|
||||
default_model = runtime_cfg.default_model,
|
||||
install_accelerated = (uci:get("openclawmgr", "main", "install_accelerated") or "1") == "1",
|
||||
provider_api_key = uci:get("openclawmgr", "main", "provider_api_key") or "",
|
||||
provider_base_url = uci:get("openclawmgr", "main", "provider_base_url") or "",
|
||||
provider_api_key = runtime_cfg.provider_api_key,
|
||||
provider_base_url = runtime_cfg.provider_base_url,
|
||||
},
|
||||
options = {
|
||||
base_dir_choices = choices,
|
||||
suggested_base_dir = default_path or "",
|
||||
default_origin = default_allowed_origin(port),
|
||||
default_origin = default_allowed_origin(runtime_cfg.port),
|
||||
}
|
||||
})
|
||||
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()
|
||||
local http = require "luci.http"
|
||||
local i18n = require "luci.i18n"
|
||||
@ -980,11 +1627,13 @@ function action_diag_info()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
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"
|
||||
|
||||
if not port:match("^%d+$") then port = "18789" end
|
||||
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",
|
||||
})
|
||||
local port = runtime_gateway.port
|
||||
local bind = runtime_gateway.bind
|
||||
|
||||
local avail_mb = 0
|
||||
if base_dir ~= "" then
|
||||
|
||||
@ -32,13 +32,14 @@ local data_dir = base_dir .. "/data"
|
||||
s = m:section(SimpleSection)
|
||||
|
||||
o = s:option(Value, "mode", translate("Mode"))
|
||||
o:value("cli-env", translate("CLI终端 (仅注入环境)"))
|
||||
o:value("configure", translate("官方配置向导 (openclaw configure)"))
|
||||
o:value("backup", translate("备份配置"))
|
||||
o:value("restore", translate("恢复配置"))
|
||||
o.default = "configure"
|
||||
o.default = "cli-env"
|
||||
o.forcewrite = true
|
||||
|
||||
o = s:option(DummyValue, "_tip", translate("CLI Config"))
|
||||
o = s:option(DummyValue, "_tip", translate("CLI终端"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function()
|
||||
return translate("Starts a web terminal (LAN only) for common OpenClaw maintenance tasks.")
|
||||
@ -93,7 +94,7 @@ o.render = function(self, section, scope)
|
||||
end
|
||||
o.write = function(self, section)
|
||||
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"
|
||||
end
|
||||
start_ttyd(mode)
|
||||
|
||||
@ -11,6 +11,10 @@
|
||||
checkUpdateUrl: "<%=url('admin/services/openclawmgr/check_update')%>",
|
||||
configUrl: "<%=url('admin/services/openclawmgr/config_data')%>",
|
||||
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"
|
||||
};
|
||||
})();
|
||||
|
||||
@ -1,12 +1,4 @@
|
||||
config openclawmgr 'main'
|
||||
option enabled '0'
|
||||
option port '18789'
|
||||
option bind 'lan'
|
||||
option token ''
|
||||
option allow_insecure_auth '1'
|
||||
option disable_device_auth '1'
|
||||
option base_dir ''
|
||||
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
|
||||
}
|
||||
|
||||
_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() {
|
||||
config_load openclawmgr
|
||||
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 token main token ""
|
||||
local port="18789"
|
||||
local bind="lan"
|
||||
local token=""
|
||||
|
||||
[ "$enabled" -eq 1 ] || return 0
|
||||
[ -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 [ "$port" -le 1024 ] 2>/dev/null; then
|
||||
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 global_dir="${base_dir}/global"
|
||||
local data_dir="${base_dir}/data"
|
||||
local env_file="${data_dir}/.openclaw/openclaw.env"
|
||||
local entry="$(_find_entry "$global_dir")"
|
||||
local openai_api_key=""
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
#!/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
|
||||
if ! uci -q get openclawmgr.main >/dev/null 2>&1; then
|
||||
uci -q set openclawmgr.main=openclawmgr >/dev/null 2>&1 || true
|
||||
@ -7,16 +19,8 @@ fi
|
||||
|
||||
# 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.port 2>/dev/null)" ] || uci -q set openclawmgr.main.port='18789'
|
||||
[ -n "$(uci -q get openclawmgr.main.bind 2>/dev/null)" ] || uci -q set openclawmgr.main.bind='lan'
|
||||
[ -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.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
|
||||
|
||||
|
||||
@ -41,6 +41,25 @@ uci_get_list() {
|
||||
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() {
|
||||
local total="${1:-0}" h m s
|
||||
case "$total" in ''|*[!0-9]*) total=0 ;; esac
|
||||
@ -254,6 +273,12 @@ gen_token() {
|
||||
(date +%s | md5sum 2>/dev/null | awk '{print $1}')
|
||||
}
|
||||
|
||||
ensure_token() {
|
||||
if [ -z "${TOKEN:-}" ]; then
|
||||
TOKEN="$(gen_token)"
|
||||
fi
|
||||
}
|
||||
|
||||
is_musl() {
|
||||
ldd --version 2>&1 | grep -qi musl && return 0
|
||||
[ -e /lib/ld-musl-*.so.1 ] 2>/dev/null && return 0
|
||||
@ -573,46 +598,58 @@ ensure_gateway_config() {
|
||||
host_header_fallback="true"
|
||||
fi
|
||||
|
||||
local allow_insecure="false"
|
||||
if [ "${ALLOW_INSECURE_AUTH:-0}" = "1" ]; then
|
||||
allow_insecure="true"
|
||||
local allow_insecure=""
|
||||
if [ -n "${ALLOW_INSECURE_AUTH:-}" ]; then
|
||||
if [ "$ALLOW_INSECURE_AUTH" = "1" ]; then
|
||||
allow_insecure="true"
|
||||
else
|
||||
allow_insecure="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
local disable_device_auth="false"
|
||||
if [ "${DISABLE_DEVICE_AUTH:-0}" = "1" ]; then
|
||||
disable_device_auth="true"
|
||||
local disable_device_auth=""
|
||||
if [ -n "${DISABLE_DEVICE_AUTH:-}" ]; then
|
||||
if [ "$DISABLE_DEVICE_AUTH" = "1" ]; then
|
||||
disable_device_auth="true"
|
||||
else
|
||||
disable_device_auth="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
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
|
||||
selected_key="$(agent_key_name)"
|
||||
selected_model="$(agent_default_model)"
|
||||
selected_base_url="$(agent_default_base_url)"
|
||||
selected_key=""
|
||||
selected_model=""
|
||||
selected_base_url=""
|
||||
override_base_url=""
|
||||
base_url_mode="default"
|
||||
if [ -n "${PROVIDER_BASE_URL:-}" ]; then
|
||||
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:-anthropic}: ${PROVIDER_BASE_URL}"
|
||||
base_url_mode="preserve"
|
||||
base_url_mode=""
|
||||
if [ -n "${DEFAULT_AGENT:-}" ]; then
|
||||
selected_key="$(agent_key_name)"
|
||||
selected_model="$(agent_default_model)"
|
||||
selected_base_url="$(agent_default_base_url)"
|
||||
base_url_mode="default"
|
||||
if [ -n "${PROVIDER_BASE_URL:-}" ]; then
|
||||
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
|
||||
|
||||
mkdir -p "$(dirname "$cfg")" 2>/dev/null || true
|
||||
CFG_PATH="$cfg" \
|
||||
ALLOWED_ORIGINS_RAW="$allowed_origins" \
|
||||
DEFAULT_ALLOWED_ORIGINS_RAW="$(default_allowed_origins)" \
|
||||
GATEWAY_PORT="$PORT" \
|
||||
GATEWAY_BIND="$BIND" \
|
||||
GATEWAY_TOKEN="$TOKEN" \
|
||||
GATEWAY_ALLOW_INSECURE="$allow_insecure" \
|
||||
GATEWAY_DISABLE_DEVICE_AUTH="$disable_device_auth" \
|
||||
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_MODEL="$selected_model" \
|
||||
DEFAULT_AGENT_BASE_URL="$selected_base_url" \
|
||||
@ -635,6 +672,25 @@ local function bool_env(name)
|
||||
return os.getenv(name) == "true"
|
||||
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 out = {}
|
||||
raw = raw or ""
|
||||
@ -685,123 +741,166 @@ local gateway = ensure_table(cfg, "gateway")
|
||||
gateway.mode = "local"
|
||||
gateway.port = tonumber(os.getenv("GATEWAY_PORT")) or 18789
|
||||
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")
|
||||
control.enabled = true
|
||||
control.basePath = nil
|
||||
control.allowedOrigins = split_lines(os.getenv("ALLOWED_ORIGINS_RAW"))
|
||||
control.allowInsecureAuth = bool_env("GATEWAY_ALLOW_INSECURE")
|
||||
control.dangerouslyDisableDeviceAuth = bool_env("GATEWAY_DISABLE_DEVICE_AUTH")
|
||||
local configured_origins = optional_env("ALLOWED_ORIGINS_RAW")
|
||||
if configured_origins then
|
||||
control.allowedOrigins = split_lines(configured_origins)
|
||||
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")
|
||||
|
||||
local env = {}
|
||||
local selected_env_key = os.getenv("DEFAULT_AGENT_KEY")
|
||||
local selected_api_key = os.getenv("DEFAULT_AGENT_API_KEY") or ""
|
||||
if selected_env_key and selected_env_key ~= "" and selected_api_key ~= "" then
|
||||
local env = ensure_table(cfg, "env")
|
||||
local selected_env_key = optional_env("DEFAULT_AGENT_KEY")
|
||||
local selected_api_key = optional_env("DEFAULT_AGENT_API_KEY")
|
||||
if selected_env_key and selected_api_key then
|
||||
env[selected_env_key] = selected_api_key
|
||||
end
|
||||
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")
|
||||
if type(models.mode) ~= "string" or models.mode == "" then
|
||||
models.mode = "merge"
|
||||
end
|
||||
local providers = ensure_table(models, "providers")
|
||||
|
||||
local current_provider_id = os.getenv("DEFAULT_AGENT_NAME") or "anthropic"
|
||||
local current_provider = ensure_table(providers, current_provider_id)
|
||||
local current_provider_id = optional_env("DEFAULT_AGENT_NAME")
|
||||
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
|
||||
current_provider.api = "openai-completions"
|
||||
current_provider.baseUrl = "https://api.openai.com/v1"
|
||||
current_provider.apiKey = selected_api_key
|
||||
update_api_key(current_provider)
|
||||
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
|
||||
current_provider.api = "anthropic-messages"
|
||||
current_provider.baseUrl = "https://api.anthropic.com"
|
||||
current_provider.apiKey = selected_api_key
|
||||
update_api_key(current_provider)
|
||||
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
|
||||
current_provider.api = "anthropic-messages"
|
||||
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.models = {
|
||||
{ id = "MiniMax-M2.5", name = "MiniMax M2.5" },
|
||||
{ id = current_model_id, name = current_model_id },
|
||||
}
|
||||
elseif current_provider_id == "moonshot" then
|
||||
current_provider.api = "openai-completions"
|
||||
current_provider.baseUrl = "https://api.moonshot.cn/v1"
|
||||
current_provider.apiKey = selected_api_key
|
||||
update_api_key(current_provider)
|
||||
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
|
||||
local primary_model = os.getenv("DEFAULT_AGENT_MODEL") or "custom-provider/custom-model"
|
||||
local custom_model_id = primary_model:match("^[^/]+/(.+)$") or primary_model
|
||||
if custom_model_id == "" then
|
||||
custom_model_id = "custom-model"
|
||||
if current_model_id == "" then
|
||||
current_model_id = "custom-model"
|
||||
end
|
||||
current_provider.api = "openai-completions"
|
||||
current_provider.baseUrl = os.getenv("DEFAULT_AGENT_OVERRIDE_BASE_URL") or current_provider.baseUrl
|
||||
current_provider.apiKey = selected_api_key
|
||||
current_provider.baseUrl = optional_env("DEFAULT_AGENT_OVERRIDE_BASE_URL") or current_provider.baseUrl
|
||||
update_api_key(current_provider)
|
||||
current_provider.authHeader = nil
|
||||
current_provider.models = {
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{ id = current_model_id, name = current_model_id },
|
||||
}
|
||||
end
|
||||
|
||||
local override_base = os.getenv("DEFAULT_AGENT_OVERRIDE_BASE_URL") or ""
|
||||
local base_url_mode = os.getenv("DEFAULT_AGENT_BASE_URL_MODE") or "default"
|
||||
local override_base = optional_env("DEFAULT_AGENT_OVERRIDE_BASE_URL") or ""
|
||||
local base_url_mode = optional_env("DEFAULT_AGENT_BASE_URL_MODE") or ""
|
||||
if current_provider_id == "custom-provider" and override_base ~= "" then
|
||||
current_provider.baseUrl = override_base
|
||||
elseif base_url_mode == "override" and override_base ~= "" then
|
||||
current_provider.baseUrl = override_base
|
||||
elseif base_url_mode == "default" then
|
||||
current_provider.baseUrl = os.getenv("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
|
||||
current_provider.baseUrl = optional_env("DEFAULT_AGENT_BASE_URL") or current_provider.baseUrl
|
||||
end
|
||||
|
||||
local agents = ensure_table(cfg, "agents")
|
||||
local defaults = ensure_table(agents, "defaults")
|
||||
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)
|
||||
if encoded then
|
||||
@ -814,7 +913,7 @@ f:close()
|
||||
|
||||
local env_path = cfg_path:gsub("openclaw%.json$", "openclaw.env")
|
||||
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)
|
||||
val = val:gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub('"', '\\"')
|
||||
envf:write(selected_env_key .. '="' .. val .. '"\n')
|
||||
@ -1147,11 +1246,7 @@ upgrade_openclaw() {
|
||||
uci -q commit "$UCI_NS" >/dev/null 2>&1 || true
|
||||
|
||||
# Ensure token exists even if runtime is already installed (service start requires it).
|
||||
TOKEN="$(uci_get token)"
|
||||
if [ -z "$TOKEN" ]; then
|
||||
TOKEN="$(gen_token)"
|
||||
uci -q set "${UCI_NS}.main.token=$TOKEN" && uci -q commit "$UCI_NS" || true
|
||||
fi
|
||||
ensure_token
|
||||
ensure_gateway_config || true
|
||||
|
||||
if have_openclaw_runtime; then
|
||||
@ -1233,6 +1328,7 @@ do_start() {
|
||||
write_installer_log "== start begin =="
|
||||
ensure_dirs
|
||||
fix_data_permissions || true
|
||||
ensure_token
|
||||
ensure_safe_port_for_start || exit 1
|
||||
/etc/init.d/openclawmgr enable >/dev/null 2>&1 || 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 =="
|
||||
ensure_dirs
|
||||
fix_data_permissions || true
|
||||
ensure_token
|
||||
ensure_safe_port_for_start || exit 1
|
||||
ensure_gateway_config || true
|
||||
/etc/init.d/openclawmgr restart >/dev/null 2>&1 || true
|
||||
@ -1266,6 +1363,7 @@ do_restart() {
|
||||
write_installer_log "== apply_config begin =="
|
||||
ensure_dirs
|
||||
fix_data_permissions || true
|
||||
ensure_token
|
||||
ensure_gateway_config || true
|
||||
|
||||
local pid=""
|
||||
@ -1425,14 +1523,8 @@ PROVIDER_BASE_URL="$(uci_get provider_base_url)"
|
||||
[ -n "$BIND" ] || BIND="lan"
|
||||
[ -n "$ENABLED" ] || ENABLED="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_CHANNEL" ] || INSTALL_CHANNEL="stable"
|
||||
[ -n "$PROVIDER_API_KEY" ] || PROVIDER_API_KEY=""
|
||||
[ -n "$PROVIDER_BASE_URL" ] || PROVIDER_BASE_URL=""
|
||||
|
||||
case "$PORT" in
|
||||
''|*[!0-9]*) PORT="18789" ;;
|
||||
@ -1449,14 +1541,17 @@ case "$ENABLED" in
|
||||
*) ENABLED="0" ;;
|
||||
esac
|
||||
case "$ALLOW_INSECURE_AUTH" in
|
||||
'') ;;
|
||||
1|true|yes|on) ALLOW_INSECURE_AUTH="1" ;;
|
||||
*) ALLOW_INSECURE_AUTH="0" ;;
|
||||
esac
|
||||
case "$DISABLE_DEVICE_AUTH" in
|
||||
'') ;;
|
||||
1|true|yes|on) DISABLE_DEVICE_AUTH="1" ;;
|
||||
*) DISABLE_DEVICE_AUTH="0" ;;
|
||||
esac
|
||||
case "$DEFAULT_AGENT" in
|
||||
'') ;;
|
||||
openai|anthropic|minimax-cn|moonshot|custom-provider) ;;
|
||||
*) DEFAULT_AGENT="anthropic" ;;
|
||||
esac
|
||||
@ -1496,6 +1591,21 @@ NODE_DIR="${BASE_DIR}/node"
|
||||
GLOBAL_DIR="${BASE_DIR}/global"
|
||||
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"
|
||||
NPM_BIN="${NODE_DIR}/bin/npm"
|
||||
|
||||
|
||||
@ -33,6 +33,45 @@ prompt_default() {
|
||||
|
||||
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() {
|
||||
BASE_DIR="${BASE_DIR:-$(uci_get base_dir)}"
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
@ -54,6 +93,8 @@ init_paths() {
|
||||
export OPENCLAW_STATE_DIR="${DATA_DIR}/.openclaw"
|
||||
export OPENCLAW_CONFIG_PATH="$CONFIG_FILE"
|
||||
export PATH="${NODE_DIR}/bin:${GLOBAL_DIR}/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
init_git_transport
|
||||
init_npm_env
|
||||
}
|
||||
|
||||
find_openclaw_entry() {
|
||||
@ -81,6 +122,51 @@ openclaw_cmd() {
|
||||
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() {
|
||||
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
@ -117,15 +203,35 @@ action_configure() {
|
||||
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() {
|
||||
while true; do
|
||||
printf '\n'
|
||||
printf '%s\n' "${BOLD}OpenClaw AI Gateway — CLI 配置入口(OpenClawMgr)${NC}"
|
||||
printf '%s\n' "${DIM}base_dir: ${BASE_DIR}${NC}"
|
||||
printf '\n'
|
||||
printf '%s\n' " ${CYAN}1)${NC} 🧭 官方配置向导 ${DIM}(openclaw configure)${NC}"
|
||||
printf '%s\n' " ${CYAN}2)${NC} 💾 备份配置"
|
||||
printf '%s\n' " ${CYAN}3)${NC} 📥 恢复配置"
|
||||
printf '%s\n' " ${CYAN}1)${NC} 💻 CLI环境 ${DIM}(仅注入环境,不执行 configure)${NC}"
|
||||
printf '%s\n' " ${CYAN}2)${NC} 🧭 官方配置向导 ${DIM}(openclaw configure)${NC}"
|
||||
printf '%s\n' " ${CYAN}3)${NC} 💾 备份配置"
|
||||
printf '%s\n' " ${CYAN}4)${NC} 📥 恢复配置"
|
||||
printf '\n'
|
||||
printf '%s\n' " ${CYAN}0)${NC} 退出"
|
||||
printf '\n'
|
||||
@ -133,9 +239,10 @@ main_menu() {
|
||||
local c=""
|
||||
c="$(prompt_default "请选择" "1")"
|
||||
case "$c" in
|
||||
1) action_configure; pause_enter ;;
|
||||
2) action_backup_config || true; pause_enter ;;
|
||||
3) action_restore_config || true; pause_enter ;;
|
||||
1) action_cli_env ;;
|
||||
2) action_configure; pause_enter ;;
|
||||
3) action_backup_config || true; pause_enter ;;
|
||||
4) action_restore_config || true; pause_enter ;;
|
||||
0) ok "再见!"; exit 0 ;;
|
||||
*) warn "无效选择" ;;
|
||||
esac
|
||||
@ -146,12 +253,13 @@ init_paths
|
||||
|
||||
case "${1:-}" in
|
||||
--help|-h)
|
||||
echo "Usage: openclawmgr-cli.sh [configure|backup|restore]"
|
||||
echo "Usage: openclawmgr-cli.sh [configure|cli-env|backup|restore]"
|
||||
;;
|
||||
*)
|
||||
case "${1:-menu}" in
|
||||
menu) main_menu ;;
|
||||
configure) action_configure; pause_enter ;;
|
||||
cli-env) action_cli_env ;;
|
||||
backup) action_backup_config || true; pause_enter ;;
|
||||
restore) action_restore_config || true; pause_enter ;;
|
||||
*) echo "Unknown command: ${1:-}" >&2; exit 2 ;;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,11 +1,13 @@
|
||||
<%
|
||||
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>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ if node then
|
||||
end
|
||||
end
|
||||
-%>
|
||||
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
|
||||
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
|
||||
|
||||
<style>
|
||||
table th, .table .th {
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
<%
|
||||
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>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -4,11 +4,13 @@ local fs = api.fs
|
||||
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.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>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -546,7 +546,7 @@ load_acl() {
|
||||
$ipt_m -A PSW2 $(comment "${comment_d}") -p udp $(REDIRECT $REDIR_PORT TPROXY)
|
||||
|
||||
[ "$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
|
||||
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"
|
||||
|
||||
@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=microsocks
|
||||
PKG_VERSION:=1.0.5
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/rofl0r/microsocks/tar.gz/v$(PKG_VERSION)?
|
||||
@ -25,7 +25,7 @@ define Package/microsocks
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=Tiny, portable SOCKS5 server
|
||||
TITLE:=Tiny, portable SOCKS5 server. Support forwarding rules
|
||||
URL:=https://github.com/rofl0r/microsocks
|
||||
DEPENDS:=+libpthread
|
||||
endef
|
||||
@ -33,6 +33,7 @@ endef
|
||||
define Package/microsocks/description
|
||||
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.
|
||||
This version supports forwarding rules.
|
||||
endef
|
||||
|
||||
define Package/microsocks/install
|
||||
|
||||
428
microsocks/patches/100-Add-SOCKS5-forwarding-rules-support.patch
Normal file
428
microsocks/patches/100-Add-SOCKS5-forwarding-rules-support.patch
Normal file
@ -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)) {
|
||||
@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mihomo
|
||||
PKG_VERSION:=1.19.22
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=1.19.23
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=nikki
|
||||
PKG_VERSION:=2026.04.01
|
||||
PKG_RELEASE:=9
|
||||
PKG_VERSION:=2026.04.08
|
||||
PKG_RELEASE:=10
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_SOURCE_PROTO:=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_LICENSE:=GPL3.0+
|
||||
|
||||
@ -35,6 +35,7 @@ config mixin 'mixin'
|
||||
option 'mode' 'rule'
|
||||
option 'match_process' 'off'
|
||||
option 'ipv6' '1'
|
||||
option 'ui_path' 'ui'
|
||||
option 'ui_url' 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip'
|
||||
option 'api_listen' '[::]:9090'
|
||||
option 'selection_cache' '1'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user