entari-plugin-hyw 0.3.5__py3-none-any.whl → 4.0.0rc14__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/Untitled-1 +1865 -0
- entari_plugin_hyw/__init__.py +979 -116
- entari_plugin_hyw/filters.py +83 -0
- entari_plugin_hyw/history.py +251 -0
- entari_plugin_hyw/misc.py +214 -0
- entari_plugin_hyw/search_cache.py +154 -0
- entari_plugin_hyw-4.0.0rc14.dist-info/METADATA +118 -0
- entari_plugin_hyw-4.0.0rc14.dist-info/RECORD +72 -0
- {entari_plugin_hyw-0.3.5.dist-info → entari_plugin_hyw-4.0.0rc14.dist-info}/WHEEL +1 -1
- {entari_plugin_hyw-0.3.5.dist-info → entari_plugin_hyw-4.0.0rc14.dist-info}/top_level.txt +1 -0
- hyw_core/__init__.py +94 -0
- hyw_core/agent.py +768 -0
- hyw_core/browser_control/__init__.py +63 -0
- hyw_core/browser_control/assets/card-dist/index.html +425 -0
- hyw_core/browser_control/assets/card-dist/logos/anthropic.svg +1 -0
- hyw_core/browser_control/assets/card-dist/logos/cerebras.svg +9 -0
- hyw_core/browser_control/assets/card-dist/logos/deepseek.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/gemini.svg +1 -0
- hyw_core/browser_control/assets/card-dist/logos/google.svg +1 -0
- hyw_core/browser_control/assets/card-dist/logos/grok.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/huggingface.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/microsoft.svg +15 -0
- hyw_core/browser_control/assets/card-dist/logos/minimax.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/mistral.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/nvida.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/openai.svg +1 -0
- hyw_core/browser_control/assets/card-dist/logos/openrouter.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/perplexity.svg +24 -0
- hyw_core/browser_control/assets/card-dist/logos/qwen.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/xai.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/xiaomi.png +0 -0
- hyw_core/browser_control/assets/card-dist/logos/zai.png +0 -0
- hyw_core/browser_control/assets/card-dist/vite.svg +1 -0
- hyw_core/browser_control/assets/index.html +5691 -0
- hyw_core/browser_control/assets/logos/anthropic.svg +1 -0
- hyw_core/browser_control/assets/logos/cerebras.svg +9 -0
- hyw_core/browser_control/assets/logos/deepseek.png +0 -0
- hyw_core/browser_control/assets/logos/gemini.svg +1 -0
- hyw_core/browser_control/assets/logos/google.svg +1 -0
- hyw_core/browser_control/assets/logos/grok.png +0 -0
- hyw_core/browser_control/assets/logos/huggingface.png +0 -0
- hyw_core/browser_control/assets/logos/microsoft.svg +15 -0
- hyw_core/browser_control/assets/logos/minimax.png +0 -0
- hyw_core/browser_control/assets/logos/mistral.png +0 -0
- hyw_core/browser_control/assets/logos/nvida.png +0 -0
- hyw_core/browser_control/assets/logos/openai.svg +1 -0
- hyw_core/browser_control/assets/logos/openrouter.png +0 -0
- hyw_core/browser_control/assets/logos/perplexity.svg +24 -0
- hyw_core/browser_control/assets/logos/qwen.png +0 -0
- hyw_core/browser_control/assets/logos/xai.png +0 -0
- hyw_core/browser_control/assets/logos/xiaomi.png +0 -0
- hyw_core/browser_control/assets/logos/zai.png +0 -0
- hyw_core/browser_control/engines/__init__.py +15 -0
- hyw_core/browser_control/engines/base.py +13 -0
- hyw_core/browser_control/engines/default.py +166 -0
- hyw_core/browser_control/engines/duckduckgo.py +171 -0
- hyw_core/browser_control/landing.html +172 -0
- hyw_core/browser_control/manager.py +173 -0
- hyw_core/browser_control/renderer.py +446 -0
- hyw_core/browser_control/service.py +940 -0
- hyw_core/config.py +154 -0
- hyw_core/core.py +462 -0
- hyw_core/crawling/__init__.py +18 -0
- hyw_core/crawling/completeness.py +437 -0
- hyw_core/crawling/models.py +88 -0
- hyw_core/definitions.py +104 -0
- hyw_core/image_cache.py +274 -0
- hyw_core/pipeline.py +502 -0
- hyw_core/search.py +171 -0
- hyw_core/stages/__init__.py +21 -0
- hyw_core/stages/base.py +95 -0
- hyw_core/stages/summary.py +191 -0
- entari_plugin_hyw/agent.py +0 -419
- entari_plugin_hyw/compressor.py +0 -59
- entari_plugin_hyw/tools.py +0 -236
- entari_plugin_hyw/vision.py +0 -35
- entari_plugin_hyw-0.3.5.dist-info/METADATA +0 -112
- entari_plugin_hyw-0.3.5.dist-info/RECORD +0 -9
entari_plugin_hyw/__init__.py
CHANGED
|
@@ -1,142 +1,1005 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from arclet.entari import MessageChain, At, Image, Quote, Text
|
|
11
|
-
from satori import Element
|
|
12
|
-
import arclet.letoderea as leto
|
|
13
|
-
from arclet.entari import MessageCreatedEvent, Session
|
|
14
|
-
from arclet.entari import BasicConfModel, metadata, plugin_config
|
|
15
|
-
import httpx
|
|
1
|
+
"""
|
|
2
|
+
entari-plugin-hyw - Entari Plugin for HYW
|
|
3
|
+
|
|
4
|
+
Use large language models to interpret chat messages.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from importlib.metadata import version as get_version
|
|
9
|
+
from typing import List, Dict, Any, Optional
|
|
16
10
|
import asyncio
|
|
17
|
-
import
|
|
11
|
+
import os
|
|
12
|
+
import base64
|
|
18
13
|
import re
|
|
19
|
-
|
|
20
|
-
Args,
|
|
21
|
-
Alconna,
|
|
22
|
-
AllParam,
|
|
23
|
-
MultiVar,
|
|
24
|
-
CommandMeta,
|
|
25
|
-
)
|
|
26
|
-
from arclet.entari import MessageChain, Session, command
|
|
27
|
-
from satori.element import Custom, E
|
|
14
|
+
import tempfile
|
|
28
15
|
|
|
29
|
-
|
|
30
|
-
from .
|
|
16
|
+
from arclet.alconna import Alconna, Args, AllParam, Arparma
|
|
17
|
+
from arclet.entari import metadata, listen, Session, plugin_config, BasicConfModel, command
|
|
18
|
+
from arclet.entari import MessageChain, Text, Image, MessageCreatedEvent, Quote, At
|
|
19
|
+
from satori.element import Custom
|
|
20
|
+
from loguru import logger
|
|
21
|
+
from arclet.entari.event.command import CommandReceive
|
|
22
|
+
from arclet.entari.event.lifespan import Cleanup
|
|
31
23
|
|
|
24
|
+
# Import from internal hyw_core
|
|
25
|
+
from hyw_core import HywCore, HywCoreConfig, QueryRequest
|
|
26
|
+
from hyw_core.browser_control import (
|
|
27
|
+
ContentRenderer,
|
|
28
|
+
get_content_renderer,
|
|
29
|
+
set_global_renderer,
|
|
30
|
+
close_screenshot_service,
|
|
31
|
+
)
|
|
32
|
+
from hyw_core.browser_control.manager import close_shared_browser
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
# Local modules
|
|
35
|
+
from .history import HistoryManager
|
|
36
|
+
from .misc import (
|
|
37
|
+
process_onebot_json,
|
|
38
|
+
process_images,
|
|
39
|
+
resolve_model_name,
|
|
40
|
+
render_refuse_answer,
|
|
41
|
+
render_image_unsupported,
|
|
42
|
+
parse_color,
|
|
43
|
+
RecentEventDeduper,
|
|
39
44
|
)
|
|
45
|
+
from .filters import parse_filter_syntax
|
|
46
|
+
from .search_cache import SearchResultCache, parse_single_index, parse_multi_indices
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
__version__ = get_version("entari_plugin_hyw")
|
|
51
|
+
except Exception:
|
|
52
|
+
__version__ = "4.0.0-rc8"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_event_deduper = RecentEventDeduper()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class HywConfig(BasicConfModel):
|
|
61
|
+
"""Plugin configuration"""
|
|
62
|
+
admins: List[str] = field(default_factory=list)
|
|
63
|
+
models: List[Dict[str, Any]] = field(default_factory=list)
|
|
64
|
+
question_command: str = "/q"
|
|
65
|
+
web_command: str = "/w"
|
|
66
|
+
help_command: str = "/h"
|
|
67
|
+
language: str = "Simplified Chinese"
|
|
68
|
+
temperature: float = 0.4
|
|
69
|
+
|
|
70
|
+
model_name: Optional[str] = None
|
|
71
|
+
api_key: Optional[str] = None
|
|
72
|
+
base_url: str = "https://openrouter.ai/api/v1"
|
|
73
|
+
|
|
74
|
+
search_engine: str = "duckduckgo"
|
|
75
|
+
|
|
76
|
+
headless: bool = False
|
|
77
|
+
save_conversation: bool = False
|
|
78
|
+
reaction: bool = False
|
|
79
|
+
quote: bool = False
|
|
80
|
+
theme_color: str = "#ff0000"
|
|
81
|
+
|
|
82
|
+
# Main model configuration (used for summary/main LLM calls)
|
|
83
|
+
main: Optional[Dict[str, Any]] = None
|
|
84
|
+
|
|
85
|
+
def __post_init__(self):
|
|
86
|
+
self.theme_color = parse_color(self.theme_color)
|
|
87
|
+
|
|
88
|
+
def to_hyw_core_config(self) -> HywCoreConfig:
|
|
89
|
+
main_cfg = self.main or {}
|
|
90
|
+
|
|
91
|
+
return HywCoreConfig.from_dict({
|
|
92
|
+
"models": self.models,
|
|
93
|
+
"model_name": self.model_name or "",
|
|
94
|
+
"api_key": self.api_key or "",
|
|
95
|
+
"base_url": self.base_url,
|
|
96
|
+
"temperature": self.temperature,
|
|
97
|
+
"search_engine": self.search_engine,
|
|
98
|
+
"headless": self.headless,
|
|
99
|
+
"language": self.language,
|
|
100
|
+
"theme_color": self.theme_color,
|
|
101
|
+
|
|
102
|
+
# Map nested 'main' config to summary stage
|
|
103
|
+
"summary_model": main_cfg.get("model_name"),
|
|
104
|
+
"summary_api_key": main_cfg.get("api_key"),
|
|
105
|
+
"summary_base_url": main_cfg.get("base_url"),
|
|
106
|
+
"summary_extra_body": main_cfg.get("extra_body"),
|
|
107
|
+
})
|
|
108
|
+
|
|
40
109
|
|
|
41
110
|
conf = plugin_config(HywConfig)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
async def
|
|
57
|
-
|
|
111
|
+
history_manager = HistoryManager()
|
|
112
|
+
renderer = ContentRenderer(headless=conf.headless)
|
|
113
|
+
set_global_renderer(renderer)
|
|
114
|
+
search_cache = SearchResultCache(ttl_seconds=600.0) # 10 minutes
|
|
115
|
+
|
|
116
|
+
# Initialize HywCore immediately at plugin load time (not lazy)
|
|
117
|
+
# This avoids the 2s delay on first user request caused by AsyncOpenAI client creation
|
|
118
|
+
_hyw_core: HywCore = HywCore(conf.to_hyw_core_config())
|
|
119
|
+
|
|
120
|
+
def get_hyw_core() -> HywCore:
|
|
121
|
+
return _hyw_core
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@listen(Cleanup)
|
|
125
|
+
async def cleanup_screenshot_service():
|
|
126
|
+
global _hyw_core
|
|
58
127
|
try:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
raise ActionFailed(f"下载图片失败,状态码: {resp.status_code}")
|
|
128
|
+
if _hyw_core:
|
|
129
|
+
await _hyw_core.close()
|
|
130
|
+
_hyw_core = None
|
|
131
|
+
await close_screenshot_service()
|
|
132
|
+
close_shared_browser()
|
|
65
133
|
except Exception as e:
|
|
66
|
-
|
|
134
|
+
logger.warning(f"Failed to cleanup: {e}")
|
|
67
135
|
|
|
68
136
|
|
|
69
|
-
def
|
|
137
|
+
async def react(session: Session, emoji: str):
|
|
138
|
+
if not conf.reaction: return
|
|
70
139
|
try:
|
|
71
|
-
|
|
72
|
-
json_str = html.unescape(json_data_str)
|
|
73
|
-
return json_str
|
|
140
|
+
await session.reaction_create(emoji=emoji)
|
|
74
141
|
except Exception as e:
|
|
75
|
-
|
|
142
|
+
logger.warning(f"Reaction failed: {e}")
|
|
76
143
|
|
|
77
144
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
145
|
+
async def process_request(
|
|
146
|
+
session: Session[MessageCreatedEvent],
|
|
147
|
+
all_param: Optional[MessageChain] = None,
|
|
148
|
+
selected_model: Optional[str] = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
mc = MessageChain(all_param)
|
|
151
|
+
if session.reply:
|
|
81
152
|
try:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
153
|
+
reply_msg_id = str(session.reply.origin.id) if hasattr(session.reply.origin, 'id') else None
|
|
154
|
+
if not (reply_msg_id and history_manager.is_bot_message(reply_msg_id)):
|
|
155
|
+
mc.extend(MessageChain(" ") + session.reply.origin.message)
|
|
156
|
+
except Exception:
|
|
157
|
+
mc.extend(MessageChain(" ") + session.reply.origin.message)
|
|
158
|
+
|
|
159
|
+
filtered = mc.get(Text) + mc.get(Image) + mc.get(Custom)
|
|
160
|
+
mc = MessageChain(filtered)
|
|
161
|
+
|
|
162
|
+
text_content = str(mc.get(Text)).strip()
|
|
163
|
+
text_content = re.sub(r'<img[^>]+>', '', text_content, flags=re.IGNORECASE)
|
|
164
|
+
|
|
165
|
+
if not text_content and not mc.get(Image) and not mc.get(Custom):
|
|
166
|
+
return
|
|
89
167
|
|
|
90
|
-
|
|
168
|
+
hist_key = None
|
|
169
|
+
if session.reply and hasattr(session.reply.origin, 'id'):
|
|
170
|
+
hist_key = history_manager.get_conversation_id(str(session.reply.origin.id))
|
|
171
|
+
|
|
172
|
+
hist_payload = history_manager.get_history(hist_key) if hist_key else []
|
|
173
|
+
context_id = f"guild_{session.guild.id}" if session.guild else f"user_{session.user.id}"
|
|
174
|
+
|
|
175
|
+
if conf.reaction: await react(session, "✨")
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
msg_text = str(mc.get(Text)).strip() if mc.get(Text) else ""
|
|
179
|
+
msg_text = re.sub(r'<img[^>]+>', '', msg_text, flags=re.IGNORECASE)
|
|
180
|
+
|
|
181
|
+
if not msg_text and (mc.get(Image) or mc.get(Custom)):
|
|
182
|
+
msg_text = "[图片]"
|
|
183
|
+
|
|
184
|
+
for custom in [e for e in mc if isinstance(e, Custom)]:
|
|
185
|
+
if custom.tag == 'onebot:json':
|
|
186
|
+
if decoded := process_onebot_json(custom.attributes()):
|
|
187
|
+
msg_text += f"\n{decoded}"
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
model = selected_model
|
|
191
|
+
if model:
|
|
192
|
+
resolved, _ = resolve_model_name(model, conf.models)
|
|
193
|
+
if resolved:
|
|
194
|
+
model = resolved
|
|
195
|
+
|
|
196
|
+
images, _ = await process_images(mc, None)
|
|
197
|
+
|
|
198
|
+
# Prepare renderer
|
|
199
|
+
local_renderer = await get_content_renderer()
|
|
200
|
+
render_tab_task = asyncio.create_task(local_renderer.prepare_tab())
|
|
201
|
+
|
|
202
|
+
async def send_noti(msg: str):
|
|
203
|
+
try:
|
|
204
|
+
if conf.quote:
|
|
205
|
+
await session.send([Quote(session.event.message.id), msg])
|
|
206
|
+
else:
|
|
207
|
+
await session.send(msg)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.warning(f"Failed to send notification: {e}")
|
|
210
|
+
|
|
211
|
+
request = QueryRequest(
|
|
212
|
+
user_input=msg_text,
|
|
213
|
+
images=images,
|
|
214
|
+
conversation_history=hist_payload,
|
|
215
|
+
model_name=model,
|
|
216
|
+
send_notification=send_noti
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
220
|
+
output_path = tf.name
|
|
221
|
+
|
|
222
|
+
core = get_hyw_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)
|
|
226
|
+
|
|
227
|
+
# 2. Get the warmed-up tab
|
|
91
228
|
try:
|
|
92
|
-
|
|
229
|
+
tab_id = await render_tab_task
|
|
93
230
|
except Exception:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
231
|
+
tab_id = None
|
|
232
|
+
|
|
233
|
+
display_session_id = history_manager.generate_short_code()
|
|
234
|
+
|
|
235
|
+
if response.should_refuse:
|
|
236
|
+
render_ok = await render_refuse_answer(
|
|
237
|
+
renderer=local_renderer,
|
|
238
|
+
output_path=output_path,
|
|
239
|
+
reason=response.refuse_reason or 'Refused',
|
|
240
|
+
theme_color=conf.theme_color,
|
|
241
|
+
tab_id=tab_id,
|
|
242
|
+
)
|
|
243
|
+
elif not response.success:
|
|
244
|
+
await session.send(f"Error: {response.error}")
|
|
245
|
+
return
|
|
246
|
+
else:
|
|
247
|
+
# 3. Explicit External Render using the Parallel Tab
|
|
248
|
+
render_ok = await core.render(
|
|
249
|
+
markdown_content=response.content,
|
|
250
|
+
output_path=output_path,
|
|
251
|
+
stats={"total_time": response.total_time},
|
|
252
|
+
references=response.references,
|
|
253
|
+
page_references=response.page_references,
|
|
254
|
+
image_references=response.image_references,
|
|
255
|
+
stages_used=response.stages_used,
|
|
256
|
+
tab_id=tab_id
|
|
257
|
+
)
|
|
258
|
+
if render_ok:
|
|
259
|
+
response.image_path = output_path
|
|
260
|
+
|
|
261
|
+
if render_ok:
|
|
262
|
+
with open(output_path, "rb") as f:
|
|
263
|
+
img_data = base64.b64encode(f.read()).decode()
|
|
264
|
+
|
|
265
|
+
msg_chain = MessageChain(Image(src=f'data:image/png;base64,{img_data}'))
|
|
266
|
+
if conf.quote:
|
|
267
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
268
|
+
|
|
269
|
+
sent = await session.send(msg_chain)
|
|
270
|
+
|
|
271
|
+
sent_id = next((str(e.id) for e in sent if hasattr(e, 'id')), None) if sent else None
|
|
272
|
+
msg_id = str(session.event.message.id) if hasattr(session.event, 'message') else str(session.event.id)
|
|
273
|
+
|
|
274
|
+
updated_history = hist_payload + [
|
|
275
|
+
{"role": "user", "content": msg_text},
|
|
276
|
+
{"role": "assistant", "content": response.content}
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
# Save to Memory
|
|
280
|
+
history_manager.remember(
|
|
281
|
+
sent_id, updated_history, [msg_id],
|
|
282
|
+
{"model": model}, context_id, code=display_session_id,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Save to Disk (Debug/Logging)
|
|
286
|
+
if conf.save_conversation:
|
|
287
|
+
# Extract traces from response
|
|
288
|
+
trace = response.stages_trace
|
|
289
|
+
instruct_traces = trace.get("instruct_rounds") if trace else None
|
|
290
|
+
|
|
291
|
+
# Check for web_results in response (needs Core update)
|
|
292
|
+
web_results = getattr(response, "web_results", [])
|
|
293
|
+
|
|
294
|
+
history_manager.save_to_disk(
|
|
295
|
+
key=sent_id,
|
|
296
|
+
image_path=output_path,
|
|
297
|
+
web_results=web_results,
|
|
298
|
+
instruct_traces=instruct_traces,
|
|
299
|
+
vision_trace=None # Vision integrated into Instruct now
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if os.path.exists(output_path):
|
|
303
|
+
os.remove(output_path)
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.exception(f"Error: {e}")
|
|
307
|
+
await session.send(f"Error: {e}")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
alc = Alconna(conf.question_command, Args["all_param;?", AllParam])
|
|
312
|
+
|
|
313
|
+
@command.on(alc)
|
|
314
|
+
async def handle_question_command(session: Session[MessageCreatedEvent], result: Arparma):
|
|
315
|
+
try:
|
|
316
|
+
mid = str(session.event.message.id) if getattr(session.event, "message", None) else str(session.event.id)
|
|
317
|
+
dedupe_key = f"{getattr(session.account, 'id', 'account')}:{mid}"
|
|
318
|
+
if _event_deduper.seen_recently(dedupe_key):
|
|
319
|
+
return
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
args = result.all_matched_args
|
|
324
|
+
all_param = args.get("all_param")
|
|
325
|
+
|
|
326
|
+
# Extract query text
|
|
327
|
+
if all_param:
|
|
328
|
+
if isinstance(all_param, MessageChain):
|
|
329
|
+
query_text = str(all_param.get(Text)).strip()
|
|
330
|
+
else:
|
|
331
|
+
query_text = str(all_param).strip()
|
|
332
|
+
else:
|
|
333
|
+
query_text = ""
|
|
334
|
+
|
|
335
|
+
# Check if replying to a cached search result
|
|
336
|
+
reply_msg_id = None
|
|
337
|
+
if session.reply and hasattr(session.reply.origin, 'id'):
|
|
338
|
+
reply_msg_id = str(session.reply.origin.id)
|
|
339
|
+
|
|
340
|
+
# Quote mode: Use cached search results
|
|
341
|
+
if reply_msg_id:
|
|
342
|
+
cached = search_cache.get(reply_msg_id)
|
|
343
|
+
if cached:
|
|
344
|
+
# Parse indices if provided
|
|
345
|
+
indices = parse_multi_indices(query_text, max_count=3) if query_text else None
|
|
346
|
+
|
|
347
|
+
# Check if too many indices requested (parse_multi_indices returns None if > max_count)
|
|
348
|
+
if query_text and indices is None:
|
|
349
|
+
# Check if it looks like indices but exceeded limit
|
|
350
|
+
if re.match(r'^[\d,、\s\-–]+$', query_text):
|
|
351
|
+
await session.send("最多选择3个结果进行总结")
|
|
352
|
+
search_cache.cleanup()
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
if conf.reaction:
|
|
356
|
+
asyncio.create_task(react(session, "✨"))
|
|
357
|
+
|
|
358
|
+
core = get_hyw_core()
|
|
359
|
+
local_renderer = await get_content_renderer()
|
|
360
|
+
tab_task = asyncio.create_task(local_renderer.prepare_tab())
|
|
361
|
+
|
|
362
|
+
# Collect screenshots for selected pages
|
|
363
|
+
screenshots = []
|
|
364
|
+
if indices:
|
|
365
|
+
# Screenshot mode: capture pages for selected indices
|
|
366
|
+
for idx in indices:
|
|
367
|
+
if idx < len(cached.results):
|
|
368
|
+
url = cached.results[idx].get("url", "")
|
|
369
|
+
if url:
|
|
370
|
+
b64_img = await core.screenshot(url)
|
|
371
|
+
if b64_img:
|
|
372
|
+
screenshots.append(b64_img)
|
|
373
|
+
|
|
374
|
+
if not screenshots:
|
|
375
|
+
try: await tab_task
|
|
376
|
+
except: pass
|
|
377
|
+
await session.send("无法截图所选页面")
|
|
378
|
+
search_cache.cleanup()
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
user_query = f"总结关于 \"{cached.query}\" 的内容"
|
|
382
|
+
else:
|
|
383
|
+
# No indices - summarize based on cached snippets (no screenshots)
|
|
384
|
+
context_parts = []
|
|
385
|
+
for i, res in enumerate(cached.results[:10]):
|
|
386
|
+
title = res.get("title", f"Result {i+1}")
|
|
387
|
+
snippet = res.get("content", "") or res.get("snippet", "")
|
|
388
|
+
context_parts.append(f"## {title}\n{snippet}")
|
|
389
|
+
|
|
390
|
+
context_message = f"基于搜索 \"{cached.query}\" 的结果摘要回答用户问题:\n\n" + "\n\n".join(context_parts)
|
|
391
|
+
user_query = query_text if query_text else f"总结关于 \"{cached.query}\" 的搜索结果"
|
|
392
|
+
|
|
393
|
+
# Build request with screenshots (if any)
|
|
394
|
+
if screenshots:
|
|
395
|
+
request = QueryRequest(
|
|
396
|
+
user_input=user_query,
|
|
397
|
+
images=screenshots,
|
|
398
|
+
conversation_history=[],
|
|
399
|
+
model_name=None,
|
|
400
|
+
)
|
|
401
|
+
else:
|
|
402
|
+
request = QueryRequest(
|
|
403
|
+
user_input=f"{context_message}\n\n用户问题: {user_query}",
|
|
404
|
+
images=[],
|
|
405
|
+
conversation_history=[],
|
|
406
|
+
model_name=None,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
410
|
+
output_path = tf.name
|
|
411
|
+
|
|
412
|
+
response = await core.query(request, output_path=None)
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
tab_id = await tab_task
|
|
416
|
+
except Exception:
|
|
417
|
+
tab_id = None
|
|
418
|
+
|
|
419
|
+
if response.success and response.content:
|
|
420
|
+
render_ok = await core.render(
|
|
421
|
+
markdown_content=response.content,
|
|
422
|
+
output_path=output_path,
|
|
423
|
+
stats={"total_time": response.total_time},
|
|
424
|
+
references=[],
|
|
425
|
+
page_references=[],
|
|
426
|
+
tab_id=tab_id
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if render_ok and os.path.exists(output_path):
|
|
430
|
+
with open(output_path, "rb") as f:
|
|
431
|
+
img_data = base64.b64encode(f.read()).decode()
|
|
432
|
+
|
|
433
|
+
msg_chain = MessageChain(Image(src=f'data:image/png;base64,{img_data}'))
|
|
434
|
+
if conf.quote:
|
|
435
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
436
|
+
|
|
437
|
+
await session.send(msg_chain)
|
|
438
|
+
os.remove(output_path)
|
|
439
|
+
else:
|
|
440
|
+
await session.send(response.content[:500])
|
|
441
|
+
else:
|
|
442
|
+
await session.send(f"总结失败: {response.error or 'Unknown error'}")
|
|
443
|
+
|
|
444
|
+
search_cache.cleanup()
|
|
445
|
+
return
|
|
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
|
+
|
|
524
|
+
# === Filter Mode: Search + Find matching links + Summarize ===
|
|
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)
|
|
532
|
+
|
|
533
|
+
if filter_error:
|
|
534
|
+
await session.send(filter_error)
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
if filters:
|
|
538
|
+
if conf.reaction:
|
|
539
|
+
asyncio.create_task(react(session, "✨"))
|
|
540
|
+
|
|
541
|
+
core = get_hyw_core()
|
|
542
|
+
local_renderer = await get_content_renderer()
|
|
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
|
+
|
|
548
|
+
# Run search and prepare tab in parallel
|
|
549
|
+
search_task = asyncio.create_task(core.search([search_query]))
|
|
550
|
+
tab_task = asyncio.create_task(local_renderer.prepare_tab())
|
|
551
|
+
|
|
552
|
+
results = await search_task
|
|
553
|
+
flat_results = results[0] if results else []
|
|
554
|
+
|
|
555
|
+
if not flat_results:
|
|
556
|
+
try: await tab_task
|
|
557
|
+
except: pass
|
|
558
|
+
await session.send("Search returned no results.")
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
visible = [r for r in flat_results if not r.get("_hidden", False)]
|
|
562
|
+
|
|
563
|
+
# Collect URLs to screenshot
|
|
564
|
+
urls_to_screenshot = []
|
|
565
|
+
for filter_type, filter_value, count in filters:
|
|
566
|
+
if filter_type == 'index':
|
|
567
|
+
idx = filter_value - 1
|
|
568
|
+
if 0 <= idx < len(visible):
|
|
569
|
+
url = visible[idx].get("url", "")
|
|
570
|
+
if url and url not in urls_to_screenshot:
|
|
571
|
+
urls_to_screenshot.append(url)
|
|
572
|
+
else:
|
|
573
|
+
try: await tab_task
|
|
574
|
+
except: pass
|
|
575
|
+
await session.send(f"⚠️ 序号 {filter_value} 超出范围 (1-{len(visible)})")
|
|
576
|
+
return
|
|
577
|
+
else:
|
|
578
|
+
found_count = 0
|
|
579
|
+
for res in visible:
|
|
580
|
+
url = res.get("url", "")
|
|
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:
|
|
584
|
+
urls_to_screenshot.append(url)
|
|
585
|
+
found_count += 1
|
|
586
|
+
if found_count >= count:
|
|
587
|
+
break
|
|
588
|
+
|
|
589
|
+
if found_count == 0:
|
|
590
|
+
try: await tab_task
|
|
591
|
+
except: pass
|
|
592
|
+
await session.send(f"⚠️ 未找到包含 \"{filter_value}\" 的链接")
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
if not urls_to_screenshot:
|
|
596
|
+
try: await tab_task
|
|
597
|
+
except: pass
|
|
598
|
+
await session.send("⚠️ 未找到匹配的链接")
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
# Take screenshots with content extraction
|
|
602
|
+
screenshot_tasks = [core.screenshot_with_content(url) for url in urls_to_screenshot]
|
|
603
|
+
screenshot_results = await asyncio.gather(*screenshot_tasks)
|
|
604
|
+
|
|
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
|
+
try: await tab_task
|
|
626
|
+
except: pass
|
|
627
|
+
await session.send("无法截图页面")
|
|
628
|
+
return
|
|
629
|
+
|
|
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]}"
|
|
636
|
+
|
|
637
|
+
request = QueryRequest(
|
|
638
|
+
user_input=user_query,
|
|
639
|
+
images=[], # No images, use text content instead
|
|
640
|
+
conversation_history=[],
|
|
641
|
+
model_name=None,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
645
|
+
output_path = tf.name
|
|
646
|
+
|
|
647
|
+
response = await core.query(request, output_path=None)
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
tab_id = await tab_task
|
|
651
|
+
except Exception:
|
|
652
|
+
tab_id = None
|
|
653
|
+
|
|
654
|
+
if response.success and response.content:
|
|
655
|
+
render_ok = await core.render(
|
|
656
|
+
markdown_content=response.content,
|
|
657
|
+
output_path=output_path,
|
|
658
|
+
stats={"total_time": response.total_time},
|
|
659
|
+
references=[],
|
|
660
|
+
page_references=page_references, # Pass screenshots for rendering
|
|
661
|
+
tab_id=tab_id
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
if render_ok and os.path.exists(output_path):
|
|
665
|
+
with open(output_path, "rb") as f:
|
|
666
|
+
img_data = base64.b64encode(f.read()).decode()
|
|
667
|
+
|
|
668
|
+
msg_chain = MessageChain(Image(src=f'data:image/png;base64,{img_data}'))
|
|
669
|
+
if conf.quote:
|
|
670
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
671
|
+
|
|
672
|
+
await session.send(msg_chain)
|
|
673
|
+
os.remove(output_path)
|
|
674
|
+
else:
|
|
675
|
+
await session.send(response.content[:500])
|
|
676
|
+
else:
|
|
677
|
+
await session.send(f"总结失败: {response.error or 'Unknown error'}")
|
|
678
|
+
|
|
679
|
+
return
|
|
680
|
+
|
|
681
|
+
# Normal query mode (no cache context)
|
|
682
|
+
await process_request(session, all_param)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# Search/Web Command (/w)
|
|
686
|
+
alc_search = Alconna(conf.web_command, Args["query;?", AllParam])
|
|
687
|
+
|
|
688
|
+
@command.on(alc_search)
|
|
689
|
+
async def handle_web_command(session: Session[MessageCreatedEvent], result: Arparma):
|
|
690
|
+
"""
|
|
691
|
+
Handle web command /w:
|
|
692
|
+
- If query is index + Quote -> Screenshot cached result
|
|
693
|
+
- If query is URL -> Screenshot
|
|
694
|
+
- If query is text -> Search
|
|
695
|
+
"""
|
|
696
|
+
query = result.all_matched_args.get("query")
|
|
697
|
+
|
|
698
|
+
# Extract query text
|
|
699
|
+
if query:
|
|
700
|
+
if isinstance(query, MessageChain):
|
|
701
|
+
query = str(query.get(Text)).strip()
|
|
702
|
+
query = str(query).strip()
|
|
703
|
+
else:
|
|
704
|
+
query = ""
|
|
705
|
+
|
|
706
|
+
# Check if replying to a cached search result
|
|
707
|
+
reply_msg_id = None
|
|
708
|
+
if session.reply and hasattr(session.reply.origin, 'id'):
|
|
709
|
+
reply_msg_id = str(session.reply.origin.id)
|
|
710
|
+
|
|
711
|
+
# Quote + Index mode: Screenshot specific cached result
|
|
712
|
+
if reply_msg_id:
|
|
713
|
+
cached = search_cache.get(reply_msg_id)
|
|
714
|
+
if cached:
|
|
715
|
+
# Parse index from query
|
|
716
|
+
idx = parse_single_index(query)
|
|
717
|
+
if idx is None:
|
|
718
|
+
# No valid index - show prompt
|
|
719
|
+
await session.send("请指定序号 (1-10)")
|
|
720
|
+
search_cache.cleanup() # Lazy cleanup
|
|
721
|
+
return
|
|
722
|
+
|
|
723
|
+
if idx >= len(cached.results):
|
|
724
|
+
await session.send(f"序号超出范围 (1-{len(cached.results)})")
|
|
725
|
+
search_cache.cleanup()
|
|
726
|
+
return
|
|
727
|
+
|
|
728
|
+
# Screenshot the cached URL
|
|
729
|
+
target_result = cached.results[idx]
|
|
730
|
+
target_url = target_result.get("url", "")
|
|
731
|
+
if not target_url:
|
|
732
|
+
await session.send("该结果无有效URL")
|
|
733
|
+
search_cache.cleanup()
|
|
734
|
+
return
|
|
735
|
+
|
|
736
|
+
if conf.reaction:
|
|
737
|
+
asyncio.create_task(react(session, "📸"))
|
|
738
|
+
|
|
739
|
+
core = get_hyw_core()
|
|
740
|
+
b64_img = await core.screenshot(target_url)
|
|
741
|
+
|
|
742
|
+
if b64_img:
|
|
743
|
+
msg_chain = MessageChain(Image(src=f'data:image/jpeg;base64,{b64_img}'))
|
|
744
|
+
if conf.quote:
|
|
745
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
746
|
+
await session.send(msg_chain)
|
|
747
|
+
else:
|
|
748
|
+
await session.send(f"截图失败: {target_url}")
|
|
749
|
+
|
|
750
|
+
search_cache.cleanup()
|
|
751
|
+
return
|
|
752
|
+
else:
|
|
753
|
+
# Reply to a non-cached message: append reply content to query
|
|
754
|
+
try:
|
|
755
|
+
# session.reply.origin.message is a list, wrap it in MessageChain
|
|
756
|
+
reply_msg = MessageChain(session.reply.origin.message)
|
|
757
|
+
reply_content = str(reply_msg.get(Text)).strip()
|
|
758
|
+
if reply_content:
|
|
759
|
+
query = f"{query} {reply_content}".strip() if query else reply_content
|
|
760
|
+
logger.info(f"/w appended reply content, new query: '{query}'")
|
|
761
|
+
except Exception as e:
|
|
762
|
+
logger.warning(f"/w failed to extract reply content: {e}")
|
|
763
|
+
|
|
764
|
+
# No query and no cache context - nothing to do
|
|
765
|
+
if not query:
|
|
99
766
|
return
|
|
100
767
|
|
|
101
|
-
mc = MessageChain(res.all_param) # type: ignore
|
|
102
768
|
try:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
769
|
+
core = get_hyw_core()
|
|
770
|
+
|
|
771
|
+
# 1. URL Detection
|
|
772
|
+
url_pattern = re.compile(r'^https?://(?:[-\w./?=&%#]+)')
|
|
773
|
+
if url_pattern.match(query):
|
|
774
|
+
# === URL Screenshot Mode ===
|
|
775
|
+
if conf.reaction: asyncio.create_task(react(session, "📸"))
|
|
776
|
+
|
|
777
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
778
|
+
output_path = tf.name
|
|
779
|
+
|
|
780
|
+
b64_img = await core.screenshot(query)
|
|
781
|
+
|
|
782
|
+
if b64_img:
|
|
783
|
+
with open(output_path, "wb") as f:
|
|
784
|
+
f.write(base64.b64decode(b64_img))
|
|
785
|
+
|
|
786
|
+
msg_chain = MessageChain(Image(src=f'data:image/jpeg;base64,{b64_img}'))
|
|
787
|
+
if conf.quote:
|
|
788
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
115
789
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
790
|
+
await session.send(msg_chain)
|
|
791
|
+
|
|
792
|
+
if conf.save_conversation:
|
|
793
|
+
mid = str(session.event.message.id) if getattr(session.event, "message", None) else str(session.event.id)
|
|
794
|
+
context_id = f"guild_{session.guild.id}" if session.guild else "user"
|
|
795
|
+
history_manager.remember(mid, [{"role": "user", "content": f"/w {query}"}], [], {}, context_id=context_id)
|
|
796
|
+
history_manager.save_to_disk(mid, image_path=output_path, web_results=[{"url": query, "title": "Screenshot", "_type": "screenshot"}])
|
|
797
|
+
|
|
798
|
+
os.remove(output_path)
|
|
799
|
+
else:
|
|
800
|
+
await session.send(f"Failed to screenshot URL: {query}")
|
|
801
|
+
return
|
|
802
|
+
|
|
803
|
+
# 2. Search Mode (Fallthrough)
|
|
804
|
+
|
|
805
|
+
# Parse enhanced filter syntax
|
|
806
|
+
filters, search_query, filter_error = parse_filter_syntax(query, max_count=3)
|
|
807
|
+
|
|
808
|
+
if filter_error:
|
|
809
|
+
await session.send(filter_error)
|
|
810
|
+
return
|
|
811
|
+
|
|
812
|
+
# Start search first
|
|
813
|
+
local_renderer = await get_content_renderer()
|
|
814
|
+
search_task = asyncio.create_task(core.search([search_query]))
|
|
815
|
+
|
|
816
|
+
# Only pre-warm tab if NOT in filter mode (filter mode = screenshots only, no card render)
|
|
817
|
+
tab_task = None
|
|
818
|
+
if not filters:
|
|
819
|
+
tab_task = asyncio.create_task(local_renderer.prepare_tab())
|
|
820
|
+
|
|
821
|
+
if conf.reaction:
|
|
822
|
+
asyncio.create_task(react(session, "🔍"))
|
|
823
|
+
|
|
824
|
+
results = await search_task
|
|
825
|
+
flat_results = results[0] if results else []
|
|
826
|
+
|
|
827
|
+
if not flat_results:
|
|
828
|
+
if tab_task:
|
|
829
|
+
try: await tab_task
|
|
830
|
+
except: pass
|
|
831
|
+
await session.send("Search returned no results.")
|
|
832
|
+
return
|
|
833
|
+
|
|
834
|
+
visible = [r for r in flat_results if not r.get("_hidden", False)]
|
|
835
|
+
|
|
836
|
+
if not visible:
|
|
837
|
+
if tab_task:
|
|
838
|
+
try: await tab_task
|
|
839
|
+
except: pass
|
|
840
|
+
await session.send("Search returned no visible results.")
|
|
841
|
+
return
|
|
842
|
+
|
|
843
|
+
# === Filter Mode: Screenshot matching links (NO tab needed) ===
|
|
844
|
+
if filters:
|
|
845
|
+
|
|
846
|
+
urls_to_screenshot = []
|
|
847
|
+
|
|
848
|
+
for filter_type, filter_value, count in filters:
|
|
849
|
+
if filter_type == 'index':
|
|
850
|
+
# Index-based (1-based)
|
|
851
|
+
idx = filter_value - 1
|
|
852
|
+
if 0 <= idx < len(visible):
|
|
853
|
+
url = visible[idx].get("url", "")
|
|
854
|
+
if url and url not in urls_to_screenshot:
|
|
855
|
+
urls_to_screenshot.append(url)
|
|
856
|
+
else:
|
|
857
|
+
await session.send(f"⚠️ 序号 {filter_value} 超出范围 (1-{len(visible)})")
|
|
858
|
+
return
|
|
859
|
+
else:
|
|
860
|
+
# Link filter: find URLs containing filter term
|
|
861
|
+
found_count = 0
|
|
862
|
+
for res in visible:
|
|
863
|
+
url = res.get("url", "")
|
|
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:
|
|
867
|
+
urls_to_screenshot.append(url)
|
|
868
|
+
found_count += 1
|
|
869
|
+
if found_count >= count:
|
|
870
|
+
break
|
|
871
|
+
|
|
872
|
+
if found_count == 0:
|
|
873
|
+
await session.send(f"⚠️ 未找到包含 \"{filter_value}\" 的链接")
|
|
874
|
+
return
|
|
875
|
+
|
|
876
|
+
if not urls_to_screenshot:
|
|
877
|
+
await session.send("⚠️ 未找到匹配的链接")
|
|
878
|
+
return
|
|
879
|
+
|
|
880
|
+
if conf.reaction:
|
|
881
|
+
asyncio.create_task(react(session, "📸"))
|
|
882
|
+
|
|
883
|
+
# Take screenshots concurrently
|
|
884
|
+
screenshot_tasks = [core.screenshot(url) for url in urls_to_screenshot]
|
|
885
|
+
screenshot_results = await asyncio.gather(*screenshot_tasks)
|
|
886
|
+
|
|
887
|
+
images = [Image(src=f'data:image/jpeg;base64,{b64}') for b64 in screenshot_results if b64]
|
|
888
|
+
|
|
889
|
+
if images:
|
|
890
|
+
msg_chain = MessageChain(images)
|
|
891
|
+
if conf.quote:
|
|
892
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
893
|
+
await session.send(msg_chain)
|
|
894
|
+
|
|
895
|
+
if conf.save_conversation:
|
|
896
|
+
mid = str(session.event.message.id) if getattr(session.event, "message", None) else str(session.event.id)
|
|
897
|
+
context_id = f"guild_{session.guild.id}" if session.guild else "user"
|
|
898
|
+
history_manager.remember(mid, [{"role": "user", "content": f"/w {query}"}], [], {}, context_id=context_id)
|
|
899
|
+
else:
|
|
900
|
+
await session.send("截图失败")
|
|
901
|
+
return
|
|
902
|
+
|
|
903
|
+
# === Normal Search Mode: Render search results as Sources card ===
|
|
904
|
+
|
|
905
|
+
# Build references from search results for Sources card
|
|
906
|
+
references = []
|
|
907
|
+
for i, res in enumerate(visible[:10]):
|
|
908
|
+
references.append({
|
|
909
|
+
"title": res.get("title", f"Result {i+1}"),
|
|
910
|
+
"url": res.get("url", ""),
|
|
911
|
+
"snippet": res.get("content", "") or res.get("snippet", ""),
|
|
912
|
+
"original_idx": i + 1,
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
try:
|
|
916
|
+
tab_id = await tab_task
|
|
917
|
+
except Exception:
|
|
918
|
+
tab_id = None
|
|
919
|
+
|
|
920
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
921
|
+
output_path = tf.name
|
|
922
|
+
|
|
923
|
+
# Render Sources card with search results (no markdown content, just references)
|
|
924
|
+
render_ok = await core.render(
|
|
925
|
+
markdown_content=f"# 搜索结果: {search_query}",
|
|
926
|
+
output_path=output_path,
|
|
927
|
+
stats={"total_time": 0},
|
|
928
|
+
references=references,
|
|
929
|
+
page_references=[],
|
|
930
|
+
stages_used=[{"name": "search", "description": f"搜索 \"{search_query}\"", "time": 0}],
|
|
931
|
+
tab_id=tab_id
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
if render_ok and os.path.exists(output_path):
|
|
935
|
+
with open(output_path, "rb") as f:
|
|
936
|
+
img_data = base64.b64encode(f.read()).decode()
|
|
937
|
+
|
|
938
|
+
msg_chain = MessageChain(Image(src=f'data:image/png;base64,{img_data}'))
|
|
939
|
+
if conf.quote:
|
|
940
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
941
|
+
|
|
942
|
+
sent = await session.send(msg_chain)
|
|
943
|
+
|
|
944
|
+
# Store in cache for future /w and /q lookups
|
|
945
|
+
sent_id = next((str(e.id) for e in sent if hasattr(e, 'id')), None) if sent else None
|
|
946
|
+
if sent_id:
|
|
947
|
+
search_cache.store(sent_id, visible[:10], search_query)
|
|
948
|
+
|
|
949
|
+
if conf.save_conversation:
|
|
950
|
+
mid = str(session.event.message.id) if getattr(session.event, "message", None) else str(session.event.id)
|
|
951
|
+
context_id = f"guild_{session.guild.id}" if session.guild else "user"
|
|
952
|
+
history_manager.remember(mid, [{"role": "user", "content": f"/w {query}"}], [], {}, context_id=context_id)
|
|
953
|
+
|
|
954
|
+
os.remove(output_path)
|
|
955
|
+
else:
|
|
956
|
+
await session.send("渲染搜索结果失败")
|
|
957
|
+
|
|
958
|
+
search_cache.cleanup() # Lazy cleanup
|
|
959
|
+
|
|
138
960
|
except Exception as e:
|
|
139
|
-
|
|
140
|
-
|
|
961
|
+
logger.error(f"Search command failed: {e}")
|
|
962
|
+
await session.send(f"Search error: {e}")
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
metadata("hyw", author=[{"name": "kumoSleeping", "email": "zjr2992@outlook.com"}], version=__version__, config=HywConfig)
|
|
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)
|
|
141
1002
|
|
|
142
|
-
|
|
1003
|
+
@listen(CommandReceive)
|
|
1004
|
+
async def remove_at(content: MessageChain):
|
|
1005
|
+
return content.lstrip(At)
|