nonebot-plugin-zhihu-parser 0.1.0__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 (40) hide show
  1. nonebot_plugin_zhihu_parser-0.1.0/.github/workflows/publish.yml +44 -0
  2. nonebot_plugin_zhihu_parser-0.1.0/.gitignore +3 -0
  3. nonebot_plugin_zhihu_parser-0.1.0/LICENSE +21 -0
  4. nonebot_plugin_zhihu_parser-0.1.0/PKG-INFO +128 -0
  5. nonebot_plugin_zhihu_parser-0.1.0/README.md +93 -0
  6. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/__init__.py +331 -0
  7. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/config.py +84 -0
  8. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/clean.py +35 -0
  9. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/constants.py +24 -0
  10. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/cookie.py +438 -0
  11. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/data.py +359 -0
  12. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/debounce.py +36 -0
  13. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/download.py +464 -0
  14. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/exception.py +53 -0
  15. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/__init__.py +1 -0
  16. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/base.py +345 -0
  17. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/__init__.py +3 -0
  18. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/card.py +340 -0
  19. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/common.py +6 -0
  20. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/content.py +1268 -0
  21. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/handlers.py +269 -0
  22. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/identity.py +8 -0
  23. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/maintenance.py +261 -0
  24. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/parser.py +85 -0
  25. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/refresher.py +167 -0
  26. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/request.py +209 -0
  27. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/parsers/zhihu/zse_signer.py +224 -0
  28. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/render.py +712 -0
  29. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/HYSongYunLangHeiW-1.ttf +0 -0
  30. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/logos/bilibili.png +0 -0
  31. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/logos/douyin.png +0 -0
  32. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/logos/kuaishou.png +0 -0
  33. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/logos/tiktok.png +0 -0
  34. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/logos/twitter.png +0 -0
  35. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/logos/weibo.png +0 -0
  36. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/logos/xhs.png +0 -0
  37. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/logos/youtube.png +0 -0
  38. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/resources/media_button.png +0 -0
  39. nonebot_plugin_zhihu_parser-0.1.0/nonebot_plugin_zhihu_parser/core/utils.py +242 -0
  40. nonebot_plugin_zhihu_parser-0.1.0/pyproject.toml +49 -0
@@ -0,0 +1,44 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distribution
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.x"
18
+
19
+ - name: Build sdist and wheel
20
+ run: |
21
+ python -m pip install --upgrade build
22
+ python -m build
23
+
24
+ - name: Store distribution packages
25
+ uses: actions/upload-artifact@v4
26
+ with:
27
+ name: python-package-distributions
28
+ path: dist/
29
+
30
+ publish:
31
+ name: Publish to PyPI
32
+ needs: [build]
33
+ runs-on: ubuntu-latest
34
+ permissions:
35
+ id-token: write
36
+ steps:
37
+ - name: Download distribution packages
38
+ uses: actions/download-artifact@v4
39
+ with:
40
+ name: python-package-distributions
41
+ path: dist/
42
+
43
+ - name: Publish to PyPI
44
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,3 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ !.gitignore
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 devil233
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.5
2
+ Name: nonebot-plugin-zhihu-parser
3
+ Version: 0.1.0
4
+ Summary: 知乎链接解析与图文转发的 NoneBot2 插件
5
+ Project-URL: Homepage, https://github.com/devil233-ui/nonebot-plugin-zhihu-parser
6
+ Project-URL: Repository, https://github.com/devil233-ui/nonebot-plugin-zhihu-parser
7
+ Project-URL: Issues, https://github.com/devil233-ui/nonebot-plugin-zhihu-parser/issues
8
+ Author-email: devil233 <1250226509@qq.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: nonebot2,zhihu,知乎,链接解析
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Communications :: Chat
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: aiofiles>=23.1.0
22
+ Requires-Dist: aiohttp>=3.8.0
23
+ Requires-Dist: apilmoji<=0.3.1
24
+ Requires-Dist: beautifulsoup4>=4.12.0
25
+ Requires-Dist: curl-cffi>=0.7.0
26
+ Requires-Dist: httpx>=0.27.0
27
+ Requires-Dist: msgspec>=0.18.0
28
+ Requires-Dist: nonebot-adapter-onebot>=2.4.0
29
+ Requires-Dist: nonebot-plugin-apscheduler>=0.4.0
30
+ Requires-Dist: nonebot2>=2.3.0
31
+ Requires-Dist: pillow>=9.5.0
32
+ Requires-Dist: tqdm>=4.67.3
33
+ Requires-Dist: typing-extensions>=4.1.0
34
+ Description-Content-Type: text/markdown
35
+
36
+ # nonebot-plugin-zhihu-parser
37
+
38
+ 知乎链接解析插件(从 AstrBot 移植):自动解析群聊里的知乎链接,发送预览卡片并转发图文内容。
39
+
40
+ ## 功能
41
+
42
+ - 支持解析「问题 / 回答 / 专栏文章 / 想法(pin)」链接
43
+ - 解析结果发送预览卡片,正文按批次合并转发,超量自动截断并提示原链接
44
+ - 引用一条含知乎链接的消息回复「解析」可重新解析
45
+ - 登录信息(Cookie)自动保活,连续失败自动提醒管理员
46
+ - 过期缓存自动清理(默认 7 天)
47
+
48
+ ## 安装
49
+
50
+ 推荐使用 nb-cli 安装:
51
+
52
+ ```bash
53
+ nb plugin install nonebot-plugin-zhihu-parser
54
+ ```
55
+
56
+ 也可以使用 pip 安装:
57
+
58
+ ```bash
59
+ pip install nonebot-plugin-zhihu-parser
60
+ ```
61
+
62
+ 未配置知乎登录信息时,部分内容会被知乎要求登录(HTTP 403),建议首次使用
63
+ 时先按下文「登录保活与失败提醒」配置 Cookie。
64
+
65
+ ## 使用方法
66
+
67
+ - 直接发送知乎链接(`www.zhihu.com/question/...`、`zhuanlan.zhihu.com/p/...`、`www.zhihu.com/pin/...`)即可自动解析
68
+ - 引用含知乎链接的消息,回复「解析」「提取」「解析知乎」触发解析
69
+ - 管理员发送「知乎保活」(或「刷新知乎ck」「知乎刷新」)手动刷新登录信息,机器人直接回复成功或失败原因
70
+
71
+ ## 配置项
72
+
73
+ 在 `.env.*` 中按需配置(以下为插件默认值):
74
+
75
+ ```dotenv
76
+ # 单次解析最多输出的图片/视频总量阈值
77
+ ZHIHU_MAX_IMAGES=50
78
+ # 合并转发的单批次图片最大数量
79
+ ZHIHU_BATCH_SIZE=10
80
+ # 同一个链接的冷却时间(单位:秒。86400秒 = 24小时)
81
+ ZHIHU_DEBOUNCE_TIME=86400
82
+ # 自动清理多久以前的缓存文件(单位:秒。604800秒 = 7天)
83
+ ZHIHU_CACHE_MAX_AGE=604800
84
+ # 知乎登录 Cookie(推荐留空,改用下方的 CK 文件管理)
85
+ ZHIHU_COOKIE=""
86
+ ```
87
+
88
+ ## 登录保活与失败提醒
89
+
90
+ 登录信息文件位于机器人工作目录 `config/nonebot_plugin_zhihu_parser/zhihu_cookies.txt`,
91
+ 单行 `k=v; k=v` 格式,需包含 `z_c0`、`d_c0`、`_xsrf`、`SESSIONID` 四个键;
92
+ 替换文件后无需重启即可生效。
93
+
94
+ - 自动保活沿用固定的日期安排,按机器人配置的时区在凌晨4点后随机延迟最多15分钟,
95
+ 不再每次都卡在整点执行。这是错峰措施,不保证避免平台风控。
96
+ - 连续两次保活失败后发送提醒,同一次故障只提醒一次;成功后重新开始计数。
97
+ 计数和送达状态保存在插件数据目录,重启不会清零。
98
+ - 通知未送达时保留待发状态,在机器人重新连接或下次保活时补发;
99
+ 已送达的接收人不会重复收到同一故障的提醒。
100
+ - 未指定接收群时,默认私聊现有的机器人管理员,不会向所有群广播。
101
+ 指定群后只通知配置的目标;同时配置私聊账号时,两类目标都会收到提醒。
102
+ - 管理员可发送“知乎保活”手动刷新,直接看到成功或失败原因;
103
+ 非管理员不能触发登录信息刷新。登录失效需要重新登录,保活不能替代登录。
104
+
105
+ 可选配置(与现有配置放在一起,修改后重启机器人生效):
106
+
107
+ ```dotenv
108
+ # 提醒接收群;留空不发送群提醒
109
+ ZHIHU_REFRESH_ALERT_GROUPS = []
110
+ # 可选:指定私聊接收账号,显式设为空列表可关闭默认管理员私聊
111
+ # ZHIHU_REFRESH_ALERT_USERS = []
112
+ # 随机延迟上限,单位秒;默认900,允许0到3600
113
+ ZHIHU_REFRESH_JITTER_SECONDS = 900
114
+ ```
115
+
116
+ 解析和保活共用同一份浏览器身份;保活请求设有超时限制。
117
+ 刷新失败不会写回响应中的空登录信息,刷新期间手动更新的登录信息也不会被覆盖。
118
+ 不要将Cookie、刷新令牌或完整响应内容发送到群聊、日志或提交到仓库。
119
+
120
+ ## 已知问题
121
+
122
+ 目前测试所谓任意风控部分均可通过单独发送解决,其实并不能确定是风控引起的;又,最初测试过一个批次输出50张图都能顺利,可以排除体积原因。
123
+
124
+ 这个奇妙风控在原项目同样存在(但没有备用方案),所以总之目前本项目只能通过独立发送解决这个问题,希望有高人指点。
125
+
126
+ ## 致谢
127
+
128
+ 原项目:[astrbot_plugin_parser](https://github.com/Zhalslar/astrbot_plugin_parser) 高性能低耦合的万能链接解析器。支持的类型:视频、图集、音频。 支持的平台:A站、B站、抖音、tiktok、微博、小红书、快手、油管、推特...
@@ -0,0 +1,93 @@
1
+ # nonebot-plugin-zhihu-parser
2
+
3
+ 知乎链接解析插件(从 AstrBot 移植):自动解析群聊里的知乎链接,发送预览卡片并转发图文内容。
4
+
5
+ ## 功能
6
+
7
+ - 支持解析「问题 / 回答 / 专栏文章 / 想法(pin)」链接
8
+ - 解析结果发送预览卡片,正文按批次合并转发,超量自动截断并提示原链接
9
+ - 引用一条含知乎链接的消息回复「解析」可重新解析
10
+ - 登录信息(Cookie)自动保活,连续失败自动提醒管理员
11
+ - 过期缓存自动清理(默认 7 天)
12
+
13
+ ## 安装
14
+
15
+ 推荐使用 nb-cli 安装:
16
+
17
+ ```bash
18
+ nb plugin install nonebot-plugin-zhihu-parser
19
+ ```
20
+
21
+ 也可以使用 pip 安装:
22
+
23
+ ```bash
24
+ pip install nonebot-plugin-zhihu-parser
25
+ ```
26
+
27
+ 未配置知乎登录信息时,部分内容会被知乎要求登录(HTTP 403),建议首次使用
28
+ 时先按下文「登录保活与失败提醒」配置 Cookie。
29
+
30
+ ## 使用方法
31
+
32
+ - 直接发送知乎链接(`www.zhihu.com/question/...`、`zhuanlan.zhihu.com/p/...`、`www.zhihu.com/pin/...`)即可自动解析
33
+ - 引用含知乎链接的消息,回复「解析」「提取」「解析知乎」触发解析
34
+ - 管理员发送「知乎保活」(或「刷新知乎ck」「知乎刷新」)手动刷新登录信息,机器人直接回复成功或失败原因
35
+
36
+ ## 配置项
37
+
38
+ 在 `.env.*` 中按需配置(以下为插件默认值):
39
+
40
+ ```dotenv
41
+ # 单次解析最多输出的图片/视频总量阈值
42
+ ZHIHU_MAX_IMAGES=50
43
+ # 合并转发的单批次图片最大数量
44
+ ZHIHU_BATCH_SIZE=10
45
+ # 同一个链接的冷却时间(单位:秒。86400秒 = 24小时)
46
+ ZHIHU_DEBOUNCE_TIME=86400
47
+ # 自动清理多久以前的缓存文件(单位:秒。604800秒 = 7天)
48
+ ZHIHU_CACHE_MAX_AGE=604800
49
+ # 知乎登录 Cookie(推荐留空,改用下方的 CK 文件管理)
50
+ ZHIHU_COOKIE=""
51
+ ```
52
+
53
+ ## 登录保活与失败提醒
54
+
55
+ 登录信息文件位于机器人工作目录 `config/nonebot_plugin_zhihu_parser/zhihu_cookies.txt`,
56
+ 单行 `k=v; k=v` 格式,需包含 `z_c0`、`d_c0`、`_xsrf`、`SESSIONID` 四个键;
57
+ 替换文件后无需重启即可生效。
58
+
59
+ - 自动保活沿用固定的日期安排,按机器人配置的时区在凌晨4点后随机延迟最多15分钟,
60
+ 不再每次都卡在整点执行。这是错峰措施,不保证避免平台风控。
61
+ - 连续两次保活失败后发送提醒,同一次故障只提醒一次;成功后重新开始计数。
62
+ 计数和送达状态保存在插件数据目录,重启不会清零。
63
+ - 通知未送达时保留待发状态,在机器人重新连接或下次保活时补发;
64
+ 已送达的接收人不会重复收到同一故障的提醒。
65
+ - 未指定接收群时,默认私聊现有的机器人管理员,不会向所有群广播。
66
+ 指定群后只通知配置的目标;同时配置私聊账号时,两类目标都会收到提醒。
67
+ - 管理员可发送“知乎保活”手动刷新,直接看到成功或失败原因;
68
+ 非管理员不能触发登录信息刷新。登录失效需要重新登录,保活不能替代登录。
69
+
70
+ 可选配置(与现有配置放在一起,修改后重启机器人生效):
71
+
72
+ ```dotenv
73
+ # 提醒接收群;留空不发送群提醒
74
+ ZHIHU_REFRESH_ALERT_GROUPS = []
75
+ # 可选:指定私聊接收账号,显式设为空列表可关闭默认管理员私聊
76
+ # ZHIHU_REFRESH_ALERT_USERS = []
77
+ # 随机延迟上限,单位秒;默认900,允许0到3600
78
+ ZHIHU_REFRESH_JITTER_SECONDS = 900
79
+ ```
80
+
81
+ 解析和保活共用同一份浏览器身份;保活请求设有超时限制。
82
+ 刷新失败不会写回响应中的空登录信息,刷新期间手动更新的登录信息也不会被覆盖。
83
+ 不要将Cookie、刷新令牌或完整响应内容发送到群聊、日志或提交到仓库。
84
+
85
+ ## 已知问题
86
+
87
+ 目前测试所谓任意风控部分均可通过单独发送解决,其实并不能确定是风控引起的;又,最初测试过一个批次输出50张图都能顺利,可以排除体积原因。
88
+
89
+ 这个奇妙风控在原项目同样存在(但没有备用方案),所以总之目前本项目只能通过独立发送解决这个问题,希望有高人指点。
90
+
91
+ ## 致谢
92
+
93
+ 原项目:[astrbot_plugin_parser](https://github.com/Zhalslar/astrbot_plugin_parser) 高性能低耦合的万能链接解析器。支持的类型:视频、图集、音频。 支持的平台:A站、B站、抖音、tiktok、微博、小红书、快手、油管、推特...
@@ -0,0 +1,331 @@
1
+ import re
2
+ from nonebot import on_message, on_command, get_bots
3
+ from nonebot.permission import SUPERUSER
4
+ from nonebot.adapters.onebot.v11 import Message, MessageSegment, MessageEvent, Bot, GroupMessageEvent, PrivateMessageEvent
5
+ from nonebot.log import logger
6
+ from nonebot import get_driver
7
+ from nonebot.rule import Rule
8
+ from nonebot.typing import T_State
9
+
10
+ from nonebot.plugin import PluginMetadata
11
+ from nonebot_plugin_zhihu_parser.config import PluginConfig
12
+ from .core.parsers.zhihu.parser import ZhihuParser
13
+ from .core.download import Downloader
14
+ from .core.render import Renderer
15
+ from .core.debounce import Debouncer
16
+ from .core.clean import CacheCleaner
17
+ from .core.exception import ParseException
18
+
19
+ # 洗血专用依赖
20
+ from nonebot import require
21
+ require("nonebot_plugin_apscheduler")
22
+ from nonebot_plugin_apscheduler import scheduler
23
+ from nonebot import on_command
24
+ from .core.parsers.zhihu.refresher import do_refresh_token
25
+ from .core.parsers.zhihu.maintenance import (
26
+ RefreshMonitor, format_refresh_result, refresh_alert_targets, refresh_job_options,
27
+ )
28
+
29
+ global_config = get_driver().config
30
+ # 从 .env 读取配置,如果没有则提供默认值(分批阈值默认10,防抖默认86400秒)
31
+ ZHIHU_BATCH_SIZE = getattr(global_config, "zhihu_batch_size", 10)
32
+ ZHIHU_DEBOUNCE_TIME = getattr(global_config, "zhihu_debounce_time", 86400)
33
+ ZHIHU_CACHE_MAX_AGE = getattr(global_config, "zhihu_cache_max_age", 86400 * 7)
34
+ ZHIHU_MAX_IMAGES = getattr(global_config, "zhihu_max_images", 50)
35
+
36
+ cfg = PluginConfig()
37
+ refresh_monitor = RefreshMonitor(
38
+ cfg.data_dir / "zhihu_refresh_state.json",
39
+ refresh_alert_targets(global_config),
40
+ do_refresh_token,
41
+ lambda: [bot for bot in get_bots().values() if isinstance(bot, Bot)],
42
+ )
43
+ zhihu_parser = None
44
+ zhihu_renderer = None
45
+ # 将配置好的防抖时间传给防抖器
46
+ zhihu_debouncer = Debouncer(ttl_seconds=ZHIHU_DEBOUNCE_TIME)
47
+
48
+ __plugin_meta__ = PluginMetadata(
49
+ name="知乎解析",
50
+ description="解析知乎问题、回答、专栏文章与想法链接,生成预览卡片并转发图文内容",
51
+ usage="直接发送知乎链接即可自动解析;引用含知乎链接的消息回复「解析」可重新解析;管理员发送「知乎保活」可手动刷新登录信息",
52
+ type="application",
53
+ homepage="https://github.com/devil233-ui/nonebot-plugin-zhihu-parser",
54
+ supported_adapters={"~onebot.v11"},
55
+ )
56
+
57
+ @get_driver().on_startup
58
+ async def init_parser():
59
+ global zhihu_parser, zhihu_renderer
60
+ downloader = Downloader(cfg)
61
+ zhihu_parser = ZhihuParser(cfg, downloader)
62
+ Renderer.load_resources()
63
+ zhihu_renderer = Renderer(cfg)
64
+ max_age = int(ZHIHU_CACHE_MAX_AGE)
65
+ zhihu_cleaner = CacheCleaner(temp_dir=cfg.cache_dir, max_age_seconds=max_age)
66
+ zhihu_cleaner.start()
67
+
68
+ days = max_age / 86400
69
+ logger.info(f"知乎解析插件:{days:g}天缓存自动清理任务已挂载启动!")
70
+
71
+ scheduler.add_job(
72
+ auto_refresh_zhihu_cookie,
73
+ **refresh_job_options(
74
+ scheduler.timezone,
75
+ getattr(global_config, "zhihu_refresh_jitter_seconds", 900),
76
+ ),
77
+ )
78
+
79
+
80
+ async def auto_refresh_zhihu_cookie():
81
+ logger.info("[知乎保活] 开始自动保活")
82
+ result = await refresh_monitor.run(cfg)
83
+ if result.ok:
84
+ logger.info(format_refresh_result(result))
85
+ else:
86
+ logger.warning(format_refresh_result(result))
87
+
88
+
89
+ @get_driver().on_bot_connect
90
+ async def resume_refresh_alerts(bot):
91
+ if isinstance(bot, Bot):
92
+ await refresh_monitor.notify_pending()
93
+
94
+ def check_zhihu_url():
95
+ async def _check(bot: Bot, event: MessageEvent, state: T_State) -> bool:
96
+ raw_msg = str(event.get_message()).replace("\\/", "/")
97
+ pat = re.compile(r"(https?://(?:www\.)?zhihu\.com/[^\s\"\'\\]+|https?://zhuanlan\.zhihu\.com/[^\s\"\'\\]+)")
98
+
99
+ # 1. 正常情况:直接检测消息里有没有知乎链接
100
+ match = pat.search(raw_msg)
101
+ if match:
102
+ state['zhihu_match'] = match
103
+ state['zhihu_raw'] = raw_msg
104
+ return True
105
+
106
+ # 2. 引用解析情况:检查是不是发了“解析”并且带了引用(reply)
107
+ # 兼容这几个词:解析、提取、解析知乎
108
+ if raw_msg.strip() in ["解析", "提取", "解析知乎"] and event.reply:
109
+ # 提取被引用原消息(哪怕是小程序 JSON 卡片,这里也会变成字符串)
110
+ reply_msg = str(event.reply.message).replace("\\/", "/")
111
+ reply_match = pat.search(reply_msg)
112
+ if reply_match:
113
+ # 狸猫换太子:把引用里的链接和文本存进 state,骗过后面的程序
114
+ state['zhihu_match'] = reply_match
115
+ state['zhihu_raw'] = reply_msg
116
+ return True
117
+
118
+ return False
119
+ return Rule(_check)
120
+
121
+ zhihu_matcher = on_message(rule=check_zhihu_url(), priority=10, block=False)
122
+
123
+ # 【新增】手动强制换血指令,方便你随时补充存活期
124
+ refresh_cmd = on_command(
125
+ "刷新知乎ck", aliases={"知乎保活", "知乎刷新"},
126
+ permission=SUPERUSER, priority=5, block=True,
127
+ )
128
+
129
+ @refresh_cmd.handle()
130
+ async def force_refresh(bot: Bot, event: MessageEvent):
131
+ await refresh_cmd.send("正在刷新知乎登录信息,请稍候。")
132
+ result = await refresh_monitor.run(cfg)
133
+ await refresh_cmd.send(format_refresh_result(result))
134
+
135
+ @zhihu_matcher.handle()
136
+ async def handle_zhihu(bot: Bot, event: MessageEvent, state: T_State):
137
+ if not zhihu_parser or not zhihu_renderer:
138
+ logger.warning("知乎解析器仍在初始化中,请稍后再试")
139
+ return
140
+
141
+ await zhihu_matcher.send("识别到知乎链接,开始解析")
142
+
143
+ text = state['zhihu_raw']
144
+ base_match = state['zhihu_match']
145
+
146
+ # 原始提取的 URL
147
+ raw_url = base_match.group(0).replace("&amp;", "&")
148
+ # [新增清洗] 暴力剔除 URL 尾部可能因为引用而粘连的中文和特殊标点
149
+ import re
150
+ url = re.sub(r'[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef\]\})】”"。,!?]+$', '', raw_url)
151
+
152
+ text = text.replace("&amp;", "&")
153
+ session_id = event.get_session_id()
154
+
155
+ if zhihu_debouncer.hit_url(session_id, url):
156
+ logger.info(f"[{session_id}] 链接 {url} 在防抖时间内,已跳过重复解析")
157
+ await zhihu_matcher.send("24小时内有人水果了,端下去罢")
158
+ return
159
+
160
+ handlers = getattr(zhihu_parser, "_handlers", {})
161
+ final_keyword = None
162
+ final_match_obj = None
163
+
164
+ for kw, func in handlers.items():
165
+ pat = getattr(func, "__parser_pattern__", None)
166
+ if pat:
167
+ if isinstance(pat, str): pat = re.compile(pat)
168
+ match = pat.search(text)
169
+ if match:
170
+ final_keyword = kw
171
+ final_match_obj = match
172
+ break
173
+
174
+ if not final_match_obj:
175
+ KNOWN_PATTERNS = [
176
+ ("/answer/", re.compile(r"zhihu\.com/question/(?P<question_id>\d+)/answer/(?P<answer_id>\d+)")),
177
+ ("www.zhihu.com/question/", re.compile(r"zhihu\.com/question/(?P<question_id>\d+)")),
178
+ ("zhuanlan.zhihu.com/p/", re.compile(r"zhuanlan\.zhihu\.com/p/(?P<article_id>\d+)")),
179
+ ("www.zhihu.com/pin/", re.compile(r"zhihu\.com/pin/(?P<pin_id>\d+)")),
180
+ ]
181
+ for exact_kw, pat in KNOWN_PATTERNS:
182
+ match = pat.search(text)
183
+ if match and exact_kw in handlers:
184
+ final_keyword = exact_kw
185
+ final_match_obj = match
186
+ break
187
+
188
+ if not final_keyword or not final_match_obj:
189
+ return
190
+
191
+ logger.warning(f"🔍 [X光机] 开始解析!命中规则: {final_keyword}, 提取参数: {final_match_obj.group(0)}")
192
+
193
+ try:
194
+ parse_res = await zhihu_parser.parse(final_keyword, final_match_obj)
195
+ if not parse_res: return
196
+
197
+ # A. 发送预览卡片
198
+ card_path = await zhihu_renderer.render_card(parse_res)
199
+ if card_path and card_path.exists():
200
+ await zhihu_matcher.send(MessageSegment.image(card_path))
201
+
202
+ # B. 分批次合并转发逻辑
203
+ bot_id = int(bot.self_id)
204
+ img_counter = 0
205
+ batch_num = 1
206
+ current_combined_msg = Message()
207
+ total_img_counter = 0
208
+ limit_reached = False
209
+
210
+ # 初始添加标题
211
+ if getattr(parse_res, "title", None):
212
+ current_combined_msg += MessageSegment.text(f"【{parse_res.title}】\n\n")
213
+
214
+ async def send_current_batch(msg: Message, is_last: bool = False):
215
+ if not msg: return
216
+ prefix = f"(第{batch_num}部分) " if not (batch_num == 1 and is_last) else ""
217
+ node = [MessageSegment.node_custom(user_id=bot_id, nickname=f"知乎解析 {prefix}", content=msg)]
218
+ try:
219
+ if isinstance(event, GroupMessageEvent):
220
+ await bot.call_api("send_group_forward_msg", group_id=event.group_id, messages=node)
221
+ else:
222
+ await bot.call_api("send_private_forward_msg", user_id=event.user_id, messages=node)
223
+ except Exception as e:
224
+ logger.error(f"发送第 {batch_num} 部分转发失败: {e}")
225
+
226
+ # [恢复] 降级单发机制:因为现在有总量限制,可以放心拆包单发了
227
+ await zhihu_matcher.send(f"⚠️ 第 {batch_num} 部分触发风控拦截,正在自动拆分单独发送...")
228
+
229
+ # [新增] 专门处理长文本折叠的内部助手函数
230
+ async def safe_send_text(text_msg: Message):
231
+ t_str = str(text_msg).strip()
232
+ if not t_str: return
233
+
234
+ # 纯文字装进合并转发里“折叠”起来
235
+ if len(t_str):
236
+ t_node = [MessageSegment.node_custom(user_id=bot_id, nickname="风控部分纯文字折叠", content=Message(t_str))]
237
+ try:
238
+ if isinstance(event, GroupMessageEvent):
239
+ await bot.call_api("send_group_forward_msg", group_id=event.group_id, messages=t_node)
240
+ else:
241
+ await bot.call_api("send_private_forward_msg", user_id=event.user_id, messages=t_node)
242
+ except Exception:
243
+ # 终极兜底:万一纯文本转发都失败,截断只发前500字
244
+ await zhihu_matcher.send(t_str[:500] + "\n...[字数过多已截断]")
245
+ else:
246
+ # 不满 150 字的短句(比如图注、小标题)直接发,不影响阅读连贯性
247
+ await zhihu_matcher.send(text_msg)
248
+
249
+ fallback_msg = Message()
250
+ for seg in msg:
251
+ if seg.type == 'text':
252
+ fallback_msg += seg
253
+ else:
254
+ # 先发送之前攒着的文字(带折叠判定)
255
+ try: await safe_send_text(fallback_msg)
256
+ except Exception: pass
257
+ fallback_msg = Message()
258
+
259
+ # 单发媒体
260
+ try: await zhihu_matcher.send(Message(seg))
261
+ except Exception as sub_e:
262
+ logger.error(f"单发媒体失败(严重风控死锁): {sub_e}")
263
+ await zhihu_matcher.send("🚫 [一张图片/视频因触发腾讯极其严格的审核被无情抹杀]")
264
+
265
+ # 收尾剩余文字
266
+ try: await safe_send_text(fallback_msg)
267
+ except Exception: pass
268
+
269
+ # 遍历正文内容
270
+ if hasattr(parse_res, "contents") and parse_res.contents:
271
+ for content in parse_res.contents:
272
+ # 1. 处理文字
273
+ if hasattr(content, "text") and content.text:
274
+ current_combined_msg += MessageSegment.text(str(content.text) + "\n")
275
+
276
+ # 2. 处理图片/媒体(增加精准异常捕获)
277
+ if hasattr(content, "get_path"):
278
+ try:
279
+ file_path = await content.get_path()
280
+ if file_path and getattr(file_path, "exists", lambda: False)():
281
+ # 换完后上一行是:
282
+ ext = file_path.suffix.lower()
283
+ if ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]:
284
+ current_combined_msg += MessageSegment.image(file_path)
285
+ current_combined_msg += MessageSegment.text("\n")
286
+ img_counter += 1
287
+ total_img_counter += 1 # [新增] 累加总数
288
+ elif ext in [".mp4", ".mov", ".avi"]:
289
+ current_combined_msg += MessageSegment.video(file_path)
290
+ current_combined_msg += MessageSegment.text("\n")
291
+ img_counter += 2
292
+ total_img_counter += 2 # [新增] 累加总数
293
+ except RuntimeError:
294
+ pass
295
+ except Exception as e:
296
+ logger.debug(f"读取媒体失败: {e}")
297
+
298
+ # [修改] 达到单批次阈值 或 达到总量限制 时触发发包
299
+ if img_counter >= ZHIHU_BATCH_SIZE or total_img_counter >= ZHIHU_MAX_IMAGES:
300
+ await send_current_batch(current_combined_msg)
301
+ current_combined_msg = Message()
302
+ img_counter = 0
303
+ batch_num += 1
304
+
305
+ # [新增] 总量阻断:超过设定总量直接掐断循环
306
+ if total_img_counter >= ZHIHU_MAX_IMAGES:
307
+ limit_reached = True
308
+ break
309
+
310
+ # [修改] 如果没有被提前阻断,才发送收尾段落
311
+ if current_combined_msg and not limit_reached:
312
+ await send_current_batch(current_combined_msg, is_last=True)
313
+
314
+ # [新增] 触发了截断则发送文案提示
315
+ if limit_reached:
316
+ await zhihu_matcher.send("不刷屏了,剩下的请点击原链接跳转阅读")
317
+
318
+ except ParseException as e:
319
+ err_msg = str(e)
320
+ logger.warning(f"知乎抓取被阻断: {err_msg}")
321
+ if e.__cause__:
322
+ logger.error(f"💥 [X光机] 底层致命报错: {e.__cause__}")
323
+
324
+ # 触发回滚!把毒数据从防抖池踢出去
325
+ zhihu_debouncer.rollback_url(session_id, url)
326
+
327
+ await zhihu_matcher.send(f"⚠️ 解析失败: {err_msg}")
328
+
329
+ except Exception as e:
330
+ logger.exception("解析知乎链接时发生严重错误")
331
+ zhihu_debouncer.rollback_url(session_id, url)