Spaces:
Running
Running
P0-R5-R2 no historical completed UI
#139
by GuXiaopeng - opened
- .gitattributes +1 -0
- app.py +24 -10
- i18n.py +78 -54
- managed_auth.py +118 -25
- static/payment_wechat_qr.png +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
static/payment_wechat_qr.png filter=lfs diff=lfs merge=lfs -text
|
app.py
CHANGED
|
@@ -821,11 +821,9 @@ def _process_pending_launch(lang_code: str) -> None:
|
|
| 821 |
if "product_entitlement_required" in raw:
|
| 822 |
st.session_state["sp_payment_required"] = True
|
| 823 |
st.error(tr(lang_code, "product_entitlement_required"))
|
| 824 |
-
render_payment_purchase(lang_code)
|
| 825 |
elif "payment_required" in raw:
|
| 826 |
st.session_state["sp_payment_required"] = True
|
| 827 |
st.error(tr(lang_code, "quota_exhausted"))
|
| 828 |
-
render_payment_purchase(lang_code)
|
| 829 |
elif "a5_product_task_path_not_active" in raw or "a5_product_task_path_not_authorized" in raw:
|
| 830 |
st.error(tr(lang_code, "product_generation_not_active"))
|
| 831 |
else:
|
|
@@ -1289,10 +1287,6 @@ def _render_task_status_once(lang_code: str) -> None:
|
|
| 1289 |
return
|
| 1290 |
status = str(task.get("status") or "")
|
| 1291 |
|
| 1292 |
-
# First-paint invariant: render useful, localized progress and manuscript
|
| 1293 |
-
# placeholders before any browser bridge call that may yield once via
|
| 1294 |
-
# st.stop(). The browser watcher mutates these stable DOM targets in place
|
| 1295 |
-
# after the bridge handshake, so no duplicate monitor/preview is needed.
|
| 1296 |
st.session_state["sp_last_ui_task_status"] = status
|
| 1297 |
_render_monitor_box(task, lang_code)
|
| 1298 |
_render_checkpoint_preview(task, lang_code)
|
|
@@ -1590,7 +1584,9 @@ def render_workspace_controls(user):
|
|
| 1590 |
st.markdown(tr(lang_code, "template_help_undetermined"))
|
| 1591 |
|
| 1592 |
product_ui = get_product_ui_projection()
|
| 1593 |
-
|
|
|
|
|
|
|
| 1594 |
products = product_ui.get("products") or {}
|
| 1595 |
tier_options = [tier for tier in ("STANDARD", "FLAGSHIP") if tier in products]
|
| 1596 |
if len(tier_options) != 2:
|
|
@@ -1610,6 +1606,10 @@ def render_workspace_controls(user):
|
|
| 1610 |
key="sp_product_tier",
|
| 1611 |
)
|
| 1612 |
selected_product = products[selected_tier]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1613 |
word_min = int(selected_product["min_body_words"])
|
| 1614 |
word_max = int(selected_product["max_body_words"])
|
| 1615 |
word_label = tr(
|
|
@@ -1628,6 +1628,7 @@ def render_workspace_controls(user):
|
|
| 1628 |
min_words=word_min,
|
| 1629 |
max_words=word_max,
|
| 1630 |
)
|
|
|
|
| 1631 |
|
| 1632 |
current_words = int(st.session_state.get("sp_words") or APP_CONFIG["target_words_default"])
|
| 1633 |
bounded_words = min(word_max, max(word_min, current_words))
|
|
@@ -1649,7 +1650,17 @@ def render_workspace_controls(user):
|
|
| 1649 |
st.session_state["sp_pending_cancel_task_id"] = str(task.get("task_id") or "")
|
| 1650 |
st.rerun()
|
| 1651 |
else:
|
| 1652 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1653 |
if st.button(
|
| 1654 |
tr(lang_code, "start_generation_unified"),
|
| 1655 |
disabled=not can_start,
|
|
@@ -1696,13 +1707,16 @@ _process_pending_resume()
|
|
| 1696 |
_process_pending_launch(lang_code)
|
| 1697 |
|
| 1698 |
if st.session_state.get("sp_payment_required"):
|
| 1699 |
-
|
|
|
|
|
|
|
| 1700 |
|
| 1701 |
# R3.2.4 keeps the approved task-monitor location. Active progress is updated
|
| 1702 |
# inside the browser by read-only edge polling; Streamlit reruns only on a
|
| 1703 |
# user action or a terminal/resumable boundary, so there is no periodic flicker.
|
| 1704 |
_task_for_monitor = st.session_state.get("sp_generation_task") or {}
|
| 1705 |
-
|
|
|
|
| 1706 |
st.markdown("<div id='sp-task-monitor-anchor'></div>", unsafe_allow_html=True)
|
| 1707 |
# Install the one-time navigation script before task-status bridge calls.
|
| 1708 |
# If the first bridge handshake yields via st.stop(), the browser still
|
|
|
|
| 821 |
if "product_entitlement_required" in raw:
|
| 822 |
st.session_state["sp_payment_required"] = True
|
| 823 |
st.error(tr(lang_code, "product_entitlement_required"))
|
|
|
|
| 824 |
elif "payment_required" in raw:
|
| 825 |
st.session_state["sp_payment_required"] = True
|
| 826 |
st.error(tr(lang_code, "quota_exhausted"))
|
|
|
|
| 827 |
elif "a5_product_task_path_not_active" in raw or "a5_product_task_path_not_authorized" in raw:
|
| 828 |
st.error(tr(lang_code, "product_generation_not_active"))
|
| 829 |
else:
|
|
|
|
| 1287 |
return
|
| 1288 |
status = str(task.get("status") or "")
|
| 1289 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1290 |
st.session_state["sp_last_ui_task_status"] = status
|
| 1291 |
_render_monitor_box(task, lang_code)
|
| 1292 |
_render_checkpoint_preview(task, lang_code)
|
|
|
|
| 1584 |
st.markdown(tr(lang_code, "template_help_undetermined"))
|
| 1585 |
|
| 1586 |
product_ui = get_product_ui_projection()
|
| 1587 |
+
product_ui_ready = product_ui.get("mode") == "a5_product"
|
| 1588 |
+
selected_remaining: int | None = None
|
| 1589 |
+
if product_ui_ready:
|
| 1590 |
products = product_ui.get("products") or {}
|
| 1591 |
tier_options = [tier for tier in ("STANDARD", "FLAGSHIP") if tier in products]
|
| 1592 |
if len(tier_options) != 2:
|
|
|
|
| 1606 |
key="sp_product_tier",
|
| 1607 |
)
|
| 1608 |
selected_product = products[selected_tier]
|
| 1609 |
+
entitlement = product_ui.get("entitlement") or {}
|
| 1610 |
+
selected_remaining = int(
|
| 1611 |
+
entitlement.get("standard_remaining" if selected_tier == "STANDARD" else "flagship_remaining") or 0
|
| 1612 |
+
)
|
| 1613 |
word_min = int(selected_product["min_body_words"])
|
| 1614 |
word_max = int(selected_product["max_body_words"])
|
| 1615 |
word_label = tr(
|
|
|
|
| 1628 |
min_words=word_min,
|
| 1629 |
max_words=word_max,
|
| 1630 |
)
|
| 1631 |
+
st.info(tr(lang_code, "product_data_loading"))
|
| 1632 |
|
| 1633 |
current_words = int(st.session_state.get("sp_words") or APP_CONFIG["target_words_default"])
|
| 1634 |
bounded_words = min(word_max, max(word_min, current_words))
|
|
|
|
| 1650 |
st.session_state["sp_pending_cancel_task_id"] = str(task.get("task_id") or "")
|
| 1651 |
st.rerun()
|
| 1652 |
else:
|
| 1653 |
+
entitlement_exhausted = bool(product_ui_ready and selected_remaining is not None and selected_remaining <= 0)
|
| 1654 |
+
if entitlement_exhausted:
|
| 1655 |
+
st.warning(tr(lang_code, "product_entitlement_required"))
|
| 1656 |
+
if st.button(tr(lang_code, "payment_open"), key="btn_open_payment_for_selected_tier"):
|
| 1657 |
+
render_payment_purchase(lang_code)
|
| 1658 |
+
can_start = bool(
|
| 1659 |
+
preflight.get("can_start_generation")
|
| 1660 |
+
and product_ui_ready
|
| 1661 |
+
and selected_remaining is not None
|
| 1662 |
+
and selected_remaining > 0
|
| 1663 |
+
)
|
| 1664 |
if st.button(
|
| 1665 |
tr(lang_code, "start_generation_unified"),
|
| 1666 |
disabled=not can_start,
|
|
|
|
| 1707 |
_process_pending_launch(lang_code)
|
| 1708 |
|
| 1709 |
if st.session_state.get("sp_payment_required"):
|
| 1710 |
+
st.warning(tr(lang_code, "product_entitlement_required"))
|
| 1711 |
+
if st.button(tr(lang_code, "payment_open"), key="btn_open_payment_after_rejection"):
|
| 1712 |
+
render_payment_purchase(lang_code)
|
| 1713 |
|
| 1714 |
# R3.2.4 keeps the approved task-monitor location. Active progress is updated
|
| 1715 |
# inside the browser by read-only edge polling; Streamlit reruns only on a
|
| 1716 |
# user action or a terminal/resumable boundary, so there is no periodic flicker.
|
| 1717 |
_task_for_monitor = st.session_state.get("sp_generation_task") or {}
|
| 1718 |
+
_task_for_monitor_id = str(_task_for_monitor.get("task_id") or "").strip()
|
| 1719 |
+
if _task_for_monitor_id:
|
| 1720 |
st.markdown("<div id='sp-task-monitor-anchor'></div>", unsafe_allow_html=True)
|
| 1721 |
# Install the one-time navigation script before task-status bridge calls.
|
| 1722 |
# If the first bridge handshake yields via st.stop(), the browser still
|
i18n.py
CHANGED
|
@@ -411,30 +411,36 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 411 |
"login_pending": "支付宝授权尚未完成,请扫码并确认授权。",
|
| 412 |
"account_title": "用户账户",
|
| 413 |
"account_id": "用户编号:**{value}**",
|
| 414 |
-
"daily_free": "
|
| 415 |
-
"bonus_papers": "
|
| 416 |
"account_chip_user": "用户 {value}",
|
| 417 |
-
"account_chip_free": "
|
| 418 |
-
"account_chip_bonus": "
|
| 419 |
"account_chip_standard": "标准版 {value}篇",
|
| 420 |
"account_chip_flagship": "旗舰版 {value}篇",
|
| 421 |
"logout_short": "退出",
|
| 422 |
-
"payment_dialog_title": "
|
| 423 |
-
"payment_notice": "
|
| 424 |
-
"product_entitlement_required": "当前所选版本额度
|
| 425 |
-
"
|
| 426 |
-
"
|
| 427 |
-
"
|
| 428 |
-
"
|
| 429 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
"payment_scan": "请使用支付宝 App 扫码支付。支付成功后点击“查询支付结果”。",
|
| 431 |
-
"payment_scan_manual": "请使用
|
| 432 |
"payment_order_no": "订单号:{order_no}",
|
| 433 |
"payment_support_prompt": "支付遇到问题或者提升论文质量?请联系管理员",
|
| 434 |
"payment_support_wechat": "微信:{value}",
|
| 435 |
"payment_support_whatsapp": "WhatsApp:{value}",
|
| 436 |
"payment_check": "查询支付结果",
|
| 437 |
-
"payment_paid": "
|
| 438 |
"payment_expired": "付款二维码已过期,请重新生成。",
|
| 439 |
"browse_files": "选择文件",
|
| 440 |
"drop_docs": "将文件拖放到此处",
|
|
@@ -652,30 +658,36 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 652 |
"login_pending": "Alipay authorization is still pending. Scan the code and confirm authorization.",
|
| 653 |
"account_title": "User Account",
|
| 654 |
"account_id": "User ID: **{value}**",
|
| 655 |
-
"daily_free": "
|
| 656 |
-
"bonus_papers": "
|
| 657 |
"account_chip_user": "User {value}",
|
| 658 |
-
"account_chip_free": "
|
| 659 |
-
"account_chip_bonus": "
|
| 660 |
"account_chip_standard": "Standard {value}",
|
| 661 |
"account_chip_flagship": "Flagship {value}",
|
| 662 |
"logout_short": "Sign out",
|
| 663 |
-
"payment_dialog_title": "
|
| 664 |
-
"payment_notice": "
|
| 665 |
-
"product_entitlement_required": "The selected plan has
|
| 666 |
-
"
|
| 667 |
-
"
|
| 668 |
-
"
|
| 669 |
-
"
|
| 670 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
"payment_scan": "Scan the code with the Alipay app. After payment, select “Check payment result.”",
|
| 672 |
-
"payment_scan_manual": "Scan with the
|
| 673 |
"payment_order_no": "Order number: {order_no}",
|
| 674 |
"payment_support_prompt": "Need help with payment or improving your paper's quality? Contact the administrator.",
|
| 675 |
"payment_support_wechat": "WeChat: {value}",
|
| 676 |
"payment_support_whatsapp": "WhatsApp: {value}",
|
| 677 |
"payment_check": "Check payment result",
|
| 678 |
-
"payment_paid": "Payment
|
| 679 |
"payment_expired": "The payment QR code has expired. Create a new one.",
|
| 680 |
"browse_files": "Choose files",
|
| 681 |
"drop_docs": "Drag and drop files here",
|
|
@@ -893,30 +905,36 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 893 |
"login_pending": "Авторизация Alipay ещё не завершена. Отсканируйте код и подтвердите доступ.",
|
| 894 |
"account_title": "Учётная запись",
|
| 895 |
"account_id": "Идентификатор пользователя: **{value}**",
|
| 896 |
-
"daily_free": "
|
| 897 |
-
"bonus_papers": "
|
| 898 |
"account_chip_user": "Пользователь {value}",
|
| 899 |
-
"account_chip_free": "
|
| 900 |
-
"account_chip_bonus": "
|
| 901 |
"account_chip_standard": "Стандарт {value}",
|
| 902 |
"account_chip_flagship": "Флагман {value}",
|
| 903 |
"logout_short": "Выйти",
|
| 904 |
-
"payment_dialog_title": "
|
| 905 |
-
"payment_notice": "
|
| 906 |
-
"product_entitlement_required": "
|
| 907 |
-
"
|
| 908 |
-
"
|
| 909 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 910 |
"payment_create": "Создать QR-код для оплаты",
|
| 911 |
-
"payment_creating": "Создаётся заказ
|
| 912 |
"payment_scan": "Отсканируйте QR-код в приложении Alipay. После оплаты нажмите «Проверить результат оплаты».",
|
| 913 |
-
"payment_scan_manual": "
|
| 914 |
"payment_order_no": "Номер заказа: {order_no}",
|
| 915 |
"payment_support_prompt": "Возникли вопросы с оплатой или хотите повысить качество научной статьи? Свяжитесь с администратором.",
|
| 916 |
"payment_support_wechat": "WeChat: {value}",
|
| 917 |
"payment_support_whatsapp": "WhatsApp: {value}",
|
| 918 |
"payment_check": "Проверить результат оплаты",
|
| 919 |
-
"payment_paid": "
|
| 920 |
"payment_expired": "Срок действия QR-кода истёк. Создайте новый код.",
|
| 921 |
"browse_files": "Выбрать файлы",
|
| 922 |
"drop_docs": "Перетащите файлы сюда",
|
|
@@ -1134,30 +1152,36 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 1134 |
"login_pending": "Alipay рұқсаты әлі аяқталған жоқ. QR-кодты сканерлеп, рұқсатты растаңыз.",
|
| 1135 |
"account_title": "Пайдаланушы есептік жазбасы",
|
| 1136 |
"account_id": "Пайдаланушы нөмірі: **{value}**",
|
| 1137 |
-
"daily_free": "
|
| 1138 |
-
"bonus_papers": "
|
| 1139 |
"account_chip_user": "Пайдаланушы {value}",
|
| 1140 |
-
"account_chip_free": "
|
| 1141 |
-
"account_chip_bonus": "
|
| 1142 |
"account_chip_standard": "Стандарт {value}",
|
| 1143 |
"account_chip_flagship": "Флагман {value}",
|
| 1144 |
"logout_short": "Шығу",
|
| 1145 |
-
"payment_dialog_title": "
|
| 1146 |
-
"payment_notice": "
|
| 1147 |
-
"product_entitlement_required": "Таңдалған нұсқаның
|
| 1148 |
-
"
|
| 1149 |
-
"
|
| 1150 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1151 |
"payment_create": "Төлемге арналған QR-код жасау",
|
| 1152 |
-
"payment_creating": "
|
| 1153 |
"payment_scan": "QR-кодты Alipay қолданбасымен сканерлеңіз. Төлемнен кейін «Төлем нәтижесін тексеру» түймесін басыңыз.",
|
| 1154 |
-
"payment_scan_manual": "
|
| 1155 |
"payment_order_no": "Тапсырыс нөмірі: {order_no}",
|
| 1156 |
"payment_support_prompt": "Төлем бойынша мәселе туындады ма немесе ғылыми мақаланың сапасын жақсартқыңыз келе ме? Әкімшіге хабарласыңыз.",
|
| 1157 |
"payment_support_wechat": "WeChat: {value}",
|
| 1158 |
"payment_support_whatsapp": "WhatsApp: {value}",
|
| 1159 |
"payment_check": "Төлем нәтижесін тексеру",
|
| 1160 |
-
"payment_paid": "Төлем с
|
| 1161 |
"payment_expired": "QR-кодтың қолданылу мерзімі аяқталды. Жаңа код жасаңыз.",
|
| 1162 |
"browse_files": "Файлдарды таңдау",
|
| 1163 |
"drop_docs": "Файлдарды осы жерге сүйреп әкеліңіз",
|
|
|
|
| 411 |
"login_pending": "支付宝授权尚未完成,请扫码并确认授权。",
|
| 412 |
"account_title": "用户账户",
|
| 413 |
"account_id": "用户编号:**{value}**",
|
| 414 |
+
"daily_free": "标准版:**{value}篇**",
|
| 415 |
+
"bonus_papers": "旗舰版:**{value}篇**",
|
| 416 |
"account_chip_user": "用户 {value}",
|
| 417 |
+
"account_chip_free": "标准版 {value}篇",
|
| 418 |
+
"account_chip_bonus": "旗舰版 {value}篇",
|
| 419 |
"account_chip_standard": "标准版 {value}篇",
|
| 420 |
"account_chip_flagship": "旗舰版 {value}篇",
|
| 421 |
"logout_short": "退出",
|
| 422 |
+
"payment_dialog_title": "购买论文额度",
|
| 423 |
+
"payment_notice": "当前版本额度不足,请购买相应版本的论文额度。",
|
| 424 |
+
"product_entitlement_required": "当前所选版本额度为0,暂时不能生成论文。购买并到账后即可继续生成该版本论文。",
|
| 425 |
+
"product_data_loading": "正在加载标准版 / 旗舰版额度与产品策略,请稍候。加载完成前暂不能生成论文。",
|
| 426 |
+
"payment_open": "购买额度",
|
| 427 |
+
"payment_channel_label": "本次付款方式",
|
| 428 |
+
"payment_channel_alipay": "Alipay",
|
| 429 |
+
"payment_channel_wechat": "WeChat",
|
| 430 |
+
"payment_scan_dual": "请使用与本次付款方式一致的二维码(当前选择:{channel})。付款后将订单号发送给管理员,核实到账后会增加对应版本额度。",
|
| 431 |
+
"payment_notice_product": "购买{tier}:支付 {amount} {currency} 可增加{credits}篇该版本论文额度。",
|
| 432 |
+
"payment_paid_product": "付款已确认,已增加{credits}篇{tier}论文额度。现在可以继续生成论文。",
|
| 433 |
+
"quota_exhausted": "当前所选版本额度不足,请购买相应版本额度。",
|
| 434 |
+
"payment_create": "创建付款订单",
|
| 435 |
+
"payment_creating": "正在创建付款订单……",
|
| 436 |
"payment_scan": "请使用支付宝 App 扫码支付。支付成功后点击“查询支付结果”。",
|
| 437 |
+
"payment_scan_manual": "请使用所选付款方式扫码。付款后请将订单号发送给管理员,管理员核实实际到账后会增加对应版本额度。",
|
| 438 |
"payment_order_no": "订单号:{order_no}",
|
| 439 |
"payment_support_prompt": "支付遇到问题或者提升论文质量?请联系管理员",
|
| 440 |
"payment_support_wechat": "微信:{value}",
|
| 441 |
"payment_support_whatsapp": "WhatsApp:{value}",
|
| 442 |
"payment_check": "查询支付结果",
|
| 443 |
+
"payment_paid": "付款已确认,额度已更新。现在可以继续生成论文。",
|
| 444 |
"payment_expired": "付款二维码已过期,请重新生成。",
|
| 445 |
"browse_files": "选择文件",
|
| 446 |
"drop_docs": "将文件拖放到此处",
|
|
|
|
| 658 |
"login_pending": "Alipay authorization is still pending. Scan the code and confirm authorization.",
|
| 659 |
"account_title": "User Account",
|
| 660 |
"account_id": "User ID: **{value}**",
|
| 661 |
+
"daily_free": "Standard: **{value} papers**",
|
| 662 |
+
"bonus_papers": "Flagship: **{value} papers**",
|
| 663 |
"account_chip_user": "User {value}",
|
| 664 |
+
"account_chip_free": "Standard {value}",
|
| 665 |
+
"account_chip_bonus": "Flagship {value}",
|
| 666 |
"account_chip_standard": "Standard {value}",
|
| 667 |
"account_chip_flagship": "Flagship {value}",
|
| 668 |
"logout_short": "Sign out",
|
| 669 |
+
"payment_dialog_title": "Purchase Paper Credits",
|
| 670 |
+
"payment_notice": "The selected plan has no remaining credits. Purchase credits for that plan to continue.",
|
| 671 |
+
"product_entitlement_required": "The selected plan has 0 remaining credits, so generation is disabled. Purchase credits for this plan and wait for confirmation before generating again.",
|
| 672 |
+
"product_data_loading": "Loading Standard / Flagship credits and product policy. Generation stays disabled until authoritative data is ready.",
|
| 673 |
+
"payment_open": "Purchase credits",
|
| 674 |
+
"payment_channel_label": "Payment method for this order",
|
| 675 |
+
"payment_channel_alipay": "Alipay",
|
| 676 |
+
"payment_channel_wechat": "WeChat",
|
| 677 |
+
"payment_scan_dual": "Use the QR code that matches the selected payment method (current: {channel}). After payment, send the order number to the administrator; the matching plan credits will be added after receipt is verified.",
|
| 678 |
+
"payment_notice_product": "Purchase {tier}: pay {currency} {amount} to add {credits} paper credits for this plan.",
|
| 679 |
+
"payment_paid_product": "Payment was confirmed. {credits} {tier} paper credits were added. You can generate again now.",
|
| 680 |
+
"quota_exhausted": "The selected plan has no remaining credits. Purchase credits for that plan to continue.",
|
| 681 |
+
"payment_create": "Create payment order",
|
| 682 |
+
"payment_creating": "Creating the payment order…",
|
| 683 |
"payment_scan": "Scan the code with the Alipay app. After payment, select “Check payment result.”",
|
| 684 |
+
"payment_scan_manual": "Scan with the selected payment method. After payment, send the order number to the administrator. Credits will be added after the administrator verifies the actual receipt.",
|
| 685 |
"payment_order_no": "Order number: {order_no}",
|
| 686 |
"payment_support_prompt": "Need help with payment or improving your paper's quality? Contact the administrator.",
|
| 687 |
"payment_support_wechat": "WeChat: {value}",
|
| 688 |
"payment_support_whatsapp": "WhatsApp: {value}",
|
| 689 |
"payment_check": "Check payment result",
|
| 690 |
+
"payment_paid": "Payment was confirmed and your credits were updated. You can generate again now.",
|
| 691 |
"payment_expired": "The payment QR code has expired. Create a new one.",
|
| 692 |
"browse_files": "Choose files",
|
| 693 |
"drop_docs": "Drag and drop files here",
|
|
|
|
| 905 |
"login_pending": "Авторизация Alipay ещё не завершена. Отсканируйте код и подтвердите доступ.",
|
| 906 |
"account_title": "Учётная запись",
|
| 907 |
"account_id": "Идентификатор пользователя: **{value}**",
|
| 908 |
+
"daily_free": "Стандарт: **{value} статьи**",
|
| 909 |
+
"bonus_papers": "Флагман: **{value} статьи**",
|
| 910 |
"account_chip_user": "Пользователь {value}",
|
| 911 |
+
"account_chip_free": "Стандарт {value}",
|
| 912 |
+
"account_chip_bonus": "Флагман {value}",
|
| 913 |
"account_chip_standard": "Стандарт {value}",
|
| 914 |
"account_chip_flagship": "Флагман {value}",
|
| 915 |
"logout_short": "Выйти",
|
| 916 |
+
"payment_dialog_title": "Покупка лимита статей",
|
| 917 |
+
"payment_notice": "Лимит выбранной версии исчерпан. Приобретите лимит для этой версии, чтобы продолжить.",
|
| 918 |
+
"product_entitlement_required": "Остаток выбранной версии равен 0, поэтому создание статьи отключено. После покупки и подтверждения лимита можно продолжить.",
|
| 919 |
+
"product_data_loading": "Загружаются лимиты Standard / Flagship и политика продукта. До получения подтверждённых данных создание статьи отключено.",
|
| 920 |
+
"payment_open": "Купить лимит",
|
| 921 |
+
"payment_channel_label": "Способ оплаты для этого заказа",
|
| 922 |
+
"payment_channel_alipay": "Alipay",
|
| 923 |
+
"payment_channel_wechat": "WeChat",
|
| 924 |
+
"payment_scan_dual": "Используйте QR-код, соответствующий выбранному способу оплаты (сейчас: {channel}). После оплаты отправьте номер заказа администратору; лимит нужной версии будет добавлен после проверки поступления средств.",
|
| 925 |
+
"payment_notice_product": "Покупка {tier}: оплата {amount} {currency} добавит {credits} квоты на статьи этой версии.",
|
| 926 |
+
"payment_paid_product": "Платёж подтверждён. Добавлено {credits} квоты {tier}. Теперь можно продолжить создание статьи.",
|
| 927 |
+
"quota_exhausted": "Лимит выбранной версии исчерпан. Приобретите лимит для этой версии, чтобы продолжить.",
|
| 928 |
"payment_create": "Создать QR-код для оплаты",
|
| 929 |
+
"payment_creating": "Создаётся платёжный заказ…",
|
| 930 |
"payment_scan": "Отсканируйте QR-код в приложении Alipay. После оплаты нажмите «Проверить результат оплаты».",
|
| 931 |
+
"payment_scan_manual": "Используйте выбранный способ оплаты. После оплаты отправьте номер заказа администратору; лимит будет добавлен после проверки фактического поступления средств.",
|
| 932 |
"payment_order_no": "Номер заказа: {order_no}",
|
| 933 |
"payment_support_prompt": "Возникли вопросы с оплатой или хотите повысить качество научной статьи? Свяжитесь с администратором.",
|
| 934 |
"payment_support_wechat": "WeChat: {value}",
|
| 935 |
"payment_support_whatsapp": "WhatsApp: {value}",
|
| 936 |
"payment_check": "Проверить результат оплаты",
|
| 937 |
+
"payment_paid": "Платёж подтверждён, лимит обновлён. Теперь можно продолжить создание статьи.",
|
| 938 |
"payment_expired": "Срок действия QR-кода истёк. Создайте новый код.",
|
| 939 |
"browse_files": "Выбрать файлы",
|
| 940 |
"drop_docs": "Перетащите файлы сюда",
|
|
|
|
| 1152 |
"login_pending": "Alipay рұқсаты әлі аяқталған жоқ. QR-кодты сканерлеп, рұқсатты растаңыз.",
|
| 1153 |
"account_title": "Пайдаланушы есептік жазбасы",
|
| 1154 |
"account_id": "Пайдаланушы нөмірі: **{value}**",
|
| 1155 |
+
"daily_free": "Стандарт: **{value} мақала**",
|
| 1156 |
+
"bonus_papers": "Флагман: **{value} мақала**",
|
| 1157 |
"account_chip_user": "Пайдаланушы {value}",
|
| 1158 |
+
"account_chip_free": "Стандарт {value}",
|
| 1159 |
+
"account_chip_bonus": "Флагман {value}",
|
| 1160 |
"account_chip_standard": "Стандарт {value}",
|
| 1161 |
"account_chip_flagship": "Флагман {value}",
|
| 1162 |
"logout_short": "Шығу",
|
| 1163 |
+
"payment_dialog_title": "Мақала лимитін сатып алу",
|
| 1164 |
+
"payment_notice": "Таңдалған нұсқаның лимиті таусылды. Жалғастыру үшін осы нұсқаға лимит сатып алыңыз.",
|
| 1165 |
+
"product_entitlement_required": "Таңдалған нұсқаның қалдығы 0, сондықтан мақала жасау өшірілді. Сатып алу расталғаннан кейін жалғастыруға болады.",
|
| 1166 |
+
"product_data_loading": "Standard / Flagship лимиттері мен өнім саясаты жүктелуде. Расталған деректер дайын болғанша мақала жасау өшіріледі.",
|
| 1167 |
+
"payment_open": "Лимит сатып алу",
|
| 1168 |
+
"payment_channel_label": "Осы тапсырыстың төлем тәсілі",
|
| 1169 |
+
"payment_channel_alipay": "Alipay",
|
| 1170 |
+
"payment_channel_wechat": "WeChat",
|
| 1171 |
+
"payment_scan_dual": "Таңдалған төлем тәсіліне сәйкес QR-кодты пайдаланыңыз (қазір: {channel}). Төлемнен кейін тапсырыс нөмірін әкімшіге жіберіңіз; түсім расталғаннан кейін тиісті нұсқа лимиті қосылады.",
|
| 1172 |
+
"payment_notice_product": "{tier} нұсқасын сатып алу: {amount} {currency} төлемі осы нұсқаға {credits} мақала лимитін қосады.",
|
| 1173 |
+
"payment_paid_product": "Төлем расталды. {tier} нұсқасына {credits} мақала лимиті қосылды. Енді мақала жасауды жалғастыруға болады.",
|
| 1174 |
+
"quota_exhausted": "Таңдалған нұсқаның лимиті таусылды. Жалғастыру үшін осы нұсқаға лимит сатып алыңыз.",
|
| 1175 |
"payment_create": "Төлемге арналған QR-код жасау",
|
| 1176 |
+
"payment_creating": "Төлем тапсырысы жасалуда…",
|
| 1177 |
"payment_scan": "QR-кодты Alipay қолданбасымен сканерлеңіз. Төлемнен кейін «Төлем нәтижесін тексеру» түймесін басыңыз.",
|
| 1178 |
+
"payment_scan_manual": "Таңдалған төлем тәсілін пайдаланыңыз. Төлемнен кейін тапсырыс нөм��рін әкімшіге жіберіңіз; нақты түсім тексерілгеннен кейін лимит қосылады.",
|
| 1179 |
"payment_order_no": "Тапсырыс нөмірі: {order_no}",
|
| 1180 |
"payment_support_prompt": "Төлем бойынша мәселе туындады ма немесе ғылыми мақаланың сапасын жақсартқыңыз келе ме? Әкімшіге хабарласыңыз.",
|
| 1181 |
"payment_support_wechat": "WeChat: {value}",
|
| 1182 |
"payment_support_whatsapp": "WhatsApp: {value}",
|
| 1183 |
"payment_check": "Төлем нәтижесін тексеру",
|
| 1184 |
+
"payment_paid": "Төлем расталды, лимит жаңартылды. Енді мақала жасауды жалғастыруға болады.",
|
| 1185 |
"payment_expired": "QR-кодтың қолданылу мерзімі аяқталды. Жаңа код жасаңыз.",
|
| 1186 |
"browse_files": "Файлдарды таңдау",
|
| 1187 |
"drop_docs": "Файлдарды осы жерге сүйреп әкеліңіз",
|
managed_auth.py
CHANGED
|
@@ -140,6 +140,7 @@ def _clear_generation_session_state() -> None:
|
|
| 140 |
"rag_engine", "up_amj_ref", "up_amj_data", "up_asq_ref",
|
| 141 |
"up_asq_data", "sp_payment_required", "sp_payment_order",
|
| 142 |
"sp_payment_request_id", "sp_payment_package_sku",
|
|
|
|
| 143 |
):
|
| 144 |
st.session_state.pop(key, None)
|
| 145 |
|
|
@@ -389,6 +390,8 @@ def start_generation_task(
|
|
| 389 |
started_task = result.get("task")
|
| 390 |
if isinstance(started_task, dict) and started_task.get("task_id"):
|
| 391 |
st.session_state["sp_tracked_task_id"] = str(started_task["task_id"])
|
|
|
|
|
|
|
| 392 |
return dict(result)
|
| 393 |
|
| 394 |
|
|
@@ -450,10 +453,23 @@ def recover_generation_task() -> dict[str, Any]:
|
|
| 450 |
_merge_quota_into_session(result)
|
| 451 |
task = result.get("task")
|
| 452 |
if isinstance(task, dict) and task.get("task_id"):
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
else:
|
| 458 |
st.session_state["sp_generation_task"] = None
|
| 459 |
st.session_state["sp_last_ui_task_status"] = ""
|
|
@@ -517,12 +533,18 @@ def logout_alipay_session(on_logout: Callable[[str, str], None] | None = None) -
|
|
| 517 |
def create_payment_order(
|
| 518 |
idempotency_key: str,
|
| 519 |
package_sku: str | None = None,
|
|
|
|
| 520 |
) -> dict[str, Any]:
|
| 521 |
normalized = str(idempotency_key or "").strip()
|
| 522 |
payload: dict[str, Any] = {"idempotency_key": normalized}
|
| 523 |
normalized_sku = str(package_sku or "").strip()
|
| 524 |
if normalized_sku:
|
| 525 |
payload["package_sku"] = normalized_sku
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
result = _edge_call(
|
| 527 |
"pay_create",
|
| 528 |
normalized,
|
|
@@ -564,6 +586,7 @@ def _render_payment_purchase_body(language: str) -> None:
|
|
| 564 |
product_mode = projection.get("mode") == "a5_product"
|
| 565 |
selected_product: dict[str, Any] | None = None
|
| 566 |
package_sku = ""
|
|
|
|
| 567 |
if product_mode:
|
| 568 |
selected_tier = str(st.session_state.get("sp_product_tier") or "STANDARD").strip().upper()
|
| 569 |
raw_product = (projection.get("products") or {}).get(selected_tier)
|
|
@@ -590,7 +613,6 @@ def _render_payment_purchase_body(language: str) -> None:
|
|
| 590 |
credits=credits,
|
| 591 |
)
|
| 592 |
)
|
| 593 |
-
existing_order = st.session_state.get("sp_payment_order")
|
| 594 |
existing_sku = str(st.session_state.get("sp_payment_package_sku") or "").strip()
|
| 595 |
order_sku = str((existing_order or {}).get("package_sku") or "").strip() if isinstance(existing_order, dict) else ""
|
| 596 |
if (existing_sku and existing_sku != package_sku) or (existing_order and order_sku != package_sku):
|
|
@@ -599,17 +621,39 @@ def _render_payment_purchase_body(language: str) -> None:
|
|
| 599 |
else:
|
| 600 |
st.warning(tr(language, "payment_notice"))
|
| 601 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
if st.button(tr(language, "payment_create"), key="sp_create_payment_order"):
|
| 603 |
st.session_state["sp_payment_request_id"] = (
|
| 604 |
f"pay_{int(time.time() * 1000)}_{secrets.token_urlsafe(12)}"
|
| 605 |
)
|
| 606 |
st.session_state["sp_payment_package_sku"] = package_sku
|
|
|
|
| 607 |
|
| 608 |
request_id = str(st.session_state.get("sp_payment_request_id") or "").strip()
|
| 609 |
order = st.session_state.get("sp_payment_order")
|
| 610 |
if request_id and not order:
|
| 611 |
st.info(tr(language, "payment_creating"))
|
| 612 |
-
order_result = create_payment_order(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
order = dict(order_result.get("order") or {})
|
| 614 |
if not order.get("merchant_order_no") or not order.get("qr_code"):
|
| 615 |
raise RuntimeError("payment_order_missing_qr_code")
|
|
@@ -623,15 +667,69 @@ def _render_payment_purchase_body(language: str) -> None:
|
|
| 623 |
order = st.session_state.get("sp_payment_order") or {}
|
| 624 |
qr_code = str(order.get("qr_code") or "").strip()
|
| 625 |
if qr_code:
|
| 626 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
order_no = str(order.get("merchant_order_no") or "").strip()
|
| 628 |
if order_no:
|
| 629 |
st.caption(tr(language, "payment_order_no", order_no=order_no))
|
| 630 |
st.code(order_no)
|
| 631 |
-
payment_provider =
|
| 632 |
-
if payment_provider == "MANUAL_ALIPAY":
|
| 633 |
-
st.caption(tr(language, "payment_scan_manual"))
|
| 634 |
-
else:
|
| 635 |
st.caption(tr(language, "payment_scan"))
|
| 636 |
support = order.get("payment_support") if isinstance(order.get("payment_support"), dict) else {}
|
| 637 |
support_wechat = str((support or {}).get("wechat") or "").strip()
|
|
@@ -660,6 +758,7 @@ def _render_payment_purchase_body(language: str) -> None:
|
|
| 660 |
st.session_state["sp_payment_order"] = None
|
| 661 |
st.session_state["sp_payment_request_id"] = ""
|
| 662 |
st.session_state["sp_payment_package_sku"] = ""
|
|
|
|
| 663 |
if product_mode and selected_product is not None:
|
| 664 |
tier_name = tr(language, f"product_tier_{str(selected_product.get('product_tier') or '').lower()}")
|
| 665 |
st.success(
|
|
@@ -676,6 +775,7 @@ def _render_payment_purchase_body(language: str) -> None:
|
|
| 676 |
st.session_state["sp_payment_order"] = None
|
| 677 |
st.session_state["sp_payment_request_id"] = ""
|
| 678 |
st.session_state["sp_payment_package_sku"] = ""
|
|
|
|
| 679 |
st.warning(tr(language, "payment_expired"))
|
| 680 |
|
| 681 |
|
|
@@ -711,8 +811,8 @@ def render_logged_in_account(
|
|
| 711 |
standard_remaining = entitlement["standard_remaining"]
|
| 712 |
flagship_remaining = entitlement["flagship_remaining"]
|
| 713 |
else:
|
| 714 |
-
|
| 715 |
-
|
| 716 |
|
| 717 |
st.markdown(
|
| 718 |
"""
|
|
@@ -834,18 +934,11 @@ def render_logged_in_account(
|
|
| 834 |
|
| 835 |
info_col, logout_col = st.columns([5.9, 1.65])
|
| 836 |
with info_col:
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
)
|
| 843 |
-
else:
|
| 844 |
-
balance_markup = (
|
| 845 |
-
f"<span>{html.escape(tr(language, 'account_chip_free', value=free_remaining))}</span>"
|
| 846 |
-
f'<span class="sp-account-separator">•</span>'
|
| 847 |
-
f"<span>{html.escape(tr(language, 'account_chip_bonus', value=bonus_papers))}</span>"
|
| 848 |
-
)
|
| 849 |
st.markdown(
|
| 850 |
f"""
|
| 851 |
<div class="sp-account-card">
|
|
|
|
| 140 |
"rag_engine", "up_amj_ref", "up_amj_data", "up_asq_ref",
|
| 141 |
"up_asq_data", "sp_payment_required", "sp_payment_order",
|
| 142 |
"sp_payment_request_id", "sp_payment_package_sku",
|
| 143 |
+
"sp_payment_channel", "sp_payment_order_channel",
|
| 144 |
):
|
| 145 |
st.session_state.pop(key, None)
|
| 146 |
|
|
|
|
| 390 |
started_task = result.get("task")
|
| 391 |
if isinstance(started_task, dict) and started_task.get("task_id"):
|
| 392 |
st.session_state["sp_tracked_task_id"] = str(started_task["task_id"])
|
| 393 |
+
# A user-started task must never inherit the prior-login completed-task UI mode.
|
| 394 |
+
st.session_state["sp_recovered_completed_task_id"] = ""
|
| 395 |
return dict(result)
|
| 396 |
|
| 397 |
|
|
|
|
| 453 |
_merge_quota_into_session(result)
|
| 454 |
task = result.get("task")
|
| 455 |
if isinstance(task, dict) and task.get("task_id"):
|
| 456 |
+
recovered_status = str(task.get("status") or "").strip().lower()
|
| 457 |
+
if recovered_status == "completed":
|
| 458 |
+
# Historical completed tasks remain authoritative in D1/Bucket, but a
|
| 459 |
+
# fresh login must open a clean workspace. Do not restore the artifact,
|
| 460 |
+
# attach the completed task to Streamlit session state, or render any
|
| 461 |
+
# historical generation/download UI.
|
| 462 |
+
st.session_state["sp_generation_task"] = None
|
| 463 |
+
st.session_state["sp_last_ui_task_status"] = ""
|
| 464 |
+
st.session_state.pop("sp_tracked_task_id", None)
|
| 465 |
+
logging.info(
|
| 466 |
+
"[SP_AUTH] completed_task_hidden_on_fresh_login task_id=%s",
|
| 467 |
+
str(task.get("task_id") or ""),
|
| 468 |
+
)
|
| 469 |
+
else:
|
| 470 |
+
st.session_state["sp_generation_task"] = dict(task)
|
| 471 |
+
st.session_state["sp_tracked_task_id"] = str(task["task_id"])
|
| 472 |
+
st.session_state["sp_last_ui_task_status"] = recovered_status
|
| 473 |
else:
|
| 474 |
st.session_state["sp_generation_task"] = None
|
| 475 |
st.session_state["sp_last_ui_task_status"] = ""
|
|
|
|
| 533 |
def create_payment_order(
|
| 534 |
idempotency_key: str,
|
| 535 |
package_sku: str | None = None,
|
| 536 |
+
payment_channel: str | None = None,
|
| 537 |
) -> dict[str, Any]:
|
| 538 |
normalized = str(idempotency_key or "").strip()
|
| 539 |
payload: dict[str, Any] = {"idempotency_key": normalized}
|
| 540 |
normalized_sku = str(package_sku or "").strip()
|
| 541 |
if normalized_sku:
|
| 542 |
payload["package_sku"] = normalized_sku
|
| 543 |
+
normalized_channel = str(payment_channel or "").strip().upper()
|
| 544 |
+
if normalized_channel:
|
| 545 |
+
if normalized_channel not in {"ALIPAY", "WECHAT"}:
|
| 546 |
+
raise RuntimeError("payment_channel_invalid")
|
| 547 |
+
payload["payment_channel"] = normalized_channel
|
| 548 |
result = _edge_call(
|
| 549 |
"pay_create",
|
| 550 |
normalized,
|
|
|
|
| 586 |
product_mode = projection.get("mode") == "a5_product"
|
| 587 |
selected_product: dict[str, Any] | None = None
|
| 588 |
package_sku = ""
|
| 589 |
+
existing_order = st.session_state.get("sp_payment_order")
|
| 590 |
if product_mode:
|
| 591 |
selected_tier = str(st.session_state.get("sp_product_tier") or "STANDARD").strip().upper()
|
| 592 |
raw_product = (projection.get("products") or {}).get(selected_tier)
|
|
|
|
| 613 |
credits=credits,
|
| 614 |
)
|
| 615 |
)
|
|
|
|
| 616 |
existing_sku = str(st.session_state.get("sp_payment_package_sku") or "").strip()
|
| 617 |
order_sku = str((existing_order or {}).get("package_sku") or "").strip() if isinstance(existing_order, dict) else ""
|
| 618 |
if (existing_sku and existing_sku != package_sku) or (existing_order and order_sku != package_sku):
|
|
|
|
| 621 |
else:
|
| 622 |
st.warning(tr(language, "payment_notice"))
|
| 623 |
|
| 624 |
+
frozen_order_channel = (
|
| 625 |
+
str((existing_order or {}).get("payment_channel") or "").strip().upper()
|
| 626 |
+
if isinstance(existing_order, dict)
|
| 627 |
+
else ""
|
| 628 |
+
)
|
| 629 |
+
if frozen_order_channel in {"ALIPAY", "WECHAT"}:
|
| 630 |
+
st.session_state["sp_payment_channel"] = frozen_order_channel
|
| 631 |
+
payment_channel = st.radio(
|
| 632 |
+
tr(language, "payment_channel_label"),
|
| 633 |
+
("ALIPAY", "WECHAT"),
|
| 634 |
+
index=0 if str(st.session_state.get("sp_payment_channel") or "ALIPAY").upper() != "WECHAT" else 1,
|
| 635 |
+
format_func=lambda value: tr(language, f"payment_channel_{value.lower()}"),
|
| 636 |
+
horizontal=True,
|
| 637 |
+
disabled=bool(existing_order),
|
| 638 |
+
key="sp_payment_channel",
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
if st.button(tr(language, "payment_create"), key="sp_create_payment_order"):
|
| 642 |
st.session_state["sp_payment_request_id"] = (
|
| 643 |
f"pay_{int(time.time() * 1000)}_{secrets.token_urlsafe(12)}"
|
| 644 |
)
|
| 645 |
st.session_state["sp_payment_package_sku"] = package_sku
|
| 646 |
+
st.session_state["sp_payment_order_channel"] = str(payment_channel).upper()
|
| 647 |
|
| 648 |
request_id = str(st.session_state.get("sp_payment_request_id") or "").strip()
|
| 649 |
order = st.session_state.get("sp_payment_order")
|
| 650 |
if request_id and not order:
|
| 651 |
st.info(tr(language, "payment_creating"))
|
| 652 |
+
order_result = create_payment_order(
|
| 653 |
+
request_id,
|
| 654 |
+
package_sku or None,
|
| 655 |
+
str(st.session_state.get("sp_payment_order_channel") or payment_channel),
|
| 656 |
+
)
|
| 657 |
order = dict(order_result.get("order") or {})
|
| 658 |
if not order.get("merchant_order_no") or not order.get("qr_code"):
|
| 659 |
raise RuntimeError("payment_order_missing_qr_code")
|
|
|
|
| 667 |
order = st.session_state.get("sp_payment_order") or {}
|
| 668 |
qr_code = str(order.get("qr_code") or "").strip()
|
| 669 |
if qr_code:
|
| 670 |
+
payment_provider = str(order.get("payment_provider") or "").strip().upper()
|
| 671 |
+
payment_channel = str(order.get("payment_channel") or st.session_state.get("sp_payment_order_channel") or "ALIPAY").strip().upper()
|
| 672 |
+
if payment_provider == "MANUAL_ALIPAY":
|
| 673 |
+
alipay_bytes = _qr_png_bytes(qr_code)
|
| 674 |
+
wechat_path = Path(__file__).resolve().parent / "static" / "payment_wechat_qr.png"
|
| 675 |
+
if not wechat_path.is_file():
|
| 676 |
+
raise RuntimeError("payment_wechat_qr_missing")
|
| 677 |
+
wechat_bytes = wechat_path.read_bytes()
|
| 678 |
+
alipay_uri = "data:image/png;base64," + base64.b64encode(alipay_bytes).decode("ascii")
|
| 679 |
+
wechat_uri = "data:image/png;base64," + base64.b64encode(wechat_bytes).decode("ascii")
|
| 680 |
+
st.markdown(
|
| 681 |
+
f"""
|
| 682 |
+
<style media="all">
|
| 683 |
+
.sp-payment-grid {{
|
| 684 |
+
display:grid;
|
| 685 |
+
grid-template-columns:repeat(2,minmax(0,1fr));
|
| 686 |
+
gap:1rem;
|
| 687 |
+
align-items:start;
|
| 688 |
+
margin:.35rem 0 .6rem;
|
| 689 |
+
}}
|
| 690 |
+
.sp-payment-card {{text-align:center; min-width:0;}}
|
| 691 |
+
.sp-payment-card strong {{display:block; margin-bottom:.4rem;}}
|
| 692 |
+
.sp-payment-qr-frame {{
|
| 693 |
+
width:min(100%,300px);
|
| 694 |
+
aspect-ratio:1/1;
|
| 695 |
+
margin:0 auto;
|
| 696 |
+
display:flex;
|
| 697 |
+
align-items:center;
|
| 698 |
+
justify-content:center;
|
| 699 |
+
overflow:hidden;
|
| 700 |
+
}}
|
| 701 |
+
.sp-payment-qr-frame img {{
|
| 702 |
+
width:100%;
|
| 703 |
+
height:100%;
|
| 704 |
+
object-fit:contain;
|
| 705 |
+
display:block;
|
| 706 |
+
}}
|
| 707 |
+
@media (max-width:640px) {{
|
| 708 |
+
.sp-payment-grid {{grid-template-columns:1fr;}}
|
| 709 |
+
.sp-payment-qr-frame {{width:min(86vw,300px);}}
|
| 710 |
+
}}
|
| 711 |
+
</style>
|
| 712 |
+
<div class="sp-payment-grid">
|
| 713 |
+
<div class="sp-payment-card">
|
| 714 |
+
<strong>{html.escape(tr(language, 'payment_channel_alipay'))}</strong>
|
| 715 |
+
<div class="sp-payment-qr-frame"><img src="{alipay_uri}" alt="Alipay QR"></div>
|
| 716 |
+
</div>
|
| 717 |
+
<div class="sp-payment-card">
|
| 718 |
+
<strong>{html.escape(tr(language, 'payment_channel_wechat'))}</strong>
|
| 719 |
+
<div class="sp-payment-qr-frame"><img src="{wechat_uri}" alt="WeChat QR"></div>
|
| 720 |
+
</div>
|
| 721 |
+
</div>
|
| 722 |
+
""",
|
| 723 |
+
unsafe_allow_html=True,
|
| 724 |
+
)
|
| 725 |
+
st.caption(tr(language, "payment_scan_dual", channel=tr(language, f"payment_channel_{payment_channel.lower()}")))
|
| 726 |
+
else:
|
| 727 |
+
st.image(_qr_png_bytes(qr_code), width=280)
|
| 728 |
order_no = str(order.get("merchant_order_no") or "").strip()
|
| 729 |
if order_no:
|
| 730 |
st.caption(tr(language, "payment_order_no", order_no=order_no))
|
| 731 |
st.code(order_no)
|
| 732 |
+
if payment_provider != "MANUAL_ALIPAY":
|
|
|
|
|
|
|
|
|
|
| 733 |
st.caption(tr(language, "payment_scan"))
|
| 734 |
support = order.get("payment_support") if isinstance(order.get("payment_support"), dict) else {}
|
| 735 |
support_wechat = str((support or {}).get("wechat") or "").strip()
|
|
|
|
| 758 |
st.session_state["sp_payment_order"] = None
|
| 759 |
st.session_state["sp_payment_request_id"] = ""
|
| 760 |
st.session_state["sp_payment_package_sku"] = ""
|
| 761 |
+
st.session_state["sp_payment_order_channel"] = ""
|
| 762 |
if product_mode and selected_product is not None:
|
| 763 |
tier_name = tr(language, f"product_tier_{str(selected_product.get('product_tier') or '').lower()}")
|
| 764 |
st.success(
|
|
|
|
| 775 |
st.session_state["sp_payment_order"] = None
|
| 776 |
st.session_state["sp_payment_request_id"] = ""
|
| 777 |
st.session_state["sp_payment_package_sku"] = ""
|
| 778 |
+
st.session_state["sp_payment_order_channel"] = ""
|
| 779 |
st.warning(tr(language, "payment_expired"))
|
| 780 |
|
| 781 |
|
|
|
|
| 811 |
standard_remaining = entitlement["standard_remaining"]
|
| 812 |
flagship_remaining = entitlement["flagship_remaining"]
|
| 813 |
else:
|
| 814 |
+
standard_remaining = "—"
|
| 815 |
+
flagship_remaining = "—"
|
| 816 |
|
| 817 |
st.markdown(
|
| 818 |
"""
|
|
|
|
| 934 |
|
| 935 |
info_col, logout_col = st.columns([5.9, 1.65])
|
| 936 |
with info_col:
|
| 937 |
+
balance_markup = (
|
| 938 |
+
f"<span>{html.escape(tr(language, 'account_chip_standard', value=standard_remaining))}</span>"
|
| 939 |
+
f'<span class="sp-account-separator">•</span>'
|
| 940 |
+
f"<span>{html.escape(tr(language, 'account_chip_flagship', value=flagship_remaining))}</span>"
|
| 941 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 942 |
st.markdown(
|
| 943 |
f"""
|
| 944 |
<div class="sp-account-card">
|
static/payment_wechat_qr.png
ADDED
|
Git LFS Details
|