From 3abe7c32f03604babc081318cebebaa8e8eb177e Mon Sep 17 00:00:00 2001 From: PineHomePC Date: Fri, 21 Aug 2026 23:21:00 +0800 Subject: [PATCH] init --- bark-notify/.gitignore | 6 ++ bark-notify/README.md | 92 ++++++++++++++++ bark-notify/examples/send_all_types.py | 85 +++++++++++++++ bark-notify/pyproject.toml | 17 +++ bark-notify/src/bark_notify/__init__.py | 15 +++ bark-notify/src/bark_notify/cli.py | 87 +++++++++++++++ bark-notify/src/bark_notify/client.py | 137 ++++++++++++++++++++++++ bark-notify/src/bark_notify/types.py | 96 +++++++++++++++++ bark-notify/uv.lock | 8 ++ 9 files changed, 543 insertions(+) create mode 100644 bark-notify/.gitignore create mode 100644 bark-notify/README.md create mode 100644 bark-notify/examples/send_all_types.py create mode 100644 bark-notify/pyproject.toml create mode 100644 bark-notify/src/bark_notify/__init__.py create mode 100644 bark-notify/src/bark_notify/cli.py create mode 100644 bark-notify/src/bark_notify/client.py create mode 100644 bark-notify/src/bark_notify/types.py create mode 100644 bark-notify/uv.lock diff --git a/bark-notify/.gitignore b/bark-notify/.gitignore new file mode 100644 index 0000000..a18610e --- /dev/null +++ b/bark-notify/.gitignore @@ -0,0 +1,6 @@ +.venv/ +__pycache__/ +*.pyc +*.egg-info/ +dist/ +build/ diff --git a/bark-notify/README.md b/bark-notify/README.md new file mode 100644 index 0000000..41768ce --- /dev/null +++ b/bark-notify/README.md @@ -0,0 +1,92 @@ +# bark-notify + +向手机发送 **Bark 通知** 的 Python 模块,覆盖全部通知类型与参数。uv 管理环境,**零第三方依赖**(仅标准库)。 + +- 服务器:标准 Bark 服务(`Server: Bark`),默认 `http://47.108.226.213:10086`,可用 `BARK_BASE_URL` 覆盖。 +- 设备密钥:默认 `pvd3kWwHNFjKXNjiC2sMe7`,可用 `BARK_DEVICE_KEY` 覆盖。 + +## 安装 + +```bash +cd bark-notify +uv sync # 创建 .venv 并以可编辑方式安装本项目 +uv run bark-notify --help # 验证 CLI 可用 +``` + +## 命令行用法 + +```bash +# 基础通知 +uv run bark-notify "标题" "正文" + +# 各级别(default / active / timeSensitive / critical / passive) +uv run bark-notify "紧急" "服务器告警" --level critical --sound alarm.caf --volume 10 +uv run bark-notify "勿扰也提醒" "会议还有 5 分钟" --level timeSensitive +uv run bark-notify "只进通知中心" "静默内容" --level passive + +# 特殊功能 +uv run bark-notify "Markdown" "### 标题\n- 项1\n- 项2" # 正文原生支持 Markdown +uv run bark-notify "分组" "第1条" --group 演示 +uv run bark-notify "跳转" "点击打开百度" --url https://www.baidu.com +uv run bark-notify "复制" "长按复制此文本" --copy "OPC 云超服" +uv run bark-notify "带图" "附带图片" --image https://picsum.photos/seed/x/400/300 +uv run bark-notify "角标" "数字角标" --badge 5 --ttl 60 +``` + +## Python API + +```python +from bark_notify import BarkClient, Level, Notification, Sound + +client = BarkClient() # 也可 BarkClient(base_url=..., device_key=...) + +client.push(Notification( + title="部署完成", + body="**v2.4.0** 已上线 ✅", + level=Level.ACTIVE, + group="部署", + url="https://example.com/status", +)) + +# GET 快捷方式(适合快速测试,参数走查询串) +client.notify("测试", "正文", level="critical", group="x") +``` + +## 参数一览 + +| 参数 | CLI | 说明 | +|------|-----|------| +| `title` / `body` | 位置参数 | 标题 / 正文(正文支持 Markdown) | +| `level` | `--level` | `default` `active` `timeSensitive` `critical` `passive` | +| `sound` | `--sound` | 内置铃声名或远程铃声 URL | +| `badge` | `--badge` | 应用角标数字 | +| `group` | `--group` | 同组通知折叠 | +| `icon` | `--icon` | 通知图标 URL | +| `image` | `--image` | 附带图片 URL | +| `copy` | `--copy` | 长按复制的文本 | +| `url` | `--url` | 点击跳转 URL | +| `is_archive` | `--archive` | 存入历史记录 | +| `call` | `--call` | 连续响铃(呼叫声) | +| `ttl` | `--ttl` | 过期秒数,超时自动删除 | +| `volume` | `--volume` | critical 通知音量 0-10 | + +## 级别差异速查 + +| 级别 | 横幅 | 声音 | 勿扰可送达 | 说明 | +|------|:----:|:----:|:---------:|------| +| `default` | ✅ | ✅ | ❌ | 跟随系统 | +| `active` | ✅ | ✅ | ❌ | 标准活跃通知 | +| `timeSensitive` | ✅ | ✅ | ✅ | 时效性 | +| `critical` | ✅ | ✅ | ✅ | 绕过静音强制响铃,需授权 | +| `passive` | ❌ | ❌ | — | 仅通知中心 | + +## 内置铃声 + +`alarm.caf` `alarms.caf` `anticipate.caf` `bell.caf` `bloom.caf` `calypso.caf` `chime.caf` `choir.caf` `complete.caf` `completion.caf` `electronic.caf` `emergency.caf` `fanfare.caf` `glass.caf` `goblin.caf` `healthnotification.caf` `horn.caf` `ladders.caf` `mario.caf` `minuet.caf` `multiwayinvitation.caf` `newmail.caf` `newsflash.caf` `noir.caf` `payment.caf` `shake.caf` `sherwoodfanfare.caf` `silence.caf` `spell.caf` `suspense.caf` `telegraph.caf` `tiptoes.caf` `typewriters.caf` `update.caf` + +## 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `BARK_BASE_URL` | `http://47.108.226.213:10086` | 服务器地址 | +| `BARK_DEVICE_KEY` | `pvd3kWwHNFjKXNjiC2sMe7` | 设备密钥 | diff --git a/bark-notify/examples/send_all_types.py b/bark-notify/examples/send_all_types.py new file mode 100644 index 0000000..33818e0 --- /dev/null +++ b/bark-notify/examples/send_all_types.py @@ -0,0 +1,85 @@ +"""发送全部类型的 Bark 通知,用于在手机上逐项核对效果。 + +用法:: + + uv run python examples/send_all_types.py + +可选参数: + + --skip active critical 跳过部分级别 + --device-key 指定设备密钥 + --base-url 指定服务器地址 + +每发一条会在终端打印进度,手机端请对照标题核对。 +""" + +from __future__ import annotations + +import argparse +import time +from typing import List, Optional + +from bark_notify import BarkClient, Level, Notification + + +def main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser(description="发送全部类型的 Bark 通知") + parser.add_argument( + "--skip", + nargs="*", + choices=[level.value for level in Level], + default=[], + help="要跳过的级别(可多个)", + ) + parser.add_argument("--device-key", help="Bark 设备密钥") + parser.add_argument("--base-url", help="Bark 服务器地址") + args = parser.parse_args(argv) + + client = BarkClient(base_url=args.base_url, device_key=args.device_key) + + # 第一波:五种级别,逐项核对打扰程度差异 + cases = [ + ("基础通知(default)", Level.DEFAULT, {}), + ("活跃通知(active)", Level.ACTIVE, {}), + ("时效性通知(timeSensitive)", Level.TIME_SENSITIVE, {}), + ("紧急通知(critical)", Level.CRITICAL, {"sound": "alarm.caf", "volume": 10}), + ("静默通知(passive)", Level.PASSIVE, {}), + ] + + for i, (title, level, extra) in enumerate(cases, start=1): + if level.value in args.skip: + print(f"[{i}/{len(cases)}] 跳过 {level.value}") + continue + notification = Notification( + title=f"[{i}] {title}", + body=f"级别: {level.value}\n由 examples/send_all_types.py 发送", + level=level, + **extra, + ) + client.push(notification) + print(f"[{i}/{len(cases)}] 已发送 {level.value}") + time.sleep(1) + + # 第二波:特殊功能 + features = [ + ("[6] Markdown 富文本", "### 任务清单\n- [x] 发基础通知\n- [x] 发特殊通知\n- [ ] 核对效果\n\n**加粗** 与 *斜体* 测试 ✅", {}), + ("[7] 自定义铃声", "使用 bells 铃声,不同于默认铃声。", {"sound": "bell.caf"}), + ("[8] 分组折叠", "属于「演示分组」组,下一条同组。", {"group": "演示分组"}), + ("[9] 分组折叠", "属于「演示分组」组,会与上一条折叠。", {"group": "演示分组"}), + ("[10] 跳转链接", "点击这条会打开百度。", {"url": "https://www.baidu.com"}), + ("[11] 复制文本", "长按这条可复制「OPC 云超服」。", {"copy": "OPC 云超服"}), + ("[12] 带图片", "附带一张示例图片,需设备可加载外网图。", {"image": "https://picsum.photos/seed/bark/400/300"}), + ] + + for i, (title, body, extra) in enumerate(features, start=6): + notification = Notification(title=title, body=body, **extra) + client.push(notification) + print(f"[{i}] 已发送 {title}") + time.sleep(1) + + print("全部发送完成,请到手机端核对。") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bark-notify/pyproject.toml b/bark-notify/pyproject.toml new file mode 100644 index 0000000..c5a7fa7 --- /dev/null +++ b/bark-notify/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "bark-notify" +version = "0.1.0" +description = "向手机发送 Bark 通知的 Python 模块,覆盖全部通知类型与参数(uv 管理)" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] + +[project.scripts] +bark-notify = "bark_notify.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/bark_notify"] diff --git a/bark-notify/src/bark_notify/__init__.py b/bark-notify/src/bark_notify/__init__.py new file mode 100644 index 0000000..0a54cc1 --- /dev/null +++ b/bark-notify/src/bark_notify/__init__.py @@ -0,0 +1,15 @@ +"""bark_notify — 向手机发送 Bark 通知,覆盖全部通知类型与参数。 + +用法示例:: + + from bark_notify import BarkClient, Level, Notification + + client = BarkClient() + client.push(Notification(title="你好", body="世界", level=Level.CRITICAL, sound="alarm.caf")) +""" + +from .client import BarkClient, BarkError +from .types import Level, Notification, Sound + +__version__ = "0.1.0" +__all__ = ["BarkClient", "BarkError", "Level", "Notification", "Sound"] diff --git a/bark-notify/src/bark_notify/cli.py b/bark-notify/src/bark_notify/cli.py new file mode 100644 index 0000000..7b926d8 --- /dev/null +++ b/bark-notify/src/bark_notify/cli.py @@ -0,0 +1,87 @@ +"""bark-notify 命令行入口。""" + +from __future__ import annotations + +import argparse +import json +import sys +from typing import List, Optional + +from .client import BarkClient, BarkError +from .types import Level, Notification + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="bark-notify", + description="向手机发送 Bark 通知,覆盖全部通知类型与参数", + ) + parser.add_argument("title", help="通知标题") + parser.add_argument("body", nargs="?", default="", help="通知正文(支持 Markdown)") + parser.add_argument("--subtitle", help="副标题") + parser.add_argument( + "--level", + choices=[level.value for level in Level], + default=Level.DEFAULT.value, + help="通知级别(默认 default)", + ) + parser.add_argument("--sound", help="铃声名(如 alarm.caf)或远程铃声 URL") + parser.add_argument("--badge", type=int, help="应用角标数字") + parser.add_argument("--group", help="分组名:同名通知折叠显示") + parser.add_argument("--icon", help="通知图标 URL") + parser.add_argument("--image", help="附带图片 URL") + parser.add_argument("--copy", help="长按通知复制的文本") + parser.add_argument("--url", help="点击通知跳转的 URL") + parser.add_argument("--archive", action="store_true", help="存入历史记录") + parser.add_argument("--call", action="store_true", help="连续响铃提醒(呼叫声)") + parser.add_argument("--ttl", type=int, help="过期时间(秒)") + parser.add_argument("--volume", type=int, choices=range(0, 11), help="关键通知音量 0-10") + parser.add_argument("--device-key", help="Bark 设备密钥(默认读 BARK_DEVICE_KEY)") + parser.add_argument("--base-url", help="Bark 服务器地址(默认读 BARK_BASE_URL)") + parser.add_argument("--json", dest="raw_json", action="store_true", help="输出服务器原始 JSON") + return parser + + +def main(argv: Optional[List[str]] = None) -> int: + args = build_parser().parse_args(argv) + + client_kwargs = {} + if args.base_url: + client_kwargs["base_url"] = args.base_url + if args.device_key: + client_kwargs["device_key"] = args.device_key + client = BarkClient(**client_kwargs) + + notification = Notification( + title=args.title, + body=args.body, + subtitle=args.subtitle, + level=Level(args.level), + sound=args.sound, + badge=args.badge, + group=args.group, + icon=args.icon, + image=args.image, + copy=args.copy, + url=args.url, + is_archive=args.archive, + call=args.call, + ttl=args.ttl, + volume=args.volume, + ) + + try: + result = client.push(notification) + except BarkError as exc: + print(f"发送失败: {exc}", file=sys.stderr) + return 1 + + if args.raw_json: + print(json.dumps(result, ensure_ascii=False)) + else: + print(f"发送成功: {result.get('message')}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bark-notify/src/bark_notify/client.py b/bark-notify/src/bark_notify/client.py new file mode 100644 index 0000000..197e2fe --- /dev/null +++ b/bark-notify/src/bark_notify/client.py @@ -0,0 +1,137 @@ +"""Bark 推送客户端:基于标准 Bark 服务器 HTTP API,零第三方依赖。""" + +from __future__ import annotations + +import json +import os +import urllib.error +import urllib.parse +import urllib.request +from typing import Any, Dict, Optional, Union + +from .types import Level, Notification, Sound + +DEFAULT_BASE_URL = os.environ.get("BARK_BASE_URL", "http://47.108.226.213:10086") +DEFAULT_DEVICE_KEY = os.environ.get("BARK_DEVICE_KEY", "pvd3kWwHNFjKXNjiC2sMe7") + + +class BarkError(RuntimeError): + """Bark 服务器返回失败或网络异常。""" + + +def _value_of(value: Union[Level, Sound, str, None]) -> Optional[str]: + """枚举成员取其值,普通字符串原样返回。""" + if isinstance(value, (Level, Sound)): + return value.value + return value + + +def _normalize_query(params: Dict[str, Any]) -> Dict[str, Any]: + """把 Python 值转成 GET 查询参数可接受的类型。""" + out: Dict[str, Any] = {} + for key, value in params.items(): + if value is None: + continue + if isinstance(value, bool): + out[key] = "true" if value else "false" + else: + out[key] = _value_of(value) + return out + + +class BarkClient: + """Bark 通知客户端。 + + 支持两种推送方式: + + - :meth:`push`:通过 ``POST /push`` 发送,支持全部参数(推荐)。 + - :meth:`notify`:通过 ``GET ///<body>`` 发送,适合快速测试。 + + 地址与密钥默认从环境变量读取,也可在构造时显式传入: + + - ``BARK_BASE_URL``:服务器地址,默认 ``http://47.108.226.213:10086``。 + - ``BARK_DEVICE_KEY``:设备密钥。 + """ + + def __init__( + self, + base_url: Optional[str] = None, + device_key: Optional[str] = None, + timeout: float = 10.0, + ) -> None: + self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/") + self.device_key = device_key or DEFAULT_DEVICE_KEY + self.timeout = timeout + + def push(self, notification: Notification) -> Dict[str, Any]: + """通过 ``POST /push`` 推送一条完整通知,返回服务器 JSON 响应。""" + payload = self._payload(notification) + payload["device_key"] = self.device_key + data = json.dumps(payload, ensure_ascii=False).encode("utf-8") + return self._request("/push", data=data) + + def notify(self, title: str, body: str = "", **kwargs: Any) -> Dict[str, Any]: + """GET 快捷推送:``/<key>/<title>/<body>?<params>``。 + + ``kwargs`` 中除 ``title/body`` 外的键会转成查询参数, + 布尔值转 ``true/false``,例如 ``level="critical"``、``group="x"``。 + """ + encoded_title = urllib.parse.quote(title) + encoded_body = urllib.parse.quote(body) + path = f"/{self.device_key}/{encoded_title}/{encoded_body}" + params = _normalize_query(kwargs) + params.pop("title", None) + params.pop("body", None) + if params: + path += "?" + urllib.parse.urlencode(params) + return self._request(path) + + # ---------- 内部工具 ---------- + + @staticmethod + def _payload(notification: Notification) -> Dict[str, Any]: + """把 Notification 转成 POST /push 的 JSON body(camelCase 键)。""" + payload: Dict[str, Any] = { + "title": notification.title, + "body": notification.body, + "level": _value_of(notification.level) or Level.DEFAULT.value, + } + optional: Dict[str, Any] = { + "subtitle": notification.subtitle, + "sound": _value_of(notification.sound), + "badge": notification.badge, + "group": notification.group, + "icon": notification.icon, + "image": notification.image, + "copy": notification.copy, + "url": notification.url, + "isArchive": notification.is_archive, + "call": notification.call, + "ttl": notification.ttl, + "volume": notification.volume, + } + for key, value in optional.items(): + if value is not None: + payload[key] = value + return payload + + def _request(self, path: str, data: Optional[bytes] = None) -> Dict[str, Any]: + url = self.base_url + path + req = urllib.request.Request(url, data=data) + if data is not None: + req.add_header("Content-Type", "application/json") + try: + with urllib.request.urlopen(req, timeout=self.timeout) as resp: + raw = resp.read().decode("utf-8") + except urllib.error.HTTPError as exc: + raw = exc.read().decode("utf-8", errors="replace") + except urllib.error.URLError as exc: + raise BarkError(f"网络错误: {exc}") from exc + + try: + result = json.loads(raw) + except json.JSONDecodeError: + result = {"code": -1, "message": raw.strip()} + if result.get("code") != 200: + raise BarkError(f"Bark 返回异常: code={result.get('code')} message={result.get('message')}") + return result diff --git a/bark-notify/src/bark_notify/types.py b/bark-notify/src/bark_notify/types.py new file mode 100644 index 0000000..387c29e --- /dev/null +++ b/bark-notify/src/bark_notify/types.py @@ -0,0 +1,96 @@ +"""Bark 通知的类型定义:级别枚举、铃声枚举与通知数据类。""" + +from __future__ import annotations + +import enum +from dataclasses import dataclass +from typing import Optional + + +class Level(enum.Enum): + """通知级别:决定在 iOS 上的打扰程度。""" + + DEFAULT = "default" + #: 跟随系统默认设置,标准横幅 + 默认铃声。 + ACTIVE = "active" + #: 标准活跃通知。 + TIME_SENSITIVE = "timeSensitive" + #: 时效性通知:即使开启勿扰模式也能送达。 + CRITICAL = "critical" + #: 关键通知:绕过静音/勿扰开关,强制响铃(需设备授权此应用)。 + PASSIVE = "passive" + #: 被动通知:仅进通知中心,无横幅、无声音。 + + +class Sound(enum.Enum): + """Bark 内置铃声(也可直接传自定义远程铃声 URL)。""" + + DEFAULT = "default" + ALARM = "alarm.caf" + ALARMS = "alarms.caf" + ANTICIPATE = "anticipate.caf" + BELL = "bell.caf" + BLOOM = "bloom.caf" + CALYPSO = "calypso.caf" + CHIME = "chime.caf" + CHOIR = "choir.caf" + COMPLETE = "complete.caf" + COMPLETION = "completion.caf" + ELECTRONIC = "electronic.caf" + EMERGENCY = "emergency.caf" + FANFARE = "fanfare.caf" + GLASS = "glass.caf" + GOBLIN = "goblin.caf" + HEALTH_NOTIFICATION = "healthnotification.caf" + HORN = "horn.caf" + LADDERS = "ladders.caf" + MARIO = "mario.caf" + MINUET = "minuet.caf" + MULTIWAY_INVITATION = "multiwayinvitation.caf" + NEW_MAIL = "newmail.caf" + NEWS_FLASH = "newsflash.caf" + NOIR = "noir.caf" + PAYMENT = "payment.caf" + SHAKE = "shake.caf" + SHERWOOD_FANFARE = "sherwoodfanfare.caf" + SILENCE = "silence.caf" + SPELL = "spell.caf" + SUSPENSE = "suspense.caf" + TELEGRAPH = "telegraph.caf" + TIPTOES = "tiptoes.caf" + TYPEWRITERS = "typewriters.caf" + UPDATE = "update.caf" + + +@dataclass +class Notification: + """一条完整的 Bark 通知,覆盖服务器支持的全部参数。""" + + title: str + body: str = "" + #: 副标题。 + subtitle: Optional[str] = None + #: 通知级别,默认 ``Level.DEFAULT``。 + level: Level = Level.DEFAULT + #: 铃声名(如 ``alarm.caf``)或远程铃声 URL。 + sound: Optional[str] = None + #: 应用角标数字。 + badge: Optional[int] = None + #: 分组名:同名的通知会在通知中心折叠成一组。 + group: Optional[str] = None + #: 通知图标 URL。 + icon: Optional[str] = None + #: 通知附带的图片 URL。 + image: Optional[str] = None + #: 长按通知时复制的文本。 + copy: Optional[str] = None + #: 点击通知跳转的 URL。 + url: Optional[str] = None + #: 是否存入 Bark 历史记录。 + is_archive: bool = False + #: 是否连续响铃提醒(呼叫声)。 + call: bool = False + #: 过期时间(秒),超时后通知自动删除。 + ttl: Optional[int] = None + #: 关键通知的音量(0-10),需要 critical 级别。 + volume: Optional[int] = None diff --git a/bark-notify/uv.lock b/bark-notify/uv.lock new file mode 100644 index 0000000..4f9acee --- /dev/null +++ b/bark-notify/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "bark-notify" +version = "0.1.0" +source = { editable = "." }