diff --git a/.workbuddy/memory/2026-09-13.md b/.workbuddy/memory/2026-09-13.md index 0c33c71..a7e3b59 100644 --- a/.workbuddy/memory/2026-09-13.md +++ b/.workbuddy/memory/2026-09-13.md @@ -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/`),已关联全部 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 diff --git a/.workbuddy/memory/DEPLOY.md b/.workbuddy/memory/DEPLOY.md index 912b184..e01b55a 100644 --- a/.workbuddy/memory/DEPLOY.md +++ b/.workbuddy/memory/DEPLOY.md @@ -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/`)。 diff --git a/.workbuddy/tmp/bark-assets/down.png b/.workbuddy/tmp/bark-assets/down.png new file mode 100644 index 0000000..5cfb716 Binary files /dev/null and b/.workbuddy/tmp/bark-assets/down.png differ diff --git a/.workbuddy/tmp/bark-assets/icon.png b/.workbuddy/tmp/bark-assets/icon.png new file mode 100644 index 0000000..b927bfb Binary files /dev/null and b/.workbuddy/tmp/bark-assets/icon.png differ diff --git a/.workbuddy/tmp/bark-assets/up.png b/.workbuddy/tmp/bark-assets/up.png new file mode 100644 index 0000000..b147416 Binary files /dev/null and b/.workbuddy/tmp/bark-assets/up.png differ diff --git a/.workbuddy/tmp/bark.js b/.workbuddy/tmp/bark.js new file mode 100644 index 0000000..76c2ca5 --- /dev/null +++ b/.workbuddy/tmp/bark.js @@ -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} 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; diff --git a/.workbuddy/tmp/deploy-bark-custom.ps1 b/.workbuddy/tmp/deploy-bark-custom.ps1 new file mode 100644 index 0000000..eb543aa --- /dev/null +++ b/.workbuddy/tmp/deploy-bark-custom.ps1 @@ -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 diff --git a/.workbuddy/tmp/deploy-bark-custom.sh b/.workbuddy/tmp/deploy-bark-custom.sh new file mode 100644 index 0000000..e745b2f --- /dev/null +++ b/.workbuddy/tmp/deploy-bark-custom.sh @@ -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" diff --git a/.workbuddy/tmp/make-bark-assets.py b/.workbuddy/tmp/make-bark-assets.py new file mode 100644 index 0000000..a6f7e7b --- /dev/null +++ b/.workbuddy/tmp/make-bark-assets.py @@ -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") diff --git a/.workbuddy/tmp/recon-assets.sh b/.workbuddy/tmp/recon-assets.sh new file mode 100644 index 0000000..7f2e527 --- /dev/null +++ b/.workbuddy/tmp/recon-assets.sh @@ -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}}' diff --git a/.workbuddy/tmp/status.xpcool.com.conf b/.workbuddy/tmp/status.xpcool.com.conf index 85b00c7..3529a70 100644 --- a/.workbuddy/tmp/status.xpcool.com.conf +++ b/.workbuddy/tmp/status.xpcool.com.conf @@ -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;