Spooker commited on
Commit
1cf891c
·
verified ·
1 Parent(s): 96cf8ec

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +54 -140
app.py CHANGED
@@ -10,18 +10,20 @@
10
  - /v1/reset-run
11
  - /health
12
  - 将原来的命令行/TTY 登录改为网页管理页:
13
- - / 管理页(需先通过管理密码
14
- - /admin/gate 管理页登录页
15
  - /admin/login/start
16
  - /admin/login/status
17
- - /admin/logout
18
  - 适配 Hugging Face Spaces:
19
  - 读取 PORT 环境变量
20
  - 凭据优先保存到 /data(若已开通持久化存储)
21
- - 可通过 Secrets 传入 API_KEY / ADMIN_PASSWORD / ACCOUNTS_JSON
 
 
 
22
  """
23
 
24
  import asyncio
 
25
  import json
26
  import os
27
  import platform
@@ -46,15 +48,12 @@ PORT = int(os.environ.get("PORT", "7860"))
46
  HOST = os.environ.get("HOST", "0.0.0.0")
47
  POLL_INTERVAL_S = int(os.environ.get("POLL_INTERVAL_S", "5"))
48
  TIMEOUT_S = int(os.environ.get("TIMEOUT_S", "300"))
49
- COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "1") != "0"
50
 
51
  # /v1/* 访问鉴权(Bearer)
52
  PROXY_API_KEY = os.environ.get("API_KEY", "")
53
- # 管理后台鉴权(网页登录 + Cookie
 
54
  ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "")
55
- ADMIN_COOKIE_NAME = "freebuff_admin_auth"
56
- ADMIN_COOKIE_VALUE = "ok"
57
- ADMIN_COOKIE_MAX_AGE = 86400 * 7
58
 
59
  MODEL_TO_AGENT = {
60
  "minimax/minimax-m2.7": "base2-free",
@@ -547,25 +546,30 @@ async def auth_middleware(request: web.Request, handler):
547
  return await handler(request)
548
 
549
 
550
- def is_admin_authed(request: web.Request) -> bool:
551
- if not ADMIN_PASSWORD:
552
- return True
553
- return request.cookies.get(ADMIN_COOKIE_NAME) == ADMIN_COOKIE_VALUE
554
-
555
 
556
- def get_supplied_admin_password(request: web.Request) -> str:
557
- return request.headers.get("X-Admin-Password") or request.query.get("password") or ""
 
 
 
 
 
 
 
 
558
 
559
 
560
  def require_admin(request: web.Request) -> Optional[web.Response]:
561
  if not ADMIN_PASSWORD:
562
  return None
563
- if is_admin_authed(request):
564
- return None
565
- supplied = get_supplied_admin_password(request)
566
- if supplied == ADMIN_PASSWORD:
567
  return None
568
- return web.json_response({"error": {"message": "Admin password required"}}, status=401)
 
 
 
569
 
570
 
571
  # ============ Responses API 辅助函数 ============
@@ -931,6 +935,9 @@ async def handle_models(request: web.Request) -> web.Response:
931
 
932
 
933
  async def handle_reset_run(request: web.Request) -> web.Response:
 
 
 
934
  run_cache.clear()
935
  log("Agent Run 缓存已清除")
936
  return web.json_response({"status": "cleared"})
@@ -971,96 +978,11 @@ async def handle_health(request: web.Request) -> web.Response:
971
  })
972
 
973
 
974
- async def handle_admin_gate(request: web.Request) -> web.Response:
975
- if is_admin_authed(request):
976
- raise web.HTTPFound("/")
977
-
978
- html = """<!doctype html>
979
- <html lang="zh-CN">
980
- <head>
981
- <meta charset="utf-8" />
982
- <meta name="viewport" content="width=device-width, initial-scale=1" />
983
- <title>Admin Login</title>
984
- <style>
985
- body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; margin: 0; background: #0b1020; color: #e8eefc; display:flex; min-height:100vh; align-items:center; justify-content:center; }
986
- .card { width: min(460px, 92vw); background: #141b34; border: 1px solid #2a355e; border-radius: 16px; padding: 22px; }
987
- input, button { width:100%; box-sizing:border-box; border-radius:12px; border:1px solid #3a4b80; background:#0d1430; color:#fff; padding:12px; margin-top:12px; }
988
- button { background:#3451ff; border:none; font-weight:700; cursor:pointer; }
989
- .muted { color:#9fb0dd; }
990
- .err { color:#ff9c9c; margin-top:10px; }
991
- code { background:#091024; padding:2px 6px; border-radius:8px; }
992
- </style>
993
- </head>
994
- <body>
995
- <form class="card" method="post" action="/admin/gate">
996
- <h1>管理页登录</h1>
997
- <p class="muted">请输入 <code>ADMIN_PASSWORD</code>,验证通过后才会进入管理页面。</p>
998
- <input type="password" name="password" placeholder="ADMIN_PASSWORD" autofocus />
999
- <button type="submit">进入管理页</button>
1000
- </form>
1001
- </body>
1002
- </html>"""
1003
- return web.Response(text=html, content_type="text/html")
1004
-
1005
-
1006
- async def handle_admin_gate_post(request: web.Request) -> web.Response:
1007
- if not ADMIN_PASSWORD:
1008
- raise web.HTTPFound("/")
1009
-
1010
- data = await request.post()
1011
- supplied = str(data.get("password", ""))
1012
-
1013
- if supplied != ADMIN_PASSWORD:
1014
- html = """<!doctype html>
1015
- <html lang="zh-CN">
1016
- <head>
1017
- <meta charset="utf-8" />
1018
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1019
- <title>Admin Login</title>
1020
- <style>
1021
- body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; margin: 0; background: #0b1020; color: #e8eefc; display:flex; min-height:100vh; align-items:center; justify-content:center; }
1022
- .card { width: min(460px, 92vw); background: #141b34; border: 1px solid #2a355e; border-radius: 16px; padding: 22px; }
1023
- input, button { width:100%; box-sizing:border-box; border-radius:12px; border:1px solid #3a4b80; background:#0d1430; color:#fff; padding:12px; margin-top:12px; }
1024
- button { background:#3451ff; border:none; font-weight:700; cursor:pointer; }
1025
- .muted { color:#9fb0dd; }
1026
- .err { color:#ff9c9c; margin-top:10px; }
1027
- code { background:#091024; padding:2px 6px; border-radius:8px; }
1028
- </style>
1029
- </head>
1030
- <body>
1031
- <form class="card" method="post" action="/admin/gate">
1032
- <h1>管理页登录</h1>
1033
- <p class="muted">请输入 <code>ADMIN_PASSWORD</code>,验证通过后才会进入管理页面。</p>
1034
- <input type="password" name="password" placeholder="ADMIN_PASSWORD" autofocus />
1035
- <button type="submit">进入管理页</button>
1036
- <div class="err">密码错误</div>
1037
- </form>
1038
- </body>
1039
- </html>"""
1040
- return web.Response(text=html, content_type="text/html", status=401)
1041
-
1042
- resp = web.HTTPFound("/")
1043
- resp.set_cookie(
1044
- ADMIN_COOKIE_NAME,
1045
- ADMIN_COOKIE_VALUE,
1046
- max_age=ADMIN_COOKIE_MAX_AGE,
1047
- httponly=True,
1048
- samesite="Lax",
1049
- secure=COOKIE_SECURE,
1050
- path="/",
1051
- )
1052
- raise resp
1053
-
1054
-
1055
- async def handle_admin_logout(request: web.Request) -> web.Response:
1056
- resp = web.HTTPFound("/admin/gate")
1057
- resp.del_cookie(ADMIN_COOKIE_NAME, path="/")
1058
- raise resp
1059
-
1060
 
1061
  async def handle_root(request: web.Request) -> web.Response:
1062
- if ADMIN_PASSWORD and not is_admin_authed(request):
1063
- raise web.HTTPFound("/admin/gate")
 
1064
 
1065
  html = f"""<!doctype html>
1066
  <html lang="zh-CN">
@@ -1073,7 +995,7 @@ async def handle_root(request: web.Request) -> web.Response:
1073
  .wrap {{ max-width: 1100px; margin: 0 auto; padding: 28px; }}
1074
  .card {{ background: #141b34; border: 1px solid #2a355e; border-radius: 16px; padding: 18px; margin-bottom: 18px; }}
1075
  .row {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }}
1076
- input, button, textarea {{ width: 100%; box-sizing: border-box; border-radius: 12px; border: 1px solid #3a4b80; background: #0d1430; color: #fff; padding: 12px; }}
1077
  button {{ cursor: pointer; background: #3451ff; border: none; font-weight: 700; }}
1078
  button.secondary {{ background: #273053; }}
1079
  a {{ color: #94c7ff; }}
@@ -1086,7 +1008,7 @@ async def handle_root(request: web.Request) -> web.Response:
1086
  <div class="wrap">
1087
  <div class="card">
1088
  <h1>Freebuff OpenAI Proxy · Hugging Face Space</h1>
1089
- <p class="muted">保留脚本的 OpenAI 兼容接口、Responses API、账号池轮询、Run 缓存、健康检查与重能力并把登录迁移到网页管理。</p>
1090
  <div class="row">
1091
  <div>
1092
  <strong>聊天接口</strong>
@@ -1109,9 +1031,9 @@ async def handle_root(request: web.Request) -> web.Response:
1109
  <div><button onclick="startLogin()">1. 发起网页登录</button></div>
1110
  <div><button class="secondary" onclick="checkLogin()">2. 检查登录状态</button></div>
1111
  <div><button class="secondary" onclick="refreshHealth()">刷新状态</button></div>
1112
- <div><button class="secondary" onclick="logout()">退出管理页</button></div>
1113
  </div>
1114
  <p class="muted">登录后账号会写入凭据文件。若 Space 挂载了持久化存储,会写入 <code>/data/manicode/credentials.json</code>。</p>
 
1115
  <pre id="loginBox">尚未发起登录。</pre>
1116
  </div>
1117
 
@@ -1126,31 +1048,29 @@ let currentLoginId = localStorage.getItem('loginId') || '';
1126
  const loginBox = document.getElementById('loginBox');
1127
  const healthBox = document.getElementById('healthBox');
1128
 
1129
- function adminHeaders() {{
1130
- return {{ 'Content-Type': 'application/json' }};
1131
- }}
1132
-
1133
  async function refreshHealth() {{
1134
- const res = await fetch('/health', {{ credentials: 'same-origin' }});
1135
- if (res.status === 401) {{
1136
- window.location.href = '/admin/gate';
1137
- return;
 
 
 
1138
  }}
1139
- const data = await res.json();
1140
- healthBox.textContent = JSON.stringify(data, null, 2);
1141
  }}
1142
 
1143
  async function startLogin() {{
1144
- const res = await fetch('/admin/login/start', {{ method: 'POST', headers: adminHeaders(), credentials: 'same-origin' }});
1145
- const data = await res.json();
 
 
1146
  if (!res.ok) {{
1147
- loginBox.textContent = JSON.stringify(data, null, 2);
1148
- if (res.status === 401) window.location.href = '/admin/gate';
1149
  return;
1150
  }}
1151
  currentLoginId = data.loginId;
1152
  localStorage.setItem('loginId', currentLoginId);
1153
- loginBox.innerHTML = `请在新标签页完成授权:\n\n${{JSON.stringify(data, null, 2)}}\n\n打开登录链接:\n${{data.loginUrl}}`;
1154
  window.open(data.loginUrl, '_blank');
1155
  }}
1156
 
@@ -1160,20 +1080,17 @@ async function checkLogin() {{
1160
  return;
1161
  }}
1162
  const url = '/admin/login/status?loginId=' + encodeURIComponent(currentLoginId);
1163
- const res = await fetch(url, {{ headers: adminHeaders(), credentials: 'same-origin' }});
1164
- const data = await res.json();
1165
- loginBox.textContent = JSON.stringify(data, null, 2);
1166
- if (res.status === 401) {{
1167
- window.location.href = '/admin/gate';
1168
- return;
 
1169
  }}
1170
  await refreshHealth();
1171
  }}
1172
 
1173
- function logout() {{
1174
- window.location.href = '/admin/logout';
1175
- }}
1176
-
1177
  refreshHealth();
1178
  </script>
1179
  </body>
@@ -1257,9 +1174,6 @@ def create_app() -> web.Application:
1257
  app.on_cleanup.append(on_cleanup)
1258
 
1259
  app.router.add_get("/", handle_root)
1260
- app.router.add_get("/admin/gate", handle_admin_gate)
1261
- app.router.add_post("/admin/gate", handle_admin_gate_post)
1262
- app.router.add_get("/admin/logout", handle_admin_logout)
1263
  app.router.add_get("/health", handle_health)
1264
  app.router.add_post("/v1/chat/completions", handle_chat_completion)
1265
  app.router.add_post("/v1/responses", handle_responses)
 
10
  - /v1/reset-run
11
  - /health
12
  - 将原来的命令行/TTY 登录改为网页管理页:
13
+ - / 管理页(使用浏览器 Basic Auth 门禁
 
14
  - /admin/login/start
15
  - /admin/login/status
 
16
  - 适配 Hugging Face Spaces:
17
  - 读取 PORT 环境变量
18
  - 凭据优先保存到 /data(若已开通持久化存储)
19
+ - 可通过 Secrets 传入 API_KEY / ADMIN_PASSWORD / ADMIN_USERNAME / ACCOUNTS_JSON
20
+ - 管理页门禁:
21
+ - 使用浏览器原生 HTTP Basic Auth
22
+ - 不使用 cookie,不需要单独登录页
23
  """
24
 
25
  import asyncio
26
+ import base64
27
  import json
28
  import os
29
  import platform
 
48
  HOST = os.environ.get("HOST", "0.0.0.0")
49
  POLL_INTERVAL_S = int(os.environ.get("POLL_INTERVAL_S", "5"))
50
  TIMEOUT_S = int(os.environ.get("TIMEOUT_S", "300"))
 
51
 
52
  # /v1/* 访问鉴权(Bearer)
53
  PROXY_API_KEY = os.environ.get("API_KEY", "")
54
+ # 管理后台鉴权(浏览器 Basic Auth
55
+ ADMIN_USERNAME = os.environ.get("ADMIN_USERNAME", "admin")
56
  ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "")
 
 
 
57
 
58
  MODEL_TO_AGENT = {
59
  "minimax/minimax-m2.7": "base2-free",
 
546
  return await handler(request)
547
 
548
 
 
 
 
 
 
549
 
550
+ def parse_basic_auth(request: web.Request) -> Tuple[Optional[str], Optional[str]]:
551
+ auth = request.headers.get("Authorization", "")
552
+ if not auth.startswith("Basic "):
553
+ return None, None
554
+ try:
555
+ decoded = base64.b64decode(auth[6:]).decode("utf-8")
556
+ username, password = decoded.split(":", 1)
557
+ return username, password
558
+ except Exception:
559
+ return None, None
560
 
561
 
562
  def require_admin(request: web.Request) -> Optional[web.Response]:
563
  if not ADMIN_PASSWORD:
564
  return None
565
+
566
+ username, password = parse_basic_auth(request)
567
+ if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
 
568
  return None
569
+
570
+ resp = web.Response(text="Authentication required", status=401)
571
+ resp.headers["WWW-Authenticate"] = 'Basic realm="Freebuff Admin"'
572
+ return resp
573
 
574
 
575
  # ============ Responses API 辅助函数 ============
 
935
 
936
 
937
  async def handle_reset_run(request: web.Request) -> web.Response:
938
+ denied = require_admin(request)
939
+ if denied:
940
+ return denied
941
  run_cache.clear()
942
  log("Agent Run 缓存已清除")
943
  return web.json_response({"status": "cleared"})
 
978
  })
979
 
980
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
981
 
982
  async def handle_root(request: web.Request) -> web.Response:
983
+ denied = require_admin(request)
984
+ if denied:
985
+ return denied
986
 
987
  html = f"""<!doctype html>
988
  <html lang="zh-CN">
 
995
  .wrap {{ max-width: 1100px; margin: 0 auto; padding: 28px; }}
996
  .card {{ background: #141b34; border: 1px solid #2a355e; border-radius: 16px; padding: 18px; margin-bottom: 18px; }}
997
  .row {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }}
998
+ button, textarea {{ width: 100%; box-sizing: border-box; border-radius: 12px; border: 1px solid #3a4b80; background: #0d1430; color: #fff; padding: 12px; }}
999
  button {{ cursor: pointer; background: #3451ff; border: none; font-weight: 700; }}
1000
  button.secondary {{ background: #273053; }}
1001
  a {{ color: #94c7ff; }}
 
1008
  <div class="wrap">
1009
  <div class="card">
1010
  <h1>Freebuff OpenAI Proxy · Hugging Face Space</h1>
1011
+ <p class="muted">当前管理页使用浏览器 <code>HTTP Basic Auth</code> 门禁。设了 <code>ADMIN_PASSWORD</code> 后访问面或管理接口时会先弹出浏览器用户名/密码框,不再使用 cookie。</p>
1012
  <div class="row">
1013
  <div>
1014
  <strong>聊天接口</strong>
 
1031
  <div><button onclick="startLogin()">1. 发起网页登录</button></div>
1032
  <div><button class="secondary" onclick="checkLogin()">2. 检查登录状态</button></div>
1033
  <div><button class="secondary" onclick="refreshHealth()">刷新状态</button></div>
 
1034
  </div>
1035
  <p class="muted">登录后账号会写入凭据文件。若 Space 挂载了持久化存储,会写入 <code>/data/manicode/credentials.json</code>。</p>
1036
+ <p class="muted">管理页账号默认用户名是 <code>{ADMIN_USERNAME}</code>,密码来自 <code>ADMIN_PASSWORD</code>。</p>
1037
  <pre id="loginBox">尚未发起登录。</pre>
1038
  </div>
1039
 
 
1048
  const loginBox = document.getElementById('loginBox');
1049
  const healthBox = document.getElementById('healthBox');
1050
 
 
 
 
 
1051
  async function refreshHealth() {{
1052
+ const res = await fetch('/health');
1053
+ const raw = await res.text();
1054
+ try {{
1055
+ const data = JSON.parse(raw);
1056
+ healthBox.textContent = JSON.stringify(data, null, 2);
1057
+ }} catch (e) {{
1058
+ healthBox.textContent = raw;
1059
  }}
 
 
1060
  }}
1061
 
1062
  async function startLogin() {{
1063
+ const res = await fetch('/admin/login/start', {{ method: 'POST' }});
1064
+ const raw = await res.text();
1065
+ let data = null;
1066
+ try {{ data = JSON.parse(raw); }} catch (e) {{}}
1067
  if (!res.ok) {{
1068
+ loginBox.textContent = data ? JSON.stringify(data, null, 2) : raw;
 
1069
  return;
1070
  }}
1071
  currentLoginId = data.loginId;
1072
  localStorage.setItem('loginId', currentLoginId);
1073
+ loginBox.textContent = JSON.stringify(data, null, 2) + "\n\n打开登录链接:\n" + data.loginUrl;
1074
  window.open(data.loginUrl, '_blank');
1075
  }}
1076
 
 
1080
  return;
1081
  }}
1082
  const url = '/admin/login/status?loginId=' + encodeURIComponent(currentLoginId);
1083
+ const res = await fetch(url);
1084
+ const raw = await res.text();
1085
+ try {{
1086
+ const data = JSON.parse(raw);
1087
+ loginBox.textContent = JSON.stringify(data, null, 2);
1088
+ }} catch (e) {{
1089
+ loginBox.textContent = raw;
1090
  }}
1091
  await refreshHealth();
1092
  }}
1093
 
 
 
 
 
1094
  refreshHealth();
1095
  </script>
1096
  </body>
 
1174
  app.on_cleanup.append(on_cleanup)
1175
 
1176
  app.router.add_get("/", handle_root)
 
 
 
1177
  app.router.add_get("/health", handle_health)
1178
  app.router.add_post("/v1/chat/completions", handle_chat_completion)
1179
  app.router.add_post("/v1/responses", handle_responses)