feat(kuma): 备好 Bark 通知增强补丁与素材(待部署)
This commit is contained in:
parent
b58f09a924
commit
226fbf901f
@ -1,5 +1,7 @@
|
||||
# 2026-09-13
|
||||
|
||||
2026-09-13 | CHG | 备好 Bark 通知增强(自定义图标/分组/重要警告/图片/角标):Kuma 官方 provider 不支持这些参数,改为「只读挂载覆盖 `/app/server/notification-providers/bark.js`」实现;素材用 Pillow 手绘(icon/down/up 三张)自托管到 `/data/www/bark-assets/`;**因沙箱连续拦截 SSH,部署改由用户执行 `.workbuddy/tmp/deploy-bark-custom.ps1`,尚未落地**
|
||||
2026-09-13 | FIX | 本机取图脚本踩坑:PIL 波形归一化数值超出画布(v=-6 → 偏移 6×振幅)导致尖峰压到文字上;改为所有波形值控制在 [-1,1] 并加 assert 断言
|
||||
2026-09-13 | CHG | Uptime Kuma 接入 Bark 告警:新增通知渠道「Bark 手机推送」(type 必须写 `Bark` 首字母大写,端点用容器网关 `http://172.17.0.1:8081/<key>`),已关联全部 9 个监控;用临时监控触发真实 DOWN 完成端到端验证(Bark 日志实测收到并返 200)后清理
|
||||
2026-09-13 | DEP | 安装用户级技能 `design-taste-frontend`(taste-skill v2,源 github.com/Leonxlnx/taste-skill,上游路径 skills/taste-skill/SKILL.md);安装前完成安全审计(P2 安全,纯 Markdown 无脚本);description 追加中文触发词;来源与审计记录见 `~/.workbuddy/skills/design-taste-frontend/SOURCE.md`
|
||||
2026-09-13 | FIX | 本机 git 走系统代理(HTTP_PROXY=127.0.0.1:58412)导致 github clone 失败(Empty reply / CONNECT 502)→ 清空代理 env 后用 `curl --noproxy "*"` 拉 codeload tarball 再解压可正常下载;另 Bash 工具在本机不可用(shim 报 dirname: command not found),需改用 PowerShell
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
|
||||
### Uptime Kuma(status.xpcool.com,2026-09-13 部署)
|
||||
- 镜像 `louislam/uptime-kuma:2.5.3`(2.x 稳定线;1.x 已停维护,v2 有破坏性变更,跨大版本需迁移数据库)。
|
||||
- 启动参数:`--restart unless-stopped -p 127.0.0.1:3001:3001 -e TZ=Asia/Shanghai -v /data/uptime-kuma:/app/data --memory 512m`。服务器内存仅 3.7G 且 **swap=0**,必须限内存,避免 OOM 波及同机其他容器。
|
||||
- 启动参数:`--restart unless-stopped -p 127.0.0.1:3001:3001 -e TZ=Asia/Shanghai -v /data/uptime-kuma:/app/data -v /data/uptime-kuma-patch/bark.js:/app/server/notification-providers/bark.js:ro --memory 512m`(**最后一个 -v 是 Bark 增强补丁挂载,2026-09-13 加**;没部署前可省略)。服务器内存仅 3.7G 且 **swap=0**,必须限内存,避免 OOM 波及同机其他容器。
|
||||
- **v2 首次启动是「选择数据库」引导页**(逻辑在 `/app/server/setup-database.js`,读 `data/db-config.json`),选 SQLite 即可,之后进入创建管理员账号页。Uptime Kuma **没有找回密码入口**,密码丢失只能改数据库/清 2FA,务必记牢。
|
||||
- nginx 反代**必须透传 WebSocket**(`proxy_http_version 1.1` + `Upgrade`/`Connection` 头),否则面板无限重连、状态刷不出来。
|
||||
- **证书**:腾讯云免费 DV,CN/SAN 均为 `status.xpcool.com`,**有效期仅 3 个月(至 2026-12-12),到期必须重新申请并替换**;落点 `/data/nginx/ssl/status.xpcool.com/`(crt/pem/csr 644、key 600),本地源 `E:\CentOS\ssl\status.xpcool.com_nginx\`。部署时用 `openssl x509 -pubkey | md5` 与 `openssl pkey -pubout | md5` 比对,确认证书与私钥配套。
|
||||
@ -76,3 +76,12 @@
|
||||
- 脚本:`.workbuddy/tmp/kuma-bark-setup.sh`(幂等写入)、`.workbuddy/tmp/kuma-bark-verify.sh`(端到端验证 + 自清理);改库前必备份 `/tmp/kuma.db.backup.<时间戳>`。
|
||||
- **端到端验证方法(推荐复用)**:临时插一个指向 `http://127.0.0.1:9/` 的监控(`interval=20/retry_interval=20/maxretries=0`)→ 重启容器 → 十几秒即 DOWN 并推送 → 查 `docker logs bark` 确认真的收到 GET 推送(200)→ 删掉临时监控与其 heartbeat 再重启。全程不清真站点,可靠性最高。
|
||||
- 全部 9 个监控 `expiry_notification=1`(证书到期通知已开),当前均 UP。
|
||||
- **Bark 通知增强(2026-09-13 · 自定义图标/分组/重要警告/图片/角标)**:⏳ **补丁与素材已备好,待执行部署**(沙箱连续拦截 SSH,改由 `.workbuddy/tmp/deploy-bark-custom.ps1` 由用户本机一键执行)。官方 provider 只支持 `barkEndpoint/barkGroup/barkSound`,且图标写死 GitHub 上的 Kuma logo。扩展办法 = **用只读挂载覆盖 provider 单文件**:
|
||||
- 宿主机 `/data/uptime-kuma-patch/bark.js` ←→ 容器 `/app/server/notification-providers/bark.js`(`:ro`)。补丁源码在 `.workbuddy/tmp/bark.js`,改动点集中在 `statusParams()` 与 `additionalParameters()`,均以中文注释标了「xpcool 定制」。
|
||||
- 行为:DOWN → `level=critical`(重要警告,穿透静音/专注)+ `volume=5` + `badge=auto`(角标自增)+ `isArchive=1` + 异常配图;UP → `badge=0`(清角标)+ 恢复配图。`call=1`(持续响铃 30s)**默认关闭**,要开就在 config 里加 `"barkCall": true`。
|
||||
- 所有项都可用通知 `config` 里的同名字段覆盖:`barkIcon / barkGroup / barkSound / barkLevel / barkVolume / barkCall / barkBadge / barkImageDown / barkImageUp / barkBadgeReset`。**但 UI 里保存通知会重写 config、丢掉这些额外字段** —— 保持默认行为即可(补丁有兜底默认值),要改就走 SQL。
|
||||
- 素材自托管在 `https://status.xpcool.com/bark-assets/{icon,down,up}.png`(落点 `/data/www/bark-assets/`,nginx 新增 `location /bark-assets/`,`alias + default_type image/png + expires 7d`)。**必须公网 HTTPS 可匿名访问**:图是手机端收到通知后自己去拉的。
|
||||
- 素材由 `.workbuddy/tmp/make-bark-assets.py`(Pillow 手绘)生成,纯几何 + 微软雅黑文字,体积 22~78 KB。
|
||||
- **⚠️ 升级 uptime-kuma 镜像后**:`bark.js` 会被挂载覆盖成旧版实现,必须重新对照新版官方 `bark.js` 更新补丁(或临时去掉挂载)。
|
||||
- **⚠️ 重建容器必须带回挂载**,否则补丁失效(表现为:通知还发,但图标/重要警告/配图/角标全没了)。完整启动参数见上。
|
||||
- 部署脚本:`.workbuddy/tmp/deploy-bark-custom.sh`(远端执行,幂等)+ `.workbuddy/tmp/deploy-bark-custom.ps1`(本机一键:上传 → 部署 → 端到端验证,日志写 `.workbuddy/tmp/logs/`)。
|
||||
|
||||
BIN
.workbuddy/tmp/bark-assets/down.png
Normal file
BIN
.workbuddy/tmp/bark-assets/down.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
.workbuddy/tmp/bark-assets/icon.png
Normal file
BIN
.workbuddy/tmp/bark-assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
.workbuddy/tmp/bark-assets/up.png
Normal file
BIN
.workbuddy/tmp/bark-assets/up.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
194
.workbuddy/tmp/bark.js
Normal file
194
.workbuddy/tmp/bark.js
Normal file
@ -0,0 +1,194 @@
|
||||
//
|
||||
// bark.js —— xpcool 定制版(基于 Uptime Kuma 2.5.3 官方实现)
|
||||
//
|
||||
// 为什么要定制:官方 Bark provider 只支持 endpoint / group / sound 三个参数,
|
||||
// 且图标写死成 GitHub 上的 Uptime Kuma 图标。本项目需要:
|
||||
// 1. 自定义推送图标(自托管 PNG,手机端直接拉取)
|
||||
// 2. 推送消息分组(可选 config.barkGroup 覆盖)
|
||||
// 3. 重要警告 level=critical,穿透静音 / 专注模式(仅 DOWN)
|
||||
// 4. 图片推送通知 image,按状态区分「异常图 / 恢复图」
|
||||
// 5. 设置角标 badge(DOWN 自增、UP 清零)
|
||||
// 6. 附带:call=1 持续响铃、isArchive=1 自动保存(仅 DOWN)
|
||||
//
|
||||
// 部署方式:宿主文件 /data/uptime-kuma-patch/bark.js 只读挂载覆盖本路径。
|
||||
// ⚠️ 升级 uptime-kuma 镜像后需重新核对此文件与新版官方实现的差异。
|
||||
//
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { default: axios } = require("axios");
|
||||
|
||||
// ===== xpcool 定制:素材地址与默认值 =====
|
||||
const barkNotificationAvatar = "https://status.xpcool.com/bark-assets/icon.png"; // 自定义推送图标
|
||||
const IMAGE_DOWN = "https://status.xpcool.com/bark-assets/down.png"; // 服务异常配图
|
||||
const IMAGE_UP = "https://status.xpcool.com/bark-assets/up.png"; // 服务恢复配图
|
||||
const DEFAULT_GROUP = "Uptime-Kuma"; // 默认推送分组
|
||||
|
||||
const successMessage = "Successes!";
|
||||
|
||||
class Bark extends NotificationProvider {
|
||||
name = "Bark";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let barkEndpoint = notification.barkEndpoint;
|
||||
|
||||
// check if the endpoint has a "/" suffix, if so, delete it first
|
||||
if (barkEndpoint.endsWith("/")) {
|
||||
barkEndpoint = barkEndpoint.substring(0, barkEndpoint.length - 1);
|
||||
}
|
||||
|
||||
// xpcool 定制:按 UP / DOWN 状态生成附加参数(重要警告、角标、配图等)
|
||||
const status = heartbeatJSON != null ? heartbeatJSON["status"] : null;
|
||||
const extra = this.statusParams(notification, status);
|
||||
|
||||
if (msg != null && heartbeatJSON != null && status === UP) {
|
||||
let title = "UptimeKuma Monitor Up";
|
||||
return await this.postNotification(notification, title, msg, barkEndpoint, extra);
|
||||
}
|
||||
|
||||
if (msg != null && heartbeatJSON != null && status === DOWN) {
|
||||
let title = "UptimeKuma Monitor Down";
|
||||
return await this.postNotification(notification, title, msg, barkEndpoint, extra);
|
||||
}
|
||||
|
||||
if (msg != null) {
|
||||
let title = "UptimeKuma Message";
|
||||
return await this.postNotification(notification, title, msg, barkEndpoint, extra);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* xpcool 定制:按告警状态生成 Bark 附加参数
|
||||
* 所有项都可用通知 config 里的同名字段覆盖(barkLevel / barkVolume / barkCall /
|
||||
* barkBadge / barkImageDown / barkImageUp / barkBadgeReset),无需改代码。
|
||||
* @param {BeanModel} notification 通知配置
|
||||
* @param {number|null} status 心跳状态(0=DOWN 1=UP)
|
||||
* @returns {object} 追加到 Bark 请求上的参数
|
||||
*/
|
||||
statusParams(notification, status) {
|
||||
const p = {};
|
||||
|
||||
if (status === DOWN) {
|
||||
// 重要警告:无视静音与专注模式(手机上需在 Bark App 里允许「重要警告」)
|
||||
p.level = notification.barkLevel || "critical";
|
||||
p.volume = notification.barkVolume != null ? notification.barkVolume : 5;
|
||||
|
||||
// 持续响铃约 30 秒:默认关闭(避免半夜长时间吵闹),
|
||||
// 想开启就在通知 config 里加 "barkCall": true
|
||||
if (notification.barkCall === true) {
|
||||
p.call = 1;
|
||||
}
|
||||
|
||||
// 角标:auto = 自动累加未处理告警数,也可填具体数字
|
||||
p.badge = notification.barkBadge != null ? notification.barkBadge : "auto";
|
||||
|
||||
// 自动保存到通知中心,便于事后回看
|
||||
p.isArchive = 1;
|
||||
|
||||
// 异常配图
|
||||
p.image = notification.barkImageDown || IMAGE_DOWN;
|
||||
} else if (status === UP) {
|
||||
// 恢复配图
|
||||
p.image = notification.barkImageUp || IMAGE_UP;
|
||||
|
||||
// 恢复时清掉角标(设 barkBadgeReset=false 可保留)
|
||||
if (notification.barkBadgeReset !== false) {
|
||||
p.badge = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional parameter for Bark v1 endpoints.
|
||||
* Leads to better on device styles (iOS 15 optimized)
|
||||
* @param {BeanModel} notification Notification to be sent
|
||||
* @param {object} extra xpcool 定制:状态相关附加参数
|
||||
* @returns {string} Additional URL parameters
|
||||
*/
|
||||
additionalParameters(notification, extra = {}) {
|
||||
// xpcool 定制:图标改为自托管 PNG(可被 config.barkIcon 覆盖)
|
||||
let params = "?icon=" + encodeURIComponent(notification.barkIcon || barkNotificationAvatar);
|
||||
|
||||
// 推送消息分组
|
||||
params += "&group=" + encodeURIComponent(notification.barkGroup || DEFAULT_GROUP);
|
||||
|
||||
// picked a sound, this should follow system's mute status when arrival
|
||||
params += "&sound=" + encodeURIComponent(notification.barkSound || "telegraph");
|
||||
|
||||
// xpcool 定制:追加状态相关参数(level / volume / call / badge / image / isArchive)
|
||||
for (const key of Object.keys(extra)) {
|
||||
const value = extra[key];
|
||||
if (value === undefined || value === null || value === "") {
|
||||
continue;
|
||||
}
|
||||
params += "&" + key + "=" + encodeURIComponent(value);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if result is successful
|
||||
* @param {object} result Axios response object
|
||||
* @returns {void}
|
||||
* @throws {Error} The status code is not in range 2xx
|
||||
*/
|
||||
checkResult(result) {
|
||||
if (result.status == null) {
|
||||
throw new Error("Bark notification failed with invalid response!");
|
||||
}
|
||||
if (result.status < 200 || result.status >= 300) {
|
||||
throw new Error("Bark notification failed with status code " + result.status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the message
|
||||
* @param {BeanModel} notification Notification to be sent
|
||||
* @param {string} title Message title
|
||||
* @param {string} subtitle Message
|
||||
* @param {string} endpoint Endpoint to send request to
|
||||
* @param {object} extra xpcool 定制:状态相关附加参数
|
||||
* @returns {Promise<string>} Success message
|
||||
*/
|
||||
async postNotification(notification, title, subtitle, endpoint, extra = {}) {
|
||||
let result;
|
||||
let config = this.getAxiosConfigWithProxy({});
|
||||
|
||||
if (notification.apiVersion === "v1" || notification.apiVersion == null) {
|
||||
// url encode title and subtitle
|
||||
title = encodeURIComponent(title);
|
||||
subtitle = encodeURIComponent(subtitle);
|
||||
const params = this.additionalParameters(notification, extra);
|
||||
result = await axios.get(`${endpoint}/${title}/${subtitle}${params}`, config);
|
||||
} else {
|
||||
// xpcool 定制:v2(POST) 分支同样带上附加参数
|
||||
const body = {
|
||||
title,
|
||||
body: subtitle,
|
||||
icon: notification.barkIcon || barkNotificationAvatar,
|
||||
sound: notification.barkSound || "telegraph",
|
||||
group: notification.barkGroup || DEFAULT_GROUP,
|
||||
};
|
||||
for (const key of Object.keys(extra)) {
|
||||
if (extra[key] !== undefined && extra[key] !== null) {
|
||||
body[key] = extra[key];
|
||||
}
|
||||
}
|
||||
result = await axios.post(endpoint, body, config);
|
||||
}
|
||||
|
||||
this.checkResult(result);
|
||||
if (result.statusText != null) {
|
||||
return "Bark notification succeed: " + result.statusText;
|
||||
}
|
||||
// because returned in range 200 ..< 300
|
||||
return successMessage;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bark;
|
||||
72
.workbuddy/tmp/deploy-bark-custom.ps1
Normal file
72
.workbuddy/tmp/deploy-bark-custom.ps1
Normal file
@ -0,0 +1,72 @@
|
||||
# =====================================================================
|
||||
# 一键部署:Uptime Kuma 的 Bark 通知增强
|
||||
# 自定义推送图标 / 消息分组 / 重要警告 / 图片推送 / 角标
|
||||
# 2026-09-13
|
||||
#
|
||||
# 用法(在本机 PowerShell 里执行):
|
||||
# powershell -ExecutionPolicy Bypass -File "E:\Project\workbuddy.xpcool.com\.workbuddy\tmp\deploy-bark-custom.ps1"
|
||||
#
|
||||
# 执行完会把全过程日志写到 logs\bark-deploy-<时间戳>.log,把它发我即可。
|
||||
# =====================================================================
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
$T = "E:\Project\workbuddy.xpcool.com\.workbuddy\tmp"
|
||||
$scp = "$env:SystemRoot\System32\OpenSSH\scp.exe"
|
||||
$ssh = "$env:SystemRoot\System32\OpenSSH\ssh.exe"
|
||||
$key = "$env:USERPROFILE\.ssh\xpcool_ed25519"
|
||||
$host_ = "xxcool@193.112.118.168"
|
||||
$port = "22025"
|
||||
|
||||
$logDir = Join-Path $T "logs"
|
||||
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||
$logFile = Join-Path $logDir ("bark-deploy-{0}.log" -f (Get-Date -Format "yyyyMMdd-HHmmss"))
|
||||
$log = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
function Say($text) {
|
||||
Write-Host $text
|
||||
$log.Add($text)
|
||||
}
|
||||
|
||||
Say "===== Bark 通知增强部署 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ====="
|
||||
|
||||
# --- 1. 上传(单个文件逐个传:多文件 scp 会静默丢文件) ---
|
||||
$pairs = @(
|
||||
@("$T\bark-assets\icon.png", "/tmp/bark-icon.png"),
|
||||
@("$T\bark-assets\down.png", "/tmp/bark-down.png"),
|
||||
@("$T\bark-assets\up.png", "/tmp/bark-up.png"),
|
||||
@("$T\bark.js", "/tmp/bark.js"),
|
||||
@("$T\status.xpcool.com.conf", "/tmp/status.xpcool.com.conf"),
|
||||
@("$T\deploy-bark-custom.sh", "/tmp/deploy-bark-custom.sh"),
|
||||
@("$T\kuma-bark-verify.sh", "/tmp/kuma-bark-verify.sh")
|
||||
)
|
||||
|
||||
Say ""
|
||||
Say "--- 1. 上传文件 ---"
|
||||
foreach ($p in $pairs) {
|
||||
$leaf = Split-Path $p[0] -Leaf
|
||||
if (-not (Test-Path $p[0])) { Say ("!! 本地缺少 {0},跳过" -f $leaf); continue }
|
||||
$out = & $scp -o BatchMode=yes -o StrictHostKeyChecking=no -i $key -P $port $p[0] "$host_:$($p[1])" 2>&1
|
||||
Say ("scp {0} -> exit={1} {2}" -f $leaf, $LASTEXITCODE, ($out -join " "))
|
||||
}
|
||||
|
||||
$out = & $ssh -o BatchMode=yes -o StrictHostKeyChecking=no -i $key -p $port $host_ "ls -l /tmp/bark-*.png /tmp/bark.js /tmp/status.xpcool.com.conf /tmp/deploy-bark-custom.sh /tmp/kuma-bark-verify.sh" 2>&1
|
||||
Say ($out -join "`n")
|
||||
|
||||
# --- 2. 执行部署 ---
|
||||
Say ""
|
||||
Say "--- 2. 部署(素材 + provider 补丁 + nginx + 重建容器) ---"
|
||||
$out = & $ssh -o BatchMode=yes -o StrictHostKeyChecking=no -i $key -p $port $host_ "chmod +x /tmp/deploy-bark-custom.sh && /tmp/deploy-bark-custom.sh" 2>&1
|
||||
Say ($out -join "`n")
|
||||
|
||||
# --- 3. 端到端验证(临时监控触发真实 DOWN,验证后自动清理) ---
|
||||
Say ""
|
||||
Say "--- 3. 端到端验证(会往你手机推一条真实的 Down 告警) ---"
|
||||
$out = & $ssh -o BatchMode=yes -o StrictHostKeyChecking=no -i $key -p $port $host_ "chmod +x /tmp/kuma-bark-verify.sh && /tmp/kuma-bark-verify.sh" 2>&1
|
||||
Say ($out -join "`n")
|
||||
|
||||
Say ""
|
||||
Say "===== 结束,日志:$logFile ====="
|
||||
$log -join "`r`n" | Out-File -FilePath $logFile -Encoding utf8
|
||||
Write-Host ""
|
||||
Write-Host ("日志已保存:" + $logFile) -ForegroundColor Green
|
||||
85
.workbuddy/tmp/deploy-bark-custom.sh
Normal file
85
.workbuddy/tmp/deploy-bark-custom.sh
Normal file
@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# 为 Uptime Kuma 的 Bark 通知启用:自定义图标 / 消息分组 / 重要警告 / 图片推送 / 角标
|
||||
# 2026-09-13
|
||||
# 前置:以下文件需先 scp 到 /tmp(单个文件逐个传,多文件 scp 会静默丢文件)
|
||||
# /tmp/bark-icon.png /tmp/bark-down.png /tmp/bark-up.png
|
||||
# /tmp/bark.js /tmp/status.xpcool.com.conf
|
||||
# 幂等:可重复执行
|
||||
|
||||
ASSET_DIR=/data/www/bark-assets
|
||||
PATCH_DIR=/data/uptime-kuma-patch
|
||||
IMAGE=louislam/uptime-kuma:2.5.3
|
||||
TS=$(date +%Y%m%d%H%M%S)
|
||||
|
||||
echo "=== 0. 备份当前容器配置与 nginx 配置(便于回滚) ==="
|
||||
sudo docker inspect uptime-kuma > "/tmp/kuma-inspect.$TS.json"
|
||||
echo "inspect 已存 /tmp/kuma-inspect.$TS.json"
|
||||
sudo docker inspect uptime-kuma --format 'restart={{json .HostConfig.RestartPolicy}} mem={{.HostConfig.Memory}}B port={{json .HostConfig.PortBindings}} env={{json .Config.Env}}'
|
||||
sudo cp -a /data/nginx/conf.d/status.xpcool.com.conf "/data/nginx/conf.d/status.xpcool.com.conf.bak.$TS"
|
||||
|
||||
echo ""
|
||||
echo "=== 1. 部署 Bark 素材 ==="
|
||||
sudo mkdir -p "$ASSET_DIR" "$PATCH_DIR"
|
||||
for f in icon down up; do
|
||||
if [ -f "/tmp/bark-$f.png" ]; then
|
||||
sudo mv -f "/tmp/bark-$f.png" "$ASSET_DIR/$f.png"
|
||||
fi
|
||||
done
|
||||
sudo chmod 644 "$ASSET_DIR"/*.png
|
||||
sudo chmod 755 "$ASSET_DIR"
|
||||
ls -l "$ASSET_DIR"
|
||||
|
||||
echo ""
|
||||
echo "=== 2. 安装 provider 补丁 ==="
|
||||
if [ -f /tmp/bark.js ]; then
|
||||
sudo mv -f /tmp/bark.js "$PATCH_DIR/bark.js"
|
||||
fi
|
||||
sudo chmod 644 "$PATCH_DIR/bark.js"
|
||||
sudo cp -f "$PATCH_DIR/bark.js" /tmp/_bark_check.js
|
||||
sudo docker cp /tmp/_bark_check.js uptime-kuma:/tmp/_bark_check.js > /dev/null
|
||||
sudo docker exec uptime-kuma node --check /tmp/_bark_check.js && echo "补丁语法检查 OK"
|
||||
sudo docker exec uptime-kuma rm -f /tmp/_bark_check.js
|
||||
rm -f /tmp/_bark_check.js
|
||||
echo "补丁 md5:$(md5sum "$PATCH_DIR/bark.js" | cut -d' ' -f1)"
|
||||
|
||||
echo ""
|
||||
echo "=== 3. 部署 nginx 配置(新增 /bark-assets/ 静态位置) ==="
|
||||
if [ -f /tmp/status.xpcool.com.conf ]; then
|
||||
sudo mv -f /tmp/status.xpcool.com.conf /data/nginx/conf.d/status.xpcool.com.conf
|
||||
fi
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
echo "nginx reload OK"
|
||||
|
||||
echo ""
|
||||
echo "=== 4. 公网取图验证(手机端要能直接拉到) ==="
|
||||
for f in icon down up; do
|
||||
curl -s -o /dev/null -m 15 -w "$f.png code=%{http_code} type=%{content_type} size=%{size_download}\n" \
|
||||
"https://status.xpcool.com/bark-assets/$f.png"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== 5. 重建容器以挂载补丁(数据卷 /data/uptime-kuma 不变) ==="
|
||||
sudo docker rm -f uptime-kuma > /dev/null 2>&1
|
||||
sudo docker run -d --name uptime-kuma --restart unless-stopped \
|
||||
-p 127.0.0.1:3001:3001 \
|
||||
-e TZ=Asia/Shanghai \
|
||||
-v /data/uptime-kuma:/app/data \
|
||||
-v /data/uptime-kuma-patch/bark.js:/app/server/notification-providers/bark.js:ro \
|
||||
--memory 512m \
|
||||
"$IMAGE" > /dev/null
|
||||
sleep 20
|
||||
sudo docker ps --filter name=uptime-kuma --format '{{.Names}} | {{.Status}} | {{.Ports}}'
|
||||
|
||||
echo ""
|
||||
echo "=== 6. 确认补丁确实生效 ==="
|
||||
echo -n "宿主机补丁 md5 : "; md5sum "$PATCH_DIR/bark.js" | cut -d' ' -f1
|
||||
echo -n "容器内文件 md5 : "; sudo docker exec uptime-kuma md5sum /app/server/notification-providers/bark.js 2>&1 | cut -d' ' -f1
|
||||
echo -n "定制标记行数 : "; sudo docker exec uptime-kuma grep -c "xpcool 定制" /app/server/notification-providers/bark.js 2>&1
|
||||
echo -n "图标常量 : "; sudo docker exec uptime-kuma grep -m1 "barkNotificationAvatar =" /app/server/notification-providers/bark.js 2>&1
|
||||
|
||||
echo ""
|
||||
echo "=== 7. 面板可用性 ==="
|
||||
curl -s -o /dev/null -m 10 -w "panel=%{http_code}\n" http://127.0.0.1:3001/
|
||||
sudo docker exec uptime-kuma sqlite3 /app/data/kuma.db "SELECT id,name,active,json_extract(config,'\$.type') FROM notification;"
|
||||
echo "DONE"
|
||||
126
.workbuddy/tmp/make-bark-assets.py
Normal file
126
.workbuddy/tmp/make-bark-assets.py
Normal file
@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
生成 Bark 推送用的图标与告警配图(2026-09-13,v2 修正版)
|
||||
产物:
|
||||
icon.png 512x512 通知图标
|
||||
down.png 1200x630 服务异常告警配图(平线 = 心跳停止)
|
||||
up.png 1200x630 服务恢复配图(正常心电)
|
||||
要点:所有坐标必须落在画布内;波形与文字分区,互不重叠。
|
||||
"""
|
||||
import os
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
|
||||
OUT = r"E:\Project\workbuddy.xpcool.com\.workbuddy\tmp\bark-assets"
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
FONT_BOLD = r"C:\Windows\Fonts\msyhbd.ttc"
|
||||
FONT_REG = r"C:\Windows\Fonts\msyh.ttc"
|
||||
|
||||
|
||||
def vgrad(size, top, bottom):
|
||||
"""竖向线性渐变"""
|
||||
w, h = size
|
||||
base = Image.new("RGB", (1, h))
|
||||
d = ImageDraw.Draw(base)
|
||||
for y in range(h):
|
||||
t = y / max(h - 1, 1)
|
||||
d.point((0, y), tuple(int(top[i] + (bottom[i] - top[i]) * t) for i in range(3)))
|
||||
return base.resize((w, h), Image.BILINEAR)
|
||||
|
||||
|
||||
def rounded_mask(size, radius):
|
||||
m = Image.new("L", size, 0)
|
||||
ImageDraw.Draw(m).rounded_rectangle([0, 0, size[0] - 1, size[1] - 1], radius=radius, fill=255)
|
||||
return m
|
||||
|
||||
|
||||
# 归一化波形(v 为相对基线的倍数,正=向上);幅度一律控制在 [-1, 1] 内
|
||||
SHAPE_NORMAL = [
|
||||
(0.00, 0.00), (0.12, 0.00), (0.16, 0.12), (0.20, 0.00),
|
||||
(0.30, 0.00), (0.33, 0.18), (0.36, 1.00), (0.40, -0.42),
|
||||
(0.44, 0.00), (0.58, 0.00), (0.64, 0.16), (0.70, 0.00), (1.00, 0.00),
|
||||
]
|
||||
SHAPE_FLAT = [ # 心跳停止:几乎一条直线,末尾一点微弱扰动
|
||||
(0.00, 0.00), (0.62, 0.00), (0.68, -0.10), (0.74, 0.06), (1.00, 0.00),
|
||||
]
|
||||
|
||||
|
||||
def ecg_points(x0, x1, ybase, amp, shape, cycles=1):
|
||||
pts, span = [], x1 - x0
|
||||
for c in range(cycles):
|
||||
for t, v in shape:
|
||||
pts.append((x0 + (c + t) * span / cycles, ybase - v * amp))
|
||||
ymin = min(p[1] for p in pts)
|
||||
ymax = max(p[1] for p in pts)
|
||||
assert amp + 1 < ybase < 630 - amp, f"基线 {ybase} 超出安全范围"
|
||||
return pts, ymin, ymax
|
||||
|
||||
|
||||
def draw_glow_line(img, pts, color, width, glow=16):
|
||||
"""带柔和外发光的折线"""
|
||||
layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
||||
ImageDraw.Draw(layer).line(pts, fill=color + (105,), width=width + glow * 2, joint="curve")
|
||||
img.alpha_composite(layer.filter(ImageFilter.GaussianBlur(glow)))
|
||||
ImageDraw.Draw(img).line(pts, fill=color + (255,), width=width, joint="curve")
|
||||
|
||||
|
||||
def center_text(d, cx, y, text, font, fill):
|
||||
l, t, r, b = d.textbbox((0, 0), text, font=font)
|
||||
d.text((cx - (r - l) / 2 - l, y), text, font=font, fill=fill)
|
||||
|
||||
|
||||
# ---------------- 1. 通知图标 512x512 ----------------
|
||||
def make_icon():
|
||||
S = 512
|
||||
img = vgrad((S, S), (30, 27, 75), (15, 23, 42)).convert("RGBA")
|
||||
img.putalpha(rounded_mask((S, S), 116))
|
||||
|
||||
d = ImageDraw.Draw(img)
|
||||
d.rounded_rectangle([14, 14, S - 15, S - 15], radius=102, outline=(99, 102, 241, 95), width=3)
|
||||
|
||||
pts, _, _ = ecg_points(80, S - 80, 272, 128, SHAPE_NORMAL, cycles=1)
|
||||
draw_glow_line(img, pts, (34, 211, 238), 20, glow=15)
|
||||
d.ellipse([65, 257, 95, 287], fill=(165, 243, 252, 255))
|
||||
|
||||
img.convert("RGB").save(os.path.join(OUT, "icon.png"), optimize=True)
|
||||
print("icon.png OK")
|
||||
|
||||
|
||||
# ---------------- 2. 告警配图 1200x630 ----------------
|
||||
def make_banner(name, top, bottom, line, dot, title, subtitle, flat):
|
||||
W, H = 1200, 630
|
||||
img = vgrad((W, H), top, bottom).convert("RGBA")
|
||||
|
||||
halo = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(halo).ellipse([700, -260, 1460, 500], fill=dot + (52,))
|
||||
img.alpha_composite(halo.filter(ImageFilter.GaussianBlur(95)))
|
||||
|
||||
# 波形放在上半区,基线 175,振幅 62 → 范围 113~237,与下方文字完全分离
|
||||
shape = SHAPE_FLAT if flat else SHAPE_NORMAL
|
||||
pts, ymin, ymax = ecg_points(110, W - 110, 175, 62, shape, cycles=1 if flat else 3)
|
||||
draw_glow_line(img, pts, line, 11, glow=13)
|
||||
|
||||
# 基线参考 + 底部细分隔线
|
||||
d = ImageDraw.Draw(img)
|
||||
d.line([(110, 175), (W - 110, 175)], fill=line + (55,), width=2)
|
||||
d.line([(110, 300), (W - 110, 300)], fill=line + (70,), width=2)
|
||||
|
||||
center_text(d, W / 2, 350, title, ImageFont.truetype(FONT_BOLD, 96), (255, 255, 255, 255))
|
||||
center_text(d, W / 2, 480, subtitle, ImageFont.truetype(FONT_REG, 32), line + (230,))
|
||||
|
||||
# 左下角状态点
|
||||
d.ellipse([112, 515, 148, 551], fill=dot + (255,))
|
||||
|
||||
img.convert("RGB").save(os.path.join(OUT, f"{name}.png"), optimize=True)
|
||||
print(f"{name}.png OK (波形 y: {ymin:.0f}~{ymax:.0f})")
|
||||
|
||||
|
||||
make_icon()
|
||||
make_banner("down", (127, 29, 29), (43, 6, 6), (252, 165, 165), (239, 68, 68),
|
||||
"服务异常", "UPTIME KUMA · SERVICE DOWN", flat=True)
|
||||
make_banner("up", (6, 78, 59), (2, 34, 25), (110, 231, 183), (16, 185, 129),
|
||||
"服务已恢复", "UPTIME KUMA · SERVICE RECOVERED", flat=False)
|
||||
|
||||
print("---")
|
||||
for f in sorted(os.listdir(OUT)):
|
||||
print(f"{f} {os.path.getsize(os.path.join(OUT, f))} bytes")
|
||||
27
.workbuddy/tmp/recon-assets.sh
Normal file
27
.workbuddy/tmp/recon-assets.sh
Normal file
@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# 勘查:静态资源落点 + 现有 logo 资源(用于 Bark 自定义图标/配图)
|
||||
echo "=== /data/www ==="
|
||||
ls -1 /data/www 2>/dev/null || echo "无 /data/www"
|
||||
|
||||
echo ""
|
||||
echo "=== 各站点的 favicon / logo 资源 ==="
|
||||
for d in /data/www/*/; do
|
||||
[ -d "$d" ] || continue
|
||||
found=$(find "$d" -maxdepth 2 -iregex ".*\(favicon\|logo\|icon\).*\.\(png\|ico\|svg\|jpg\|webp\)" 2>/dev/null | head -6)
|
||||
if [ -n "$found" ]; then
|
||||
echo "--- $d"
|
||||
echo "$found"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== /data 目录 ==="
|
||||
ls -1 /data
|
||||
|
||||
echo ""
|
||||
echo "=== 当前 status 站点配置 ==="
|
||||
cat /data/nginx/conf.d/status.xpcool.com.conf
|
||||
|
||||
echo ""
|
||||
echo "=== bark 容器信息 ==="
|
||||
sudo docker inspect bark --format '{{.Config.Image}} | {{json .NetworkSettings.Ports}} | {{json .Mounts}}'
|
||||
@ -1,6 +1,7 @@
|
||||
# status.xpcool.com 服务监控面板(Uptime Kuma 2.5.3)
|
||||
# 反代后端容器 uptime-kuma:127.0.0.1:3001
|
||||
# 2026-09-13 部署;证书为腾讯云免费 DV 证书(本地源 E:\CentOS\ssl\status.xpcool.com_nginx\)
|
||||
# 2026-09-13 追加 /bark-assets/ 静态位置,供 Bark 推送取图标与告警配图(手机端直接访问,必须公开)
|
||||
|
||||
# HTTP -> HTTPS 跳转
|
||||
server {
|
||||
@ -24,6 +25,16 @@ server {
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
|
||||
# Bark 推送素材:手机收到通知后会主动来拉取,故必须可匿名访问、走 HTTPS
|
||||
location /bark-assets/ {
|
||||
alias /data/www/bark-assets/;
|
||||
default_type image/png;
|
||||
autoindex off;
|
||||
access_log off;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
}
|
||||
|
||||
# 面板依赖 WebSocket 实时推送状态,必须透传 Upgrade 头,否则页面无限重连
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user