feat(kuma): 备好 Bark 通知增强补丁与素材(待部署)

This commit is contained in:
夏犀麟 2026-09-13 17:57:32 +08:00
parent b58f09a924
commit 226fbf901f
11 changed files with 527 additions and 1 deletions

View File

@ -1,5 +1,7 @@
# 2026-09-13 # 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 | 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 | 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 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

View File

@ -52,7 +52,7 @@
### Uptime Kumastatus.xpcool.com2026-09-13 部署) ### Uptime Kumastatus.xpcool.com2026-09-13 部署)
- 镜像 `louislam/uptime-kuma:2.5.3`2.x 稳定线1.x 已停维护v2 有破坏性变更,跨大版本需迁移数据库)。 - 镜像 `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务必记牢。 - **v2 首次启动是「选择数据库」引导页**(逻辑在 `/app/server/setup-database.js`,读 `data/db-config.json`),选 SQLite 即可之后进入创建管理员账号页。Uptime Kuma **没有找回密码入口**,密码丢失只能改数据库/清 2FA务必记牢。
- nginx 反代**必须透传 WebSocket**`proxy_http_version 1.1` + `Upgrade`/`Connection` 头),否则面板无限重连、状态刷不出来。 - nginx 反代**必须透传 WebSocket**`proxy_http_version 1.1` + `Upgrade`/`Connection` 头),否则面板无限重连、状态刷不出来。
- **证书**:腾讯云免费 DVCN/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` 比对,确认证书与私钥配套。 - **证书**:腾讯云免费 DVCN/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.<时间戳>` - 脚本:`.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 再重启。全程不清真站点,可靠性最高。 - **端到端验证方法(推荐复用)**:临时插一个指向 `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。 - 全部 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/`)。

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

194
.workbuddy/tmp/bark.js Normal file
View 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. 设置角标 badgeDOWN 自增、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;

View 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

View 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"

View File

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""
生成 Bark 推送用的图标与告警配图2026-09-13v2 修正版
产物
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")

View 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}}'

View File

@ -1,6 +1,7 @@
# status.xpcool.com 服务监控面板Uptime Kuma 2.5.3 # status.xpcool.com 服务监控面板Uptime Kuma 2.5.3
# 反代后端容器 uptime-kuma127.0.0.1:3001 # 反代后端容器 uptime-kuma127.0.0.1:3001
# 2026-09-13 部署;证书为腾讯云免费 DV 证书(本地源 E:\CentOS\ssl\status.xpcool.com_nginx\ # 2026-09-13 部署;证书为腾讯云免费 DV 证书(本地源 E:\CentOS\ssl\status.xpcool.com_nginx\
# 2026-09-13 追加 /bark-assets/ 静态位置,供 Bark 推送取图标与告警配图(手机端直接访问,必须公开)
# HTTP -> HTTPS 跳转 # HTTP -> HTTPS 跳转
server { server {
@ -24,6 +25,16 @@ server {
add_header X-Content-Type-Options nosniff always; add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN 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 头,否则页面无限重连 # 面板依赖 WebSocket 实时推送状态,必须透传 Upgrade 头,否则页面无限重连
location / { location / {
proxy_pass http://127.0.0.1:3001; proxy_pass http://127.0.0.1:3001;