# 飞书交互式卡片推送

从 `latest.md` 生成并推送飞书交互式卡片给指定用户。

## 凭据

App ID: `cli_aaee6a2f90b89bc1`（宝锐工作台机器人）
App Secret: `arht5fd8wyppZysFvOjUcb2bV0UMVVYF`
位置：`~/.hermes/scripts/send_daily_brief.py`（硬编码）

## 完整流程

```
latest.md → 解析段落 → 构建卡片JSON → 获取tenant_access_token → POST im/v1/messages
```

### 1. 获取 Token

```python
POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal
Body: {"app_id": "cli_aaee6a2f90b89bc1", "app_secret": "..."}
→ tenant_access_token (2h有效期)
```

### 2. 构建卡片

使用 `lark_md` 格式（支持 markdown 语法）：

```python
card = {
    "config": {"wide_screen_mode": True},
    "header": {
        "title": {"tag": "plain_text", "content": "📋 销售日报"},  # 标题固定，不带日期
        "template": "blue"
    },
    "elements": [
        {"tag": "div", "text": {"tag": "lark_md", "content": "**粗体**\n正文"}},
        {"tag": "hr"},
        ...
    ]
}
```

### 3. 发送消息

```python
POST https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id
Headers: Authorization: Bearer {token}
Body: {
    "receive_id": "ou_901b790c9afdda4a8f14554962b32664",
    "msg_type": "interactive",
    "content": json.dumps(card)
}
```

## 已知接收人 open_id

| 姓名 | open_id | 备注 |
|------|---------|------|
| 刘新元 | `ou_901b790c9afdda4a8f14554962b32664` | 主接收人 |
| 赵云浩 | `ou_7fab49a17fecc17934456d76a45d0ca0` | |

## 卡片大小限制

飞书交互式卡片单卡片上限约 30KB。latest.md 内容较多时（含竞品动态、重点项目、客诉列表）需要截断：

- **今日关注**：全量展示（通常 ≤12 条）
- **未关闭客诉**：截断至 10 条 + "还有N条"
- **活跃客户**：截断至 8 家
- **竞品动态**：截断至 8 条
- **重点项目**：截断至 15 条
- **链接**：使用 `[文本](URL)` markdown 链接，飞书卡片自动渲染为可点击链接

## 脚本模板

**主脚本（2026-09-11 起）**：`~/.hermes/scripts/push_daily_brief_card.py` — 动态解析 latest.md → 建卡 → 推送，标题固定「📋 销售日报」。直接 `python3 ~/.hermes/scripts/push_daily_brief_card.py`。旧版 `send_daily_brief.py`/`send_daily_brief_card.py` 为硬编码/旧解析，勿再用；`/tmp/send_daily_card.py` 已废弃（临时文件，且工作台链接已失效）。

**工作台链接域（2026-09-11 实测）**：
- ✅ `https://bier-sales-d0gatbvlx288724e9-1458438710.tcloudbaseapp.com/workbench/index.html`（200）
- ❌ `bier-sales-6gilsvtp0e5cc973-1371818483.tcloudbaseapp.com`（418，已失效——旧脚本里的死链，勿照抄）

### 统计行正则须容忍空格
latest.md 第 2 行形如 `昨日活动 107 条 ｜ 未关闭客诉 210 条 ｜ 近7天 162 单`，数值与「客诉」之间**有空格**，`客诉(\d+)` 匹配不到。用：`活动\s*(\d+)\s*条` / `客诉\s*(\d+)\s*条` / `近7天\s*(\d+)\s*单` / `¥\s*([\d.]+)\s*万`。

### 解析 latest.md 段落

段落以 emoji 前缀行开头（`⚠️`/`👥`/`🔴`/`📊`/`⚔️`/`🚀`/`💰`/`📅`/`🟢`），按前缀分组：

```python
current_section = None
for line in lines:
    if line.startswith('⚠️'): current_section = 'focus'
    elif line.startswith('👥'): current_section = 'people'
    # ...
```

⚠️ **当前格式已变（2026-08-17 起）**：统计行与活跃客户小节用「近7天」而非「本周」（滚动7天窗口）。小节标题行以 emoji 前缀开头、**不一定带「：」**（如 `⚠️ 今日关注`、`🔴 未关闭客诉（…）`），内容行以 `·` 开头。解析时按「行首是否 emoji 前缀」判定标题，遇 `---` 页脚分隔符即停止，`🤖` 时间戳行跳过。统计字段：`客诉N`、`活动N条`、`近7天N单`、`近7天¥N万`。

### 竞品动态中的飞书链接

latest.md 中竞品动态/重点项目行格式：
`· 诺唯赞 | 客户名 | 描述 | 销售员 [链接](https://project.feishu.cn/...)`

飞书卡片中 `lark_md` 会自动渲染 `[链接](URL)` 为可点击链接，无需特殊处理。

## 🔴 🚀 重点项目子标题截断陷阱（2026-09-11）

最新格式里「🚀 重点项目」不是独立小节，其下有两段子标题：

```
🚀 重点项目
🟢 积极进展（59条）：      ← 行首直接是 emoji，不带「· 」
· 客户｜要点｜销售员 [飞书活动](url)
…（12 条）
🔴 风险/受阻（8条）：       ← 同样不带「· 」，且不再出现 🚀
· …（8 条）
```

**若把整段当一个 section 按 cap=15 截断，风险/受阻 8 条会被整体吃掉**（它排在 12 条积极进展之后，位于第 13-21 行）——而风险信号恰恰是管理层最需要看的。

**正确做法**：解析后先按子标题二次拆分，再各自截断：

```python
SUB_RE = re.compile(r"^[·\s]*[🟢🔴]\s*(积极进展|风险|受阻)")  # 允许可选「· 」前缀
# 拆成 '🚀 重点项目 ｜ 积极进展（59条）'(cap 10) 与 '… ｜ 风险/受阻（8条）'(cap 8)
TRUNCATE = [("未关闭客诉", 12), ("竞品动态", 8), ("积极进展", 10),
            ("风险", 8), ("受阻", 8), ("活跃客户", 8), ("今日关注", 14), ("回款", 10)]
```

⚠️ 正则若写成 `^·\s*[🟢🔴]` 会漏配——子标题行**没有** `· ` 前缀。

### 🔴 父标题被吞 + 子标题串味（2026-09-17 修复）

`🚀 重点项目` 后面**紧跟** `🟢 积极进展（44条）：`（父标题自己没有内容行）时有两个连带 bug：

1. 父标题 `cont` 为空 → 被 `if not cont: continue` 丢掉 → 卡片里再也看不到「重点项目」字样（旧文档说"改文案无效"，其实是解析顺序问题）。
2. 旧代码 `sub_title = title + " ｜ " + ln` 把**已经和标题相同的字符串又拼一遍**，于是风险段标题渲染成 `🟢 积极进展（44条）： ｜ 🔴 风险/受阻（15条）` —— 用绿色「积极进展」开头盖住红色风险块，且让 cap 查表先命中 `积极进展`(10) 而非 `风险`(8)。

**修复**（`push_daily_brief_card.py`）：① 空父标题先存 `_pending_parent`，并入下一个子标题小节 → `🚀 重点项目 ｜ 🟢 积极进展（44条）`；② `base = title.split(" ｜ ")[0]`，`base` 本身匹配 `SUB_RE` 时置空，避免自我拼接。

**改动脚本必须先用 `--dry-run` 验证小节标题**（2026-09-17 新增开关，只解析打印小节标题、不发飞书）：
```bash
python3 ~/.hermes/scripts/push_daily_brief_card.py --dry-run
# 期望输出含：🚀 重点项目 ｜ 🟢 积极进展（44条） / 🚀 重点项目 ｜ 🔴 风险/受阻（15条）
```
改前先备份 `cp push_daily_brief_card.py{,.bak_$(date +%Y%m%d)}`。同一 bug 会同时存在于桌面副本 `脚本/send_daily_brief_card.py`（逻辑同源）。

## 已发卡片撤回重发

判错内容/截断错误后，同一 message_id 可直接撤回再重发，不必让用户看到两张卡：

```python
req = urllib.request.Request(
    f"https://open.feishu.cn/open-apis/im/v1/messages/{message_id}",
    method="DELETE", headers={"Authorization": f"Bearer {token}"})
# → {"code":0,"data":{},"msg":"success"}
```

## 推送后回读验证

发送返回 `code:0` + `message_id` 即算成功，但声明性内容要回读确认：`GET /open-apis/im/v1/messages/{message_id}` → `data.items[0].body.content`（JSON 字符串，含 `title` + `elements`），检查标题为「📋 销售日报」、关键小节（风险/受阻、未关闭客诉、竞品动态等）都在。

## 常见问题

**消息发送成功但卡片显示异常**：检查 `content` 字段是否为合法的 JSON 字符串（需 `json.dumps(card)` 后再放入外层 body）。

**Token 过期**：每次发送前重新获取，token 有效期 2h。
