返回提交历史
Modified
g4f/cli/__init__.py
+55
-1
Modified
g4f/cli/client.py
+2
-0
Modified
g4f/mcp/__init__.py
+14
-0
Modified
g4f/mcp/server.py
+10
-1
Modified
g4f/mcp/tools.py
+499
-3
Added
g4f/tray.py
+268
-0
Modified
setup.py
+7
-1
XFEstudio/gpt4free
feat: add system tray integration and new tools for file and web operations
c4eca02e
代码差异
7 个文件
+855
-6
@@ -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(
@@ -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
@@ -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',
@@ -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",
@@ -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()
@@ -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