Skip to content

Commit 920fb47

Browse files
committed
新增 API 信息获取功能,更新 index.html 以显示 API 调用信息;新增 mdns_publisher.py 脚本以支持局域网内服务广播,更新 README.md 以包含使用说明和注意事项;在 server.py 中实现获取 API 配置信息的端点,增强用户体验。
1 parent 126b3cd commit 920fb47

4 files changed

Lines changed: 278 additions & 14 deletions

File tree

README.md

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,24 @@
99
## 📝 目录
1010

1111
* [项目概述](#项目概述)
12-
* [免责声明](#-免责声明)
13-
* [核心特性](#-核心特性-python-版本)
14-
* [重要提示](#️-重要提示-python-版本)
12+
* [免责声明](#免责声明)
13+
* [核心特性](#核心特性-python-版本)
14+
* [重要提示](#️重要提示-python-版本)
1515
* [开始使用](#-开始使用-python-版本)
1616
* [1. 先决条件](#1-先决条件)
1717
* [2. 安装](#2-安装)
18-
* [3. 认证设置 (关键!)](#3-认证设置-关键步骤)
18+
* [3. 认证设置 (关键步骤!)](#3-认证设置-关键步骤)
1919
* [4. 运行代理](#4-运行代理)
2020
* [5. API 使用](#5-api-使用)
21-
* [6. 配置客户端](#6-配置客户端-以-open-webui-为例)
22-
* [多平台指南](#-多平台指南-python-版本)
23-
* [故障排除](#-故障排除-python-版本)
24-
* [关于 Camoufox](#-关于-camoufox)
25-
* [关于 `fetch_camoufox_data.py`](#-关于-fetch_camoufox_datapy)
26-
* [贡献](#-贡献)
27-
* [License](#-license)
28-
* [未来计划 / Roadmap](#-未来计划--roadmap)
21+
* [6. 配置客户端 (以 Open WebUI 为例)](#6-配置客户端-以-open-webui-为例)
22+
* [7. (可选) 局域网域名访问 (mDNS)](#7-可选-局域网域名访问-mdns)
23+
* [多平台指南](#多平台指南-python-版本)
24+
* [故障排除](#故障排除-python-版本)
25+
* [关于 Camoufox](#关于-camoufox)
26+
* [关于 `fetch_camoufox_data.py`](#关于-fetch_camoufox_datapy)
27+
* [贡献](#贡献)
28+
* [License](#license)
29+
* [未来计划 / Roadmap](#未来计划--roadmap)
2930
* [控制日志输出](#控制日志输出-python-版本)
3031

3132
---
@@ -306,6 +307,37 @@
306307
7. 保存设置。
307308
8. 现在,你应该可以在 Open WebUI 中选择 `aistudio-gemini-py` 模型并开始聊天了。
308309
310+
### 7. (可选) 局域网域名访问 (mDNS)
311+
312+
项目包含一个辅助脚本 `mdns_publisher.py`,它使用 mDNS (Bonjour/ZeroConf) 在你的局域网内广播此代理服务。这允许你和其他局域网内的设备通过一个更友好的 `.local` 域名(例如 `http://chatui.local:2048`)来访问服务,而无需记住或查找服务器的 IP 地址。
313+
314+
**用途:**
315+
316+
* 当你希望在手机、平板或其他电脑上方便地访问运行在 Mac/PC 上的代理服务时。
317+
* 避免因 IP 地址变化而需要更新客户端配置。
318+
319+
**如何使用:**
320+
321+
1. **安装依赖:** 此脚本需要额外的库。在你的虚拟环境中运行:
322+
```bash
323+
pip install zeroconf netifaces
324+
```
325+
2. **运行脚本:** 你需要**同时运行** `server.py` (监听在 `0.0.0.0` 和指定端口,如 2048) 和 `mdns_publisher.py`
326+
**另一个终端**窗口,运行:
327+
```bash
328+
python mdns_publisher.py
329+
```
330+
* 默认广播的域名是 `chatui.local`,广播的端口是脚本内 `PORT` 变量定义的端口 (当前为 2048)。
331+
* 你可以使用 `--name yourname` 参数来修改广播的域名前缀,例如 `python mdns_publisher.py --name mychat` 将广播 `mychat.local`
332+
* 此脚本**不需要** `sudo` 权限运行。
333+
3. **访问服务:** 在局域网内的其他支持 mDNS 的设备上,通过浏览器访问 `http://<你设置的域名>.local:<端口>`,例如 `http://chatui.local:2048`
334+
335+
**注意:**
336+
337+
* 确保你的防火墙允许 UDP 端口 5353 (mDNS) 的通信。
338+
* 客户端设备需要支持 mDNS 才能解析 `.local` 域名。
339+
* 此脚本广播的是 `server.py` 实际监听的端口 (由 `mdns_publisher.py` 中的 `PORT` 变量决定)。
340+
309341
## 💻 多平台指南 (Python 版本)
310342
311343
* **macOS / Linux**: 通常开箱即用。确保 Python, pip 已安装。按照安装步骤安装 Camoufox 和 Playwright 依赖。

index.html

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,15 @@
287287
<body>
288288
<div class="container">
289289
<h1>AI Studio Proxy Chat</h1>
290+
<div id="api-info-area" style="padding: 10px 20px; border-bottom: 1px solid var(--border-color); background-color: var(--bg-color); font-size: 0.9em; color: var(--text-color);">
291+
<details>
292+
<summary style="cursor: pointer; font-weight: 500; color: var(--primary-color);">查看 API 调用信息</summary>
293+
<div id="api-info-content" style="margin-top: 8px;">
294+
<!-- API 信息将加载到这里 -->
295+
正在加载 API 信息...
296+
</div>
297+
</details>
298+
</div>
290299
<div id="chatbox">
291300
<!-- Chat messages will be appended here -->
292301
</div>
@@ -328,10 +337,49 @@ <h1>AI Studio Proxy Chat</h1>
328337
console.log("Chat initialized/cleared.");
329338
}
330339

331-
// --- Display Initial System Prompt ---
340+
// --- Load API Info ---
341+
async function loadApiInfo() {
342+
const infoContent = document.getElementById('api-info-content');
343+
try {
344+
const response = await fetch('/api/info');
345+
if (!response.ok) {
346+
throw new Error(`无法获取 API 信息: ${response.status} ${response.statusText}`);
347+
}
348+
const data = await response.json();
349+
350+
// 更新内容区域,而不是整个 api-info-area
351+
infoContent.innerHTML = `
352+
<style>
353+
#api-info-area pre {
354+
background-color: var(--input-bg);
355+
padding: 8px 12px;
356+
border-radius: var(--border-radius-sm);
357+
border: 1px solid var(--input-border);
358+
overflow-x: auto;
359+
white-space: pre-wrap; /* 允许换行 */
360+
word-wrap: break-word; /* 防止长 URL 溢出 */
361+
font-family: monospace;
362+
font-size: 0.95em;
363+
margin: 5px 0;
364+
}
365+
#api-info-area strong { color: var(--primary-color); }
366+
</style>
367+
<strong>API Base URL:</strong> <pre><code>${data.api_base_url || '未知'}</code></pre>
368+
<strong>Model Name:</strong> <pre><code>${data.model_name || '未知'}</code></pre>
369+
<strong>API Key:</strong> <pre><code>${data.api_key_required ? '是 (需要自行配置)' : '否 (Not Required)'}</code></pre>
370+
`;
371+
372+
} catch (error) {
373+
console.error("获取 API 信息失败:", error);
374+
infoContent.innerHTML = `<span style="color: var(--error-msg-text);">加载 API 信息失败: ${error.message}</span>`;
375+
}
376+
}
377+
378+
// --- Display Initial System Prompt ---
332379
initializeChat(); // Initialize on load
380+
loadApiInfo(); // Load API info after chat initialization
333381

334-
// --- Event Listeners ---
382+
// --- Event Listeners ---
335383
sendButton.addEventListener('click', sendMessage);
336384
clearButton.addEventListener('click', initializeChat); // Add listener for clear button
337385
userInput.addEventListener('keydown', (event) => {

mdns_publisher.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import socket
2+
import time
3+
import argparse
4+
import signal
5+
import sys
6+
from zeroconf import ServiceInfo, Zeroconf
7+
import netifaces # Used for more reliable IP address discovery
8+
9+
# --- Configuration ---
10+
DEFAULT_SERVICE_NAME = "AI Studio Chat UI" # User-friendly name shown in Bonjour browsers
11+
DEFAULT_DOMAIN_NAME = "chatui" # The base name, .local will be appended
12+
SERVICE_TYPE = "_http._tcp.local." # Standard for HTTP services
13+
PORT = 2048 # Port where server.py is running
14+
15+
# --- Global Variables ---
16+
zeroconf = None
17+
info = None
18+
19+
def get_lan_ip():
20+
"""
21+
Attempts to find the primary LAN IP address of the machine.
22+
Prioritizes the interface associated with the default gateway.
23+
"""
24+
try:
25+
# Find the default gateway
26+
gateways = netifaces.gateways()
27+
default_gateway_info = gateways.get('default', {}).get(netifaces.AF_INET)
28+
29+
if default_gateway_info:
30+
interface = default_gateway_info[1]
31+
addresses = netifaces.ifaddresses(interface)
32+
ipv4_info = addresses.get(netifaces.AF_INET)
33+
if ipv4_info:
34+
ip_address = ipv4_info[0]['addr']
35+
print(f" 发现默认网关接口 '{interface}' 的 IP: {ip_address}") # 中文
36+
return ip_address
37+
else:
38+
print(" 警告: 未找到默认网关信息。") # 中文
39+
40+
# Fallback: Iterate through all interfaces if default gateway method fails
41+
print(" 尝试遍历所有接口...") # 中文
42+
for interface in netifaces.interfaces():
43+
addresses = netifaces.ifaddresses(interface)
44+
ipv4_info = addresses.get(netifaces.AF_INET)
45+
if ipv4_info:
46+
ip_address = ipv4_info[0]['addr']
47+
# Avoid loopback and obviously wrong addresses
48+
if not ip_address.startswith("127.") and ip_address != '0.0.0.0':
49+
print(f" 使用接口 '{interface}' 的 IP: {ip_address}") # 中文
50+
return ip_address
51+
52+
except Exception as e:
53+
print(f" 获取 IP 地址时出错 (netifaces): {e}") # 中文
54+
55+
# Final fallback: Use standard socket method (less reliable)
56+
try:
57+
print(" 回退到标准 socket 方法获取 IP...") # 中文
58+
hostname = socket.gethostname()
59+
ip_address = socket.gethostbyname(hostname)
60+
if not ip_address.startswith("127."):
61+
print(f" 使用 socket.gethostbyname 获取的 IP: {ip_address}") # 中文
62+
return ip_address
63+
# Try getting IP associated with default route socket connection
64+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
65+
s.settimeout(0)
66+
try:
67+
# Doesn't have to be reachable
68+
s.connect(('10.254.254.254', 1))
69+
ip_address = s.getsockname()[0]
70+
except Exception:
71+
ip_address = '127.0.0.1' # Default if connect fails
72+
finally:
73+
s.close()
74+
if not ip_address.startswith("127."):
75+
print(f" 使用 socket 连接获取的 IP: {ip_address}") # 中文
76+
return ip_address
77+
78+
except socket.gaierror as e:
79+
print(f" 获取 IP 地址时出错 (socket): {e}") # 中文
80+
81+
print("❌ 错误: 无法自动检测有效的局域网 IP 地址。") # 中文
82+
return None
83+
84+
def register_service(name: str):
85+
global zeroconf, info
86+
87+
ip_address = get_lan_ip()
88+
if not ip_address:
89+
print("无法启动 mDNS 服务,因为未能确定 IP 地址。") # 中文
90+
sys.exit(1)
91+
92+
hostname = f"{name}.local." # Fully qualified domain name for mDNS
93+
94+
print(f"\n--- 正在注册 mDNS 服务 ---") # 中文
95+
print(f" 服务类型: {SERVICE_TYPE}") # 中文
96+
print(f" 服务名称: {DEFAULT_SERVICE_NAME}") # 中文
97+
print(f" 域名: {hostname.rstrip('.')}") # 中文
98+
print(f" IP 地址: {ip_address}") # 中文
99+
print(f" 端口: {PORT}") # 中文
100+
101+
# Construct the ServiceInfo object
102+
info = ServiceInfo(
103+
SERVICE_TYPE,
104+
f"{DEFAULT_SERVICE_NAME}.{SERVICE_TYPE}", # Unique instance name
105+
addresses=[socket.inet_aton(ip_address)],
106+
port=PORT,
107+
properties={}, # No specific properties needed for basic HTTP
108+
server=hostname, # This links the service to the desired hostname
109+
)
110+
111+
zeroconf = Zeroconf()
112+
print(" 正在广播服务...") # 中文
113+
zeroconf.register_service(info)
114+
print(f"✅ 服务已注册。现在可以在局域网内尝试访问 http://{hostname.rstrip('.')}:{PORT}") # 中文
115+
print(" (按 Ctrl+C 停止广播)") # 中文
116+
117+
def unregister_service(signum, frame):
118+
global zeroconf, info
119+
print("\n--- 收到停止信号,正在注销 mDNS 服务 ---") # 中文
120+
if zeroconf and info:
121+
try:
122+
zeroconf.unregister_service(info)
123+
zeroconf.close()
124+
print("✅ 服务已注销。") # 中文
125+
except Exception as e:
126+
print(f" 注销服务时出错: {e}") # 中文
127+
sys.exit(0)
128+
129+
if __name__ == "__main__":
130+
parser = argparse.ArgumentParser(description="使用 mDNS/Bonjour 在局域网内广播 AI Studio Proxy 服务。") # 中文
131+
parser.add_argument(
132+
"--name",
133+
type=str,
134+
default=DEFAULT_DOMAIN_NAME,
135+
help=f"要在局域网内广播的域名基础部分 (将附加 '.local')。默认为: '{DEFAULT_DOMAIN_NAME}'" # 中文
136+
)
137+
args = parser.parse_args()
138+
139+
# Setup signal handling for graceful shutdown
140+
signal.signal(signal.SIGINT, unregister_service) # Handle Ctrl+C
141+
signal.signal(signal.SIGTERM, unregister_service) # Handle termination signal
142+
143+
try:
144+
# Check dependencies
145+
try:
146+
import netifaces
147+
import zeroconf
148+
except ImportError as e:
149+
print(f"❌ 错误: 缺少依赖库 '{e.name}'。") # 中文
150+
print(" 请先运行安装命令: pip install zeroconf netifaces") # 中文
151+
sys.exit(1)
152+
153+
register_service(args.name)
154+
# Keep the script alive while zeroconf runs in background threads
155+
while True:
156+
time.sleep(1)
157+
except Exception as e:
158+
print(f"\n❌ 脚本运行时发生意外错误: {e}") # 中文
159+
# Ensure cleanup happens even on unexpected errors before exit
160+
unregister_service(None, None)

server.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,30 @@ async def read_index():
859859
raise HTTPException(status_code=404, detail="index.html not found")
860860
return FileResponse(index_html_path)
861861

862+
# --- 新增:获取 API 配置信息的端点 ---
863+
@app.get("/api/info")
864+
async def get_api_info(request: Request):
865+
"""返回 API 配置信息,如基础 URL 和模型名称"""
866+
print("[API] 收到 /api/info 请求。") # 中文
867+
host = request.headers.get('host') or f"{args.host}:{args.port}" # 回退到启动参数 (需要确保args可访问)
868+
# 简单的方案:假设是 http。如果部署在 https 后,需要调整。
869+
# 或者从请求头 X-Forwarded-Proto 获取协议
870+
scheme = request.headers.get('x-forwarded-proto', 'http')
871+
base_url = f"{scheme}://{host}" # 基础 URL,不包含 /v1
872+
api_base = f"{base_url}/v1" # API 端点基础路径
873+
874+
# 注意:直接访问 args 可能在 uvicorn 运行时有问题。
875+
# 更健壮的方式是通过 request 或全局状态管理获取 host/port。
876+
# 这里使用 request.headers.get('host') 作为主要方式。
877+
878+
return JSONResponse(content={
879+
"model_name": MODEL_NAME,
880+
"api_base_url": api_base, # e.g., http://127.0.0.1:2048/v1
881+
"server_base_url": base_url, # e.g., http://127.0.0.1:2048
882+
"api_key_required": False, # 当前不需要 API 密钥
883+
"message": "API Key is not required for this proxy."
884+
})
885+
862886
# --- API Endpoints --- (Translate print statements)
863887
@app.get("/health")
864888
async def health_check():

0 commit comments

Comments
 (0)