mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* feat(channels): generalize IM group targets and add task/goal run notifications
- Generalize proactive group notification capability (proactive_group_notifications), binding validation, and scheduled task group selection across Feishu, QQ, and WeCom while keeping WeChat personal-only.
- Add QQ group active notification delivery via POST /v2/groups/{group_openid}/messages (msg_type=2) and WeCom group active notification delivery via WS Client.sendMessage(chatid, body) with bot_id:group:chatid workspace isolation.
- Generalize notification outbox sources via migration 0043_runtime_im_notices (source_kind, source_key, run_id, nullable task_id/task_run_id) with backward-compatible SQLite/PostgreSQL upgrades.
- Add opt-in ordinary task & goal run IM notifications (run_completed, run_failed, human_input_requested) on the shared outbox, delivery worker, renderer, and settings UI.
* fix(notifications): address review feedback and group/runtime notification edge cases
* test(migrations): update CURRENT_HEAD to 0043_runtime_im_notices across migration chain tests
* fix(channels): preserve group binding names and legacy authorization
Carry owner-chosen group names through existing connect state, keep private bindings out of legacy group fallback, and restore completion metadata retries. Cover real binding and worker finalization paths and synchronize notification docs.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
164 lines
7.9 KiB
Python
164 lines
7.9 KiB
Python
"""Read-only feature-flag endpoint for the frontend bootstrap.
|
|
|
|
Reports which optional features are exposed over HTTP so the frontend can gate
|
|
UI and avoid firing requests that the backend would reject. Config-only flags
|
|
read through ``get_config`` so edits to ``config.yaml`` take effect on the next
|
|
request, while startup-scoped capabilities report the runtime that actually
|
|
started.
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.gateway.browser_capability import browser_capability
|
|
from app.gateway.conversation_access import conversation_references_enabled
|
|
from app.gateway.deps import get_config
|
|
from app.gateway.knowledge_scope_admission import RAGFLOW_KNOWLEDGE_SEARCH_PROVIDER
|
|
from app.gateway.routers.thread_activity import thread_activity_available
|
|
from app.gateway.run_models import MAX_CONVERSATION_REFERENCES
|
|
from deerflow.config.app_config import AppConfig
|
|
from deerflow.scheduler.runtime import scheduler_tools_enabled
|
|
from deerflow.subagents.capacity import configured_subagent_max_running
|
|
|
|
router = APIRouter(prefix="/api", tags=["features"])
|
|
|
|
|
|
class AgentsApiFeature(BaseModel):
|
|
"""Availability of the custom-agent management API."""
|
|
|
|
enabled: bool = Field(..., description="Whether the agents_api routes are exposed over HTTP")
|
|
|
|
|
|
class BrowserControlFeature(BaseModel):
|
|
"""Availability of live agentic browser control."""
|
|
|
|
enabled: bool = Field(..., description="Whether the live browser routes and UI are available")
|
|
|
|
|
|
class McpTasksFeature(BaseModel):
|
|
"""Availability of the durable MCP task runtime."""
|
|
|
|
enabled: bool = Field(..., description="Whether durable MCP task APIs and UI are available")
|
|
|
|
|
|
class SubagentBatchesFeature(BaseModel):
|
|
"""Persistence, worker, and process capacity for native-subagent batches."""
|
|
|
|
enabled: bool = Field(..., description="Compatibility alias for worker_running")
|
|
repository_available: bool = Field(..., description="Whether durable batch history APIs are available")
|
|
worker_running: bool = Field(..., description="Whether this Gateway process is executing durable batch work")
|
|
max_running: int = Field(..., description="Native subagent execution slots in this Gateway process")
|
|
|
|
|
|
class ConversationReferencesFeature(BaseModel):
|
|
"""Availability of explicit conversation references on run requests."""
|
|
|
|
enabled: bool = Field(..., description="Whether the opt-in read_conversation tool is configured, so run requests may carry conversation_references")
|
|
max_references: int = Field(..., description="Maximum conversation references accepted on one run request")
|
|
|
|
|
|
class KnowledgeBaseFeature(BaseModel):
|
|
"""Availability of RAGFlow retrieval scope selection in chat."""
|
|
|
|
scope_selection_enabled: bool = Field(
|
|
...,
|
|
description="Whether chat may select a per-message RAGFlow retrieval scope",
|
|
)
|
|
|
|
|
|
class ScheduledTasksFeature(BaseModel):
|
|
"""Availability of scheduled tasks in this Gateway process."""
|
|
|
|
available: bool = Field(..., description="Whether the scheduled-task APIs have persistence (tasks can be listed and managed)")
|
|
running: bool = Field(..., description="Whether this process's scheduler poller is running, so tasks run on schedule and new tasks can be created")
|
|
tool_enabled: bool = Field(..., description="Whether chats can create and manage tasks and scheduled runs can stop their own schedule")
|
|
min_interval_seconds: int = Field(..., description="Shortest interval and earliest one-time delay, in seconds")
|
|
group_notifications: bool = Field(..., description="Whether scheduled tasks support explicitly selected group notifications")
|
|
|
|
|
|
class ThreadActivityFeature(BaseModel):
|
|
"""Availability of the activity feed and per-user read markers."""
|
|
|
|
available: bool = Field(..., description="Whether GET /api/thread-activity, POST /api/threads/{thread_id}/read and the thread search `unread` field work (SQL persistence)")
|
|
|
|
|
|
class FeaturesResponse(BaseModel):
|
|
"""Frontend-facing feature availability flags."""
|
|
|
|
agents_api: AgentsApiFeature
|
|
browser_control: BrowserControlFeature
|
|
mcp_tasks: McpTasksFeature
|
|
subagent_batches: SubagentBatchesFeature
|
|
conversation_references: ConversationReferencesFeature
|
|
knowledge_base: KnowledgeBaseFeature
|
|
scheduled_tasks: ScheduledTasksFeature
|
|
thread_activity: ThreadActivityFeature
|
|
|
|
|
|
@router.get(
|
|
"/features",
|
|
response_model=FeaturesResponse,
|
|
summary="List Feature Flags",
|
|
description="Report which optional features are available, so the frontend can gate UI before issuing requests.",
|
|
)
|
|
async def list_features(request: Request, config: AppConfig = Depends(get_config)) -> FeaturesResponse:
|
|
"""Return availability of optional frontend features."""
|
|
browser = browser_capability(config)
|
|
subagent_batch_worker_running = bool(getattr(request.app.state, "subagent_batches_available", False))
|
|
return FeaturesResponse(
|
|
agents_api=AgentsApiFeature(enabled=config.agents_api.enabled),
|
|
browser_control=BrowserControlFeature(enabled=browser.available),
|
|
# MCP task bindings and the submitter are startup-scoped. Report the
|
|
# capability that actually started rather than a hot-reloaded config
|
|
# value that would require a Gateway restart to take effect.
|
|
mcp_tasks=McpTasksFeature(enabled=bool(getattr(request.app.state, "mcp_tasks_available", False))),
|
|
subagent_batches=SubagentBatchesFeature(
|
|
# Keep the historical `enabled` field as a compatibility alias
|
|
# while exposing read persistence independently from execution.
|
|
# A stopped/disabled worker must not hide durable history/export.
|
|
enabled=subagent_batch_worker_running,
|
|
repository_available=getattr(request.app.state, "subagent_batch_repo", None) is not None,
|
|
worker_running=subagent_batch_worker_running,
|
|
max_running=configured_subagent_max_running(),
|
|
),
|
|
# Same predicate as run admission (``prepare_conversation_reader``), read
|
|
# through ``get_config`` so enabling the tool in config.yaml shows up
|
|
# without a restart. A UI with no entry point still needs no change here.
|
|
conversation_references=ConversationReferencesFeature(
|
|
enabled=conversation_references_enabled(config),
|
|
max_references=MAX_CONVERSATION_REFERENCES,
|
|
),
|
|
knowledge_base=KnowledgeBaseFeature(
|
|
scope_selection_enabled=_knowledge_scope_selection_enabled(config),
|
|
),
|
|
scheduled_tasks=_scheduled_tasks_feature(request, config),
|
|
# Startup-scoped: the read repository exists only with SQL persistence.
|
|
thread_activity=ThreadActivityFeature(available=thread_activity_available(request.app.state)),
|
|
)
|
|
|
|
|
|
def _scheduled_tasks_feature(request: Request, config: AppConfig) -> ScheduledTasksFeature:
|
|
# Repository and service are startup-scoped (same rationale as mcp_tasks):
|
|
# report what this process actually started.
|
|
state = request.app.state
|
|
service = getattr(state, "scheduled_task_service", None)
|
|
running = bool(getattr(service, "is_running", False))
|
|
scheduler = getattr(config, "scheduler", None)
|
|
repository_available = getattr(state, "scheduled_task_repo", None) is not None
|
|
return ScheduledTasksFeature(
|
|
available=repository_available,
|
|
running=running,
|
|
tool_enabled=running and scheduler_tools_enabled(config),
|
|
min_interval_seconds=int(getattr(scheduler, "min_once_delay_seconds", 60)),
|
|
group_notifications=repository_available and bool(getattr(getattr(config, "channel_connections", None), "enabled", False)),
|
|
)
|
|
|
|
|
|
def _knowledge_scope_selection_enabled(config: AppConfig) -> bool:
|
|
"""Fail closed unless the effective knowledge_search entry is RAGFlow."""
|
|
settings = config.knowledge_base
|
|
if not settings.enabled or not settings.scope_selection_enabled:
|
|
return False
|
|
tool = config.get_tool_config("knowledge_search")
|
|
return tool is not None and tool.use == RAGFLOW_KNOWLEDGE_SEARCH_PROVIDER
|