entari-plugin-hyw 4.0.0rc5__py3-none-any.whl
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.
Potentially problematic release.
This version of entari-plugin-hyw might be problematic. Click here for more details.
- entari_plugin_hyw/__init__.py +532 -0
- entari_plugin_hyw/assets/card-dist/index.html +387 -0
- entari_plugin_hyw/assets/card-dist/logos/anthropic.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/cerebras.svg +9 -0
- entari_plugin_hyw/assets/card-dist/logos/deepseek.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/gemini.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/google.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/grok.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/huggingface.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/microsoft.svg +15 -0
- entari_plugin_hyw/assets/card-dist/logos/minimax.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/mistral.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/nvida.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/openai.svg +1 -0
- entari_plugin_hyw/assets/card-dist/logos/openrouter.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/perplexity.svg +24 -0
- entari_plugin_hyw/assets/card-dist/logos/qwen.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/xai.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/xiaomi.png +0 -0
- entari_plugin_hyw/assets/card-dist/logos/zai.png +0 -0
- entari_plugin_hyw/assets/card-dist/vite.svg +1 -0
- entari_plugin_hyw/assets/icon/anthropic.svg +1 -0
- entari_plugin_hyw/assets/icon/cerebras.svg +9 -0
- entari_plugin_hyw/assets/icon/deepseek.png +0 -0
- entari_plugin_hyw/assets/icon/gemini.svg +1 -0
- entari_plugin_hyw/assets/icon/google.svg +1 -0
- entari_plugin_hyw/assets/icon/grok.png +0 -0
- entari_plugin_hyw/assets/icon/huggingface.png +0 -0
- entari_plugin_hyw/assets/icon/microsoft.svg +15 -0
- entari_plugin_hyw/assets/icon/minimax.png +0 -0
- entari_plugin_hyw/assets/icon/mistral.png +0 -0
- entari_plugin_hyw/assets/icon/nvida.png +0 -0
- entari_plugin_hyw/assets/icon/openai.svg +1 -0
- entari_plugin_hyw/assets/icon/openrouter.png +0 -0
- entari_plugin_hyw/assets/icon/perplexity.svg +24 -0
- entari_plugin_hyw/assets/icon/qwen.png +0 -0
- entari_plugin_hyw/assets/icon/xai.png +0 -0
- entari_plugin_hyw/assets/icon/xiaomi.png +0 -0
- entari_plugin_hyw/assets/icon/zai.png +0 -0
- entari_plugin_hyw/browser/__init__.py +10 -0
- entari_plugin_hyw/browser/engines/base.py +13 -0
- entari_plugin_hyw/browser/engines/bing.py +95 -0
- entari_plugin_hyw/browser/engines/searxng.py +137 -0
- entari_plugin_hyw/browser/landing.html +172 -0
- entari_plugin_hyw/browser/manager.py +153 -0
- entari_plugin_hyw/browser/service.py +275 -0
- entari_plugin_hyw/card-ui/.gitignore +24 -0
- entari_plugin_hyw/card-ui/README.md +5 -0
- entari_plugin_hyw/card-ui/index.html +16 -0
- entari_plugin_hyw/card-ui/package-lock.json +2342 -0
- entari_plugin_hyw/card-ui/package.json +31 -0
- entari_plugin_hyw/card-ui/public/logos/anthropic.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/cerebras.svg +9 -0
- entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/gemini.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/google.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/microsoft.svg +15 -0
- entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/openai.svg +1 -0
- entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/perplexity.svg +24 -0
- entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
- entari_plugin_hyw/card-ui/public/vite.svg +1 -0
- entari_plugin_hyw/card-ui/src/App.vue +756 -0
- entari_plugin_hyw/card-ui/src/assets/vue.svg +1 -0
- entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +41 -0
- entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +382 -0
- entari_plugin_hyw/card-ui/src/components/SectionCard.vue +41 -0
- entari_plugin_hyw/card-ui/src/components/StageCard.vue +240 -0
- entari_plugin_hyw/card-ui/src/main.ts +5 -0
- entari_plugin_hyw/card-ui/src/style.css +29 -0
- entari_plugin_hyw/card-ui/src/test_regex.js +103 -0
- entari_plugin_hyw/card-ui/src/types.ts +61 -0
- entari_plugin_hyw/card-ui/tsconfig.app.json +16 -0
- entari_plugin_hyw/card-ui/tsconfig.json +7 -0
- entari_plugin_hyw/card-ui/tsconfig.node.json +26 -0
- entari_plugin_hyw/card-ui/vite.config.ts +16 -0
- entari_plugin_hyw/definitions.py +130 -0
- entari_plugin_hyw/history.py +248 -0
- entari_plugin_hyw/image_cache.py +274 -0
- entari_plugin_hyw/misc.py +135 -0
- entari_plugin_hyw/modular_pipeline.py +351 -0
- entari_plugin_hyw/render_vue.py +401 -0
- entari_plugin_hyw/search.py +116 -0
- entari_plugin_hyw/stage_base.py +88 -0
- entari_plugin_hyw/stage_instruct.py +328 -0
- entari_plugin_hyw/stage_instruct_review.py +92 -0
- entari_plugin_hyw/stage_summary.py +164 -0
- entari_plugin_hyw-4.0.0rc5.dist-info/METADATA +116 -0
- entari_plugin_hyw-4.0.0rc5.dist-info/RECORD +99 -0
- entari_plugin_hyw-4.0.0rc5.dist-info/WHEEL +5 -0
- entari_plugin_hyw-4.0.0rc5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
|
|
2
|
+
import urllib.parse
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Dict, Any
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from .base import SearchEngine
|
|
7
|
+
|
|
8
|
+
class SearXNGEngine(SearchEngine):
|
|
9
|
+
"""
|
|
10
|
+
Parser for DuckDuckGo and SearXNG results.
|
|
11
|
+
Handles both Markdown (from Crawl4AI) and HTML (fallback).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def build_url(self, query: str, limit: int = 10) -> str:
|
|
15
|
+
encoded_query = urllib.parse.quote(query)
|
|
16
|
+
# Default fallback if not configurable per instance, but usually this is what we support as "searxng"
|
|
17
|
+
base = "https://lite.duckduckgo.com/lite/"
|
|
18
|
+
return f"{base}?q={encoded_query}"
|
|
19
|
+
|
|
20
|
+
def parse(self, content: str) -> List[Dict[str, Any]]:
|
|
21
|
+
# Prioritize HTML parsing if content looks like HTML
|
|
22
|
+
if "<html" in content.lower() or "<!doctype" in content.lower() or "<div" in content.lower():
|
|
23
|
+
results = self._parse_html(content)
|
|
24
|
+
if results:
|
|
25
|
+
return results
|
|
26
|
+
|
|
27
|
+
# Fallback to Markdown
|
|
28
|
+
return self._parse_markdown(content)
|
|
29
|
+
|
|
30
|
+
def _parse_html(self, content: str) -> List[Dict[str, Any]]:
|
|
31
|
+
results = []
|
|
32
|
+
seen_urls = set()
|
|
33
|
+
|
|
34
|
+
# Simple regex for DDG Lite / SearXNG HTML structure
|
|
35
|
+
link_regex = re.compile(r'<a[^>]+href=["\'](http[^"\']+)["\'][^>]*>(.*?)</a>', re.IGNORECASE | re.DOTALL)
|
|
36
|
+
|
|
37
|
+
pos = 0
|
|
38
|
+
while True:
|
|
39
|
+
match = link_regex.search(content, pos)
|
|
40
|
+
if not match:
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
href = match.group(1)
|
|
44
|
+
title_html = match.group(2)
|
|
45
|
+
|
|
46
|
+
# Clean title
|
|
47
|
+
title = re.sub(r'<[^>]+>', '', title_html).strip()
|
|
48
|
+
|
|
49
|
+
pos = match.end()
|
|
50
|
+
|
|
51
|
+
# Filter junk
|
|
52
|
+
if "search" in href and "q=" in href: continue
|
|
53
|
+
if "google.com" in href or "bing.com" in href: continue
|
|
54
|
+
if href in seen_urls: continue
|
|
55
|
+
|
|
56
|
+
# Look ahead for snippet
|
|
57
|
+
snippet_chunk = content[pos:pos+1000]
|
|
58
|
+
snippet_match = re.search(r'(.*?)<a', snippet_chunk, re.DOTALL | re.IGNORECASE)
|
|
59
|
+
raw_snippet = snippet_match.group(1) if snippet_match else snippet_chunk
|
|
60
|
+
|
|
61
|
+
# Clean HTML tags from snippet
|
|
62
|
+
snippet = re.sub(r'<[^>]+>', ' ', raw_snippet)
|
|
63
|
+
snippet = re.sub(r'\s+', ' ', snippet).strip()
|
|
64
|
+
|
|
65
|
+
# No truncation as per user request (or very generous limit)
|
|
66
|
+
snippet = snippet[:5000]
|
|
67
|
+
|
|
68
|
+
# Valid result check
|
|
69
|
+
if title and len(title) > 2 and snippet:
|
|
70
|
+
# Extract images from the result block (rough heuristic)
|
|
71
|
+
images = []
|
|
72
|
+
img_matches = re.findall(r'<img[^>]+src=["\'](http[^"\']+)["\']', snippet_match.group(0) if snippet_match else snippet_chunk)
|
|
73
|
+
for img_url in img_matches:
|
|
74
|
+
if not any(x in img_url for x in ['favicon', 'icon', 'tracking', 'pixel']):
|
|
75
|
+
images.append(img_url)
|
|
76
|
+
|
|
77
|
+
results.append({
|
|
78
|
+
"title": title,
|
|
79
|
+
"url": href,
|
|
80
|
+
"domain": urllib.parse.urlparse(href).hostname or "",
|
|
81
|
+
"content": snippet,
|
|
82
|
+
"images": images[:3] # Limit per result
|
|
83
|
+
})
|
|
84
|
+
seen_urls.add(href)
|
|
85
|
+
|
|
86
|
+
logger.info(f"SearXNG Parser(HTML) found {len(results)} results.")
|
|
87
|
+
return results
|
|
88
|
+
|
|
89
|
+
def _parse_markdown(self, content: str) -> List[Dict[str, Any]]:
|
|
90
|
+
results = []
|
|
91
|
+
seen_urls = set()
|
|
92
|
+
|
|
93
|
+
# Link regex: [Title](URL)
|
|
94
|
+
link_regex = re.compile(r'\[(.*?)\]\((https?://.*?)\)')
|
|
95
|
+
|
|
96
|
+
lines = content.split('\n')
|
|
97
|
+
current_result = None
|
|
98
|
+
|
|
99
|
+
for line in lines:
|
|
100
|
+
line = line.strip()
|
|
101
|
+
if not line: continue
|
|
102
|
+
|
|
103
|
+
# Check for link
|
|
104
|
+
match = link_regex.search(line)
|
|
105
|
+
if match:
|
|
106
|
+
# Save previous result
|
|
107
|
+
if current_result:
|
|
108
|
+
results.append(current_result)
|
|
109
|
+
|
|
110
|
+
title, href = match.groups()
|
|
111
|
+
|
|
112
|
+
# Filter junk
|
|
113
|
+
if "search" in href and "q=" in href: continue
|
|
114
|
+
if "google.com" in href or "bing.com" in href: continue
|
|
115
|
+
if href in seen_urls:
|
|
116
|
+
current_result = None
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
seen_urls.add(href)
|
|
120
|
+
|
|
121
|
+
current_result = {
|
|
122
|
+
"title": title,
|
|
123
|
+
"url": href,
|
|
124
|
+
"domain": urllib.parse.urlparse(href).hostname or "",
|
|
125
|
+
"content": ""
|
|
126
|
+
}
|
|
127
|
+
elif current_result:
|
|
128
|
+
# Append snippet
|
|
129
|
+
if len(current_result["content"]) < 5000:
|
|
130
|
+
current_result["content"] += " " + line
|
|
131
|
+
|
|
132
|
+
# Append last
|
|
133
|
+
if current_result:
|
|
134
|
+
results.append(current_result)
|
|
135
|
+
|
|
136
|
+
logger.info(f"SearXNG Parser(Markdown) found {len(results)} results.")
|
|
137
|
+
return results
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>entari-plugin-hyw Browser</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--theme-color: #ef4444;
|
|
10
|
+
--text-primary: #2c2c2e;
|
|
11
|
+
--text-body: #3a3a3c;
|
|
12
|
+
--text-muted: #86868b;
|
|
13
|
+
--bg-border: #f2f2f2;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
background-color: var(--bg-border);
|
|
18
|
+
color: var(--text-primary);
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
height: 100vh;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#main-container {
|
|
30
|
+
width: 560px;
|
|
31
|
+
background: white;
|
|
32
|
+
padding: 40px;
|
|
33
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
34
|
+
position: relative;
|
|
35
|
+
transform: scale(1.1);
|
|
36
|
+
/* Slightly larger for visibility */
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.corner-badge {
|
|
40
|
+
position: absolute;
|
|
41
|
+
top: -10px;
|
|
42
|
+
left: -10px;
|
|
43
|
+
height: 28px;
|
|
44
|
+
padding: 0 12px;
|
|
45
|
+
background-color: var(--theme-color);
|
|
46
|
+
color: white;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
font-size: 12px;
|
|
50
|
+
font-weight: 800;
|
|
51
|
+
text-transform: uppercase;
|
|
52
|
+
letter-spacing: 0.5px;
|
|
53
|
+
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.15);
|
|
54
|
+
z-index: 10;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
h1 {
|
|
58
|
+
font-size: 32px;
|
|
59
|
+
font-weight: 900;
|
|
60
|
+
text-transform: uppercase;
|
|
61
|
+
letter-spacing: -1px;
|
|
62
|
+
margin: 0 0 24px 0;
|
|
63
|
+
line-height: 1.1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.brand-text {
|
|
67
|
+
color: var(--theme-color);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.section-title {
|
|
71
|
+
font-size: 17px;
|
|
72
|
+
font-bold: 700;
|
|
73
|
+
text-transform: uppercase;
|
|
74
|
+
letter-spacing: -0.5px;
|
|
75
|
+
margin-bottom: 12px;
|
|
76
|
+
color: var(--text-primary);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.content-box {
|
|
80
|
+
padding-top: 24px;
|
|
81
|
+
border-top: 1px solid #eee;
|
|
82
|
+
margin-top: 32px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.summary {
|
|
86
|
+
font-size: 15px;
|
|
87
|
+
line-height: 1.6;
|
|
88
|
+
color: var(--text-body);
|
|
89
|
+
text-align: justify;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.summary-en {
|
|
93
|
+
margin-top: 16px;
|
|
94
|
+
color: var(--text-muted);
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
font-style: italic;
|
|
97
|
+
line-height: 1.5;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
b {
|
|
101
|
+
color: var(--theme-color);
|
|
102
|
+
font-weight: 700;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Subtle loading animation to show it's "live" */
|
|
106
|
+
.progress-bar {
|
|
107
|
+
height: 3px;
|
|
108
|
+
background: #f3f3f3;
|
|
109
|
+
width: 100%;
|
|
110
|
+
margin-top: 32px;
|
|
111
|
+
position: relative;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.progress-fill {
|
|
116
|
+
position: absolute;
|
|
117
|
+
height: 100%;
|
|
118
|
+
background: var(--theme-color);
|
|
119
|
+
width: 30%;
|
|
120
|
+
left: -30%;
|
|
121
|
+
animation: moveProgress 2s infinite ease-in-out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@keyframes moveProgress {
|
|
125
|
+
0% {
|
|
126
|
+
left: -30%;
|
|
127
|
+
width: 20%;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
50% {
|
|
131
|
+
width: 50%;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
100% {
|
|
135
|
+
left: 100%;
|
|
136
|
+
width: 20%;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
140
|
+
</head>
|
|
141
|
+
|
|
142
|
+
<body>
|
|
143
|
+
<div id="main-container">
|
|
144
|
+
<!-- Corner Badge Style -->
|
|
145
|
+
<div class="corner-badge">
|
|
146
|
+
Status: Ready
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<h1>entari-plugin-<span class="brand-text">hyw</span></h1>
|
|
150
|
+
|
|
151
|
+
<div class="content-box">
|
|
152
|
+
<div class="section-title">Browser Service</div>
|
|
153
|
+
<div class="summary">
|
|
154
|
+
这是一个受 <b>entari-plugin-hyw</b> 插件控制的自动化浏览器实例。<br>
|
|
155
|
+
它负责网络搜索、内容爬取以及卡片 UI 的实时渲染。<br>
|
|
156
|
+
请勿关闭此窗口,以确保插件功能正常运行。
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="summary-en">
|
|
160
|
+
This is an automated browser instance controlled by the <b>entari-plugin-hyw</b> plugin.
|
|
161
|
+
It handles web searches, content crawling, and real-time Card UI rendering.
|
|
162
|
+
Please do not close this window to ensure the plugin functions correctly.
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="progress-bar">
|
|
167
|
+
<div class="progress-fill"></div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</body>
|
|
171
|
+
|
|
172
|
+
</html>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared Browser Manager (DrissionPage)
|
|
3
|
+
|
|
4
|
+
Manages a single ChromiumPage browser instance.
|
|
5
|
+
Supports multi-tab concurrency via DrissionPage's built-in tab management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from DrissionPage import ChromiumPage, ChromiumOptions
|
|
12
|
+
from DrissionPage.errors import PageDisconnectedError
|
|
13
|
+
|
|
14
|
+
class SharedBrowserManager:
|
|
15
|
+
"""
|
|
16
|
+
Manages a shared DrissionPage Chromium browser.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_instance: Optional["SharedBrowserManager"] = None
|
|
20
|
+
_lock = threading.Lock()
|
|
21
|
+
|
|
22
|
+
def __init__(self, headless: bool = True):
|
|
23
|
+
self.headless = headless
|
|
24
|
+
self._page: Optional[ChromiumPage] = None
|
|
25
|
+
self._starting = False
|
|
26
|
+
self._tab_lock = threading.Lock()
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def get_instance(cls, headless: bool = True) -> "SharedBrowserManager":
|
|
30
|
+
"""Get or create the singleton SharedBrowserManager."""
|
|
31
|
+
with cls._lock:
|
|
32
|
+
if cls._instance is None:
|
|
33
|
+
cls._instance = cls(headless=headless)
|
|
34
|
+
return cls._instance
|
|
35
|
+
|
|
36
|
+
def start(self) -> bool:
|
|
37
|
+
"""Start the Chromium browser."""
|
|
38
|
+
if self._page is not None:
|
|
39
|
+
# Check if alive
|
|
40
|
+
try:
|
|
41
|
+
if self._page.run_cdp('Browser.getVersion'):
|
|
42
|
+
return True
|
|
43
|
+
except (PageDisconnectedError, Exception):
|
|
44
|
+
self._page = None
|
|
45
|
+
|
|
46
|
+
with self._lock:
|
|
47
|
+
if self._starting:
|
|
48
|
+
return False
|
|
49
|
+
self._starting = True
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
logger.info("SharedBrowserManager: Starting DrissionPage browser...")
|
|
53
|
+
|
|
54
|
+
# Configure options
|
|
55
|
+
co = ChromiumOptions()
|
|
56
|
+
co.headless(self.headless)
|
|
57
|
+
co.auto_port() # Auto find available port
|
|
58
|
+
|
|
59
|
+
# Anti-detection settings
|
|
60
|
+
co.set_argument('--no-sandbox')
|
|
61
|
+
co.set_argument('--disable-gpu')
|
|
62
|
+
# Essential for loading local files and avoiding CORS issues
|
|
63
|
+
co.set_argument('--allow-file-access-from-files')
|
|
64
|
+
co.set_argument('--disable-web-security')
|
|
65
|
+
# Hide scrollbars globally
|
|
66
|
+
co.set_argument('--hide-scrollbars')
|
|
67
|
+
# 十万的原因是滚动条屏蔽(大概吧)
|
|
68
|
+
co.set_argument('--window-size=1280,20000')
|
|
69
|
+
self._page = ChromiumPage(addr_or_opts=co)
|
|
70
|
+
|
|
71
|
+
# Show Landing Page
|
|
72
|
+
try:
|
|
73
|
+
import os
|
|
74
|
+
landing_path = os.path.join(os.path.dirname(__file__), 'landing.html')
|
|
75
|
+
if os.path.exists(landing_path):
|
|
76
|
+
self._page.get(f"file://{landing_path}")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.warning(f"SharedBrowserManager: Failed to show landing page: {e}")
|
|
79
|
+
|
|
80
|
+
logger.success(f"SharedBrowserManager: Browser ready (port={self._page.address})")
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"SharedBrowserManager: Failed to start browser: {e}")
|
|
85
|
+
self._page = None
|
|
86
|
+
raise
|
|
87
|
+
finally:
|
|
88
|
+
self._starting = False
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def page(self) -> Optional[ChromiumPage]:
|
|
92
|
+
"""Get the main ChromiumPage instance."""
|
|
93
|
+
if self._page is None:
|
|
94
|
+
self.start()
|
|
95
|
+
return self._page
|
|
96
|
+
|
|
97
|
+
def new_tab(self, url: str) -> Any:
|
|
98
|
+
"""Thread-safe tab creation."""
|
|
99
|
+
with self._tab_lock:
|
|
100
|
+
page = self.page
|
|
101
|
+
if not page:
|
|
102
|
+
raise RuntimeError("Browser not available")
|
|
103
|
+
return page.new_tab(url)
|
|
104
|
+
|
|
105
|
+
def close(self):
|
|
106
|
+
"""Shutdown the browser."""
|
|
107
|
+
with self._lock:
|
|
108
|
+
if self._page:
|
|
109
|
+
try:
|
|
110
|
+
self._page.quit()
|
|
111
|
+
logger.info("SharedBrowserManager: Browser closed.")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning(f"SharedBrowserManager: Error closing browser: {e}")
|
|
114
|
+
finally:
|
|
115
|
+
self._page = None
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def hide_scrollbars(page: ChromiumPage):
|
|
119
|
+
"""
|
|
120
|
+
Robustly hide scrollbars using CDP commands.
|
|
121
|
+
This eliminates the reserved space/gutter that standard CSS might miss.
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
# Emulation.setScrollbarsHidden is a CDP command
|
|
125
|
+
page.run_cdp('Emulation.setScrollbarsHidden', hidden=True)
|
|
126
|
+
logger.debug("SharedBrowserManager: CDP scrollbars hidden.")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning(f"SharedBrowserManager: Failed to hide scrollbars via CDP: {e}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Module-level singleton accessor
|
|
132
|
+
_shared_manager: Optional[SharedBrowserManager] = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_shared_browser_manager(headless: bool = True) -> SharedBrowserManager:
|
|
136
|
+
"""
|
|
137
|
+
Get or create the shared browser manager.
|
|
138
|
+
"""
|
|
139
|
+
global _shared_manager
|
|
140
|
+
|
|
141
|
+
if _shared_manager is None:
|
|
142
|
+
_shared_manager = SharedBrowserManager.get_instance(headless=headless)
|
|
143
|
+
_shared_manager.start()
|
|
144
|
+
|
|
145
|
+
return _shared_manager
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def close_shared_browser():
|
|
149
|
+
"""Close the shared browser manager."""
|
|
150
|
+
global _shared_manager
|
|
151
|
+
if _shared_manager:
|
|
152
|
+
_shared_manager.close()
|
|
153
|
+
_shared_manager = None
|