Skip to content

Commit 47ba9af

Browse files
tptppclaude
andauthored
feat(channel): add cron job message delivery via DingTalk/Feishu channels (#142)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5ad23dc commit 47ba9af

30 files changed

Lines changed: 5364 additions & 58 deletions

File tree

packages/derisk-core/src/derisk/channel/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Channel configuration
77
- Channel message format
88
- Channel handler interface
9+
- Message router for agent integration
910
1011
Example:
1112
```python
@@ -31,11 +32,15 @@
3132
from .base import (
3233
ChannelCapabilities,
3334
ChannelConfig,
35+
ChannelConnectionState,
3436
ChannelHandler,
3537
ChannelMessage,
3638
ChannelSender,
3739
ChannelType,
40+
SendMessageResult,
3841
)
42+
from .registry import ChannelHandlerRegistry
43+
from .router import ChannelMessageRouter
3944
from .schemas import DingTalkConfig, FeishuConfig
4045

4146
__all__ = [
@@ -45,9 +50,15 @@
4550
"ChannelSender",
4651
"ChannelMessage",
4752
"ChannelCapabilities",
53+
"ChannelConnectionState",
54+
"SendMessageResult",
4855
# Interfaces
4956
"ChannelHandler",
57+
# Registry
58+
"ChannelHandlerRegistry",
59+
# Router
60+
"ChannelMessageRouter",
5061
# Platform configs
5162
"DingTalkConfig",
5263
"FeishuConfig",
53-
]
64+
]

packages/derisk-core/src/derisk/channel/base.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,39 @@
1212
from derisk._private.pydantic import BaseModel, ConfigDict, Field
1313

1414

15+
class ChannelConnectionState(str, Enum):
16+
"""Channel connection state enumeration."""
17+
18+
DISCONNECTED = "disconnected"
19+
CONNECTING = "connecting"
20+
CONNECTED = "connected"
21+
ERROR = "error"
22+
RECONNECTING = "reconnecting"
23+
24+
25+
class SendMessageResult(BaseModel):
26+
"""Result of sending a message through a channel."""
27+
28+
model_config = ConfigDict(title="SendMessageResult")
29+
30+
success: bool = Field(
31+
...,
32+
description="Whether the message was sent successfully",
33+
)
34+
message_id: Optional[str] = Field(
35+
default=None,
36+
description="Message ID returned by the channel platform",
37+
)
38+
error: Optional[str] = Field(
39+
default=None,
40+
description="Error message if sending failed",
41+
)
42+
timestamp: Optional[datetime] = Field(
43+
default=None,
44+
description="Timestamp when the message was sent",
45+
)
46+
47+
1548
class ChannelType(str, Enum):
1649
"""Channel type enumeration."""
1750

@@ -83,7 +116,7 @@ class ChannelCapabilities(BaseModel):
83116
class ChannelConfig(BaseModel):
84117
"""Base channel configuration."""
85118

86-
model_config = ConfigDict(title="ChannelConfig")
119+
model_config = ConfigDict(title="ChannelConfig", extra="allow")
87120

88121
enabled: bool = Field(
89122
default=True,
@@ -97,6 +130,10 @@ class ChannelConfig(BaseModel):
97130
default=None,
98131
description="Channel description",
99132
)
133+
platform_config: Optional[Dict[str, Any]] = Field(
134+
default=None,
135+
description="Platform-specific configuration (FeishuConfig, DingTalkConfig, etc.)",
136+
)
100137

101138

102139
class ChannelMessage(BaseModel):
@@ -161,6 +198,88 @@ class ChannelHandler(ABC):
161198
They handle message processing, signature validation, and capability reporting.
162199
"""
163200

201+
def __init__(self, channel_id: str, config: "ChannelConfig"):
202+
"""Initialize the channel handler.
203+
204+
Args:
205+
channel_id: The unique identifier for this channel.
206+
config: The channel configuration.
207+
"""
208+
self._channel_id = channel_id
209+
self._config = config
210+
self._connection_state: ChannelConnectionState = (
211+
ChannelConnectionState.DISCONNECTED
212+
)
213+
214+
@classmethod
215+
def get_default_capabilities(cls) -> ChannelCapabilities:
216+
"""Get default capabilities for this channel type.
217+
218+
This class method can be called without instantiating the handler.
219+
Subclasses should override this to return their specific capabilities.
220+
221+
Returns:
222+
ChannelCapabilities instance with default values.
223+
"""
224+
return ChannelCapabilities()
225+
226+
@property
227+
def channel_id(self) -> str:
228+
"""Get the channel ID."""
229+
return self._channel_id
230+
231+
@property
232+
def connection_state(self) -> ChannelConnectionState:
233+
"""Get the current connection state."""
234+
return self._connection_state
235+
236+
@abstractmethod
237+
async def start(self) -> None:
238+
"""Start the channel handler.
239+
240+
This should establish the connection to the platform (WebSocket, Stream, etc.)
241+
and start listening for incoming messages.
242+
"""
243+
pass
244+
245+
@abstractmethod
246+
async def stop(self) -> None:
247+
"""Stop the channel handler.
248+
249+
This should close all connections and stop listening for messages.
250+
"""
251+
pass
252+
253+
@abstractmethod
254+
async def send_message(
255+
self,
256+
receiver_id: str,
257+
content: str,
258+
content_type: str = "text",
259+
**kwargs,
260+
) -> SendMessageResult:
261+
"""Send a message to a receiver through this channel.
262+
263+
Args:
264+
receiver_id: The ID of the receiver (user or group).
265+
content: The message content.
266+
content_type: The content type (text, markdown, etc.).
267+
**kwargs: Additional parameters (reply_to, mentions, etc.).
268+
269+
Returns:
270+
SendMessageResult indicating success or failure.
271+
"""
272+
pass
273+
274+
@abstractmethod
275+
def get_connection_url(self) -> Optional[str]:
276+
"""Get the webhook URL for setting up the channel integration.
277+
278+
Returns:
279+
The webhook URL if applicable, None if using WebSocket/Stream mode.
280+
"""
281+
pass
282+
164283
@abstractmethod
165284
async def process_message(
166285
self,
@@ -220,4 +339,4 @@ async def test_connection(self, channel_id: str) -> bool:
220339
Returns:
221340
True if the connection is successful, False otherwise.
222341
"""
223-
pass
342+
pass

0 commit comments

Comments
 (0)