# pi-channels-streaming-card

Pi 消息通道插件（飞书/Lark、DingTalk、企微、webhook），内置**原生流式卡片**能力，并针对飞书深度增强：**图片自动识别**、**重启自愈**、**答案兜底**。

> Fork 自 [@amaster.ai/pi-channels](https://github.com/TGYD-helige/pi)（Apache-2.0），新增飞书流式卡片 + 视觉识别 + 自愈能力。

## ✨ 特性

- **飞书打字机流式输出**：基于飞书 CardKit `cardElement.content` 接口，文字逐字蹦出（动画跟随生成速度，不拖慢输出）
- **思考与工具折叠面板**：思考过程、工具调用收进原生 `collapsible_panel`（默认收起，点击展开）；上一轮答案归档为「过程 N」同样收纳
- **上下文底栏**：`已完成 · 耗时 · 模型 · ↑输入 · ↓输出 · ctx 用量/窗口 百分比`
- **🖼 飞书图片自动识别（无需切换模型）**：
  - 图片消息到达后自动通过 `im.messageResource.get` 拉取字节，**纯内存处理（零磁盘写入）**
  - 自动调用视觉模型（qwen-vl-plus 等，从 `models.json` 读取）生成描述，前置注入主模型提示词
  - 主模型保持 deepseek-v4-flash 等文本模型即可「看懂」图片
- **🛟 重启自愈**：
  - 流式卡片状态持久化到 `card-state.json`（创建即存、流式中节流保存、完成即删）
  - 启动时自动把上次中断的卡片标记为 `⚠️ 已中断（重启）`，不再永久卡在「思考中」
  - 孤儿集合只在启动时捕获一次，**重试窗口绝不误伤新创建的流式卡片**
- **📨 答案兜底**：若卡片中途死亡（更新全部失败），自动转纯文本消息补发，答案永不丢失
- **🖼 图片消息卡片独立发送**：回复图片消息时卡片以新消息发送，规避飞书引用缩略图的兼容问题
- 其他通道（DingTalk/企微/webhook）保持原 pi-channels 行为不变

## 📦 安装

先卸载原版 pi-channels（避免冲突），再安装本插件：

```bash
pi remove npm:@amaster.ai/pi-channels
pi install npm:pi-channels-streaming-card     # 发布到 npm 后
# 或从 GitHub 安装：
pi install git:github.com/<你的用户名>/pi-channels-streaming-card
```

重启 pi 生效：

```bash
# 交互模式下 /reload，或重启进程
```

## ⚙️ 配置

配置方式和 pi-channels 完全一致（`~/.pi/agent/settings.json`）：

```json
{
  "pi-channels": {
    "adapters": {
      "feishu": {
        "type": "feishu",
        "appId": "cli_xxx",
        "appSecret": "xxx",
        "eventMode": "websocket",
        "respondToMentionsOnly": true
      }
    },
    "bridge": {
      "enabled": true,
      "provider": "opencode-go",
      "model": "deepseek-v4-flash",
      "streamingCards": true
    }
  }
}
```

`bridge.streamingCards: true` 开启飞书流式卡片（CardKit 打字机）。

### 🖼 图片识别配置（可选）

在 `~/.pi/agent/models.json` 配置任意 OpenAI 兼容的视觉模型即可（插件自动探测含 `vl/vision/image` 的模型 id）：

```json
{
  "providers": {
    "qwen-vl": {
      "baseUrl": "https://<你的百炼网关>/compatible-mode/v1",
      "apiKey": "sk-xxx",
      "api": "openai-completions",
      "models": [{ "id": "qwen-vl-plus", "name": "通义千问 VL Plus（识图）" }]
    }
  }
}
```

未配置视觉模型时，图片消息降级为纯文本 `[图片]`，不影响其他功能。

## 🎴 卡片效果

```
┌─────────────────────────────────┐
│ π pi                    ✅ 已完成 │  ← 头部状态（蓝=进行中/绿=完成/红=失败/橙=中断）
│                                  │
│  答案内容（打字机逐字输出）        │  ← 主内容（仅最终答案）
│                                  │
│  ▸ 思考与工具 · 6 次工具调用      │  ← 原生折叠面板（默认收起）
│    - 思考 1 · completed          │
│    - ✓ bash                     │
│    - ✕ edit · 失败              │
│─────────────────────────────────│
│ 已完成 · 28s · deepseek-v4-flash │  ← 底栏
│ · ↑129k · ↓379 · ctx 129k/1m 13%│
└─────────────────────────────────┘
```

## 🛠 实现说明

- 创建卡片实体：`POST /cardkit/v1/cards`（`streaming_mode: true`）
- 发送卡片：`im.message.create` + `content: {type:"card", data:{card_id}}`（图片消息改为独立新消息，不 reply）
- 流式文本：`PUT /cardkit/v1/cards/{id}/elements/{element_id}/content`（打字机动效，`print_strategy: "fast"` 保证不落后于生成速度）
- 结构更新：`PUT /cardkit/v1/cards/{id}` 全量更新（头部状态、折叠面板、底栏）
- 完成时：`card.settings` 关闭 `streaming_mode`（移除打字光标）
- CardKit 失败时自动回退到 `im.message.patch` 全卡更新（功能不中断）
- 图片识别：`im.messageResource.get`（`params: {type:'image'}`）→ 内存字节 → base64 data URL → OpenAI 兼容 `/chat/completions` 视觉请求
- 自愈：卡片快照持久化 → 启动时 `healOrphanedCards`（仅处理启动时存在的孤儿，3s 延迟 + 指数退避重试，序列号大跳步规避 sequence 冲突）

## 📄 许可

Apache-2.0（与上游 pi-channels 一致）
