feat(agent): agent 0.10.0 新增资源/日志/配置回读端点,前端接入 VPS 资源与公网 Nginx 日志(v1.2.4)

agent 0.10.0:新增 GET /api/v1/metrics(资源采集,复用 internal/sysinfo;CPU 两次采样差值 + 1 秒内复用上次结果)、GET /api/v1/logs/nginx(日志尾部,路径白名单来自 nginx -T / 环境变量 / 默认目录,32KB 块倒读、单行 4KB 截断、上限 2000 行)、GET /api/v1/frps/config(frps.toml 回读,256KB 上限)

internal/sysinfo:补 LoadAvg 与 UptimeSeconds(Linux /proc/loadavg 与 /proc/uptime、Windows GetTickCount64、其它平台空值)

应用侧:AgentClient 三个只读方法;request 错误改为带状态码的 statusError,新增 ErrAgentEndpointMissing,把 404/405 统一转成「agent 版本过低需 ≥ 0.10.0」提示;补 service/handler/router

前端:公网 Agent 页「VPS 资源」横排带(占满环境与依赖整行);日志中心「公网 Nginx」页签;frps 同源配置弹窗「比对 VPS 实际文件」

二进制:内嵌 fonu-agent-linux-amd64 / arm64 用新代码重编;VERSION → 1.2.4
This commit is contained in:
2026-09-24 10:15:11 +08:00
parent f75e672a45
commit 2f59f5adc4
19 changed files with 953 additions and 7 deletions
+54 -1
View File
@@ -589,9 +589,17 @@
已启用的 TCP/UDP 隧道远程端口:<span class="mono">{{ tcpRemotePorts.join('、') }}</span>
—— 请确认 VPS 防火墙 / 安全组已放行这些端口(frps 侧 allowPorts 已按此生成,未放行时隧道会连不上)。
</p>
<p v-if="frpsCompare" class="form-hint" :class="{ 'frps-compare--warn': !frpsCompare.ok }">
{{ frpsCompare.text }}
</p>
<pre class="logs-box">{{ frpsConfigContent || '生成中…' }}</pre>
</div>
<div class="modal-footer"><n-button @click="showFRPSConfig = false">关闭</n-button></div>
<div class="modal-footer">
<n-button size="small" :loading="comparingFrps" :disabled="!frpsConfigTarget" @click="compareFrpsConfig">
比对 VPS 实际文件
</n-button>
<n-button @click="showFRPSConfig = false">关闭</n-button>
</div>
</div>
</n-modal>
@@ -940,6 +948,47 @@ const showServerDetail = ref(false)
// frps.toml 同源配置弹窗:按服务端生成,与 frpc 侧端口/认证一致
const showFRPSConfig = ref(false)
const frpsConfigContent = ref('')
// frps.toml 与 VPS 实际文件的比对结果(P1-3,需 agent ≥ 0.10.0)
const frpsCompare = ref<{ ok: boolean; text: string } | null>(null)
const comparingFrps = ref(false)
// 比对前统一去注释、空行与首尾空白:注释和排版差异不算「被手工修改」
function normalizedConfigLines(text: string) {
return text
.split('\n')
.map((line) => line.trim())
.filter((line) => line !== '' && !line.startsWith('#'))
}
async function compareFrpsConfig() {
const target = frpsConfigTarget.value
if (!target) return
comparingFrps.value = true
frpsCompare.value = null
try {
const remote = await api.getFrpAgentFRPSConfig(target.id)
if (!remote.exists) {
frpsCompare.value = { ok: false, text: `VPS 上还没有 ${remote.path || 'frps.toml'},可直接下发写入` }
return
}
const expected = normalizedConfigLines(frpsConfigContent.value)
const actual = normalizedConfigLines(remote.content)
const index = expected.findIndex((line, i) => actual[i] !== line)
if (index === -1 && expected.length === actual.length) {
frpsCompare.value = { ok: true, text: `与 VPS 上的 ${remote.path} 一致(已忽略注释与空行)` }
return
}
const at = index === -1 ? Math.min(expected.length, actual.length) : index
frpsCompare.value = {
ok: false,
text: `不一致:第 ${at + 1} 行 下发「${expected[at] ?? '(无)'}」/ VPS「${actual[at] ?? '(无)'}」——VPS 上的 frps.toml 可能被手工改过,重新下发会覆盖`,
}
} catch (error) {
frpsCompare.value = { ok: false, text: error instanceof Error ? error.message : '比对失败' }
} finally {
comparingFrps.value = false
}
}
const frpsConfigTarget = ref<FrpServer | null>(null)
const frpsConfigLoading = ref(false)
const showRuntimeModal = ref(false)
@@ -1747,6 +1796,7 @@ async function loadExport() {
async function openFRPSConfig(server: FrpServer) {
frpsConfigTarget.value = server
showFRPSConfig.value = true
frpsCompare.value = null
frpsConfigLoading.value = true
try { frpsConfigContent.value = (await api.getFrpFRPSConfig(server.id)).content }
catch (error) {
@@ -2148,4 +2198,7 @@ onUnmounted(() => { if (timer) window.clearInterval(timer); stopDownloadPolling(
@media (max-width: 1100px) { .stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px) { .stats-row, .form-grid, .download-proxy-form, .server-detail__grid, .detail-fields, .detail-stats, .server-status-summary { grid-template-columns: 1fr; } .server-status-summary__item--state { min-height: 0; padding-right: 0; padding-bottom: var(--fonu-space-3); border-right: 0; border-bottom: 1px solid var(--fonu-border); } .status-bars { gap: 3px; } .status-bar { height: 28px; } .binary-row { align-items: flex-start; flex-direction: column; } .release-row { grid-template-columns: 1fr; } .release-progress-placeholder { display: none; } .switch-row span:nth-of-type(2) { margin-left: 0; } }
@media (max-width: 760px) { .frp-proxy-modal { display: block; max-height: 92vh; overflow-y: auto; } .frp-proxy-modal .proxy-modal__form { max-height: none; } .frp-proxy-modal .proxy-modal__scroll { overflow: visible; } .frp-proxy-modal .proxy-modal__help { width: auto; border-top: 1px solid var(--fonu-border); border-left: 0; } }
.frps-compare--warn {
color: var(--fonu-warning);
}
</style>