mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 08:25:57 +00:00
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:
@@ -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}"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user