飛書群消息接入日報管線 — 設計文檔
0. 背景
當前社區日報管線 (bot/wechat-bot/) 只採集微信群消息:lldb 解 SQLCipher → extract_day.py 抽消息 → generate_report.py 評分 → wechat-summary-bot 出海報。
社區已開通 1 個飛書群,希望把飛書群作為第二個信息源匯入同一份日報,最終產出仍是單份 <date>.detailed.md、單張海報。
1. 目標
- 飛書群消息每日批量接入,與微信側並列
- 零改動複用現有 prompt / 評分 / 去重 / 匿名化 / 週末補跑邏輯
generate_report.py改動控制在 ~30 行以內- 飛書側失敗不影響微信側,反之亦然
- 顯示階段區分來源:飛書群標 "飛書群",微信群標 "微信群"
2. 非目標 (YAGNI)
- 實時 webhook / 事件訂閱
- 給飛書群回貼日報
- 飛書 image OCR
- 飛書機器人交互式命令
- 多飛書 tenant
- 重寫微信側以抽公共庫
3. 整體架構
┌─────────────────────┐
│ Feishu Open API │
│ (im/v1/messages) │
└──────────┬──────────┘
│ 每日定時
Mac 微信 (4.1.2) │ Asia/Shanghai
│ lldb attach │ 0:00–24:00
▼ ▼
(1)(2) 解 db (vendor/wechat-db-decrypt-macos) ┌─────────────────────────┐
│ │ bot/feishu-bot/scripts/ │
▼ │ extract_day.py │
(3) bot/wechat-bot/scripts/extract_day.py └──────────┬──────────────┘
→ bot/wechat-bot/data/daily/<date>.json │
▼
bot/feishu-bot/data/daily/<date>.feishu.json
│ │
└──────────────────────┬──────────────────────────┘
▼
(4) bot/wechat-bot/scripts/generate_report.py (改 ~30 行)
- 讀 <date>.json + <date>.feishu.json
- 合併為 (group_label → messages[]) 字典
- 每群獨立調 LLM(與現一致)
- 渲染時按 platform 字段加群類型前綴
▼
bot/wechat-bot/data/reports/<date>.detailed.md (單一最終產物)
bot/wechat-bot/data/reports/<date>.json
▼
wechat-summary-bot 出海報,發佈到站點 /daily
性質:
- 飛書與微信兩支獨立、可重跑
- 沒有"實時",每日批量
- prompt / 評分 / 去重 / 匿名化全部複用
- 來源差異只體現在顯示標籤
4. 飛書自建應用 & 權限
應用形態:僅團隊內可見的自建應用,啟用機器人能力,不上架。
權限範圍(最小集合):
| 權限 scope | 用途 |
|---|---|
im:message.group_msg | 讀群消息(iter_messages 調 /im/v1/messages?container_id_type=chat) |
im:chat:readonly | 列機器人在的群 + 拿群名(inventory.py / get_chat_name) |
不申請:發送消息、文件上傳、企業通訊錄、外部聯繫人、群成員列表。
注:scope 名以飛書 API 報錯時實際給出的為準。早期草稿曾列:
im:message:readonly(實際拉群消息時飛書要求im:message.group_msg,參見 飛書錯誤碼 230027)im:chat.member:read(飛書後臺無此名)contact:user.id:readonly(聯繫人接口未被使用)
share_user類型當前用 open_id 作為佔位、不解析真名,所以不需要聯繫人權限。
回調訂閱:本期不開。
機器人入群:手動 @ 拉一次。
身份信息(bot/feishu-bot/.env):
FEISHU_APP_ID=cli_xxxxxxxx
FEISHU_APP_SECRET=xxxxxxxxxxxx
FEISHU_CHAT_IDS=oc_xxxxxxxxxxxx # 當前 1 個,逗號分隔可擴展
FEISHU_GROUP_LABELS=oc_xxx=Hermes Agent 中文社區飛書群 1;oc_yyy=Hermes Agent 中文社區飛書群 2
FEISHU_GROUP_LABELS 可選:把每個 chat_id 顯式映射到日報裡出現的群名前綴。多條目用 ; 分隔,單條 chat_id=label 用 = 分隔。缺省時按 "Hermes Agent 中文社區飛書群" + FEISHU_CHAT_IDS 中的順序號補。
5. 抽取腳本 bot/feishu-bot/scripts/extract_day.py
輸入:日期(默認前一天,Asia/Shanghai 0:00 → 次日 0:00)。
輸出:bot/feishu-bot/data/daily/<date>.feishu.json。
5.1 輸出 schema
與微信側 bot/wechat-bot/data/daily/<date>.json 嚴格對齊,方便 generate_report.py 直接拼接 groups 列表。
{
"date": "2026-04-30",
"tz": "Asia/Shanghai",
"platform": "feishu",
"window_start": 1714492800,
"window_end": 1714579200,
"groups": [
{
"group_id": "oc_xxx",
"group_name": "Hermes Agent 中文社區飛書群 1",
"platform": "feishu",
"chat_name": "Hermes 中文社區",
"message_count": 87,
"messages": [
{ "ts": 1714492800, "time": "09:01:27", "sender_wxid": "ou_aaa", "sender_name": "", "type": "text", "text": "..." },
{ "ts": 1714492810, "time": "09:01:30", "sender_wxid": "ou_bbb", "sender_name": "", "type": "post", "text": "標題\n\n正文段一\n[查看鏈接](https://...)" },
{ "ts": 1714492820, "time": "09:02:00", "sender_wxid": "ou_ccc", "sender_name": "", "type": "share", "text": "[轉發鏈接] 文章標題 — https://..." },
{ "ts": 1714492830, "time": "09:02:10", "sender_wxid": "ou_ddd", "sender_name": "", "type": "file", "text": "[文件] xxx.pdf" }
]
}
]
}
字段說明:
groups是列表(與微信側一致),不是字典group_id= 飛書chat_idgroup_name= 日報裡實際顯示的名字(已加 "飛書群" 後綴;按FEISHU_GROUP_LABELS或順序號補)chat_name= 飛書原始群名(僅排查用,下游不消費)- 每條 message 的字段名完全沿用微信側:
sender_wxid存open_id(明知命名不準,為對齊成本忍受);sender_name留空字符串(open_id 已是不可讀,下游format_transcript會回落到 wxid) type是抽取期記錄的消息類型(text/post/share/file);generate_report.py不消費此字段,僅供調試
message_count 是被保留消息數(抽取後),不是源數。
5.2 實現要點
模塊拆分:
bot/feishu-bot/scripts/_feishu.py— 唯一入口,包含:get_tenant_token(app_id, app_secret) -> strclass FeishuClient:封裝 GET/POST + token 注入 + 退避重試iter_messages(chat_id, start_ts, end_ts) -> Iterator[dict]:處理分頁page_tokenget_chat_name(chat_id) -> str:帶本次運行內 LRU 緩存decode_message(raw) -> dict | None:消息類型分發
extract_day.py:CLI 層,調度_feishu.py、寫文件inventory.py:排查用,列機器人在的群、最近 7 日消息量
認證:
POST /open-apis/auth/v3/tenant_access_token/internal拿tenant_access_token- TTL ~2h,本期每次腳本啟動時拿一次,不寫盤緩存(避免憑證洩露與刷新邏輯複雜化)
- 檢測
code == 99991663或 401 → 刷新後重試一次
取消息:
GET /open-apis/im/v1/messages?container_id_type=chat&container_id=...&start_time=...&end_time=...&page_size=50- 分頁跟
page_token直到has_more=false - 時間戳:飛書 API 的
start_time/end_time是秒級 Unix 時間戳的字符串
消息類型分發 (decode_message):
| msg_type | 處理 |
|---|---|
text | body = content.text |
post / post_v2 | _decode_post():遞歸遍歷 content 列表,文字片段拼接,@/a 元素保留顯示文本 + URL,圖片/at 標記成 [圖片] / @xxx。多段拼接保留 \n\n |
share_chat | chatId → get_chat_name(chatId),body = "[轉發鏈接] 群名 — https://..." |
share_user | userId → 基本信息,body = "[轉發名片] xxx" |
file | body = "[文件] " + content.file_name,不下載 |
image / audio / video / sticker / red_packet / system 等 | 返回 None,調用方丟棄 |
| 未知 type | 返回 None,記 warn |
_decode_post 處理 content 是 JSON 字符串嵌套數組的情況,解析失敗時 fallback 成 [富文本無法解析] 佔位。
群名解析:
- 默認
GET /open-apis/im/v1/chats/:chat_id拿name - 若
FEISHU_GROUP_LABELS裡有顯式映射,用映射作為輸出 key,群真實名當chat_name字段保留 - 若沒有映射且
FEISHU_CHAT_IDS有多個 → 按順序補 "飛書群 1 / 2 / 3 ..."
匿名化:抽取階段不做。generate_report.py 的 sanitize_highlights() 已對人名/wxid 做清洗,open_id 是隨機字符串不會被 LLM 當成人名輸出。<date>.feishu.json 裡保留 open_id 僅供調試 / 復算。
CLI:與微信側 extract_day.py 對齊:
python3 bot/feishu-bot/scripts/extract_day.py # 默認昨日
python3 bot/feishu-bot/scripts/extract_day.py 2026-04-30 # 顯式日期
python3 bot/feishu-bot/scripts/extract_day.py --all # 自機器人入群以來每天
python3 bot/feishu-bot/scripts/extract_day.py --dry-run # 不寫盤
python3 bot/feishu-bot/scripts/extract_day.py --no-overwrite # 已存在則報錯退出
依賴:僅 requests + python-dotenv,不引入飛書官方 SDK。
週末補跑:與微信側 bot/wechat-bot/scripts/extract_day.py 對齊——週末不出日報。extract_day.py 內部實現"未傳日期 + 當日是週一 → 自動循環抽取週六、週日、週一",行為與微信側完全一致。手動傳日期時不補跑。
6. generate_report.py 改造點
唯一需要改既有代碼的地方。改動侷限兩點:
6.1 數據加載(_run_single_day 頭部)
當前 _run_single_day 直接讀 DAILY_DIR / f"{date_str}.json"。改為新建一個 _load_daily(date_str) 輔助函數:
- 讀
bot/wechat-bot/data/daily/<date>.json(微信) - 讀
bot/feishu-bot/data/daily/<date>.feishu.json(飛書)—— 路徑解析用ROOT.parent / "feishu-bot" / "data" / "daily" - 兩份都不存在 → 返回
None,調用方維持原有"找不到當日數據"報錯 - 任一存在 → 取第一份的頂層元數據(
date、tz、window_*),把所有份的groups列表串接成一份
由於飛書 schema 已與微信對齊,串接後的 groups 列表可直接進入 for g in data["groups"] if g["message_count"] >= MIN_MESSAGES_PER_GROUP 流程,無需再做規範化。
6.2 來源標籤 _display_source()
完全不用改。
- 微信側
group_name是"Hermes Agent 中文社區 N",正則(Hermes Agent 中文社區)\s*(\d+)命中 →"...微信群 N" - 飛書側
group_name已經是"Hermes Agent 中文社區飛書群 N","社區" 與數字之間隔著 "飛書群",正則不命中 → 原樣輸出
→ 兩類來源能自然區分,不引入新分支。
6.3 LLM 輸入預處理
保持現狀——飛書消息的 sender_wxid 存 open_id,format_transcript 已有 m["sender_name"] or m["sender_wxid"] or "?" 的回落,會顯示成 [time] ou_xxxx: text,LLM 不會把 open_id 當人名。
share / file 類型的 text 字段已帶 [轉發鏈接] / [文件] 前綴,LLM 看得懂。
6.4 去重不用改
dedupe_highlights() key 是 (topic.lower(), first-24-normalized-chars(summary)),與來源無關。飛書與微信群討論同一話題會被自然合併成一條 highlight,source 字段拼成 "微信群 3 / 飛書群 1"。prune_report.py 的 dedupe key 與此一致,不動。
總改動行數估計:≤ 25 行(僅在 _run_single_day 頭部抽出 _load_daily() 函數 + 調整一行調用)。
7. 目錄結構
bot/feishu-bot/
├── README.md # 用法 + 飛書自建應用配置步驟
├── CLAUDE.md # 給 AI 的快速 context
├── .env.example # FEISHU_APP_ID / SECRET / CHAT_IDS / GROUP_LABELS
├── .gitignore # data/, .env
├── scripts/
│ ├── _feishu.py # 客戶端、token、消息解碼(single source of truth)
│ ├── extract_day.py # CLI,與微信側對齊
│ └── inventory.py # 列機器人在的群、最近 7 日消息量
├── data/
│ └── daily/<date>.feishu.json # 抽取產物,gitignored
└── tests/
├── fixtures/ # 離線 JSON:post_*.json、share_*.json、file_*.json
└── test_decoders.py # 單測
8. 配置 & 憑證
bot/feishu-bot/.env— 飛書 secrets,不串到 wechat-bot 的.envgenerate_report.py在合併飛書數據時不需要任何 Feishu 憑證——它只讀已經落盤的 JSON.gitignore必須包含bot/feishu-bot/data/、bot/feishu-bot/.env
9. 錯誤處理 & 冪等性
| 場景 | 行為 |
|---|---|
| 飛書 API 5xx / 網絡抖動 | 指數退避 3 次;仍失敗則整次抽取失敗、不寫半成品(與微信側"上游失敗不汙染下游"一致) |
tenant_access_token 過期 | 檢測 99991663 / 401,刷新後重試 1 次 |
| 限流(429) | 退避後重試 |
<date>.feishu.json 已存在 | 默認覆蓋;--no-overwrite 時退出 |
generate_report.py 僅看到微信,沒飛書 | 正常出報告,不報錯 |
generate_report.py 僅看到飛書,沒微信 | 同上,不報錯 |
| 飛書群機器人被踢 | API 403,extract_day 報錯並 exit code 非 0;不靜默 |
share_chat 引用的鏈接已刪除 | 解析降級為"[已失效轉發]"佔位 |
decode_message 遇到未知 type | 返回 None 丟棄,stderr warn 一行 |
10. 測試
bot/wechat-bot/ 當前無測試。本次破例引入輕量單測,因為消息類型解碼是飛書側最易錯點:
tests/test_decoders.py— 用離線 JSON fixture 測_decode_post()/_decode_share()/_decode_file()- 不測端到端(要打活的飛書 API,集成進 CI 不划算)
- 跑法:
/usr/bin/python3 -m unittest discover bot/feishu-bot/tests,無外部依賴
11. 風險與權衡
- 機器人拉群 = 信任授權:飛書自建應用擁有
im:message:readonly等於讀群所有消息。社區成員需要被告知。 - 飛書 API 限流:默認 50 QPS / app,1 個群每天幾百條遠低於上限。可不做特殊處理。
- 群名變更:若運營改了飛書群名,下次抽取的 group key 會變,造成
dedupe_highlights跨日不一致。緩解:用FEISHU_GROUP_LABELS顯式映射兜底。 tenant_access_token不緩存:每次腳本啟動都拿一次,浪費一次 RTT,但避免憑證落盤。當前一天跑一次,可接受。- 微信側 schema 漂移:本設計假定
bot/wechat-bot/data/daily/<date>.json的 group → messages 形態穩定。若未來微信側引入platform字段,要保證向後兼容。
12. 實施順序
預期分 3 個 PR:
- 飛書自建應用 +
_feishu.py+extract_day.py+ 測試(不接 generate_report) generate_report.py改 4 點 + 端到端在某天數據上人工驗證- README / CLAUDE.md / 運營 SOP(拉機器人入群、首跑校驗、日常監控)
實現細節由後續 plan 文檔展開。