entari-plugin-hyw 4.0.0rc7__py3-none-any.whl → 4.0.0rc9__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.
- entari_plugin_hyw/Untitled-1 +1865 -0
- entari_plugin_hyw/__init__.py +726 -394
- entari_plugin_hyw/history.py +26 -13
- entari_plugin_hyw/misc.py +3 -0
- entari_plugin_hyw/search_cache.py +154 -0
- {entari_plugin_hyw-4.0.0rc7.dist-info → entari_plugin_hyw-4.0.0rc9.dist-info}/METADATA +3 -1
- entari_plugin_hyw-4.0.0rc9.dist-info/RECORD +68 -0
- {entari_plugin_hyw-4.0.0rc7.dist-info → entari_plugin_hyw-4.0.0rc9.dist-info}/WHEEL +1 -1
- {entari_plugin_hyw-4.0.0rc7.dist-info → entari_plugin_hyw-4.0.0rc9.dist-info}/top_level.txt +1 -0
- hyw_core/__init__.py +94 -0
- hyw_core/browser_control/__init__.py +65 -0
- hyw_core/browser_control/assets/card-dist/index.html +409 -0
- hyw_core/browser_control/assets/index.html +5691 -0
- hyw_core/browser_control/engines/__init__.py +17 -0
- {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/duckduckgo.py +42 -8
- {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/google.py +1 -1
- {entari_plugin_hyw/browser → hyw_core/browser_control}/manager.py +15 -8
- entari_plugin_hyw/render_vue.py → hyw_core/browser_control/renderer.py +29 -14
- {entari_plugin_hyw/browser → hyw_core/browser_control}/service.py +340 -112
- hyw_core/config.py +154 -0
- hyw_core/core.py +322 -0
- hyw_core/definitions.py +83 -0
- entari_plugin_hyw/modular_pipeline.py → hyw_core/pipeline.py +121 -97
- {entari_plugin_hyw → hyw_core}/search.py +19 -14
- hyw_core/stages/__init__.py +21 -0
- entari_plugin_hyw/stage_base.py → hyw_core/stages/base.py +2 -2
- entari_plugin_hyw/stage_summary.py → hyw_core/stages/summary.py +34 -11
- entari_plugin_hyw/assets/card-dist/index.html +0 -387
- entari_plugin_hyw/browser/__init__.py +0 -10
- entari_plugin_hyw/browser/engines/bing.py +0 -95
- entari_plugin_hyw/card-ui/.gitignore +0 -24
- entari_plugin_hyw/card-ui/README.md +0 -5
- entari_plugin_hyw/card-ui/index.html +0 -16
- entari_plugin_hyw/card-ui/package-lock.json +0 -2342
- entari_plugin_hyw/card-ui/package.json +0 -31
- entari_plugin_hyw/card-ui/public/logos/anthropic.svg +0 -1
- entari_plugin_hyw/card-ui/public/logos/cerebras.svg +0 -9
- entari_plugin_hyw/card-ui/public/logos/deepseek.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/gemini.svg +0 -1
- entari_plugin_hyw/card-ui/public/logos/google.svg +0 -1
- entari_plugin_hyw/card-ui/public/logos/grok.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/huggingface.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/microsoft.svg +0 -15
- entari_plugin_hyw/card-ui/public/logos/minimax.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/mistral.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/nvida.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/openai.svg +0 -1
- entari_plugin_hyw/card-ui/public/logos/openrouter.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/perplexity.svg +0 -24
- entari_plugin_hyw/card-ui/public/logos/qwen.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xai.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/xiaomi.png +0 -0
- entari_plugin_hyw/card-ui/public/logos/zai.png +0 -0
- entari_plugin_hyw/card-ui/public/vite.svg +0 -1
- entari_plugin_hyw/card-ui/src/App.vue +0 -787
- entari_plugin_hyw/card-ui/src/assets/vue.svg +0 -1
- entari_plugin_hyw/card-ui/src/components/HelloWorld.vue +0 -41
- entari_plugin_hyw/card-ui/src/components/MarkdownContent.vue +0 -382
- entari_plugin_hyw/card-ui/src/components/SectionCard.vue +0 -41
- entari_plugin_hyw/card-ui/src/components/StageCard.vue +0 -240
- entari_plugin_hyw/card-ui/src/main.ts +0 -5
- entari_plugin_hyw/card-ui/src/style.css +0 -29
- entari_plugin_hyw/card-ui/src/test_regex.js +0 -103
- entari_plugin_hyw/card-ui/src/types.ts +0 -61
- entari_plugin_hyw/card-ui/tsconfig.app.json +0 -16
- entari_plugin_hyw/card-ui/tsconfig.json +0 -7
- entari_plugin_hyw/card-ui/tsconfig.node.json +0 -26
- entari_plugin_hyw/card-ui/vite.config.ts +0 -16
- entari_plugin_hyw/definitions.py +0 -174
- entari_plugin_hyw/stage_instruct.py +0 -355
- entari_plugin_hyw/stage_instruct_deepsearch.py +0 -104
- entari_plugin_hyw/stage_vision.py +0 -113
- entari_plugin_hyw-4.0.0rc7.dist-info/RECORD +0 -102
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/anthropic.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/cerebras.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/deepseek.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/gemini.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/google.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/grok.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/huggingface.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/microsoft.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/minimax.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/mistral.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/nvida.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/openai.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/openrouter.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/perplexity.svg +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/qwen.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/xai.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/xiaomi.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/logos/zai.png +0 -0
- {entari_plugin_hyw → hyw_core/browser_control}/assets/card-dist/vite.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/anthropic.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/cerebras.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/deepseek.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/gemini.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/google.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/grok.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/huggingface.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/microsoft.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/minimax.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/mistral.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/nvida.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/openai.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/openrouter.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/perplexity.svg +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/qwen.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/xai.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/xiaomi.png +0 -0
- {entari_plugin_hyw/assets/icon → hyw_core/browser_control/assets/logos}/zai.png +0 -0
- {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/base.py +0 -0
- {entari_plugin_hyw/browser → hyw_core/browser_control}/engines/default.py +0 -0
- {entari_plugin_hyw/browser → hyw_core/browser_control}/landing.html +0 -0
- {entari_plugin_hyw → hyw_core}/image_cache.py +0 -0
entari_plugin_hyw/__init__.py
CHANGED
|
@@ -1,62 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
entari-plugin-hyw - Entari Plugin for HYW
|
|
3
|
+
|
|
4
|
+
Use large language models to interpret chat messages.
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
from dataclasses import dataclass, field
|
|
2
8
|
from importlib.metadata import version as get_version
|
|
3
|
-
from typing import List, Dict, Any, Optional
|
|
9
|
+
from typing import List, Dict, Any, Optional
|
|
4
10
|
import time
|
|
5
11
|
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
import secrets
|
|
14
|
+
import base64
|
|
15
|
+
import re
|
|
16
|
+
import tempfile
|
|
6
17
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
__version__ = get_version("entari_plugin_hyw")
|
|
10
|
-
except Exception:
|
|
11
|
-
__version__ = "0.0.0"
|
|
12
|
-
|
|
13
|
-
from arclet.alconna import Alconna, Args, AllParam, CommandMeta, Option, Arparma, MultiVar, store_true
|
|
14
|
-
from arclet.entari import metadata, listen, Session, plugin_config, BasicConfModel, plugin, command
|
|
15
|
-
from arclet.letoderea import on
|
|
18
|
+
from arclet.alconna import Alconna, Args, AllParam, Arparma
|
|
19
|
+
from arclet.entari import metadata, listen, Session, plugin_config, BasicConfModel, command
|
|
16
20
|
from arclet.entari import MessageChain, Text, Image, MessageCreatedEvent, Quote, At
|
|
17
21
|
from satori.element import Custom
|
|
18
22
|
from loguru import logger
|
|
19
|
-
import arclet.letoderea as leto
|
|
20
23
|
from arclet.entari.event.command import CommandReceive
|
|
21
|
-
|
|
22
|
-
from .modular_pipeline import ModularPipeline
|
|
23
|
-
from .history import HistoryManager
|
|
24
|
-
from .render_vue import ContentRenderer, get_content_renderer
|
|
25
|
-
from .misc import process_onebot_json, process_images, resolve_model_name, render_refuse_answer, render_image_unsupported, REFUSE_ANSWER_MARKDOWN
|
|
26
24
|
from arclet.entari.event.lifespan import Cleanup
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
import
|
|
30
|
-
import
|
|
26
|
+
# Import from internal hyw_core
|
|
27
|
+
from hyw_core import HywCore, HywCoreConfig, QueryRequest
|
|
28
|
+
from hyw_core.browser_control import (
|
|
29
|
+
ContentRenderer,
|
|
30
|
+
get_content_renderer,
|
|
31
|
+
set_global_renderer,
|
|
32
|
+
close_screenshot_service,
|
|
33
|
+
)
|
|
34
|
+
from hyw_core.browser_control.manager import close_shared_browser
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
# Local modules
|
|
37
|
+
from .history import HistoryManager
|
|
38
|
+
from .misc import (
|
|
39
|
+
process_onebot_json,
|
|
40
|
+
process_images,
|
|
41
|
+
resolve_model_name,
|
|
42
|
+
render_refuse_answer,
|
|
43
|
+
render_image_unsupported,
|
|
44
|
+
)
|
|
45
|
+
from .search_cache import SearchResultCache, parse_single_index, parse_multi_indices
|
|
33
46
|
|
|
34
47
|
|
|
35
|
-
def
|
|
48
|
+
def parse_filter_syntax(query: str, max_count: int = 3):
|
|
36
49
|
"""
|
|
37
|
-
Parse
|
|
38
|
-
|
|
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
|
|
39
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
|
+
return filters, search_query, None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
__version__ = get_version("entari_plugin_hyw")
|
|
116
|
+
except Exception:
|
|
117
|
+
__version__ = "4.0.0-rc8"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parse_color(color: str) -> str:
|
|
40
121
|
if not color:
|
|
41
122
|
return "#ef4444"
|
|
42
|
-
|
|
43
123
|
color = str(color).strip()
|
|
44
|
-
|
|
45
|
-
# Hex format: #fff or #ffffff or ffffff
|
|
46
124
|
if color.startswith('#') and len(color) in [4, 7]:
|
|
47
125
|
return color
|
|
48
126
|
if re.match(r'^[0-9a-fA-F]{6}$', color):
|
|
49
127
|
return f'#{color}'
|
|
50
|
-
|
|
51
|
-
# RGB tuple: (r, g, b) or r,g,b
|
|
52
128
|
rgb_match = re.match(r'^\(?(\d+)[,\s]+(\d+)[,\s]+(\d+)\)?$', color)
|
|
53
129
|
if rgb_match:
|
|
54
130
|
r, g, b = (max(0, min(255, int(x))) for x in rgb_match.groups())
|
|
55
131
|
return f'#{r:02x}{g:02x}{b:02x}'
|
|
56
|
-
|
|
57
|
-
logger.warning(f"Invalid color '{color}', using default #ef4444")
|
|
58
132
|
return "#ef4444"
|
|
59
133
|
|
|
134
|
+
|
|
60
135
|
class _RecentEventDeduper:
|
|
61
136
|
def __init__(self, ttl_seconds: float = 30.0, max_size: int = 2048):
|
|
62
137
|
self.ttl_seconds = ttl_seconds
|
|
@@ -77,158 +152,104 @@ class _RecentEventDeduper:
|
|
|
77
152
|
expired = [k for k, ts in self._seen.items() if now - ts > self.ttl_seconds]
|
|
78
153
|
for k in expired:
|
|
79
154
|
self._seen.pop(k, None)
|
|
80
|
-
if len(self._seen) > self.max_size:
|
|
81
|
-
for k, _ in sorted(self._seen.items(), key=lambda kv: kv[1])[: len(self._seen) - self.max_size]:
|
|
82
|
-
self._seen.pop(k, None)
|
|
83
155
|
|
|
84
156
|
_event_deduper = _RecentEventDeduper()
|
|
85
157
|
|
|
86
|
-
@dataclass
|
|
87
|
-
class ModelConfig:
|
|
88
|
-
"""Model configuration for a specific stage."""
|
|
89
|
-
model_name: Optional[str] = None
|
|
90
|
-
api_key: Optional[str] = None
|
|
91
|
-
base_url: Optional[str] = None
|
|
92
|
-
extra_body: Optional[Dict[str, Any]] = None
|
|
93
|
-
model_provider: Optional[str] = None
|
|
94
|
-
input_price: Optional[float] = None
|
|
95
|
-
output_price: Optional[float] = None
|
|
96
|
-
image_input: bool = True
|
|
97
|
-
|
|
98
158
|
|
|
99
159
|
@dataclass
|
|
100
160
|
class HywConfig(BasicConfModel):
|
|
101
|
-
|
|
161
|
+
"""Plugin configuration"""
|
|
102
162
|
admins: List[str] = field(default_factory=list)
|
|
103
163
|
models: List[Dict[str, Any]] = field(default_factory=list)
|
|
104
164
|
question_command: str = "/q"
|
|
105
165
|
language: str = "Simplified Chinese"
|
|
106
166
|
temperature: float = 0.4
|
|
107
167
|
|
|
108
|
-
# Root-level defaults (backward compatible)
|
|
109
168
|
model_name: Optional[str] = None
|
|
110
169
|
api_key: Optional[str] = None
|
|
111
170
|
base_url: str = "https://openrouter.ai/api/v1"
|
|
112
|
-
|
|
113
|
-
model_provider: Optional[str] = None
|
|
114
|
-
input_price: Optional[float] = None
|
|
115
|
-
output_price: Optional[float] = None
|
|
116
|
-
|
|
117
|
-
# Nested Stage Configs
|
|
118
|
-
instruct: Optional[ModelConfig] = None
|
|
119
|
-
qa: Optional[ModelConfig] = None
|
|
120
|
-
main: Optional[ModelConfig] = None # Summary stage
|
|
121
|
-
vision: Optional[ModelConfig] = None # Vision description stage
|
|
122
|
-
|
|
123
|
-
# Search/Fetch Settings
|
|
171
|
+
|
|
124
172
|
search_engine: str = "google"
|
|
125
173
|
|
|
126
|
-
# Rendering Settings
|
|
127
174
|
headless: bool = False
|
|
128
|
-
render_timeout_ms: int = 6000
|
|
129
|
-
render_image_timeout_ms: int = 3000
|
|
130
|
-
|
|
131
|
-
# Bot Behavior
|
|
132
175
|
save_conversation: bool = False
|
|
133
176
|
reaction: bool = False
|
|
134
177
|
quote: bool = False
|
|
135
|
-
|
|
136
|
-
# UI Theme
|
|
137
178
|
theme_color: str = "#ff0000"
|
|
179
|
+
|
|
180
|
+
# Nested configurations
|
|
181
|
+
main: Optional[Dict[str, Any]] = None
|
|
182
|
+
instruct: Optional[Dict[str, Any]] = None
|
|
183
|
+
vision: Optional[Dict[str, Any]] = None
|
|
184
|
+
deepsearch_instruct: Optional[Dict[str, Any]] = None
|
|
185
|
+
deepsearch_agent: Optional[Dict[str, Any]] = None
|
|
138
186
|
|
|
139
187
|
def __post_init__(self):
|
|
140
|
-
"""Parse and normalize theme color after initialization."""
|
|
141
188
|
self.theme_color = parse_color(self.theme_color)
|
|
142
|
-
# Convert dicts to ModelConfig if needed
|
|
143
|
-
if isinstance(self.instruct, dict):
|
|
144
|
-
self.instruct = ModelConfig(**self.instruct)
|
|
145
|
-
if isinstance(self.qa, dict):
|
|
146
|
-
self.qa = ModelConfig(**self.qa)
|
|
147
|
-
if isinstance(self.main, dict):
|
|
148
|
-
self.main = ModelConfig(**self.main)
|
|
149
|
-
if isinstance(self.vision, dict):
|
|
150
|
-
self.vision = ModelConfig(**self.vision)
|
|
151
189
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
stage: "instruct", "qa", or "main" (summary)
|
|
158
|
-
|
|
159
|
-
Returns:
|
|
160
|
-
Dict with model_name, api_key, base_url, extra_body, etc.
|
|
161
|
-
"""
|
|
162
|
-
# Determine primary and secondary config sources
|
|
163
|
-
primary = None
|
|
164
|
-
secondary = None
|
|
165
|
-
|
|
166
|
-
if stage == "instruct":
|
|
167
|
-
primary = self.instruct
|
|
168
|
-
secondary = self.main # Fallback to main
|
|
169
|
-
elif stage == "qa":
|
|
170
|
-
# QA fallback to main as well if ever used
|
|
171
|
-
primary = self.qa
|
|
172
|
-
secondary = self.main
|
|
173
|
-
elif stage == "main":
|
|
174
|
-
primary = self.main
|
|
175
|
-
elif stage == "vision":
|
|
176
|
-
primary = self.vision
|
|
177
|
-
secondary = self.main # Fallback to main if vision not fully configured
|
|
178
|
-
|
|
179
|
-
# Build result with fallback logic
|
|
180
|
-
def resolve(field_name: str, is_essential: bool = True):
|
|
181
|
-
"""Resolve a field with fallback: Primary -> Secondary -> Root."""
|
|
182
|
-
# 1. Try Primary
|
|
183
|
-
val = getattr(primary, field_name, None) if primary else None
|
|
184
|
-
|
|
185
|
-
# 2. Try Secondary (if value missing)
|
|
186
|
-
if val is None and secondary:
|
|
187
|
-
val = getattr(secondary, field_name, None)
|
|
188
|
-
|
|
189
|
-
# 3. Try Root (if value still missing)
|
|
190
|
-
if val is None:
|
|
191
|
-
val = getattr(self, field_name, None)
|
|
192
|
-
return val
|
|
190
|
+
def to_hyw_core_config(self) -> HywCoreConfig:
|
|
191
|
+
main_cfg = self.main or {}
|
|
192
|
+
instruct_cfg = self.instruct or {}
|
|
193
193
|
|
|
194
|
+
return HywCoreConfig.from_dict({
|
|
195
|
+
"models": self.models,
|
|
196
|
+
"model_name": self.model_name or "",
|
|
197
|
+
"api_key": self.api_key or "",
|
|
198
|
+
"base_url": self.base_url,
|
|
199
|
+
"temperature": self.temperature,
|
|
200
|
+
"search_engine": self.search_engine,
|
|
201
|
+
"headless": self.headless,
|
|
202
|
+
"language": self.language,
|
|
203
|
+
"theme_color": self.theme_color,
|
|
204
|
+
|
|
205
|
+
# Map nested 'main' config to summary stage
|
|
206
|
+
"summary_model": main_cfg.get("model_name"),
|
|
207
|
+
"summary_api_key": main_cfg.get("api_key"),
|
|
208
|
+
"summary_base_url": main_cfg.get("base_url"),
|
|
209
|
+
"summary_extra_body": main_cfg.get("extra_body"),
|
|
210
|
+
|
|
211
|
+
# Map nested 'instruct' config to instruct stage
|
|
212
|
+
"instruct_model": instruct_cfg.get("model_name"),
|
|
213
|
+
"instruct_api_key": instruct_cfg.get("api_key"),
|
|
214
|
+
"instruct_base_url": instruct_cfg.get("base_url"),
|
|
215
|
+
"instruct_extra_body": instruct_cfg.get("extra_body"),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
def get_model_config(self, stage: str) -> Dict[str, Any]:
|
|
194
219
|
return {
|
|
195
|
-
"model_name":
|
|
196
|
-
"api_key":
|
|
197
|
-
"base_url":
|
|
198
|
-
"extra_body": resolve("extra_body", is_essential=False),
|
|
199
|
-
"model_provider": resolve("model_provider", is_essential=False),
|
|
200
|
-
"input_price": resolve("input_price", is_essential=False),
|
|
201
|
-
"output_price": resolve("output_price", is_essential=False),
|
|
220
|
+
"model_name": self.model_name,
|
|
221
|
+
"api_key": self.api_key,
|
|
222
|
+
"base_url": self.base_url,
|
|
202
223
|
}
|
|
203
224
|
|
|
204
225
|
|
|
205
226
|
conf = plugin_config(HywConfig)
|
|
206
227
|
history_manager = HistoryManager()
|
|
207
228
|
renderer = ContentRenderer(headless=conf.headless)
|
|
208
|
-
from .render_vue import set_global_renderer
|
|
209
229
|
set_global_renderer(renderer)
|
|
230
|
+
search_cache = SearchResultCache(ttl_seconds=600.0) # 10 minutes
|
|
210
231
|
|
|
211
|
-
|
|
212
|
-
from .browser.service import prestart_browser, close_screenshot_service
|
|
213
|
-
# prestart_browser(headless=conf.headless) # Removed to avoid RuntimeError: no running event loop
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
class GlobalCache:
|
|
217
|
-
models_image_path: Optional[str] = None
|
|
232
|
+
_hyw_core: Optional[HywCore] = None
|
|
218
233
|
|
|
219
|
-
|
|
234
|
+
def get_hyw_core() -> HywCore:
|
|
235
|
+
global _hyw_core
|
|
236
|
+
if _hyw_core is None:
|
|
237
|
+
_hyw_core = HywCore(conf.to_hyw_core_config())
|
|
238
|
+
return _hyw_core
|
|
220
239
|
|
|
221
240
|
|
|
222
241
|
@listen(Cleanup)
|
|
223
242
|
async def cleanup_screenshot_service():
|
|
224
|
-
|
|
243
|
+
global _hyw_core
|
|
225
244
|
try:
|
|
245
|
+
if _hyw_core:
|
|
246
|
+
await _hyw_core.close()
|
|
247
|
+
_hyw_core = None
|
|
226
248
|
await close_screenshot_service()
|
|
227
|
-
|
|
228
|
-
from .browser.manager import close_shared_browser
|
|
229
|
-
await close_shared_browser()
|
|
249
|
+
close_shared_browser()
|
|
230
250
|
except Exception as e:
|
|
231
|
-
logger.warning(f"Failed to cleanup
|
|
251
|
+
logger.warning(f"Failed to cleanup: {e}")
|
|
252
|
+
|
|
232
253
|
|
|
233
254
|
async def react(session: Session, emoji: str):
|
|
234
255
|
if not conf.reaction: return
|
|
@@ -237,52 +258,35 @@ async def react(session: Session, emoji: str):
|
|
|
237
258
|
except Exception as e:
|
|
238
259
|
logger.warning(f"Reaction failed: {e}")
|
|
239
260
|
|
|
261
|
+
|
|
240
262
|
async def process_request(
|
|
241
263
|
session: Session[MessageCreatedEvent],
|
|
242
264
|
all_param: Optional[MessageChain] = None,
|
|
243
265
|
selected_model: Optional[str] = None,
|
|
244
|
-
selected_vision_model: Optional[str] = None,
|
|
245
|
-
conversation_key_override: Optional[str] = None,
|
|
246
|
-
local_mode: bool = False,
|
|
247
266
|
) -> None:
|
|
248
267
|
mc = MessageChain(all_param)
|
|
249
268
|
if session.reply:
|
|
250
269
|
try:
|
|
251
|
-
# Check if reply is from self (the bot)
|
|
252
|
-
# 1. Check by Message ID (reliable for bot's own messages if recorded)
|
|
253
270
|
reply_msg_id = str(session.reply.origin.id) if hasattr(session.reply.origin, 'id') else None
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if reply_msg_id and history_manager.is_bot_message(reply_msg_id):
|
|
257
|
-
is_bot = True
|
|
258
|
-
|
|
259
|
-
if is_bot:
|
|
260
|
-
pass # Reply is from bot - ignoring
|
|
261
|
-
else:
|
|
271
|
+
if not (reply_msg_id and history_manager.is_bot_message(reply_msg_id)):
|
|
262
272
|
mc.extend(MessageChain(" ") + session.reply.origin.message)
|
|
263
|
-
except Exception
|
|
264
|
-
logger.warning(f"Failed to process reply origin: {e}")
|
|
273
|
+
except Exception:
|
|
265
274
|
mc.extend(MessageChain(" ") + session.reply.origin.message)
|
|
266
275
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
276
|
+
filtered = mc.get(Text) + mc.get(Image) + mc.get(Custom)
|
|
277
|
+
mc = MessageChain(filtered)
|
|
278
|
+
|
|
272
279
|
text_content = str(mc.get(Text)).strip()
|
|
273
|
-
# Remove HTML image tags from text content to prevent "unreasonable code behavior"
|
|
274
280
|
text_content = re.sub(r'<img[^>]+>', '', text_content, flags=re.IGNORECASE)
|
|
275
|
-
|
|
281
|
+
|
|
276
282
|
if not text_content and not mc.get(Image) and not mc.get(Custom):
|
|
277
283
|
return
|
|
278
284
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if not hist_key and session.reply and hasattr(session.reply.origin, 'id'):
|
|
285
|
+
hist_key = None
|
|
286
|
+
if session.reply and hasattr(session.reply.origin, 'id'):
|
|
282
287
|
hist_key = history_manager.get_conversation_id(str(session.reply.origin.id))
|
|
283
288
|
|
|
284
289
|
hist_payload = history_manager.get_history(hist_key) if hist_key else []
|
|
285
|
-
meta = history_manager.get_metadata(hist_key) if hist_key else {}
|
|
286
290
|
context_id = f"guild_{session.guild.id}" if session.guild else f"user_{session.user.id}"
|
|
287
291
|
|
|
288
292
|
if conf.reaction: await react(session, "✨")
|
|
@@ -291,87 +295,29 @@ async def process_request(
|
|
|
291
295
|
msg_text = str(mc.get(Text)).strip() if mc.get(Text) else ""
|
|
292
296
|
msg_text = re.sub(r'<img[^>]+>', '', msg_text, flags=re.IGNORECASE)
|
|
293
297
|
|
|
294
|
-
# If message is empty but has images, use a placeholder
|
|
295
298
|
if not msg_text and (mc.get(Image) or mc.get(Custom)):
|
|
296
|
-
|
|
299
|
+
msg_text = "[图片]"
|
|
297
300
|
|
|
298
301
|
for custom in [e for e in mc if isinstance(e, Custom)]:
|
|
299
302
|
if custom.tag == 'onebot:json':
|
|
300
|
-
if decoded := process_onebot_json(custom.attributes()):
|
|
303
|
+
if decoded := process_onebot_json(custom.attributes()):
|
|
304
|
+
msg_text += f"\n{decoded}"
|
|
301
305
|
break
|
|
302
306
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if model and model != "off":
|
|
307
|
-
resolved, err = resolve_model_name(model, conf.models)
|
|
307
|
+
model = selected_model
|
|
308
|
+
if model:
|
|
309
|
+
resolved, _ = resolve_model_name(model, conf.models)
|
|
308
310
|
if resolved:
|
|
309
311
|
model = resolved
|
|
310
|
-
elif err:
|
|
311
|
-
logger.warning(f"Model resolution warning for {model}: {err}")
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
if vision_model and vision_model != "off":
|
|
315
|
-
resolved_v, err_v = resolve_model_name(vision_model, conf.models)
|
|
316
|
-
if resolved_v:
|
|
317
|
-
vision_model = resolved_v
|
|
318
|
-
elif err_v:
|
|
319
|
-
logger.warning(f"Vision model resolution warning for {vision_model}: {err_v}")
|
|
320
|
-
|
|
321
|
-
images, err = await process_images(mc, vision_model)
|
|
322
|
-
|
|
323
|
-
# Check image input support
|
|
324
|
-
model_cfg_dict = next((m for m in conf.models if m.get("name") == model), None)
|
|
325
|
-
image_input_supported = True
|
|
326
|
-
if model_cfg_dict:
|
|
327
|
-
image_input_supported = model_cfg_dict.get("image_input", True)
|
|
313
|
+
images, _ = await process_images(mc, None)
|
|
328
314
|
|
|
329
|
-
#
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if images and not image_input_supported:
|
|
334
|
-
logger.warning(f"Model '{model}' does not support images, but user sent {len(images)} images.")
|
|
335
|
-
|
|
336
|
-
# Start renderer for the unsupported card
|
|
337
|
-
renderer = await get_content_renderer()
|
|
338
|
-
render_tab_task = asyncio.create_task(renderer.prepare_tab())
|
|
339
|
-
|
|
340
|
-
# Wait for tab and render unsupported
|
|
341
|
-
try:
|
|
342
|
-
tab_id = await render_tab_task
|
|
343
|
-
except Exception as e:
|
|
344
|
-
tab_id = None
|
|
345
|
-
|
|
346
|
-
import tempfile
|
|
347
|
-
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
348
|
-
output_path = tf.name
|
|
349
|
-
|
|
350
|
-
render_ok = await render_image_unsupported(
|
|
351
|
-
renderer=renderer,
|
|
352
|
-
output_path=output_path,
|
|
353
|
-
theme_color=conf.theme_color,
|
|
354
|
-
tab_id=tab_id
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
if render_ok:
|
|
358
|
-
with open(output_path, "rb") as f:
|
|
359
|
-
img_data = base64.b64encode(f.read()).decode()
|
|
360
|
-
await session.send(MessageChain(Image(src=f'data:image/png;base64,{img_data}')))
|
|
361
|
-
if os.path.exists(output_path):
|
|
362
|
-
os.remove(output_path)
|
|
363
|
-
return
|
|
364
|
-
|
|
365
|
-
renderer = await get_content_renderer()
|
|
366
|
-
render_tab_task = asyncio.create_task(renderer.prepare_tab())
|
|
367
|
-
tab_id = None
|
|
315
|
+
# Prepare renderer
|
|
316
|
+
local_renderer = await get_content_renderer()
|
|
317
|
+
render_tab_task = asyncio.create_task(local_renderer.prepare_tab())
|
|
368
318
|
|
|
369
|
-
# Call Pipeline directly
|
|
370
|
-
safe_input = msg_text
|
|
371
|
-
|
|
372
319
|
async def send_noti(msg: str):
|
|
373
320
|
try:
|
|
374
|
-
# Send simple text notification
|
|
375
321
|
if conf.quote:
|
|
376
322
|
await session.send([Quote(session.event.message.id), msg])
|
|
377
323
|
else:
|
|
@@ -379,215 +325,601 @@ async def process_request(
|
|
|
379
325
|
except Exception as e:
|
|
380
326
|
logger.warning(f"Failed to send notification: {e}")
|
|
381
327
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
selected_vision_model=vision_model,
|
|
390
|
-
)
|
|
391
|
-
finally:
|
|
392
|
-
await pipeline.close()
|
|
393
|
-
|
|
394
|
-
# Step 1 Results
|
|
395
|
-
step1_vision_model = resp.get("vision_model_used")
|
|
396
|
-
step1_model = resp.get("model_used")
|
|
397
|
-
step1_history = resp.get("conversation_history", [])
|
|
398
|
-
step1_stats = resp.get("stats", {})
|
|
399
|
-
|
|
400
|
-
final_resp = resp
|
|
328
|
+
request = QueryRequest(
|
|
329
|
+
user_input=msg_text,
|
|
330
|
+
images=images,
|
|
331
|
+
conversation_history=hist_payload,
|
|
332
|
+
model_name=model,
|
|
333
|
+
send_notification=send_noti
|
|
334
|
+
)
|
|
401
335
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
336
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
337
|
+
output_path = tf.name
|
|
405
338
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
339
|
+
core = get_hyw_core()
|
|
340
|
+
# 1. Query ONLY (no render path provided)
|
|
341
|
+
# Pass output_path=None so it returns raw response without internal rendering
|
|
342
|
+
response = await core.query(request, output_path=None)
|
|
409
343
|
|
|
410
|
-
#
|
|
344
|
+
# 2. Get the warmed-up tab
|
|
411
345
|
try:
|
|
412
346
|
tab_id = await render_tab_task
|
|
413
|
-
except Exception
|
|
414
|
-
logger.warning(f"Failed to prepare render tab: {e}")
|
|
347
|
+
except Exception:
|
|
415
348
|
tab_id = None
|
|
416
349
|
|
|
417
|
-
|
|
418
|
-
import tempfile
|
|
419
|
-
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
420
|
-
output_path = tf.name
|
|
421
|
-
model_used = final_resp.get("model_used")
|
|
350
|
+
display_session_id = history_manager.generate_short_code()
|
|
422
351
|
|
|
423
|
-
|
|
424
|
-
if hist_key:
|
|
425
|
-
display_session_id = history_manager.get_code_by_key(hist_key)
|
|
426
|
-
if not display_session_id:
|
|
427
|
-
display_session_id = history_manager.generate_short_code()
|
|
428
|
-
else:
|
|
429
|
-
display_session_id = history_manager.generate_short_code()
|
|
430
|
-
|
|
431
|
-
# Use stats_list if available, otherwise standard stats
|
|
432
|
-
stats_to_render = final_resp.get("stats_list", final_resp.get("stats", {}))
|
|
433
|
-
|
|
434
|
-
# Check if refuse_answer was triggered
|
|
435
|
-
if final_resp.get("refuse_answer"):
|
|
436
|
-
logger.info(f"Refuse answer triggered. Rendering refuse image. Reason: {final_resp.get('refuse_reason', '')}")
|
|
352
|
+
if response.should_refuse:
|
|
437
353
|
render_ok = await render_refuse_answer(
|
|
438
|
-
renderer=
|
|
354
|
+
renderer=local_renderer,
|
|
439
355
|
output_path=output_path,
|
|
440
|
-
reason=
|
|
356
|
+
reason=response.refuse_reason or 'Refused',
|
|
441
357
|
theme_color=conf.theme_color,
|
|
442
358
|
tab_id=tab_id,
|
|
443
359
|
)
|
|
360
|
+
elif not response.success:
|
|
361
|
+
await session.send(f"Error: {response.error}")
|
|
362
|
+
return
|
|
444
363
|
else:
|
|
445
|
-
|
|
446
|
-
render_ok = await
|
|
447
|
-
markdown_content=content,
|
|
364
|
+
# 3. Explicit External Render using the Parallel Tab
|
|
365
|
+
render_ok = await core.render(
|
|
366
|
+
markdown_content=response.content,
|
|
448
367
|
output_path=output_path,
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
theme_color=conf.theme_color,
|
|
368
|
+
stats={"total_time": response.total_time},
|
|
369
|
+
references=response.references,
|
|
370
|
+
page_references=response.page_references,
|
|
371
|
+
image_references=response.image_references,
|
|
372
|
+
stages_used=response.stages_used,
|
|
373
|
+
tab_id=tab_id
|
|
456
374
|
)
|
|
457
|
-
|
|
375
|
+
if render_ok:
|
|
376
|
+
response.image_path = output_path
|
|
458
377
|
|
|
459
|
-
|
|
460
|
-
if not render_ok:
|
|
461
|
-
logger.error("Render failed; skipping reply.")
|
|
462
|
-
if os.path.exists(output_path):
|
|
463
|
-
try:
|
|
464
|
-
os.remove(output_path)
|
|
465
|
-
except Exception as exc:
|
|
466
|
-
logger.warning(f"Failed to delete render output {output_path}: {exc}")
|
|
467
|
-
sent = None
|
|
468
|
-
else:
|
|
469
|
-
# Convert to base64
|
|
378
|
+
if render_ok:
|
|
470
379
|
with open(output_path, "rb") as f:
|
|
471
380
|
img_data = base64.b64encode(f.read()).decode()
|
|
472
|
-
|
|
473
|
-
# Build single reply chain (image only now)
|
|
474
|
-
elements = []
|
|
475
|
-
elements.append(Image(src=f'data:image/png;base64,{img_data}'))
|
|
476
|
-
|
|
477
|
-
msg_chain = MessageChain(*elements)
|
|
478
381
|
|
|
382
|
+
msg_chain = MessageChain(Image(src=f'data:image/png;base64,{img_data}'))
|
|
479
383
|
if conf.quote:
|
|
480
384
|
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
481
|
-
|
|
482
|
-
# Use reply_to instead of manual Quote insertion to avoid ActionFailed errors
|
|
385
|
+
|
|
483
386
|
sent = await session.send(msg_chain)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
387
|
+
|
|
388
|
+
sent_id = next((str(e.id) for e in sent if hasattr(e, 'id')), None) if sent else None
|
|
389
|
+
msg_id = str(session.event.message.id) if hasattr(session.event, 'message') else str(session.event.id)
|
|
390
|
+
|
|
391
|
+
updated_history = hist_payload + [
|
|
392
|
+
{"role": "user", "content": msg_text},
|
|
393
|
+
{"role": "assistant", "content": response.content}
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
# Save to Memory
|
|
397
|
+
history_manager.remember(
|
|
398
|
+
sent_id, updated_history, [msg_id],
|
|
399
|
+
{"model": model}, context_id, code=display_session_id,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Save to Disk (Debug/Logging)
|
|
403
|
+
if conf.save_conversation:
|
|
404
|
+
# Extract traces from response
|
|
405
|
+
trace = response.stages_trace
|
|
406
|
+
instruct_traces = trace.get("instruct_rounds") if trace else None
|
|
407
|
+
|
|
408
|
+
# Check for web_results in response (needs Core update)
|
|
409
|
+
web_results = getattr(response, "web_results", [])
|
|
410
|
+
|
|
505
411
|
history_manager.save_to_disk(
|
|
506
|
-
sent_id,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
412
|
+
key=sent_id,
|
|
413
|
+
image_path=output_path,
|
|
414
|
+
web_results=web_results,
|
|
415
|
+
instruct_traces=instruct_traces,
|
|
416
|
+
vision_trace=None # Vision integrated into Instruct now
|
|
511
417
|
)
|
|
512
|
-
except Exception as e:
|
|
513
|
-
logger.warning(f"Failed to save conversation: {e}")
|
|
514
418
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
try:
|
|
518
|
-
os.remove(output_path)
|
|
519
|
-
except Exception:
|
|
520
|
-
pass
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
419
|
+
if os.path.exists(output_path):
|
|
420
|
+
os.remove(output_path)
|
|
524
421
|
|
|
525
422
|
except Exception as e:
|
|
526
423
|
logger.exception(f"Error: {e}")
|
|
527
|
-
|
|
528
|
-
if conf.quote:
|
|
529
|
-
await session.send([Quote(session.event.message.id), err_msg])
|
|
530
|
-
else:
|
|
531
|
-
await session.send(err_msg)
|
|
532
|
-
|
|
533
|
-
# Save conversation on error if response was generated
|
|
534
|
-
if 'resp' in locals() and resp and conf.save_conversation:
|
|
535
|
-
try:
|
|
536
|
-
# Use a temporary ID for error cases
|
|
537
|
-
error_id = f"error_{int(time.time())}_{secrets.token_hex(4)}"
|
|
538
|
-
|
|
539
|
-
# Try to salvage history
|
|
540
|
-
partial_hist = []
|
|
541
|
-
if 'resp' in locals() and resp:
|
|
542
|
-
partial_hist = resp.get("conversation_history", [])
|
|
543
|
-
elif 'context' in locals() and context and hasattr(context, 'instruct_history'):
|
|
544
|
-
partial_hist = context.instruct_history
|
|
545
|
-
|
|
546
|
-
related_ids = []
|
|
547
|
-
if 'session' in locals():
|
|
548
|
-
msg_id = str(session.event.message.id) if hasattr(session.event, 'message') else str(session.event.id)
|
|
549
|
-
related_ids = [msg_id]
|
|
424
|
+
await session.send(f"Error: {e}")
|
|
550
425
|
|
|
551
|
-
history_manager.remember(error_id, partial_hist, related_ids, {"model": "error", "error": str(e)}, context_id, code=display_session_id if 'display_session_id' in locals() else None)
|
|
552
|
-
|
|
553
|
-
# Save debug data on error
|
|
554
|
-
web_res = context.web_results if 'context' in locals() and context else []
|
|
555
|
-
|
|
556
|
-
history_manager.save_to_disk(
|
|
557
|
-
error_id,
|
|
558
|
-
web_results=web_res
|
|
559
|
-
)
|
|
560
426
|
|
|
561
|
-
except Exception as save_err:
|
|
562
|
-
logger.error(f"Failed to save error conversation: {save_err}")
|
|
563
427
|
|
|
564
|
-
|
|
565
|
-
alc = Alconna(
|
|
566
|
-
conf.question_command,
|
|
567
|
-
Args["all_param;?", AllParam],
|
|
568
|
-
)
|
|
428
|
+
alc = Alconna(conf.question_command, Args["all_param;?", AllParam])
|
|
569
429
|
|
|
570
430
|
@command.on(alc)
|
|
571
431
|
async def handle_question_command(session: Session[MessageCreatedEvent], result: Arparma):
|
|
572
|
-
"""Handle main Question command"""
|
|
573
432
|
try:
|
|
574
|
-
logger.info(f"Question Command Triggered. Message: {result}")
|
|
575
433
|
mid = str(session.event.message.id) if getattr(session.event, "message", None) else str(session.event.id)
|
|
576
434
|
dedupe_key = f"{getattr(session.account, 'id', 'account')}:{mid}"
|
|
577
435
|
if _event_deduper.seen_recently(dedupe_key):
|
|
578
|
-
logger.warning(f"Duplicate command event ignored: {dedupe_key}")
|
|
579
436
|
return
|
|
580
437
|
except Exception:
|
|
581
438
|
pass
|
|
582
|
-
|
|
583
|
-
logger.info(f"Question Command Triggered. Message: {session.event.message}")
|
|
584
439
|
|
|
585
440
|
args = result.all_matched_args
|
|
441
|
+
all_param = args.get("all_param")
|
|
442
|
+
|
|
443
|
+
# Extract query text
|
|
444
|
+
if all_param:
|
|
445
|
+
if isinstance(all_param, MessageChain):
|
|
446
|
+
query_text = str(all_param.get(Text)).strip()
|
|
447
|
+
else:
|
|
448
|
+
query_text = str(all_param).strip()
|
|
449
|
+
else:
|
|
450
|
+
query_text = ""
|
|
451
|
+
|
|
452
|
+
# Check if replying to a cached search result
|
|
453
|
+
reply_msg_id = None
|
|
454
|
+
if session.reply and hasattr(session.reply.origin, 'id'):
|
|
455
|
+
reply_msg_id = str(session.reply.origin.id)
|
|
456
|
+
|
|
457
|
+
# Quote mode: Use cached search results
|
|
458
|
+
if reply_msg_id:
|
|
459
|
+
cached = search_cache.get(reply_msg_id)
|
|
460
|
+
if cached:
|
|
461
|
+
# Parse indices if provided
|
|
462
|
+
indices = parse_multi_indices(query_text, max_count=3) if query_text else None
|
|
463
|
+
|
|
464
|
+
# Check if too many indices requested (parse_multi_indices returns None if > max_count)
|
|
465
|
+
if query_text and indices is None:
|
|
466
|
+
# Check if it looks like indices but exceeded limit
|
|
467
|
+
import re
|
|
468
|
+
if re.match(r'^[\d,、\s\-–]+$', query_text):
|
|
469
|
+
await session.send("最多选择3个结果进行总结")
|
|
470
|
+
search_cache.cleanup()
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
if conf.reaction:
|
|
474
|
+
asyncio.create_task(react(session, "✨"))
|
|
475
|
+
|
|
476
|
+
core = get_hyw_core()
|
|
477
|
+
local_renderer = await get_content_renderer()
|
|
478
|
+
tab_task = asyncio.create_task(local_renderer.prepare_tab())
|
|
479
|
+
|
|
480
|
+
# Collect screenshots for selected pages
|
|
481
|
+
screenshots = []
|
|
482
|
+
if indices:
|
|
483
|
+
# Screenshot mode: capture pages for selected indices
|
|
484
|
+
for idx in indices:
|
|
485
|
+
if idx < len(cached.results):
|
|
486
|
+
url = cached.results[idx].get("url", "")
|
|
487
|
+
if url:
|
|
488
|
+
b64_img = await core.screenshot(url)
|
|
489
|
+
if b64_img:
|
|
490
|
+
screenshots.append(b64_img)
|
|
491
|
+
|
|
492
|
+
if not screenshots:
|
|
493
|
+
try: await tab_task
|
|
494
|
+
except: pass
|
|
495
|
+
await session.send("无法截图所选页面")
|
|
496
|
+
search_cache.cleanup()
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
user_query = f"总结关于 \"{cached.query}\" 的内容"
|
|
500
|
+
else:
|
|
501
|
+
# No indices - summarize based on cached snippets (no screenshots)
|
|
502
|
+
context_parts = []
|
|
503
|
+
for i, res in enumerate(cached.results[:10]):
|
|
504
|
+
title = res.get("title", f"Result {i+1}")
|
|
505
|
+
snippet = res.get("content", "") or res.get("snippet", "")
|
|
506
|
+
context_parts.append(f"## {title}\n{snippet}")
|
|
507
|
+
|
|
508
|
+
context_message = f"基于搜索 \"{cached.query}\" 的结果摘要回答用户问题:\n\n" + "\n\n".join(context_parts)
|
|
509
|
+
user_query = query_text if query_text else f"总结关于 \"{cached.query}\" 的搜索结果"
|
|
510
|
+
|
|
511
|
+
# Build request with screenshots (if any)
|
|
512
|
+
if screenshots:
|
|
513
|
+
request = QueryRequest(
|
|
514
|
+
user_input=user_query,
|
|
515
|
+
images=screenshots,
|
|
516
|
+
conversation_history=[],
|
|
517
|
+
model_name=None,
|
|
518
|
+
)
|
|
519
|
+
else:
|
|
520
|
+
request = QueryRequest(
|
|
521
|
+
user_input=f"{context_message}\n\n用户问题: {user_query}",
|
|
522
|
+
images=[],
|
|
523
|
+
conversation_history=[],
|
|
524
|
+
model_name=None,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
528
|
+
output_path = tf.name
|
|
529
|
+
|
|
530
|
+
response = await core.query(request, output_path=None)
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
tab_id = await tab_task
|
|
534
|
+
except Exception:
|
|
535
|
+
tab_id = None
|
|
536
|
+
|
|
537
|
+
if response.success and response.content:
|
|
538
|
+
render_ok = await core.render(
|
|
539
|
+
markdown_content=response.content,
|
|
540
|
+
output_path=output_path,
|
|
541
|
+
stats={"total_time": response.total_time},
|
|
542
|
+
references=[],
|
|
543
|
+
page_references=[],
|
|
544
|
+
tab_id=tab_id
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if render_ok and os.path.exists(output_path):
|
|
548
|
+
with open(output_path, "rb") as f:
|
|
549
|
+
img_data = base64.b64encode(f.read()).decode()
|
|
550
|
+
|
|
551
|
+
msg_chain = MessageChain(Image(src=f'data:image/png;base64,{img_data}'))
|
|
552
|
+
if conf.quote:
|
|
553
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
554
|
+
|
|
555
|
+
await session.send(msg_chain)
|
|
556
|
+
os.remove(output_path)
|
|
557
|
+
else:
|
|
558
|
+
await session.send(response.content[:500])
|
|
559
|
+
else:
|
|
560
|
+
await session.send(f"总结失败: {response.error or 'Unknown error'}")
|
|
561
|
+
|
|
562
|
+
search_cache.cleanup()
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
# === Filter Mode: Search + Find matching links + Summarize ===
|
|
566
|
+
filters, search_query, filter_error = parse_filter_syntax(query_text, max_count=3)
|
|
567
|
+
|
|
568
|
+
if filter_error:
|
|
569
|
+
await session.send(filter_error)
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
if filters:
|
|
573
|
+
if conf.reaction:
|
|
574
|
+
asyncio.create_task(react(session, "✨"))
|
|
575
|
+
|
|
576
|
+
core = get_hyw_core()
|
|
577
|
+
local_renderer = await get_content_renderer()
|
|
578
|
+
|
|
579
|
+
# Run search and prepare tab in parallel
|
|
580
|
+
search_task = asyncio.create_task(core.search([search_query]))
|
|
581
|
+
tab_task = asyncio.create_task(local_renderer.prepare_tab())
|
|
582
|
+
|
|
583
|
+
results = await search_task
|
|
584
|
+
flat_results = results[0] if results else []
|
|
585
|
+
|
|
586
|
+
if not flat_results:
|
|
587
|
+
try: await tab_task
|
|
588
|
+
except: pass
|
|
589
|
+
await session.send("Search returned no results.")
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
visible = [r for r in flat_results if not r.get("_hidden", False)]
|
|
593
|
+
|
|
594
|
+
# Collect URLs to screenshot
|
|
595
|
+
urls_to_screenshot = []
|
|
596
|
+
for filter_type, filter_value, count in filters:
|
|
597
|
+
if filter_type == 'index':
|
|
598
|
+
idx = filter_value - 1
|
|
599
|
+
if 0 <= idx < len(visible):
|
|
600
|
+
url = visible[idx].get("url", "")
|
|
601
|
+
if url and url not in urls_to_screenshot:
|
|
602
|
+
urls_to_screenshot.append(url)
|
|
603
|
+
else:
|
|
604
|
+
try: await tab_task
|
|
605
|
+
except: pass
|
|
606
|
+
await session.send(f"序号 {filter_value} 超出范围 (1-{len(visible)})")
|
|
607
|
+
return
|
|
608
|
+
else:
|
|
609
|
+
found_count = 0
|
|
610
|
+
for res in visible:
|
|
611
|
+
url = res.get("url", "")
|
|
612
|
+
if filter_value in url.lower() and url not in urls_to_screenshot:
|
|
613
|
+
urls_to_screenshot.append(url)
|
|
614
|
+
found_count += 1
|
|
615
|
+
if found_count >= count:
|
|
616
|
+
break
|
|
617
|
+
|
|
618
|
+
if found_count == 0:
|
|
619
|
+
try: await tab_task
|
|
620
|
+
except: pass
|
|
621
|
+
await session.send(f"未找到包含 \"{filter_value}\" 的链接")
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
if not urls_to_screenshot:
|
|
625
|
+
try: await tab_task
|
|
626
|
+
except: pass
|
|
627
|
+
await session.send("未找到匹配的链接")
|
|
628
|
+
return
|
|
629
|
+
|
|
630
|
+
# Take screenshots concurrently
|
|
631
|
+
screenshot_tasks = [core.screenshot(url) for url in urls_to_screenshot]
|
|
632
|
+
screenshot_results = await asyncio.gather(*screenshot_tasks)
|
|
633
|
+
screenshots = [b64 for b64 in screenshot_results if b64]
|
|
634
|
+
|
|
635
|
+
if not screenshots:
|
|
636
|
+
try: await tab_task
|
|
637
|
+
except: pass
|
|
638
|
+
await session.send("无法截图页面")
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
# Pass screenshots to LLM for summarization
|
|
642
|
+
user_query = f"总结关于 \"{search_query}\" 的内容"
|
|
643
|
+
|
|
644
|
+
request = QueryRequest(
|
|
645
|
+
user_input=user_query,
|
|
646
|
+
images=screenshots,
|
|
647
|
+
conversation_history=[],
|
|
648
|
+
model_name=None,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
652
|
+
output_path = tf.name
|
|
653
|
+
|
|
654
|
+
response = await core.query(request, output_path=None)
|
|
655
|
+
|
|
656
|
+
try:
|
|
657
|
+
tab_id = await tab_task
|
|
658
|
+
except Exception:
|
|
659
|
+
tab_id = None
|
|
660
|
+
|
|
661
|
+
if response.success and response.content:
|
|
662
|
+
render_ok = await core.render(
|
|
663
|
+
markdown_content=response.content,
|
|
664
|
+
output_path=output_path,
|
|
665
|
+
stats={"total_time": response.total_time},
|
|
666
|
+
references=[],
|
|
667
|
+
page_references=[],
|
|
668
|
+
tab_id=tab_id
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
if render_ok and os.path.exists(output_path):
|
|
672
|
+
with open(output_path, "rb") as f:
|
|
673
|
+
img_data = base64.b64encode(f.read()).decode()
|
|
674
|
+
|
|
675
|
+
msg_chain = MessageChain(Image(src=f'data:image/png;base64,{img_data}'))
|
|
676
|
+
if conf.quote:
|
|
677
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
678
|
+
|
|
679
|
+
await session.send(msg_chain)
|
|
680
|
+
os.remove(output_path)
|
|
681
|
+
else:
|
|
682
|
+
await session.send(response.content[:500])
|
|
683
|
+
else:
|
|
684
|
+
await session.send(f"总结失败: {response.error or 'Unknown error'}")
|
|
685
|
+
|
|
686
|
+
return
|
|
586
687
|
|
|
587
|
-
|
|
688
|
+
# Normal query mode (no cache context)
|
|
689
|
+
await process_request(session, all_param)
|
|
588
690
|
|
|
589
|
-
metadata("hyw", author=[{"name": "kumoSleeping", "email": "zjr2992@outlook.com"}], version=__version__, config=HywConfig)
|
|
590
691
|
|
|
692
|
+
# Search/Web Command (/w)
|
|
693
|
+
alc_search = Alconna("/w", Args["query;?", AllParam])
|
|
694
|
+
|
|
695
|
+
@command.on(alc_search)
|
|
696
|
+
async def handle_web_command(session: Session[MessageCreatedEvent], result: Arparma):
|
|
697
|
+
"""
|
|
698
|
+
Handle web command /w:
|
|
699
|
+
- If query is index + Quote -> Screenshot cached result
|
|
700
|
+
- If query is URL -> Screenshot
|
|
701
|
+
- If query is text -> Search
|
|
702
|
+
"""
|
|
703
|
+
query = result.all_matched_args.get("query")
|
|
704
|
+
|
|
705
|
+
# Extract query text
|
|
706
|
+
if query:
|
|
707
|
+
if isinstance(query, MessageChain):
|
|
708
|
+
query = str(query.get(Text)).strip()
|
|
709
|
+
query = str(query).strip()
|
|
710
|
+
else:
|
|
711
|
+
query = ""
|
|
712
|
+
|
|
713
|
+
# Check if replying to a cached search result
|
|
714
|
+
reply_msg_id = None
|
|
715
|
+
if session.reply and hasattr(session.reply.origin, 'id'):
|
|
716
|
+
reply_msg_id = str(session.reply.origin.id)
|
|
717
|
+
|
|
718
|
+
# Quote + Index mode: Screenshot specific cached result
|
|
719
|
+
if reply_msg_id:
|
|
720
|
+
cached = search_cache.get(reply_msg_id)
|
|
721
|
+
if cached:
|
|
722
|
+
# Parse index from query
|
|
723
|
+
idx = parse_single_index(query)
|
|
724
|
+
if idx is None:
|
|
725
|
+
# No valid index - show prompt
|
|
726
|
+
await session.send("请指定序号 (1-10)")
|
|
727
|
+
search_cache.cleanup() # Lazy cleanup
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
if idx >= len(cached.results):
|
|
731
|
+
await session.send(f"序号超出范围 (1-{len(cached.results)})")
|
|
732
|
+
search_cache.cleanup()
|
|
733
|
+
return
|
|
734
|
+
|
|
735
|
+
# Screenshot the cached URL
|
|
736
|
+
target_result = cached.results[idx]
|
|
737
|
+
target_url = target_result.get("url", "")
|
|
738
|
+
if not target_url:
|
|
739
|
+
await session.send("该结果无有效URL")
|
|
740
|
+
search_cache.cleanup()
|
|
741
|
+
return
|
|
742
|
+
|
|
743
|
+
if conf.reaction:
|
|
744
|
+
asyncio.create_task(react(session, "📸"))
|
|
745
|
+
|
|
746
|
+
core = get_hyw_core()
|
|
747
|
+
b64_img = await core.screenshot(target_url)
|
|
748
|
+
|
|
749
|
+
if b64_img:
|
|
750
|
+
msg_chain = MessageChain(Image(src=f'data:image/jpeg;base64,{b64_img}'))
|
|
751
|
+
if conf.quote:
|
|
752
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
753
|
+
await session.send(msg_chain)
|
|
754
|
+
else:
|
|
755
|
+
await session.send(f"截图失败: {target_url}")
|
|
756
|
+
|
|
757
|
+
search_cache.cleanup()
|
|
758
|
+
return
|
|
759
|
+
|
|
760
|
+
# No query and no cache context - nothing to do
|
|
761
|
+
if not query:
|
|
762
|
+
return
|
|
763
|
+
|
|
764
|
+
try:
|
|
765
|
+
core = get_hyw_core()
|
|
766
|
+
|
|
767
|
+
# 1. URL Detection
|
|
768
|
+
url_pattern = re.compile(r'^https?://(?:[-\w./?=&%#]+)')
|
|
769
|
+
if url_pattern.match(query):
|
|
770
|
+
# === URL Screenshot Mode ===
|
|
771
|
+
if conf.reaction: asyncio.create_task(react(session, "📸"))
|
|
772
|
+
|
|
773
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tf:
|
|
774
|
+
output_path = tf.name
|
|
775
|
+
|
|
776
|
+
b64_img = await core.screenshot(query)
|
|
777
|
+
|
|
778
|
+
if b64_img:
|
|
779
|
+
with open(output_path, "wb") as f:
|
|
780
|
+
f.write(base64.b64decode(b64_img))
|
|
781
|
+
|
|
782
|
+
msg_chain = MessageChain(Image(src=f'data:image/jpeg;base64,{b64_img}'))
|
|
783
|
+
if conf.quote:
|
|
784
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
785
|
+
|
|
786
|
+
await session.send(msg_chain)
|
|
787
|
+
|
|
788
|
+
if conf.save_conversation:
|
|
789
|
+
mid = str(session.event.message.id) if getattr(session.event, "message", None) else str(session.event.id)
|
|
790
|
+
context_id = f"guild_{session.guild.id}" if session.guild else "user"
|
|
791
|
+
history_manager.remember(mid, [{"role": "user", "content": f"/w {query}"}], [], {}, context_id=context_id)
|
|
792
|
+
history_manager.save_to_disk(mid, image_path=output_path, web_results=[{"url": query, "title": "Screenshot", "_type": "screenshot"}])
|
|
793
|
+
|
|
794
|
+
os.remove(output_path)
|
|
795
|
+
else:
|
|
796
|
+
await session.send(f"Failed to screenshot URL: {query}")
|
|
797
|
+
return
|
|
798
|
+
|
|
799
|
+
# 2. Search Mode (Fallthrough)
|
|
800
|
+
|
|
801
|
+
# Parse enhanced filter syntax
|
|
802
|
+
filters, search_query, filter_error = parse_filter_syntax(query, max_count=3)
|
|
803
|
+
|
|
804
|
+
if filter_error:
|
|
805
|
+
await session.send(filter_error)
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
# Search first
|
|
809
|
+
search_task = asyncio.create_task(core.search([search_query]))
|
|
810
|
+
|
|
811
|
+
if conf.reaction:
|
|
812
|
+
asyncio.create_task(react(session, "🔍"))
|
|
813
|
+
|
|
814
|
+
results = await search_task
|
|
815
|
+
flat_results = results[0] if results else []
|
|
816
|
+
|
|
817
|
+
if not flat_results:
|
|
818
|
+
await session.send("Search returned no results.")
|
|
819
|
+
return
|
|
820
|
+
|
|
821
|
+
visible = [r for r in flat_results if not r.get("_hidden", False)]
|
|
822
|
+
|
|
823
|
+
if not visible:
|
|
824
|
+
await session.send("Search returned no visible results.")
|
|
825
|
+
return
|
|
826
|
+
|
|
827
|
+
# === Filter Mode: Screenshot matching links ===
|
|
828
|
+
if filters:
|
|
829
|
+
urls_to_screenshot = []
|
|
830
|
+
|
|
831
|
+
for filter_type, filter_value, count in filters:
|
|
832
|
+
if filter_type == 'index':
|
|
833
|
+
# Index-based (1-based)
|
|
834
|
+
idx = filter_value - 1
|
|
835
|
+
if 0 <= idx < len(visible):
|
|
836
|
+
url = visible[idx].get("url", "")
|
|
837
|
+
if url and url not in urls_to_screenshot:
|
|
838
|
+
urls_to_screenshot.append(url)
|
|
839
|
+
else:
|
|
840
|
+
await session.send(f"序号 {filter_value} 超出范围 (1-{len(visible)})")
|
|
841
|
+
return
|
|
842
|
+
else:
|
|
843
|
+
# Link filter: find URLs containing filter term
|
|
844
|
+
found_count = 0
|
|
845
|
+
for res in visible:
|
|
846
|
+
url = res.get("url", "")
|
|
847
|
+
if filter_value in url.lower() and url not in urls_to_screenshot:
|
|
848
|
+
urls_to_screenshot.append(url)
|
|
849
|
+
found_count += 1
|
|
850
|
+
if found_count >= count:
|
|
851
|
+
break
|
|
852
|
+
|
|
853
|
+
if found_count == 0:
|
|
854
|
+
await session.send(f"未找到包含 \"{filter_value}\" 的链接")
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
if not urls_to_screenshot:
|
|
858
|
+
await session.send("未找到匹配的链接")
|
|
859
|
+
return
|
|
860
|
+
|
|
861
|
+
if conf.reaction:
|
|
862
|
+
asyncio.create_task(react(session, "📸"))
|
|
863
|
+
|
|
864
|
+
# Take screenshots concurrently
|
|
865
|
+
screenshot_tasks = [core.screenshot(url) for url in urls_to_screenshot]
|
|
866
|
+
screenshot_results = await asyncio.gather(*screenshot_tasks)
|
|
867
|
+
|
|
868
|
+
images = [Image(src=f'data:image/jpeg;base64,{b64}') for b64 in screenshot_results if b64]
|
|
869
|
+
|
|
870
|
+
if images:
|
|
871
|
+
msg_chain = MessageChain(images)
|
|
872
|
+
if conf.quote:
|
|
873
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
874
|
+
await session.send(msg_chain)
|
|
875
|
+
|
|
876
|
+
if conf.save_conversation:
|
|
877
|
+
mid = str(session.event.message.id) if getattr(session.event, "message", None) else str(session.event.id)
|
|
878
|
+
context_id = f"guild_{session.guild.id}" if session.guild else "user"
|
|
879
|
+
history_manager.remember(mid, [{"role": "user", "content": f"/w {query}"}], [], {}, context_id=context_id)
|
|
880
|
+
else:
|
|
881
|
+
await session.send("截图失败")
|
|
882
|
+
return
|
|
883
|
+
|
|
884
|
+
# === Normal Search Mode: Screenshot search results page ===
|
|
885
|
+
search_service = core._search_service
|
|
886
|
+
search_url = search_service._build_search_url(search_query)
|
|
887
|
+
|
|
888
|
+
# Handle address bar search marker
|
|
889
|
+
if search_url.startswith("__ADDRESS_BAR_SEARCH__:"):
|
|
890
|
+
import urllib.parse
|
|
891
|
+
encoded_query = urllib.parse.quote_plus(search_query)
|
|
892
|
+
search_url = f"https://www.google.com/search?q={encoded_query}"
|
|
893
|
+
|
|
894
|
+
b64_img = await core.screenshot(search_url)
|
|
895
|
+
|
|
896
|
+
if b64_img:
|
|
897
|
+
msg_chain = MessageChain(Image(src=f'data:image/jpeg;base64,{b64_img}'))
|
|
898
|
+
if conf.quote:
|
|
899
|
+
msg_chain = MessageChain(Quote(session.event.message.id)) + msg_chain
|
|
900
|
+
|
|
901
|
+
sent = await session.send(msg_chain)
|
|
902
|
+
|
|
903
|
+
# Store in cache for future /w and /q lookups
|
|
904
|
+
sent_id = next((str(e.id) for e in sent if hasattr(e, 'id')), None) if sent else None
|
|
905
|
+
if sent_id:
|
|
906
|
+
search_cache.store(sent_id, visible[:10], search_query)
|
|
907
|
+
|
|
908
|
+
if conf.save_conversation:
|
|
909
|
+
mid = str(session.event.message.id) if getattr(session.event, "message", None) else str(session.event.id)
|
|
910
|
+
context_id = f"guild_{session.guild.id}" if session.guild else "user"
|
|
911
|
+
history_manager.remember(mid, [{"role": "user", "content": f"/w {query}"}], [], {}, context_id=context_id)
|
|
912
|
+
else:
|
|
913
|
+
await session.send(f"截图搜索页面失败: {search_url}")
|
|
914
|
+
|
|
915
|
+
search_cache.cleanup() # Lazy cleanup
|
|
916
|
+
|
|
917
|
+
except Exception as e:
|
|
918
|
+
logger.error(f"Search command failed: {e}")
|
|
919
|
+
await session.send(f"Search error: {e}")
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
metadata("hyw", author=[{"name": "kumoSleeping", "email": "zjr2992@outlook.com"}], version=__version__, config=HywConfig)
|
|
591
923
|
|
|
592
924
|
@listen(CommandReceive)
|
|
593
925
|
async def remove_at(content: MessageChain):
|