GuXiaopeng commited on
Commit
500ba6f
·
verified ·
1 Parent(s): a008915

Improve Alipay login message and timeout handling

Browse files
Files changed (2) hide show
  1. i18n.py +12 -4
  2. managed_auth.py +163 -30
i18n.py CHANGED
@@ -300,9 +300,11 @@ TEXTS: dict[str, dict[str, str]] = {
300
  "core_literature_parsing": "🧠 正在深度解析用户上传的核心文献……",
301
  "section_complete_progress": "🇬🇧 【{name}】英文完成,翻译任务已进入后台。",
302
  "login_title": "支付宝扫码登录",
303
- "login_info": "每位用户每天免费生成3篇论文。支付宝仅用于账户识别,不显示您的支付宝用户标识。",
304
  "login_creating": "正在创建安全登录请求……",
305
  "login_create_error": "无法创建登录请求:{error}",
 
 
306
  "login_reconnect": "重新连接登录服务",
307
  "login_open": "打开支付宝扫码登录",
308
  "login_caption": "请在新页面使用手机支付宝扫码并确认授权,然后返回本页面检查登录结果。",
@@ -413,9 +415,11 @@ TEXTS: dict[str, dict[str, str]] = {
413
  "core_literature_parsing": "🧠 Parsing the uploaded core literature in depth…",
414
  "section_complete_progress": "🇬🇧 English section 【{name}】 complete; translation is running in the background.",
415
  "login_title": "Sign in with Alipay",
416
- "login_info": "Each user may generate three papers free of charge each day. Alipay is used only to identify the account; your Alipay identifier is not displayed.",
417
  "login_creating": "Creating a secure sign-in request…",
418
  "login_create_error": "Unable to create the sign-in request: {error}",
 
 
419
  "login_reconnect": "Reconnect to Sign-in Service",
420
  "login_open": "Open Alipay QR Sign-in",
421
  "login_caption": "Use Alipay on your phone to scan and authorize in the new page, then return here to check the result.",
@@ -526,9 +530,11 @@ TEXTS: dict[str, dict[str, str]] = {
526
  "core_literature_parsing": "🧠 Выполняется углублённая обработка загруженной основной литературы…",
527
  "section_complete_progress": "🇬🇧 Английский раздел 【{name}】 завершён; перевод выполняется в фоне.",
528
  "login_title": "Вход через Alipay",
529
- "login_info": "Каждый пользователь может бесплатно формировать три статьи в день. Alipay используется только для идентификации учётной записи; идентификатор Alipay не отображается.",
530
  "login_creating": "Создание защищённого запроса на вход…",
531
  "login_create_error": "Не удалось создать запрос на вход: {error}",
 
 
532
  "login_reconnect": "Повторно подключиться к сервису входа",
533
  "login_open": "Открыть вход по QR-коду Alipay",
534
  "login_caption": "Откройте новую страницу, отсканируйте QR-код приложением Alipay и подтвердите авторизацию, затем вернитесь сюда.",
@@ -639,9 +645,11 @@ TEXTS: dict[str, dict[str, str]] = {
639
  "core_literature_parsing": "🧠 Жүктелген негізгі әдебиеттер терең өңделуде…",
640
  "section_complete_progress": "🇬🇧 【{name}】 ағылшын бөлімі аяқталды; аударма фондық режимде орындалуда.",
641
  "login_title": "Alipay арқылы кіру",
642
- "login_info": "Әр пайдаланушы күніне үш мақаланы тегін дайындай алады. Alipay тек есептік жазбаны сәйкестендіру үшін қолданылады; Alipay идентификаторы көрсетілмейді.",
643
  "login_creating": "Қауіпсіз кіру сұрауы құрылуда…",
644
  "login_create_error": "Кіру с��рауын құру мүмкін болмады: {error}",
 
 
645
  "login_reconnect": "Кіру қызметіне қайта қосылу",
646
  "login_open": "Alipay QR-коды арқылы кіруді ашу",
647
  "login_caption": "Жаңа бетте телефондағы Alipay қолданбасымен QR-кодты сканерлеп, рұқсатты растаңыз, содан кейін осы бетке оралыңыз.",
 
300
  "core_literature_parsing": "🧠 正在深度解析用户上传的核心文献……",
301
  "section_complete_progress": "🇬🇧 【{name}】英文完成,翻译任务已进入后台。",
302
  "login_title": "支付宝扫码登录",
303
+ "login_info": "每位小伙伴每日可免费生成 3 篇论文。",
304
  "login_creating": "正在创建安全登录请求……",
305
  "login_create_error": "无法创建登录请求:{error}",
306
+ "login_service_timeout": "登录服务响应较慢,请稍后重试。",
307
+ "login_service_connection_error": "暂时无法连接登录服务,请检查网络后重试。",
308
  "login_reconnect": "重新连接登录服务",
309
  "login_open": "打开支付宝扫码登录",
310
  "login_caption": "请在新页面使用手机支付宝扫码并确认授权,然后返回本页面检查登录结果。",
 
415
  "core_literature_parsing": "🧠 Parsing the uploaded core literature in depth…",
416
  "section_complete_progress": "🇬🇧 English section 【{name}】 complete; translation is running in the background.",
417
  "login_title": "Sign in with Alipay",
418
+ "login_info": "You can generate up to 3 papers for free each day.",
419
  "login_creating": "Creating a secure sign-in request…",
420
  "login_create_error": "Unable to create the sign-in request: {error}",
421
+ "login_service_timeout": "The sign-in service is taking longer than expected. Please try again.",
422
+ "login_service_connection_error": "The sign-in service is temporarily unavailable. Check your connection and try again.",
423
  "login_reconnect": "Reconnect to Sign-in Service",
424
  "login_open": "Open Alipay QR Sign-in",
425
  "login_caption": "Use Alipay on your phone to scan and authorize in the new page, then return here to check the result.",
 
530
  "core_literature_parsing": "🧠 Выполняется углублённая обработка загруженной основной литературы…",
531
  "section_complete_progress": "🇬🇧 Английский раздел 【{name}】 завершён; перевод выполняется в фоне.",
532
  "login_title": "Вход через Alipay",
533
+ "login_info": "Вы можете бесплатно создавать до 3 статей в день.",
534
  "login_creating": "Создание защищённого запроса на вход…",
535
  "login_create_error": "Не удалось создать запрос на вход: {error}",
536
+ "login_service_timeout": "Сервис входа отвечает медленнее обычного. Пожалуйста, попробуйте ещё раз.",
537
+ "login_service_connection_error": "Сервис входа временно недоступен. Проверьте подключение и повторите попытку.",
538
  "login_reconnect": "Повторно подключиться к сервису входа",
539
  "login_open": "Открыть вход по QR-коду Alipay",
540
  "login_caption": "Откройте новую страницу, отсканируйте QR-код приложением Alipay и подтвердите авторизацию, затем вернитесь сюда.",
 
645
  "core_literature_parsing": "🧠 Жүктелген негізгі әдебиеттер терең өңделуде…",
646
  "section_complete_progress": "🇬🇧 【{name}】 ағылшын бөлімі аяқталды; аударма фондық режимде орындалуда.",
647
  "login_title": "Alipay арқылы кіру",
648
+ "login_info": "Күніне 3 мақалаға дейін тегін жасай аласыз.",
649
  "login_creating": "Қауіпсіз кіру сұрауы құрылуда…",
650
  "login_create_error": "Кіру с��рауын құру мүмкін болмады: {error}",
651
+ "login_service_timeout": "Кіру қызметі баяу жауап беріп жатыр. Сәл кейінірек қайталап көріңіз.",
652
+ "login_service_connection_error": "Кіру қызметі уақытша қолжетімсіз. Желіні тексеріп, қайта көріңіз.",
653
  "login_reconnect": "Кіру қызметіне қайта қосылу",
654
  "login_open": "Alipay QR-коды арқылы кіруді ашу",
655
  "login_caption": "Жаңа бетте телефондағы Alipay қолданбасымен QR-кодты сканерлеп, рұқсатты растаңыз, содан кейін осы бетке оралыңыз.",
managed_auth.py CHANGED
@@ -14,8 +14,21 @@ import streamlit as st
14
  from i18n import tr
15
 
16
 
17
- REQUEST_TIMEOUT = (8, 25)
 
 
 
 
18
  SESSION_VERIFY_INTERVAL_SECONDS = 60
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  def _runtime_value(name: str) -> str:
@@ -43,23 +56,75 @@ def _service_key() -> str:
43
 
44
 
45
  def _post(path: str, payload: dict[str, Any]) -> dict[str, Any]:
46
- response = requests.post(
47
- f"{_api_base()}{path}",
48
- headers={
49
- "Authorization": f"Bearer {_service_key()}",
50
- "Content-Type": "application/json",
51
- },
52
- json=payload,
53
- timeout=REQUEST_TIMEOUT,
54
- )
55
- try:
56
- data = response.json()
57
- except ValueError as exc:
58
- raise RuntimeError(f"Authentication service returned non-JSON content (HTTP {response.status_code}).") from exc
59
- if response.status_code >= 400:
60
- message = data.get("message") or data.get("reason") or data.get("error") or f"HTTP {response.status_code}"
61
- raise RuntimeError(str(message))
62
- return data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
 
65
  def _clear_login_request() -> None:
@@ -77,31 +142,47 @@ def _verify_existing_session() -> bool:
77
  session_token = st.session_state.get("sp_session_token")
78
  if not session_token:
79
  return False
 
80
  last_verified = float(st.session_state.get("sp_last_verified_at", 0))
81
  if time.time() - last_verified < SESSION_VERIFY_INTERVAL_SECONDS:
82
  return bool(st.session_state.get("sp_user"))
 
83
  try:
84
  result = _post("/session/verify", {"session_token": session_token})
85
  except Exception:
86
  _clear_session()
87
  return False
 
88
  if not result.get("ok") or not result.get("valid"):
89
  _clear_session()
90
  return False
 
91
  st.session_state["sp_user"] = result.get("user", {})
92
- st.session_state["sp_session_expires_at"] = result.get("session_expires_at")
 
 
93
  st.session_state["sp_last_verified_at"] = time.time()
94
  return True
95
 
96
 
97
  def _render_login_link(url: str, language: str) -> None:
98
  if hasattr(st, "link_button"):
99
- st.link_button(tr(language, "login_open"), url, use_container_width=True)
 
 
 
 
100
  return
 
101
  safe_url = html.escape(url, quote=True)
102
  label = html.escape(tr(language, "login_open"))
103
  st.markdown(
104
- f"""<a href="{safe_url}" target="_blank" style="display:block;text-align:center;padding:0.75rem;border-radius:0.5rem;background:#1677ff;color:white;text-decoration:none;font-weight:600;">{label}</a>""",
 
 
 
 
 
105
  unsafe_allow_html=True,
106
  )
107
 
@@ -109,8 +190,10 @@ def _render_login_link(url: str, language: str) -> None:
109
  def _create_login_request() -> dict[str, Any]:
110
  result = _post("/login/create", {})
111
  required = ("login_request_id", "poll_token", "authorization_url")
 
112
  if not all(result.get(name) for name in required):
113
  raise RuntimeError("Authentication service returned incomplete login data.")
 
114
  request_data = {
115
  "login_request_id": result["login_request_id"],
116
  "poll_token": result["poll_token"],
@@ -121,7 +204,10 @@ def _create_login_request() -> dict[str, Any]:
121
  return request_data
122
 
123
 
124
- def _check_login_status(request_data: dict[str, Any], language: str) -> None:
 
 
 
125
  result = _post(
126
  "/login/status",
127
  {
@@ -129,31 +215,45 @@ def _check_login_status(request_data: dict[str, Any], language: str) -> None:
129
  "poll_token": request_data["poll_token"],
130
  },
131
  )
 
132
  status = result.get("status")
133
  if status == "pending":
134
  st.info(tr(language, "login_pending"))
135
  return
 
136
  if status != "authenticated":
137
- raise RuntimeError(result.get("message") or result.get("reason") or f"Unknown login status: {status}")
 
 
 
 
 
138
  session_token = result.get("session_token")
139
  user = result.get("user")
 
140
  if not session_token or not user:
141
  raise RuntimeError("Login succeeded but the session data is incomplete.")
 
142
  st.session_state["sp_session_token"] = session_token
143
- st.session_state["sp_session_expires_at"] = result.get("session_expires_at")
 
 
144
  st.session_state["sp_user"] = user
145
  st.session_state["sp_last_verified_at"] = time.time()
 
146
  _clear_login_request()
147
  st.rerun()
148
 
149
 
150
  def logout_alipay_session() -> None:
151
  session_token = st.session_state.get("sp_session_token")
 
152
  if session_token:
153
  try:
154
  _post("/logout", {"session_token": session_token})
155
  except Exception:
156
  pass
 
157
  _clear_session()
158
  _clear_login_request()
159
  st.rerun()
@@ -164,12 +264,18 @@ def render_logged_in_account(language: str) -> None:
164
  user_id = user.get("id", "—")
165
  free_limit = user.get("daily_free_limit", 3)
166
  bonus_papers = user.get("bonus_papers", 0)
 
167
  with st.sidebar:
168
  st.markdown(f"### {tr(language, 'account_title')}")
169
  st.write(tr(language, "account_id", value=user_id))
170
  st.write(tr(language, "daily_free", value=free_limit))
171
  st.write(tr(language, "bonus_papers", value=bonus_papers))
172
- if st.button(tr(language, "logout"), key="sp_logout_button", use_container_width=True):
 
 
 
 
 
173
  logout_alipay_session()
174
 
175
 
@@ -184,27 +290,54 @@ def require_alipay_login(language: str = "zh") -> bool:
184
  unsafe_allow_html=True,
185
  )
186
  st.info(tr(language, "login_info"))
 
187
  request_data = st.session_state.get("sp_login_request")
188
  if not request_data:
189
  try:
190
  with st.spinner(tr(language, "login_creating")):
191
  request_data = _create_login_request()
192
  except Exception as exc:
193
- st.error(tr(language, "login_create_error", error=exc))
194
- if st.button(tr(language, "login_reconnect"), use_container_width=True):
 
 
 
 
 
 
 
 
 
195
  st.rerun()
196
  st.stop()
197
 
198
  _render_login_link(request_data["authorization_url"], language)
199
  st.caption(tr(language, "login_caption"))
 
200
  left, right = st.columns(2)
201
- if left.button(tr(language, "login_check"), type="primary", use_container_width=True):
 
 
 
 
 
202
  try:
203
  with st.spinner(tr(language, "login_checking")):
204
  _check_login_status(request_data, language)
205
  except Exception as exc:
206
- st.error(tr(language, "login_check_error", error=exc))
207
- if right.button(tr(language, "login_regenerate"), use_container_width=True):
 
 
 
 
 
 
 
 
 
 
208
  _clear_login_request()
209
  st.rerun()
 
210
  st.stop()
 
14
  from i18n import tr
15
 
16
 
17
+ # Connection timeout: 10 seconds; read timeout: 30 seconds.
18
+ REQUEST_TIMEOUT = (10, 30)
19
+ # One initial request plus one automatic retry for temporary network failures.
20
+ REQUEST_ATTEMPTS = 2
21
+ RETRY_DELAY_SECONDS = 1.5
22
  SESSION_VERIFY_INTERVAL_SECONDS = 60
23
+ TRANSIENT_HTTP_STATUS = {502, 503, 504}
24
+
25
+
26
+ class AuthServiceTimeoutError(RuntimeError):
27
+ """The authentication service did not respond within the allowed time."""
28
+
29
+
30
+ class AuthServiceConnectionError(RuntimeError):
31
+ """The authentication service could not be reached."""
32
 
33
 
34
  def _runtime_value(name: str) -> str:
 
56
 
57
 
58
  def _post(path: str, payload: dict[str, Any]) -> dict[str, Any]:
59
+ """Call a protected Cloudflare endpoint with one transient-failure retry."""
60
+ url = f"{_api_base()}{path}"
61
+ headers = {
62
+ "Authorization": f"Bearer {_service_key()}",
63
+ "Content-Type": "application/json",
64
+ }
65
+
66
+ last_error: Exception | None = None
67
+
68
+ for attempt in range(REQUEST_ATTEMPTS):
69
+ try:
70
+ response = requests.post(
71
+ url,
72
+ headers=headers,
73
+ json=payload,
74
+ timeout=REQUEST_TIMEOUT,
75
+ )
76
+
77
+ if (
78
+ response.status_code in TRANSIENT_HTTP_STATUS
79
+ and attempt < REQUEST_ATTEMPTS - 1
80
+ ):
81
+ time.sleep(RETRY_DELAY_SECONDS)
82
+ continue
83
+
84
+ try:
85
+ data = response.json()
86
+ except ValueError as exc:
87
+ raise RuntimeError(
88
+ "Authentication service returned non-JSON content "
89
+ f"(HTTP {response.status_code})."
90
+ ) from exc
91
+
92
+ if response.status_code >= 400:
93
+ message = (
94
+ data.get("message")
95
+ or data.get("reason")
96
+ or data.get("error")
97
+ or f"HTTP {response.status_code}"
98
+ )
99
+ raise RuntimeError(str(message))
100
+
101
+ return data
102
+
103
+ except requests.exceptions.Timeout as exc:
104
+ last_error = exc
105
+ if attempt < REQUEST_ATTEMPTS - 1:
106
+ time.sleep(RETRY_DELAY_SECONDS)
107
+ continue
108
+ raise AuthServiceTimeoutError("authentication_service_timeout") from exc
109
+
110
+ except requests.exceptions.ConnectionError as exc:
111
+ last_error = exc
112
+ if attempt < REQUEST_ATTEMPTS - 1:
113
+ time.sleep(RETRY_DELAY_SECONDS)
114
+ continue
115
+ raise AuthServiceConnectionError(
116
+ "authentication_service_connection_error"
117
+ ) from exc
118
+
119
+ raise RuntimeError(str(last_error or "Unknown authentication service error."))
120
+
121
+
122
+ def _friendly_error(language: str, error: Exception) -> str:
123
+ if isinstance(error, AuthServiceTimeoutError):
124
+ return tr(language, "login_service_timeout")
125
+ if isinstance(error, AuthServiceConnectionError):
126
+ return tr(language, "login_service_connection_error")
127
+ return str(error)
128
 
129
 
130
  def _clear_login_request() -> None:
 
142
  session_token = st.session_state.get("sp_session_token")
143
  if not session_token:
144
  return False
145
+
146
  last_verified = float(st.session_state.get("sp_last_verified_at", 0))
147
  if time.time() - last_verified < SESSION_VERIFY_INTERVAL_SECONDS:
148
  return bool(st.session_state.get("sp_user"))
149
+
150
  try:
151
  result = _post("/session/verify", {"session_token": session_token})
152
  except Exception:
153
  _clear_session()
154
  return False
155
+
156
  if not result.get("ok") or not result.get("valid"):
157
  _clear_session()
158
  return False
159
+
160
  st.session_state["sp_user"] = result.get("user", {})
161
+ st.session_state["sp_session_expires_at"] = result.get(
162
+ "session_expires_at"
163
+ )
164
  st.session_state["sp_last_verified_at"] = time.time()
165
  return True
166
 
167
 
168
  def _render_login_link(url: str, language: str) -> None:
169
  if hasattr(st, "link_button"):
170
+ st.link_button(
171
+ tr(language, "login_open"),
172
+ url,
173
+ use_container_width=True,
174
+ )
175
  return
176
+
177
  safe_url = html.escape(url, quote=True)
178
  label = html.escape(tr(language, "login_open"))
179
  st.markdown(
180
+ (
181
+ f'<a href="{safe_url}" target="_blank" rel="noopener noreferrer" '
182
+ 'style="display:block;text-align:center;padding:0.75rem;'
183
+ 'border-radius:0.5rem;background:#1677ff;color:white;'
184
+ f'text-decoration:none;font-weight:600;">{label}</a>'
185
+ ),
186
  unsafe_allow_html=True,
187
  )
188
 
 
190
  def _create_login_request() -> dict[str, Any]:
191
  result = _post("/login/create", {})
192
  required = ("login_request_id", "poll_token", "authorization_url")
193
+
194
  if not all(result.get(name) for name in required):
195
  raise RuntimeError("Authentication service returned incomplete login data.")
196
+
197
  request_data = {
198
  "login_request_id": result["login_request_id"],
199
  "poll_token": result["poll_token"],
 
204
  return request_data
205
 
206
 
207
+ def _check_login_status(
208
+ request_data: dict[str, Any],
209
+ language: str,
210
+ ) -> None:
211
  result = _post(
212
  "/login/status",
213
  {
 
215
  "poll_token": request_data["poll_token"],
216
  },
217
  )
218
+
219
  status = result.get("status")
220
  if status == "pending":
221
  st.info(tr(language, "login_pending"))
222
  return
223
+
224
  if status != "authenticated":
225
+ raise RuntimeError(
226
+ result.get("message")
227
+ or result.get("reason")
228
+ or f"Unknown login status: {status}"
229
+ )
230
+
231
  session_token = result.get("session_token")
232
  user = result.get("user")
233
+
234
  if not session_token or not user:
235
  raise RuntimeError("Login succeeded but the session data is incomplete.")
236
+
237
  st.session_state["sp_session_token"] = session_token
238
+ st.session_state["sp_session_expires_at"] = result.get(
239
+ "session_expires_at"
240
+ )
241
  st.session_state["sp_user"] = user
242
  st.session_state["sp_last_verified_at"] = time.time()
243
+
244
  _clear_login_request()
245
  st.rerun()
246
 
247
 
248
  def logout_alipay_session() -> None:
249
  session_token = st.session_state.get("sp_session_token")
250
+
251
  if session_token:
252
  try:
253
  _post("/logout", {"session_token": session_token})
254
  except Exception:
255
  pass
256
+
257
  _clear_session()
258
  _clear_login_request()
259
  st.rerun()
 
264
  user_id = user.get("id", "—")
265
  free_limit = user.get("daily_free_limit", 3)
266
  bonus_papers = user.get("bonus_papers", 0)
267
+
268
  with st.sidebar:
269
  st.markdown(f"### {tr(language, 'account_title')}")
270
  st.write(tr(language, "account_id", value=user_id))
271
  st.write(tr(language, "daily_free", value=free_limit))
272
  st.write(tr(language, "bonus_papers", value=bonus_papers))
273
+
274
+ if st.button(
275
+ tr(language, "logout"),
276
+ key="sp_logout_button",
277
+ use_container_width=True,
278
+ ):
279
  logout_alipay_session()
280
 
281
 
 
290
  unsafe_allow_html=True,
291
  )
292
  st.info(tr(language, "login_info"))
293
+
294
  request_data = st.session_state.get("sp_login_request")
295
  if not request_data:
296
  try:
297
  with st.spinner(tr(language, "login_creating")):
298
  request_data = _create_login_request()
299
  except Exception as exc:
300
+ st.error(
301
+ tr(
302
+ language,
303
+ "login_create_error",
304
+ error=_friendly_error(language, exc),
305
+ )
306
+ )
307
+ if st.button(
308
+ tr(language, "login_reconnect"),
309
+ use_container_width=True,
310
+ ):
311
  st.rerun()
312
  st.stop()
313
 
314
  _render_login_link(request_data["authorization_url"], language)
315
  st.caption(tr(language, "login_caption"))
316
+
317
  left, right = st.columns(2)
318
+
319
+ if left.button(
320
+ tr(language, "login_check"),
321
+ type="primary",
322
+ use_container_width=True,
323
+ ):
324
  try:
325
  with st.spinner(tr(language, "login_checking")):
326
  _check_login_status(request_data, language)
327
  except Exception as exc:
328
+ st.error(
329
+ tr(
330
+ language,
331
+ "login_check_error",
332
+ error=_friendly_error(language, exc),
333
+ )
334
+ )
335
+
336
+ if right.button(
337
+ tr(language, "login_regenerate"),
338
+ use_container_width=True,
339
+ ):
340
  _clear_login_request()
341
  st.rerun()
342
+
343
  st.stop()