Spaces:
Running
Running
| from html import escape | |
| CSS = """ | |
| :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; background: #0b1020; color: #eef2ff; min-height: 100vh; } | |
| main { width: min(760px, calc(100% - 32px)); margin: 48px auto; } | |
| .card { background: #151b2f; border: 1px solid #303957; border-radius: 18px; padding: 28px; box-shadow: 0 18px 60px #0007; } | |
| h1 { margin: 0 0 8px; font-size: 30px; } | |
| h2 { margin-top: 28px; font-size: 20px; } | |
| p, li { color: #cbd5e1; line-height: 1.6; } | |
| label { display: block; margin: 18px 0 7px; font-weight: 700; } | |
| input, textarea { width: 100%; border: 1px solid #465171; border-radius: 10px; background: #0d1325; color: #fff; padding: 12px 13px; font: inherit; } | |
| textarea { min-height: 120px; resize: vertical; } | |
| button, .button { display: inline-block; margin-top: 20px; border: 0; border-radius: 10px; background: #facc15; color: #151515; font-weight: 800; padding: 12px 18px; cursor: pointer; text-decoration: none; } | |
| button.secondary, .button.secondary { background: #334155; color: #fff; } | |
| button.danger { background: #ef4444; color: #fff; } | |
| .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px; } | |
| .stat { background: #0d1325; border: 1px solid #303957; border-radius: 12px; padding: 14px; } | |
| .stat b { display: block; margin-bottom: 6px; } | |
| code { color: #fde68a; overflow-wrap: anywhere; } | |
| .notice { border-left: 4px solid #facc15; background: #272313; padding: 12px 14px; border-radius: 8px; } | |
| .error { border-left-color: #ef4444; background: #31151b; } | |
| .success { border-left-color: #22c55e; background: #10281d; } | |
| .row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } | |
| .row form { margin: 0; } | |
| .check { display: flex; gap: 10px; align-items: flex-start; font-weight: 500; } | |
| .check input { width: auto; margin-top: 5px; } | |
| small { color: #94a3b8; } | |
| hr { border: 0; border-top: 1px solid #303957; margin: 28px 0; } | |
| """ | |
| def shell(title: str, content: str) -> str: | |
| return f"""<!doctype html> | |
| <html lang="zh-Hant"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>{escape(title)}</title> | |
| <style>{CSS}</style> | |
| </head> | |
| <body><main><section class="card">{content}</section></main></body> | |
| </html>""" | |
| def setup_page(error: str = "") -> str: | |
| error_html = f'<p class="notice error">{escape(error)}</p>' if error else "" | |
| return shell( | |
| "首次設定 — Arena Playwright API", | |
| f""" | |
| <h1>🎭 首次設定</h1> | |
| <p>所有執行設定都會在這個 Space 內完成。本版本使用暫存設定,Space 重啟後需重新設定。</p> | |
| {error_html} | |
| <p class="notice">請先從 Space 的 <b>Logs</b> 找到本次啟動產生的 <code>SETUP CODE</code>。這可避免陌生人搶先設定公開 Space。</p> | |
| <form method="post" action="/setup"> | |
| <label for="bootstrap_code">SETUP CODE</label> | |
| <input id="bootstrap_code" name="bootstrap_code" type="password" required autocomplete="one-time-code"> | |
| <label for="admin_password">管理員密碼</label> | |
| <input id="admin_password" name="admin_password" type="password" minlength="12" autocomplete="new-password" placeholder="若已設定 ADMIN_PASSWORD Secret,可留空"> | |
| <small>至少 12 個字元,用於稍後登入管理頁。</small> | |
| <label for="arena_auth_token">自己的 Arena Cookie(選填後備)</label> | |
| <textarea id="arena_auth_token" name="arena_auth_token" placeholder="若已設定 ARENA_EMAIL+ARENA_PASSWORD Secrets,可留空"></textarea> | |
| <small>可使用 Cookie,或由 Space 內的 Playwright 使用非 Google Email/Password 登入。</small> | |
| <label for="bridge_api_key">API Key(可使用 BRIDGE_API_KEY Secret,或留空自動產生)</label> | |
| <input id="bridge_api_key" name="bridge_api_key" type="password" minlength="20" autocomplete="new-password"> | |
| <label class="check"> | |
| <input name="authorized_use" type="checkbox" value="yes" required> | |
| <span>我確認這是我自己的帳號/獲准工作階段,且已在 Arena 官方網站自行閱讀並處理必要條款與人工驗證。</span> | |
| </label> | |
| <button type="submit">儲存並啟動 Playwright</button> | |
| </form> | |
| """, | |
| ) | |
| def setup_complete_page(api_key: str, base_url: str) -> str: | |
| return shell( | |
| "設定完成 — Arena Playwright API", | |
| f""" | |
| <h1>✅ 設定完成</h1> | |
| <p class="notice success">Playwright 瀏覽器已啟動。請立即保存下面的 API Key。</p> | |
| <label>OpenAI API Base URL</label> | |
| <input readonly value="{escape(base_url.rstrip('/') + '/v1')}"> | |
| <label>API Key</label> | |
| <input readonly value="{escape(api_key)}"> | |
| <p><b>OpenWebUI:</b>URL 使用上面的 <code>/v1</code>,API Key 使用這個值。</p> | |
| <div class="row"> | |
| <a class="button" href="/admin">前往管理頁</a> | |
| <a class="button secondary" href="/docs">API 文件</a> | |
| </div> | |
| """, | |
| ) | |
| def login_page(error: str = "") -> str: | |
| error_html = f'<p class="notice error">{escape(error)}</p>' if error else "" | |
| return shell( | |
| "管理員登入", | |
| f""" | |
| <h1>管理員登入</h1> | |
| {error_html} | |
| <form method="post" action="/login"> | |
| <label for="password">管理員密碼</label> | |
| <input id="password" name="password" type="password" required autocomplete="current-password"> | |
| <button type="submit">登入</button> | |
| </form> | |
| """, | |
| ) | |
| def admin_page( | |
| *, | |
| browser_ready: bool, | |
| api_key: str, | |
| masked_token: str, | |
| message: str = "", | |
| error: bool = False, | |
| ) -> str: | |
| message_html = "" | |
| if message: | |
| kind = "error" if error else "success" | |
| message_html = f'<p class="notice {kind}">{escape(message)}</p>' | |
| status = "已啟動" if browser_ready else "未啟動" | |
| return shell( | |
| "管理 — Arena Playwright API", | |
| f""" | |
| <h1>管理設定</h1> | |
| <p>所有設定都是暫存;Space 重啟後會清除並產生新的 SETUP CODE。</p> | |
| {message_html} | |
| <div class="grid"> | |
| <div class="stat"><b>Playwright</b>{escape(status)}</div> | |
| <div class="stat"><b>Arena Cookie</b><code>{escape(masked_token)}</code></div> | |
| </div> | |
| <h2>目前 API 連線</h2> | |
| <label>API Base URL</label> | |
| <input readonly value="/v1"> | |
| <label>API Key</label> | |
| <input readonly value="{escape(api_key)}"> | |
| <h2>更新設定</h2> | |
| <form method="post" action="/admin/update"> | |
| <label for="arena_auth_token">新的 Arena Cookie</label> | |
| <textarea id="arena_auth_token" name="arena_auth_token" placeholder="留空代表維持目前 Cookie"></textarea> | |
| <label for="bridge_api_key">新的 API Key</label> | |
| <input id="bridge_api_key" name="bridge_api_key" type="password" minlength="20" placeholder="留空代表維持目前 API Key"> | |
| <label for="admin_password">新的管理員密碼</label> | |
| <input id="admin_password" name="admin_password" type="password" minlength="12" placeholder="留空代表維持目前密碼"> | |
| <button type="submit">更新並重新啟動瀏覽器</button> | |
| </form> | |
| <hr> | |
| <div class="row"> | |
| <form method="post" action="/admin/regenerate-key"><button class="secondary" type="submit">重新產生 API Key</button></form> | |
| <form method="post" action="/admin/restart"><button class="secondary" type="submit">立即重新登入/重製 Cookie</button></form> | |
| <form method="post" action="/logout"><button class="danger" type="submit">登出</button></form> | |
| </div> | |
| """, | |
| ) | |