feat: 配置版本历史/对比/回滚、国内 IM 通知渠道、阈值告警与 API Token(v1.2.9)
CI / verify (push) Canceled after 0s

- 配置版本:fsutil 版本化备份(内容相同自动去重),主 nginx.conf 与 VPS vhost 的版本列表/逐行差异/一键回滚,回滚先校验语法与 nginx -t 再写回

- agent 0.12.2:写 vhost 前留版本;nginx -t 校验失败改为恢复上一个可用版本(此前直接删文件),新增域名 vhost 版本端点,双架构二进制已重编

- 通知:补企业微信 / 钉钉 / 飞书机器人,切换预设时服务地址跟随该预设默认值

- 阈值告警:新增 internal/monitor 巡检(CPU/内存/磁盘持续超阈值、隧道连续 N 次未注册),默认关闭,带静默期与恢复通知

- API Token:api_tokens 表 + Authorization Bearer 认证,只读/读写两种范围;令牌管理与改密/退出仅限登录会话
This commit is contained in:
2026-09-24 20:50:17 +08:00
parent 8f75e99339
commit 862442e77f
42 changed files with 3034 additions and 190 deletions
+73
View File
@@ -0,0 +1,73 @@
// 极简行级差异:Nginx 配置通常只有几十行,用 LCS 就够,不为此引入第三方 diff 依赖。
export interface DiffLine {
kind: 'same' | 'add' | 'del'
text: string
oldLine?: number
newLine?: number
}
// maxDiffCells 限制 LCS 表大小:超长文本(例如整份日志误粘进来)退化为「整体替换」,
// 避免 O(n*m) 的表把页面卡死。
const maxDiffCells = 250_000
export function diffLines(fromText: string, toText: string): DiffLine[] {
const from = splitLines(fromText)
const to = splitLines(toText)
if (from.length * to.length > maxDiffCells) {
return [
...from.map((text, index) => ({ kind: 'del' as const, text, oldLine: index + 1 })),
...to.map((text, index) => ({ kind: 'add' as const, text, newLine: index + 1 })),
]
}
// dp[i][j] = from[i..] 与 to[j..] 的最长公共子序列长度
const dp: number[][] = Array.from({ length: from.length + 1 }, () => new Array<number>(to.length + 1).fill(0))
for (let i = from.length - 1; i >= 0; i -= 1) {
for (let j = to.length - 1; j >= 0; j -= 1) {
dp[i][j] = from[i] === to[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
}
}
const out: DiffLine[] = []
let i = 0
let j = 0
while (i < from.length && j < to.length) {
if (from[i] === to[j]) {
out.push({ kind: 'same', text: from[i], oldLine: i + 1, newLine: j + 1 })
i += 1
j += 1
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
out.push({ kind: 'del', text: from[i], oldLine: i + 1 })
i += 1
} else {
out.push({ kind: 'add', text: to[j], newLine: j + 1 })
j += 1
}
}
while (i < from.length) {
out.push({ kind: 'del', text: from[i], oldLine: i + 1 })
i += 1
}
while (j < to.length) {
out.push({ kind: 'add', text: to[j], newLine: j + 1 })
j += 1
}
return out
}
export function diffStats(lines: DiffLine[]): { added: number; removed: number } {
let added = 0
let removed = 0
for (const line of lines) {
if (line.kind === 'add') added += 1
else if (line.kind === 'del') removed += 1
}
return { added, removed }
}
function splitLines(text: string): string[] {
if (text === '') return []
return text.replace(/\r\n/g, '\n').replace(/\n+$/, '').split('\n')
}