entari-plugin-hyw 4.0.0rc11__py3-none-any.whl → 4.0.0rc13__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 +175 -133
- entari_plugin_hyw/filters.py +83 -0
- entari_plugin_hyw/misc.py +42 -0
- {entari_plugin_hyw-4.0.0rc11.dist-info → entari_plugin_hyw-4.0.0rc13.dist-info}/METADATA +1 -1
- {entari_plugin_hyw-4.0.0rc11.dist-info → entari_plugin_hyw-4.0.0rc13.dist-info}/RECORD +13 -11
- hyw_core/agent.py +705 -0
- hyw_core/browser_control/service.py +75 -11
- hyw_core/core.py +148 -1
- hyw_core/definitions.py +73 -52
- hyw_core/search.py +10 -0
- hyw_core/stages/summary.py +1 -3
- {entari_plugin_hyw-4.0.0rc11.dist-info → entari_plugin_hyw-4.0.0rc13.dist-info}/WHEEL +0 -0
- {entari_plugin_hyw-4.0.0rc11.dist-info → entari_plugin_hyw-4.0.0rc13.dist-info}/top_level.txt +0 -0
entari_plugin_hyw/__init__.py
CHANGED
|
@@ -7,10 +7,8 @@ Use large language models to interpret chat messages.
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from importlib.metadata import version as get_version
|
|
9
9
|
from typing import List, Dict, Any, Optional
|
|
10
|
-
import time
|
|
11
10
|
import asyncio
|
|
12
11
|
import os
|
|
13
|
-
import secrets
|
|
14
12
|
import base64
|
|
15
13
|
import re
|
|
16
14
|
import tempfile
|
|
@@ -41,126 +39,21 @@ from .misc import (
|
|
|
41
39
|
resolve_model_name,
|
|
42
40
|
render_refuse_answer,
|
|
43
41
|
render_image_unsupported,
|
|
42
|
+
parse_color,
|
|
43
|
+
RecentEventDeduper,
|
|
44
44
|
)
|
|
45
|
+
from .filters import parse_filter_syntax
|
|
45
46
|
from .search_cache import SearchResultCache, parse_single_index, parse_multi_indices
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
def parse_filter_syntax(query: str, max_count: int = 3):
|
|
49
|
-
"""
|
|
50
|
-
Parse enhanced filter syntax supporting:
|
|
51
|
-
- Chinese/English colons (: :) and commas (, ,)
|
|
52
|
-
- Multiple filters: "mcmod=2, github=1 : xxx"
|
|
53
|
-
- Index lists: "1, 2, 3 : xxx"
|
|
54
|
-
- Max total selections
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
(filters, search_query, error_msg)
|
|
58
|
-
filters: list of (filter_type, filter_value, count) tuples
|
|
59
|
-
filter_type: 'index' or 'link'
|
|
60
|
-
count: how many to get (default 1)
|
|
61
|
-
search_query: the actual search query
|
|
62
|
-
error_msg: error message if exceeded max
|
|
63
|
-
"""
|
|
64
|
-
if not query:
|
|
65
|
-
return [], query, None
|
|
66
|
-
|
|
67
|
-
# Normalize Chinese punctuation to English
|
|
68
|
-
normalized = query.replace(':', ':').replace(',', ',').replace('、', ',')
|
|
69
|
-
|
|
70
|
-
# Handle escaped colons: \: or /: -> placeholder
|
|
71
|
-
normalized = re.sub(r'[/\\]:', '\x00COLON\x00', normalized)
|
|
72
|
-
|
|
73
|
-
# Split by colon - last part is the search query
|
|
74
|
-
parts = normalized.split(':')
|
|
75
|
-
if len(parts) < 2:
|
|
76
|
-
# No colon found, restore escaped colons and return as-is
|
|
77
|
-
return [], query.replace('\\:', ':').replace('/:', ':'), None
|
|
78
|
-
|
|
79
|
-
# Everything after the last colon is the search query
|
|
80
|
-
search_query = parts[-1].strip().replace('\x00COLON\x00', ':')
|
|
81
|
-
|
|
82
|
-
# Everything before is the filter specification
|
|
83
|
-
filter_spec = ':'.join(parts[:-1]).strip().replace('\x00COLON\x00', ':')
|
|
84
|
-
|
|
85
|
-
if not filter_spec or not search_query:
|
|
86
|
-
return [], query.replace('\\:', ':').replace('/:', ':'), None
|
|
87
|
-
|
|
88
|
-
# Parse filter specifications (comma-separated)
|
|
89
|
-
filter_items = [f.strip() for f in filter_spec.split(',') if f.strip()]
|
|
90
|
-
|
|
91
|
-
filters = []
|
|
92
|
-
for item in filter_items:
|
|
93
|
-
# Check for "filter=count" pattern (e.g., "mcmod=2")
|
|
94
|
-
eq_match = re.match(r'^(\w+)\s*=\s*(\d+)$', item)
|
|
95
|
-
if eq_match:
|
|
96
|
-
filter_name = eq_match.group(1).lower()
|
|
97
|
-
count = int(eq_match.group(2))
|
|
98
|
-
filters.append(('link', filter_name, count))
|
|
99
|
-
elif item.isdigit():
|
|
100
|
-
# Pure index
|
|
101
|
-
filters.append(('index', int(item), 1))
|
|
102
|
-
else:
|
|
103
|
-
# Filter name without count (default count=1)
|
|
104
|
-
filters.append(('link', item.lower(), 1))
|
|
105
|
-
|
|
106
|
-
# Calculate total count
|
|
107
|
-
total = sum(f[2] for f in filters)
|
|
108
|
-
if total > max_count:
|
|
109
|
-
return [], search_query, f"最多选择{max_count}个结果 (当前选择了{total}个)"
|
|
110
|
-
|
|
111
|
-
# Append filter names to search query
|
|
112
|
-
# Extract filter names (only 'link' type, skip 'index' type)
|
|
113
|
-
filter_names = [f[1] for f in filters if f[0] == 'link']
|
|
114
|
-
if filter_names:
|
|
115
|
-
# Append filter names to search query: "search_query filter1 filter2"
|
|
116
|
-
search_query = f"{search_query} {' '.join(filter_names)}"
|
|
117
|
-
|
|
118
|
-
return filters, search_query, None
|
|
119
|
-
|
|
120
|
-
|
|
121
49
|
try:
|
|
122
50
|
__version__ = get_version("entari_plugin_hyw")
|
|
123
51
|
except Exception:
|
|
124
52
|
__version__ = "4.0.0-rc8"
|
|
125
53
|
|
|
126
54
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return "#ef4444"
|
|
130
|
-
color = str(color).strip()
|
|
131
|
-
if color.startswith('#') and len(color) in [4, 7]:
|
|
132
|
-
return color
|
|
133
|
-
if re.match(r'^[0-9a-fA-F]{6}$', color):
|
|
134
|
-
return f'#{color}'
|
|
135
|
-
rgb_match = re.match(r'^\(?(\d+)[,\s]+(\d+)[,\s]+(\d+)\)?$', color)
|
|
136
|
-
if rgb_match:
|
|
137
|
-
r, g, b = (max(0, min(255, int(x))) for x in rgb_match.groups())
|
|
138
|
-
return f'#{r:02x}{g:02x}{b:02x}'
|
|
139
|
-
return "#ef4444"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
class _RecentEventDeduper:
|
|
143
|
-
def __init__(self, ttl_seconds: float = 30.0, max_size: int = 2048):
|
|
144
|
-
self.ttl_seconds = ttl_seconds
|
|
145
|
-
self.max_size = max_size
|
|
146
|
-
self._seen: Dict[str, float] = {}
|
|
147
|
-
|
|
148
|
-
def seen_recently(self, key: str) -> bool:
|
|
149
|
-
now = time.time()
|
|
150
|
-
if len(self._seen) > self.max_size:
|
|
151
|
-
self._prune(now)
|
|
152
|
-
ts = self._seen.get(key)
|
|
153
|
-
if ts is None or now - ts > self.ttl_seconds:
|
|
154
|
-
self._seen[key] = now
|
|
155
|
-
return False
|
|
156
|
-
return True
|
|
157
|
-
|
|
158
|
-
def _prune(self, now: float):
|
|
159
|
-
expired = [k for k, ts in self._seen.items() if now - ts > self.ttl_seconds]
|
|
160
|
-
for k in expired:
|
|
161
|
-
self._seen.pop(k, None)
|
|
162
|
-
|
|
163
|
-
_event_deduper = _RecentEventDeduper()
|
|
55
|
+
_event_deduper = RecentEventDeduper()
|
|
56
|
+
|
|
164
57
|
|
|
165
58
|
|
|
166
59
|
@dataclass
|
|
@@ -170,6 +63,7 @@ class HywConfig(BasicConfModel):
|
|
|
170
63
|
models: List[Dict[str, Any]] = field(default_factory=list)
|
|
171
64
|
question_command: str = "/q"
|
|
172
65
|
web_command: str = "/w"
|
|
66
|
+
help_command: str = "/h"
|
|
173
67
|
language: str = "Simplified Chinese"
|
|
174
68
|
temperature: float = 0.4
|
|
175
69
|
|
|
@@ -326,9 +220,9 @@ async def process_request(
|
|
|
326
220
|
output_path = tf.name
|
|
327
221
|
|
|
328
222
|
core = get_hyw_core()
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
response = await core.
|
|
223
|
+
# Use agent mode with tool-calling capability
|
|
224
|
+
# Agent can autonomously call web_tool up to 2 times, with IM notifications
|
|
225
|
+
response = await core.query_agent(request, output_path=None)
|
|
332
226
|
|
|
333
227
|
# 2. Get the warmed-up tab
|
|
334
228
|
try:
|
|
@@ -453,7 +347,6 @@ async def handle_question_command(session: Session[MessageCreatedEvent], result:
|
|
|
453
347
|
# Check if too many indices requested (parse_multi_indices returns None if > max_count)
|
|
454
348
|
if query_text and indices is None:
|
|
455
349
|
# Check if it looks like indices but exceeded limit
|
|
456
|
-
import re
|
|
457
350
|
if re.match(r'^[\d,、\s\-–]+$', query_text):
|
|
458
351
|
await session.send("最多选择3个结果进行总结")
|
|
459
352
|
search_cache.cleanup()
|
|
@@ -551,8 +444,91 @@ async def handle_question_command(session: Session[MessageCreatedEvent], result:
|
|
|
551
444
|
search_cache.cleanup()
|
|
552
445
|
return
|
|
553
446
|
|
|
447
|
+
# === URL Mode: Direct URL Screenshot + Summarize ===
|
|
448
|
+
# Detect URL in query and handle directly without Agent
|
|
449
|
+
url_match = re.search(r'https?://\S+', query_text)
|
|
450
|
+
if url_match:
|
|
451
|
+
url = url_match.group(0)
|
|
452
|
+
# Extract user intent (text before/after URL)
|
|
453
|
+
user_intent = query_text.replace(url, '').strip()
|
|
454
|
+
if not user_intent:
|
|
455
|
+
user_intent = "总结这个页面的内容"
|
|
456
|
+
|
|
457
|
+
if conf.reaction:
|
|
458
|
+
asyncio.create_task(react(session, "✨"))
|
|
459
|
+
|
|
460
|
+
core = get_hyw_core()
|
|
461
|
+
local_renderer = await get_content_renderer()
|
|
462
|
+
|
|
463
|
+
# Run screenshot and prepare tab in parallel
|
|
464
|
+
screenshot_task = asyncio.create_task(core.screenshot(url))
|
|
465
|
+
tab_task = asyncio.create_task(local_renderer.prepare_tab())
|
|
466
|
+
|
|
467
|
+
# Send notification
|
|
468
|
+
short_url = url[:40] + "..." if len(url) > 40 else url
|
|
469
|
+
await session.send(f"📸 正在截图: {short_url}")
|
|
470
|
+
|
|
471
|
+
b64_img = await screenshot_task
|
|
472
|
+
|
|
473
|
+
if not b64_img:
|
|
474
|
+
try: await tab_task
|
|
475
|
+
except: pass
|
|
476
|
+
await session.send(f"❌ 截图失败: {url}")
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
# Summarize with screenshot
|
|
480
|
+
request = QueryRequest(
|
|
481
|
+
user_input=f"{user_intent}\n\nURL: {url}",
|
|
482
|
+
images=[b64_img],
|
|
483
|
+
conversation_history=[],
|
|
484
|
+
model_name=None,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
488
|
+
output_path = tf.name
|
|
489
|
+
|
|
490
|
+
response = await core.query(request, output_path=None)
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
tab_id = await tab_task
|
|
494
|
+
except Exception:
|
|
495
|
+
tab_id = None
|
|
496
|
+
|
|
497
|
+
if response.success and response.content:
|
|
498
|
+
render_ok = await core.render(
|
|
499
|
+
markdown_content=response.content,
|
|
500
|
+
output_path=output_path,
|
|
501
|
+
stats={"total_time": response.total_time},
|
|
502
|
+
references=[],
|
|
503
|
+
page_references=[{"url": url, "title": "截图页面", "raw_screenshot_b64": b64_img}],
|
|
504
|
+
tab_id=tab_id
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
if render_ok and os.path.exists(output_path):
|
|
508
|
+
with open(output_path, "rb") as f:
|
|
509
|
+
img_data = base64.b64encode(f.read()).decode()
|
|
510
|
+
|
|
511
|
+
msg_chain = MessageChain(Image(src=f'data:image/png;base64,{img_data}'))
|
|
512
|
+
if conf.quote:
|
|
513
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
514
|
+
|
|
515
|
+
await session.send(msg_chain)
|
|
516
|
+
os.remove(output_path)
|
|
517
|
+
else:
|
|
518
|
+
await session.send(response.content[:500])
|
|
519
|
+
else:
|
|
520
|
+
await session.send(f"总结失败: {response.error or 'Unknown error'}")
|
|
521
|
+
|
|
522
|
+
return
|
|
523
|
+
|
|
554
524
|
# === Filter Mode: Search + Find matching links + Summarize ===
|
|
555
|
-
|
|
525
|
+
# Only trigger filter syntax for short queries (≤20 chars, excluding URLs), otherwise use Agent
|
|
526
|
+
filters = []
|
|
527
|
+
filter_error = None
|
|
528
|
+
# Calculate length excluding URLs
|
|
529
|
+
query_without_urls = re.sub(r'https?://\S+', '', query_text).strip()
|
|
530
|
+
if len(query_without_urls) <= 20:
|
|
531
|
+
filters, search_query, filter_error = parse_filter_syntax(query_text, max_count=3)
|
|
556
532
|
|
|
557
533
|
if filter_error:
|
|
558
534
|
await session.send(filter_error)
|
|
@@ -565,6 +541,10 @@ async def handle_question_command(session: Session[MessageCreatedEvent], result:
|
|
|
565
541
|
core = get_hyw_core()
|
|
566
542
|
local_renderer = await get_content_renderer()
|
|
567
543
|
|
|
544
|
+
# Send pre-notification BEFORE search
|
|
545
|
+
filter_desc = ", ".join([f[1] if f[0] != 'index' else f"第{f[1]}个" for f in filters])
|
|
546
|
+
await session.send(f"🔍 正在搜索 \"{search_query}\" 并匹配 [{filter_desc}]...")
|
|
547
|
+
|
|
568
548
|
# Run search and prepare tab in parallel
|
|
569
549
|
search_task = asyncio.create_task(core.search([search_query]))
|
|
570
550
|
tab_task = asyncio.create_task(local_renderer.prepare_tab())
|
|
@@ -592,13 +572,15 @@ async def handle_question_command(session: Session[MessageCreatedEvent], result:
|
|
|
592
572
|
else:
|
|
593
573
|
try: await tab_task
|
|
594
574
|
except: pass
|
|
595
|
-
await session.send(f"序号 {filter_value} 超出范围 (1-{len(visible)})")
|
|
575
|
+
await session.send(f"⚠️ 序号 {filter_value} 超出范围 (1-{len(visible)})")
|
|
596
576
|
return
|
|
597
577
|
else:
|
|
598
578
|
found_count = 0
|
|
599
579
|
for res in visible:
|
|
600
580
|
url = res.get("url", "")
|
|
601
|
-
|
|
581
|
+
title = res.get("title", "")
|
|
582
|
+
# Match filter against both URL and title
|
|
583
|
+
if (filter_value in url.lower() or filter_value in title.lower()) and url not in urls_to_screenshot:
|
|
602
584
|
urls_to_screenshot.append(url)
|
|
603
585
|
found_count += 1
|
|
604
586
|
if found_count >= count:
|
|
@@ -607,32 +589,54 @@ async def handle_question_command(session: Session[MessageCreatedEvent], result:
|
|
|
607
589
|
if found_count == 0:
|
|
608
590
|
try: await tab_task
|
|
609
591
|
except: pass
|
|
610
|
-
await session.send(f"未找到包含 \"{filter_value}\" 的链接")
|
|
592
|
+
await session.send(f"⚠️ 未找到包含 \"{filter_value}\" 的链接")
|
|
611
593
|
return
|
|
612
594
|
|
|
613
595
|
if not urls_to_screenshot:
|
|
614
596
|
try: await tab_task
|
|
615
597
|
except: pass
|
|
616
|
-
await session.send("未找到匹配的链接")
|
|
598
|
+
await session.send("⚠️ 未找到匹配的链接")
|
|
617
599
|
return
|
|
618
600
|
|
|
619
|
-
# Take screenshots
|
|
620
|
-
screenshot_tasks = [core.
|
|
601
|
+
# Take screenshots with content extraction
|
|
602
|
+
screenshot_tasks = [core.screenshot_with_content(url) for url in urls_to_screenshot]
|
|
621
603
|
screenshot_results = await asyncio.gather(*screenshot_tasks)
|
|
622
|
-
screenshots = [b64 for b64 in screenshot_results if b64]
|
|
623
604
|
|
|
624
|
-
|
|
605
|
+
# Build page references for rendering (with screenshots)
|
|
606
|
+
# and collect text content for LLM
|
|
607
|
+
page_references = []
|
|
608
|
+
text_contents = []
|
|
609
|
+
successful_count = 0
|
|
610
|
+
|
|
611
|
+
for url, result in zip(urls_to_screenshot, screenshot_results):
|
|
612
|
+
if isinstance(result, dict) and result.get("screenshot_b64"):
|
|
613
|
+
successful_count += 1
|
|
614
|
+
page_references.append({
|
|
615
|
+
"url": url,
|
|
616
|
+
"title": result.get("title", "截图页面"),
|
|
617
|
+
"raw_screenshot_b64": result.get("screenshot_b64"),
|
|
618
|
+
})
|
|
619
|
+
# Collect text for LLM
|
|
620
|
+
content = result.get("content", "")
|
|
621
|
+
if content:
|
|
622
|
+
text_contents.append(f"## 来源: {result.get('title', url)}\n\n{content}")
|
|
623
|
+
|
|
624
|
+
if not page_references:
|
|
625
625
|
try: await tab_task
|
|
626
626
|
except: pass
|
|
627
627
|
await session.send("无法截图页面")
|
|
628
628
|
return
|
|
629
629
|
|
|
630
|
-
#
|
|
631
|
-
|
|
630
|
+
# Send result notification
|
|
631
|
+
await session.send(f"🔍 搜索 \"{search_query}\" 并截图 {successful_count} 个匹配结果")
|
|
632
|
+
|
|
633
|
+
# Pass TEXT content to LLM for summarization (not images)
|
|
634
|
+
combined_content = "\n\n---\n\n".join(text_contents) if text_contents else "无法提取网页内容"
|
|
635
|
+
user_query = f"总结关于 \"{search_query}\" 的内容。\n\n网页内容:\n{combined_content[:8000]}"
|
|
632
636
|
|
|
633
637
|
request = QueryRequest(
|
|
634
638
|
user_input=user_query,
|
|
635
|
-
images=
|
|
639
|
+
images=[], # No images, use text content instead
|
|
636
640
|
conversation_history=[],
|
|
637
641
|
model_name=None,
|
|
638
642
|
)
|
|
@@ -653,7 +657,7 @@ async def handle_question_command(session: Session[MessageCreatedEvent], result:
|
|
|
653
657
|
output_path=output_path,
|
|
654
658
|
stats={"total_time": response.total_time},
|
|
655
659
|
references=[],
|
|
656
|
-
page_references=
|
|
660
|
+
page_references=page_references, # Pass screenshots for rendering
|
|
657
661
|
tab_id=tab_id
|
|
658
662
|
)
|
|
659
663
|
|
|
@@ -850,25 +854,27 @@ async def handle_web_command(session: Session[MessageCreatedEvent], result: Arpa
|
|
|
850
854
|
if url and url not in urls_to_screenshot:
|
|
851
855
|
urls_to_screenshot.append(url)
|
|
852
856
|
else:
|
|
853
|
-
await session.send(f"序号 {filter_value} 超出范围 (1-{len(visible)})")
|
|
857
|
+
await session.send(f"⚠️ 序号 {filter_value} 超出范围 (1-{len(visible)})")
|
|
854
858
|
return
|
|
855
859
|
else:
|
|
856
860
|
# Link filter: find URLs containing filter term
|
|
857
861
|
found_count = 0
|
|
858
862
|
for res in visible:
|
|
859
863
|
url = res.get("url", "")
|
|
860
|
-
|
|
864
|
+
title = res.get("title", "")
|
|
865
|
+
# Match filter against both URL and title
|
|
866
|
+
if (filter_value in url.lower() or filter_value in title.lower()) and url not in urls_to_screenshot:
|
|
861
867
|
urls_to_screenshot.append(url)
|
|
862
868
|
found_count += 1
|
|
863
869
|
if found_count >= count:
|
|
864
870
|
break
|
|
865
871
|
|
|
866
872
|
if found_count == 0:
|
|
867
|
-
await session.send(f"未找到包含 \"{filter_value}\" 的链接")
|
|
873
|
+
await session.send(f"⚠️ 未找到包含 \"{filter_value}\" 的链接")
|
|
868
874
|
return
|
|
869
875
|
|
|
870
876
|
if not urls_to_screenshot:
|
|
871
|
-
await session.send("未找到匹配的链接")
|
|
877
|
+
await session.send("⚠️ 未找到匹配的链接")
|
|
872
878
|
return
|
|
873
879
|
|
|
874
880
|
if conf.reaction:
|
|
@@ -958,6 +964,42 @@ async def handle_web_command(session: Session[MessageCreatedEvent], result: Arpa
|
|
|
958
964
|
|
|
959
965
|
metadata("hyw", author=[{"name": "kumoSleeping", "email": "zjr2992@outlook.com"}], version=__version__, config=HywConfig)
|
|
960
966
|
|
|
967
|
+
# Help command (/h)
|
|
968
|
+
alc_help = Alconna(conf.help_command)
|
|
969
|
+
|
|
970
|
+
@command.on(alc_help)
|
|
971
|
+
async def handle_help_command(session: Session[MessageCreatedEvent], result: Arparma):
|
|
972
|
+
"""Display help information for all commands."""
|
|
973
|
+
help_text = f"""HYW Plugin v{__version__}
|
|
974
|
+
|
|
975
|
+
Question Agent:
|
|
976
|
+
• {conf.question_command} tell me...
|
|
977
|
+
• {conf.question_command} [picture] tell me...
|
|
978
|
+
• [quote] {conf.question_command} tell me...
|
|
979
|
+
Question Filter:
|
|
980
|
+
• {conf.question_command} github: fastapi
|
|
981
|
+
• {conf.question_command} 1,2: minecraft
|
|
982
|
+
• {conf.question_command} mcmod=2: forge mod
|
|
983
|
+
Question Context:
|
|
984
|
+
• [quote: question] + {conf.question_command} tell me more...
|
|
985
|
+
Web_tool Search:
|
|
986
|
+
• {conf.web_command} query
|
|
987
|
+
Web_tool Screenshot:
|
|
988
|
+
• {conf.web_command} https://example.com
|
|
989
|
+
Web_tool Filter(search and screenshot):
|
|
990
|
+
• {conf.web_command} github: fastapi
|
|
991
|
+
• {conf.web_command} 1,2: minecraft
|
|
992
|
+
• {conf.web_command} mcmod=2: forge mod
|
|
993
|
+
Web_tool Context(screenshot):
|
|
994
|
+
• [quote: web_tool search] + {conf.web_command} 1
|
|
995
|
+
• [quote: web_tool search] + {conf.web_command} 1, 3
|
|
996
|
+
Web_tool Context(question):
|
|
997
|
+
• [quote: web_tool screenshot] + {conf.question_command} tell me...
|
|
998
|
+
• [quote: web_tool search] + {conf.question_command} tell me...
|
|
999
|
+
"""
|
|
1000
|
+
|
|
1001
|
+
await session.send(help_text)
|
|
1002
|
+
|
|
961
1003
|
@listen(CommandReceive)
|
|
962
1004
|
async def remove_at(content: MessageChain):
|
|
963
1005
|
return content.lstrip(At)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filter syntax parsing utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import List, Tuple, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_filter_syntax(query: str, max_count: int = 3):
|
|
10
|
+
"""
|
|
11
|
+
Parse enhanced filter syntax supporting:
|
|
12
|
+
- Chinese/English colons (: :) and commas (, ,)
|
|
13
|
+
- Multiple filters: "mcmod=2, github=1 : xxx"
|
|
14
|
+
- Index lists: "1, 2, 3 : xxx"
|
|
15
|
+
- Max total selections
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
(filters, search_query, error_msg)
|
|
19
|
+
filters: list of (filter_type, filter_value, count) tuples
|
|
20
|
+
filter_type: 'index' or 'link'
|
|
21
|
+
count: how many to get (default 1)
|
|
22
|
+
search_query: the actual search query
|
|
23
|
+
error_msg: error message if exceeded max
|
|
24
|
+
"""
|
|
25
|
+
if not query:
|
|
26
|
+
return [], query, None
|
|
27
|
+
|
|
28
|
+
# Skip filter parsing if query contains URL (has :// pattern)
|
|
29
|
+
if re.search(r'https?://', query):
|
|
30
|
+
return [], query.strip(), None
|
|
31
|
+
|
|
32
|
+
# Normalize Chinese punctuation to English
|
|
33
|
+
normalized = query.replace(':', ':').replace(',', ',').replace('、', ',')
|
|
34
|
+
|
|
35
|
+
# Handle escaped colons: \: or /: -> placeholder
|
|
36
|
+
normalized = re.sub(r'[/\\]:', '\x00COLON\x00', normalized)
|
|
37
|
+
|
|
38
|
+
# Split by colon - last part is the search query
|
|
39
|
+
parts = normalized.split(':')
|
|
40
|
+
if len(parts) < 2:
|
|
41
|
+
# No colon found, restore escaped colons and return as-is
|
|
42
|
+
return [], query.replace('\\:', ':').replace('/:', ':'), None
|
|
43
|
+
|
|
44
|
+
# Everything after the last colon is the search query
|
|
45
|
+
search_query = parts[-1].strip().replace('\x00COLON\x00', ':')
|
|
46
|
+
|
|
47
|
+
# Everything before is the filter specification
|
|
48
|
+
filter_spec = ':'.join(parts[:-1]).strip().replace('\x00COLON\x00', ':')
|
|
49
|
+
|
|
50
|
+
if not filter_spec or not search_query:
|
|
51
|
+
return [], query.replace('\\:', ':').replace('/:', ':'), None
|
|
52
|
+
|
|
53
|
+
# Parse filter specifications (comma-separated)
|
|
54
|
+
filter_items = [f.strip() for f in filter_spec.split(',') if f.strip()]
|
|
55
|
+
|
|
56
|
+
filters = []
|
|
57
|
+
for item in filter_items:
|
|
58
|
+
# Check for "filter=count" pattern (e.g., "mcmod=2")
|
|
59
|
+
eq_match = re.match(r'^(\w+)\s*=\s*(\d+)$', item)
|
|
60
|
+
if eq_match:
|
|
61
|
+
filter_name = eq_match.group(1).lower()
|
|
62
|
+
count = int(eq_match.group(2))
|
|
63
|
+
filters.append(('link', filter_name, count))
|
|
64
|
+
elif item.isdigit():
|
|
65
|
+
# Pure index
|
|
66
|
+
filters.append(('index', int(item), 1))
|
|
67
|
+
else:
|
|
68
|
+
# Filter name without count (default count=1)
|
|
69
|
+
filters.append(('link', item.lower(), 1))
|
|
70
|
+
|
|
71
|
+
# Calculate total count
|
|
72
|
+
total = sum(f[2] for f in filters)
|
|
73
|
+
if total > max_count:
|
|
74
|
+
return [], search_query, f"最多选择{max_count}个结果 (当前选择了{total}个)"
|
|
75
|
+
|
|
76
|
+
# Append filter names to search query
|
|
77
|
+
# Extract filter names (only 'link' type, skip 'index' type)
|
|
78
|
+
filter_names = [f[1] for f in filters if f[0] == 'link']
|
|
79
|
+
if filter_names:
|
|
80
|
+
# Append filter names to search query: "search_query filter1 filter2"
|
|
81
|
+
search_query = f"{search_query} {' '.join(filter_names)}"
|
|
82
|
+
|
|
83
|
+
return filters, search_query, None
|
entari_plugin_hyw/misc.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import base64
|
|
3
3
|
import httpx
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
4
6
|
from typing import Dict, Any, List, Optional
|
|
5
7
|
from loguru import logger
|
|
6
8
|
from arclet.entari import MessageChain, Image
|
|
@@ -170,3 +172,43 @@ async def render_image_unsupported(
|
|
|
170
172
|
theme_color=theme_color,
|
|
171
173
|
tab_id=tab_id
|
|
172
174
|
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def parse_color(color: str) -> str:
|
|
178
|
+
"""Parse color string to hex format."""
|
|
179
|
+
if not color:
|
|
180
|
+
return "#ef4444"
|
|
181
|
+
color = str(color).strip()
|
|
182
|
+
if color.startswith('#') and len(color) in [4, 7]:
|
|
183
|
+
return color
|
|
184
|
+
if re.match(r'^[0-9a-fA-F]{6}$', color):
|
|
185
|
+
return f'#{color}'
|
|
186
|
+
rgb_match = re.match(r'^\(?(\d+)[,\s]+(\d+)[,\s]+(\d+)\)?$', color)
|
|
187
|
+
if rgb_match:
|
|
188
|
+
r, g, b = (max(0, min(255, int(x))) for x in rgb_match.groups())
|
|
189
|
+
return f'#{r:02x}{g:02x}{b:02x}'
|
|
190
|
+
return "#ef4444"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class RecentEventDeduper:
|
|
194
|
+
"""Deduplicates recent events based on a key with TTL."""
|
|
195
|
+
|
|
196
|
+
def __init__(self, ttl_seconds: float = 30.0, max_size: int = 2048):
|
|
197
|
+
self.ttl_seconds = ttl_seconds
|
|
198
|
+
self.max_size = max_size
|
|
199
|
+
self._seen: Dict[str, float] = {}
|
|
200
|
+
|
|
201
|
+
def seen_recently(self, key: str) -> bool:
|
|
202
|
+
now = time.time()
|
|
203
|
+
if len(self._seen) > self.max_size:
|
|
204
|
+
self._prune(now)
|
|
205
|
+
ts = self._seen.get(key)
|
|
206
|
+
if ts is None or now - ts > self.ttl_seconds:
|
|
207
|
+
self._seen[key] = now
|
|
208
|
+
return False
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
def _prune(self, now: float):
|
|
212
|
+
expired = [k for k, ts in self._seen.items() if now - ts > self.ttl_seconds]
|
|
213
|
+
for k in expired:
|
|
214
|
+
self._seen.pop(k, None)
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
entari_plugin_hyw/Untitled-1,sha256=wbsr5i9iorqBMIYK4aAnpNTek3mXbhvyo2YOYw38pE4,30187
|
|
2
|
-
entari_plugin_hyw/__init__.py,sha256=
|
|
2
|
+
entari_plugin_hyw/__init__.py,sha256=0q-i4F_xm--klgtp__r7cx_xPK7rV164aRzISEmVO7o,39053
|
|
3
|
+
entari_plugin_hyw/filters.py,sha256=sQnLaiqqZ2NkykcH4QgzFImP-JW3uVU7l6iuAAyUsJg,3080
|
|
3
4
|
entari_plugin_hyw/history.py,sha256=0XJwbfvXH5T1EPt4G1J5wWMJsKi0FfmajY5cvw8CQWE,12065
|
|
4
|
-
entari_plugin_hyw/misc.py,sha256=
|
|
5
|
+
entari_plugin_hyw/misc.py,sha256=5IqF5Z2C_6Ufy5TI89uX5hX5fVYcXOTZIQUIu_tvf54,6855
|
|
5
6
|
entari_plugin_hyw/search_cache.py,sha256=7MIhTm5_YnZjc0aBaX7AE4AJp0VT8eU6ObR6mTkoerc,4285
|
|
6
7
|
hyw_core/__init__.py,sha256=Jlr9Ic-BLOPTnff6OctUCdjDMdK4nssTF_vHie4QKTo,1958
|
|
8
|
+
hyw_core/agent.py,sha256=xKvO9CIo0MX7mBQ6DuLDaDLvREyRIZMNXKk4tpOMi1U,29496
|
|
7
9
|
hyw_core/config.py,sha256=DHxwToUVLm1nT88gG05e3hVzSLxXMk9BjgjAnhGCADk,4918
|
|
8
|
-
hyw_core/core.py,sha256=
|
|
9
|
-
hyw_core/definitions.py,sha256=
|
|
10
|
+
hyw_core/core.py,sha256=_jN4831OeHQ_aM7sIlzcwYb5_Lp82kp2XmqpJD_tsLA,16097
|
|
11
|
+
hyw_core/definitions.py,sha256=pH46L-N25pSuPaIiN7l7yfoD6oHK6BLHigE0eYLFmJQ,4270
|
|
10
12
|
hyw_core/image_cache.py,sha256=t8pr1kgH2ngK9IhrBAhzUqhBWERNztUywMzgCFZEtQk,9899
|
|
11
13
|
hyw_core/pipeline.py,sha256=ZWwF0DHa29-65lUMU1_Fem3xQmxl7X_vgeni0ErOb8Q,22826
|
|
12
|
-
hyw_core/search.py,sha256=
|
|
14
|
+
hyw_core/search.py,sha256=VvfNSb9Hf7ZQWlNtnZfYe2eO9qPjYtwJxVlud6OdeCQ,7787
|
|
13
15
|
hyw_core/browser_control/__init__.py,sha256=IeMErRC6fbq1PJWNK3klSbarSrUwOM4yyd_kJ6uWCPM,1406
|
|
14
16
|
hyw_core/browser_control/landing.html,sha256=wgqldumdylz69T83pvOkrigT1Mdb9GY0_KU0ceLGwdY,4642
|
|
15
17
|
hyw_core/browser_control/manager.py,sha256=-dHb0FamRsLfuU3jqX5cKaDo8DOOFV32zY912GuMdXU,6048
|
|
16
18
|
hyw_core/browser_control/renderer.py,sha256=s-QNIU-NMVQGLd_drLmeERgHsTm6C9XYm78CObt2KXc,17409
|
|
17
|
-
hyw_core/browser_control/service.py,sha256=
|
|
19
|
+
hyw_core/browser_control/service.py,sha256=_TcRRQuSei8PCsZs8hYU_DJ4DRMQ6jAFNqWmwca35Tc,43984
|
|
18
20
|
hyw_core/browser_control/assets/index.html,sha256=BpbM0vD9OYicE5MBHSVLo3j_y-MpULI82PMqmBKpWT8,2328623
|
|
19
21
|
hyw_core/browser_control/assets/card-dist/index.html,sha256=Xw-hQ5ctdQkK-1jV8_gqMdgVGNZDwWZvIAqNrh2eK7g,2210054
|
|
20
22
|
hyw_core/browser_control/assets/card-dist/vite.svg,sha256=SnSK_UQ5GLsWWRyDTEAdrjPoeGGrXbrQgRw6O0qSFPs,1497
|
|
@@ -63,8 +65,8 @@ hyw_core/crawling/completeness.py,sha256=OKdS8XlYYWDU1Vl1k-u7yEFqppukuJv-YQB0Px5
|
|
|
63
65
|
hyw_core/crawling/models.py,sha256=pCKe0k9xT3taSAlTlh0PazcLV0xYsm8p3XIkLHGf-LM,2353
|
|
64
66
|
hyw_core/stages/__init__.py,sha256=W89cWpq-HBLi2FprtJQSjQNLzpbhM8ZCkqPG61D_imE,521
|
|
65
67
|
hyw_core/stages/base.py,sha256=EfnTkISXbBNxjARykqIhmMrVqw2tqZl7ozJbJEbRnhI,2806
|
|
66
|
-
hyw_core/stages/summary.py,sha256=
|
|
67
|
-
entari_plugin_hyw-4.0.
|
|
68
|
-
entari_plugin_hyw-4.0.
|
|
69
|
-
entari_plugin_hyw-4.0.
|
|
70
|
-
entari_plugin_hyw-4.0.
|
|
68
|
+
hyw_core/stages/summary.py,sha256=sgHCm_Leq_pkJ4YcgQuf8croiOP1oKz171TnzJwRwVs,7080
|
|
69
|
+
entari_plugin_hyw-4.0.0rc13.dist-info/METADATA,sha256=NhbBeH0svhxJ6MvHl6vKF-BCzg-jRczAhlg_3fXD_lM,3845
|
|
70
|
+
entari_plugin_hyw-4.0.0rc13.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
71
|
+
entari_plugin_hyw-4.0.0rc13.dist-info/top_level.txt,sha256=ah76OrufRX0okOl4Fv8MO6PXiT0IaZ1oG0eDrdAPoNo,27
|
|
72
|
+
entari_plugin_hyw-4.0.0rc13.dist-info/RECORD,,
|