跳到主要內容

飛書群消息接入日報管線 — 設計文檔

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_id
  • group_name = 日報裡實際顯示的名字(已加 "飛書群" 後綴;按 FEISHU_GROUP_LABELS 或順序號補)
  • chat_name = 飛書原始群名(僅排查用,下游不消費)
  • 每條 message 的字段名完全沿用微信側:sender_wxidopen_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) -> str
    • class FeishuClient:封裝 GET/POST + token 注入 + 退避重試
    • iter_messages(chat_id, start_ts, end_ts) -> Iterator[dict]:處理分頁 page_token
    • get_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/internaltenant_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處理
textbody = content.text
post / post_v2_decode_post():遞歸遍歷 content 列表,文字片段拼接,@/a 元素保留顯示文本 + URL,圖片/at 標記成 [圖片] / @xxx。多段拼接保留 \n\n
share_chatchatIdget_chat_name(chatId)body = "[轉發鏈接] 群名 — https://..."
share_useruserId → 基本信息,body = "[轉發名片] xxx"
filebody = "[文件] " + 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_idname
  • FEISHU_GROUP_LABELS 裡有顯式映射,用映射作為輸出 key,群真實名當 chat_name 字段保留
  • 若沒有映射且 FEISHU_CHAT_IDS 有多個 → 按順序補 "飛書群 1 / 2 / 3 ..."

匿名化:抽取階段不做generate_report.pysanitize_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,調用方維持原有"找不到當日數據"報錯
  • 任一存在 → 取第一份的頂層元數據(datetzwindow_*),把所有份的 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_wxidopen_idformat_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 的 .env
  • generate_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:

  1. 飛書自建應用 + _feishu.py + extract_day.py + 測試(不接 generate_report)
  2. generate_report.py 改 4 點 + 端到端在某天數據上人工驗證
  3. README / CLAUDE.md / 運營 SOP(拉機器人入群、首跑校驗、日常監控)

實現細節由後續 plan 文檔展開。