refactor(channels): update IM channels for new runtime architecture

Update app/channels/:
- feishu.py - adapt to new run streaming APIs
- manager.py - update channel manager for new runtime

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rayhpeng
2026-04-22 11:30:08 +08:00
parent b5e18f5b47
commit 892a06fe98
2 changed files with 115 additions and 78 deletions
+29 -7
View File
@@ -9,11 +9,12 @@ import re
import threading import threading
from typing import Any, Literal from typing import Any, Literal
from app.plugins.auth.security.actor_context import bind_user_actor_context
from app.channels.base import Channel from app.channels.base import Channel
from app.channels.commands import KNOWN_CHANNEL_COMMANDS from app.channels.commands import KNOWN_CHANNEL_COMMANDS
from app.channels.message_bus import InboundMessage, InboundMessageType, MessageBus, OutboundMessage, ResolvedAttachment from app.channels.message_bus import InboundMessage, InboundMessageType, MessageBus, OutboundMessage, ResolvedAttachment
from deerflow.config.paths import VIRTUAL_PATH_PREFIX, get_paths from deerflow.config.paths import VIRTUAL_PATH_PREFIX, get_paths
from deerflow.runtime.user_context import get_effective_user_id from deerflow.runtime.actor_context import get_effective_user_id
from deerflow.sandbox.sandbox_provider import get_sandbox_provider from deerflow.sandbox.sandbox_provider import get_sandbox_provider
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -298,15 +299,35 @@ class FeishuChannel(Channel):
text = msg.text text = msg.text
for file in files: for file in files:
if file.get("image_key"): if file.get("image_key"):
virtual_path = await self._receive_single_file(msg.thread_ts, file["image_key"], "image", thread_id) virtual_path = await self._receive_single_file(
msg.thread_ts,
file["image_key"],
"image",
thread_id,
user_id=msg.user_id,
)
text = text.replace("[image]", virtual_path, 1) text = text.replace("[image]", virtual_path, 1)
elif file.get("file_key"): elif file.get("file_key"):
virtual_path = await self._receive_single_file(msg.thread_ts, file["file_key"], "file", thread_id) virtual_path = await self._receive_single_file(
msg.thread_ts,
file["file_key"],
"file",
thread_id,
user_id=msg.user_id,
)
text = text.replace("[file]", virtual_path, 1) text = text.replace("[file]", virtual_path, 1)
msg.text = text msg.text = text
return msg return msg
async def _receive_single_file(self, message_id: str, file_key: str, type: Literal["image", "file"], thread_id: str) -> str: async def _receive_single_file(
self,
message_id: str,
file_key: str,
type: Literal["image", "file"],
thread_id: str,
*,
user_id: str | None = None,
) -> str:
request = self._GetMessageResourceRequest.builder().message_id(message_id).file_key(file_key).type(type).build() request = self._GetMessageResourceRequest.builder().message_id(message_id).file_key(file_key).type(type).build()
def inner(): def inner():
@@ -345,9 +366,10 @@ class FeishuChannel(Channel):
return f"Failed to obtain the [{type}]" return f"Failed to obtain the [{type}]"
paths = get_paths() paths = get_paths()
user_id = get_effective_user_id() with bind_user_actor_context(user_id):
paths.ensure_thread_dirs(thread_id, user_id=user_id) effective_user_id = get_effective_user_id()
uploads_dir = paths.sandbox_uploads_dir(thread_id, user_id=user_id).resolve() paths.ensure_thread_dirs(thread_id, user_id=effective_user_id)
uploads_dir = paths.sandbox_uploads_dir(thread_id, user_id=effective_user_id).resolve()
ext = "png" if type == "image" else "bin" ext = "png" if type == "image" else "bin"
raw_filename = getattr(response, "file_name", "") or f"feishu_{file_key[-12:]}.{ext}" raw_filename = getattr(response, "file_name", "") or f"feishu_{file_key[-12:]}.{ext}"
+23 -8
View File
@@ -14,10 +14,11 @@ from typing import Any
import httpx import httpx
from langgraph_sdk.errors import ConflictError from langgraph_sdk.errors import ConflictError
from app.plugins.auth.security.actor_context import bind_user_actor_context
from app.channels.commands import KNOWN_CHANNEL_COMMANDS from app.channels.commands import KNOWN_CHANNEL_COMMANDS
from app.channels.message_bus import InboundMessage, InboundMessageType, MessageBus, OutboundMessage, ResolvedAttachment from app.channels.message_bus import InboundMessage, InboundMessageType, MessageBus, OutboundMessage, ResolvedAttachment
from app.channels.store import ChannelStore from app.channels.store import ChannelStore
from deerflow.runtime.user_context import get_effective_user_id from deerflow.runtime.actor_context import get_effective_user_id
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -328,7 +329,7 @@ def _format_artifact_text(artifacts: list[str]) -> str:
_OUTPUTS_VIRTUAL_PREFIX = "/mnt/user-data/outputs/" _OUTPUTS_VIRTUAL_PREFIX = "/mnt/user-data/outputs/"
def _resolve_attachments(thread_id: str, artifacts: list[str]) -> list[ResolvedAttachment]: def _resolve_attachments(thread_id: str, artifacts: list[str], *, user_id: str | None = None) -> list[ResolvedAttachment]:
"""Resolve virtual artifact paths to host filesystem paths with metadata. """Resolve virtual artifact paths to host filesystem paths with metadata.
Only paths under ``/mnt/user-data/outputs/`` are accepted; any other Only paths under ``/mnt/user-data/outputs/`` are accepted; any other
@@ -342,15 +343,16 @@ def _resolve_attachments(thread_id: str, artifacts: list[str]) -> list[ResolvedA
attachments: list[ResolvedAttachment] = [] attachments: list[ResolvedAttachment] = []
paths = get_paths() paths = get_paths()
user_id = get_effective_user_id() with bind_user_actor_context(user_id):
outputs_dir = paths.sandbox_outputs_dir(thread_id, user_id=user_id).resolve() effective_user_id = get_effective_user_id()
outputs_dir = paths.sandbox_outputs_dir(thread_id, user_id=effective_user_id).resolve()
for virtual_path in artifacts: for virtual_path in artifacts:
# Security: only allow files from the agent outputs directory # Security: only allow files from the agent outputs directory
if not virtual_path.startswith(_OUTPUTS_VIRTUAL_PREFIX): if not virtual_path.startswith(_OUTPUTS_VIRTUAL_PREFIX):
logger.warning("[Manager] rejected non-outputs artifact path: %s", virtual_path) logger.warning("[Manager] rejected non-outputs artifact path: %s", virtual_path)
continue continue
try: try:
actual = paths.resolve_virtual_path(thread_id, virtual_path, user_id=user_id) actual = paths.resolve_virtual_path(thread_id, virtual_path, user_id=effective_user_id)
# Verify the resolved path is actually under the outputs directory # Verify the resolved path is actually under the outputs directory
# (guards against path-traversal even after prefix check) # (guards against path-traversal even after prefix check)
try: try:
@@ -382,13 +384,15 @@ def _prepare_artifact_delivery(
thread_id: str, thread_id: str,
response_text: str, response_text: str,
artifacts: list[str], artifacts: list[str],
*,
user_id: str | None = None,
) -> tuple[str, list[ResolvedAttachment]]: ) -> tuple[str, list[ResolvedAttachment]]:
"""Resolve attachments and append filename fallbacks to the text response.""" """Resolve attachments and append filename fallbacks to the text response."""
attachments: list[ResolvedAttachment] = [] attachments: list[ResolvedAttachment] = []
if not artifacts: if not artifacts:
return response_text, attachments return response_text, attachments
attachments = _resolve_attachments(thread_id, artifacts) attachments = _resolve_attachments(thread_id, artifacts, user_id=user_id)
resolved_virtuals = {attachment.virtual_path for attachment in attachments} resolved_virtuals = {attachment.virtual_path for attachment in attachments}
unresolved = [path for path in artifacts if path not in resolved_virtuals] unresolved = [path for path in artifacts if path not in resolved_virtuals]
@@ -411,6 +415,7 @@ async def _ingest_inbound_files(thread_id: str, msg: InboundMessage) -> list[dic
from deerflow.uploads.manager import claim_unique_filename, ensure_uploads_dir, normalize_filename from deerflow.uploads.manager import claim_unique_filename, ensure_uploads_dir, normalize_filename
with bind_user_actor_context(msg.user_id):
uploads_dir = ensure_uploads_dir(thread_id) uploads_dir = ensure_uploads_dir(thread_id)
seen_names = {entry.name for entry in uploads_dir.iterdir() if entry.is_file()} seen_names = {entry.name for entry in uploads_dir.iterdir() if entry.is_file()}
@@ -745,7 +750,12 @@ class ChannelManager:
len(artifacts), len(artifacts),
) )
response_text, attachments = _prepare_artifact_delivery(thread_id, response_text, artifacts) response_text, attachments = _prepare_artifact_delivery(
thread_id,
response_text,
artifacts,
user_id=msg.user_id,
)
if not response_text: if not response_text:
if attachments: if attachments:
@@ -836,7 +846,12 @@ class ChannelManager:
result = last_values if last_values is not None else {"messages": [{"type": "ai", "content": latest_text}]} result = last_values if last_values is not None else {"messages": [{"type": "ai", "content": latest_text}]}
response_text = _extract_response_text(result) response_text = _extract_response_text(result)
artifacts = _extract_artifacts(result) artifacts = _extract_artifacts(result)
response_text, attachments = _prepare_artifact_delivery(thread_id, response_text, artifacts) response_text, attachments = _prepare_artifact_delivery(
thread_id,
response_text,
artifacts,
user_id=msg.user_id,
)
if not response_text: if not response_text:
if attachments: if attachments: