feat: 主机环境一键初始化(页面内装依赖 + 修转发/GRE)
- 新增 GET /api/system/env:主机环境体检 * 运行权限 / IPv4 转发 / GRE 内核模块 / 包管理器 / 各协议依赖 / xl2tpd * 返回 failed/blocking/ready 汇总,供前端渲染与门控 - 新增 POST /api/system/fix:白名单修复动作 * ip_forward:sysctl 开启 + 持久化到 /etc/sysctl.d/99-opsx-vpn.conf * gre_module:modprobe + 持久化到 /etc/modules-load.d/opsx-vpn.conf * install:<协议>:复用 binmgr.InstallViaPackageManager(apt/yum/dnf/apk/pacman) * tool:xl2tpd:L2TP 依赖补齐 * 仅 admin 可调用;actions 与协议名均走白名单,绝不接受调用方传入命令串;全部写审计日志 - Dashboard:协议「安装命令」按钮改为「一键安装」(调 /binaries/install,含包管理器降级) - Dashboard:主机前提警告条新增「一键修复」按钮(按缺失项逐项修复) - i18n:中英文补齐 oneClickInstall / oneClickFix / fixSuccess / fixFailed / fixNeedRoot / fixNothing
This commit is contained in:
1 parent
ef7eb378d2
commit
a15786ea84
6 files changed
+450
-6
No files matched your search
@@ -108,7 +108,13 @@
|
||||
"otherDistros": "Other Distros",
|
||||
"copyRecommended": "Copy Recommended",
|
||||
"getInstallCmdFailed": "Failed to get install command",
|
||||
"aptDefault": "apt default"
|
||||
"aptDefault": "apt default",
|
||||
"oneClickInstall": "Install",
|
||||
"oneClickFix": "Auto Fix",
|
||||
"fixSuccess": "Fixed: {items}",
|
||||
"fixFailed": "Fix failed: {items}",
|
||||
"fixNeedRoot": "Root privileges required to modify system config",
|
||||
"fixNothing": "Nothing to auto-fix"
|
||||
},
|
||||
"tunnels": {
|
||||
"searchPlaceholder": "Search by name",
|
||||
|
||||
@@ -108,7 +108,13 @@
|
||||
"otherDistros": "其他发行版",
|
||||
"copyRecommended": "复制推荐命令",
|
||||
"getInstallCmdFailed": "获取安装命令失败",
|
||||
"aptDefault": "apt 默认"
|
||||
"aptDefault": "apt 默认",
|
||||
"oneClickInstall": "一键安装",
|
||||
"oneClickFix": "一键修复",
|
||||
"fixSuccess": "已修复:{items}",
|
||||
"fixFailed": "修复失败:{items}",
|
||||
"fixNeedRoot": "需要以 root 运行才能修改系统配置",
|
||||
"fixNothing": "当前没有可自动修复的项"
|
||||
},
|
||||
"tunnels": {
|
||||
"searchPlaceholder": "搜索名称",
|
||||
|
||||
@@ -29,7 +29,15 @@
|
||||
</el-row>
|
||||
|
||||
<el-alert v-if="readinessLoaded && !hostReady" class="host-readiness" type="warning" :closable="false" show-icon
|
||||
:title="$t('dashboard.hostReadinessWarning')" :description="readinessHint" />
|
||||
:title="$t('dashboard.hostReadinessWarning')" :description="readinessHint">
|
||||
<template #default>
|
||||
<div class="readiness-actions">
|
||||
<span class="readiness-detail">{{ readinessHint }}</span>
|
||||
<el-button v-if="readiness.root" size="small" type="warning" plain
|
||||
:loading="fixing" @click="fixHostEnv">{{ $t('dashboard.oneClickFix') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-row :gutter="16" style="margin-top:16px">
|
||||
<el-col :xs="24" :lg="10">
|
||||
@@ -63,9 +71,9 @@
|
||||
<el-table-column :label="$t('common.operation')" width="120">
|
||||
<template #default="{ row }">
|
||||
<template v-if="!row.available && !row.custom">
|
||||
<el-button v-if="deployableProtocols.includes(row.name)" size="small" type="primary"
|
||||
:loading="deploying === row.name" @click="deployProtocol(row.name)">{{ $t('dashboard.deploy') }}</el-button>
|
||||
<el-button v-else size="small" @click="showInstallCmd(row.name)">{{ $t('dashboard.installCmd') }}</el-button>
|
||||
<el-button size="small" type="primary"
|
||||
:loading="deploying === row.name" @click="deployProtocol(row.name)">{{ $t('dashboard.oneClickInstall') }}</el-button>
|
||||
<el-button size="small" link @click="showInstallCmd(row.name)">{{ $t('dashboard.installCmd') }}</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -129,10 +137,44 @@ let chart = null
|
||||
const deployableProtocols = ['frp', 'openvpn']
|
||||
const protocolToPkg = { wireguard: 'wireguard', ipsec: 'strongswan', ikev2: 'strongswan', l2tp: 'strongswan', openvpn: 'openvpn', frp: 'frp' }
|
||||
const deploying = ref('')
|
||||
const fixing = ref(false)
|
||||
const installDialogVisible = ref(false)
|
||||
const installDialogTitle = ref('')
|
||||
const installData = ref({ command: '', pm: '', detected: false, all_commands: {} })
|
||||
|
||||
// 一键修复主机环境(IPv4 转发 / GRE 模块)
|
||||
async function fixHostEnv() {
|
||||
if (!readiness.value.root) {
|
||||
ElMessage.warning(t('dashboard.fixNeedRoot'))
|
||||
return
|
||||
}
|
||||
fixing.value = true
|
||||
const done = []
|
||||
const failed = []
|
||||
try {
|
||||
const actions = []
|
||||
if (!readiness.value.ipv4_forwarding) actions.push(['ip_forward', t('dashboard.readinessForwarding')])
|
||||
if (readiness.value.gre_loaded === false) actions.push(['gre_module', 'GRE'])
|
||||
if (!actions.length) {
|
||||
ElMessage.info(t('dashboard.fixNothing'))
|
||||
return
|
||||
}
|
||||
for (const [action, label] of actions) {
|
||||
try {
|
||||
await api.post('/system/fix', { action })
|
||||
done.push(label)
|
||||
} catch (e) {
|
||||
failed.push(`${label}: ${e.response?.data?.error || e.message}`)
|
||||
}
|
||||
}
|
||||
if (done.length) ElMessage.success(t('dashboard.fixSuccess', { items: done.join('、') }))
|
||||
if (failed.length) ElMessage.error(t('dashboard.fixFailed', { items: failed.join('; ') }))
|
||||
await load()
|
||||
} finally {
|
||||
fixing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deployProtocol(name) {
|
||||
const pkg = protocolToPkg[name] || name
|
||||
deploying.value = name
|
||||
@@ -259,6 +301,8 @@ onUnmounted(() => {
|
||||
.stat-num { font-size: 26px; font-weight: 700; color: var(--text-primary); line-height: 1.2; }
|
||||
|
||||
.dep-hint { font-size: 12px; color: var(--text-secondary); font-weight: normal; }
|
||||
.readiness-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 4px; }
|
||||
.readiness-detail { font-size: 13px; color: var(--el-text-color-secondary); }
|
||||
.host-readiness { margin-top: var(--space-4); }
|
||||
.install-command { margin-top: var(--space-4); }
|
||||
.install-command-head, .other-command { display:flex; align-items:center; gap:8px; }
|
||||
|
||||
Reference in new issue
Block a user