Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
43 changes: 43 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Test and coverage artifacts
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.tox/
.nox/

# Build and packaging artifacts
build/
dist/
*.egg-info/
*.egg
pip-wheel-metadata/

# Virtual environments
.venv/
venv/
env/
ENV/

# Local environment files
.env
.env.*
!.env.example

# Python tooling caches
.mypy_cache/
.ruff_cache/
.pyre/
.pytype/

# OS and editor noise
.DS_Store
.idea/
.vscode/
*.swp
*.swo
9 changes: 9 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
global-exclude */__pycache__/*
global-exclude *.pyc
global-exclude .pytest_cache/*
include LICENSE
include README.md
include README.zh.md
recursive-include docs *.md
recursive-include samples/channel *.py
prune tests
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Lark Channel SDK for Python

`lark-channel-sdk` is a Python package for building Feishu and Lark
conversational bots. It provides `FeishuChannel` as the main entry point for
event listening, message normalization, policy control, outbound messaging,
media handling, card callbacks, and streaming replies.

## Install

```bash
pip install lark-channel-sdk
```

## Minimal Example

```python
import asyncio
import os

from lark_channel import FeishuChannel

channel = FeishuChannel(
app_id=os.environ["LARK_APP_ID"],
app_secret=os.environ["LARK_APP_SECRET"],
)


async def on_message(msg):
await channel.send(
msg.chat_id,
{"markdown": f"received: {msg.content_text}"},
{"reply_to": msg.message_id},
)


channel.on("message", on_message)
asyncio.run(channel.connect())
```

## Documentation

- [Quickstart](docs/quickstart.md)
- [API reference](docs/reference.md)
- [Markdown messages](docs/markdown.md)
- [Webhook server adapter](docs/webhook-server.md)
- [CardKit streaming](docs/cardkit-streaming.md)
- [Deduplication architecture](docs/dedup-architecture.md)
- [Echo bot sample](samples/channel/echo_bot.py)

## Migration from `lark_oapi.channel`

Install the standalone package and update the import path:

```bash
pip install lark-channel-sdk
```

```python
from lark_channel import FeishuChannel
```

`lark-channel-sdk` can be installed alongside `lark-oapi`. Use
`lark-channel-sdk` for Channel bot workflows and keep using `lark-oapi` when
your application needs the full OpenAPI SDK surface.

## Local Development

```bash
pip install -e ".[test]"
pytest
```

## License

This project is licensed under the MIT License. See [LICENSE](LICENSE).
73 changes: 73 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Lark Channel Python SDK

`lark-channel-sdk` 是用于构建飞书和 Lark 会话式机器人的 Python 包。它以
`FeishuChannel` 为主要入口,封装事件监听、消息归一化、策略控制、出站消息、
媒体处理、卡片回调和流式回复。

## 安装

```bash
pip install lark-channel-sdk
```

## 最小示例

```python
import asyncio
import os

from lark_channel import FeishuChannel

channel = FeishuChannel(
app_id=os.environ["LARK_APP_ID"],
app_secret=os.environ["LARK_APP_SECRET"],
)


async def on_message(msg):
await channel.send(
msg.chat_id,
{"markdown": f"received: {msg.content_text}"},
{"reply_to": msg.message_id},
)


channel.on("message", on_message)
asyncio.run(channel.connect())
```

## 文档

- [快速开始](docs/quickstart.md)
- [API 参考](docs/reference.md)
- [Markdown 消息](docs/markdown.md)
- [Webhook 服务适配](docs/webhook-server.md)
- [CardKit 流式回复](docs/cardkit-streaming.md)
- [去重架构](docs/dedup-architecture.md)
- [Echo bot 示例](samples/channel/echo_bot.py)

## 从 `lark_oapi.channel` 迁移

安装独立包并更新 import 路径:

```bash
pip install lark-channel-sdk
```

```python
from lark_channel import FeishuChannel
```

`lark-channel-sdk` 可以与 `lark-oapi` 安装在同一环境中。Channel bot 工作流使用
`lark-channel-sdk`;如果应用需要完整 OpenAPI SDK 能力,继续使用 `lark-oapi`。

## 本地开发

```bash
pip install -e ".[test]"
pytest
```

## 许可证

本项目使用 MIT License,详见 [LICENSE](LICENSE)。
139 changes: 139 additions & 0 deletions docs/cardkit-streaming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Streaming with CardKit

For normal markdown streaming, prefer the high-level `channel.stream(...)`
helper:

```python
async def produce(stream):
for chunk in ["hello", " ", "world"]:
await stream.append(chunk)

await channel.stream(chat_id, {"markdown": produce}, {"reply_to": message_id})
```

`channel.stream(...)` owns CardKit preallocation, throttling, cancellation
handling, and the final `finish_streaming_card(...)` call. Use the lower-level
methods below only when you need custom CardKit control.

## Low-level APIs

| Method | Purpose |
|---|---|
| `await channel.create_card_instance(spec)` | Allocate a `card_id` from a card JSON spec |
| `await channel.send_card_by_reference(to, card_id, ...)` | Send a message that points to the preallocated card |
| `await channel.update_card_element_content(card_id, element_id, content, sequence)` | Patch one element's text during streaming |
| `await channel.finish_streaming_card(card_id, sequence)` | Close `streaming_mode` so users see the final card |

The high-level `channel.stream(..., {"markdown": producer}, ...)` path wraps
these methods through `MarkdownStreamController` and is the recommended public
API for token streaming.

## Required Permissions

A bot must have the required message and CardKit scopes enabled before calling
CardKit APIs. Scope names can vary by tenant UI; verify the exact names in the
Feishu developer console.

| Scope name (zh-CN) | Scope ID | Used by |
|---|---|---|
| 发送消息 | `im:message:send_as_bot` | `send_card_by_reference` |
| 获取与发送单聊、群组消息 | `im:message` | Inbound and outbound message operations |
| 创建卡片实体 | `cardkit:card:write` | `create_card_instance` |
| 更新卡片实体 | `cardkit:card` | `update_card_element_content`, `finish_streaming_card` |

If your tenant still exposes the legacy `cardkit:card:read` /
`cardkit:card:update` split, enable both. After changing scopes, re-install the
bot into the tenant; existing tokens do not pick up new scopes.

## Sequence Semantics

`update_card_element_content(card_id, element_id, content, sequence)` carries a
strictly increasing `sequence` number per `card_id`.

- The first patch must have `sequence >= 1`.
- Each subsequent patch must have `sequence >` the previous one.
- Gaps are allowed, for example `1, 3, 5`.
- `finish_streaming_card(card_id, sequence)` follows the same rule: its
`sequence` must exceed the largest sequence used in any update call for that
card.

Recommended pattern:

```python
seq = 0

async def patch(text):
nonlocal seq
seq += 1
await channel.update_card_element_content(card_id, "main", text, sequence=seq)

# ... stream tokens, calling patch() ...

seq += 1
await channel.finish_streaming_card(card_id, sequence=seq)
```

## `finish_streaming_card` vs `update_card`

These methods are not interchangeable.

| Method | When to use | What it does |
|---|---|---|
| `finish_streaming_card(card_id, sequence)` | Streaming output complete | Sets `config.streaming_mode = false` on the preallocated card. |
| `update_card(message_id, card)` | One-shot card replacement | Replaces the whole card payload of a sent message. Uses `message_id`, not `card_id`, and has no `sequence`. |

If you need to update a card after `finish_streaming_card`, use
`update_card(message_id, card)` with the `message_id` returned from
`send_card_by_reference(...)`, not the `card_id`.

## API Error Hints

Low-level CardKit methods raise `FeishuChannelError(code=unknown, ...)` for
non-zero CardKit responses in this release. Inspect the raw API response inside
the exception message and use the upstream API code as a troubleshooting hint.

| API code | Likely cause | Fix |
|---|---|---|
| `99991672` / `99991679` | Missing create-card scope | Add scope and re-install bot |
| `99991680` / `99991681` | Missing update-card scope | Add scope and re-install bot |
| `230099` | `sequence` regressed or was reused | Reset the stream and allocate a new `card_id` |
| `230001` | Card JSON spec malformed | Validate against CardKit 2.0 schema |
| `230002` / `230020` | Card already finished, or message recalled | Allocate a new `card_id` |

## Core Flow Example

This snippet focuses on the CardKit calls. It assumes `channel` is already
connected and `chat_id` is known.

```python
card_id = await channel.create_card_instance({
"schema": "2.0",
"config": {"streaming_mode": True, "summary": {"content": ""}},
"body": {
"elements": [
{"tag": "markdown", "element_id": "main", "content": "..."},
],
},
})

send_result = await channel.send_card_by_reference(chat_id, card_id)
if not send_result.success:
raise RuntimeError(send_result.error)

seq = 0
accumulated = ""
for token in ["hello", " ", "world"]:
accumulated += token
seq += 1
await channel.update_card_element_content(
card_id,
"main",
accumulated,
sequence=seq,
)

seq += 1
await channel.finish_streaming_card(card_id, sequence=seq)
```

Return to the [project README](../README.md).
Loading