chore: 删除 bark-notify 模块及相关文件

This commit is contained in:
2026-08-23 23:22:42 +08:00
parent 795e889a99
commit e4fa481e17
10 changed files with 18 additions and 545 deletions
-6
View File
@@ -1,6 +0,0 @@
.venv/
__pycache__/
*.pyc
*.egg-info/
dist/
build/
-94
View File
@@ -1,94 +0,0 @@
# bark-notify
> 📍 工作区位置:`code/bark-notify/`(推送工具)。整体目录规范见根 `CLAUDE.md`。
向手机发送 **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` | 设备密钥 |
@@ -1,85 +0,0 @@
"""发送全部类型的 Bark 通知,用于在手机上逐项核对效果。
用法::
uv run python examples/send_all_types.py
可选参数:
--skip active critical 跳过部分级别
--device-key <key> 指定设备密钥
--base-url <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())
-17
View File
@@ -1,17 +0,0 @@
[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"]
@@ -1,15 +0,0 @@
"""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"]
-87
View File
@@ -1,87 +0,0 @@
"""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())
-137
View File
@@ -1,137 +0,0 @@
"""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 /<key>/<title>/<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 bodycamelCase 键)。"""
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
-96
View File
@@ -1,96 +0,0 @@
"""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
-8
View File
@@ -1,8 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "bark-notify"
version = "0.1.0"
source = { editable = "." }
+18
View File
@@ -447,6 +447,8 @@ dpm 后端新增数据源层,以**园区账号 token** 拉取**本园区**数
- 领域层是核心模型与规则,**不依赖**业务层 / 接口层 / 基础设施层的具体实现(只面向其接口)。
- 基础设施层实现接口,**不得反向依赖**上层。
**全异步**:接口层 `async def` → 业务层 async → 仓储 asyncSQLAlchemy async),事件循环内杜绝阻塞调用;外部 HTTP 用 `httpx.AsyncClient`。见 13.4。
### 13.2 算力通信规范
- **内部**FastAPIserver-core)与**算力调度服务**compute-engine / 算力引擎)经**内部 IP / loopback** 交互(`http://127.0.0.1:3000`),不经过公网。
@@ -460,6 +462,22 @@ server-core 的工程化(数据目录 / 容器启动 / 运维脚本)参考 `
- **`serverrun/`**:容器与微服务启动(`start-all.sh` 编排、各服务 `docker-compose.yml`、`envcopy.sh` 配置加密、`encrypt-certs.sh` 证书加密)。
- **`ops/`**:运维脚本(`init_admin` / `db_backup` / 数据迁移 / `clear_redis_tokens` / 算力套餐与分区初始化等)。
### 13.4 异步与并发(设计之初即考虑)
**全异步调用链**:接口层 `async def` → 业务层 async → 仓储 asyncSQLAlchemy async)。事件循环内**杜绝阻塞**(不用 `time.sleep` / 同步 IO;外部调用统一 `httpx.AsyncClient` 带超时 / 重试 / 熔断)。
| 服务 | 异步方案 | 要点 |
|------|---------|------|
| **MySQL** | SQLAlchemy 2.0 `create_async_engine` + async session`asyncmy` / `aiomysql` | 连接池(`pool_size` / `max_overflow` / `pool_pre_ping`);**每请求独立 async session**`async with` 管事务边界);开发环境 `aiosqlite` |
| **Redis** | `redis.asyncio` | 会话 / 缓存 / 限流 / **分布式锁**(`SET NX EX`);连接池 |
| **OSS 对象存储** | 异步封装(阿里云 OSS / MinIO`aioboto3` 或 oss2 线程池包装) | 上传异步、断点续传、回调通知,不阻塞请求线程 |
**并发正确性**
- 写操作(抢单 / 扣款 / 报名 / 领券)用**行锁**(`SELECT ... FOR UPDATE`+ 幂等键,防并发重复。
- 乐观锁(`version` 字段)处理并发更新冲突。
- 事务边界清晰:多表写(如 `_adopt_phone`、结算 + 佣金)包在一个事务内。
- 外部依赖(算力引擎 / 云厂商)异步调用 + 超时重试,失败可降级。
---
## 附:与既有设计文档的关系