Metadata-Version: 2.4
Name: dfcode-remote
Version: 0.2.0
Summary: Bridge service connecting Feishu (Lark) to dfcode REST API
Author-email: DFcode Team <bw4199579+dfcode@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/DFcode-Team-VaaT/dfcode
Project-URL: Repository, https://github.com/DFcode-Team-VaaT/dfcode
Keywords: dfcode,feishu,lark,bridge,remote
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: lark-oapi>=1.3.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: python-dotenv>=1.0.0

# DfCode Remote — Feishu Bridge

Python 服务，通过飞书 WebSocket 长连接接收用户消息，调用 dfcode 的 REST API 控制编码会话，通过 SSE 事件流获取实时更新，渲染为飞书卡片推送给用户。

## 概述

**核心优势**：dfcode 已有完整的 HTTP API + SSE 事件流 + 结构化消息数据，不需要像 remote_claude 那样用 PTY + pyte 解析终端输出。我们直接拿到 markdown、tool calls、permissions 等结构化数据。

| 维度     | remote_claude               | dfcode remote               |
| -------- | --------------------------- | --------------------------- |
| 数据源   | PTY + pyte 解析终端输出     | REST API + SSE 结构化数据   |
| 通信协议 | Unix Socket + mmap 共享内存 | HTTP + SSE                  |
| 输出解析 | ANSI 转义序列 → 组件        | MessageV2 Parts（已结构化） |
| 进程管理 | pty.fork() 启动 CLI         | dfcode serve 独立运行       |
| 卡片内容 | 终端文本 + font color       | Markdown + 工具调用摘要     |

## 架构图

```
飞书用户 ──→ 飞书服务器 ──→ WebSocket 长连接 ──→ packages/remote (Python)
                                                      │
                                                 ┌────┴────┐
                                                 │         │
                                            HTTP API    SSE Stream
                                            (控制)      (监听)
                                                 │         │
                                                 └────┬────┘
                                                      │
                                              dfcode serve :4096
                                              (headless 模式)
```

### 数据流

1. 用户在飞书发消息 → WebSocket 事件 → `handler.py` 路由
2. 命令消息 → 直接处理（/attach, /list, /kill 等）
3. 普通消息 → `dfcode_client` 调用 `POST /session/:id/prompt_async`
4. SSE 事件流 `GET /event` → `event_listener.py` 实时监听
5. `message.part.updated` 事件 → 增量更新飞书卡片
6. `permission.asked` 事件 → 渲染交互按钮卡片
7. `question.asked` 事件 → 渲染选项按钮卡片
8. `session.idle` 事件 → 标记完成状态

## 快速开始

### 前置条件

- Python >= 3.10
- 已创建的飞书应用（自定义应用）
- 运行中的 dfcode serve（默认端口 4096）

### 1. 安装

```bash
pip install dfcode-remote
```

从源码安装（开发模式）：

```bash
cd packages/remote
pip install -e .
```

### 2. 飞书应用配置

1. 登录[飞书开放平台](https://open.feishu.cn/)
2. 创建自定义应用，获取 **App ID** 和 **App Secret**
3. 在「权限管理」中添加以下权限：
   - `im:message` — 接收消息
   - `im:message:send_as_bot` — 以机器人身份发送消息
   - `im:chat` — 获取群组信息
   - `cardkit:card` — 使用 CardKit 发送/更新卡片
4. 在「事件订阅」中启用以下事件（均使用**长连接**模式）：
   - `im.message.receive_v1` — 接收消息事件
   - `card.action.trigger` — 卡片按钮点击回调（在「卡片回调」中单独配置）
5. 在「版本管理与发布」中发布应用
6. 将机器人添加到飞书群组

### 3. 配置环境变量

通过 `.env` 文件或直接设置环境变量：

```bash
export FEISHU_APP_ID=cli_xxxxxxxxxxxx
export FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export DFCODE_URL=http://localhost:4096
```

或复制模板：

```bash
cp .env.example .env
# 编辑 .env 填入你的配置
```

### 4. 启动

```bash
# 先启动 dfcode 服务（另一个终端）
dfcode serve
# 或开发模式：bun dev serve

# 启动飞书 bridge
dfcode-remote

# 指定 dfcode 地址
dfcode-remote --dfcode-url http://host:4096

# 启用调试日志
dfcode-remote --debug

# 也可以用 python -m 方式运行
python -m dfcode_remote
```

## 环境变量

| 变量名              | 说明                           | 默认值                  | 必填 |
| ------------------- | ------------------------------ | ----------------------- | ---- |
| `FEISHU_APP_ID`     | 飞书应用 App ID                | -                       | 是   |
| `FEISHU_APP_SECRET` | 飞书应用 App Secret            | -                       | 是   |
| `DFCODE_URL`        | dfcode serve 地址              | `http://localhost:4096` | 否   |
| `DFCODE_DIRECTORY`  | 默认工作目录                   | `.`                     | 否   |
| `DFCODE_USERNAME`   | dfcode 基础认证用户名          | -                       | 否   |
| `DFCODE_PASSWORD`   | dfcode 基础认证密码            | -                       | 否   |
| `ALLOWED_USERS`     | 允许的用户 ID 列表（逗号分隔） | -                       | 否   |
| `ENABLE_URGENT`     | 任务完成时发送加急通知         | `true`                  | 否   |

## 支持的命令

| 命令      | 用法                 | 说明                                 |
| --------- | -------------------- | ------------------------------------ | ------------------ |
| `/menu`   | `/menu`              | 显示命令帮助菜单                     |
| `/list`   | `/list`              | 列出跨项目“可继续对话”的活跃会话     |
| `/new`    | `/new [title]`       | 创建新会话并绑定到当前群             |
| `/attach` | `/attach <序号       | session_id>`                         | 绑定到已有活跃会话 |
| `/detach` | `/detach`            | 解绑当前会话                         |
| `/status` | `/status`            | 查看当前绑定会话的状态               |
| `/kill`   | `/kill [session_id]` | 终止会话（不指定则终止当前绑定会话） |

### 使用示例

```
/new 帮我写个爬虫
→ ✅ 已创建并绑定会话
  id: ses_xxx
  dir: /path/to/project

写个 Python 脚本抓取 example.com 的标题
→ [卡片流式更新中...]

/list
→ 1. 项目A
  id: ses_xxx
  dir: /Users/foo/project-a
  status: busy

/attach 1
→ ✅ 已绑定会话
  id: ses_xxx
  dir: /Users/foo/project-a

/status
→ id: ses_xxx
  title: 项目A
  dir: /Users/foo/project-a
  status: busy

/detach
→ ✅ 已解绑当前会话

/attach ses_xxx
→ ✅ 已绑定会话 ses_xxx

/kill
→ ✅ 已终止会话 abc12345
```

## 卡片交互

### 权限请求

当 dfcode 需要执行敏感操作（如写入文件）时，卡片会显示交互按钮：

```
🔐 write_file: /path/to/file
[允许] [始终允许] [拒绝]
```

- **允许**：仅本次允许该操作
- **始终允许**：永久允许此类操作
- **拒绝**：拒绝该操作

点击后按钮会变为灰色（已处理状态），dfcode 继续执行。

### 问题选择

当 dfcode 需要用户选择时，卡片会显示选项按钮：

```
❓ 请选择要使用的框架:
[React] [Vue] [Angular]
```

点击选项后，选择会发送给 dfcode。

### 中止按钮

当会话处于 busy 状态时，卡片右下角显示中止按钮：

```
⛔ 中止
```

点击后会调用 `POST /session/:id/abort` 终止当前任务。

## 事件流

`event_listener.py` 监听以下 SSE 事件并映射到卡片更新：

| SSE 事件类型                                         | 处理逻辑           | 卡片更新                      |
| ---------------------------------------------------- | ------------------ | ----------------------------- |
| `server.connected`                                   | 忽略               | -                             |
| `server.heartbeat`                                   | 忽略               | -                             |
| `message.part.updated` (type=text)                   | 追加/更新文本内容  | 内容区 markdown 更新          |
| `message.part.updated` (type=tool, status=running)   | 添加工具执行中面板 | 折叠面板（⏳ 图标）           |
| `message.part.updated` (type=tool, status=completed) | 更新工具完成状态   | 折叠面板（🔧 图标）+ 输出摘要 |
| `message.part.updated` (type=step-finish)            | 更新 token 用量    | 状态区显示 tokens/cost        |
| `message.part.updated` (type=reasoning)              | 添加推理内容       | 可折叠面板（💭 Reasoning）    |
| `permission.asked`                                   | 渲染权限按钮       | 交互区显示允许/拒绝按钮       |
| `question.asked`                                     | 渲染问题选项       | 交互区显示选项按钮            |
| `session.status` (idle)                              | 标记完成           | 状态变为 ✅ 完成，发送通知    |
| `session.error`                                      | 显示错误           | 发送错误文本消息              |

### 防抖机制

- 0.5 秒内多次事件合并为一次 `update_card` 调用
- 减少飞书 API 调用频率

## 卡片分裂

飞书卡片有内容块数量限制（`MAX_CARD_BLOCKS = 50`）。当内容块接近限制时：

1. 当前卡片被**冻结**（`build_frozen_card`）— 移除交互区，显示完成状态
2. 创建**新卡片**继续接收后续内容
3. 旧卡片保留历史内容，新卡片显示新内容

触发条件：

```python
if state.content_block_count() >= MAX_CARD_BLOCKS - 5:
    await self._freeze_and_split(state)
```

## 测试

```bash
# 运行所有测试
python -m unittest discover -s tests -v

# 运行特定测试文件
python -m unittest tests/test_card_builder.py -v
python -m unittest tests/test_sse.py -v
```

### 测试覆盖

- `test_card_builder.py` — 卡片构建、流式卡片、冻结卡片、菜单卡片
- `test_markdown.py` — Markdown 转换、工具输出格式化、截断逻辑
- `test_persistence.py` — 绑定持久化、原子写入
- `test_types.py` — 数据类解析（SessionInfo, PartInfo, SSEEvent 等）
- `test_sse.py` — SSE 流解析、帧解析、超时处理

## 验证流程

### 冒烟测试

1. `dfcode serve` 启动 dfcode（或 `bun dev serve`）
2. `dfcode-remote` 启动飞书 bridge
3. 飞书群发 `/new` → 创建会话
4. 发送 "帮我在当前目录创建一个 hello.py 文件"
5. 观察卡片流式更新 → 权限弹窗 → 点击允许 → 完成通知
6. `/list` 确认会话状态
7. `/kill` 终止会话

## 项目结构

```
packages/remote/
├── pyproject.toml              # pip 包配置
├── .env.example                # 环境变量模板
├── README.md                   # 本文档
├── main.py                     # 兼容入口（委托给 dfcode_remote.cli）
├── dfcode_remote/
│   ├── __init__.py             # 包版本
│   ├── __main__.py             # python -m dfcode_remote 入口
│   ├── cli.py                  # CLI 入口：解析参数、启动 bot + SSE
│   ├── dfcode_client/
│   │   ├── client.py           # httpx 异步客户端，封装 dfcode REST API
│   │   ├── sse.py              # SSE 事件流消费者
│   │   └── types.py            # 响应类型定义（dataclass）
│   ├── lark_client/
│   │   ├── bot.py              # 飞书 WebSocket 长连接启动
│   │   ├── handler.py          # 命令路由 + 消息处理
│   │   ├── card_builder.py     # dfcode 消息 → 飞书卡片 JSON
│   │   ├── card_service.py     # 飞书 CardKit API 封装
│   │   └── event_listener.py   # SSE → 卡片更新桥接
│   └── utils/
│       ├── config.py           # dotenv 配置加载
│       ├── markdown.py         # Markdown → 飞书卡片内容转换
│       └── persistence.py      # JSON 文件持久化（chat 绑定）
└── tests/                      # 单元测试
```

## 已知限制

1. **单会话绑定**：一个飞书群同一时间只能绑定一个 dfcode 会话（可通过 `/attach` 切换）
2. **无多群广播**：一个 dfcode 会话不能同时推送到多个飞书群
3. **无用户白名单 UI**：`ALLOWED_USERS` 需要手动配置用户 ID
4. **卡片块限制**：单卡片最多 50 个内容块，超出会触发卡片分裂
5. **图片不支持**：Markdown 中的图片会被替换为 `[alt text]` 占位符
6. **长文本截断**：单卡片内容超过 30000 字符会被截断

## 未来工作

- [ ] `/start <directory>` 命令 — 创建指定目录的会话
- [ ] `/new-group <name>` 命令 — 创建飞书群并绑定新会话
- [ ] 多会话同时绑定 — 一个群绑定多个会话，支持切换
- [ ] 会话完成时 @mention 群成员
- [ ] 更细粒度的权限控制（按工具类型）
- [ ] 图片上传支持（通过飞书图片 API）

## 许可证

MIT
