XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

feat: add system tray integration and new tools for file and web operations

c4eca02e
hlohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

7 个文件 +855 -6
Modified g4f/cli/__init__.py +55 -1
@@ -270,6 +270,56 @@ def get_auth_parser(exit_on_error: bool = True) -> ArgumentParser:
270 270 auth_parser.add_argument("action", nargs="?", choices=["status", "login", "logout"], default="login", help="Action to perform (default: login)")
271 271 return auth_parser
272 272
273
274 # --------------------------------------------------------------
275 # SYSTRAY PARSER / RUNNER
276 # --------------------------------------------------------------
277 def get_tray_parser(exit_on_error: bool = True) -> ArgumentParser:
278 """
279 Parser for:
280 g4f systray ...
281 """
282 tray_parser = ArgumentParser(
283 description="Run g4f as a system tray application",
284 exit_on_error=exit_on_error,
285 )
286 tray_parser.add_argument(
287 "--port", "-p",
288 type=int,
289 default=DEFAULT_PORT,
290 help=f"Port for the API server (default: {DEFAULT_PORT}).",
291 )
292 tray_parser.add_argument(
293 "--host",
294 default="0.0.0.0",
295 help="Bind host for the API server (default: 0.0.0.0).",
296 )
297 tray_parser.add_argument(
298 "--debug", "-d",
299 action="store_true",
300 help="Enable verbose logging.",
301 )
302 tray_parser.add_argument(
303 "--no-autostart",
304 action="store_true",
305 help="Do not start the API server automatically on launch.",
306 )
307 return tray_parser
308
309
310 def run_tray_args(args):
311 """
312 Launches the system tray icon using the parsed CLI arguments.
313 """
314 from ..tray import run_tray
315 run_tray(
316 port=args.port,
317 host=args.host,
318 debug=args.debug,
319 no_autostart=args.no_autostart,
320 )
321
322
273 323 # --------------------------------------------------------------
274 324 # MAIN ENTRYPOINT
275 325 # --------------------------------------------------------------
@@ -287,7 +337,7 @@ def main():
287 337
288 338
289 339 mode_parser = ArgumentParser(description="Select mode to run g4f in.", exit_on_error=False)
290 mode_parser.add_argument("mode", nargs="?", choices=["api", "gui", "client", "mcp", "auth", "dev"], default="api", help="Mode to run g4f in (default: api).")
340 mode_parser.add_argument("mode", nargs="?", choices=["api", "gui", "client", "mcp", "auth", "dev", "systray", "tray"], default="api", help="Mode to run g4f in (default: api).")
291 341
292 342 # Preserve original remaining so the API parser gets all args if mode
293 343 # detection fails (e.g. `python -m g4f --port 8080` without a mode prefix).
@@ -332,6 +382,10 @@ def main():
332 382 parser = get_mcp_parser()
333 383 args = parser.parse_args(remaining)
334 384 run_mcp_args(args)
385 elif args.mode in ("systray", "tray"):
386 parser = get_tray_parser()
387 args = parser.parse_args(remaining)
388 run_tray_args(args)
335 389 else:
336 390 # No mode provided
337 391 raise argparse.ArgumentError(
Modified g4f/cli/client.py +2 -0
@@ -157,6 +157,7 @@ async def stream_response(
157 157
158 158
159 159 async def save_content(content, media: Optional['MediaResponse'], filepath: str, allowed_types=None) -> bool:
160 global aiohttp
160 161 if media:
161 162 for url in media.get_list():
162 163 if url.startswith(("http://", "https://")):
@@ -290,6 +291,7 @@ async def run_args(input_val, args):
290 291
291 292
292 293 async def async_run_client_args(args, exit_on_error=True):
294 global aiohttp
293 295 input_txt = ""
294 296 media = []
295 297 rest = 0
Modified g4f/mcp/__init__.py +14 -0
@@ -24,6 +24,13 @@ from .tools import (
24 24 FileWriteTool,
25 25 FileListTool,
26 26 FileDeleteTool,
27 CreateDirectoryTool,
28 CreateFileTool,
29 FetchWebpageTool,
30 FileSearchGlobTool,
31 GrepSearchTool,
32 GithubRepoTool,
33 GithubTextSearchTool,
27 34 )
28 35 from .pa_provider import (
29 36 execute_safe_code,
@@ -52,6 +59,13 @@ __all__ = [
52 59 'FileWriteTool',
53 60 'FileListTool',
54 61 'FileDeleteTool',
62 'CreateDirectoryTool',
63 'CreateFileTool',
64 'FetchWebpageTool',
65 'FileSearchGlobTool',
66 'GrepSearchTool',
67 'GithubRepoTool',
68 'GithubTextSearchTool',
55 69 # PA provider system
56 70 'execute_safe_code',
57 71 'load_pa_provider',
Modified g4f/mcp/server.py +10 -1
@@ -29,7 +29,9 @@ from ..image.copy_images import get_media_dir, copy_media, get_source_url
29 29 from .tools import (
30 30 MarkItDownTool, TextToAudioTool, WebSearchTool, WebScrapeTool, ImageGenerationTool,
31 31 PythonExecuteTool, FileReadTool, FileReadLinesTool, FileSearchTool,
32 FileWriteTool, FileListTool, FileDeleteTool, ApplyPatchTool
32 FileWriteTool, FileListTool, FileDeleteTool, ApplyPatchTool,
33 CreateDirectoryTool, CreateFileTool, FetchWebpageTool,
34 FileSearchGlobTool, GrepSearchTool, GithubRepoTool, GithubTextSearchTool,
33 35 )
34 36
35 37
@@ -82,6 +84,13 @@ class MCPServer:
82 84 'file_write': FileWriteTool(),
83 85 'file_list': FileListTool(safe_mode=safe_mode),
84 86 'file_delete': FileDeleteTool(),
87 'create_directory': CreateDirectoryTool(),
88 'create_file': CreateFileTool(),
89 'fetch_webpage': FetchWebpageTool(),
90 'file_search_glob': FileSearchGlobTool(),
91 'grep_search': GrepSearchTool(),
92 'github_repo': GithubRepoTool(),
93 'github_text_search': GithubTextSearchTool(),
85 94 }
86 95 self.server_info = {
87 96 "name": "gpt4free-mcp-server",
Added g4f/tray.py +268 -0
@@ -0,0 +1,268 @@
1 """
2 System tray integration for g4f.
3
4 Starts the g4f API server in a background thread and places an icon in the
5 system tray with quick-access menu items.
6
7 Requirements:
8 pip install pystray pillow
9 """
10
11 from __future__ import annotations
12
13 import threading
14 import webbrowser
15 import logging
16 from typing import Optional
17
18 logger = logging.getLogger(__name__)
19
20
21 # SVG source for the tray icon (https://g4f.dev/dist/img/g4f.svg)
22 _G4F_SVG = """\
23 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
24 <defs>
25 <linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
26 <stop offset="0%" style="stop-color:#6e48aa"/>
27 <stop offset="100%" style="stop-color:#4776E6"/>
28 </linearGradient>
29 <filter id="glow">
30 <feGaussianBlur stdDeviation="3" result="blur"/>
31 <feMerge>
32 <feMergeNode in="blur"/>
33 <feMergeNode in="SourceGraphic"/>
34 </feMerge>
35 </filter>
36 <filter id="shadow">
37 <feDropShadow dx="0" dy="4" stdDeviation="10" flood-color="#1a1a2e" flood-opacity="0.5"/>
38 </filter>
39 </defs>
40 <circle cx="256" cy="256" r="240" fill="url(#bgGrad)" filter="url(#shadow)"/>
41 <circle cx="256" cy="256" r="210" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1.5"/>
42 <text x="250" y="328" font-family="Arial,Helvetica,sans-serif" font-size="194" font-weight="900"
43 fill="#f8f9fa" text-anchor="middle" letter-spacing="-5" filter="url(#glow)">G4F</text>
44 </svg>"""
45
46
47 def _make_icon():
48 """
49 Return the g4f tray icon as a PIL Image (64×64 RGBA).
50
51 Rendering priority:
52 1. cairosvg — renders the official g4f SVG to PNG then loads with Pillow.
53 2. Pillow-only fallback — simple programmatic circle + text icon.
54 """
55 try:
56 from PIL import Image
57 import io
58 except ImportError:
59 return None
60
61 # --- attempt SVG rendering via cairosvg ---
62 try:
63 import cairosvg
64 png_bytes = cairosvg.svg2png(
65 bytestring=_G4F_SVG.encode(),
66 output_width=64,
67 output_height=64,
68 )
69 return Image.open(io.BytesIO(png_bytes)).convert("RGBA")
70 except Exception:
71 pass
72
73 # --- Pillow-only fallback ---
74 try:
75 from PIL import ImageDraw, ImageFont
76 except ImportError:
77 return None
78
79 size = 64
80 img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
81 draw = ImageDraw.Draw(img)
82
83 # Gradient-ish background (approximate purple→blue)
84 draw.ellipse([2, 2, size - 2, size - 2], fill=(110, 72, 170, 255))
85
86 try:
87 font = ImageFont.truetype("arial.ttf", 18)
88 except Exception:
89 font = ImageFont.load_default()
90
91 text = "G4F"
92 bbox = draw.textbbox((0, 0), text, font=font)
93 text_w = bbox[2] - bbox[0]
94 text_h = bbox[3] - bbox[1]
95 draw.text(((size - text_w) // 2, (size - text_h) // 2), text, fill=(248, 249, 250, 255), font=font)
96
97 return img
98
99
100 def run_tray(
101 port: int = 1337,
102 host: str = "0.0.0.0",
103 debug: bool = False,
104 no_autostart: bool = False,
105 ):
106 """
107 Launch the g4f system tray application.
108
109 Parameters
110 ----------
111 port : int
112 Port for the API server (default 1337).
113 host : str
114 Bind host for the API server (default 0.0.0.0).
115 debug : bool
116 Enable verbose logging.
117 no_autostart : bool
118 If True, the API server is NOT started automatically on launch.
119 """
120 try:
121 import pystray
122 except ImportError:
123 raise ImportError(
124 "pystray is required for system tray support. "
125 "Install it with: pip install pystray pillow"
126 )
127
128 from g4f.api import AppConfig, run_api
129
130 browser_url = f"http://127.0.0.1:{port}"
131
132 # ------------------------------------------------------------------ #
133 # Server management #
134 # ------------------------------------------------------------------ #
135 _server_thread: Optional[threading.Thread] = None
136 _server_running = threading.Event()
137
138 def _start_server():
139 nonlocal _server_thread
140 if _server_running.is_set():
141 return
142 _server_running.set()
143 AppConfig.set_config(gui=True)
144
145 def _run():
146 try:
147 run_api(
148 bind=f"{host}:{port}",
149 port=None,
150 debug=debug,
151 )
152 except Exception as exc:
153 logger.error("API server error: %s", exc)
154 finally:
155 _server_running.clear()
156
157 _server_thread = threading.Thread(target=_run, daemon=True, name="g4f-api")
158 _server_thread.start()
159
160 def _stop_server():
161 # uvicorn does not expose a clean stop; we clear the flag so the UI
162 # reflects the intent. The daemon thread will be killed on process exit.
163 _server_running.clear()
164
165 # ------------------------------------------------------------------ #
166 # Menu callbacks #
167 # ------------------------------------------------------------------ #
168 def on_open_browser(icon, item):
169 webbrowser.open(browser_url)
170
171 def on_toggle_server(icon, item):
172 if _server_running.is_set():
173 _stop_server()
174 else:
175 _start_server()
176 icon.update_menu()
177
178 def on_quit(icon, item):
179 _stop_server()
180 icon.stop()
181
182 # ------------------------------------------------------------------ #
183 # Dynamic menu #
184 # ------------------------------------------------------------------ #
185 def server_label(item) -> str:
186 return "Stop Server" if _server_running.is_set() else "Start Server"
187
188 def server_enabled(item) -> bool:
189 return True
190
191 menu = pystray.Menu(
192 pystray.MenuItem("Open in Browser", on_open_browser, default=True),
193 pystray.Menu.SEPARATOR,
194 pystray.MenuItem(server_label, on_toggle_server, enabled=server_enabled),
195 pystray.Menu.SEPARATOR,
196 pystray.MenuItem("Quit", on_quit),
197 )
198
199 icon_image = _make_icon()
200 if icon_image is None:
201 # Fallback: 1×1 transparent image (pystray still works, icon may be blank)
202 try:
203 from PIL import Image
204 icon_image = Image.new("RGBA", (1, 1))
205 except ImportError:
206 raise ImportError(
207 "pillow is required for system tray support. "
208 "Install it with: pip install pystray pillow"
209 )
210
211 tray_icon = pystray.Icon(
212 name="g4f",
213 icon=icon_image,
214 title="g4f AI Server",
215 menu=menu,
216 )
217
218 # Auto-start the server unless opted out
219 if not no_autostart:
220 _start_server()
221
222 tray_icon.run()
223
224
225 def _tray_main():
226 """
227 Standalone entry point installed as the ``g4f-tray`` console script.
228 Parses CLI arguments and delegates to :func:`run_tray`.
229 """
230 import argparse
231 from g4f.config import DEFAULT_PORT
232
233 parser = argparse.ArgumentParser(
234 description="Run g4f as a system tray application",
235 prog="g4f-tray",
236 )
237 parser.add_argument(
238 "--port", "-p",
239 type=int,
240 default=DEFAULT_PORT,
241 help=f"Port for the API server (default: {DEFAULT_PORT}).",
242 )
243 parser.add_argument(
244 "--host",
245 default="0.0.0.0",
246 help="Bind host for the API server (default: 0.0.0.0).",
247 )
248 parser.add_argument(
249 "--debug", "-d",
250 action="store_true",
251 help="Enable verbose logging.",
252 )
253 parser.add_argument(
254 "--no-autostart",
255 action="store_true",
256 help="Do not start the API server automatically on launch.",
257 )
258 args = parser.parse_args()
259 run_tray(
260 port=args.port,
261 host=args.host,
262 debug=args.debug,
263 no_autostart=args.no_autostart,
264 )
265
266
267 if __name__ == "__main__":
268 _tray_main()
Modified setup.py +7 -1
@@ -99,7 +99,12 @@ EXTRA_REQUIRE = {
99 99 "files": [
100 100 "beautifulsoup4",
101 101 "markitdown[all]",
102 ]
102 ],
103 "tray": [
104 "pystray",
105 "pillow",
106 "cairosvg",
107 ],
103 108 }
104 109
105 110 DESCRIPTION = (
@@ -126,6 +131,7 @@ setup(
126 131 'console_scripts': [
127 132 'g4f=g4f.cli:main',
128 133 'g4f-mcp=g4f.mcp.server:main',
134 'g4f-tray=g4f.tray:_tray_main',
129 135 ],
130 136 },
131 137 url='https://github.com/xtekky/gpt4free', # Link to your GitHub repository