mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +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>
88 lines
3.7 KiB
Python
88 lines
3.7 KiB
Python
"""SDK-free checks for explicit group notification destinations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
|
|
|
|
def notification_group_display_name(metadata: object) -> str | None:
|
|
"""Read the bounded label from the owner's browser-issued binding state."""
|
|
if not isinstance(metadata, Mapping):
|
|
return None
|
|
value = metadata.get("notification_group_name")
|
|
if not isinstance(value, str):
|
|
return None
|
|
return " ".join(value.split())[:256] or None
|
|
|
|
|
|
def is_feishu_notification_group(*, provider: object, status: object, workspace_id: object, metadata: object) -> bool:
|
|
"""Check the Feishu group shape; callers must separately verify owner and ID."""
|
|
return (
|
|
provider == "feishu" and status == "connected" and isinstance(workspace_id, str) and bool(workspace_id.strip()) and isinstance(metadata, Mapping) and metadata.get("chat_type") == "group" and metadata.get("chat_id") == workspace_id
|
|
)
|
|
|
|
|
|
def qq_notification_group_openid(
|
|
*,
|
|
provider: object,
|
|
status: object,
|
|
workspace_id: object,
|
|
metadata: object,
|
|
app_id: str | None = None,
|
|
) -> str | None:
|
|
"""Return the bound QQ ``group_openid`` when the connection is a valid QQ group binding."""
|
|
if provider != "qq" or status != "connected" or not isinstance(workspace_id, str) or ":group:" not in workspace_id:
|
|
return None
|
|
bound_app_id, _sep, group_openid = workspace_id.partition(":group:")
|
|
if not bound_app_id.strip() or bound_app_id != bound_app_id.strip() or not group_openid.strip() or group_openid != group_openid.strip():
|
|
return None
|
|
if app_id is not None and bound_app_id != app_id:
|
|
return None
|
|
if not isinstance(metadata, Mapping) or metadata.get("chat_type") != "group":
|
|
return None
|
|
meta_openid = metadata.get("group_openid")
|
|
if meta_openid is not None and (not isinstance(meta_openid, str) or meta_openid != group_openid):
|
|
return None
|
|
return group_openid
|
|
|
|
|
|
def wecom_notification_group_chat_id(
|
|
*,
|
|
provider: object,
|
|
status: object,
|
|
workspace_id: object,
|
|
metadata: object,
|
|
bot_id: str | None = None,
|
|
) -> str | None:
|
|
"""Return the bound WeCom ``chatid`` when the connection is a valid WeCom group binding."""
|
|
if provider != "wecom" or status != "connected" or not isinstance(workspace_id, str) or not isinstance(metadata, Mapping):
|
|
return None
|
|
if metadata.get("chattype") != "group":
|
|
return None
|
|
chatid = metadata.get("chatid")
|
|
if not isinstance(chatid, str) or not chatid.strip() or chatid != chatid.strip():
|
|
return None
|
|
aibotid = metadata.get("aibotid")
|
|
if aibotid is None or aibotid == "":
|
|
expected_workspace_id = f"group:{chatid}"
|
|
elif isinstance(aibotid, str) and bool(aibotid.strip()) and aibotid == aibotid.strip():
|
|
if bot_id and aibotid != bot_id:
|
|
return None
|
|
expected_workspace_id = f"{aibotid}:group:{chatid}"
|
|
else:
|
|
return None
|
|
if workspace_id != expected_workspace_id:
|
|
return None
|
|
return chatid
|
|
|
|
|
|
def is_notification_group(*, provider: object, status: object, workspace_id: object, metadata: object) -> bool:
|
|
"""Whether a channel connection record represents an eligible group notification target."""
|
|
if is_feishu_notification_group(provider=provider, status=status, workspace_id=workspace_id, metadata=metadata):
|
|
return True
|
|
if qq_notification_group_openid(provider=provider, status=status, workspace_id=workspace_id, metadata=metadata) is not None:
|
|
return True
|
|
if wecom_notification_group_chat_id(provider=provider, status=status, workspace_id=workspace_id, metadata=metadata) is not None:
|
|
return True
|
|
return False
|