// // 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;