💋 Sync 2026-04-08 20:31:38

This commit is contained in:
github-actions[bot] 2026-04-08 20:31:38 +08:00
parent d6e7b3ef08
commit 92a3fac9ca
29 changed files with 3458 additions and 1106 deletions

View File

@ -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({

View File

@ -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

View File

@ -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,

View File

@ -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
} : {})
}
};

View File

@ -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

View File

@ -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;
}

View File

@ -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, "&#39;");
}
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();
});
}

View File

@ -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

View File

@ -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)

View File

@ -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"
};
})();

View File

@ -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 ''

View File

@ -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=""

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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"

View File

@ -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

View 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)) {

View File

@ -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)?

View File

@ -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+

View File

@ -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'