feat: sync httpdns sdk/platform updates without large binaries

This commit is contained in:
robin
2026-03-04 17:59:14 +08:00
parent 853897a6f8
commit 532891fad0
700 changed files with 6096 additions and 2712 deletions

View File

@@ -1,3 +1,139 @@
{$layout}
<p class="comment">此功能暂未开放,敬请期待。</p>
<div class="margin"></div>
<!-- 自动升级设置 -->
<div class="ui segment">
<h3>自动升级</h3>
<table class="ui table definition">
<tr>
<td class="title">开启自动升级</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="autoUpgrade" v-model="config.autoUpgrade" @change="updateAutoUpgrade">
<label></label>
</div>
<p class="comment">开启后边缘节点、DNS节点、HTTPDNS节点每分钟检查新版本并自动下载升级。关闭后节点不会自动升级但管理员仍可在下方手动升级。</p>
</td>
</tr>
</table>
</div>
<div class="margin"></div>
<!-- 手动升级 -->
<div class="ui segment">
<h3 style="display: flex; justify-content: space-between; align-items: center;">
<span>手动升级</span>
<button class="ui button primary tiny" v-if="totalUpgradeCount > 0"
@click.prevent="upgradeAll()">全部升级({{totalUpgradeCount}})</button>
</h3>
<div v-if="modules.length == 0">
<p class="comment">暂无需要升级的节点。</p>
</div>
<div v-for="mod in modules" class="ui segment" style="margin-bottom: 1em; padding: 0;">
<h4 style="cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 0.8em 1em; margin: 0; background: #f9fafb; border-bottom: 1px solid rgba(34,36,38,.15);"
@click.prevent="mod.expanded = !mod.expanded">
<div>
<i class="icon angle down" v-if="mod.expanded"></i>
<i class="icon angle right" v-else></i>
{{mod.name}}
<span class="ui label tiny basic" v-if="mod.count > 0"
style="margin-left: 0.5em;">{{mod.count}}个待升级</span>
</div>
<button class="ui button primary tiny" v-if="mod.count > 0"
@click.stop.prevent="upgradeModule(mod.code)">升级所有{{mod.name}}</button>
</h4>
<div v-show="mod.expanded" style="padding: 1em;">
<div v-for="cluster in mod.clusters" style="margin-bottom: 1em;">
<h5 style="cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 0.6em; background: #f3f4f5; border-radius: 4px; margin: 0;"
@click.prevent="cluster.expanded = !cluster.expanded">
<div>
<i class="icon angle down" v-if="cluster.expanded"></i>
<i class="icon angle right" v-else></i>
{{cluster.name}}
<span class="ui label tiny basic" style="margin-left: 0.5em;">{{cluster.count}}个待升级</span>
</div>
<div>
<button class="ui button tiny primary" v-if="countCheckedNodesInCluster(cluster) > 0"
@click.stop.prevent="upgradeBatchInCluster(mod.code, cluster)">批量升级({{countCheckedNodesInCluster(cluster)}})</button>
<button class="ui button tiny"
@click.stop.prevent="upgradeCluster(mod.code, cluster.id)">升级集群内所有节点</button>
</div>
</h5>
<div v-show="cluster.expanded" style="margin-top: 0.5em;">
<table class="ui table selectable celled small" style="margin: 0;">
<thead>
<tr>
<th style="width:3em">
<div class="ui checkbox" @click.prevent="toggleCheckAll(cluster)">
<input type="checkbox" :checked="isAllChecked(cluster)">
<label></label>
</div>
</th>
<th>节点名</th>
<th>访问IP</th>
<th>SSH地址</th>
<th>版本变化</th>
<th class="four wide">节点状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="node in cluster.nodes">
<td>
<div class="ui checkbox" v-if="!isNodeUpgradeFinished(node)">
<input type="checkbox" v-model="node.isChecked">
<label></label>
</div>
</td>
<td>
{{node.name}}
<a :href="nodeDetailURL(mod.code, cluster.id, node.id)" title="节点详情" style="margin-left: 0.4em"><i class="icon external alternate small"></i></a>
</td>
<td>
<span v-if="node.accessIP && node.accessIP.length > 0" class="ui label tiny basic">{{node.accessIP}}</span>
<span v-else class="disabled">-</span>
</td>
<td>
<span v-if="node.login != null && node.login.type == 'ssh' && node.loginParams != null && node.loginParams.host != null && node.loginParams.host.length > 0">
{{node.loginParams.host}}:{{node.loginParams.port}}
</span>
<span v-else class="disabled">没有设置</span>
</td>
<td>
<span v-if="node.oldVersion">v{{node.oldVersion}}</span>
<span v-else class="ui label tiny">未知</span>
-&gt; <strong>v{{node.newVersion}}</strong>
</td>
<td>
<div v-if="node.installStatus != null && (node.installStatus.isRunning || node.installStatus.isFinished)">
<div v-if="node.installStatus.isRunning && !node.installStatus.isFinished"
class="blue">
<i class="notched circle loading icon"></i> 升级中...
</div>
<div v-if="node.installStatus.isFinished">
<span v-if="node.installStatus.isOk" class="green"><i
class="icon check circle"></i>
已升级成功</span>
<span v-if="!node.installStatus.isOk" class="red"><i
class="icon warning circle"></i>
升级过程中发生错误:{{node.installStatus.error}}</span>
</div>
</div>
<span v-else class="disabled">等待升级</span>
</td>
<td>
<a href="" @click.prevent="upgradeNode(mod.code, node)" v-if="!node.isUpgrading">升级</a>
<span v-if="node.isUpgrading" class="blue">升级中...</span>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,457 @@
Tea.context(function () {
let that = this
// 计算总待升级数
this.totalUpgradeCount = 0
this.modules.forEach(function (mod) {
mod.expanded = true
that.totalUpgradeCount += mod.count
if (mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
cluster.expanded = true
if (cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
node.isUpgrading = false
node.isChecked = false
})
}
})
}
})
// 正在升级的节点ID集合按模块
this.upgradingNodeIds = {
"node": [],
"dns": [],
"httpdns": []
}
// 启动状态轮询
this.$delay(function () {
this.pollStatus()
})
/**
* 获取节点详情页URL
*/
this.nodeDetailURL = function (moduleCode, clusterId, nodeId) {
switch (moduleCode) {
case "node":
return "/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
case "dns":
return "/ns/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
case "httpdns":
return "/httpdns/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
default:
return "#"
}
}
/**
* 判断节点升级是否已完成(成功)
*/
this.isNodeUpgradeFinished = function (node) {
return node.installStatus != null && node.installStatus.isFinished && node.installStatus.isOk
}
/**
* 全选/取消全选
*/
this.toggleCheckAll = function (cluster) {
if (cluster.nodes == null) return
let allChecked = that.isAllChecked(cluster)
cluster.nodes.forEach(function (node) {
if (!that.isNodeUpgradeFinished(node)) {
node.isChecked = !allChecked
}
})
}
/**
* 是否全部选中
*/
this.isAllChecked = function (cluster) {
if (cluster.nodes == null || cluster.nodes.length == 0) return false
let checkableNodes = cluster.nodes.filter(function (node) {
return !that.isNodeUpgradeFinished(node)
})
if (checkableNodes.length == 0) return false
return checkableNodes.every(function (node) { return node.isChecked })
}
/**
* 计算集群内选中的节点数
*/
this.countCheckedNodesInCluster = function (cluster) {
if (cluster.nodes == null) return 0
return cluster.nodes.filter(function (node) { return node.isChecked }).length
}
/**
* 全部升级
*/
this.upgradeAll = function () {
var vue = this
teaweb.confirm("确定要升级所有模块的所有待升级节点吗?", function () {
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: "",
scope: "all",
clusterId: 0,
nodeId: 0
})
.success(function () {
// 标记所有节点为升级中
that.modules.forEach(function (mod) {
if (mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
if (cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
that.trackNode(mod.code, node.id)
})
}
})
}
})
})
.fail(function (resp) {
teaweb.warn("升级请求失败:" + resp.message)
})
})
}
/**
* 按模块升级
*/
this.upgradeModule = function (moduleCode) {
var vue = this
teaweb.confirm("确定要升级该模块的所有待升级节点吗?", function () {
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: moduleCode,
scope: "module",
clusterId: 0,
nodeId: 0
})
.success(function () {
that.markModuleUpgrading(moduleCode)
})
.fail(function (resp) {
teaweb.warn("升级请求失败:" + resp.message)
})
})
}
/**
* 按集群升级
*/
this.upgradeCluster = function (moduleCode, clusterId) {
var vue = this
teaweb.confirm("确定要升级该集群的所有待升级节点吗?", function () {
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: moduleCode,
scope: "cluster",
clusterId: clusterId,
nodeId: 0
})
.success(function () {
that.markClusterUpgrading(moduleCode, clusterId)
})
.fail(function (resp) {
teaweb.warn("升级请求失败:" + resp.message)
})
})
}
/**
* 批量升级集群内选中的节点
*/
this.upgradeBatchInCluster = function (moduleCode, cluster) {
if (cluster.nodes == null) return
var checkedNodes = cluster.nodes.filter(function (node) { return node.isChecked })
if (checkedNodes.length == 0) return
var vue = this
teaweb.confirm("确定要批量升级选中的 " + checkedNodes.length + " 个节点吗?", function () {
checkedNodes.forEach(function (node) {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
node.isChecked = false
that.trackNode(moduleCode, node.id)
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: moduleCode,
scope: "node",
clusterId: 0,
nodeId: node.id
})
})
})
}
/**
* 升级单个节点
*/
this.upgradeNode = function (moduleCode, node) {
var vue = this
teaweb.confirm("确定要升级节点 \"" + node.name + "\" 吗?", function () {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
that.trackNode(moduleCode, node.id)
vue.$post("/settings/upgrade/upgradeNode")
.params({
module: moduleCode,
scope: "node",
clusterId: 0,
nodeId: node.id
})
.success(function () { })
.fail(function (resp) {
node.isUpgrading = false
teaweb.warn("升级请求失败:" + resp.message)
})
})
}
/**
* 标记模块下所有节点为升级中
*/
this.markModuleUpgrading = function (moduleCode) {
that.modules.forEach(function (mod) {
if (mod.code == moduleCode && mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
if (cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
that.trackNode(moduleCode, node.id)
})
}
})
}
})
}
/**
* 标记集群下所有节点为升级中
*/
this.markClusterUpgrading = function (moduleCode, clusterId) {
that.modules.forEach(function (mod) {
if (mod.code == moduleCode && mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
if (cluster.id == clusterId && cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
node.installStatus = {
isRunning: true,
isFinished: false,
isOk: false,
error: "",
errorCode: ""
}
node.isUpgrading = true
that.trackNode(moduleCode, node.id)
})
}
})
}
})
}
/**
* 追踪节点升级状态
*/
this.trackNode = function (moduleCode, nodeId) {
if (that.upgradingNodeIds[moduleCode] == null) {
that.upgradingNodeIds[moduleCode] = []
}
if (that.upgradingNodeIds[moduleCode].indexOf(nodeId) < 0) {
that.upgradingNodeIds[moduleCode].push(nodeId)
}
}
/**
* 状态轮询
*/
this.pollStatus = function () {
var vue = this
// 检查是否有正在升级的节点
let hasUpgrading = false
for (let key in that.upgradingNodeIds) {
if (that.upgradingNodeIds[key].length > 0) {
hasUpgrading = true
break
}
}
if (!hasUpgrading) {
setTimeout(function () { that.pollStatus() }, 5000)
return
}
vue.$post("/settings/upgrade/status")
.params({
nodeIdsJSON: JSON.stringify(that.upgradingNodeIds)
})
.success(function (resp) {
let statuses = resp.data.statuses
if (statuses == null) {
return
}
// 更新各模块节点状态
for (let moduleCode in statuses) {
let nodeStatuses = statuses[moduleCode]
if (nodeStatuses == null) {
continue
}
nodeStatuses.forEach(function (ns) {
that.updateNodeStatus(moduleCode, ns.id, ns.installStatus)
})
}
})
.done(function () {
setTimeout(function () { that.pollStatus() }, 3000)
})
}
/**
* 更新节点安装状态
*/
this.updateNodeStatus = function (moduleCode, nodeId, installStatus) {
that.modules.forEach(function (mod) {
if (mod.code == moduleCode && mod.clusters != null) {
mod.clusters.forEach(function (cluster) {
if (cluster.nodes != null) {
cluster.nodes.forEach(function (node) {
if (node.id == nodeId && installStatus != null) {
node.installStatus = installStatus
// 升级完成后移除跟踪
if (installStatus.isFinished) {
node.isUpgrading = false
let idx = that.upgradingNodeIds[moduleCode].indexOf(nodeId)
if (idx >= 0) {
that.upgradingNodeIds[moduleCode].splice(idx, 1)
}
if (installStatus.isOk) {
// 升级成功,延迟后从列表中移除
setTimeout(function () {
let nIdx = cluster.nodes.indexOf(node)
if (nIdx >= 0) {
cluster.nodes.splice(nIdx, 1)
cluster.count--
mod.count--
that.totalUpgradeCount--
if (cluster.count <= 0) {
let cIdx = mod.clusters.indexOf(cluster)
if (cIdx >= 0) {
mod.clusters.splice(cIdx, 1)
}
}
if (mod.count <= 0) {
let mIdx = that.modules.indexOf(mod)
if (mIdx >= 0) {
that.modules.splice(mIdx, 1)
}
}
}
}, 2000)
} else {
// 升级失败,根据 errorCode 给出具体提示
that.handleUpgradeError(moduleCode, node, installStatus)
}
}
}
})
}
})
}
})
}
/**
* 处理升级错误,根据 errorCode 提供更友好的提示
*/
this.handleUpgradeError = function (moduleCode, node, installStatus) {
let errorCode = installStatus.errorCode || ""
let errMsg = installStatus.error || ""
switch (errorCode) {
case "EMPTY_LOGIN":
case "EMPTY_SSH_HOST":
case "EMPTY_SSH_PORT":
case "EMPTY_GRANT":
// SSH 信息未配置的错误,不弹窗(页面上已有"没有设置"提示)
break
case "CREATE_ROOT_DIRECTORY_FAILED":
teaweb.warn("节点 \"" + node.name + "\" 创建根目录失败:" + errMsg)
break
case "INSTALL_HELPER_FAILED":
teaweb.warn("节点 \"" + node.name + "\" 安装助手失败:" + errMsg)
break
case "TEST_FAILED":
teaweb.warn("节点 \"" + node.name + "\" 环境测试失败:" + errMsg)
break
case "RPC_TEST_FAILED":
teaweb.warn("节点 \"" + node.name + "\" RPC通讯测试失败" + errMsg)
break
}
}
/**
* 更新自动升级状态
*/
this.updateAutoUpgrade = function () {
var vue = this
// @change 已经翻转了值,先记录新值并立即恢复
let newValue = that.config.autoUpgrade
that.config.autoUpgrade = !newValue
let msg = newValue ? "确定要开启自动升级功能吗?开启后节点会自动下载安装新版本。" : "确定要关闭自动升级功能吗?关闭后只能通过这里手动执行升级。"
teaweb.confirm(msg, function () {
vue.$post("/settings/upgrade")
.params({
autoUpgrade: newValue ? 1 : 0
})
.success(function () {
that.config.autoUpgrade = newValue
teaweb.successToast("设置保存成功")
})
.fail(function (resp) {
teaweb.warn("设置保存失败:" + resp.message)
})
})
}
})