entari-plugin-hyw 3.5.0rc1__py3-none-any.whl → 3.5.0rc2__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.

Files changed (32) hide show
  1. entari_plugin_hyw/__init__.py +77 -82
  2. entari_plugin_hyw/assets/card-dist/index.html +360 -99
  3. entari_plugin_hyw/card-ui/src/App.vue +246 -52
  4. entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +122 -67
  5. entari_plugin_hyw/card-ui/src/components/StageCard.vue +46 -26
  6. entari_plugin_hyw/card-ui/src/test_regex.js +103 -0
  7. entari_plugin_hyw/card-ui/src/types.ts +1 -0
  8. entari_plugin_hyw/{core/history.py → history.py} +25 -1
  9. entari_plugin_hyw/image_cache.py +283 -0
  10. entari_plugin_hyw/{core/pipeline.py → pipeline.py} +102 -27
  11. entari_plugin_hyw/{utils/prompts.py → prompts.py} +7 -24
  12. entari_plugin_hyw/render_vue.py +314 -0
  13. entari_plugin_hyw/{utils/search.py → search.py} +227 -10
  14. {entari_plugin_hyw-3.5.0rc1.dist-info → entari_plugin_hyw-3.5.0rc2.dist-info}/METADATA +1 -1
  15. {entari_plugin_hyw-3.5.0rc1.dist-info → entari_plugin_hyw-3.5.0rc2.dist-info}/RECORD +18 -29
  16. entari_plugin_hyw/core/__init__.py +0 -0
  17. entari_plugin_hyw/core/config.py +0 -35
  18. entari_plugin_hyw/core/hyw.py +0 -48
  19. entari_plugin_hyw/core/render_vue.py +0 -255
  20. entari_plugin_hyw/test_output/render_0.jpg +0 -0
  21. entari_plugin_hyw/test_output/render_1.jpg +0 -0
  22. entari_plugin_hyw/test_output/render_2.jpg +0 -0
  23. entari_plugin_hyw/test_output/render_3.jpg +0 -0
  24. entari_plugin_hyw/test_output/render_4.jpg +0 -0
  25. entari_plugin_hyw/tests/ui_test_output.jpg +0 -0
  26. entari_plugin_hyw/tests/verify_ui.py +0 -139
  27. entari_plugin_hyw/utils/__init__.py +0 -2
  28. entari_plugin_hyw/utils/browser.py +0 -40
  29. entari_plugin_hyw/utils/playwright_tool.py +0 -36
  30. /entari_plugin_hyw/{utils/misc.py → misc.py} +0 -0
  31. {entari_plugin_hyw-3.5.0rc1.dist-info → entari_plugin_hyw-3.5.0rc2.dist-info}/WHEEL +0 -0
  32. {entari_plugin_hyw-3.5.0rc1.dist-info → entari_plugin_hyw-3.5.0rc2.dist-info}/top_level.txt +0 -0
@@ -1,35 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Optional, Dict, Any, List
3
-
4
- @dataclass
5
- class HYWConfig:
6
- api_key: str
7
- model_name: str
8
- vision_model_name: Optional[str] = None
9
- vision_api_key: Optional[str] = None
10
- vision_base_url: Optional[str] = None
11
- base_url: str = "https://openrouter.ai/api/v1"
12
- fusion_mode: bool = False
13
- save_conversation: bool = False
14
- headless: bool = True
15
- instruct_model_name: Optional[str] = None
16
- instruct_api_key: Optional[str] = None
17
- instruct_base_url: Optional[str] = None
18
- search_base_url: str = "https://lite.duckduckgo.com/lite/?q={query}"
19
- image_search_base_url: str = "https://duckduckgo.com/?q={query}&iax=images&ia=images"
20
- search_params: Optional[str] = None # e.g. "&kl=cn-zh" for China region
21
- search_limit: int = 8
22
- extra_body: Optional[Dict[str, Any]] = None
23
- vision_extra_body: Optional[Dict[str, Any]] = None
24
- instruct_extra_body: Optional[Dict[str, Any]] = None
25
- temperature: float = 0.4
26
- max_turns: int = 10
27
- enable_browser_fallback: bool = False
28
- language: str = "Simplified Chinese"
29
- input_price: Optional[float] = None # $ per 1M input tokens
30
- output_price: Optional[float] = None # $ per 1M output tokens
31
- vision_input_price: Optional[float] = None
32
- vision_output_price: Optional[float] = None
33
- instruct_input_price: Optional[float] = None
34
- instruct_output_price: Optional[float] = None
35
-
@@ -1,48 +0,0 @@
1
- from typing import Any, Dict, List, Optional
2
- from loguru import logger
3
- from .config import HYWConfig
4
- from .pipeline import ProcessingPipeline
5
-
6
- class HYW:
7
- """
8
- V2 Core Wrapper (Facade).
9
- Delegates all logic to ProcessingPipeline.
10
- Ensures safe lifecycle management.
11
- """
12
- def __init__(self, config: HYWConfig):
13
- self.config = config
14
- # No persistent pipeline - we create one per request to ensure thread safety
15
- # self.pipeline = ProcessingPipeline(config)
16
- logger.info(f"HYW V2 (Ironclad) initialized - Model: {config.model_name}")
17
-
18
- async def agent(self, user_input: str, conversation_history: List[Dict] = None, images: List[str] = None,
19
- selected_model: str = None, selected_vision_model: str = None, local_mode: bool = False) -> Dict[str, Any]:
20
- """
21
- Main entry point for the plugin (called by __init__.py).
22
- Creates a fresh pipeline instance for each request to avoid state contamination (race conditions).
23
- """
24
- pipeline = ProcessingPipeline(self.config)
25
- try:
26
- # Delegate completely to pipeline
27
- result = await pipeline.execute(
28
- user_input,
29
- conversation_history or [],
30
- model_name=selected_model,
31
- images=images,
32
- selected_vision_model=selected_vision_model,
33
- )
34
- return result
35
- finally:
36
- await pipeline.close()
37
-
38
- async def close(self):
39
- """Explicit async close method. NO __del__."""
40
- # Close shared resources
41
- try:
42
- from ..utils.search import close_shared_crawler
43
- await close_shared_crawler()
44
- except Exception:
45
- pass
46
-
47
- # Legacy Compatibility (optional attributes just to prevent blind attribute errors if referenced externally)
48
- # in V2 we strongly discourage accessing internal tools directly.
@@ -1,255 +0,0 @@
1
- """
2
- Vue-based Card Renderer (Minimal Python)
3
-
4
- Python only provides raw data. All frontend logic (markdown, syntax highlighting,
5
- math rendering, citations) is handled by the Vue frontend.
6
- """
7
-
8
- import json
9
- import gc
10
- import uuid
11
- import os
12
- from pathlib import Path
13
- from typing import List, Dict, Any
14
-
15
- import asyncio
16
- from loguru import logger
17
- from playwright.async_api import async_playwright
18
-
19
-
20
- class ContentRenderer:
21
- """Minimal renderer - only passes raw data to Vue template."""
22
-
23
-
24
- def __init__(self, template_path: str = None):
25
- if template_path is None:
26
- current_dir = Path(__file__).parent
27
- plugin_root = current_dir.parent
28
- template_path = plugin_root / "assets" / "card-dist" / "index.html"
29
-
30
- self.template_path = Path(template_path)
31
- if not self.template_path.exists():
32
- raise FileNotFoundError(f"Vue template not found: {self.template_path}")
33
-
34
- self.template_content = self.template_path.read_text(encoding="utf-8")
35
- logger.info(f"ContentRenderer: loaded Vue template ({len(self.template_content)} bytes)")
36
-
37
- # Persistent state
38
- self.playwright = None
39
- self.browser = None
40
- self.context = None
41
- self.page = None
42
- self._lock = asyncio.Lock()
43
- self._render_count = 0
44
- self._max_renders_before_restart = 50 # Prevent memory leaks
45
-
46
- async def start(self):
47
- """Initialize the browser and page."""
48
- if self.page:
49
- return
50
-
51
- logger.info("ContentRenderer: Starting persistent browser...")
52
- try:
53
- self.playwright = await async_playwright().start()
54
- self.browser = await self.playwright.chromium.launch(headless=True, args=['--no-sandbox', '--disable-setuid-sandbox'])
55
- self.context = await self.browser.new_context(
56
- viewport={"width": 520, "height": 1400},
57
- device_scale_factor=2.4,
58
- )
59
- self.page = await self.context.new_page()
60
-
61
- # Load the template once
62
- await self.page.goto(self.template_path.as_uri(), wait_until="networkidle")
63
- logger.info("ContentRenderer: Browser started and template loaded.")
64
-
65
- except Exception as e:
66
- logger.error(f"ContentRenderer: Failed to start browser: {e}")
67
- await self.close()
68
- raise
69
-
70
- async def close(self):
71
- """Clean up browser resources."""
72
- if self.page:
73
- await self.page.close()
74
- self.page = None
75
- if self.context:
76
- await self.context.close()
77
- self.context = None
78
- if self.browser:
79
- await self.browser.close()
80
- self.browser = None
81
- if self.playwright:
82
- await self.playwright.stop()
83
- self.playwright = None
84
- logger.info("ContentRenderer: Browser closed.")
85
-
86
- async def _get_page(self):
87
- """Get or recreate the persistent page."""
88
- if self._render_count >= self._max_renders_before_restart:
89
- logger.info(f"ContentRenderer: Restarting browser after {self._render_count} renders...")
90
- await self.close()
91
- self._render_count = 0
92
-
93
- if not self.page:
94
- await self.start()
95
-
96
- return self.page
97
-
98
- async def render(
99
- self,
100
- markdown_content: str,
101
- output_path: str,
102
- stats: Dict[str, Any] = None,
103
- references: List[Dict[str, Any]] = None,
104
- page_references: List[Dict[str, Any]] = None,
105
- image_references: List[Dict[str, Any]] = None,
106
- stages_used: List[Dict[str, Any]] = None,
107
- image_timeout: int = 3000,
108
- **kwargs
109
- ) -> bool:
110
- """Render content to image using persistent browser."""
111
-
112
- resolved_output_path = Path(output_path).resolve()
113
- resolved_output_path.parent.mkdir(parents=True, exist_ok=True)
114
-
115
- # Prepare data
116
- stats_dict = stats[0] if isinstance(stats, list) and stats else (stats or {})
117
-
118
- render_data = {
119
- "markdown": markdown_content,
120
- "total_time": stats_dict.get("total_time", 0) or 0,
121
- "stages": [
122
- {
123
- "name": s.get("name", "Step"),
124
- "model": s.get("model", ""),
125
- "provider": s.get("provider", ""),
126
- "time": s.get("time", 0),
127
- "cost": s.get("cost", 0),
128
- "references": s.get("references") or s.get("search_results"),
129
- "image_references": s.get("image_references"),
130
- "crawled_pages": s.get("crawled_pages"),
131
- }
132
- for s in (stages_used or [])
133
- ],
134
- "references": references or [],
135
- "page_references": page_references or [],
136
- "image_references": image_references or [],
137
- "stats": stats_dict,
138
- }
139
- import time
140
- start_time = time.time()
141
-
142
- # Reorder images
143
- self._reorder_images_in_stages(render_data["markdown"], render_data["stages"])
144
-
145
- async with self._lock:
146
- try:
147
- page = await self._get_page()
148
-
149
- # Update data via JS
150
- # Using evaluate to call window.updateRenderData
151
- await page.evaluate("(data) => window.updateRenderData(data)", render_data)
152
-
153
-
154
- # Wait for Vue to update DOM
155
- # Give Vue a moment to patch the DOM (insert img tags)
156
- await asyncio.sleep(0.1)
157
-
158
- # Wait for all images to load
159
- try:
160
- await page.wait_for_function(
161
- "() => Array.from(document.images).every(img => img.complete)",
162
- timeout=image_timeout
163
- )
164
- except Exception:
165
- logger.warning(f"ContentRenderer: Timeout waiting for images to load ({image_timeout}ms), taking screenshot anyway.")
166
-
167
- # Resize height if needed?
168
- # The page height might change. We capture full page or specific element.
169
- # If capturing element:
170
- element = await page.query_selector("#main-container")
171
- if element:
172
- # Clean previous screenshots? No, overwrite.
173
- await element.screenshot(path=str(resolved_output_path), type="jpeg", quality=98)
174
- else:
175
- await page.screenshot(path=str(resolved_output_path), full_page=True, type="jpeg", quality=98)
176
-
177
- self._render_count += 1
178
-
179
- duration = time.time() - start_time
180
- logger.success(f"ContentRenderer: Rendered in {duration:.3f}s (No.{self._render_count})")
181
- return True
182
-
183
- except Exception as exc:
184
- logger.error(f"ContentRenderer: render failed ({exc})")
185
- # If render failed, maybe browser is dead. Close it to force restart next time.
186
- await self.close()
187
- return False
188
- finally:
189
- gc.collect()
190
-
191
- async def render_models_list(
192
- self,
193
- models: List[Dict[str, Any]],
194
- output_path: str,
195
- default_base_url: str = "https://openrouter.ai/api/v1",
196
- **kwargs
197
- ) -> bool:
198
- """Render models list."""
199
- lines = ["# 模型列表"]
200
- for idx, model in enumerate(models or [], start=1):
201
- name = model.get("name", "unknown")
202
- base_url = model.get("base_url") or default_base_url
203
- provider = model.get("provider", "")
204
- lines.append(f"{idx}. **{name}** \n - base_url: {base_url} \n - provider: {provider}")
205
-
206
- markdown_content = "\n\n".join(lines) if len(lines) > 1 else "# 模型列表\n暂无模型"
207
-
208
- return await self.render(
209
- markdown_content=markdown_content,
210
- output_path=output_path,
211
- stats={},
212
- references=[],
213
- stages_used=[],
214
- )
215
-
216
- def _reorder_images_in_stages(self, markdown: str, stages: List[Dict[str, Any]]) -> None:
217
- """Reorder image references in stages based on appearance in markdown."""
218
- import re
219
-
220
- # 1. Extract clean URLs from markdown
221
- # Matches: ![...](https://...)
222
- img_urls = []
223
- for match in re.finditer(r'!\[.*?\]\((.*?)\)', markdown):
224
- # Url might be followed by title: "url" "title"
225
- url_part = match.group(1).split()[0].strip()
226
- if url_part and url_part not in img_urls:
227
- img_urls.append(url_part)
228
-
229
- if not img_urls:
230
- return
231
-
232
- # 2. Reorder each stage's image_references
233
- for stage in stages:
234
- refs = stage.get("image_references")
235
- if not refs:
236
- continue
237
-
238
- # Map url -> ref object
239
- ref_map = {r["url"]: r for r in refs}
240
-
241
- new_refs = []
242
- seen_urls = set()
243
-
244
- # First, add images found in markdown in order
245
- for url in img_urls:
246
- if url in ref_map:
247
- new_refs.append(ref_map[url])
248
- seen_urls.add(url)
249
-
250
- # Then add remaining images not found in markdown
251
- for r in refs:
252
- if r["url"] not in seen_urls:
253
- new_refs.append(r)
254
-
255
- stage["image_references"] = new_refs
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,139 +0,0 @@
1
- import sys
2
- import asyncio
3
- from pathlib import Path
4
-
5
- # Directly add the core directory to sys.path to avoid importing the parent package (and triggering entari init)
6
- core_dir = Path(__file__).parent.parent / "core"
7
- sys.path.append(str(core_dir))
8
-
9
- # Import directly as a module
10
- from render_vue import ContentRenderer
11
-
12
- def verify_ui():
13
- renderer = ContentRenderer()
14
-
15
- # Test with real GTNH conversation data
16
- markdown_content = """# 终极硬核整合包格雷科技新视野
17
-
18
- <summary>
19
- 《格雷科技:新视野》(GregTech: New Horizons,简称 GTNH)是一款基于 Minecraft 1.7.10 版本的深度硬核科技向整合包。它以 GregTech 5 Unofficial 为核心,通过超过 8 年的持续开发,将 300 多个模组深度集成,构建了极其严苛且逻辑严密的科技树,是公认的生存挑战巅峰之作。
20
- </summary>
21
-
22
- ## 核心机制与游戏体验
23
- GTNH 的核心在于"格雷化"改造,几乎所有模组的合成表都经过重新设计,以匹配其严苛的阶级制度 [4][8]。玩家需要从原始的石器时代开始,历经蒸汽时代、电力时代,最终向星际航行迈进。其游戏过程极其漫长,旨在让玩家在每一毫秒的进度中感受工业发展的成就感 [3][7]。
24
-
25
- ![GTNH 游戏场景](https://i.ytimg.com/vi/5T-oSWAgaMM/maxresdefault.jpg)
26
-
27
- ## 科技阶层与任务系统
28
- 整合包拥有 15 个清晰的科技等级(Tiers),最终目标是建造"星门"(Stargate)[2]。为了引导玩家不迷失在复杂的工业流程中,GTNH 内置了超过 3900 条任务的巨型任务书,涵盖了从基础生存到高阶多方块结构的详细指导 [4][7]。
29
-
30
- - 15 个科技等级
31
- - 任务数量:3900+
32
- - 最终目标:建造"星门"
33
-
34
- > 机动战士高达系列是日本动画史上最具影响力的动画作品之一,深受全球观众的喜爱。
35
-
36
- | 特性 | 详细描述 |
37
- | :--- | :--- |
38
- | **基础版本** | Minecraft 1.7.10 (高度优化) |
39
- | **任务数量** | 3900+ 任务引导 [7] |
40
- | **科技阶层** | 15 个技术等级 [2] |
41
- | **核心模组** | GregTech 5 Unofficial, Thaumcraft 等 [8] |
42
-
43
- ## 安装与运行建议
44
- 由于其高度集成的特性,官方强烈建议使用 **Prism Launcher** 进行安装和管理 [5]。在运行环境方面,虽然基于旧版 MC,但通过社区努力,目前推荐使用 **Java 17-25** 版本以获得最佳的内存管理和性能优化,确保大型自动化工厂运行流畅 [5]。
45
-
46
- ```bash
47
- curl -s https://raw.githubusercontent.com/GTNewHorizons/GT-New-Horizons-Modpack/master/README.md
48
- java -version
49
- java -Xmx1024M -Xms1024M -jar prism-launcher.jar
50
- ```
51
- """
52
-
53
- stages = [
54
- {
55
- "name": "instruct",
56
- "status": "completed",
57
- "cost": 0.0002,
58
- "time": 1.83,
59
- "model": "qwen/qwen3-235b-a22b-2507",
60
- "description": "Planning search strategy"
61
- },
62
- {
63
- "name": "search",
64
- "status": "completed",
65
- "cost": 0.0,
66
- "time": 0.5,
67
- "references": [
68
- {"title": "GTNH 2025 Server Information", "url": "https://stonelegion.com/mc-gtnh-2026/gtnh-2025-server-information-including-client-download/"},
69
- {"title": "GT New Horizons Wiki", "url": "https://gtnh.miraheze.org/wiki/Main_Page"},
70
- {"title": "GT New Horizons - GitHub", "url": "https://github.com/GTNewHorizons/GT-New-Horizons-Modpack"},
71
- {"title": "GT New Horizons - CurseForge", "url": "https://www.curseforge.com/minecraft/modpacks/gt-new-horizons"},
72
- {"title": "Installing and Migrating - GTNH", "url": "https://gtnh.miraheze.org/wiki/Installing_and_Migrating"},
73
- {"title": "Modlist - GT New Horizons", "url": "https://wiki.gtnewhorizons.com/wiki/Modlist"},
74
- {"title": "GregTech: New Horizons - Home", "url": "https://www.gtnewhorizons.com/"},
75
- {"title": "GT New Horizons - FTB Wiki", "url": "https://ftb.fandom.com/wiki/GT_New_Horizons"}
76
- ],
77
- "image_references": [
78
- {
79
- "title": "GTNH Live Lets Play",
80
- "url": "https://i.ytimg.com/vi/5T-oSWAgaMM/maxresdefault.jpg",
81
- "thumbnail": "https://tse4.mm.bing.net/th/id/OIP.b_56VnY4nyrzeqp1JetmFQHaEK?pid=Api"
82
- },
83
- {
84
- "title": "GTNH Modpack Cover",
85
- "url": "https://i.mcmod.cn/modpack/cover/20240113/1705139595_29797_dSkE.jpg",
86
- "thumbnail": "https://tse1.mm.bing.net/th/id/OIP.KNKaZX1d_4Ueq6vpl1qJNAHaEo?pid=Api"
87
- },
88
- {
89
- "title": "GTNH Steam Age",
90
- "url": "https://i.ytimg.com/vi/8IPwXxqB71w/maxresdefault.jpg",
91
- "thumbnail": "https://tse4.mm.bing.net/th/id/OIP.P-KrnI4GBH21yPgwpNPSzAHaEK?pid=Api"
92
- },
93
- {
94
- "title": "GTNH MCMod Cover",
95
- "url": "https://i.mcmod.cn/post/cover/20230201/1675241030_2_VqDc.jpg",
96
- "thumbnail": "https://tse2.mm.bing.net/th/id/OIP.GvYz7YWrg-fnpAHjOiW3OAHaEo?pid=Api"
97
- },
98
- {
99
- "title": "GTNH Tectech Tutorial",
100
- "url": "http://i0.hdslb.com/bfs/archive/1ed1e53341fd44018138f2823b2fe6c499fb9c9c.jpg",
101
- "thumbnail": "https://tse4.mm.bing.net/th/id/OIP.0Wg7xFHTjhxIV9hKuUo4xwHaEo?pid=Api"
102
- }
103
- ]
104
- },
105
- {
106
- "name": "agent",
107
- "status": "completed",
108
- "cost": 0.0018,
109
- "time": 13.0,
110
- "model": "google/gemini-3-flash-preview",
111
- "description": "Synthesizing information..."
112
- }
113
- ]
114
-
115
- output_path = Path(__file__).parent / "ui_test_output.jpg"
116
- print(f"🎨 Rendering to {output_path}...")
117
-
118
- try:
119
- # Since render is async, we need to run it in an event loop
120
- async def run_render():
121
- await renderer.render(
122
- markdown_content=markdown_content,
123
- output_path=str(output_path),
124
- stats={"total_time": 8.5},
125
- stages_used=stages,
126
- references=[{"title": f"Ref {i}", "url": "http://example.com"} for i in range(10)],
127
- page_references=[{"title": f"Page {i}", "url": "http://example.com"} for i in range(2)],
128
- image_references=[]
129
- )
130
-
131
- asyncio.run(run_render())
132
- print(f"✨ Success! Image saved to: {output_path}")
133
- except Exception as e:
134
- print(f"❌ Error: {e}")
135
- import traceback
136
- traceback.print_exc()
137
-
138
- if __name__ == "__main__":
139
- verify_ui()
@@ -1,2 +0,0 @@
1
- from .prompts import AGENT_SP
2
- from .misc import process_onebot_json, process_images
@@ -1,40 +0,0 @@
1
- from typing import Any
2
- from loguru import logger
3
- from crawl4ai import AsyncWebCrawler
4
- from crawl4ai.async_configs import CrawlerRunConfig
5
- from crawl4ai.cache_context import CacheMode
6
-
7
-
8
- class BrowserTool:
9
- """Crawl4AI-based page fetcher."""
10
-
11
- def __init__(self, config: Any):
12
- self.config = config
13
-
14
- async def navigate(self, url: str) -> str:
15
- """Fetch URL content via Crawl4AI and return markdown."""
16
- if not url:
17
- return "Error: missing url"
18
- try:
19
- async with AsyncWebCrawler() as crawler:
20
- result = await crawler.arun(
21
- url=url,
22
- config=CrawlerRunConfig(
23
- wait_until="networkidle",
24
- wait_for_images=True,
25
- cache_mode=CacheMode.BYPASS,
26
- word_count_threshold=1,
27
- screenshot=False,
28
- ),
29
- )
30
- if not result.success:
31
- return f"Error navigating to {url}: {result.error_message or result.status_code}"
32
-
33
- content = result.markdown or result.extracted_content or result.cleaned_html or result.html or ""
34
- return content[:8000]
35
- except Exception as e:
36
- logger.error(f"HTTP navigation failed: {e}")
37
- return f"Error navigating to {url}: {e}"
38
-
39
- async def close(self):
40
- return None
@@ -1,36 +0,0 @@
1
- from typing import Any
2
- from loguru import logger
3
- from crawl4ai.async_configs import CrawlerRunConfig
4
- from crawl4ai.cache_context import CacheMode
5
- from .search import get_shared_crawler
6
-
7
-
8
- class PlaywrightTool:
9
- """
10
- Backwards-compatible wrapper now powered by Crawl4AI.
11
- """
12
- def __init__(self, config: Any):
13
- self.config = config
14
-
15
- async def navigate(self, url: str) -> str:
16
- if not url:
17
- return "Error: Missing url"
18
-
19
- try:
20
- crawler = await get_shared_crawler()
21
- result = await crawler.arun(
22
- url=url,
23
- config=CrawlerRunConfig(
24
- wait_until="networkidle",
25
- wait_for_images=True,
26
- cache_mode=CacheMode.BYPASS,
27
- word_count_threshold=1,
28
- screenshot=False,
29
- ),
30
- )
31
- if not result.success:
32
- return f"Error: crawl failed ({result.error_message or result.status_code})"
33
- return (result.markdown or result.extracted_content or result.cleaned_html or result.html or "")[:8000]
34
- except Exception as e:
35
- logger.warning(f"Crawl navigation failed: {e}")
36
- return f"Error: Crawl navigation failed: {e}"
File without changes