nonebot-plugin-parsehub 1.0.0__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/PKG-INFO +21 -19
  2. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/README.md +17 -17
  3. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/pyproject.toml +5 -3
  4. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/pyproject.toml.orig +6 -3
  5. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/SPEC.md +18 -3
  6. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/__init__.py +6 -11
  7. nonebot_plugin_parsehub-1.0.2/src/nonebot_plugin_parsehub/bilibili.py +252 -0
  8. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/budget.py +0 -11
  9. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/cache.py +2 -17
  10. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/card/SPEC.md +2 -1
  11. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/card/__init__.py +2 -3
  12. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/card/cover.py +6 -9
  13. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/card/fonts.py +3 -10
  14. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/card/layout.py +0 -15
  15. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/dedupe.py +0 -26
  16. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/delivery.py +34 -13
  17. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/errors.py +27 -45
  18. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/parsehub_client.py +1 -1
  19. nonebot_plugin_parsehub-1.0.2/src/nonebot_plugin_parsehub/paths.py +56 -0
  20. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/service.py +54 -6
  21. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/switches.py +29 -40
  22. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/triggers.py +8 -7
  23. nonebot_plugin_parsehub-1.0.0/src/nonebot_plugin_parsehub/paths.py +0 -93
  24. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/LICENSE +0 -0
  25. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/app.py +0 -0
  26. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/baidu.png +0 -0
  27. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/bilibili.png +0 -0
  28. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/coolapk.png +0 -0
  29. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/douban.png +0 -0
  30. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/douyin.png +0 -0
  31. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/facebook.png +0 -0
  32. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/instagram.png +0 -0
  33. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/kuaishou.png +0 -0
  34. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/medium.png +0 -0
  35. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/pipix.png +0 -0
  36. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/snapchat.png +0 -0
  37. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/threads.png +0 -0
  38. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/tiktok.png +0 -0
  39. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/toutiao.png +0 -0
  40. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/wechat.png +0 -0
  41. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/weibo.png +0 -0
  42. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/x.png +0 -0
  43. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/xiaoheihe.png +0 -0
  44. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/xiaohongshu.png +0 -0
  45. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/youtube.png +0 -0
  46. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/zhihu.png +0 -0
  47. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/assets/logos/zuiyou.png +0 -0
  48. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/config.py +0 -0
  49. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/feedback.py +0 -0
  50. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/keys.py +0 -0
  51. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/media.py +0 -0
  52. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/platforms.py +0 -0
  53. {nonebot_plugin_parsehub-1.0.0 → nonebot_plugin_parsehub-1.0.2}/src/nonebot_plugin_parsehub/relay.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nonebot-plugin-parsehub
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: 基于 ParseHub 的社交媒体链接解析插件:群里出现分享链接时自动解析并发送媒体
5
5
  Keywords: nonebot,nonebot2,plugin,parsehub,parser,media,onebot
6
6
  Author: ByteColtX
@@ -16,7 +16,9 @@ Requires-Dist: httpx>=0.28.0,<1.0.0
16
16
  Requires-Dist: nonebot-adapter-onebot>=2.4.6,<3.0.0
17
17
  Requires-Dist: nonebot-plugin-localstore>=0.7.4,<1.0.0
18
18
  Requires-Dist: nonebot2>=2.5.0,<3.0.0
19
- Requires-Dist: parsehub>=2.3.10,<3.0.0
19
+ Requires-Dist: numpy>=2.0.0,<3.0.0
20
+ Requires-Dist: opencv-python-headless>=4.13.0.92
21
+ Requires-Dist: parsehub>=2.3.13,<3.0.0
20
22
  Requires-Dist: pillow>=12.1.0,<13.0.0
21
23
  Requires-Dist: pydantic>=2.0.0,<3.0.0
22
24
  Requires-Python: >=3.12
@@ -45,7 +47,9 @@ Description-Content-Type: text/markdown
45
47
 
46
48
  基于 [ParseHub](https://github.com/z-mio/ParseHub) 的社交媒体链接解析插件:群里出现受支持的分享链接时自动解析,发回视频 / 图集 / 图文,并配一张 App Store「Today」风格的信息卡片。
47
49
 
48
- - **自动解析**:发链接或分享文案即可,也可用 `解析` 命令手动触发
50
+ > 仅支持 **OneBot v11** 适配器(NapCat / SnowLuma / LuckyLilliaBot 等)。
51
+
52
+ - **自动解析**:发链接或分享文案即可,也可用 `/解析` 命令手动触发
49
53
  - **按会话开关**:`解析开关` 控制某个群 / 私聊,超管和群管理可操作
50
54
  - **24 个平台**:Bilibili、抖音、小红书、微博、快手、知乎、贴吧、豆瓣、今日头条、微信公众号、微信视频号、酷安、皮皮虾、最右、小黑盒、Bangumi、X/Twitter、Instagram、YouTube、TikTok、Threads、Facebook、Snapchat、Medium
51
55
  - **卡片 + 媒体**:先发信息卡片,再发视频 / 图集;2 个以上媒体合并转发,单个作品最多 20 个
@@ -56,13 +60,9 @@ Description-Content-Type: text/markdown
56
60
 
57
61
  ## 🎨 效果图
58
62
 
59
- B 站视频卡片:
60
-
61
- <img src="https://raw.githubusercontent.com/ByteColtX/nonebot-plugin-parsehub/main/docs/card-bilibili.jpg" width="420" alt="B 站卡片">
62
-
63
- 抖音图集卡片:
64
-
65
- <img src="https://raw.githubusercontent.com/ByteColtX/nonebot-plugin-parsehub/main/docs/card-douyin.jpg" width="420" alt="抖音卡片">
63
+ | B 站视频卡片 | 抖音图集卡片 |
64
+ | :---: | :---: |
65
+ | <img src="https://raw.githubusercontent.com/ByteColtX/nonebot-plugin-parsehub/main/docs/card-bilibili.jpg" width="420" alt="B 站卡片"> | <img src="https://raw.githubusercontent.com/ByteColtX/nonebot-plugin-parsehub/main/docs/card-douyin.jpg" width="420" alt="抖音卡片"> |
66
66
 
67
67
  ## 💿 安装
68
68
 
@@ -76,7 +76,7 @@ nb plugin install nonebot-plugin-parsehub --upgrade
76
76
 
77
77
  使用 uv 安装
78
78
 
79
- 在 nonebot2 项目的插件目录下, 打开命令行, 输入以下指令即可安装
79
+ 在 nonebot2 项目的根目录下, 打开命令行, 输入以下指令即可安装
80
80
 
81
81
  ```bash
82
82
  uv add nonebot-plugin-parsehub
@@ -127,7 +127,7 @@ plugins = ["nonebot_plugin_parsehub"]
127
127
 
128
128
  > 列表类配置必须是合法 JSON(如 `PARSEHUB_PROXY_PLATFORMS=["twitter", "youtube"]`);
129
129
  > 数值项有下界校验(如 `PARSEHUB_TIMEOUT >= 5`),写 0 或负数会在启动时直接报错。
130
- > 哪些群 / 私聊解析由命令 `解析开关` 控制,没有对应的配置项。
130
+ > 哪些群 / 私聊解析由命令 `/解析开关` 控制,没有对应的配置项。
131
131
 
132
132
  需要登录态的平台(X、Instagram、TikTok、豆瓣、微信视频号 等),cookie 优先从 ParseHub 自己的配置读取
133
133
  (`ph set cookie <平台>`),也可以用 `PARSEHUB_COOKIE` 兜底。
@@ -138,16 +138,18 @@ plugins = ["nonebot_plugin_parsehub"]
138
138
 
139
139
  | 指令 | 权限 | 需要@ | 范围 | 说明 |
140
140
  | :--- | :---: | :---: | :---: | :--- |
141
- | `解析 <链接或分享文案>`(别名 `ph`、`解析链接`) | 群员 | 否 | 群聊/私聊 | 手动解析,不做去重拦截 |
142
- | `解析平台` | 群员 | 否 | 群聊/私聊 | 列出支持的平台 |
143
- | `解析开关 [开\|关]`、`解析开`、`解析关` | 主人/群管 | 否 | 群聊/私聊 | 按会话开关解析(私聊仅主人) |
144
- | `解析状态` | 群员 | 否 | 群聊/私聊 | 查看当前会话的开关状态 |
141
+ | `/解析 <链接或分享文案>`(别名 `/ph`、`/解析链接`) | 群员 | 否 | 群聊/私聊 | 手动解析,不做去重拦截 |
142
+ | `/解析平台` | 群员 | 否 | 群聊/私聊 | 列出支持的平台 |
143
+ | `/解析开关 [开\|关]`、`/解析开`、`/解析关` | 主人/群管 | 否 | 群聊/私聊 | 按会话开关解析(私聊仅主人) |
144
+ | `/解析状态` | 群员 | 否 | 群聊/私聊 | 查看当前会话的开关状态 |
145
+
146
+ > 命令按 NoneBot 的 `COMMAND_START` 匹配(默认只有 `/`)。想让无前缀写法也算命令,把 `COMMAND_START` 加上 `""`。
145
147
 
146
148
  ### 触发规则
147
149
 
148
150
  - 群里发链接或分享文案即自动解析,一条消息最多处理 **3 个**链接
149
- - `解析关` 后该会话不再自动解析;手动 `解析` 只对超管 / 群管理有效
150
- - 命令前缀支持空前缀与 `/`(`COMMAND_START` 需包含 `""`)
151
+ - `/解析关` 后该会话不再自动解析;手动 `/解析` 只对超管 / 群管理有效
152
+ - 不带前缀的「解析 <链接>」**不是**命令,会当作普通消息自动解析(带去重、不额外提示)
151
153
 
152
154
  ### 解析开关
153
155
 
@@ -162,7 +164,7 @@ plugins = ["nonebot_plugin_parsehub"]
162
164
  - **保留 24 小时**(命中续期),总量上限 1GB,超了按最久没用过的删
163
165
  - **不同写法归一**:`b23.tv/xxx` 与 `bilibili.com/video/BVxxx` 是同一个作品
164
166
  - 时间线:120 秒内同会话重复 → 静默;120 秒 ~ 24 小时 → 直接发缓存;超过 24 小时 → 重新解析
165
- - 手动 `解析 <链接>` 同样吃缓存(只是不做去重拦截)
167
+ - 手动 `/解析 <链接>` 同样吃缓存(只是不做去重拦截)
166
168
 
167
169
  ### 说明
168
170
 
@@ -18,7 +18,9 @@
18
18
 
19
19
  基于 [ParseHub](https://github.com/z-mio/ParseHub) 的社交媒体链接解析插件:群里出现受支持的分享链接时自动解析,发回视频 / 图集 / 图文,并配一张 App Store「Today」风格的信息卡片。
20
20
 
21
- - **自动解析**:发链接或分享文案即可,也可用 `解析` 命令手动触发
21
+ > 仅支持 **OneBot v11** 适配器(NapCat / SnowLuma / LuckyLilliaBot 等)。
22
+
23
+ - **自动解析**:发链接或分享文案即可,也可用 `/解析` 命令手动触发
22
24
  - **按会话开关**:`解析开关` 控制某个群 / 私聊,超管和群管理可操作
23
25
  - **24 个平台**:Bilibili、抖音、小红书、微博、快手、知乎、贴吧、豆瓣、今日头条、微信公众号、微信视频号、酷安、皮皮虾、最右、小黑盒、Bangumi、X/Twitter、Instagram、YouTube、TikTok、Threads、Facebook、Snapchat、Medium
24
26
  - **卡片 + 媒体**:先发信息卡片,再发视频 / 图集;2 个以上媒体合并转发,单个作品最多 20 个
@@ -29,13 +31,9 @@
29
31
 
30
32
  ## 🎨 效果图
31
33
 
32
- B 站视频卡片:
33
-
34
- <img src="https://raw.githubusercontent.com/ByteColtX/nonebot-plugin-parsehub/main/docs/card-bilibili.jpg" width="420" alt="B 站卡片">
35
-
36
- 抖音图集卡片:
37
-
38
- <img src="https://raw.githubusercontent.com/ByteColtX/nonebot-plugin-parsehub/main/docs/card-douyin.jpg" width="420" alt="抖音卡片">
34
+ | B 站视频卡片 | 抖音图集卡片 |
35
+ | :---: | :---: |
36
+ | <img src="https://raw.githubusercontent.com/ByteColtX/nonebot-plugin-parsehub/main/docs/card-bilibili.jpg" width="420" alt="B 站卡片"> | <img src="https://raw.githubusercontent.com/ByteColtX/nonebot-plugin-parsehub/main/docs/card-douyin.jpg" width="420" alt="抖音卡片"> |
39
37
 
40
38
  ## 💿 安装
41
39
 
@@ -49,7 +47,7 @@ nb plugin install nonebot-plugin-parsehub --upgrade
49
47
 
50
48
  使用 uv 安装
51
49
 
52
- 在 nonebot2 项目的插件目录下, 打开命令行, 输入以下指令即可安装
50
+ 在 nonebot2 项目的根目录下, 打开命令行, 输入以下指令即可安装
53
51
 
54
52
  ```bash
55
53
  uv add nonebot-plugin-parsehub
@@ -100,7 +98,7 @@ plugins = ["nonebot_plugin_parsehub"]
100
98
 
101
99
  > 列表类配置必须是合法 JSON(如 `PARSEHUB_PROXY_PLATFORMS=["twitter", "youtube"]`);
102
100
  > 数值项有下界校验(如 `PARSEHUB_TIMEOUT >= 5`),写 0 或负数会在启动时直接报错。
103
- > 哪些群 / 私聊解析由命令 `解析开关` 控制,没有对应的配置项。
101
+ > 哪些群 / 私聊解析由命令 `/解析开关` 控制,没有对应的配置项。
104
102
 
105
103
  需要登录态的平台(X、Instagram、TikTok、豆瓣、微信视频号 等),cookie 优先从 ParseHub 自己的配置读取
106
104
  (`ph set cookie <平台>`),也可以用 `PARSEHUB_COOKIE` 兜底。
@@ -111,16 +109,18 @@ plugins = ["nonebot_plugin_parsehub"]
111
109
 
112
110
  | 指令 | 权限 | 需要@ | 范围 | 说明 |
113
111
  | :--- | :---: | :---: | :---: | :--- |
114
- | `解析 <链接或分享文案>`(别名 `ph`、`解析链接`) | 群员 | 否 | 群聊/私聊 | 手动解析,不做去重拦截 |
115
- | `解析平台` | 群员 | 否 | 群聊/私聊 | 列出支持的平台 |
116
- | `解析开关 [开\|关]`、`解析开`、`解析关` | 主人/群管 | 否 | 群聊/私聊 | 按会话开关解析(私聊仅主人) |
117
- | `解析状态` | 群员 | 否 | 群聊/私聊 | 查看当前会话的开关状态 |
112
+ | `/解析 <链接或分享文案>`(别名 `/ph`、`/解析链接`) | 群员 | 否 | 群聊/私聊 | 手动解析,不做去重拦截 |
113
+ | `/解析平台` | 群员 | 否 | 群聊/私聊 | 列出支持的平台 |
114
+ | `/解析开关 [开\|关]`、`/解析开`、`/解析关` | 主人/群管 | 否 | 群聊/私聊 | 按会话开关解析(私聊仅主人) |
115
+ | `/解析状态` | 群员 | 否 | 群聊/私聊 | 查看当前会话的开关状态 |
116
+
117
+ > 命令按 NoneBot 的 `COMMAND_START` 匹配(默认只有 `/`)。想让无前缀写法也算命令,把 `COMMAND_START` 加上 `""`。
118
118
 
119
119
  ### 触发规则
120
120
 
121
121
  - 群里发链接或分享文案即自动解析,一条消息最多处理 **3 个**链接
122
- - `解析关` 后该会话不再自动解析;手动 `解析` 只对超管 / 群管理有效
123
- - 命令前缀支持空前缀与 `/`(`COMMAND_START` 需包含 `""`)
122
+ - `/解析关` 后该会话不再自动解析;手动 `/解析` 只对超管 / 群管理有效
123
+ - 不带前缀的「解析 <链接>」**不是**命令,会当作普通消息自动解析(带去重、不额外提示)
124
124
 
125
125
  ### 解析开关
126
126
 
@@ -135,7 +135,7 @@ plugins = ["nonebot_plugin_parsehub"]
135
135
  - **保留 24 小时**(命中续期),总量上限 1GB,超了按最久没用过的删
136
136
  - **不同写法归一**:`b23.tv/xxx` 与 `bilibili.com/video/BVxxx` 是同一个作品
137
137
  - 时间线:120 秒内同会话重复 → 静默;120 秒 ~ 24 小时 → 直接发缓存;超过 24 小时 → 重新解析
138
- - 手动 `解析 <链接>` 同样吃缓存(只是不做去重拦截)
138
+ - 手动 `/解析 <链接>` 同样吃缓存(只是不做去重拦截)
139
139
 
140
140
  ### 说明
141
141
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parsehub"
3
- version = "1.0.0"
3
+ version = "1.0.2"
4
4
  description = "基于 ParseHub 的社交媒体链接解析插件:群里出现分享链接时自动解析并发送媒体"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -27,7 +27,9 @@ dependencies = [
27
27
  "nonebot-adapter-onebot>=2.4.6,<3.0.0",
28
28
  "nonebot-plugin-localstore>=0.7.4,<1.0.0",
29
29
  "nonebot2>=2.5.0,<3.0.0",
30
- "parsehub>=2.3.10,<3.0.0",
30
+ "numpy>=2.0.0,<3.0.0",
31
+ "opencv-python-headless>=4.13.0.92",
32
+ "parsehub>=2.3.13,<3.0.0",
31
33
  "pillow>=12.1.0,<13.0.0",
32
34
  "pydantic>=2.0.0,<3.0.0",
33
35
  ]
@@ -57,7 +59,7 @@ test = [
57
59
  ]
58
60
 
59
61
  [build-system]
60
- requires = ["uv_build>=0.10.0"]
62
+ requires = ["uv_build>=0.10.0,<0.14"]
61
63
  build-backend = "uv_build"
62
64
 
63
65
  [tool.nonebot]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parsehub"
3
- version = "1.0.0"
3
+ version = "1.0.2"
4
4
  description = "基于 ParseHub 的社交媒体链接解析插件:群里出现分享链接时自动解析并发送媒体"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -20,7 +20,10 @@ dependencies = [
20
20
  "nonebot-adapter-onebot>=2.4.6,<3.0.0",
21
21
  "nonebot-plugin-localstore>=0.7.4,<1.0.0",
22
22
  "nonebot2>=2.5.0,<3.0.0",
23
- "parsehub>=2.3.10,<3.0.0",
23
+ # 卡片渐变遮罩与视频抽帧是代码里直接 import 的,必须显式声明
24
+ "numpy>=2.0.0,<3.0.0",
25
+ "opencv-python-headless>=4.13.0.92",
26
+ "parsehub>=2.3.13,<3.0.0",
24
27
  "pillow>=12.1.0,<13.0.0",
25
28
  "pydantic>=2.0.0,<3.0.0",
26
29
  ]
@@ -46,7 +49,7 @@ test = [
46
49
  ]
47
50
 
48
51
  [build-system]
49
- requires = ["uv_build>=0.10.0"]
52
+ requires = ["uv_build>=0.10.0,<0.14"]
50
53
  build-backend = "uv_build"
51
54
 
52
55
  [tool.nonebot]
@@ -16,10 +16,13 @@ tags: [nonebot, parsehub, module]
16
16
 
17
17
  ## Boundary
18
18
 
19
- - **允许依赖**:nonebot2、onebot v11 适配器、parsehub、Pillow、httpx、localstore。
19
+ - **允许依赖**:nonebot2、onebot v11 适配器、parsehub、Pillow、httpx、localstore、
20
+ numpy、opencv-python-headless(后两者只在 `card` 里用)。
20
21
  - **禁止**:`triggers` 之外的模块 import matcher / `on_command`;`delivery` 之外的模块
21
- 调用 `sender.send`;`service` import `Bot`;`card`、`cache`、`dedupe`、`relay`、
22
- `keys`、`platforms`、`budget`、`media` import nonebot(`card` 只允许 Pillow/httpx)。
22
+ 调用 `sender.send`;`service` import `Bot`。
23
+ - **日志**:一律用 `nonebot.logger`(官方指导),领域模块也不例外;除 logger 之外,
24
+ `cache`、`dedupe`、`relay`、`keys`、`platforms`、`budget`、`media`、`card` 不碰
25
+ nonebot 的 matcher / `Bot` / 事件类型。
23
26
  - 包内一律相对 import:插件既可能作为已安装包加载,也可能作为本地插件加载。
24
27
 
25
28
  ## Module map
@@ -33,6 +36,7 @@ tags: [nonebot, parsehub, module]
33
36
  | `delivery.py` | `Sender` 协议、三种上传模式、合并转发、`MediaReport` |
34
37
  | `feedback.py` | `Reaction` 与全部提示文案 |
35
38
  | `parsehub_client.py` | 懒加载客户端、cookie 读取 |
39
+ | `bilibili.py` | B 站默认P归一与单P时长查询 |
36
40
  | `cache.py` | 作品级媒体缓存(原子写、TTL、容量) |
37
41
  | `dedupe.py` | 会话级去重 |
38
42
  | `relay.py` | 只读中转服务 |
@@ -63,6 +67,17 @@ tags: [nonebot, parsehub, module]
63
67
  - 平台白名单在 `Settings` 里归一成 id 集合,运行期只做集合查找。
64
68
  - 配置键是唯一入口:`Config` 只负责读取与校验,运行期一律用 `Settings`,
65
69
  不再散读 `plugin_config`。
70
+ - 命令严格按 `COMMAND_START` 匹配:`should_auto` 不排除命令文本,靠命令匹配器的
71
+ `priority=10` + `block=True` 先收口。裸写的「解析 <链接>」不是命令,按普通消息
72
+ 自动解析——默认前缀只有 `/`,若自动规则也跳过它,消息会被静默丢弃。
73
+ - `paths` 把 localstore 当硬依赖(`require` 后 import),不保留临时目录兜底。
74
+ - 日志统一用 `nonebot.logger`:框架的 loguru 已经管住格式、插件标签与 `LOG_LEVEL`,
75
+ 在包内再桥接一层 stdlib logging 只是多一个可能失效的中间层。
76
+ - 第三方依赖全是**硬依赖**:parsehub / Pillow / numpy / opencv / localstore 缺了就是
77
+ 装错了,代码里不写 `except ImportError` 兜底,也不往别的目录写;降级只发生在产品层
78
+ (卡片→文字、字体→系统字体、体积探测→未知)。
79
+ - B 站分P由 `bilibili` 收口:解析前去掉 `p`(只解析默认P),解析后用
80
+ `pages[0].duration` 覆盖 ParseHub 给的分P总时长;查不到就保持原值。
66
81
 
67
82
  ## Known limitations
68
83
 
@@ -24,19 +24,19 @@ from __future__ import annotations
24
24
  from nonebot import get_driver
25
25
  from nonebot.plugin import PluginMetadata
26
26
 
27
- from . import feedback, triggers
27
+ from . import triggers
28
28
  from .app import App
29
29
  from .config import Config, Settings, plugin_config
30
30
 
31
31
  __plugin_meta__ = PluginMetadata(
32
- name="ParseHub 链接解析",
32
+ name="轻量、异步、开箱即用的社交媒体聚合解析插件",
33
33
  description=(
34
34
  "群里出现受支持的分享链接时,用 ParseHub 自动解析并发送媒体"
35
35
  "(视频 / 图集 / 正文)。"
36
36
  ),
37
37
  usage=(
38
- "发链接即可自动解析;命令:解析 <链接或分享文案>、解析平台、"
39
- "解析开关 [开|关](超管 / 群管理)、解析状态"
38
+ "发链接即可自动解析;命令:/解析 <链接或分享文案>、/解析平台、"
39
+ "/解析开关 [开|关](超管 / 群管理)、/解析状态"
40
40
  ),
41
41
  type="application",
42
42
  homepage="https://github.com/ByteColtX/nonebot-plugin-parsehub",
@@ -45,16 +45,11 @@ __plugin_meta__ = PluginMetadata(
45
45
  extra={"parsehub": "https://github.com/z-mio/ParseHub"},
46
46
  )
47
47
 
48
- # 表情 ID:收到 / 成功 / 失败。
49
- REACT_START = feedback.Reaction.START
50
- REACT_OK = feedback.Reaction.OK
51
- REACT_FAIL = feedback.Reaction.FAIL
52
-
53
48
  # 进程内唯一的插件实例;测试可以自己 App.build() 一个。
54
49
  APP = App.build(Settings.from_config(plugin_config))
55
50
 
56
- # matcher 注册(处理函数闭包捕获 APP)。
57
- MATCHERS = triggers.create_matchers(APP)
51
+ # matcher 注册(处理函数闭包捕获 APP);注册本身有副作用,不需要保留返回值。
52
+ triggers.create_matchers(APP)
58
53
 
59
54
  driver = get_driver()
60
55
  driver.on_startup(APP.startup)
@@ -0,0 +1,252 @@
1
+ # Copyright 2026 ByteColtX
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """B 站分P兼容:只解析默认P,并按默认P的时长判断。
5
+
6
+ ParseHub 的 B 站解析器在 ``p=1`` 时直接取 ``View.duration``,而 B 站这个字段
7
+ 是**所有分P的时长总和**;分P越多,插件越容易误判成超长视频。这里在解析前后各
8
+ 补一刀:
9
+
10
+ 1. 解析前去掉 ``p`` 参数,让 ParseHub 始终解析默认P;
11
+ 2. 解析后向 B 站查一次 ``pages[0].duration``,覆盖结果里的总时长。
12
+
13
+ 第 2 步失败时保持原样(退化成旧行为),不影响解析和下载。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from typing import Any
20
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
21
+
22
+ import httpx
23
+ from nonebot import logger
24
+
25
+ # B 站视频 ID。BV 号大小写敏感,av 号只在路径或参数里认。
26
+ _BV_PATTERN = re.compile(r"(BV[0-9A-Za-z]{10})")
27
+ _AV_PATTERN = re.compile(r"(?:/|=)av(\d+)", re.IGNORECASE)
28
+ _AID_PATTERN = re.compile(r"[?&]aid=(\d+)")
29
+
30
+ # 需要做默认P归一的主机。短链没有 ID,但同样可能带 p 参数。
31
+ _VIDEO_HOSTS = ("bilibili.com", "b23.tv", "bili2233.cn")
32
+ _SHORT_HOSTS = ("b23.tv", "bili2233.cn")
33
+
34
+ _VIEW_API = "https://api.bilibili.com/x/web-interface/view"
35
+ _USER_AGENT = (
36
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
37
+ "(KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"
38
+ )
39
+ _TIMEOUT_SECONDS = 5.0
40
+
41
+
42
+ def _video_params(url: str) -> dict[str, str] | None:
43
+ """从链接里取出 B 站视频 ID。
44
+
45
+ Args:
46
+ url: 可能是完整链或短链展开后的链接。
47
+
48
+ Returns:
49
+ ``{"bvid": ...}`` 或 ``{"aid": ...}``;认不出来时返回 None。
50
+ """
51
+ match = _BV_PATTERN.search(url)
52
+ if match:
53
+ return {"bvid": match.group(1)}
54
+ match = _AV_PATTERN.search(url)
55
+ if match:
56
+ return {"aid": match.group(1)}
57
+ match = _AID_PATTERN.search(url)
58
+ if match:
59
+ return {"aid": match.group(1)}
60
+ return None
61
+
62
+
63
+ def _is_video_host(host: str) -> bool:
64
+ """判断主机是不是 B 站视频站(含短链站)。
65
+
66
+ Args:
67
+ host: 已小写的主机名。
68
+
69
+ Returns:
70
+ 命中返回 True。
71
+ """
72
+ return any(
73
+ host == item or host.endswith("." + item) for item in _VIDEO_HOSTS
74
+ )
75
+
76
+
77
+ def _is_short_host(host: str) -> bool:
78
+ """判断主机是不是 B 站短链站。
79
+
80
+ Args:
81
+ host: 已小写的主机名。
82
+
83
+ Returns:
84
+ 命中返回 True。
85
+ """
86
+ return any(
87
+ host == item or host.endswith("." + item) for item in _SHORT_HOSTS
88
+ )
89
+
90
+
91
+ def default_part_url(url: str) -> str:
92
+ """去掉 B 站视频链接里的 ``p`` 参数。
93
+
94
+ 去掉后 ParseHub 会解析默认P(P1);对短链也先去掉,展开后即使带 ``p``
95
+ 也不会被保留。
96
+
97
+ Args:
98
+ url: 待解析的链接。
99
+
100
+ Returns:
101
+ 去掉 ``p`` 后的链接;非 B 站视频链接原样返回。
102
+ """
103
+ parsed = urlsplit(url)
104
+ host = (parsed.hostname or "").lower()
105
+ if not _is_video_host(host):
106
+ return url
107
+ if not _is_short_host(host) and _video_params(url) is None:
108
+ return url
109
+ query = [
110
+ (key, value)
111
+ for key, value in parse_qsl(parsed.query, keep_blank_values=True)
112
+ if key.lower() != "p"
113
+ ]
114
+ return urlunsplit(parsed._replace(query=urlencode(query)))
115
+
116
+
117
+ async def resolve_default_part_url(url: str, proxy: str = "") -> str:
118
+ """展开短链后再去掉 ``p``,确保短链也只解析默认P。
119
+
120
+ 短链本身没有 ID,ParseHub 展开时会把重定向目标里的 ``p`` 保留下来;这里先
121
+ 自己展开一次,拿到完整链后统一去掉 ``p``。展开失败就退回原链接。
122
+
123
+ Args:
124
+ url: 用户发的链接。
125
+ proxy: 展开短链用的代理。
126
+
127
+ Returns:
128
+ 指向默认P的完整链;非 B 站短链原样返回。
129
+ """
130
+ parsed = urlsplit(url)
131
+ host = (parsed.hostname or "").lower()
132
+ if not _is_short_host(host):
133
+ return default_part_url(url)
134
+ try:
135
+ async with _client(proxy) as client:
136
+ response = await client.get(
137
+ url,
138
+ headers={"User-Agent": _USER_AGENT},
139
+ follow_redirects=True,
140
+ )
141
+ response.raise_for_status()
142
+ resolved = str(response.url)
143
+ except Exception as exc: # noqa: BLE001 - 展开失败交给 ParseHub 自己处理
144
+ logger.debug(
145
+ f"[parsehub] 展开 B 站短链失败: {type(exc).__name__}: {exc}"
146
+ )
147
+ return default_part_url(url)
148
+ # 重定向到非视频页时把短链还给 ParseHub,让它按原链接选解析器。
149
+ if _video_params(resolved) is None:
150
+ return default_part_url(url)
151
+ return default_part_url(resolved)
152
+
153
+
154
+ def default_page_seconds(data: Any) -> int: # noqa: ANN401 - B 站接口 JSON
155
+ """从 B 站 ``view`` 接口的 ``data`` 里取默认P时长。
156
+
157
+ Args:
158
+ data: 接口返回的 ``data`` 字段。
159
+
160
+ Returns:
161
+ 默认P(``pages[0]``)的秒数;拿不到时退回总时长,再拿不到返回 0。
162
+ """
163
+ if not isinstance(data, dict):
164
+ return 0
165
+ pages = data.get("pages")
166
+ if isinstance(pages, list) and pages and isinstance(pages[0], dict):
167
+ try:
168
+ return max(int(pages[0].get("duration") or 0), 0)
169
+ except (TypeError, ValueError):
170
+ pass
171
+ try:
172
+ return max(int(data.get("duration") or 0), 0)
173
+ except (TypeError, ValueError):
174
+ return 0
175
+
176
+
177
+ def _client(proxy: str) -> httpx.AsyncClient:
178
+ """构造查接口用的 HTTP 客户端(测试里替换成不联网的假实现)。
179
+
180
+ Args:
181
+ proxy: 代理地址,空字符串表示直连。
182
+
183
+ Returns:
184
+ 异步 HTTP 客户端。
185
+ """
186
+ return httpx.AsyncClient(
187
+ proxy=proxy or None,
188
+ timeout=_TIMEOUT_SECONDS,
189
+ follow_redirects=True,
190
+ )
191
+
192
+
193
+ async def default_part_seconds(
194
+ url: str,
195
+ proxy: str = "",
196
+ cookie: str = "",
197
+ ) -> int:
198
+ """查询 B 站视频默认P的时长。
199
+
200
+ Args:
201
+ url: 视频链接(完整链或已展开的短链)。
202
+ proxy: 解析代理,空字符串表示直连。
203
+ cookie: 登录态 cookie,空字符串表示不带。
204
+
205
+ Returns:
206
+ 默认P的秒数;不是 B 站视频或请求失败时返回 0。
207
+ """
208
+ params = _video_params(url)
209
+ if params is None:
210
+ return 0
211
+ headers = {
212
+ "User-Agent": _USER_AGENT,
213
+ "Referer": "https://www.bilibili.com/",
214
+ }
215
+ if cookie:
216
+ headers["Cookie"] = cookie
217
+ try:
218
+ async with _client(proxy) as client:
219
+ response = await client.get(
220
+ _VIEW_API, params=params, headers=headers
221
+ )
222
+ response.raise_for_status()
223
+ payload = response.json()
224
+ except Exception as exc: # noqa: BLE001 - 查时长失败不能影响解析
225
+ logger.debug(
226
+ f"[parsehub] 查询 B 站默认P时长失败: {type(exc).__name__}: {exc}"
227
+ )
228
+ return 0
229
+ data = payload.get("data") if isinstance(payload, dict) else None
230
+ return default_page_seconds(data)
231
+
232
+
233
+ def apply_duration(result: Any, seconds: int) -> None: # noqa: ANN401 - parsehub 结果
234
+ """把默认P时长写回解析结果里的第一个媒体。
235
+
236
+ 卡片和时长上限都从 ``result.media[].duration`` 读,改这里两边一起生效。
237
+
238
+ Args:
239
+ result: ParseHub 的解析结果。
240
+ seconds: 默认P的秒数。
241
+ """
242
+ if seconds <= 0:
243
+ return
244
+ media = getattr(result, "media", None)
245
+ items = list(media) if isinstance(media, (list, tuple)) else [media]
246
+ for item in items:
247
+ if item is not None and hasattr(item, "duration"):
248
+ try:
249
+ item.duration = seconds
250
+ except (AttributeError, TypeError):
251
+ pass
252
+ return
@@ -52,17 +52,6 @@ class Budget:
52
52
  """
53
53
  return self.remaining <= 0
54
54
 
55
- def slice(self, cap: float) -> float:
56
- """取一段不超过剩余预算的超时时间。
57
-
58
- Args:
59
- cap: 这一段最多允许多久(秒)。
60
-
61
- Returns:
62
- 实际可用的秒数,至少 :data:`_MIN_SLICE_SECONDS`。
63
- """
64
- return max(_MIN_SLICE_SECONDS, min(cap, self.remaining))
65
-
66
55
  def timeout(self) -> AbstractAsyncContextManager[None]:
67
56
  """套在某一段异步调用外面的超时上下文。
68
57
 
@@ -20,14 +20,13 @@
20
20
 
21
21
  所有方法都是同步的(磁盘 IO),调用方负责放进线程池。
22
22
 
23
- 模块不依赖 nonebot,方便单独测试。
23
+ 除 ``nonebot.logger`` 外不依赖 nonebot,方便单独测试。
24
24
  """
25
25
 
26
26
  from __future__ import annotations
27
27
 
28
28
  import hashlib
29
29
  import json
30
- import logging
31
30
  import os
32
31
  import shutil
33
32
  import time
@@ -36,7 +35,7 @@ from collections.abc import Iterable
36
35
  from dataclasses import dataclass
37
36
  from pathlib import Path
38
37
 
39
- logger = logging.getLogger("nonebot_plugin_parsehub.cache")
38
+ from nonebot import logger
40
39
 
41
40
  # 目录与文件名。
42
41
  FILES_DIR_NAME = "files"
@@ -183,20 +182,6 @@ class MediaCache:
183
182
  keys=tuple(meta.get("keys") or ()),
184
183
  )
185
184
 
186
- def entry_keys(self, entry: Path) -> list[str]:
187
- """条目对应的所有作品 key。
188
-
189
- Args:
190
- entry: 条目目录。
191
-
192
- Returns:
193
- 作品 key 列表;读不出来时为空列表。
194
- """
195
- meta = self._read_meta(entry)
196
- if meta is None:
197
- return []
198
- return [str(key) for key in meta.get("keys") or () if key]
199
-
200
185
  def remember_key(self, entry: Path, key: str) -> None:
201
186
  """把 key 补记到条目里(老条目命中时顺手补上)。
202
187
 
@@ -16,7 +16,8 @@ tags: [nonebot, parsehub, card]
16
16
 
17
17
  ## Boundary
18
18
 
19
- - 只依赖 Pillow、httpx 与包内的 `platforms`;**不 import nonebot**。
19
+ - 只依赖 Pillow、httpx、numpy(渐变遮罩)、`nonebot.logger` 与包内的 `platforms`;
20
+ 视频抽帧另用 opencv(缺了就跳过这一级兜底)。不碰 matcher / `Bot` / 事件类型。
20
21
  - 所有渲染是同步阻塞的 CPU 活,调用方必须放进线程池(`CardRenderer` 负责)。
21
22
 
22
23
  ## Layout
@@ -16,11 +16,12 @@
16
16
  from __future__ import annotations
17
17
 
18
18
  import asyncio
19
- import logging
20
19
  from collections.abc import Callable
21
20
  from pathlib import Path
22
21
  from typing import Any
23
22
 
23
+ from nonebot import logger
24
+
24
25
  from ..platforms import PLATFORMS
25
26
  from . import cover, fonts, layout
26
27
  from .cover import fetch_cover
@@ -96,8 +97,6 @@ __all__ = [
96
97
  "wrap",
97
98
  ]
98
99
 
99
- logger = logging.getLogger("nonebot_plugin_parsehub.card")
100
-
101
100
 
102
101
  class CardRenderer:
103
102
  """把解析结果渲染成卡片图。