Spaces:
Running
Running
R3.2.8.6-R1.2 Option A Local Resume Capability Reconciliation
#29
by GuXiaopeng - opened
app.py
CHANGED
|
@@ -305,6 +305,76 @@ def _task_dir(task_id: str) -> Path:
|
|
| 305 |
return Path(_user_workspace) / "tasks" / _safe_key(task_id)
|
| 306 |
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
def _task_preview_path(task_id: str) -> str:
|
| 309 |
"""Return the unguessable local static preview URL for this task, if any."""
|
| 310 |
request_path = _task_dir(task_id) / "request.json"
|
|
@@ -532,12 +602,15 @@ def _process_pending_resume() -> None:
|
|
| 532 |
task_id = str(st.session_state.get("sp_pending_resume_task_id") or "").strip()
|
| 533 |
if not task_id:
|
| 534 |
return
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
| 536 |
st.session_state["sp_pending_resume_task_id"] = ""
|
| 537 |
-
st.session_state
|
| 538 |
st.session_state["sp_terminal_notice"] = {
|
| 539 |
-
"kind": "
|
| 540 |
-
"message":
|
| 541 |
}
|
| 542 |
st.rerun()
|
| 543 |
_launch_executor(task_id, force=True)
|
|
@@ -816,7 +889,8 @@ def _render_task_status_once(lang_code: str) -> None:
|
|
| 816 |
_render_checkpoint_preview(task, lang_code)
|
| 817 |
|
| 818 |
if status == "resumable":
|
| 819 |
-
|
|
|
|
| 820 |
elif status == "completed":
|
| 821 |
output_path = _task_output_path(task)
|
| 822 |
if output_path and output_path.is_file():
|
|
@@ -914,9 +988,17 @@ def render_workspace_controls(user):
|
|
| 914 |
|
| 915 |
|
| 916 |
if task_status == "resumable":
|
| 917 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 918 |
colA, colB = st.columns([1, 1])
|
| 919 |
-
if colA.button(tr(lang_code, "resume"), key="resume_task"):
|
| 920 |
st.session_state["sp_pending_resume_task_id"] = str(task.get("task_id") or "")
|
| 921 |
st.rerun()
|
| 922 |
if colB.button(tr(lang_code, "clear"), key="clear_task"):
|
|
|
|
| 305 |
return Path(_user_workspace) / "tasks" / _safe_key(task_id)
|
| 306 |
|
| 307 |
|
| 308 |
+
def _local_resume_capability(task: dict) -> tuple[bool, str]:
|
| 309 |
+
"""Return whether this HF runtime has the local material required to resume.
|
| 310 |
+
|
| 311 |
+
Worker/D1 remains authoritative for the remote task state. This function
|
| 312 |
+
answers only the local capability question and never changes task/quota
|
| 313 |
+
state.
|
| 314 |
+
"""
|
| 315 |
+
task = dict(task or {})
|
| 316 |
+
task_id = str(task.get("task_id") or "").strip()
|
| 317 |
+
if not task_id:
|
| 318 |
+
return False, "task_id_missing"
|
| 319 |
+
|
| 320 |
+
task_dir = _task_dir(task_id)
|
| 321 |
+
request_path = task_dir / "request.json"
|
| 322 |
+
if not request_path.is_file():
|
| 323 |
+
return False, "request_missing"
|
| 324 |
+
try:
|
| 325 |
+
request_payload = json.loads(request_path.read_text(encoding="utf-8"))
|
| 326 |
+
except Exception:
|
| 327 |
+
return False, "request_unreadable"
|
| 328 |
+
if not isinstance(request_payload, dict):
|
| 329 |
+
return False, "request_invalid"
|
| 330 |
+
if str(request_payload.get("task_id") or "").strip() != task_id:
|
| 331 |
+
return False, "request_task_mismatch"
|
| 332 |
+
|
| 333 |
+
request_hash = str(request_payload.get("request_hash") or "").strip().lower()
|
| 334 |
+
if not re.fullmatch(r"[a-f0-9]{64}", request_hash):
|
| 335 |
+
return False, "request_hash_invalid"
|
| 336 |
+
if request_hash != _canonical_request_hash(request_payload):
|
| 337 |
+
return False, "request_hash_mismatch"
|
| 338 |
+
|
| 339 |
+
task_root = task_dir.resolve()
|
| 340 |
+
for field in ("reference_files", "data_files", "interview_files"):
|
| 341 |
+
items = request_payload.get(field) or []
|
| 342 |
+
if not isinstance(items, list):
|
| 343 |
+
return False, f"{field}_invalid"
|
| 344 |
+
for item in items:
|
| 345 |
+
local_path = (task_dir / str(item)).resolve()
|
| 346 |
+
if task_root not in local_path.parents or not local_path.is_file():
|
| 347 |
+
return False, "input_missing"
|
| 348 |
+
|
| 349 |
+
try:
|
| 350 |
+
checkpoint_seq = int(task.get("checkpoint_seq") or 0)
|
| 351 |
+
except (TypeError, ValueError):
|
| 352 |
+
return False, "checkpoint_seq_invalid"
|
| 353 |
+
if checkpoint_seq <= 0:
|
| 354 |
+
return True, "ready"
|
| 355 |
+
|
| 356 |
+
expected_stage = str(task.get("checkpoint_stage") or "").strip()
|
| 357 |
+
try:
|
| 358 |
+
candidates = sorted((task_dir / "checkpoints").glob(f"checkpoint_{checkpoint_seq:05d}_*.json"))
|
| 359 |
+
except Exception:
|
| 360 |
+
candidates = []
|
| 361 |
+
for candidate in candidates:
|
| 362 |
+
try:
|
| 363 |
+
payload = json.loads(candidate.read_text(encoding="utf-8"))
|
| 364 |
+
if not isinstance(payload, dict):
|
| 365 |
+
continue
|
| 366 |
+
if str(payload.get("request_hash") or "").strip().lower() != request_hash:
|
| 367 |
+
continue
|
| 368 |
+
if int(payload.get("checkpoint_seq") or 0) != checkpoint_seq:
|
| 369 |
+
continue
|
| 370 |
+
if expected_stage and str(payload.get("checkpoint_stage") or "").strip() != expected_stage:
|
| 371 |
+
continue
|
| 372 |
+
return True, "ready"
|
| 373 |
+
except Exception:
|
| 374 |
+
continue
|
| 375 |
+
return False, "checkpoint_missing"
|
| 376 |
+
|
| 377 |
+
|
| 378 |
def _task_preview_path(task_id: str) -> str:
|
| 379 |
"""Return the unguessable local static preview URL for this task, if any."""
|
| 380 |
request_path = _task_dir(task_id) / "request.json"
|
|
|
|
| 602 |
task_id = str(st.session_state.get("sp_pending_resume_task_id") or "").strip()
|
| 603 |
if not task_id:
|
| 604 |
return
|
| 605 |
+
task = dict(st.session_state.get("sp_generation_task") or {})
|
| 606 |
+
capable, reason = _local_resume_capability(task)
|
| 607 |
+
if str(task.get("task_id") or "").strip() != task_id or not capable:
|
| 608 |
+
logging.warning("[SP_UI] resume_unavailable task_id=%s reason=%s", task_id, reason)
|
| 609 |
st.session_state["sp_pending_resume_task_id"] = ""
|
| 610 |
+
lang_code = str(st.session_state.get("sp_lang_code") or "zh")
|
| 611 |
st.session_state["sp_terminal_notice"] = {
|
| 612 |
+
"kind": "warning",
|
| 613 |
+
"message": tr(lang_code, "resume_unavailable_runtime_replaced"),
|
| 614 |
}
|
| 615 |
st.rerun()
|
| 616 |
_launch_executor(task_id, force=True)
|
|
|
|
| 889 |
_render_checkpoint_preview(task, lang_code)
|
| 890 |
|
| 891 |
if status == "resumable":
|
| 892 |
+
resume_capable, _ = _local_resume_capability(task)
|
| 893 |
+
st.warning(tr(lang_code, "checkpoint_warning" if resume_capable else "resume_unavailable_runtime_replaced"))
|
| 894 |
elif status == "completed":
|
| 895 |
output_path = _task_output_path(task)
|
| 896 |
if output_path and output_path.is_file():
|
|
|
|
| 988 |
|
| 989 |
|
| 990 |
if task_status == "resumable":
|
| 991 |
+
resume_capable, resume_reason = _local_resume_capability(task)
|
| 992 |
+
if resume_capable:
|
| 993 |
+
st.warning(tr(lang_code, "checkpoint_warning"))
|
| 994 |
+
else:
|
| 995 |
+
logging.warning(
|
| 996 |
+
"[SP_UI] local_resume_capability_missing task_id=%s reason=%s",
|
| 997 |
+
str(task.get("task_id") or ""), resume_reason,
|
| 998 |
+
)
|
| 999 |
+
st.warning(tr(lang_code, "resume_unavailable_runtime_replaced"))
|
| 1000 |
colA, colB = st.columns([1, 1])
|
| 1001 |
+
if colA.button(tr(lang_code, "resume"), key="resume_task", disabled=not resume_capable):
|
| 1002 |
st.session_state["sp_pending_resume_task_id"] = str(task.get("task_id") or "")
|
| 1003 |
st.rerun()
|
| 1004 |
if colB.button(tr(lang_code, "clear"), key="clear_task"):
|
i18n.py
CHANGED
|
@@ -215,6 +215,7 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 215 |
"system_busy_warning": "🚨 有用户正在生成论文",
|
| 216 |
"system_busy_info": "💡 预计25–35分钟后可正常使用",
|
| 217 |
"checkpoint_warning": "⚠️ 检测到暂停的未完成论文任务。您可以继续原任务,或删除后重新开始。",
|
|
|
|
| 218 |
"resume": "🔄 继续原任务",
|
| 219 |
"clear": "🗑️ 删除并重新开始",
|
| 220 |
"cancel_generation": "⛔ 取消生成",
|
|
@@ -385,6 +386,7 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 385 |
"system_busy_warning": "🚨 Another user is generating a paper",
|
| 386 |
"system_busy_info": "💡 The system is expected to be available in 25–35 minutes",
|
| 387 |
"checkpoint_warning": "⚠️ A paused unfinished paper task was detected. You can continue it or delete it and start again.",
|
|
|
|
| 388 |
"resume": "🔄 Continue Previous Task",
|
| 389 |
"clear": "🗑️ Delete and Start Again",
|
| 390 |
"cancel_generation": "⛔ Cancel Generation",
|
|
@@ -555,6 +557,7 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 555 |
"system_busy_warning": "🚨 Другой пользователь формирует статью",
|
| 556 |
"system_busy_info": "💡 Ожидаемое время доступности — через 25–35 минут",
|
| 557 |
"checkpoint_warning": "⚠️ Обнаружена приостановленная незавершённая задача. Вы можете продолжить её или удалить и начать заново.",
|
|
|
|
| 558 |
"resume": "🔄 Продолжить предыдущую задачу",
|
| 559 |
"clear": "🗑️ Удалить и начать заново",
|
| 560 |
"cancel_generation": "⛔ Отменить генерацию",
|
|
@@ -725,6 +728,7 @@ TEXTS: dict[str, dict[str, str]] = {
|
|
| 725 |
"system_busy_warning": "🚨 Басқа пайдаланушы мақала дайындап жатыр",
|
| 726 |
"system_busy_info": "💡 Жүйе шамамен 25–35 минуттан кейін қолжетімді болады",
|
| 727 |
"checkpoint_warning": "⚠️ Аяқталмаған, уақытша тоқтатылған тапсырма анықталды. Оны жалғастыруға немесе өшіріп, қайта бастауға болады.",
|
|
|
|
| 728 |
"resume": "🔄 Алдыңғы тапсырманы жалғастыру",
|
| 729 |
"clear": "🗑️ Өшіріп, қайта бастау",
|
| 730 |
"cancel_generation": "⛔ Генерацияны тоқтату",
|
|
|
|
| 215 |
"system_busy_warning": "🚨 有用户正在生成论文",
|
| 216 |
"system_busy_info": "💡 预计25–35分钟后可正常使用",
|
| 217 |
"checkpoint_warning": "⚠️ 检测到暂停的未完成论文任务。您可以继续原任务,或删除后重新开始。",
|
| 218 |
+
"resume_unavailable_runtime_replaced": "⚠️ 检测到暂停的未完成论文任务,但当前运行环境缺少继续所需的本地请求或断点文件。为避免错误恢复,继续按钮已禁用;如需释放本任务并重新开始,请明确点击“删除并重新开始”。",
|
| 219 |
"resume": "🔄 继续原任务",
|
| 220 |
"clear": "🗑️ 删除并重新开始",
|
| 221 |
"cancel_generation": "⛔ 取消生成",
|
|
|
|
| 386 |
"system_busy_warning": "🚨 Another user is generating a paper",
|
| 387 |
"system_busy_info": "💡 The system is expected to be available in 25–35 minutes",
|
| 388 |
"checkpoint_warning": "⚠️ A paused unfinished paper task was detected. You can continue it or delete it and start again.",
|
| 389 |
+
"resume_unavailable_runtime_replaced": "⚠️ A paused unfinished paper task was detected, but this runtime no longer has the local request or checkpoint files required to continue it. Resume has been disabled to prevent an unsafe recovery. To release this task and start again, explicitly choose ‘Delete and Start Again’.",
|
| 390 |
"resume": "🔄 Continue Previous Task",
|
| 391 |
"clear": "🗑️ Delete and Start Again",
|
| 392 |
"cancel_generation": "⛔ Cancel Generation",
|
|
|
|
| 557 |
"system_busy_warning": "🚨 Другой пользователь формирует статью",
|
| 558 |
"system_busy_info": "💡 Ожидаемое время доступности — через 25–35 минут",
|
| 559 |
"checkpoint_warning": "⚠️ Обнаружена приостановленная незавершённая задача. Вы можете продолжить её или удалить и начать заново.",
|
| 560 |
+
"resume_unavailable_runtime_replaced": "⚠️ Обнаружена приостановленная незавершённая задача, но в текущей среде выполнения отсутствуют локальный файл запроса или контрольная точка, необходимые для продолжения. Чтобы избежать некорректного восстановления, кнопка продолжения отключена. Для освобождения задачи и нового запуска явно выберите «Удалить и начать заново».",
|
| 561 |
"resume": "🔄 Продолжить предыдущую задачу",
|
| 562 |
"clear": "🗑️ Удалить и начать заново",
|
| 563 |
"cancel_generation": "⛔ Отменить генерацию",
|
|
|
|
| 728 |
"system_busy_warning": "🚨 Басқа пайдаланушы мақала дайындап жатыр",
|
| 729 |
"system_busy_info": "💡 Жүйе шамамен 25–35 минуттан кейін қолжетімді болады",
|
| 730 |
"checkpoint_warning": "⚠️ Аяқталмаған, уақытша тоқтатылған тапсырма анықталды. Оны жалғастыруға немесе өшіріп, қайта бастауға болады.",
|
| 731 |
+
"resume_unavailable_runtime_replaced": "⚠️ Уақытша тоқтатылған аяқталмаған тапсырма анықталды, бірақ ағымдағы іске қосу ортасында оны жалғастыруға қажет жергілікті сұрау немесе бақылау нүктесі файлдары жоқ. Қате қалпына келтіруге жол бермеу үшін жалғастыру түймесі өшірілді. Тапсырманы босатып, қайта бастау үшін «Өшіріп, қайта бастау» әрекетін нақты таңдаңыз.",
|
| 732 |
"resume": "🔄 Алдыңғы тапсырманы жалғастыру",
|
| 733 |
"clear": "🗑️ Өшіріп, қайта бастау",
|
| 734 |
"cancel_generation": "⛔ Генерацияны тоқтату",
|