Skip to content

Commit 93e7f76

Browse files
committed
Add Feishu channel extension support
1 parent bf3c45d commit 93e7f76

10 files changed

Lines changed: 531 additions & 1 deletion

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ res = asyncio.run(agent.run("hello!"))
8080
print(res)
8181
```
8282

83+
## Feishu bot channel
84+
85+
VeADK now provides `veadk.extensions.FeishuChannelExtension` for bridging a Feishu bot with a `Runner`. It maps `union_id` to `user_id`, and `thread_id` / `chat_id` to `session_id`, so VeADK memory and tracing can work directly in Feishu conversations.
86+
87+
```python
88+
from veadk import Agent, Runner
89+
from veadk.extensions import FeishuChannelExtension
90+
91+
agent = Agent()
92+
runner = Runner(agent=agent, app_name="feishu_demo")
93+
channel = FeishuChannelExtension(runner=runner)
94+
```
95+
96+
Configure credentials with `TOOL_FEISHU_CHANNEL_APP_ID` and `TOOL_FEISHU_CHANNEL_APP_SECRET`, or in `config.yaml` under `tool.feishu_channel`.
97+
8398
## Command line tools
8499

85100
VeADK provides several useful command line tools for faster deployment and optimization, such as:

config.yaml.full

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ tool:
5656
endpoint: # `app_id`
5757
api_key: # `app_secret`
5858
token: # `user_token`
59+
# [optional] for Feishu bot channel extension based on lark_oapi.channel
60+
feishu_channel:
61+
app_id:
62+
app_secret:
63+
transport: ws # `ws` | `webhook`
5964
# [optional] for Volcengine Lake AI Service https://www.volcengine.com/product/las
6065
mobile_use:
6166
tool_id: #https://console.volcengine.com/ACEP

docs/docs/configuration.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ volcengine:
6060
| Lark | `TOOL_LARK_ENDPOINT` | Lark 应用 ID(app_id) |
6161
| | `TOOL_LARK_API_KEY` | Lark 应用密钥(app_secret) |
6262
| | `TOOL_LARK_TOKEN` | Lark 用户 token |
63+
| Feishu Channel | `TOOL_FEISHU_CHANNEL_APP_ID` | 飞书机器人应用 ID(app_id) |
64+
| | `TOOL_FEISHU_CHANNEL_APP_SECRET` | 飞书机器人应用密钥(app_secret) |
65+
| | `TOOL_FEISHU_CHANNEL_TRANSPORT` | Channel 传输模式,默认 `ws` |
6366
| LAS | `TOOL_LAS_URL` | LAS SSE 服务地址(含 token) |
6467
| | `TOOL_LAS_DATASET_ID` | LAS 数据集 ID |
6568
| VOD | `TOOL_VOD_GROUPS` | 视频编辑能力组 |

docs/docs/tools/feishu-channel.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# 飞书 Channel 扩展
2+
3+
`veadk.extensions.FeishuChannelExtension` 用于把飞书机器人的入站消息桥接到 VeADK `Runner`
4+
5+
它默认基于 `lark_oapi.channel.FeishuChannel``message` 事件工作,并按下面的规则映射会话身份:
6+
7+
- `sender.union_id -> Runner.user_id`
8+
- `conversation.thread_id -> Runner.session_id`
9+
- 如果线程 ID 不存在,则回退到 `chat_id -> Runner.session_id`
10+
11+
这样做的好处是,VeADK 现有的短期记忆、长期记忆、Tracing 和多租户隔离能力可以直接复用。
12+
13+
## 安装
14+
15+
```bash
16+
pip install veadk-python[extensions]
17+
```
18+
19+
如果你只想安装这个能力,也可以单独安装:
20+
21+
```bash
22+
pip install lark-oapi
23+
```
24+
25+
## 配置
26+
27+
环境变量:
28+
29+
- `TOOL_FEISHU_CHANNEL_APP_ID`
30+
- `TOOL_FEISHU_CHANNEL_APP_SECRET`
31+
- `TOOL_FEISHU_CHANNEL_TRANSPORT`,默认 `ws`
32+
33+
或在 `config.yaml` 中配置:
34+
35+
```yaml title="config.yaml"
36+
tool:
37+
feishu_channel:
38+
app_id: cli_xxx
39+
app_secret: xxx
40+
transport: ws
41+
```
42+
43+
## 最小示例
44+
45+
```python
46+
--8<-- "examples/channel/feishu_bot.py"
47+
```
48+
49+
## 说明
50+
51+
- 默认使用飞书 `Channel` 的 WebSocket 模式,因此只要机器人已订阅消息事件,就可以直接启动连接。
52+
- 默认回复会使用 `reply_to=原消息 message_id`,让 VeADK 输出继续挂在当前飞书消息线程下。
53+
- 你可以通过 `session_id_factory``user_id_factory` 覆盖默认映射逻辑。
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import asyncio
2+
3+
from veadk import Agent, Runner
4+
from veadk.extensions import FeishuChannelExtension
5+
from veadk.memory.short_term_memory import ShortTermMemory
6+
7+
agent = Agent(
8+
name="feishu_agent",
9+
instruction="你是一个通过飞书机器人与用户沟通的助手。",
10+
)
11+
12+
runner = Runner(
13+
agent=agent,
14+
app_name="veadk_feishu_demo",
15+
user_id="veadk_feishu_default_user",
16+
short_term_memory=ShortTermMemory(),
17+
)
18+
19+
channel = FeishuChannelExtension(
20+
runner=runner,
21+
channel_kwargs={
22+
"transport": "ws",
23+
},
24+
)
25+
26+
27+
async def main():
28+
await channel.connect()
29+
30+
31+
if __name__ == "__main__":
32+
asyncio.run(main())

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ nav:
5555
# - 最佳实践(记忆相关): memory/best-practice-memory.md
5656
- 连接能力源——工具:
5757
- 内置工具: tools/builtin.md
58+
- 飞书 Channel 扩展: tools/feishu-channel.md
5859
- 自定义工具: tools/function.md
5960
- 护栏工具: tools/guardrail.md
6061
- 连接数据源——知识库:

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ extensions = [
5353
"llama-index-vector-stores-redis>=0.6.1", # For Redis database
5454
"llama-index-vector-stores-opensearch==0.6.1", # For Opensearch database
5555
"opensearch-py==2.8.0",
56+
"lark-oapi",
5657
]
5758
database = [
5859
"redis>=5.0", # For Redis database
@@ -97,4 +98,4 @@ include-package-data = true
9798
exclude = [
9899
"veadk/integrations/ve_faas/template/*",
99100
"veadk/integrations/ve_faas/web_template/*"
100-
]
101+
]
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from types import SimpleNamespace
2+
3+
import pytest
4+
5+
from veadk.extensions.feishu_channel import FeishuChannelExtension
6+
7+
8+
class FakeChannel:
9+
def __init__(self):
10+
self.handlers = {}
11+
self.sent_messages = []
12+
13+
def on(self, event_name, handler):
14+
self.handlers[event_name] = handler
15+
16+
async def send(self, chat_id, body, options=None):
17+
self.sent_messages.append((chat_id, body, options))
18+
19+
20+
class FakeRunner:
21+
def __init__(self):
22+
self.calls = []
23+
24+
async def run(self, messages, user_id="", session_id="", **kwargs):
25+
self.calls.append(
26+
{
27+
"messages": messages,
28+
"user_id": user_id,
29+
"session_id": session_id,
30+
}
31+
)
32+
return f"echo:{messages}"
33+
34+
35+
def build_message(**overrides):
36+
message = SimpleNamespace(
37+
id="om_001",
38+
message_id="om_001",
39+
chat_id="oc_chat",
40+
chat_type="p2p",
41+
thread_id="",
42+
reply_to_message_id="",
43+
content_text="你好",
44+
sender_id="ou_sender",
45+
sender=SimpleNamespace(
46+
union_id="on_union",
47+
open_id="ou_sender",
48+
user_id="u_sender",
49+
),
50+
conversation=SimpleNamespace(
51+
chat_id="oc_chat",
52+
chat_type="p2p",
53+
thread_id="",
54+
),
55+
reply=SimpleNamespace(message_id=""),
56+
)
57+
for key, value in overrides.items():
58+
setattr(message, key, value)
59+
return message
60+
61+
62+
@pytest.mark.asyncio
63+
async def test_extension_uses_union_id_and_thread_id():
64+
runner = FakeRunner()
65+
channel = FakeChannel()
66+
extension = FeishuChannelExtension(runner=runner, channel=channel)
67+
68+
message = build_message(
69+
thread_id="thread_1",
70+
conversation=SimpleNamespace(
71+
chat_id="oc_chat",
72+
chat_type="group",
73+
thread_id="thread_1",
74+
),
75+
)
76+
77+
await extension._on_message(message)
78+
79+
assert runner.calls == [
80+
{
81+
"messages": "你好",
82+
"user_id": "on_union",
83+
"session_id": "thread_1",
84+
}
85+
]
86+
assert channel.sent_messages == [
87+
("oc_chat", {"text": "echo:你好"}, {"reply_to": "om_001"})
88+
]
89+
90+
91+
@pytest.mark.asyncio
92+
async def test_extension_falls_back_to_chat_id_when_thread_missing():
93+
runner = FakeRunner()
94+
channel = FakeChannel()
95+
extension = FeishuChannelExtension(runner=runner, channel=channel)
96+
97+
message = build_message(
98+
sender=SimpleNamespace(union_id="", open_id="ou_fallback", user_id="u_sender")
99+
)
100+
101+
await extension._on_message(message)
102+
103+
assert runner.calls[0]["user_id"] == "ou_fallback"
104+
assert runner.calls[0]["session_id"] == "oc_chat"
105+
106+
107+
@pytest.mark.asyncio
108+
async def test_extension_ignores_empty_message_by_default():
109+
runner = FakeRunner()
110+
channel = FakeChannel()
111+
extension = FeishuChannelExtension(runner=runner, channel=channel)
112+
113+
message = build_message(content_text=" ")
114+
115+
await extension._on_message(message)
116+
117+
assert runner.calls == []
118+
assert channel.sent_messages == []

veadk/extensions/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from veadk.extensions.feishu_channel import FeishuChannelExtension
16+
17+
__all__ = ["FeishuChannelExtension"]

0 commit comments

Comments
 (0)