Files
Estrella Pan 667f769a2d fix(plugins): 处理 Codex 审查的六个问题
- apprise:urls 含令牌,改为 secret_field;secret_field 支持 default_factory,可声明列表 / 字典秘密字段
- run-command:stderr 改为自建管道边读边丢,只保留最后 2000 字节;完成仍只看 proc.wait(),结束后给读取 0.5 秒再关闭管道
- run-command:{files} 按原始路径列表展开,含换行的路径不再拆成两个参数
- 通知:插件渠道 extra 里的已声明字段与内置渠道一样展开 $VAR
- slack:只在 HTTP 400 invalid_blocks 时退回纯文字重试,其它失败不重发
- onebot:先发文字,再单独发海报;结果只看文字,去掉重试
- strm-writer:media_root 为 "/" 时正确映射完整路径
2026-10-21 11:26:23 +02:00

129 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OneBot v11 通知渠道。
POST JSON 到 ``{url}/send_private_msg`` 或 ``{url}/send_group_msg``,消息为消息段
数组。先发文字;文字成功且开启海报、宿主给出海报地址时,再单独发一条图片消息。
HTTP 2xx 且返回 ``status == "ok"`` 或 ``retcode == 0`` 才算成功,发送结果只看文字:
OneBot 实现取不到海报时只丢图片,文字已经送达,不重发。
协议文档:https://github.com/botuniverse/onebot-11
"""
from collections.abc import Callable
from contextlib import suppress
from typing import Any, Literal
import httpx2
from pydantic import BaseModel, Field
from ab_sdk import Plugin, points, provider, secret_field
from ab_sdk.notify import NotificationMessage, NotifierFactory, NotifierSettings
class Options(BaseModel):
url: str = Field(
"",
title="API 地址",
description="OneBot v11 HTTP API 地址,如 http://napcat:3000",
)
access_token: str = secret_field(
title="Access Token", description="可选:以 Authorization: Bearer 发送"
)
message_type: Literal["private", "group"] = Field("private", title="消息类型")
target: int = Field(0, title="目标", description="私聊填 QQ 号,群聊填群号")
send_poster: bool = Field(
True,
title="发送海报",
description="需在通知设置里配置 base_url,宿主才会给出可访问的海报地址",
)
class Channel:
def __init__(
self,
options: Options,
settings: NotifierSettings,
client_factory: Callable[[], httpx2.AsyncClient],
) -> None:
self.options = options
self.settings = settings
self.client_factory = client_factory
async def _post(self, message: NotificationMessage) -> tuple[bool, str]:
opts = self.options
if not opts.url or not opts.target:
return False, "未填写 API 地址或目标"
# 单集通知的正文已含番剧名;系统事件的标题与正文分开,拼在一起发
text = (
message.body
if message.kind == "episode"
else f"{message.title}\n{message.body}"
)
try:
async with self.client_factory() as client:
ok, detail = await self._send(
client, [{"type": "text", "data": {"text": text}}]
)
# SDK 只给出公开海报 URL(没有本地路径),由 OneBot 实现自行下载;
# 图片失败(含请求异常)不影响已送达的文字
if ok and opts.send_poster and message.poster_url:
image = {"type": "image", "data": {"file": message.poster_url}}
with suppress(httpx2.HTTPError):
await self._send(client, [image])
except httpx2.HTTPError as e:
return False, f"请求失败:{e}"
return ok, detail
async def _send(
self, client: httpx2.AsyncClient, segments: list[dict[str, Any]]
) -> tuple[bool, str]:
opts = self.options
id_key = "group_id" if opts.message_type == "group" else "user_id"
headers = {}
if opts.access_token:
headers["Authorization"] = f"Bearer {opts.access_token}"
resp = await client.post(
f"{opts.url.rstrip('/')}/send_{opts.message_type}_msg",
json={id_key: opts.target, "message": segments},
headers=headers,
)
try:
body = resp.json()
except ValueError:
body = None
if (
resp.is_success
and isinstance(body, dict)
and (body.get("status") == "ok" or body.get("retcode") == 0)
):
return True, "ok"
detail = (
(body.get("wording") or body.get("msg")) if isinstance(body, dict) else None
)
if not resp.is_success:
# 反向代理的错误页不是 JSON,先报 HTTP 状态
detail = f"HTTP {resp.status_code}" + (f",{detail}" if detail else "")
return False, f"OneBot 返回错误:{detail or '响应不是预期的 JSON'}"
async def send(self, message: NotificationMessage) -> bool:
ok, _ = await self._post(message)
return ok
async def test(self) -> tuple[bool, str]:
return await self._post(
NotificationMessage(
kind="event", title="AutoBangumi 通知测试", body="通知测试成功!"
)
)
class OneBotNotifier(Plugin[Options]):
config_model = Options
# 测试可替换为带 MockTransport 的客户端
client_factory: Callable[[], httpx2.AsyncClient] = staticmethod(
lambda: httpx2.AsyncClient(timeout=10)
)
@provider(points.NOTIFIER, id="onebot")
def channel(self) -> NotifierFactory:
return lambda settings: Channel(self.config, settings, self.client_factory)