entari-plugin-hyw 4.0.0rc16__tar.gz → 4.0.0rc17__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.

Potentially problematic release.


This version of entari-plugin-hyw might be problematic. Click here for more details.

Files changed (58) hide show
  1. {entari_plugin_hyw-4.0.0rc16/src/entari_plugin_hyw.egg-info → entari_plugin_hyw-4.0.0rc17}/PKG-INFO +1 -1
  2. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/pyproject.toml +1 -1
  3. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/__init__.py +142 -15
  4. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/search_cache.py +110 -11
  5. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17/src/entari_plugin_hyw.egg-info}/PKG-INFO +1 -1
  6. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/agent.py +6 -6
  7. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/index.html +23 -23
  8. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/core.py +13 -21
  9. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/definitions.py +17 -3
  10. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/MANIFEST.in +0 -0
  11. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/README.md +0 -0
  12. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/setup.cfg +0 -0
  13. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/filters.py +0 -0
  14. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/history.py +0 -0
  15. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/misc.py +0 -0
  16. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw.egg-info/SOURCES.txt +0 -0
  17. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw.egg-info/dependency_links.txt +0 -0
  18. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw.egg-info/requires.txt +0 -0
  19. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw.egg-info/top_level.txt +0 -0
  20. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/__init__.py +0 -0
  21. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/__init__.py +0 -0
  22. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/anthropic.svg +0 -0
  23. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/cerebras.svg +0 -0
  24. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/deepseek.png +0 -0
  25. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/gemini.svg +0 -0
  26. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/google.svg +0 -0
  27. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/grok.png +0 -0
  28. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/huggingface.png +0 -0
  29. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/microsoft.svg +0 -0
  30. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/minimax.png +0 -0
  31. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/mistral.png +0 -0
  32. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/nvida.png +0 -0
  33. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/openai.svg +0 -0
  34. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/openrouter.png +0 -0
  35. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/perplexity.svg +0 -0
  36. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/qwen.png +0 -0
  37. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/xai.png +0 -0
  38. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/xiaomi.png +0 -0
  39. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/logos/zai.png +0 -0
  40. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/vite.svg +0 -0
  41. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/engines/__init__.py +0 -0
  42. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/engines/base.py +0 -0
  43. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/engines/default.py +0 -0
  44. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/engines/duckduckgo.py +0 -0
  45. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/landing.html +0 -0
  46. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/manager.py +0 -0
  47. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/renderer.py +0 -0
  48. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/service.py +0 -0
  49. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/config.py +0 -0
  50. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/crawling/__init__.py +0 -0
  51. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/crawling/completeness.py +0 -0
  52. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/crawling/models.py +0 -0
  53. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/image_cache.py +0 -0
  54. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/pipeline.py +0 -0
  55. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/search.py +0 -0
  56. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/stages/__init__.py +0 -0
  57. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/stages/base.py +0 -0
  58. {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/stages/summary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entari_plugin_hyw
3
- Version: 4.0.0rc16
3
+ Version: 4.0.0rc17
4
4
  Summary: Use large language models to interpret chat messages
5
5
  Author-email: kumoSleeping <zjr2992@outlook.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "entari_plugin_hyw"
7
- version = "4.0.0-rc16"
7
+ version = "4.0.0-rc17"
8
8
  description = "Use large language models to interpret chat messages"
9
9
  authors = [{name = "kumoSleeping", email = "zjr2992@outlook.com"}]
10
10
  dependencies = [
@@ -43,7 +43,7 @@ from .misc import (
43
43
  RecentEventDeduper,
44
44
  )
45
45
  from .filters import parse_filter_syntax
46
- from .search_cache import SearchResultCache, parse_single_index, parse_multi_indices
46
+ from .search_cache import SearchResultCache, parse_single_index, parse_multi_indices, crop_to_square_thumbnail
47
47
 
48
48
 
49
49
  try:
@@ -305,6 +305,22 @@ async def process_request(
305
305
  await session.send(f"Error: {response.error}")
306
306
  return
307
307
  else:
308
+ # Process screenshots: cache full images and create thumbnails
309
+ for ref in response.references:
310
+ if ref.get("raw_screenshot_b64"):
311
+ full_b64 = ref["raw_screenshot_b64"]
312
+ url = ref.get("url", "")
313
+
314
+ # Cache full screenshot
315
+ cache_id = search_cache.store_screenshot(full_b64, url)
316
+ ref["screenshot_cache_id"] = cache_id
317
+
318
+ # Create thumbnail (1:1 crop from top)
319
+ thumbnail = crop_to_square_thumbnail(full_b64, max_size=400)
320
+ if thumbnail:
321
+ ref["raw_screenshot_b64"] = thumbnail
322
+ ref["is_thumbnail"] = True
323
+
308
324
  # 3. Explicit External Render using the Parallel Tab
309
325
  render_ok = await core.render(
310
326
  markdown_content=response.content,
@@ -476,37 +492,148 @@ async def handle_web_command(session: Session[MessageCreatedEvent], result: Arpa
476
492
  if session.reply and hasattr(session.reply.origin, 'id'):
477
493
  reply_msg_id = str(session.reply.origin.id)
478
494
 
479
- # Quote + Index mode: Screenshot specific cached result
495
+ # Quote + Index mode: Screenshot specific cached result(s)
480
496
  if reply_msg_id:
481
497
  cached = search_cache.get(reply_msg_id)
482
498
  if cached:
483
- # Parse index from query
499
+ # Case 1: No query - show all results as Sources card
500
+ if not query:
501
+ local_renderer = await get_content_renderer()
502
+ tab_task = asyncio.create_task(local_renderer.prepare_tab())
503
+
504
+ # Build references from cached results
505
+ references = []
506
+ for i, res in enumerate(cached.results[:10]):
507
+ references.append({
508
+ "title": res.get("title", f"Result {i+1}"),
509
+ "url": res.get("url", ""),
510
+ "snippet": res.get("content", "") or res.get("snippet", ""),
511
+ "original_idx": i + 1,
512
+ })
513
+
514
+ try:
515
+ tab_id = await tab_task
516
+ except Exception:
517
+ tab_id = None
518
+
519
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
520
+ output_path = tf.name
521
+
522
+ core = get_hyw_core()
523
+ render_ok = await core.render(
524
+ markdown_content=f"# 搜索结果: {cached.query}",
525
+ output_path=output_path,
526
+ stats={"total_time": 0},
527
+ references=references,
528
+ page_references=[],
529
+ stages_used=[{"name": "cache", "description": f"缓存结果 ({len(references)} 条)", "time": 0}],
530
+ tab_id=tab_id
531
+ )
532
+
533
+ if render_ok and os.path.exists(output_path):
534
+ with open(output_path, "rb") as f:
535
+ img_data = base64.b64encode(f.read()).decode()
536
+
537
+ msg_chain = MessageChain(Image(src=f'data:image/png;base64,{img_data}'))
538
+ if conf.quote:
539
+ msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
540
+
541
+ sent = await session.send(msg_chain)
542
+
543
+ # Re-cache with new message ID for chaining
544
+ sent_id = next((str(e.id) for e in sent if hasattr(e, 'id')), None) if sent else None
545
+ if sent_id:
546
+ search_cache.store(sent_id, cached.results[:10], cached.query)
547
+
548
+ os.remove(output_path)
549
+ else:
550
+ await session.send("渲染搜索结果失败")
551
+
552
+ search_cache.cleanup()
553
+ return
554
+
555
+ # Case 2: Multi-index mode - try parsing multiple indices first
556
+ indices = parse_multi_indices(query)
557
+ if indices is not None:
558
+ # Validate all indices
559
+ invalid_indices = [i + 1 for i in indices if i >= len(cached.results)]
560
+ if invalid_indices:
561
+ await session.send(f"序号超出范围: {invalid_indices} (最大: {len(cached.results)})")
562
+ search_cache.cleanup()
563
+ return
564
+
565
+ # Collect URLs to screenshot
566
+ urls_to_screenshot = []
567
+ for idx in indices:
568
+ target_url = cached.results[idx].get("url", "")
569
+ if target_url and target_url not in urls_to_screenshot:
570
+ urls_to_screenshot.append(target_url)
571
+
572
+ if not urls_to_screenshot:
573
+ await session.send("所选结果无有效URL")
574
+ search_cache.cleanup()
575
+ return
576
+
577
+ if conf.reaction:
578
+ asyncio.create_task(react(session, "📸"))
579
+
580
+ core = get_hyw_core()
581
+ screenshot_results = await core.screenshot_batch(urls_to_screenshot)
582
+
583
+ images = [Image(src=f'data:image/jpeg;base64,{b64}') for b64 in screenshot_results if b64]
584
+
585
+ if images:
586
+ msg_chain = MessageChain(images)
587
+ if conf.quote:
588
+ msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
589
+ await session.send(msg_chain)
590
+ else:
591
+ await session.send("截图失败")
592
+
593
+ search_cache.cleanup()
594
+ return
595
+
596
+ # Case 3: Single index fallback
484
597
  idx = parse_single_index(query)
485
598
  if idx is None:
486
599
  # No valid index - show prompt
487
- await session.send("请指定序号 (1-10)")
600
+ await session.send("请指定序号,如: /w 1 或 /w 2、3")
488
601
  search_cache.cleanup() # Lazy cleanup
489
602
  return
490
-
603
+
491
604
  if idx >= len(cached.results):
492
605
  await session.send(f"序号超出范围 (1-{len(cached.results)})")
493
606
  search_cache.cleanup()
494
607
  return
495
-
496
- # Screenshot the cached URL
608
+
609
+ # Screenshot the cached URL - check if already cached first
497
610
  target_result = cached.results[idx]
498
611
  target_url = target_result.get("url", "")
612
+ screenshot_cache_id = target_result.get("screenshot_cache_id")
613
+
499
614
  if not target_url:
500
615
  await session.send("该结果无有效URL")
501
616
  search_cache.cleanup()
502
617
  return
503
-
504
- if conf.reaction:
505
- asyncio.create_task(react(session, "📸"))
506
-
507
- core = get_hyw_core()
508
- b64_img = await core.screenshot(target_url)
509
-
618
+
619
+ # Try to get from cache first
620
+ b64_img = None
621
+ if screenshot_cache_id:
622
+ b64_img = search_cache.get_screenshot(screenshot_cache_id)
623
+ if b64_img:
624
+ logger.info(f"/w using cached screenshot: {screenshot_cache_id}")
625
+
626
+ # Fetch if not in cache
627
+ if not b64_img:
628
+ if conf.reaction:
629
+ asyncio.create_task(react(session, "📸"))
630
+
631
+ core = get_hyw_core()
632
+ b64_img = await core.screenshot(target_url)
633
+ else:
634
+ if conf.reaction:
635
+ asyncio.create_task(react(session, "✨"))
636
+
510
637
  if b64_img:
511
638
  msg_chain = MessageChain(Image(src=f'data:image/jpeg;base64,{b64_img}'))
512
639
  if conf.quote:
@@ -514,7 +641,7 @@ async def handle_web_command(session: Session[MessageCreatedEvent], result: Arpa
514
641
  await session.send(msg_chain)
515
642
  else:
516
643
  await session.send(f"截图失败: {target_url}")
517
-
644
+
518
645
  search_cache.cleanup()
519
646
  return
520
647
  else:
@@ -6,9 +6,13 @@ deep query operations on search results.
6
6
  """
7
7
 
8
8
  import time
9
+ import base64
10
+ import io
9
11
  from dataclasses import dataclass, field
10
12
  from typing import Dict, List, Any, Optional
11
13
 
14
+ from loguru import logger
15
+
12
16
 
13
17
  @dataclass
14
18
  class CacheEntry:
@@ -18,21 +22,31 @@ class CacheEntry:
18
22
  timestamp: float = field(default_factory=time.time)
19
23
 
20
24
 
25
+ @dataclass
26
+ class ScreenshotCacheEntry:
27
+ """A cached full screenshot."""
28
+ screenshot_b64: str
29
+ url: str
30
+ timestamp: float = field(default_factory=time.time)
31
+
32
+
21
33
  class SearchResultCache:
22
34
  """
23
35
  In-memory cache for search results with TTL-based expiration.
24
-
36
+
25
37
  Cleanup is lazy - performed at the end of each request.
26
38
  """
27
-
39
+
28
40
  def __init__(self, ttl_seconds: float = 600.0): # 10 minutes default
29
41
  self._cache: Dict[str, CacheEntry] = {}
42
+ self._screenshot_cache: Dict[str, ScreenshotCacheEntry] = {} # screenshot_id -> full screenshot
43
+ self._screenshot_counter: int = 0
30
44
  self.ttl_seconds = ttl_seconds
31
-
45
+
32
46
  def store(self, message_id: str, results: List[Dict[str, Any]], query: str):
33
47
  """
34
48
  Store search results associated with a message ID.
35
-
49
+
36
50
  Args:
37
51
  message_id: The sent message ID that contains the search results image
38
52
  results: List of search result dicts with url, title, content, etc.
@@ -43,28 +57,65 @@ class SearchResultCache:
43
57
  query=query,
44
58
  timestamp=time.time()
45
59
  )
46
-
60
+
47
61
  def get(self, message_id: str) -> Optional[CacheEntry]:
48
62
  """
49
63
  Get cached search results for a message ID.
50
-
64
+
51
65
  Returns None if not found or expired.
52
66
  """
53
67
  entry = self._cache.get(message_id)
54
68
  if entry is None:
55
69
  return None
56
-
70
+
57
71
  # Check expiration
58
72
  if time.time() - entry.timestamp > self.ttl_seconds:
59
73
  del self._cache[message_id]
60
74
  return None
61
-
75
+
62
76
  return entry
63
-
77
+
78
+ def store_screenshot(self, screenshot_b64: str, url: str) -> str:
79
+ """
80
+ Store a full screenshot and return its cache ID.
81
+
82
+ Args:
83
+ screenshot_b64: Base64 encoded full screenshot
84
+ url: The URL that was screenshotted
85
+
86
+ Returns:
87
+ A short cache ID for referencing this screenshot
88
+ """
89
+ self._screenshot_counter += 1
90
+ cache_id = f"ss{self._screenshot_counter:04x}"
91
+ self._screenshot_cache[cache_id] = ScreenshotCacheEntry(
92
+ screenshot_b64=screenshot_b64,
93
+ url=url,
94
+ timestamp=time.time()
95
+ )
96
+ return cache_id
97
+
98
+ def get_screenshot(self, cache_id: str) -> Optional[str]:
99
+ """
100
+ Get a cached full screenshot by ID.
101
+
102
+ Returns:
103
+ Base64 encoded screenshot or None if not found/expired
104
+ """
105
+ entry = self._screenshot_cache.get(cache_id)
106
+ if entry is None:
107
+ return None
108
+
109
+ if time.time() - entry.timestamp > self.ttl_seconds:
110
+ del self._screenshot_cache[cache_id]
111
+ return None
112
+
113
+ return entry.screenshot_b64
114
+
64
115
  def cleanup(self):
65
116
  """
66
117
  Remove all expired entries.
67
-
118
+
68
119
  Called lazily at the end of each request.
69
120
  """
70
121
  now = time.time()
@@ -74,11 +125,59 @@ class SearchResultCache:
74
125
  ]
75
126
  for k in expired_keys:
76
127
  del self._cache[k]
77
-
128
+
129
+ # Also cleanup screenshot cache
130
+ expired_ss = [
131
+ k for k, v in self._screenshot_cache.items()
132
+ if now - v.timestamp > self.ttl_seconds
133
+ ]
134
+ for k in expired_ss:
135
+ del self._screenshot_cache[k]
136
+
78
137
  def __len__(self) -> int:
79
138
  return len(self._cache)
80
139
 
81
140
 
141
+ def crop_to_square_thumbnail(screenshot_b64: str, max_size: int = 400) -> Optional[str]:
142
+ """
143
+ Crop a screenshot to a 1:1 square from the top and resize.
144
+
145
+ Args:
146
+ screenshot_b64: Base64 encoded image
147
+ max_size: Maximum dimension of the output square
148
+
149
+ Returns:
150
+ Base64 encoded cropped/resized image, or None on error
151
+ """
152
+ try:
153
+ from PIL import Image
154
+
155
+ # Decode base64
156
+ img_data = base64.b64decode(screenshot_b64)
157
+ img = Image.open(io.BytesIO(img_data))
158
+
159
+ width, height = img.size
160
+
161
+ # Crop to square from top
162
+ square_size = min(width, height)
163
+ # Crop from top-left, taking full width if width < height
164
+ crop_box = (0, 0, square_size, square_size)
165
+ cropped = img.crop(crop_box)
166
+
167
+ # Resize if larger than max_size
168
+ if square_size > max_size:
169
+ cropped = cropped.resize((max_size, max_size), Image.Resampling.LANCZOS)
170
+
171
+ # Encode back to base64
172
+ buffer = io.BytesIO()
173
+ cropped.save(buffer, format='JPEG', quality=85)
174
+ return base64.b64encode(buffer.getvalue()).decode('utf-8')
175
+
176
+ except Exception as e:
177
+ logger.warning(f"Failed to crop screenshot: {e}")
178
+ return None
179
+
180
+
82
181
  def parse_single_index(text: str) -> Optional[int]:
83
182
  """
84
183
  Parse a single index from text like "1" or "3".
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entari_plugin_hyw
3
- Version: 4.0.0rc16
3
+ Version: 4.0.0rc17
4
4
  Summary: Use large language models to interpret chat messages
5
5
  Author-email: kumoSleeping <zjr2992@outlook.com>
6
6
  License: MIT
@@ -2,7 +2,7 @@
2
2
  Agent Pipeline
3
3
 
4
4
  Tool-calling agent that can autonomously use web_tool to search/screenshot.
5
- Maximum 2 rounds of tool calls, up to 3 parallel calls per round.
5
+ Maximum 3 rounds of tool calls, up to 3 parallel calls per round.
6
6
  """
7
7
 
8
8
  import asyncio
@@ -53,8 +53,8 @@ class AgentSession:
53
53
 
54
54
  @property
55
55
  def should_force_summary(self) -> bool:
56
- """Force summary after 2 rounds of tool calls."""
57
- return self.round_count >= 2
56
+ """Force summary after 3 rounds of tool calls."""
57
+ return self.round_count >= 3
58
58
 
59
59
 
60
60
  def parse_filter_syntax(query: str, max_count: int = 3):
@@ -135,11 +135,11 @@ class AgentPipeline:
135
135
  Flow:
136
136
  1. 用户输入 → LLM (with tools)
137
137
  2. If tool_call: execute all tools in parallel → notify user with batched message → loop
138
- 3. If call_count >= 2 rounds: force summary on next call
138
+ 3. If call_count >= 3 rounds: force summary on next call
139
139
  4. Return final content
140
140
  """
141
141
 
142
- MAX_TOOL_ROUNDS = 2 # Maximum rounds of tool calls
142
+ MAX_TOOL_ROUNDS = 3 # Maximum rounds of tool calls
143
143
  MAX_PARALLEL_TOOLS = 3 # Maximum parallel tool calls per round
144
144
  MAX_LLM_RETRIES = 3 # Maximum retries for empty API responses
145
145
  LLM_RETRY_DELAY = 1.0 # Delay between retries in seconds
@@ -243,7 +243,7 @@ class AgentPipeline:
243
243
  # Send initial status notification
244
244
  if self.send_func:
245
245
  try:
246
- await self.send_func("💭 正在理解用户意图...")
246
+ await self.send_func("💭 何意味...")
247
247
  except Exception as e:
248
248
  logger.warning(f"AgentPipeline: Failed to send initial notification: {e}")
249
249