mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 21:50:36 +00:00
* fix(channels): keep IM binding codes on screen and report goal state in replies Connecting Slack, Discord, Feishu, DingTalk, WeCom, Buzz or QQ now opens a dialog that keeps the /connect command on screen with a copy button, its expiry time and the binding status, instead of a toast that disappears. The bots' /connect replies and the IM /goal replies now use the owner's web UI language (else channel_connections.notification_locale), and /goal reports the goal's state as the web goal bar does: in progress, why it stopped, or that it was met. * docs(changelog): note the IM binding dialog and localized IM replies (#7591) * test(channels): check IM reply tables against every notification locale The connect-reply check compared a fixed en-US/zh-CN pair, and the /goal reply table had none. Both now match NOTIFICATION_LOCALES with the same keys in each, like the existing _GOAL_REASON_TEXT check, so a new locale fails here instead of raising KeyError at reply time. --------- Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
71 lines
3.1 KiB
Python
71 lines
3.1 KiB
Python
"""Session-authenticated, owner-scoped UI preferences."""
|
|
|
|
from typing import Annotated, Literal
|
|
|
|
from fastapi import APIRouter, Header, HTTPException, Request, Response
|
|
from pydantic import BaseModel, ConfigDict, StringConstraints, ValidationError
|
|
|
|
from app.gateway.auth_disabled import AUTH_SOURCE_SESSION
|
|
from app.gateway.deps import get_current_user_from_request
|
|
from deerflow.config.channel_connections_config import NotificationLocale
|
|
from deerflow.persistence.engine import get_session_factory
|
|
from deerflow.persistence.user.preferences import UserPreferencesRepository
|
|
|
|
router = APIRouter(prefix="/api/v1/auth/preferences", tags=["auth"])
|
|
|
|
|
|
class Preferences(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", strict=True)
|
|
notification_enabled: bool | None = None
|
|
model_name: Annotated[str, StringConstraints(max_length=200)] | None = None
|
|
mode: Literal["flash", "thinking", "pro", "ultra"] | None = None
|
|
# Provider vocabularies differ (issue #5073): accept any effort token so a
|
|
# model-specific default such as ``max`` can be remembered. The UI checks
|
|
# remembered values against the selected model; the factory enforces
|
|
# declared contracts while preserving legacy direct-request behavior.
|
|
reasoning_effort: Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Za-z0-9_.-]+$")] | None = None
|
|
# The web app's UI language, synced on sign-in and on every language
|
|
# switch. Scheduled-task IM notices and the IM /connect and /goal replies
|
|
# are written in it; without it they use
|
|
# ``channel_connections.notification_locale``.
|
|
locale: NotificationLocale | None = None
|
|
|
|
|
|
async def _owner(request: Request, expected_user: str) -> str:
|
|
user = await get_current_user_from_request(request)
|
|
if getattr(request.state, "auth_source", None) != AUTH_SOURCE_SESSION:
|
|
raise HTTPException(403, "Preferences require an authenticated browser session")
|
|
owner = str(user.id)
|
|
if owner != expected_user:
|
|
raise HTTPException(409, "The signed-in account changed; reload this page")
|
|
return owner
|
|
|
|
|
|
def _repository() -> UserPreferencesRepository:
|
|
sessions = get_session_factory()
|
|
if sessions is None:
|
|
raise HTTPException(503, "Preference persistence is unavailable")
|
|
return UserPreferencesRepository(sessions)
|
|
|
|
|
|
@router.get("", response_model=Preferences)
|
|
async def get_preferences(request: Request, x_expected_user_id: str = Header(...)) -> Preferences:
|
|
owner = await _owner(request, x_expected_user_id)
|
|
stored = await _repository().get(owner)
|
|
# An invalid optional cosmetic setting must not hide the rest of the page.
|
|
valid = {}
|
|
for key, value in stored.items():
|
|
try:
|
|
Preferences.model_validate({key: value})
|
|
except ValidationError:
|
|
continue
|
|
valid[key] = value
|
|
return Preferences(**valid)
|
|
|
|
|
|
@router.patch("", status_code=204, response_class=Response)
|
|
async def patch_preferences(body: Preferences, request: Request, x_expected_user_id: str = Header(...)) -> Response:
|
|
owner = await _owner(request, x_expected_user_id)
|
|
await _repository().patch(owner, body.model_dump(exclude_unset=True))
|
|
return Response(status_code=204)
|