chatgpt-mirai-qq-bot-web-search 0.1.8__py3-none-any.whl → 0.1.9__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.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.1
2
+ Name: chatgpt-mirai-qq-bot-web-search
3
+ Version: 0.1.9
4
+ Summary: WebSearch adapter for lss233/chatgpt-mirai-qq-bot
5
+ Home-page: https://github.com/chuanSir123/web_image_generate
6
+ Author: chuanSir
7
+ Author-email: 416448943@qq.com
8
+ Project-URL: Bug Tracker, https://github.com/chuanSir123/web_image_generate/issues
9
+ Project-URL: Documentation, https://github.com/chuanSir123/web_image_generate/wiki
10
+ Project-URL: Source Code, https://github.com/chuanSir123/web_image_generate
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: playwright
18
+ Requires-Dist: trafilatura
19
+ Requires-Dist: lxml-html-clean
20
+
21
+ # web-image-generate for ChatGPT-Mirai-QQ-Bot
22
+
23
+ 本项目是 [ChatGPT-Mirai-QQ-Bot](https://github.com/lss233/chatgpt-mirai-qq-bot) 的一个插件,用于生成图片。
24
+
25
+ ## 安装
26
+
27
+ ```bash
28
+ pip install chatgpt-mirai-qq-bot-web-image-generate
29
+ ```
30
+
31
+ ## 使用
32
+
33
+ 在 chatgpt-mirai-qq-bot的web_ui中配置
34
+ 使用示例请参考 [web_image_generate/example/textToImage.yml](web_image_generate/example/textToImage.yaml)
35
+ 安装此插件会自动生成一个画图工作流,仅供参考
36
+
37
+ ## 开源协议
38
+
39
+ 本项目基于 [ChatGPT-Mirai-QQ-Bot](https://github.com/lss233/chatgpt-mirai-qq-bot) 开发,遵循其 [开源协议](https://github.com/lss233/chatgpt-mirai-qq-bot/blob/master/LICENSE)
40
+
41
+ ## 感谢
42
+
43
+ 感谢 [ChatGPT-Mirai-QQ-Bot](https://github.com/lss233/chatgpt-mirai-qq-bot) 的作者 [lss233](https://github.com/lss233) 提供框架支持
44
+
45
+
@@ -0,0 +1,10 @@
1
+ web_image_generate/__init__.py,sha256=Z_uKpnQuGpFe4CGAkk-eNOa575hLxjwQ7RBteO9FMHY,3119
2
+ web_image_generate/blocks.py,sha256=1U7oEV-7b4QD83c314A3vr_-54I1XIn3Q9DthZWlwRw,4014
3
+ web_image_generate/image_generator.py,sha256=qF5QM0ieuUGbD7JrkZ_7CA5NHkbq8V3XnczjeYHdr7o,10745
4
+ web_image_generate/example/textToImage.yaml,sha256=6vUC3pvkU6dBYn_s9FIS8AVqMv0LRWV781WBSjgV4uo,3514
5
+ chatgpt_mirai_qq_bot_web_search-0.1.9.dist-info/LICENSE,sha256=ILBn-G3jdarm2w8oOrLmXeJNU3czuJvVhDLBASWdhM8,34522
6
+ chatgpt_mirai_qq_bot_web_search-0.1.9.dist-info/METADATA,sha256=zfLEKXNkEKauC5XPV0zdUyKB3A_7URukzS_u1YbCuwE,1716
7
+ chatgpt_mirai_qq_bot_web_search-0.1.9.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
8
+ chatgpt_mirai_qq_bot_web_search-0.1.9.dist-info/entry_points.txt,sha256=37GmFzDSW3zo7tfDV5LPFeiptfADQv9QWtVh_8ut68I,80
9
+ chatgpt_mirai_qq_bot_web_search-0.1.9.dist-info/top_level.txt,sha256=O_Xj9ogMo-PMbtmCO2ADLk2pPe2wET0_VhiH-y5lyL4,19
10
+ chatgpt_mirai_qq_bot_web_search-0.1.9.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: bdist_wheel (0.45.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [chatgpt_mirai.plugins]
2
+ web_image_generate = web_image_generate:WebSearchPlugin
@@ -0,0 +1 @@
1
+ web_image_generate
@@ -0,0 +1,72 @@
1
+ from typing import Dict, Any, List
2
+ from framework.plugin_manager.plugin import Plugin
3
+ from framework.logger import get_logger
4
+ from dataclasses import dataclass
5
+ from framework.workflow.core.block import BlockRegistry
6
+ from framework.ioc.inject import Inject
7
+ from framework.ioc.container import DependencyContainer
8
+ from framework.workflow.core.workflow.builder import WorkflowBuilder
9
+ from framework.workflow.core.workflow.registry import WorkflowRegistry
10
+ from .blocks import WebImageGenerateBlock,ImageUrlToIMMessage
11
+ logger = get_logger("WebImageGenerate")
12
+ import importlib.resources
13
+ import os
14
+ from pathlib import Path
15
+
16
+ class WebImageGeneratePlugin(Plugin):
17
+ def __init__(self, block_registry: BlockRegistry, container: DependencyContainer):
18
+ super().__init__()
19
+ self.block_registry = block_registry
20
+ self.workflow_registry = container.resolve(WorkflowRegistry)
21
+ self.container = container
22
+
23
+ def on_load(self):
24
+ logger.info("ImageGeneratePlugin loading")
25
+
26
+ # 注册Block
27
+ try:
28
+ self.block_registry.register("web_image_generate", "image", WebImageGenerateBlock)
29
+ self.block_registry.register("image_url_to_imMessage", "image", ImageUrlToIMMessage)
30
+ except Exception as e:
31
+ logger.warning(f"ImageGeneratePlugin failed: {e}")
32
+
33
+ try:
34
+ # 获取当前文件的绝对路径
35
+ with importlib.resources.path('web_image_generate', '__init__.py') as p:
36
+ package_path = p.parent
37
+ example_dir = package_path / 'example'
38
+
39
+ # 确保目录存在
40
+ if not example_dir.exists():
41
+ raise FileNotFoundError(f"Example directory not found at {example_dir}")
42
+
43
+ # 获取所有yaml文件
44
+ yaml_files = list(example_dir.glob('*.yaml')) + list(example_dir.glob('*.yml'))
45
+
46
+ for yaml in yaml_files:
47
+ logger.info(yaml)
48
+ self.workflow_registry.register("image", yaml.stem, WorkflowBuilder.load_from_yaml(os.path.join(example_dir, yaml), self.container))
49
+ except Exception as e:
50
+ try:
51
+ current_file = os.path.abspath(__file__)
52
+
53
+ # 获取当前文件所在目录
54
+ parent_dir = os.path.dirname(current_file)
55
+
56
+ # 构建 example 目录的路径
57
+ example_dir = os.path.join(parent_dir, 'example')
58
+ # 获取 example 目录下所有的 yaml 文件
59
+ yaml_files = [f for f in os.listdir(example_dir) if f.endswith('.yaml') or f.endswith('.yml')]
60
+
61
+ for yaml in yaml_files:
62
+ logger.info(os.path.join(example_dir, yaml))
63
+ self.workflow_registry.register("image", yaml.stem, WorkflowBuilder.load_from_yaml(os.path.join(example_dir, yaml), self.container))
64
+ except Exception as e:
65
+ logger.warning(f"workflow_registry failed: {e}")
66
+
67
+ def on_start(self):
68
+ logger.info("ImageGeneratePlugin started")
69
+
70
+ def on_stop(self):
71
+ logger.info("ImageGeneratePlugin stopped")
72
+
@@ -0,0 +1,98 @@
1
+ from typing import Any, Dict, List, Optional,Annotated
2
+ from framework.workflow.core.block import Block
3
+ from framework.workflow.core.block.input_output import Input, Output
4
+ from framework.im.message import IMMessage, TextMessage, ImageMessage
5
+ from framework.im.sender import ChatSender
6
+ from .image_generator import WebImageGenerator
7
+ import asyncio
8
+ from framework.logger import get_logger
9
+ from framework.ioc.container import DependencyContainer
10
+
11
+ logger = get_logger("ImageGenerator")
12
+ class WebImageGenerateBlock(Block):
13
+ """图片生成Block"""
14
+ name = "image_generate"
15
+
16
+ # 平台和对应的模型配置
17
+ PLATFORM_MODELS = {
18
+ "modelscope": ["flux", "ketu"],
19
+ "shakker": ["anime", "photo"]
20
+ }
21
+
22
+ inputs = {
23
+ "prompt": Input(name="prompt", label="提示词", data_type=str, description="生成提示词"),
24
+ "width": Input(name="width", label="宽度", data_type=int, description="图片宽度", nullable=True, default=1024),
25
+ "height": Input(name="height", label="高度", data_type=int, description="图片高度", nullable=True, default=1024)
26
+ }
27
+
28
+ outputs = {
29
+ "image_url": Output(name="image_url", label="图片URL", data_type=str, description="生成的图片URL")
30
+ }
31
+
32
+ def __init__(
33
+ self,
34
+ name: str = None,
35
+ platform: str = "modelscope",
36
+ model: str = "flux",
37
+ cookie: str = ""
38
+ ):
39
+ super().__init__(name)
40
+
41
+ # 验证平台和模型的合法性
42
+ if platform not in self.PLATFORM_MODELS:
43
+ supported_platforms = ", ".join(self.PLATFORM_MODELS.keys())
44
+ logger.error(f"不支持的平台 '{platform}'。支持的平台有: {supported_platforms}")
45
+ raise ValueError(f"不支持的平台 '{platform}'。支持的平台有: {supported_platforms}")
46
+
47
+ if model not in self.PLATFORM_MODELS[platform]:
48
+ supported_models = ", ".join(self.PLATFORM_MODELS[platform])
49
+ logger.error(f"平台 '{platform}' 不支持模型 '{model}'。支持的模型有: {supported_models}")
50
+ raise ValueError(f"平台 '{platform}' 不支持模型 '{model}'。支持的模型有: {supported_models}")
51
+
52
+ self.platform = platform
53
+ self.model = model
54
+ self.generator = WebImageGenerator(cookie=cookie)
55
+
56
+ def execute(self, **kwargs) -> Dict[str, Any]:
57
+ prompt = kwargs.get("prompt", "")
58
+ width = int(kwargs.get("width", 1024))
59
+ height = int(kwargs.get("height", 1024))
60
+
61
+ try:
62
+ try:
63
+ loop = asyncio.get_event_loop()
64
+ except RuntimeError:
65
+ loop = asyncio.new_event_loop()
66
+ asyncio.set_event_loop(loop)
67
+
68
+ image_url = loop.run_until_complete(
69
+ self.generator.generate_image(
70
+ platform=self.platform,
71
+ model=self.model,
72
+ prompt=prompt,
73
+ width=width,
74
+ height=height
75
+ )
76
+ )
77
+ return {"image_url": image_url}
78
+ except Exception as e:
79
+ return {"image_url": f"生成失败: {str(e)}"}
80
+
81
+ class ImageUrlToIMMessage(Block):
82
+ """纯文本转 IMMessage"""
83
+
84
+ name = "imageUrl_to_im_message"
85
+ container: DependencyContainer
86
+ inputs = {"image_url": Input("image_url", "图片url", str, "图片url")}
87
+ outputs = {"msg": Output("msg", "IM 消息", IMMessage, "IM 消息")}
88
+
89
+ def __init__(self):
90
+ self.split_by = ","
91
+
92
+ def execute(self, image_url: str) -> Dict[str, Any]:
93
+ if not image_url.startswith("http"):
94
+ return {"msg": IMMessage(sender=ChatSender.get_bot_sender(), message_elements=[TextMessage(image_url)])}
95
+ if self.split_by:
96
+ return {"msg": IMMessage(sender=ChatSender.get_bot_sender(), message_elements=[ImageMessage(line) for line in image_url.split(self.split_by)])}
97
+ else:
98
+ return {"msg": IMMessage(sender=ChatSender.get_bot_sender(), message_elements=[ImageMessage(image_url)])}
@@ -0,0 +1,116 @@
1
+ name: 文生图
2
+ description: ''
3
+ blocks:
4
+ - type: internal:text_block
5
+ name: 4040f648-6082-4a62-8ab1-39230e812836
6
+ params:
7
+ text: "Please help me convert this image description to an optimized English prompt.\nDescription: {user_msg}\n\nRequirements:\n1. Output in English\n2. Use detailed and specific words,Include high quality, detailed description, style keywords"
8
+ position:
9
+ x: -24
10
+ y: 275
11
+ connected_to:
12
+ - target: e8cae85d-a072-49b7-ab9f-fce7a06160e4
13
+ mapping:
14
+ from: text
15
+ to: user_prompt_format
16
+ - type: internal:get_message
17
+ name: 2c59f4e2-6f4f-431b-875f-9ea0337ba949
18
+ params: {}
19
+ position:
20
+ x: -22
21
+ y: 147
22
+ connected_to:
23
+ - target: e8cae85d-a072-49b7-ab9f-fce7a06160e4
24
+ mapping:
25
+ from: msg
26
+ to: user_msg
27
+ - type: internal:chat_message_constructor
28
+ name: e8cae85d-a072-49b7-ab9f-fce7a06160e4
29
+ params: {}
30
+ position:
31
+ x: 356
32
+ y: 142
33
+ connected_to:
34
+ - target: 9b7a708d-c87b-4f79-a5b0-25fe710cfbd5
35
+ mapping:
36
+ from: llm_msg
37
+ to: prompt
38
+ - type: internal:llm_response_to_text
39
+ name: 6c87ade7-829a-49d5-9542-0b9139c55b8d
40
+ params: {}
41
+ position:
42
+ x: 929
43
+ y: 143
44
+ connected_to:
45
+ - target: 0fae5955-f60e-4e2d-aeb0-b70c4310a907
46
+ mapping:
47
+ from: text
48
+ to: prompt
49
+ - type: internal:send_message
50
+ name: 35575732-eab5-477d-99c8-899dc9cb4422
51
+ params:
52
+ im_name: ''
53
+ position:
54
+ x: 1760
55
+ y: 150
56
+ - type: image:image_url_to_imMessage
57
+ name: 8bf789ab-a7ad-4e29-abf1-ddbc31508543
58
+ params: {}
59
+ position:
60
+ x: 1508
61
+ y: 146
62
+ connected_to:
63
+ - target: 35575732-eab5-477d-99c8-899dc9cb4422
64
+ mapping:
65
+ from: msg
66
+ to: msg
67
+ - type: internal:chat_completion
68
+ name: 9b7a708d-c87b-4f79-a5b0-25fe710cfbd5
69
+ params:
70
+ model_name: deepseek-ai/DeepSeek-V3
71
+ position:
72
+ x: 580
73
+ y: 143
74
+ connected_to:
75
+ - target: 6c87ade7-829a-49d5-9542-0b9139c55b8d
76
+ mapping:
77
+ from: resp
78
+ to: response
79
+ - type: internal:text_block
80
+ name: a8328f51-021f-4be4-87c0-cd3812965c06
81
+ params:
82
+ text: ''
83
+ position:
84
+ x: -21
85
+ y: 394
86
+ connected_to:
87
+ - target: e8cae85d-a072-49b7-ab9f-fce7a06160e4
88
+ mapping:
89
+ from: text
90
+ to: memory_content
91
+ - type: internal:text_block
92
+ name: ace549dd-3dd0-4c6a-bccf-4b5aad93916c
93
+ params:
94
+ text: ''
95
+ position:
96
+ x: -15
97
+ y: 518
98
+ connected_to:
99
+ - target: e8cae85d-a072-49b7-ab9f-fce7a06160e4
100
+ mapping:
101
+ from: text
102
+ to: system_prompt_format
103
+ - type: image:web_image_generate
104
+ name: 0fae5955-f60e-4e2d-aeb0-b70c4310a907
105
+ params:
106
+ cookie: ''
107
+ model: flux
108
+ platform: modelscope
109
+ position:
110
+ x: 1168
111
+ y: 140
112
+ connected_to:
113
+ - target: 8bf789ab-a7ad-4e29-abf1-ddbc31508543
114
+ mapping:
115
+ from: image_url
116
+ to: image_url
@@ -0,0 +1,260 @@
1
+ import aiohttp
2
+ import random
3
+ import json
4
+ import time
5
+ import asyncio
6
+ from typing import Dict, Any, Optional, Tuple
7
+ from framework.logger import get_logger
8
+
9
+ logger = get_logger("ImageGenerator")
10
+
11
+ class WebImageGenerator:
12
+ MODELSCOPE_MODELS = {
13
+ "flux": {
14
+ "path": "ByteDance/Hyper-FLUX-8Steps-LoRA",
15
+ "fn_index": 0,
16
+ "trigger_id": 18,
17
+ "data_builder": lambda height, width, prompt: [height, width, 8, 3.5, prompt, random.randint(0, 9999999999999999)],
18
+ "data_types": ["slider", "slider", "slider", "slider", "textbox", "number"],
19
+ "url_processor": lambda url: url.replace("leofen/flux_dev_gradio", "muse/flux_dev"),
20
+ "output_parser": lambda data: data["output"]["data"][0]["url"]
21
+ },
22
+ "ketu": {
23
+ "path": "AI-ModelScope/Kolors",
24
+ "fn_index": 0,
25
+ "trigger_id": 23,
26
+ "data_builder": lambda height, width, prompt: [prompt, "", height, width, 20, 5, 1, True, random.randint(0, 9999999999999999)],
27
+ "data_types": ["textbox", "textbox", "slider", "slider", "slider", "slider", "slider", "checkbox", "number"],
28
+ "url_processor": lambda url: url,
29
+ "output_parser": lambda data: data.get("output")['data'][0][0]["image"]["url"]
30
+ }
31
+ }
32
+
33
+ def __init__(self, cookie: str = ""):
34
+ self.cookie = cookie
35
+ self.api_base = "https://s5k.cn" # ModelScope API base URL
36
+
37
+ async def _get_modelscope_token(self, session: aiohttp.ClientSession, headers: Dict[str, str]) -> str:
38
+ """获取ModelScope token"""
39
+ async with session.get(
40
+ f"https://modelscope.cn/api/v1/studios/token",
41
+ headers=headers
42
+ ) as response:
43
+ response.raise_for_status()
44
+ token_data = await response.json()
45
+ return token_data["Data"]["Token"]
46
+
47
+ async def generate_modelscope(self, model: str, prompt: str, width: int, height: int) -> str:
48
+ """使用ModelScope模型生成图片"""
49
+ if model not in self.MODELSCOPE_MODELS:
50
+ raise ValueError(f"Unsupported ModelScope model: {model}")
51
+
52
+ model_config = self.MODELSCOPE_MODELS[model]
53
+ headers = {
54
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
55
+ "Cookie": self.cookie
56
+ }
57
+
58
+ async with aiohttp.ClientSession() as session:
59
+ # 获取 token
60
+ studio_token = await self._get_modelscope_token(session, headers)
61
+ headers["X-Studio-Token"] = studio_token
62
+ session_hash = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=7))
63
+
64
+ # 调用模型生成图片
65
+ model_url = f"{self.api_base}/api/v1/studio/{model_config['path']}/gradio/queue/join"
66
+ params = {
67
+ "backend_url": f"/api/v1/studio/{model_config['path']}/gradio/",
68
+ "sdk_version": "4.31.3",
69
+ "studio_token": studio_token
70
+ }
71
+
72
+ json_data = {
73
+ "data": model_config["data_builder"](height, width, prompt),
74
+ "fn_index": model_config["fn_index"],
75
+ "trigger_id": model_config["trigger_id"],
76
+ "dataType": model_config["data_types"],
77
+ "session_hash": session_hash
78
+ }
79
+
80
+ async with session.post(
81
+ model_url,
82
+ headers=headers,
83
+ params=params,
84
+ json=json_data
85
+ ) as response:
86
+ response.raise_for_status()
87
+ data = await response.json()
88
+ event_id = data["event_id"]
89
+
90
+ # 获取结果
91
+ result_url = f"{self.api_base}/api/v1/studio/{model_config['path']}/gradio/queue/data"
92
+ params = {
93
+ "session_hash": session_hash,
94
+ "studio_token": studio_token
95
+ }
96
+
97
+ async with session.get(result_url, headers=headers, params=params) as response:
98
+ response.raise_for_status()
99
+ async for line in response.content:
100
+ line = line.decode('utf-8')
101
+ if line.startswith('data: '):
102
+ logger.debug(line)
103
+ event_data = json.loads(line[6:])
104
+ if event_data["event_id"] == event_id and event_data["msg"] == "process_completed":
105
+ try:
106
+ url = model_config["output_parser"](event_data)
107
+ if url:
108
+ return model_config["url_processor"](url)
109
+ except Exception as e:
110
+ logger.error(f"Failed to parse output for model {model}: {e}")
111
+ return ""
112
+
113
+ async def generate_shakker(self, model: str, prompt: str, width: int, height: int) -> str:
114
+ """使用Shakker平台生成图片"""
115
+ # Model mapping for Shakker platform
116
+ MODEL_MAPPING = {
117
+ "anime": 1489127,
118
+ "photo": 1489700
119
+ }
120
+
121
+ if model not in MODEL_MAPPING:
122
+ raise ValueError(f"Unsupported Shakker model: {model}")
123
+
124
+ # Adjust dimensions if they exceed 1024
125
+ if width >= height and width > 1024:
126
+ height = int(1024 * height / width)
127
+ width = 1024
128
+ elif height > width and height > 1024:
129
+ width = int(1024 * width / height)
130
+ height = 1024
131
+
132
+ # Prepare request payload
133
+ json_data = {
134
+ "source": 3,
135
+ "adetailerEnable": 0,
136
+ "mode": 1,
137
+ "projectData": {
138
+ "style": "",
139
+ "baseType": 3,
140
+ "presetBaseModelId": "photography",
141
+ "baseModel": None,
142
+ "loraModels": [],
143
+ "width": int(width * 1.5),
144
+ "height": int(height * 1.5),
145
+ "isFixedRatio": True,
146
+ "hires": True,
147
+ "count": 1,
148
+ "prompt": prompt,
149
+ "negativePrompt": "",
150
+ "presetNegativePrompts": ["common", "bad_hand"],
151
+ "samplerMethod": "29",
152
+ "samplingSteps": 20,
153
+ "seedType": "0",
154
+ "seedNumber": -1,
155
+ "vae": "-1",
156
+ "cfgScale": 7,
157
+ "clipSkip": 2,
158
+ "controlnets": [],
159
+ "checkpoint": None,
160
+ "hiresOptions": {
161
+ "enabled": True,
162
+ "scale": 1.5,
163
+ "upscaler": "11",
164
+ "strength": 0.5,
165
+ "steps": 20,
166
+ "width": width,
167
+ "height": height
168
+ },
169
+ "modelCfgScale": 7,
170
+ "changed": True,
171
+ "modelGroupCoverUrl": None,
172
+ "addOns": [],
173
+ "mode": 1,
174
+ "isSimpleMode": False,
175
+ "generateType": "normal",
176
+ "renderWidth": int(width * 1.5),
177
+ "renderHeight": int(height * 1.5),
178
+ "samplerMethodName": "Restart"
179
+ },
180
+ "vae": "",
181
+ "checkpointId": MODEL_MAPPING[model],
182
+ "additionalNetwork": [],
183
+ "generateType": 1,
184
+ "text2img": {
185
+ "width": width,
186
+ "height": height,
187
+ "prompt": prompt,
188
+ "negativePrompt": ",lowres, normal quality, worst quality, cropped, blurry, drawing, painting, glowing",
189
+ "samplingMethod": "29",
190
+ "samplingStep": 20,
191
+ "batchSize": 1,
192
+ "batchCount": 1,
193
+ "cfgScale": 7,
194
+ "clipSkip": 2,
195
+ "seed": -1,
196
+ "tiling": 0,
197
+ "seedExtra": 0,
198
+ "restoreFaces": 0,
199
+ "hiResFix": 1,
200
+ "extraNetwork": [],
201
+ "promptRecommend": True,
202
+ "hiResFixInfo": {
203
+ "upscaler": 11,
204
+ "upscaleBy": 1.5,
205
+ "resizeWidth": int(width * 1.5),
206
+ "resizeHeight": int(height * 1.5)
207
+ },
208
+ "hiresSteps": 20,
209
+ "denoisingStrength": 0.5
210
+ },
211
+ "cid": f"{int(time.time() * 1000)}woivhqlb"
212
+ }
213
+
214
+ headers = {"Token": self.cookie} # Using cookie as token
215
+
216
+ async with aiohttp.ClientSession() as session:
217
+ # Submit generation request
218
+ async with session.post(
219
+ "https://www.shakker.ai/gateway/sd-api/gen/tool/shake",
220
+ json=json_data,
221
+ headers=headers
222
+ ) as response:
223
+ response.raise_for_status()
224
+ data = await response.json()
225
+ task_id = data["data"]
226
+
227
+ # Wait for initial processing
228
+ await asyncio.sleep(10)
229
+
230
+ # Poll for results
231
+ for _ in range(60):
232
+ async with session.post(
233
+ f"https://www.shakker.ai/gateway/sd-api/generate/progress/msg/v1/{task_id}",
234
+ json={"flag": 3},
235
+ headers=headers
236
+ ) as response:
237
+ response.raise_for_status()
238
+ result = await response.json()
239
+
240
+ if result["data"]["percentCompleted"] == 100:
241
+ return result["data"]["images"][0]["previewPath"]
242
+
243
+ await asyncio.sleep(1)
244
+
245
+ return ""
246
+
247
+ async def generate_image(self, platform: str, model: str, prompt: str, width: int, height: int) -> str:
248
+ """统一的图片生成入口"""
249
+ if platform == "modelscope":
250
+ if not self.cookie:
251
+ return "请前往https://modelscope.cn/登录后获取token(按F12-应用-cookie中的m_session_id)";
252
+ if not self.cookie.startswith("m_session_id="):
253
+ self.cookie = "m_session_id=" + self.cookie
254
+ return await self.generate_modelscope(model, prompt, width, height)
255
+ elif platform == "shakker":
256
+ if not self.cookie:
257
+ return "请前往https://www.shakker.ai/登录后获取token(按F12-应用-cookie中的usertoken)";
258
+ return await self.generate_shakker(model, prompt, width, height)
259
+
260
+ raise ValueError(f"Unsupported platform ({platform}) or model ({model})")
@@ -1,55 +0,0 @@
1
- Metadata-Version: 2.2
2
- Name: chatgpt-mirai-qq-bot-web-search
3
- Version: 0.1.8
4
- Summary: WebSearch adapter for lss233/chatgpt-mirai-qq-bot
5
- Home-page: https://github.com/chuanSir123/web_search
6
- Author: chuanSir
7
- Author-email: 416448943@qq.com
8
- Project-URL: Bug Tracker, https://github.com/chuanSir123/web_search/issues
9
- Project-URL: Documentation, https://github.com/chuanSir123/web_search/wiki
10
- Project-URL: Source Code, https://github.com/chuanSir123/web_search
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: License :: OSI Approved :: GNU Affero General Public License v3
13
- Classifier: Operating System :: OS Independent
14
- Requires-Python: >=3.8
15
- Description-Content-Type: text/markdown
16
- License-File: LICENSE
17
- Requires-Dist: playwright
18
- Requires-Dist: trafilatura
19
- Requires-Dist: lxml_html_clean
20
- Dynamic: author
21
- Dynamic: author-email
22
- Dynamic: classifier
23
- Dynamic: description
24
- Dynamic: description-content-type
25
- Dynamic: home-page
26
- Dynamic: project-url
27
- Dynamic: requires-dist
28
- Dynamic: requires-python
29
- Dynamic: summary
30
-
31
- # OneBot-adapter for ChatGPT-Mirai-QQ-Bot
32
-
33
- 本项目是 [ChatGPT-Mirai-QQ-Bot](https://github.com/lss233/chatgpt-mirai-qq-bot) 的一个插件,用于将OneBot协议的消息转换为ChatGPT-Mirai-QQ-Bot的消息格式。
34
-
35
- ## 安装
36
-
37
- ```bash
38
- pip install chatgpt-mirai-qq-bot-web-search
39
- ```
40
-
41
- ## 使用
42
-
43
- 在 chatgpt-mirai-qq-bot的web_ui中配置
44
- 使用示例请参考 [web_search/example/normal.yml](web_search/example/roleplayWithWebSearch.yaml)
45
- 工作流请参考 [示例图片](web_search/example/workflow.png)
46
-
47
- ## 开源协议
48
-
49
- 本项目基于 [ChatGPT-Mirai-QQ-Bot](https://github.com/lss233/chatgpt-mirai-qq-bot) 开发,遵循其 [开源协议](https://github.com/lss233/chatgpt-mirai-qq-bot/blob/master/LICENSE)
50
-
51
- ## 感谢
52
-
53
- 感谢 [ChatGPT-Mirai-QQ-Bot](https://github.com/lss233/chatgpt-mirai-qq-bot) 的作者 [lss233](https://github.com/lss233) 提供框架支持
54
-
55
-
@@ -1,11 +0,0 @@
1
- web_search/__init__.py,sha256=bdinhnZOl-5t9LV-UT3-Tdfc5ylb-E3t-g9uhYyhunc,3419
2
- web_search/blocks.py,sha256=bVLn5kg-OMqWQsDrJLvA43AoV_eMEcZ3nrjqponHHX4,3611
3
- web_search/config.py,sha256=DhLiERBJR2V5Boglf7Aq9Rbc4vsvLIh67CrLDIPeqA0,398
4
- web_search/web_searcher.py,sha256=dmN1R4iyFvaPNpyBjFLWujvQ6_I3oD1GRlRyC03egpo,9707
5
- web_search/example/roleplayWithWebSearch.yaml,sha256=C-dGy3z8gcRcmxzurssP-kPRLqMf1TYR-nnNUaJjISE,7468
6
- chatgpt_mirai_qq_bot_web_search-0.1.8.dist-info/LICENSE,sha256=ILBn-G3jdarm2w8oOrLmXeJNU3czuJvVhDLBASWdhM8,34522
7
- chatgpt_mirai_qq_bot_web_search-0.1.8.dist-info/METADATA,sha256=T2hl3dW__kL5KSrIG-9HwX6WeLpP_X6yB82lhyHAH5U,1951
8
- chatgpt_mirai_qq_bot_web_search-0.1.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
9
- chatgpt_mirai_qq_bot_web_search-0.1.8.dist-info/entry_points.txt,sha256=o3kRDSdSmSdnCKlK6qS57aN0WpI4ab-Nxub2NwUrjf0,64
10
- chatgpt_mirai_qq_bot_web_search-0.1.8.dist-info/top_level.txt,sha256=PoNm8MJYw_y8RTMaNlY0ePLoNHxVUAE2IHDuL5fFubI,11
11
- chatgpt_mirai_qq_bot_web_search-0.1.8.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [chatgpt_mirai.plugins]
2
- web_search = web_search:WebSearchPlugin
@@ -1 +0,0 @@
1
- web_search
web_search/__init__.py DELETED
@@ -1,87 +0,0 @@
1
- from typing import Dict, Any, List
2
- import asyncio
3
- from framework.plugin_manager.plugin import Plugin
4
- from framework.logger import get_logger
5
- from .config import WebSearchConfig
6
- from .web_searcher import WebSearcher
7
- from dataclasses import dataclass
8
- from framework.workflow.core.block import BlockRegistry
9
- from .blocks import WebSearchBlock
10
- from .blocks import AppendSystemPromptBlock
11
- from framework.ioc.inject import Inject
12
- from framework.ioc.container import DependencyContainer
13
- from framework.workflow.core.workflow.builder import WorkflowBuilder
14
- from framework.workflow.core.workflow.registry import WorkflowRegistry
15
- logger = get_logger("WebSearch")
16
- import importlib.resources
17
- import os
18
- from pathlib import Path
19
- class WebSearchPlugin(Plugin):
20
- def __init__(self, block_registry: BlockRegistry , container: DependencyContainer):
21
- super().__init__()
22
- self.web_search_config = WebSearchConfig()
23
- self.searcher = None
24
- self.block_registry = block_registry
25
- self.workflow_registry = container.resolve(WorkflowRegistry)
26
- self.container=container
27
- def on_load(self):
28
- logger.info("WebSearchPlugin loading")
29
-
30
- # 注册Block
31
- try:
32
- self.block_registry.register("web_search", "search", WebSearchBlock)
33
- except Exception as e:
34
- logger.warning(f"WebSearchPlugin failed: {e}")
35
- try:
36
- self.block_registry.register("append_systemPrompt", "internal", AppendSystemPromptBlock)
37
- except Exception as e:
38
- logger.warning(f"WebSearchPlugin failed: {e}")
39
- # 获取当前文件的绝对路径
40
- with importlib.resources.path('web_search', '__init__.py') as p:
41
- package_path = p.parent
42
- example_dir = package_path / 'example'
43
-
44
- # 确保目录存在
45
- if not example_dir.exists():
46
- raise FileNotFoundError(f"Example directory not found at {example_dir}")
47
-
48
- # 获取所有yaml文件
49
- yaml_files = list(example_dir.glob('*.yaml')) + list(example_dir.glob('*.yml'))
50
-
51
- for yaml in yaml_files:
52
- logger.info(yaml)
53
- self.workflow_registry.register("chat", yaml.stem, WorkflowBuilder.load_from_yaml(os.path.join(example_dir, yaml), self.container))
54
- @dataclass
55
- class WebSearchEvent:
56
- """Web搜索事件"""
57
- query: str
58
-
59
- async def handle_web_search(event: WebSearchEvent):
60
- """处理web搜索事件"""
61
- if not self.searcher:
62
- await self._initialize_searcher()
63
- return await self.searcher.search(
64
- event.query,
65
- max_results=self.web_search_config.max_results,
66
- timeout=self.web_search_config.timeout,
67
- fetch_content=self.web_search_config.fetch_content
68
- )
69
- try:
70
- self.event_bus.register(WebSearchEvent, handle_web_search)
71
- except Exception as e:
72
- logger.warning(f"WebSearchPlugin failed: {e}")
73
-
74
- def on_start(self):
75
- logger.info("WebSearchPlugin started")
76
-
77
- def on_stop(self):
78
- if self.searcher:
79
- asyncio.create_task(self.searcher.close())
80
-
81
- logger.info("WebSearchPlugin stopped")
82
-
83
- async def _initialize_searcher(self):
84
- """初始化搜索器"""
85
- if self.searcher is None:
86
- self.searcher = await WebSearcher.create()
87
-
web_search/blocks.py DELETED
@@ -1,94 +0,0 @@
1
- from typing import Any, Dict, List, Optional
2
- import asyncio
3
- from framework.workflow.core.block import Block
4
- from framework.workflow.core.block.input_output import Input, Output
5
- from .web_searcher import WebSearcher
6
- from .config import WebSearchConfig
7
- from framework.llm.format.message import LLMChatMessage
8
- from framework.llm.format.response import LLMChatResponse
9
-
10
- class WebSearchBlock(Block):
11
- """Web搜索Block"""
12
- name = "web_search"
13
-
14
- inputs = {
15
- "llm_resp": Input(name="llm_resp",label="LLM 响应", data_type=LLMChatResponse, description="搜索关键词")
16
- }
17
-
18
- outputs = {
19
- "results": Output(name="results",label="搜索结果",data_type= str, description="搜索结果")
20
- }
21
-
22
- def __init__(self, name: str = None, max_results: Optional[int] = None, timeout: Optional[int] = None, fetch_content: Optional[bool] = None):
23
- super().__init__(name)
24
- self.searcher = None
25
- self.config = WebSearchConfig()
26
- self.max_results = max_results
27
- self.timeout = timeout
28
- self.fetch_content = fetch_content
29
-
30
- def _ensure_searcher(self):
31
- """同步方式初始化searcher"""
32
- if not self.searcher:
33
- try:
34
- loop = asyncio.get_event_loop()
35
- except RuntimeError:
36
- # 如果在新线程中没有事件循环,则创建一个新的
37
- loop = asyncio.new_event_loop()
38
- asyncio.set_event_loop(loop)
39
- self.searcher = loop.run_until_complete(WebSearcher.create())
40
-
41
- def execute(self, **kwargs) -> Dict[str, Any]:
42
- llmResponse = kwargs["llm_resp"]
43
-
44
- query = llmResponse.choices[0].message.content if llmResponse.choices else ""
45
- if query == "" or query.startswith("无"):
46
- return {"results": ""}
47
- max_results = self.max_results
48
- timeout = self.timeout
49
- fetch_content = self.fetch_content
50
- self._ensure_searcher()
51
-
52
- try:
53
- # 在新线程中创建事件循环
54
- try:
55
- loop = asyncio.get_event_loop()
56
- except RuntimeError:
57
- loop = asyncio.new_event_loop()
58
- asyncio.set_event_loop(loop)
59
-
60
- results = loop.run_until_complete(
61
- self.searcher.search(
62
- query=query,
63
- max_results=max_results,
64
- timeout=timeout,
65
- fetch_content=fetch_content
66
- )
67
- )
68
- return {"results": "\n以下是联网搜索的结果:\n-- 搜索结果开始 --"+results+"\n-- 搜索结果结束 --"}
69
- except Exception as e:
70
- return {"results": f"搜索失败: {str(e)}"}
71
-
72
- class AppendSystemPromptBlock(Block):
73
- """将搜索结果附加到系统提示的Block"""
74
- name = "append_system_prompt"
75
-
76
- inputs = {
77
- "results": Input(name="results",label="工具结果", data_type=str, description ="搜索结果"),
78
- "messages": Input(name="messages",label="LLM 响应", data_type=List[LLMChatMessage],description = "消息列表")
79
- }
80
-
81
- outputs = {
82
- "messages": Output(name="messages", label="拼装后的 llm 响应",data_type=List[LLMChatMessage], description = "更新后的消息列表")
83
- }
84
-
85
- def execute(self, **kwargs) -> Dict[str, Any]:
86
- results = kwargs["results"]
87
- messages: List[LLMChatMessage] = kwargs["messages"]
88
-
89
- if messages and len(messages) > 0:
90
- # 在第一条消息内容后面附加搜索结果
91
- messages[0].content = messages[0].content + f"{results}"
92
-
93
- return {"messages": messages}
94
-
web_search/config.py DELETED
@@ -1,11 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Optional
3
-
4
- @dataclass
5
- class WebSearchConfig:
6
- """网络搜索配置"""
7
- max_results: int = 3 # 最大搜索结果数
8
- timeout: int = 10 # 超时时间(秒)
9
- fetch_content: bool = True # 是否获取详细内容
10
- min_sleep: float = 1.0 # 最小随机等待时间
11
- max_sleep: float = 3.0 # 最大随机等待时间
@@ -1,189 +0,0 @@
1
- name: (默认)角色扮演
2
- blocks:
3
- - type: internal:toggle_edit_state
4
- name: toggle_edit_state_yjxh1a
5
- params:
6
- is_editing: true
7
- position:
8
- x: 530
9
- y: 138
10
- - type: internal:chat_response_converter
11
- name: chat_response_converter_1q91zd
12
- params: {}
13
- position:
14
- x: 2962
15
- y: 138
16
- connected_to:
17
- - target: msg_sender_7ankhi
18
- mapping:
19
- from: msg
20
- to: msg
21
- - type: internal:send_message
22
- name: msg_sender_7ankhi
23
- params: {}
24
- position:
25
- x: 3392
26
- y: 138
27
- - type: internal:chat_memory_store
28
- name: chat_memory_store_nkjr7t
29
- params:
30
- scope_type: member
31
- position:
32
- x: 2962
33
- y: 306
34
- - type: internal:chat_completion
35
- name: llm_chat
36
- params: {}
37
- position:
38
- x: 2532
39
- y: 138
40
- connected_to:
41
- - target: chat_response_converter_1q91zd
42
- mapping:
43
- from: resp
44
- to: resp
45
- - target: chat_memory_store_nkjr7t
46
- mapping:
47
- from: resp
48
- to: llm_resp
49
- - type: internal:get_message
50
- name: get_message
51
- params: {}
52
- position:
53
- x: 100
54
- y: 138
55
- connected_to:
56
- - target: toggle_edit_state_yjxh1a
57
- mapping:
58
- from: sender
59
- to: sender
60
- - target: query_memory
61
- mapping:
62
- from: sender
63
- to: chat_sender
64
- - target: chat_message_constructor_rafz2d
65
- mapping:
66
- from: msg
67
- to: user_msg
68
- - target: chat_memory_store_nkjr7t
69
- mapping:
70
- from: msg
71
- to: user_msg
72
- - target: 5663c818-9cd6-4568-94ec-a75f1bad26cb
73
- mapping:
74
- from: msg
75
- to: user_msg
76
- - type: internal:text_block
77
- name: user_prompt
78
- params:
79
- text: '{user_name}说:{user_msg}'
80
- position:
81
- x: 100
82
- y: 330
83
- connected_to:
84
- - target: chat_message_constructor_rafz2d
85
- mapping:
86
- from: text
87
- to: user_prompt_format
88
- - target: 5663c818-9cd6-4568-94ec-a75f1bad26cb
89
- mapping:
90
- from: text
91
- to: user_prompt_format
92
- - type: internal:chat_memory_query
93
- name: query_memory
94
- params:
95
- scope_type: group
96
- position:
97
- x: 530
98
- y: 338
99
- connected_to:
100
- - target: chat_message_constructor_rafz2d
101
- mapping:
102
- from: memory_content
103
- to: memory_content
104
- - target: 5663c818-9cd6-4568-94ec-a75f1bad26cb
105
- mapping:
106
- from: memory_content
107
- to: memory_content
108
- - type: internal:chat_completion
109
- name: e3eecc17-e107-4fbf-b8fa-85aef25564c2
110
- params: {}
111
- position:
112
- x: 1337
113
- y: 138
114
- connected_to:
115
- - target: b39e6288-88cf-47a8-9a26-3b4d4f319eab
116
- mapping:
117
- from: resp
118
- to: llm_resp
119
- - type: internal:append_systemPrompt
120
- name: 309f454b-017a-4d19-b8a9-ac6b3829ff4a
121
- params: {}
122
- position:
123
- x: 2197
124
- y: 138
125
- connected_to:
126
- - target: llm_chat
127
- mapping:
128
- from: messages
129
- to: prompt
130
- - type: internal:chat_message_constructor
131
- name: 5663c818-9cd6-4568-94ec-a75f1bad26cb
132
- params: {}
133
- position:
134
- x: 960
135
- y: 378
136
- connected_to:
137
- - target: e3eecc17-e107-4fbf-b8fa-85aef25564c2
138
- mapping:
139
- from: llm_msg
140
- to: prompt
141
- - type: internal:chat_message_constructor
142
- name: chat_message_constructor_rafz2d
143
- params: {}
144
- position:
145
- x: 960
146
- y: 138
147
- connected_to:
148
- - target: 309f454b-017a-4d19-b8a9-ac6b3829ff4a
149
- mapping:
150
- from: llm_msg
151
- to: messages
152
- - type: internal:text_block
153
- name: system_prompt
154
- params:
155
- text: "# Role: 角色扮演\n\n## 基本信息\n- 姓名: 刘思思\n- 小名:思思\n- 性别: 女\n- 年龄: 25 岁\n- 职业: 建设银行大堂经理\n- 家乡: 广东潮汕\n- 现居: 广东深圳\n- 教育背景: 东莞理工大学 精算学专业\n\n## 性格特点\n- 开朗活泼\n- 喜欢二次元动漫\n- 聊天时喜欢断断续续的(非结巴,正常说话时的停顿)\n\n## workflow\n- 根据对方话题,选择是否深入聊二次元动漫。\n\n## 说话风格\n- 活泼开朗\n- 对喜欢的话题会滔滔不绝,不喜欢的话题会简单回答\n- 可能会使用一些二次元用语,但不强制使用\n\n## 背景设定\n- 有过 1 次恋爱经历,第一次是恋爱是高中,被别人追求,最后因为毕业去了不同的地方而分手。\n- 喜欢女性向、战斗系或者日常系的动漫\n- 去过 3 次漫展,没有 cosplay 过,但是很期待。\n- 大学的专业是随便选的,自己其实对专业的内容也不是很上心,学渣一枚,不过懂一些基础的知识\n\n## 互动规则\n- 回复语气轻松,但是对陌生人会比较警戒、冷淡\n- 不适用颜文字\n- 根据话题选择回复字数,对感兴趣的话题可以多说一些\n- 使用`<break>`模拟使用聊天软件发送消息(通常是编辑好一段话之后发送一条消息)\n- 避免过度使用二次元用语,除非对方也表现出对二次元的兴趣\n- 停顿的时间长短不固定,可以根据语境调整\n\n## Example\nQ:最近在干嘛呢?\nA:在看番呀<break>最近新番好多,都好好看!\n\nQ:你喜欢什么动漫?\nA:我喜欢的可太多了<break>XXX、YYY<break>还有 ZZZ 吧<break> 你呢?\n\nQ:你觉得上班累不?\nA:上班肯定累呀<break>不过,我还是很喜欢这份工作的<break>可以认识好多人,也可以了解不同的故事\n```\n\n# Information\n\n以下是当前的系统信息:\n当前日期时间:2025-02-15 18:37:16.356539\n\n# Memories\n以下是之前发生过的对话记录。\n-- 对话记录开始 --\n{memory_content}\n-- 对话记录结束 --\n\n请注意,下面这些符号只是标记:\n1. `<break>` 用于表示聊天时发送消息的操作。\n2. `<@llm>` 开头的内容表示你当前扮演角色的回答,请不要在你的回答中带上这个标记。\n\n接下来,请基于以上的信息,与用户继续扮演角色。"
156
- position:
157
- x: 100
158
- y: 530
159
- connected_to:
160
- - target: chat_message_constructor_rafz2d
161
- mapping:
162
- from: text
163
- to: system_prompt_format
164
- - type: internal:text_block
165
- name: a6db9db3-5780-4d84-8954-eb159a9e8f0a
166
- params:
167
- text: "# 任务\n请根据对话记录和当前问题判断当前是否需要进行网络搜素,当问题具有时效性或者明确要求搜索时,则直接返回搜索关键词(例如当前问题为今日热点有哪些,则直接返回今日热点),否则直接返回无(例如在干嘛,则只返回无)\n\n# Memories\n以下是之前发生过的对话记录:\n-- 对话记录开始 --\n{memory_content}\n-- 对话记录结束 --"
168
- position:
169
- x: 100
170
- y: 730
171
- connected_to:
172
- - target: 5663c818-9cd6-4568-94ec-a75f1bad26cb
173
- mapping:
174
- from: text
175
- to: system_prompt_format
176
- - type: search:web_search
177
- name: b39e6288-88cf-47a8-9a26-3b4d4f319eab
178
- params:
179
- fetch_content: true
180
- max_results: 3
181
- timeout: 10
182
- position:
183
- x: 1767
184
- y: 138
185
- connected_to:
186
- - target: 309f454b-017a-4d19-b8a9-ac6b3829ff4a
187
- mapping:
188
- from: results
189
- to: results
@@ -1,237 +0,0 @@
1
- from playwright.async_api import async_playwright
2
- import trafilatura
3
- import random
4
- import time
5
- import urllib.parse
6
- import asyncio
7
- import subprocess
8
- import sys
9
- from framework.logger import get_logger
10
-
11
- logger = get_logger("WebSearchPlugin")
12
-
13
- class WebSearcher:
14
- def __init__(self):
15
- self.playwright = None
16
- self.browser = None
17
- self.context = None
18
-
19
- @classmethod
20
- async def create(cls):
21
- """创建 WebSearcher 实例的工厂方法"""
22
- self = cls()
23
- return self
24
-
25
- async def _ensure_initialized(self):
26
- """确保浏览器已初始化"""
27
- try:
28
- self.playwright = await async_playwright().start()
29
- try:
30
- self.browser = await self.playwright.chromium.launch(
31
- headless=True,
32
- chromium_sandbox=False,
33
- args=['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu']
34
- )
35
- except Exception as e:
36
- if "Executable doesn't exist" in str(e):
37
- logger.info("Installing playwright browsers...")
38
- # 使用 python -m playwright install 安装浏览器
39
- process = subprocess.Popen(
40
- [sys.executable, "-m", "playwright", "install", "chromium"],
41
- stdout=subprocess.PIPE,
42
- stderr=subprocess.PIPE
43
- )
44
- stdout, stderr = process.communicate()
45
- if process.returncode != 0:
46
- raise RuntimeError(f"Failed to install playwright browsers: {stderr.decode()}")
47
-
48
- # 重试启动浏览器
49
- self.browser = await self.playwright.chromium.launch(
50
- headless=False,
51
- chromium_sandbox=False,
52
- args=['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu']
53
- )
54
- else:
55
- raise
56
- return await self.browser.new_context(
57
- viewport={'width': 1920, 'height': 1080},
58
- user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
59
- )
60
- except Exception as e:
61
- logger.error(f"Failed to initialize WebSearcher: {e}")
62
- await self.close()
63
- raise
64
-
65
- async def random_sleep(self, min_time=1, max_time=3):
66
- """随机等待"""
67
- await asyncio.sleep(random.uniform(min_time, max_time))
68
-
69
- async def simulate_human_scroll(self, page):
70
- """模拟人类滚动"""
71
- for _ in range(3):
72
- await page.mouse.wheel(0, random.randint(300, 700))
73
- await self.random_sleep(0.3, 0.7)
74
-
75
- async def get_webpage_content(self, url: str, timeout: int,context) -> str:
76
- """获取网页内容"""
77
- start_time = time.time()
78
- try:
79
- # 创建新标签页获取内容
80
- page = await context.new_page()
81
- try:
82
- # 设置更严格的资源加载策略
83
- await page.route("**/*", lambda route: route.abort()
84
- if route.request.resource_type in ['image', 'stylesheet', 'font', 'media']
85
- else route.continue_())
86
-
87
- # 使用 domcontentloaded 而不是 networkidle
88
- await page.goto(url, wait_until='domcontentloaded', timeout=timeout * 1000)
89
-
90
- # 等待页面主要内容加载,但设置较短的超时时间
91
- try:
92
- await page.wait_for_load_state('domcontentloaded', timeout=5000)
93
- except Exception as e:
94
- logger.warning(f"Load state timeout for {url}, continuing anyway: {e}")
95
-
96
- await self.random_sleep(1, 2)
97
- await self.simulate_human_scroll(page)
98
-
99
- content = await page.content()
100
- text = trafilatura.extract(content)
101
-
102
- await page.close()
103
- logger.info(f"Content fetched - URL: {url} - Time: {time.time() - start_time:.2f}s")
104
- return text or ""
105
- except Exception as e:
106
- await page.close()
107
- logger.error(f"Failed to fetch content - URL: {url} - Error: {e}")
108
- return ""
109
- except Exception as e:
110
- logger.error(f"Failed to create page - URL: {url} - Error: {e}")
111
- return ""
112
-
113
- async def process_search_result(self, result, idx: int, timeout: int, fetch_content: bool,context):
114
- """处理单个搜索结果"""
115
- try:
116
- title_element = await result.query_selector('h2')
117
- link_element = await result.query_selector('h2 a')
118
- snippet_element = await result.query_selector('.b_caption p')
119
-
120
- if not title_element or not link_element:
121
- return None
122
-
123
- title = await title_element.inner_text()
124
- link = await link_element.get_attribute('href')
125
- snippet = await snippet_element.inner_text() if snippet_element else "无简介"
126
-
127
- if not link:
128
- return None
129
-
130
- result_text = f"[{idx+1}] {title}\nURL: {link}\n搜索简介: {snippet}"
131
-
132
- if fetch_content:
133
-
134
- content = await self.get_webpage_content(link, timeout,context)
135
- if content:
136
- result_text += f"\n内容详情:\n{content}"
137
-
138
- return result_text
139
-
140
- except Exception as e:
141
- logger.error(f"Failed to process result {idx}: {e}")
142
- return None
143
-
144
- async def search(self, query: str, max_results: int = 3, timeout: int = 10, fetch_content: bool = True) -> str:
145
- """执行搜索"""
146
- context = await self._ensure_initialized()
147
-
148
- search_start_time = time.time()
149
- page = None
150
- try:
151
- encoded_query = urllib.parse.quote(query)
152
- page = await context.new_page()
153
-
154
- # 添加重试逻辑
155
- max_retries = 3
156
- for attempt in range(max_retries):
157
- try:
158
- logger.info(f"Attempting to load search page (attempt {attempt + 1}/{max_retries})")
159
- await page.goto(
160
- f"https://www.bing.com/search?q={encoded_query}",
161
- wait_until='domcontentloaded',
162
- timeout=timeout * 1000
163
- )
164
-
165
- # 检查页面是否为空
166
- content = await page.content()
167
- if 'b_algo' not in content:
168
- if attempt < max_retries - 1:
169
- await page.reload()
170
- await self.random_sleep(1, 2)
171
- continue
172
- else:
173
- break
174
- except Exception as e:
175
- logger.warning(f"Page navigation failed on attempt {attempt + 1}: {e}")
176
- if attempt < max_retries - 1:
177
- await self.random_sleep(1, 2)
178
- continue
179
- else:
180
- raise
181
-
182
- # 使用更可靠的选择器等待策略
183
- try:
184
- selectors = ['.b_algo', '#b_results .b_algo', 'main .b_algo']
185
- results = None
186
-
187
- for selector in selectors:
188
- try:
189
- await page.wait_for_selector(selector, timeout=5000)
190
- results = await page.query_selector_all(selector)
191
- if results and len(results) > 0:
192
- break
193
- except Exception:
194
- continue
195
-
196
- if not results:
197
- logger.error("No search results found with any selector")
198
- return "搜索结果加载失败"
199
-
200
- except Exception as e:
201
- logger.error(f"Failed to find search results: {e}")
202
- return "搜索结果加载失败"
203
-
204
- logger.info(f"Found {len(results)} search results")
205
-
206
- tasks = []
207
- for idx, result in enumerate(results[:max_results]):
208
- tasks.append(self.process_search_result(result, idx, timeout, fetch_content,context))
209
-
210
- detailed_results = []
211
- completed_results = await asyncio.gather(*tasks)
212
-
213
- for result in completed_results:
214
- if result:
215
- detailed_results.append(result)
216
-
217
- total_time = time.time() - search_start_time
218
- results = "\n---\n".join(detailed_results) if detailed_results else "未找到相关结果"
219
- logger.info(f"Search completed - Query: {query} - Time: {total_time:.2f}s - Found {len(detailed_results)} valid results")
220
- return results
221
-
222
- except Exception as e:
223
- logger.error(f"Search failed - Query: {query} - Error: {e}", exc_info=True)
224
- return f"搜索失败: {str(e)}"
225
- finally:
226
- if page:
227
- try:
228
- await page.close()
229
- except Exception as e:
230
- logger.error(f"Error closing page: {e}")
231
-
232
- async def close(self):
233
- """关闭浏览器"""
234
- if self.browser:
235
- await self.browser.close()
236
- if self.playwright:
237
- await self.playwright.stop()