mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-20 12:54:45 +08:00
feat: add IM channels for Feishu, Slack, and Telegram (#1010)
* feat: add IM channels system for Feishu, Slack, and Telegram integration Bridge external messaging platforms to DeerFlow via LangGraph Server with async message bus, thread management, and per-channel configuration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review comments on IM channels system Fix topic_id handling in store remove/list_entries and manager commands, correct Telegram reply threading, remove unused imports/variables, update docstrings and docs to match implementation, and prevent config mutation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * update skill creator * fix im reply text * fix comments --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ from src.gateway.routers.uploads import UploadResponse
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app_config():
|
||||
"""Provide a minimal AppConfig mock."""
|
||||
@@ -45,6 +46,7 @@ def client(mock_app_config):
|
||||
# __init__
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestClientInit:
|
||||
def test_default_params(self, client):
|
||||
assert client._model_name is None
|
||||
@@ -86,6 +88,7 @@ class TestClientInit:
|
||||
# list_models / list_skills / get_memory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestConfigQueries:
|
||||
def test_list_models(self, client):
|
||||
result = client.list_models()
|
||||
@@ -135,6 +138,7 @@ class TestConfigQueries:
|
||||
# stream / chat
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_agent_mock(chunks: list[dict]):
|
||||
"""Create a mock agent whose .stream() yields the given chunks."""
|
||||
agent = MagicMock()
|
||||
@@ -314,6 +318,7 @@ class TestChat:
|
||||
# _extract_text
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestExtractText:
|
||||
def test_string(self):
|
||||
assert DeerFlowClient._extract_text("hello") == "hello"
|
||||
@@ -340,6 +345,7 @@ class TestExtractText:
|
||||
# _ensure_agent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEnsureAgent:
|
||||
def test_creates_agent(self, client):
|
||||
"""_ensure_agent creates an agent on first call."""
|
||||
@@ -374,6 +380,7 @@ class TestEnsureAgent:
|
||||
# get_model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetModel:
|
||||
def test_found(self, client):
|
||||
model_cfg = MagicMock()
|
||||
@@ -402,6 +409,7 @@ class TestGetModel:
|
||||
# MCP config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMcpConfig:
|
||||
def test_get_mcp_config(self, client):
|
||||
server = MagicMock()
|
||||
@@ -457,6 +465,7 @@ class TestMcpConfig:
|
||||
# Skills management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSkillsManagement:
|
||||
def _make_skill(self, name="test-skill", enabled=True):
|
||||
s = MagicMock()
|
||||
@@ -556,6 +565,7 @@ class TestSkillsManagement:
|
||||
# Memory management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMemoryManagement:
|
||||
def test_reload_memory(self, client):
|
||||
data = {"version": "1.0", "facts": []}
|
||||
@@ -605,6 +615,7 @@ class TestMemoryManagement:
|
||||
# Uploads
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestUploads:
|
||||
def test_upload_files(self, client):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -678,6 +689,7 @@ class TestUploads:
|
||||
# Artifacts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestArtifacts:
|
||||
def test_get_artifact(self, client):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -759,9 +771,13 @@ class TestScenarioMultiTurnConversation:
|
||||
|
||||
def test_stream_collects_all_event_types_across_turns(self, client):
|
||||
"""A full turn emits messages-tuple (tool_call, tool_result, ai text) + values + end."""
|
||||
ai_tc = AIMessage(content="", id="ai-1", tool_calls=[
|
||||
{"name": "web_search", "args": {"query": "LangGraph"}, "id": "tc-1"},
|
||||
])
|
||||
ai_tc = AIMessage(
|
||||
content="",
|
||||
id="ai-1",
|
||||
tool_calls=[
|
||||
{"name": "web_search", "args": {"query": "LangGraph"}, "id": "tc-1"},
|
||||
],
|
||||
)
|
||||
tool_r = ToolMessage(content="LangGraph is a framework...", id="tm-1", tool_call_id="tc-1", name="web_search")
|
||||
ai_final = AIMessage(content="LangGraph is a framework for building agents.", id="ai-2")
|
||||
|
||||
@@ -809,13 +825,21 @@ class TestScenarioToolChain:
|
||||
|
||||
def test_multi_tool_chain(self, client):
|
||||
"""Agent calls bash → reads output → calls write_file → responds."""
|
||||
ai_bash = AIMessage(content="", id="ai-1", tool_calls=[
|
||||
{"name": "bash", "args": {"cmd": "ls /mnt/user-data/workspace"}, "id": "tc-1"},
|
||||
])
|
||||
ai_bash = AIMessage(
|
||||
content="",
|
||||
id="ai-1",
|
||||
tool_calls=[
|
||||
{"name": "bash", "args": {"cmd": "ls /mnt/user-data/workspace"}, "id": "tc-1"},
|
||||
],
|
||||
)
|
||||
bash_result = ToolMessage(content="README.md\nsrc/", id="tm-1", tool_call_id="tc-1", name="bash")
|
||||
ai_write = AIMessage(content="", id="ai-2", tool_calls=[
|
||||
{"name": "write_file", "args": {"path": "/mnt/user-data/outputs/listing.txt", "content": "README.md\nsrc/"}, "id": "tc-2"},
|
||||
])
|
||||
ai_write = AIMessage(
|
||||
content="",
|
||||
id="ai-2",
|
||||
tool_calls=[
|
||||
{"name": "write_file", "args": {"path": "/mnt/user-data/outputs/listing.txt", "content": "README.md\nsrc/"}, "id": "tc-2"},
|
||||
],
|
||||
)
|
||||
write_result = ToolMessage(content="File written successfully.", id="tm-2", tool_call_id="tc-2", name="write_file")
|
||||
ai_final = AIMessage(content="I listed the workspace and saved the output.", id="ai-3")
|
||||
|
||||
@@ -862,10 +886,13 @@ class TestScenarioFileLifecycle:
|
||||
|
||||
with patch.object(DeerFlowClient, "_get_uploads_dir", return_value=uploads_dir):
|
||||
# Step 1: Upload
|
||||
result = client.upload_files("t-lifecycle", [
|
||||
tmp_path / "report.txt",
|
||||
tmp_path / "data.csv",
|
||||
])
|
||||
result = client.upload_files(
|
||||
"t-lifecycle",
|
||||
[
|
||||
tmp_path / "report.txt",
|
||||
tmp_path / "data.csv",
|
||||
],
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert len(result["files"]) == 2
|
||||
assert {f["filename"] for f in result["files"]} == {"report.txt", "data.csv"}
|
||||
@@ -1166,10 +1193,13 @@ class TestScenarioMemoryWorkflow:
|
||||
def test_memory_full_lifecycle(self, client):
|
||||
"""get_memory → reload → get_status covers the full memory API."""
|
||||
initial_data = {"version": "1.0", "facts": [{"id": "f1", "content": "User likes Python"}]}
|
||||
updated_data = {"version": "1.0", "facts": [
|
||||
{"id": "f1", "content": "User likes Python"},
|
||||
{"id": "f2", "content": "User prefers dark mode"},
|
||||
]}
|
||||
updated_data = {
|
||||
"version": "1.0",
|
||||
"facts": [
|
||||
{"id": "f1", "content": "User likes Python"},
|
||||
{"id": "f2", "content": "User prefers dark mode"},
|
||||
],
|
||||
}
|
||||
|
||||
config = MagicMock()
|
||||
config.enabled = True
|
||||
@@ -1208,9 +1238,7 @@ class TestScenarioSkillInstallAndUse:
|
||||
# Create .skill archive
|
||||
skill_src = tmp_path / "my-analyzer"
|
||||
skill_src.mkdir()
|
||||
(skill_src / "SKILL.md").write_text(
|
||||
"---\nname: my-analyzer\ndescription: Analyze code\nlicense: MIT\n---\nAnalysis skill"
|
||||
)
|
||||
(skill_src / "SKILL.md").write_text("---\nname: my-analyzer\ndescription: Analyze code\nlicense: MIT\n---\nAnalysis skill")
|
||||
archive = tmp_path / "my-analyzer.skill"
|
||||
with zipfile.ZipFile(archive, "w") as zf:
|
||||
zf.write(skill_src / "SKILL.md", "my-analyzer/SKILL.md")
|
||||
@@ -1319,11 +1347,15 @@ class TestScenarioEdgeCases:
|
||||
|
||||
def test_concurrent_tool_calls_in_single_message(self, client):
|
||||
"""Agent produces multiple tool_calls in one AIMessage — emitted as single messages-tuple."""
|
||||
ai = AIMessage(content="", id="ai-1", tool_calls=[
|
||||
{"name": "web_search", "args": {"q": "a"}, "id": "tc-1"},
|
||||
{"name": "web_search", "args": {"q": "b"}, "id": "tc-2"},
|
||||
{"name": "bash", "args": {"cmd": "echo hi"}, "id": "tc-3"},
|
||||
])
|
||||
ai = AIMessage(
|
||||
content="",
|
||||
id="ai-1",
|
||||
tool_calls=[
|
||||
{"name": "web_search", "args": {"q": "a"}, "id": "tc-1"},
|
||||
{"name": "web_search", "args": {"q": "b"}, "id": "tc-2"},
|
||||
{"name": "bash", "args": {"cmd": "echo hi"}, "id": "tc-3"},
|
||||
],
|
||||
)
|
||||
chunks = [{"messages": [ai]}]
|
||||
agent = _make_agent_mock(chunks)
|
||||
|
||||
@@ -1367,6 +1399,7 @@ class TestScenarioEdgeCases:
|
||||
# Gateway conformance — validate client output against Gateway Pydantic models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGatewayConformance:
|
||||
"""Validate that DeerFlowClient return dicts conform to Gateway Pydantic response models.
|
||||
|
||||
@@ -1441,9 +1474,7 @@ class TestGatewayConformance:
|
||||
def test_install_skill(self, client, tmp_path):
|
||||
skill_dir = tmp_path / "my-skill"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\nname: my-skill\ndescription: A test skill\n---\nBody\n"
|
||||
)
|
||||
(skill_dir / "SKILL.md").write_text("---\nname: my-skill\ndescription: A test skill\n---\nBody\n")
|
||||
|
||||
archive = tmp_path / "my-skill.skill"
|
||||
with zipfile.ZipFile(archive, "w") as zf:
|
||||
|
||||
Reference in New Issue
Block a user