mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* 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>
137 lines
5.3 KiB
Python
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)
|