nonebot-plugin-htmlrender 0.6.7__tar.gz → 0.7.0a1__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.
- nonebot_plugin_htmlrender-0.7.0a1/.github/workflows/_test-os-arch.yml +67 -0
- nonebot_plugin_htmlrender-0.7.0a1/.github/workflows/codecov.yml +20 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/.pre-commit-config.yaml +5 -5
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/PKG-INFO +3 -1
- nonebot_plugin_htmlrender-0.7.0a1/nonebot_plugin_htmlrender/__init__.py +64 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/browser.py +128 -42
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/config.py +33 -2
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/data_source.py +5 -5
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/install.py +32 -23
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/process.py +39 -11
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/utils.py +62 -1
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/pyproject.toml +3 -1
- nonebot_plugin_htmlrender-0.7.0a1/tests/conftest.py +42 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/test_browser.py +77 -23
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/test_htmlrender.py +5 -4
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/test_process.py +17 -30
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/uv.lock +164 -127
- nonebot_plugin_htmlrender-0.6.7/.github/workflows/codecov.yml +0 -43
- nonebot_plugin_htmlrender-0.6.7/nonebot_plugin_htmlrender/__init__.py +0 -55
- nonebot_plugin_htmlrender-0.6.7/tests/conftest.py +0 -15
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/.dockerignore +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/.github/workflows/docker_image.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/.github/workflows/publish.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/.gitignore +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/.python-version +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/LICENSE +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/README.md +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/docker-compose.yaml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/dockerfile +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/docs/example.md +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/docs/html2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/docs/md2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/docs/template2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/docs/text2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/entrypoint.sh +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/docker-compose.yaml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/entrypoint.sh +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/plugins/render/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/plugins/render/html2pic.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/plugins/render/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/plugins/render/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/plugins/render/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/plugins/render/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/plugins/render/utils.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/example/pyproject.toml +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/consts.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/signal.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/templates/github-markdown-light.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/templates/katex/katex.min.b64_fonts.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/templates/katex/katex.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/templates/katex/mathtex-script-type.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/templates/katex/mhchem.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/templates/markdown.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/templates/pygments-default.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/templates/text.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/nonebot_plugin_htmlrender/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/resources/test_template_filter.png +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/test_deprecated_decorator.py +0 -0
- {nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/tests/test_install.py +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
on:
|
|
2
|
+
workflow_call:
|
|
3
|
+
inputs:
|
|
4
|
+
python-version:
|
|
5
|
+
required: true
|
|
6
|
+
type: string
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Test (Py ${{ inputs.python-version }} — ${{ matrix.os_label }})
|
|
11
|
+
runs-on: ${{ matrix.runs_on }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: true
|
|
14
|
+
matrix:
|
|
15
|
+
include:
|
|
16
|
+
- runs_on: ubuntu-latest
|
|
17
|
+
os_label: ubuntu-latest x64
|
|
18
|
+
arch: x64
|
|
19
|
+
platform: linux
|
|
20
|
+
|
|
21
|
+
- runs_on: macos-latest
|
|
22
|
+
os_label: macos-latest arm64
|
|
23
|
+
arch: arm64
|
|
24
|
+
platform: macos
|
|
25
|
+
|
|
26
|
+
- runs_on: windows-latest
|
|
27
|
+
os_label: windows-latest x64
|
|
28
|
+
arch: x64
|
|
29
|
+
platform: windows
|
|
30
|
+
|
|
31
|
+
- runs_on: ubuntu-24.04-arm
|
|
32
|
+
os_label: ubuntu-24.04-arm arm64
|
|
33
|
+
arch: arm64
|
|
34
|
+
platform: linux
|
|
35
|
+
|
|
36
|
+
env:
|
|
37
|
+
OS: ${{ matrix.os_label }}
|
|
38
|
+
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
39
|
+
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
|
|
43
|
+
- name: Set up uv
|
|
44
|
+
uses: astral-sh/setup-uv@v6
|
|
45
|
+
with:
|
|
46
|
+
python-version: ${{ inputs.python-version }}
|
|
47
|
+
enable-cache: true
|
|
48
|
+
cache-suffix: ${{ matrix.runs_on }}-${{ inputs.python-version }}
|
|
49
|
+
|
|
50
|
+
- name: Sync deps
|
|
51
|
+
run: uv sync --locked --all-extras --dev
|
|
52
|
+
|
|
53
|
+
- name: Install Playwright (Linux)
|
|
54
|
+
if: matrix.platform == 'linux'
|
|
55
|
+
run: uv run playwright install --with-deps && uv run playwright install --with-deps
|
|
56
|
+
- name: Install Playwright (non-Linux)
|
|
57
|
+
if: matrix.platform != 'linux'
|
|
58
|
+
run: uv run playwright install && uv run playwright install
|
|
59
|
+
|
|
60
|
+
- name: Run tests
|
|
61
|
+
run: uv run pytest -s -n auto
|
|
62
|
+
|
|
63
|
+
- name: Upload coverage to Codecov
|
|
64
|
+
uses: codecov/codecov-action@v5
|
|
65
|
+
with:
|
|
66
|
+
env_vars: OS,PYTHON_VERSION
|
|
67
|
+
verbose: true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Code Coverage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ master, dev , feat/*]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ master ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
per-python:
|
|
11
|
+
name: Python ${{ matrix.python-version }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
|
|
16
|
+
|
|
17
|
+
uses: ./.github/workflows/_test-os-arch.yml
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
secrets: inherit
|
{nonebot_plugin_htmlrender-0.6.7 → nonebot_plugin_htmlrender-0.7.0a1}/.pre-commit-config.yaml
RENAMED
|
@@ -7,10 +7,10 @@ ci:
|
|
|
7
7
|
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
|
8
8
|
repos:
|
|
9
9
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
10
|
-
rev: v0.
|
|
10
|
+
rev: v0.12.12
|
|
11
11
|
hooks:
|
|
12
|
-
- id: ruff
|
|
13
|
-
args: [--fix]
|
|
14
|
-
stages: [pre-commit]
|
|
15
12
|
- id: ruff-format
|
|
16
|
-
|
|
13
|
+
- id: ruff-check
|
|
14
|
+
args: [ --fix, --exit-non-zero-on-fix ]
|
|
15
|
+
types_or: [ python, pyi ]
|
|
16
|
+
require_serial: true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-htmlrender
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0a1
|
|
4
4
|
Summary: 通过浏览器渲染图片
|
|
5
5
|
Project-URL: Homepage, https://github.com/kexue-z/nonebot-plugin-htmlrender
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/kexue-z/nonebot-plugin-htmlrender/issues
|
|
@@ -31,11 +31,13 @@ Requires-Python: <4.0,>=3.9
|
|
|
31
31
|
Requires-Dist: aiofiles>=0.8.0
|
|
32
32
|
Requires-Dist: jinja2>=3.0.3
|
|
33
33
|
Requires-Dist: markdown>=3.3.6
|
|
34
|
+
Requires-Dist: nonebot-plugin-localstore>=0.7.4
|
|
34
35
|
Requires-Dist: nonebot2>=2.4.2
|
|
35
36
|
Requires-Dist: playwright>=1.48.0
|
|
36
37
|
Requires-Dist: pygments>=2.10.0
|
|
37
38
|
Requires-Dist: pymdown-extensions>=9.1
|
|
38
39
|
Requires-Dist: python-markdown-math>=0.8
|
|
40
|
+
Requires-Dist: tenacity>=9.1.2
|
|
39
41
|
Description-Content-Type: text/markdown
|
|
40
42
|
|
|
41
43
|
# nonebot-plugin-htmlrender
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import nonebot
|
|
2
|
+
from nonebot import require
|
|
3
|
+
|
|
4
|
+
require("nonebot_plugin_localstore")
|
|
5
|
+
from nonebot.log import logger
|
|
6
|
+
from nonebot.plugin import PluginMetadata
|
|
7
|
+
|
|
8
|
+
from nonebot_plugin_htmlrender.browser import (
|
|
9
|
+
get_new_page,
|
|
10
|
+
shutdown_htmlrender,
|
|
11
|
+
startup_htmlrender,
|
|
12
|
+
)
|
|
13
|
+
from nonebot_plugin_htmlrender.config import Config, plugin_config
|
|
14
|
+
from nonebot_plugin_htmlrender.data_source import (
|
|
15
|
+
capture_element,
|
|
16
|
+
html_to_pic,
|
|
17
|
+
md_to_pic,
|
|
18
|
+
template_to_html,
|
|
19
|
+
template_to_pic,
|
|
20
|
+
text_to_pic,
|
|
21
|
+
)
|
|
22
|
+
from nonebot_plugin_htmlrender.utils import _clear_playwright_env_vars
|
|
23
|
+
|
|
24
|
+
__plugin_meta__ = PluginMetadata(
|
|
25
|
+
name="nonebot-plugin-htmlrender",
|
|
26
|
+
description="通过浏览器渲染图片",
|
|
27
|
+
usage="",
|
|
28
|
+
type="library",
|
|
29
|
+
config=Config,
|
|
30
|
+
homepage="https://github.com/kexue-z/nonebot-plugin-htmlrender",
|
|
31
|
+
extra={},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
driver = nonebot.get_driver()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@driver.on_startup
|
|
38
|
+
async def init(**kwargs):
|
|
39
|
+
logger.info("HTMLRender Starting...")
|
|
40
|
+
await startup_htmlrender(**kwargs)
|
|
41
|
+
logger.opt(colors=True).info(
|
|
42
|
+
f"HTMLRender Started with <cyan>{plugin_config.htmlrender_browser}</cyan>."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@driver.on_shutdown
|
|
47
|
+
async def shutdown():
|
|
48
|
+
logger.info("HTMLRender Shutting down...")
|
|
49
|
+
await shutdown_htmlrender()
|
|
50
|
+
_clear_playwright_env_vars()
|
|
51
|
+
logger.info("HTMLRender Shut down.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"capture_element",
|
|
56
|
+
"get_new_page",
|
|
57
|
+
"html_to_pic",
|
|
58
|
+
"md_to_pic",
|
|
59
|
+
"shutdown_htmlrender",
|
|
60
|
+
"startup_htmlrender",
|
|
61
|
+
"template_to_html",
|
|
62
|
+
"template_to_pic",
|
|
63
|
+
"text_to_pic",
|
|
64
|
+
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections.abc import AsyncIterator
|
|
2
|
-
from contextlib import asynccontextmanager
|
|
2
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
5
|
from nonebot.log import logger
|
|
@@ -10,10 +10,17 @@ from playwright.async_api import (
|
|
|
10
10
|
Playwright,
|
|
11
11
|
async_playwright,
|
|
12
12
|
)
|
|
13
|
+
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
|
13
14
|
|
|
14
15
|
from nonebot_plugin_htmlrender.config import plugin_config
|
|
15
16
|
from nonebot_plugin_htmlrender.install import install_browser
|
|
16
|
-
from nonebot_plugin_htmlrender.utils import
|
|
17
|
+
from nonebot_plugin_htmlrender.utils import (
|
|
18
|
+
_prepare_playwright_env_vars,
|
|
19
|
+
clean_playwright_cache,
|
|
20
|
+
proxy_settings,
|
|
21
|
+
suppress_and_log,
|
|
22
|
+
with_lock,
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
_browser: Optional[Browser] = None
|
|
19
26
|
_playwright: Optional[Playwright] = None
|
|
@@ -40,22 +47,6 @@ async def _launch(browser_type: str, **kwargs) -> Browser:
|
|
|
40
47
|
return await _browser_cls.launch(**kwargs)
|
|
41
48
|
|
|
42
49
|
|
|
43
|
-
async def init_browser(**kwargs) -> Browser:
|
|
44
|
-
"""
|
|
45
|
-
初始化浏览器实例。
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
**kwargs: 传递给`playwright.launch`的关键字参数。
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Browser: 浏览器实例。
|
|
52
|
-
|
|
53
|
-
Raises:
|
|
54
|
-
RuntimeError: 如果浏览器无法启动或安装失败。
|
|
55
|
-
"""
|
|
56
|
-
return await start_browser(**kwargs)
|
|
57
|
-
|
|
58
|
-
|
|
59
50
|
@asynccontextmanager
|
|
60
51
|
async def get_new_page(device_scale_factor: float = 2, **kwargs) -> AsyncIterator[Page]:
|
|
61
52
|
"""
|
|
@@ -88,7 +79,7 @@ async def get_browser(**kwargs) -> Browser:
|
|
|
88
79
|
if _browser and _browser.is_connected():
|
|
89
80
|
return _browser
|
|
90
81
|
|
|
91
|
-
return await
|
|
82
|
+
return await startup_htmlrender(**kwargs)
|
|
92
83
|
|
|
93
84
|
|
|
94
85
|
async def _connect_via_cdp(**kwargs) -> Browser:
|
|
@@ -106,12 +97,12 @@ async def _connect_via_cdp(**kwargs) -> Browser:
|
|
|
106
97
|
"""
|
|
107
98
|
kwargs["endpoint_url"] = plugin_config.htmlrender_connect_over_cdp
|
|
108
99
|
logger.info(
|
|
109
|
-
f"
|
|
100
|
+
f"Connecting to Chromium via CDP ({plugin_config.htmlrender_connect_over_cdp})"
|
|
110
101
|
)
|
|
111
102
|
if _playwright is not None:
|
|
112
103
|
return await _playwright.chromium.connect_over_cdp(**kwargs)
|
|
113
104
|
else:
|
|
114
|
-
raise RuntimeError("Playwright
|
|
105
|
+
raise RuntimeError("Playwright is not initialized")
|
|
115
106
|
|
|
116
107
|
|
|
117
108
|
async def _connect(browser_type: str, **kwargs) -> Browser:
|
|
@@ -131,16 +122,40 @@ async def _connect(browser_type: str, **kwargs) -> Browser:
|
|
|
131
122
|
_browser_cls: BrowserType = getattr(_playwright, browser_type)
|
|
132
123
|
kwargs["ws_endpoint"] = plugin_config.htmlrender_connect
|
|
133
124
|
logger.info(
|
|
134
|
-
f"
|
|
125
|
+
f"Connecting to {browser_type.capitalize()} via "
|
|
126
|
+
f"WebSocket endpoint: {plugin_config.htmlrender_connect}"
|
|
135
127
|
)
|
|
136
128
|
if _playwright is not None:
|
|
137
129
|
return await _browser_cls.connect(**kwargs)
|
|
138
130
|
else:
|
|
139
|
-
raise RuntimeError("Playwright
|
|
131
|
+
raise RuntimeError("Playwright is not initialized")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@retry(
|
|
135
|
+
retry=retry_if_exception_type(RuntimeError),
|
|
136
|
+
stop=stop_after_attempt(4),
|
|
137
|
+
wait=wait_fixed(1),
|
|
138
|
+
reraise=True,
|
|
139
|
+
before_sleep=lambda retry_state: logger.warning(
|
|
140
|
+
f"Attempt {retry_state.attempt_number} failed, retrying..."
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
async def _check_env_with_install_retry(**kwargs):
|
|
144
|
+
try:
|
|
145
|
+
return await check_playwright_env(**kwargs)
|
|
146
|
+
except RuntimeError:
|
|
147
|
+
if plugin_config.htmlrender_ci_mode:
|
|
148
|
+
raise
|
|
149
|
+
try:
|
|
150
|
+
await install_browser()
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.error(f"Browser installation failed: {e!s}")
|
|
153
|
+
raise RuntimeError(f"install_browser failed: {e}") from e
|
|
154
|
+
raise
|
|
140
155
|
|
|
141
156
|
|
|
142
157
|
@with_lock
|
|
143
|
-
async def
|
|
158
|
+
async def startup_htmlrender(**kwargs) -> Browser:
|
|
144
159
|
"""
|
|
145
160
|
启动 Playwright 浏览器实例。
|
|
146
161
|
|
|
@@ -152,8 +167,14 @@ async def start_browser(**kwargs) -> Browser:
|
|
|
152
167
|
"""
|
|
153
168
|
global _browser, _playwright
|
|
154
169
|
|
|
155
|
-
await
|
|
170
|
+
await shutdown_htmlrender()
|
|
171
|
+
|
|
172
|
+
if not plugin_config.htmlrender_ci_mode:
|
|
173
|
+
clean_playwright_cache()
|
|
174
|
+
_prepare_playwright_env_vars()
|
|
175
|
+
|
|
156
176
|
_playwright = await async_playwright().start()
|
|
177
|
+
logger.debug("Playwright started")
|
|
157
178
|
|
|
158
179
|
if (
|
|
159
180
|
plugin_config.htmlrender_browser == "chromium"
|
|
@@ -174,35 +195,100 @@ async def start_browser(**kwargs) -> Browser:
|
|
|
174
195
|
|
|
175
196
|
if plugin_config.htmlrender_browser_executable_path:
|
|
176
197
|
kwargs["executable_path"] = plugin_config.htmlrender_browser_executable_path
|
|
177
|
-
else:
|
|
178
198
|
try:
|
|
179
|
-
await
|
|
180
|
-
except
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
199
|
+
_browser = await _launch(plugin_config.htmlrender_browser, **kwargs)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
raise RuntimeError(
|
|
202
|
+
f"Failed to launch browser with executable path"
|
|
203
|
+
f" '{plugin_config.htmlrender_browser_executable_path}': {e}"
|
|
204
|
+
) from e
|
|
205
|
+
else:
|
|
206
|
+
_browser = await _check_env_with_install_retry(**kwargs)
|
|
185
207
|
|
|
186
208
|
return _browser
|
|
187
209
|
|
|
188
210
|
|
|
189
|
-
async def
|
|
190
|
-
|
|
191
|
-
|
|
211
|
+
async def shutdown_htmlrender() -> None:
|
|
212
|
+
is_remote = bool(
|
|
213
|
+
plugin_config.htmlrender_connect or plugin_config.htmlrender_connect_over_cdp
|
|
214
|
+
)
|
|
215
|
+
async with AsyncExitStack() as stack:
|
|
216
|
+
await _schedule_browser_shutdown(stack, is_remote=is_remote)
|
|
217
|
+
await _schedule_playwright_shutdown(stack)
|
|
218
|
+
_clear_globals()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def _schedule_browser_shutdown(stack: AsyncExitStack, *, is_remote: bool) -> None:
|
|
222
|
+
global _browser
|
|
223
|
+
if not _browser:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
should_close = (not is_remote) and plugin_config.htmlrender_shutdown_browser_on_exit
|
|
227
|
+
if not should_close:
|
|
228
|
+
logger.info(
|
|
229
|
+
"Skipping browser shutdown due to configuration or remote connection."
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
browser = _browser
|
|
234
|
+
if not browser.is_connected():
|
|
235
|
+
logger.info("Browser was already disconnected.")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
logger.debug("Disconnecting browser...")
|
|
239
|
+
|
|
240
|
+
async def _close_browser():
|
|
192
241
|
with suppress_and_log():
|
|
193
|
-
await
|
|
194
|
-
|
|
242
|
+
await browser.close()
|
|
243
|
+
logger.info("Disconnected browser.")
|
|
244
|
+
|
|
245
|
+
stack.push_async_callback(_close_browser)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def _schedule_playwright_shutdown(stack: AsyncExitStack) -> None:
|
|
249
|
+
global _playwright
|
|
250
|
+
if not _playwright:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
pw = _playwright
|
|
254
|
+
logger.debug("Stopping Playwright...")
|
|
255
|
+
|
|
256
|
+
async def _stop_pw():
|
|
195
257
|
with suppress_and_log():
|
|
196
|
-
await
|
|
258
|
+
await pw.stop()
|
|
259
|
+
logger.info("Playwright stopped.")
|
|
260
|
+
|
|
261
|
+
stack.push_async_callback(_stop_pw)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _clear_globals() -> None:
|
|
265
|
+
global _browser, _playwright
|
|
266
|
+
_browser = None
|
|
267
|
+
_playwright = None
|
|
197
268
|
|
|
198
269
|
|
|
199
|
-
async def check_playwright_env(**kwargs):
|
|
200
|
-
"""
|
|
270
|
+
async def check_playwright_env(**kwargs) -> Browser:
|
|
271
|
+
"""
|
|
272
|
+
检查Playwright环境,复用_launch方法避免逻辑重复。
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
**kwargs: 传递给`playwright.launch`的关键字参数。
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
RuntimeError: 如果Playwright环境设置不正确。
|
|
279
|
+
"""
|
|
201
280
|
logger.info("Checking Playwright environment...")
|
|
281
|
+
global _browser, _playwright
|
|
282
|
+
|
|
202
283
|
try:
|
|
203
|
-
|
|
204
|
-
|
|
284
|
+
_playwright = await async_playwright().start()
|
|
285
|
+
_browser = await _launch(plugin_config.htmlrender_browser, **kwargs)
|
|
286
|
+
logger.success("Playwright environment is set up correctly.")
|
|
287
|
+
return _browser
|
|
288
|
+
|
|
205
289
|
except Exception as e:
|
|
290
|
+
await shutdown_htmlrender()
|
|
291
|
+
|
|
206
292
|
raise RuntimeError(
|
|
207
293
|
"Playwright environment is not set up correctly. "
|
|
208
294
|
"Refer to https://playwright.dev/python/docs/intro#system-requirements"
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
from pathlib import Path
|
|
1
2
|
from typing import Any, Optional
|
|
2
3
|
|
|
3
4
|
from nonebot import get_driver, get_plugin_config
|
|
4
5
|
from nonebot.compat import model_validator
|
|
6
|
+
from nonebot.log import logger
|
|
7
|
+
import nonebot_plugin_localstore as store
|
|
5
8
|
from pydantic import BaseModel, Field
|
|
6
9
|
|
|
7
10
|
from nonebot_plugin_htmlrender.consts import BROWSER_CHANNEL_TYPES, BROWSER_ENGINE_TYPES
|
|
8
11
|
|
|
12
|
+
plugin_cache_dir: Path = store.get_plugin_cache_dir()
|
|
13
|
+
plugin_config_dir: Path = store.get_plugin_config_dir()
|
|
14
|
+
plugin_data_dir: Path = store.get_plugin_data_dir()
|
|
15
|
+
|
|
9
16
|
|
|
10
17
|
class Config(BaseModel):
|
|
11
18
|
"""插件配置类。"""
|
|
@@ -14,6 +21,26 @@ class Config(BaseModel):
|
|
|
14
21
|
default="chromium",
|
|
15
22
|
description="Playwright浏览器引擎类型,默认值为 'chromium'。",
|
|
16
23
|
)
|
|
24
|
+
htmlrender_storage_path: Path = Field(
|
|
25
|
+
default=plugin_data_dir,
|
|
26
|
+
description="存储路径,不填则使用 `nonebot-plugin-localstore` 管理",
|
|
27
|
+
)
|
|
28
|
+
htmlrender_cache_path: Path = Field(
|
|
29
|
+
default=plugin_cache_dir,
|
|
30
|
+
description="缓存路径,不填则使用 `nonebot-plugin-localstore` 管理",
|
|
31
|
+
)
|
|
32
|
+
htmlrender_config_path: Path = Field(
|
|
33
|
+
default=plugin_config_dir,
|
|
34
|
+
description="配置路径,不填则使用 `nonebot-plugin-localstore` 管理",
|
|
35
|
+
)
|
|
36
|
+
htmlrender_shutdown_browser_on_exit: bool = Field(
|
|
37
|
+
default=True,
|
|
38
|
+
description="在插件关闭时关闭浏览器实例,在连接到远程浏览器时默认为 False。",
|
|
39
|
+
)
|
|
40
|
+
htmlrender_ci_mode: bool = Field(
|
|
41
|
+
default=False,
|
|
42
|
+
description="启用CI模式,跳过浏览器安装和环境变量",
|
|
43
|
+
)
|
|
17
44
|
htmlrender_download_host: Optional[str] = Field(
|
|
18
45
|
default=None, description="下载Playwright浏览器时的主机地址。"
|
|
19
46
|
)
|
|
@@ -29,7 +56,7 @@ class Config(BaseModel):
|
|
|
29
56
|
htmlrender_browser_channel: Optional[str] = Field(
|
|
30
57
|
default=None, description="Playwright浏览器通道类型。"
|
|
31
58
|
)
|
|
32
|
-
htmlrender_browser_executable_path: Optional[
|
|
59
|
+
htmlrender_browser_executable_path: Optional[Path] = Field(
|
|
33
60
|
default=None, description="Playwright浏览器可执行文件的路径。"
|
|
34
61
|
)
|
|
35
62
|
htmlrender_connect_over_cdp: Optional[str] = Field(
|
|
@@ -38,7 +65,6 @@ class Config(BaseModel):
|
|
|
38
65
|
htmlrender_connect: Optional[str] = Field(
|
|
39
66
|
default=None, description="通过Playwright协议连接Playwright浏览器的端点地址。"
|
|
40
67
|
)
|
|
41
|
-
|
|
42
68
|
htmlrender_browser_args: Optional[str] = Field(
|
|
43
69
|
default=None, description="Playwright 浏览器启动参数。"
|
|
44
70
|
)
|
|
@@ -76,3 +102,8 @@ class Config(BaseModel):
|
|
|
76
102
|
|
|
77
103
|
global_config = get_driver().config
|
|
78
104
|
plugin_config = get_plugin_config(Config)
|
|
105
|
+
|
|
106
|
+
if plugin_config.htmlrender_ci_mode:
|
|
107
|
+
logger.info(
|
|
108
|
+
"CI mode enabled, skipping browser installation and environment variable setup."
|
|
109
|
+
)
|
|
@@ -87,7 +87,7 @@ async def md_to_pic(
|
|
|
87
87
|
if md_path:
|
|
88
88
|
md = await read_file(md_path)
|
|
89
89
|
else:
|
|
90
|
-
raise Exception("
|
|
90
|
+
raise Exception("md or md_path must be provided")
|
|
91
91
|
logger.debug(md)
|
|
92
92
|
md = markdown.markdown(
|
|
93
93
|
md,
|
|
@@ -174,7 +174,7 @@ async def template_to_html(
|
|
|
174
174
|
if filters:
|
|
175
175
|
for filter_name, filter_func in filters.items():
|
|
176
176
|
template_env.filters[filter_name] = filter_func
|
|
177
|
-
logger.debug(f"
|
|
177
|
+
logger.debug(f"Custom filter loaded: {filter_name}")
|
|
178
178
|
|
|
179
179
|
template = template_env.get_template(template_name)
|
|
180
180
|
|
|
@@ -209,9 +209,9 @@ async def html_to_pic(
|
|
|
209
209
|
"""
|
|
210
210
|
# logger.debug(f"html:\n{html}")
|
|
211
211
|
if "file:" not in template_path:
|
|
212
|
-
raise Exception("template_path
|
|
212
|
+
raise Exception("template_path should be file:///path/to/template")
|
|
213
213
|
async with get_new_page(device_scale_factor, **kwargs) as page:
|
|
214
|
-
page.on("console", lambda msg: logger.debug(f"
|
|
214
|
+
page.on("console", lambda msg: logger.debug(f"[Browser Console]: {msg.text}"))
|
|
215
215
|
await page.goto(template_path)
|
|
216
216
|
await page.set_content(html, wait_until="networkidle")
|
|
217
217
|
await page.wait_for_timeout(wait)
|
|
@@ -266,7 +266,7 @@ async def template_to_pic(
|
|
|
266
266
|
if filters:
|
|
267
267
|
for filter_name, filter_func in filters.items():
|
|
268
268
|
template_env.filters[filter_name] = filter_func
|
|
269
|
-
logger.debug(f"
|
|
269
|
+
logger.debug(f"Custom filter loaded: {filter_name}")
|
|
270
270
|
|
|
271
271
|
template = template_env.get_template(template_name)
|
|
272
272
|
|
|
@@ -86,7 +86,10 @@ async def download_context() -> AsyncIterator[None]:
|
|
|
86
86
|
try:
|
|
87
87
|
best_mirror = await check_mirror_connectivity()
|
|
88
88
|
if best_mirror is not None:
|
|
89
|
-
logger.
|
|
89
|
+
logger.opt(colors=True).info(
|
|
90
|
+
f"Using Mirror source: <cyan>{best_mirror.name}</cyan> "
|
|
91
|
+
f"{best_mirror.url}"
|
|
92
|
+
)
|
|
90
93
|
os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = best_mirror.url
|
|
91
94
|
else:
|
|
92
95
|
logger.info("Mirror source not available, using default")
|
|
@@ -125,30 +128,25 @@ async def read_stream(
|
|
|
125
128
|
if stream is None:
|
|
126
129
|
return ""
|
|
127
130
|
|
|
128
|
-
last_progress = "" # 上一次显示的进度
|
|
129
131
|
output = [] # 存储读取到的文本内容
|
|
130
132
|
|
|
131
133
|
while True:
|
|
132
134
|
try:
|
|
133
|
-
|
|
134
|
-
if not
|
|
135
|
+
line_data = await stream.readline()
|
|
136
|
+
if not line_data:
|
|
135
137
|
break
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
continue
|
|
139
|
+
line = line_data.decode().strip()
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
if callback:
|
|
142
|
+
try:
|
|
143
|
+
await callback(line)
|
|
144
|
+
except UnicodeEncodeError:
|
|
145
|
+
safe_text = "".join(c if ord(c) < 128 else "?" for c in line)
|
|
146
|
+
await callback(safe_text)
|
|
142
147
|
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
if callback:
|
|
146
|
-
await callback(f"Progress: {text}")
|
|
147
|
-
last_progress = text
|
|
148
|
-
else:
|
|
149
|
-
if callback:
|
|
150
|
-
await callback(text)
|
|
151
|
-
output.append(text)
|
|
148
|
+
if line:
|
|
149
|
+
output.append(line)
|
|
152
150
|
|
|
153
151
|
except asyncio.IncompleteReadError:
|
|
154
152
|
break
|
|
@@ -171,6 +169,7 @@ async def execute_install_command(timeout: int) -> tuple[bool, str]:
|
|
|
171
169
|
try:
|
|
172
170
|
logger.debug("Starting playwright install process...")
|
|
173
171
|
install_signal_handler()
|
|
172
|
+
|
|
174
173
|
process = await create_process(
|
|
175
174
|
"playwright",
|
|
176
175
|
"install",
|
|
@@ -179,13 +178,21 @@ async def execute_install_command(timeout: int) -> tuple[bool, str]:
|
|
|
179
178
|
stdout=asyncio.subprocess.PIPE,
|
|
180
179
|
stderr=asyncio.subprocess.PIPE,
|
|
181
180
|
)
|
|
182
|
-
logger.debug("Process created successfully")
|
|
183
181
|
|
|
184
182
|
async def stdout_callback(line: str) -> None:
|
|
185
|
-
|
|
183
|
+
line_stripped = line.strip()
|
|
184
|
+
if (
|
|
185
|
+
not line_stripped.startswith("Progress:")
|
|
186
|
+
and "|" not in line_stripped
|
|
187
|
+
and "%" not in line_stripped
|
|
188
|
+
and line_stripped
|
|
189
|
+
):
|
|
190
|
+
logger.info(line_stripped)
|
|
186
191
|
|
|
187
192
|
async def stderr_callback(line: str) -> None:
|
|
188
|
-
|
|
193
|
+
line_stripped = line.strip()
|
|
194
|
+
if line_stripped:
|
|
195
|
+
logger.warning(f"Install error: {line_stripped}")
|
|
189
196
|
|
|
190
197
|
stdout_task = asyncio.create_task(read_stream(process.stdout, stdout_callback))
|
|
191
198
|
stderr_task = asyncio.create_task(read_stream(process.stderr, stderr_callback))
|
|
@@ -195,18 +202,20 @@ async def execute_install_command(timeout: int) -> tuple[bool, str]:
|
|
|
195
202
|
asyncio.gather(stdout_task, stderr_task), timeout=timeout
|
|
196
203
|
)
|
|
197
204
|
except asyncio.TimeoutError:
|
|
205
|
+
logger.error(f"Timed out ({timeout}s)")
|
|
198
206
|
await terminate_process(process)
|
|
199
207
|
return False, f"Timed out ({timeout}s)"
|
|
200
208
|
|
|
201
209
|
await process.wait()
|
|
202
|
-
|
|
210
|
+
return_code = process.returncode
|
|
203
211
|
|
|
204
|
-
if
|
|
205
|
-
return False, f"Exited with code {
|
|
212
|
+
if return_code != 0:
|
|
213
|
+
return False, f"Exited with code {return_code}"
|
|
206
214
|
|
|
207
215
|
return True, "Installation completed"
|
|
208
216
|
|
|
209
217
|
except Exception as e:
|
|
218
|
+
logger.error(f"An error occurred during installation: {e!s}")
|
|
210
219
|
return False, f"An error occurred during installation: {e!s}"
|
|
211
220
|
|
|
212
221
|
|