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.
- {entari_plugin_hyw-4.0.0rc16/src/entari_plugin_hyw.egg-info → entari_plugin_hyw-4.0.0rc17}/PKG-INFO +1 -1
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/pyproject.toml +1 -1
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/__init__.py +142 -15
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/search_cache.py +110 -11
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17/src/entari_plugin_hyw.egg-info}/PKG-INFO +1 -1
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/agent.py +6 -6
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/index.html +23 -23
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/core.py +13 -21
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/definitions.py +17 -3
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/MANIFEST.in +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/README.md +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/setup.cfg +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/filters.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/history.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/misc.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw.egg-info/SOURCES.txt +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw.egg-info/dependency_links.txt +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw.egg-info/requires.txt +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw.egg-info/top_level.txt +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/__init__.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/assets/card-dist/vite.svg +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/engines/__init__.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/engines/base.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/engines/default.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/engines/duckduckgo.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/landing.html +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/manager.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/renderer.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/browser_control/service.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/config.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/crawling/__init__.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/crawling/completeness.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/crawling/models.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/image_cache.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/pipeline.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/search.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/stages/__init__.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/stages/base.py +0 -0
- {entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/hyw_core/stages/summary.py +0 -0
|
@@ -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-
|
|
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 = [
|
{entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/__init__.py
RENAMED
|
@@ -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
|
-
#
|
|
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("
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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:
|
{entari_plugin_hyw-4.0.0rc16 → entari_plugin_hyw-4.0.0rc17}/src/entari_plugin_hyw/search_cache.py
RENAMED
|
@@ -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".
|
|
@@ -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
|
|
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
|
|
57
|
-
return self.round_count >=
|
|
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 >=
|
|
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 =
|
|
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
|
|