BG5 commited on
Commit
4e7fad0
·
verified ·
1 Parent(s): 13c3300

Rename in to api.txt

Browse files
Files changed (2) hide show
  1. api.txt +465 -0
  2. in +0 -0
api.txt ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, Response, WebSocket, WebSocketDisconnect, Query
2
+ import httpx
3
+ import uvicorn
4
+ import asyncio
5
+ import websockets
6
+ import json
7
+ import subprocess
8
+ import threading
9
+ import time
10
+ import os
11
+ import random
12
+ from typing import Dict
13
+ from loguru import logger
14
+ from starlette.responses import StreamingResponse
15
+ import socket
16
+
17
+ app = FastAPI()
18
+
19
+ # 浏览器实例管理
20
+ BROWSERS: Dict[str, dict] = {} # {browser_id: {"process": Popen, "ws": str, "port": int, "status": str, "info": dict}}
21
+ # 浏览器实例状态: open, closed
22
+ PORT_RANGE = (9300, 9700)
23
+ CHROME_PATH = "google-chrome" # Linux下可用命令,或自定义绝对路径
24
+ PROFILE_BASE = "/tmp/profiles"
25
+
26
+ import re
27
+ import platform
28
+ import psutil
29
+ import datetime
30
+
31
+ def get_system_info():
32
+ # CPU型号
33
+ try:
34
+ with open("/proc/cpuinfo") as f:
35
+ cpuinfo = f.read()
36
+ model_match = re.search(r"model name\s*:\s*(.+)", cpuinfo)
37
+ cpu_model = model_match.group(1) if model_match else "Unknown"
38
+ except Exception:
39
+ cpu_model = "Unknown"
40
+
41
+ # GPU型号
42
+ try:
43
+ gpu_info = subprocess.check_output("nvidia-smi --query-gpu=name --format=csv,noheader", shell=True).decode().strip()
44
+ gpu_model = gpu_info.split('\n')[0] if gpu_info else "Unknown"
45
+ except Exception:
46
+ try:
47
+ lspci_info = subprocess.check_output("lspci | grep VGA", shell=True).decode().strip()
48
+ gpu_model = lspci_info.split(":")[-1].strip() if lspci_info else "Unknown"
49
+ except Exception:
50
+ gpu_model = "Unknown"
51
+
52
+ # 操作系统信息
53
+ os_name = platform.system()
54
+ os_version = platform.version()
55
+ os_release = platform.release()
56
+
57
+ # 主机名
58
+ hostname = socket.gethostname()
59
+ hostname_short = '-'.join(hostname.split('-')[-3:])
60
+ # 启动时间
61
+ boot_time = datetime.datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S")
62
+
63
+ # 运行时长
64
+ uptime_seconds = int(time.time() - psutil.boot_time())
65
+ uptime = str(datetime.timedelta(seconds=uptime_seconds))
66
+
67
+ # 物理内存
68
+ mem = psutil.virtual_memory()
69
+ mem_total = f"{int(mem.total / 1024 / 1024)}MB"
70
+
71
+ # CPU核心数
72
+ cpu_count_logical = psutil.cpu_count()
73
+ cpu_count_physical = psutil.cpu_count(logical=False)
74
+
75
+ # 磁盘
76
+ disk = psutil.disk_usage('/')
77
+ disk_total = f"{int(disk.total / 1024 / 1024 / 1024)}GB"
78
+ disk_used = f"{int(disk.used / 1024 / 1024 / 1024)}GB"
79
+
80
+ # 当前用户
81
+ try:
82
+ user = os.getlogin()
83
+ except Exception:
84
+ user = "Unknown"
85
+
86
+ # IP地址
87
+ try:
88
+ ip = socket.gethostbyname(hostname)
89
+ except Exception:
90
+ ip = "Unknown"
91
+
92
+ return {
93
+ "cpu_model": cpu_model,
94
+ "gpu_model": gpu_model,
95
+ "os": f"{os_name} {os_release} ({os_version})",
96
+ "hostname": hostname_short,
97
+ "boot_time": boot_time,
98
+ "uptime": uptime,
99
+ "mem_total": mem_total,
100
+ "cpu_count_logical": cpu_count_logical,
101
+ "cpu_count_physical": cpu_count_physical,
102
+ "disk_total": disk_total,
103
+ "disk_used": disk_used,
104
+ "user": user,
105
+ "ip": ip
106
+ }
107
+ def get_system_usage():
108
+ cpu = subprocess.check_output("top -bn1 | grep 'Cpu(s)'", shell=True).decode().strip()
109
+ mem = subprocess.check_output("free -m | grep Mem", shell=True).decode().strip()
110
+ # 字段映射
111
+ cpu_key_map = {
112
+ "us": "user",
113
+ "sy": "system",
114
+ "ni": "nice",
115
+ "id": "idle",
116
+ "wa": "iowait",
117
+ "hi": "hardware_irq",
118
+ "si": "software_irq",
119
+ "st": "steal"
120
+ }
121
+ # 解析CPU信息
122
+ cpu_parts = cpu.split(":")[1].split(",")
123
+ cpu_info = {}
124
+ for part in cpu_parts:
125
+ value, key = part.strip().split(" ", 1)
126
+ key = key.strip().replace("%", "")
127
+ mapped_key = cpu_key_map.get(key, key)
128
+ cpu_info[mapped_key] = value.strip() + "%"
129
+ # 解析内存信息
130
+ mem_values = mem.split()[1:4]
131
+ mem_total, mem_used, mem_free = mem_values
132
+ mem_available = int(mem_total) - int(mem_used)
133
+ mem_info = {
134
+ "Total": f"{mem_total}MB",
135
+ "Used": f"{mem_used}MB",
136
+ "Free": f"{mem_free}MB",
137
+ "Available": f"{mem_available}MB"
138
+ }
139
+ sys_info = get_system_info()
140
+ logger.info(f"CPU: {cpu_info}, Memory: {mem_info}")
141
+ info = {"CPU": cpu_info, "Memory": mem_info, "System": sys_info}
142
+ return info
143
+
144
+
145
+ def get_free_port():
146
+ used_ports = {b["port"] for b in BROWSERS.values()}
147
+ for _ in range(100):
148
+ port = random.randint(*PORT_RANGE)
149
+ if port not in used_ports:
150
+ return port
151
+ raise RuntimeError("No free port available")
152
+
153
+
154
+ def gen_browser_id():
155
+ return str(int(time.time() * 1000)) + str(random.randint(1000, 9999))
156
+
157
+
158
+ def get_default_launch_args():
159
+ return [
160
+ "--disable-gpu",
161
+ "--disable-dev-shm-usage",
162
+ "--disable-software-rasterizer",
163
+ "--disable-extensions",
164
+ "--disable-background-networking",
165
+ "--disable-default-apps",
166
+ "--disable-sync",
167
+ "--disable-translate",
168
+ "--disable-features=TranslateUI",
169
+ "--no-first-run",
170
+ "--no-default-browser-check",
171
+ "--remote-allow-origins=*",
172
+ "--incognito",
173
+ "--disable-blink-features=AutomationControlled",
174
+ "--disable-notifications",
175
+ "--disable-popup-blocking",
176
+ "--disable-infobars",
177
+ "--disable-features=TranslateUI,NotificationIndicator",
178
+ "--headless"
179
+ ]
180
+
181
+
182
+ def wait_port(host, port, timeout=5.0):
183
+ """等待端口可用"""
184
+ start = time.time()
185
+ while time.time() - start < timeout:
186
+ try:
187
+ with socket.create_connection((host, port), timeout=0.5):
188
+ return True
189
+ except Exception:
190
+ time.sleep(0.1)
191
+ return False
192
+
193
+
194
+ @app.get("/system/usage")
195
+ async def system_usage():
196
+ """
197
+ 获取系统资源使用情况
198
+ """
199
+ try:
200
+ usage_info = get_system_usage()
201
+ return {"success": True, "msg": "success", "data": usage_info}
202
+ except Exception as e:
203
+ logger.error(f"获取系统资源使用情况失败: {e}")
204
+ return {"success": False, "msg": "failed", "error": str(e)}
205
+
206
+
207
+ # BitBrowser风格API
208
+ @app.post("/browser/creat")
209
+ @app.post("/browser/update")
210
+ @app.post("/browser/open")
211
+ async def open_browser(request: Request):
212
+ data = await request.json()
213
+ browser_id = data.get("id", '')
214
+ if browser_id in BROWSERS:
215
+ logger.info(f"Browser ID {browser_id}: {BROWSERS[browser_id]}")
216
+ if BROWSERS[browser_id]["status"] == "open":
217
+ return {
218
+ "success": True,
219
+ "msg": "already opened",
220
+ "data": {"id": browser_id, "ws": BROWSERS[browser_id]["ws"]},
221
+ "info": BROWSERS[browser_id]["info"]
222
+ }
223
+ else:
224
+ port = get_free_port()
225
+ BROWSERS[browser_id]["port"] = port
226
+ else:
227
+ # 新建浏览器实例
228
+ browser_id = gen_browser_id()
229
+ port = get_free_port()
230
+ logger.info(f"新建浏览器实例: {browser_id} :{port}")
231
+
232
+ profile_dir = f"{PROFILE_BASE}/{browser_id}"
233
+ os.makedirs(profile_dir, exist_ok=True)
234
+ # 支持自定义启动参数并去重
235
+ launch_args = data.get("launchArgs", "")
236
+ args = [CHROME_PATH, f"--remote-debugging-port={port}", f"--user-data-dir={profile_dir}"]
237
+ # 加入默认参数
238
+ logger.info(f"使用默认参数: {get_default_launch_args()}")
239
+ args.extend(get_default_launch_args())
240
+ if launch_args:
241
+ # 拆分参数并去重,保留顺序
242
+ base_args = args[1:] # 除去chrome可执行文件
243
+ extra_args = [a for a in launch_args.split() if a]
244
+ all_args = base_args + extra_args
245
+ seen = set()
246
+ deduped_args = []
247
+ for arg in all_args:
248
+ key = arg.split('=')[0].strip() if '=' in arg else arg.strip()
249
+ if key not in seen:
250
+ seen.add(key)
251
+ deduped_args.append(arg)
252
+ args = [CHROME_PATH] + deduped_args
253
+ proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
254
+ if not wait_port("127.0.0.1", port, timeout=60):
255
+ proc.terminate()
256
+ return {"success": False, "msg": f"chrome端口{port}未就绪", "data": {}}
257
+ async with httpx.AsyncClient() as client:
258
+ resp = await client.get(f"http://127.0.0.1:{port}/json/version")
259
+ if resp.status_code == 200:
260
+ browser_info = resp.json()
261
+ ws_url = browser_info.get("webSocketDebuggerUrl").replace("ws://127.0.0.1", "wss://0.0.0.0")
262
+ # 去掉端口号
263
+ ws_url = re.sub(r":\d+", "", ws_url)
264
+ logger.info(f"Browser opened with ID: {browser_id}, Info: {browser_info}")
265
+ BROWSERS[browser_id] = {"process": proc, "ws": ws_url, "port": port, "status": "open", "info": browser_info}
266
+ return {"success": True, "msg": "success", "data": {"id": browser_id, "ws": ws_url}, "info": browser_info}
267
+
268
+
269
+ @app.post("/browser/close")
270
+ async def close_browser(request: Request):
271
+ data = await request.json()
272
+ browser_id = data.get("id")
273
+ b = BROWSERS.get(browser_id)
274
+ if not b:
275
+ return {"success": False, "msg": "not found"}
276
+ b["process"].terminate()
277
+ b["status"] = "closed"
278
+ return {"success": True, "msg": "closed", "data": {"id": browser_id}}
279
+
280
+
281
+ @app.post("/browser/delete")
282
+ async def delete_browser(request: Request):
283
+ data = await request.json()
284
+ browser_id = data.get("id")
285
+ b = BROWSERS.pop(browser_id, None)
286
+ if not b:
287
+ return {"success": False, "msg": "not found"}
288
+ try:
289
+ b["process"].terminate()
290
+ except Exception:
291
+ pass
292
+ # 删除用户数据目录
293
+ profile_dir = f"{PROFILE_BASE}/{browser_id}"
294
+ if os.path.exists(profile_dir):
295
+ import shutil
296
+ shutil.rmtree(profile_dir, ignore_errors=True)
297
+ return {"success": True, "msg": "deleted", "data": {"id": browser_id}}
298
+
299
+
300
+ # @app.post("/browser/update")
301
+ # async def update_browser(request: Request):
302
+ # data = await request.json()
303
+ # # 兼容 BitBrowser 结构
304
+ # launch_args = ""
305
+ # if isinstance(data.get("browserFingerPrint"), dict):
306
+ # launch_args = data["browserFingerPrint"].get("launchArgs", "")
307
+ # elif "launchArgs" in data:
308
+ # launch_args = data["launchArgs"]
309
+ # # 合并 launchArgs 到 data
310
+ # if launch_args:
311
+ # data["launchArgs"] = launch_args
312
+ # return await open_browser(Request({**request.scope, "body": json.dumps(data).encode()}, receive=request._receive))
313
+
314
+
315
+ @app.post("/browser/ports")
316
+ async def browser_ports():
317
+ # 返回 {id: port} 格式
318
+ data = {k: str(v["ws"]) for k, v in BROWSERS.items() if v["status"] == "open"}
319
+ return {"success": True, "data": data}
320
+
321
+
322
+ @app.post("/health")
323
+ async def health():
324
+ return {"success": True, "msg": "ok"}
325
+
326
+
327
+ CDP_PATHS = [
328
+ "/json",
329
+ "/json/list",
330
+ "/json/version",
331
+ "/json/protocol",
332
+ ]
333
+
334
+
335
+ def get_browser_by_id(browser_id: str):
336
+ b = BROWSERS.get(browser_id)
337
+ if not b or b["status"] != "open":
338
+ return None
339
+ return b
340
+
341
+
342
+ async def find_browser_by_target_id(tab_type,target_id: str):
343
+ """
344
+ 根据 targetId 智能查找对应的浏览器实例
345
+
346
+ 1. 检查 target_id 是否是一个 browser_id
347
+ 2. 如果不是,查询所有浏览器实例的页面列表,找到匹配 target_id 的页面所在浏览器
348
+ 3. 如果找不到,返回第一个可用的浏览器实例
349
+ """
350
+ if tab_type == "browser":
351
+ # 查询所有浏览器实例,找到匹配的页面
352
+ for browser_id, browser in BROWSERS.items():
353
+ ws_url = browser.get("info", {}).get("webSocketDebuggerUrl", "")
354
+ if target_id in ws_url:
355
+ return browser
356
+ # 查询所有浏览器实例,找到匹配的页面
357
+ for browser_id, browser in BROWSERS.items():
358
+ if browser["status"] != "open":
359
+ continue
360
+ try:
361
+ # 尝试获取浏览器的页面列表
362
+ port = browser["port"]
363
+ async with httpx.AsyncClient() as client:
364
+ resp = await client.get(f"http://127.0.0.1:{port}/json/list")
365
+ if resp.status_code == 200:
366
+ pages = resp.json()
367
+ # 检查页面是否包含 target_id
368
+ for page in pages:
369
+ if page.get("id") == target_id:
370
+ return browser
371
+ except Exception as e:
372
+ logger.error(f"查询浏览器 {browser_id} 页面列表失败: {e}")
373
+
374
+ # 如果找不到,返回第一个可用的浏览器实例
375
+ for browser_id, browser in BROWSERS.items():
376
+ if browser["status"] == "open":
377
+ return browser
378
+
379
+ return None
380
+
381
+
382
+ @app.api_route("/json", methods=["GET"])
383
+ @app.api_route("/json/list", methods=["GET"])
384
+ @app.api_route("/json/version", methods=["GET"])
385
+ @app.api_route("/json/protocol", methods=["GET"])
386
+ async def cdp_native_proxy(request: Request):
387
+ # 这个接口只返回最后一个浏览器实例的内容
388
+ # 1. 获取最后一个浏览器实例
389
+ if not BROWSERS:
390
+ return Response(content="No browser instance", status_code=404)
391
+ browser_id = next(reversed(BROWSERS.keys()))
392
+ b = get_browser_by_id(browser_id)
393
+ if not b:
394
+ return Response(content="browser not found", status_code=404)
395
+ port = b["port"]
396
+ # 2. 构造目标URL
397
+ path = request.url.path
398
+ url = f"http://127.0.0.1:{port}{path}"
399
+ # 3. 代理请求
400
+ headers = dict(request.headers)
401
+ headers["host"] = "127.0.0.1"
402
+ body = await request.body()
403
+ async with httpx.AsyncClient(follow_redirects=True) as client:
404
+ resp = await client.request(
405
+ request.method,
406
+ url,
407
+ headers=headers,
408
+ content=body,
409
+ params=request.query_params
410
+ )
411
+ return Response(
412
+ content=resp.content,
413
+ status_code=resp.status_code,
414
+ headers=dict(resp.headers)
415
+ )
416
+
417
+
418
+ @app.websocket("/devtools/{tab_type}/{target_id}")
419
+ async def cdp_native_ws_proxy(websocket: WebSocket, tab_type: str, target_id: str):
420
+ if not target_id:
421
+ await websocket.close()
422
+ return
423
+ # 智能查找 target_id 对应的浏览器实例
424
+ browser = await find_browser_by_target_id(tab_type,target_id)
425
+ if not browser:
426
+ await websocket.close(code=1008, reason="无法找到有效的浏览器实例")
427
+ return
428
+
429
+ await websocket.accept()
430
+ tab_type = "page" if tab_type == "page" else "browser"
431
+ logger.info(f"WebSocket连接: {browser}")
432
+ port = browser["port"]
433
+ target_url = f"ws://127.0.0.1:{port}/devtools/{tab_type}/{target_id}"
434
+ logger.info(f"Forwarding to: {target_url}")
435
+
436
+ try:
437
+ async with websockets.connect(target_url) as target_ws:
438
+ async def forward_client_to_server():
439
+ try:
440
+ while True:
441
+ data = await websocket.receive_text()
442
+ await target_ws.send(data)
443
+ except WebSocketDisconnect:
444
+ pass
445
+
446
+ async def forward_server_to_client():
447
+ try:
448
+ while True:
449
+ response = await target_ws.recv()
450
+ await websocket.send_text(response)
451
+ except websockets.exceptions.ConnectionClosed:
452
+ pass
453
+
454
+ await asyncio.gather(
455
+ forward_client_to_server(),
456
+ forward_server_to_client()
457
+ )
458
+ except Exception as e:
459
+ logger.error(f"WebSocket代理错误: {e}")
460
+ finally:
461
+ await websocket.close()
462
+
463
+
464
+ if __name__ == "__main__":
465
+ uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=True)
in DELETED
File without changes