deer-flow/backend/app/gateway/scheduled_task_errors.py
Totoro 5ae95e6a34
feat: add Feishu group notifications and fix mention caret placement (#6762)
* feat(feishu): deliver scheduled-task notifications to bound users

* feat: add Feishu group notifications and fix mention caret placement

* fix(frontend): preserve group targets while capabilities load

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-13 07:17:56 +08:00

137 lines
5.3 KiB
Python

"""Coded errors for the scheduled-task REST routes and the chat capability.
Every error raised by ``/api/scheduled-tasks*``, the shared validation and the
conversation capability is ``HTTPException(status, detail={"code", "message",
"params"})`` (``params`` omitted when empty), the same shape the skills export
uses. Messages stay English for API clients and the agent; the web UI
translates by ``code``. ``contracts/scheduled_task_errors_contract.json`` lists
every code, split into the ones the UI must translate and the ones only the
agent sees.
"""
from __future__ import annotations
from typing import Any
from fastapi import HTTPException
SCHEDULER_UI_ERROR_CODES: frozenset[str] = frozenset(
{
"invalid_request",
"invalid_schedule",
"invalid_schedule_type",
"invalid_timezone",
"interval_too_short",
"interval_too_long",
"once_in_past",
"once_too_soon",
"once_time_passed",
"invalid_context_mode",
"reuse_thread_requires_thread",
"thread_not_found",
"invalid_assistant",
"unknown_assistant",
"invalid_goal",
"goal_requires_fresh_thread",
"invalid_stop_condition",
"invalid_notification_group",
"invalid_max_runs",
"end_at_in_past",
"end_at_before_first_run",
"frequent_requires_limit",
"max_runs_not_above_used",
"limits_exhausted",
"task_not_found",
"task_running",
"run_queued",
"task_changed",
"task_finished",
"task_quota_exceeded",
"scheduler_not_running",
"scheduler_unavailable",
"trigger_failed",
}
)
SCHEDULER_AGENT_ERROR_CODES: frozenset[str] = frozenset(
{
"timezone_required",
"authentication_required",
"permission_denied",
"scheduler_tools_disabled",
"authority_expired",
"conversation_not_found",
"interactive_run_required",
"unsupported_action",
"unsupported_fields",
"task_id_required",
"note_not_verbatim",
"note_limit_reached",
"trial_requires_direct_request",
"no_stop_authority",
"occurrence_not_active",
}
)
def scheduler_error(status_code: int, code: str, message: str, **params: Any) -> HTTPException:
"""Build the coded ``HTTPException`` for one scheduled-task error."""
detail: dict[str, Any] = {"code": code, "message": message}
if params:
detail["params"] = params
return HTTPException(status_code=status_code, detail=detail)
def tool_error_payload(exc: HTTPException) -> dict[str, Any]:
"""Project a scheduled-task ``HTTPException`` into the capability's error result."""
detail = exc.detail
if isinstance(detail, dict) and isinstance(detail.get("code"), str):
payload: dict[str, Any] = {"error": str(detail.get("message") or ""), "code": detail["code"], "status_code": exc.status_code}
params = detail.get("params")
if params:
payload["params"] = params
return payload
return {"error": str(detail), "code": "error", "status_code": exc.status_code}
def active_occurrence_message(status: str) -> str:
"""English message for a mutation blocked by an active occurrence."""
detail = f"Scheduled task has an active {status} occurrence; retry after it finishes"
if status == "queued":
detail += " or cancel the queued occurrence by pausing the task"
return detail
def active_occurrence_error(status: str) -> HTTPException:
"""409 for a queued (``run_queued``) or launching/running (``task_running``) occurrence."""
return scheduler_error(409, "run_queued" if status == "queued" else "task_running", active_occurrence_message(status))
def limits_exhausted_error(exc: Any) -> HTTPException:
"""409 ``limits_exhausted`` from a ``ScheduledTaskLimitsExhausted``."""
if exc.limit == "end_at":
message = f"The end time {exc.end_at} has passed. Set a later end_at or clear it (end_at: null) in the same request to reactivate."
else:
message = f"All {exc.max_runs} automatic runs are used. Raise max_runs above {exc.used} or clear it (max_runs: null) in the same request to reactivate."
return scheduler_error(409, "limits_exhausted", message, limit=exc.limit, used=exc.used, max_runs=exc.max_runs, end_at=exc.end_at)
def repository_error(exc: Exception) -> HTTPException:
"""Map a scheduled-task repository exception to its coded error."""
from deerflow.persistence.scheduled_tasks import ActiveScheduledTaskMutationConflict, ScheduledTaskLimitsExhausted, ScheduledTaskQuotaExceeded
if isinstance(exc, ActiveScheduledTaskMutationConflict):
return active_occurrence_error(exc.status)
if isinstance(exc, ScheduledTaskQuotaExceeded):
return scheduler_error(409, "task_quota_exceeded", str(exc), limit=20)
if isinstance(exc, ScheduledTaskLimitsExhausted):
return limits_exhausted_error(exc)
message = str(exc)
if "fresh_thread_per_run" in message:
return scheduler_error(422, "goal_requires_fresh_thread", message)
if message.startswith("Goal objective"):
return scheduler_error(422, "invalid_goal", message, max_chars=4000)
if message.startswith("at most 10 standing notes"):
return scheduler_error(422, "note_limit_reached", message, limit=10)
return scheduler_error(422, "invalid_request", message)